From fc1fc264ec02b326560291132e2e782b081f3c62 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:24:43 -0400 Subject: [PATCH] debugger: Generate inline values based on debugger.scm file (#33081) ## Context To support inline values a language will have to implement their own provider trait that walks through tree sitter nodes. This is overly complicated, hard to accurately implement for each language, and lacks proper extension support. This PR switches to a singular inline provider that uses a language's `debugger.scm` query field to capture variables and scopes. The inline provider is able to use this information to generate inlays that take scope into account and work with any language that defines a debugger query file. ### Todos - [x] Implement a utility test function to easily test inline values - [x] Generate inline values based on captures - [x] Reimplement Python, Rust, and Go support - [x] Take scope into account when iterating through variable captures - [x] Add tests for Go inline values - [x] Remove old inline provider code and trait implementations Release Notes: - debugger: Generate inline values based on a language debugger.scm file --- Cargo.lock | 1 + crates/dap/src/inline_value.rs | 640 ------------------ crates/dap/src/registry.rs | 26 +- crates/dap_adapters/src/dap_adapters.rs | 6 - crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/tests/inline_values.rs | 468 +++++++++++-- crates/editor/src/editor.rs | 3 +- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 84 ++- crates/language/src/language.rs | 50 ++ crates/language/src/language_registry.rs | 4 +- crates/languages/src/go/debugger.scm | 26 + crates/languages/src/python/debugger.scm | 43 ++ crates/languages/src/rust/debugger.scm | 50 ++ crates/project/src/debugger/dap_store.rs | 27 +- crates/project/src/debugger/session.rs | 11 +- crates/project/src/project.rs | 96 ++- 17 files changed, 786 insertions(+), 751 deletions(-) create mode 100644 crates/languages/src/go/debugger.scm create mode 100644 crates/languages/src/python/debugger.scm create mode 100644 crates/languages/src/rust/debugger.scm diff --git a/Cargo.lock b/Cargo.lock index 70a05cf4aa..0c832b83aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4348,6 +4348,7 @@ dependencies = [ "terminal_view", "theme", "tree-sitter", + "tree-sitter-go", "tree-sitter-json", "ui", "unindent", diff --git a/crates/dap/src/inline_value.rs b/crates/dap/src/inline_value.rs index 881797e20f..47d7833085 100644 --- a/crates/dap/src/inline_value.rs +++ b/crates/dap/src/inline_value.rs @@ -1,5 +1,3 @@ -use std::collections::{HashMap, HashSet}; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum VariableLookupKind { Variable, @@ -20,641 +18,3 @@ pub struct InlineValueLocation { pub row: usize, pub column: usize, } - -/// A trait for providing inline values for debugging purposes. -/// -/// Implementors of this trait are responsible for analyzing a given node in the -/// source code and extracting variable information, including their names, -/// scopes, and positions. This information is used to display inline values -/// during debugging sessions. Implementors must also handle variable scoping -/// themselves by traversing the syntax tree upwards to determine whether a -/// variable is local or global. -pub trait InlineValueProvider: 'static + Send + Sync { - /// Provides a list of inline value locations based on the given node and source code. - /// - /// # Parameters - /// - `node`: The root node of the active debug line. Implementors should traverse - /// upwards from this node to gather variable information and determine their scope. - /// - `source`: The source code as a string slice, used to extract variable names. - /// - `max_row`: The maximum row to consider when collecting variables. Variables - /// declared beyond this row should be ignored. - /// - /// # Returns - /// A vector of `InlineValueLocation` instances, each representing a variable's - /// name, scope, and the position of the inline value should be shown. - fn provide( - &self, - node: language::Node, - source: &str, - max_row: usize, - ) -> Vec; -} - -pub struct RustInlineValueProvider; - -impl InlineValueProvider for RustInlineValueProvider { - fn provide( - &self, - mut node: language::Node, - source: &str, - max_row: usize, - ) -> Vec { - let mut variables = Vec::new(); - let mut variable_names = HashSet::new(); - let mut scope = VariableScope::Local; - - loop { - let mut variable_names_in_scope = HashMap::new(); - for child in node.named_children(&mut node.walk()) { - if child.start_position().row >= max_row { - break; - } - - if scope == VariableScope::Local && child.kind() == "let_declaration" { - if let Some(identifier) = child.child_by_field_name("pattern") { - let variable_name = source[identifier.byte_range()].to_string(); - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = variable_names_in_scope.get(&variable_name) { - variables.remove(*index); - } - - variable_names_in_scope.insert(variable_name.clone(), variables.len()); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: identifier.end_position().row, - column: identifier.end_position().column, - }); - } - } else if child.kind() == "static_item" { - if let Some(name) = child.child_by_field_name("name") { - let variable_name = source[name.byte_range()].to_string(); - variables.push(InlineValueLocation { - variable_name, - scope: scope.clone(), - lookup: VariableLookupKind::Expression, - row: name.end_position().row, - column: name.end_position().column, - }); - } - } - } - - variable_names.extend(variable_names_in_scope.keys().cloned()); - - if matches!(node.kind(), "function_item" | "closure_expression") { - scope = VariableScope::Global; - } - - if let Some(parent) = node.parent() { - node = parent; - } else { - break; - } - } - - variables - } -} - -pub struct PythonInlineValueProvider; - -impl InlineValueProvider for PythonInlineValueProvider { - fn provide( - &self, - mut node: language::Node, - source: &str, - max_row: usize, - ) -> Vec { - let mut variables = Vec::new(); - let mut variable_names = HashSet::new(); - let mut scope = VariableScope::Local; - - loop { - let mut variable_names_in_scope = HashMap::new(); - for child in node.named_children(&mut node.walk()) { - if child.start_position().row >= max_row { - break; - } - - if scope == VariableScope::Local { - match child.kind() { - "expression_statement" => { - if let Some(expr) = child.child(0) { - if expr.kind() == "assignment" { - if let Some(param) = expr.child(0) { - let param_identifier = if param.kind() == "identifier" { - Some(param) - } else if param.kind() == "typed_parameter" { - param.child(0) - } else { - None - }; - - if let Some(identifier) = param_identifier { - if identifier.kind() == "identifier" { - let variable_name = - source[identifier.byte_range()].to_string(); - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = - variable_names_in_scope.get(&variable_name) - { - variables.remove(*index); - } - - variable_names_in_scope - .insert(variable_name.clone(), variables.len()); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: identifier.end_position().row, - column: identifier.end_position().column, - }); - } - } - } - } - } - } - "function_definition" => { - if let Some(params) = child.child_by_field_name("parameters") { - for param in params.named_children(&mut params.walk()) { - let param_identifier = if param.kind() == "identifier" { - Some(param) - } else if param.kind() == "typed_parameter" { - param.child(0) - } else { - None - }; - - if let Some(identifier) = param_identifier { - if identifier.kind() == "identifier" { - let variable_name = - source[identifier.byte_range()].to_string(); - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = - variable_names_in_scope.get(&variable_name) - { - variables.remove(*index); - } - - variable_names_in_scope - .insert(variable_name.clone(), variables.len()); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: identifier.end_position().row, - column: identifier.end_position().column, - }); - } - } - } - } - } - "for_statement" => { - if let Some(target) = child.child_by_field_name("left") { - if target.kind() == "identifier" { - let variable_name = source[target.byte_range()].to_string(); - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = variable_names_in_scope.get(&variable_name) - { - variables.remove(*index); - } - - variable_names_in_scope - .insert(variable_name.clone(), variables.len()); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: target.end_position().row, - column: target.end_position().column, - }); - } - } - } - _ => {} - } - } - } - - variable_names.extend(variable_names_in_scope.keys().cloned()); - - if matches!(node.kind(), "function_definition" | "module") - && node.range().end_point.row < max_row - { - scope = VariableScope::Global; - } - - if let Some(parent) = node.parent() { - node = parent; - } else { - break; - } - } - - variables - } -} - -pub struct GoInlineValueProvider; - -impl InlineValueProvider for GoInlineValueProvider { - fn provide( - &self, - mut node: language::Node, - source: &str, - max_row: usize, - ) -> Vec { - let mut variables = Vec::new(); - let mut variable_names = HashSet::new(); - let mut scope = VariableScope::Local; - - loop { - let mut variable_names_in_scope = HashMap::new(); - for child in node.named_children(&mut node.walk()) { - if child.start_position().row >= max_row { - break; - } - - if scope == VariableScope::Local { - match child.kind() { - "var_declaration" => { - for var_spec in child.named_children(&mut child.walk()) { - if var_spec.kind() == "var_spec" { - if let Some(name_node) = var_spec.child_by_field_name("name") { - let variable_name = - source[name_node.byte_range()].to_string(); - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = - variable_names_in_scope.get(&variable_name) - { - variables.remove(*index); - } - - variable_names_in_scope - .insert(variable_name.clone(), variables.len()); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: name_node.end_position().row, - column: name_node.end_position().column, - }); - } - } - } - } - "short_var_declaration" => { - if let Some(left_side) = child.child_by_field_name("left") { - for identifier in left_side.named_children(&mut left_side.walk()) { - if identifier.kind() == "identifier" { - let variable_name = - source[identifier.byte_range()].to_string(); - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = - variable_names_in_scope.get(&variable_name) - { - variables.remove(*index); - } - - variable_names_in_scope - .insert(variable_name.clone(), variables.len()); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: identifier.end_position().row, - column: identifier.end_position().column, - }); - } - } - } - } - "assignment_statement" => { - if let Some(left_side) = child.child_by_field_name("left") { - for identifier in left_side.named_children(&mut left_side.walk()) { - if identifier.kind() == "identifier" { - let variable_name = - source[identifier.byte_range()].to_string(); - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = - variable_names_in_scope.get(&variable_name) - { - variables.remove(*index); - } - - variable_names_in_scope - .insert(variable_name.clone(), variables.len()); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: identifier.end_position().row, - column: identifier.end_position().column, - }); - } - } - } - } - "function_declaration" | "method_declaration" => { - if let Some(params) = child.child_by_field_name("parameters") { - for param in params.named_children(&mut params.walk()) { - if param.kind() == "parameter_declaration" { - if let Some(name_node) = param.child_by_field_name("name") { - let variable_name = - source[name_node.byte_range()].to_string(); - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = - variable_names_in_scope.get(&variable_name) - { - variables.remove(*index); - } - - variable_names_in_scope - .insert(variable_name.clone(), variables.len()); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: name_node.end_position().row, - column: name_node.end_position().column, - }); - } - } - } - } - } - "for_statement" => { - if let Some(clause) = child.named_child(0) { - if clause.kind() == "for_clause" { - if let Some(init) = clause.named_child(0) { - if init.kind() == "short_var_declaration" { - if let Some(left_side) = - init.child_by_field_name("left") - { - if left_side.kind() == "expression_list" { - for identifier in left_side - .named_children(&mut left_side.walk()) - { - if identifier.kind() == "identifier" { - let variable_name = source - [identifier.byte_range()] - .to_string(); - - if variable_names - .contains(&variable_name) - { - continue; - } - - if let Some(index) = - variable_names_in_scope - .get(&variable_name) - { - variables.remove(*index); - } - - variable_names_in_scope.insert( - variable_name.clone(), - variables.len(), - ); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: - VariableLookupKind::Variable, - row: identifier.end_position().row, - column: identifier - .end_position() - .column, - }); - } - } - } - } - } - } - } else if clause.kind() == "range_clause" { - if let Some(left) = clause.child_by_field_name("left") { - if left.kind() == "expression_list" { - for identifier in left.named_children(&mut left.walk()) - { - if identifier.kind() == "identifier" { - let variable_name = - source[identifier.byte_range()].to_string(); - - if variable_name == "_" { - continue; - } - - if variable_names.contains(&variable_name) { - continue; - } - - if let Some(index) = - variable_names_in_scope.get(&variable_name) - { - variables.remove(*index); - } - variable_names_in_scope.insert( - variable_name.clone(), - variables.len(), - ); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Local, - lookup: VariableLookupKind::Variable, - row: identifier.end_position().row, - column: identifier.end_position().column, - }); - } - } - } - } - } - } - } - _ => {} - } - } else if child.kind() == "var_declaration" { - for var_spec in child.named_children(&mut child.walk()) { - if var_spec.kind() == "var_spec" { - if let Some(name_node) = var_spec.child_by_field_name("name") { - let variable_name = source[name_node.byte_range()].to_string(); - variables.push(InlineValueLocation { - variable_name, - scope: VariableScope::Global, - lookup: VariableLookupKind::Expression, - row: name_node.end_position().row, - column: name_node.end_position().column, - }); - } - } - } - } - } - - variable_names.extend(variable_names_in_scope.keys().cloned()); - - if matches!(node.kind(), "function_declaration" | "method_declaration") { - scope = VariableScope::Global; - } - - if let Some(parent) = node.parent() { - node = parent; - } else { - break; - } - } - - variables - } -} -#[cfg(test)] -mod tests { - use super::*; - use tree_sitter::Parser; - - #[test] - fn test_go_inline_value_provider() { - let provider = GoInlineValueProvider; - let source = r#" -package main - -func main() { - items := []int{1, 2, 3, 4, 5} - for i, v := range items { - println(i, v) - } - for j := 0; j < 10; j++ { - println(j) - } -} -"#; - - let mut parser = Parser::new(); - if parser - .set_language(&tree_sitter_go::LANGUAGE.into()) - .is_err() - { - return; - } - let Some(tree) = parser.parse(source, None) else { - return; - }; - let root_node = tree.root_node(); - - let mut main_body = None; - for child in root_node.named_children(&mut root_node.walk()) { - if child.kind() == "function_declaration" { - if let Some(name) = child.child_by_field_name("name") { - if &source[name.byte_range()] == "main" { - if let Some(body) = child.child_by_field_name("body") { - main_body = Some(body); - break; - } - } - } - } - } - - let Some(main_body) = main_body else { - return; - }; - - let variables = provider.provide(main_body, source, 100); - assert!(variables.len() >= 2); - - let variable_names: Vec<&str> = - variables.iter().map(|v| v.variable_name.as_str()).collect(); - assert!(variable_names.contains(&"items")); - assert!(variable_names.contains(&"j")); - } - - #[test] - fn test_go_inline_value_provider_counter_pattern() { - let provider = GoInlineValueProvider; - let source = r#" -package main - -func main() { - N := 10 - for i := range N { - println(i) - } -} -"#; - - let mut parser = Parser::new(); - if parser - .set_language(&tree_sitter_go::LANGUAGE.into()) - .is_err() - { - return; - } - let Some(tree) = parser.parse(source, None) else { - return; - }; - let root_node = tree.root_node(); - - let mut main_body = None; - for child in root_node.named_children(&mut root_node.walk()) { - if child.kind() == "function_declaration" { - if let Some(name) = child.child_by_field_name("name") { - if &source[name.byte_range()] == "main" { - if let Some(body) = child.child_by_field_name("body") { - main_body = Some(body); - break; - } - } - } - } - } - - let Some(main_body) = main_body else { - return; - }; - let variables = provider.provide(main_body, source, 100); - - let variable_names: Vec<&str> = - variables.iter().map(|v| v.variable_name.as_str()).collect(); - assert!(variable_names.contains(&"N")); - assert!(variable_names.contains(&"i")); - } -} diff --git a/crates/dap/src/registry.rs b/crates/dap/src/registry.rs index 2786de227e..9435b16b92 100644 --- a/crates/dap/src/registry.rs +++ b/crates/dap/src/registry.rs @@ -8,10 +8,7 @@ use task::{ AdapterSchema, AdapterSchemas, DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate, }; -use crate::{ - adapters::{DebugAdapter, DebugAdapterName}, - inline_value::InlineValueProvider, -}; +use crate::adapters::{DebugAdapter, DebugAdapterName}; use std::{collections::BTreeMap, sync::Arc}; /// Given a user build configuration, locator creates a fill-in debug target ([DebugScenario]) on behalf of the user. @@ -33,7 +30,6 @@ pub trait DapLocator: Send + Sync { struct DapRegistryState { adapters: BTreeMap>, locators: FxHashMap>, - inline_value_providers: FxHashMap>, } #[derive(Clone, Default)] @@ -82,22 +78,6 @@ impl DapRegistry { schemas } - pub fn add_inline_value_provider( - &self, - language: String, - provider: Arc, - ) { - let _previous_value = self - .0 - .write() - .inline_value_providers - .insert(language, provider); - debug_assert!( - _previous_value.is_none(), - "Attempted to insert a new inline value provider when one is already registered" - ); - } - pub fn locators(&self) -> FxHashMap> { self.0.read().locators.clone() } @@ -106,10 +86,6 @@ impl DapRegistry { self.0.read().adapters.get(name).cloned() } - pub fn inline_value_provider(&self, language: &str) -> Option> { - self.0.read().inline_value_providers.get(language).cloned() - } - pub fn enumerate_adapters(&self) -> Vec { self.0.read().adapters.keys().cloned().collect() } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 414d0a91a3..79c56fdf25 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -18,7 +18,6 @@ use dap::{ GithubRepo, }, configure_tcp_connection, - inline_value::{GoInlineValueProvider, PythonInlineValueProvider, RustInlineValueProvider}, }; use gdb::GdbDebugAdapter; use go::GoDebugAdapter; @@ -44,10 +43,5 @@ pub fn init(cx: &mut App) { { registry.add_adapter(Arc::from(dap::FakeAdapter {})); } - - registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider)); - registry - .add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider)); - registry.add_inline_value_provider("Go".to_string(), Arc::from(GoInlineValueProvider)); }) } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index e259b8a4b3..91f9acad3c 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -81,3 +81,4 @@ unindent.workspace = true util = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } zlog.workspace = true +tree-sitter-go.workspace = true diff --git a/crates/debugger_ui/src/tests/inline_values.rs b/crates/debugger_ui/src/tests/inline_values.rs index 6fed57ecac..45cab2a306 100644 --- a/crates/debugger_ui/src/tests/inline_values.rs +++ b/crates/debugger_ui/src/tests/inline_values.rs @@ -246,10 +246,10 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { - let x = 10; + let x: 10 = 10; let value = 42; let y = 4; let tester = { @@ -303,11 +303,11 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; - let value = 42; + let value: 42 = 42; let y = 4; let tester = { let y = 10; @@ -360,12 +360,12 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; let value: 42 = 42; - let y = 4; + let y: 4 = 4; let tester = { let y = 10; let y = 5; @@ -417,7 +417,7 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; @@ -474,14 +474,14 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; let value: 42 = 42; let y: 4 = 4; let tester = { - let y = 10; + let y: 4 = 10; let y = 5; let b = 3; vec![y, 20, 30] @@ -581,15 +581,15 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; let value: 42 = 42; - let y = 4; + let y: 10 = 4; let tester = { let y: 10 = 10; - let y = 5; + let y: 10 = 5; let b = 3; vec![y, 20, 30] }; @@ -688,14 +688,14 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; let value: 42 = 42; - let y = 4; + let y: 5 = 4; let tester = { - let y = 10; + let y: 5 = 10; let y: 5 = 5; let b = 3; vec![y, 20, 30] @@ -807,17 +807,17 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; let value: 42 = 42; - let y = 4; + let y: 5 = 4; let tester = { - let y = 10; + let y: 5 = 10; let y: 5 = 5; let b: 3 = 3; - vec![y, 20, 30] + vec![y: 5, 20, 30] }; let caller = || { @@ -926,7 +926,7 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; @@ -1058,7 +1058,7 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; @@ -1115,21 +1115,21 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { - let x = 10; - let value = 42; - let y = 4; - let tester = { + let x: 10 = 10; + let value: 42 = 42; + let y: 4 = 4; + let tester: size=3 = { let y = 10; let y = 5; let b = 3; vec![y, 20, 30] }; - let caller = || { - let x = 3; + let caller: = || { + let x: 10 = 3; println!("x={}", x); }; @@ -1193,10 +1193,10 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 1: usize = 1; + static mut GLOBAL: usize = 1; fn main() { - let x = 10; + let x: 3 = 10; let value = 42; let y = 4; let tester = { @@ -1208,7 +1208,7 @@ fn main() { let caller = || { let x: 3 = 3; - println!("x={}", x); + println!("x={}", x: 3); }; caller(); @@ -1338,7 +1338,7 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 2: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; @@ -1362,7 +1362,7 @@ fn main() { GLOBAL = 2; } - let result = value * 2 * x; + let result = value: 42 * 2 * x: 10; println!("Simple test executed: value={}, result={}", value, result); assert!(true); } @@ -1483,7 +1483,7 @@ fn main() { editor.update_in(cx, |editor, window, cx| { pretty_assertions::assert_eq!( r#" - static mut GLOBAL: 2: usize = 1; + static mut GLOBAL: usize = 1; fn main() { let x: 10 = 10; @@ -1507,8 +1507,8 @@ fn main() { GLOBAL = 2; } - let result: 840 = value * 2 * x; - println!("Simple test executed: value={}, result={}", value, result); + let result: 840 = value: 42 * 2 * x: 10; + println!("Simple test executed: value={}, result={}", value: 42, result: 840); assert!(true); } "# @@ -1519,6 +1519,7 @@ fn main() { } fn rust_lang() -> Language { + let debug_variables_query = include_str!("../../../languages/src/rust/debugger.scm"); Language::new( LanguageConfig { name: "Rust".into(), @@ -1530,6 +1531,8 @@ fn rust_lang() -> Language { }, Some(tree_sitter_rust::LANGUAGE.into()), ) + .with_debug_variables_query(debug_variables_query) + .unwrap() } #[gpui::test] @@ -1818,8 +1821,8 @@ def process_data(untyped_param, typed_param: int, another_typed: str): def process_data(untyped_param: test_value, typed_param: 42: int, another_typed: world: str): # Local variables x: 10 = 10 - result: 84 = typed_param * 2 - text: Hello, world = "Hello, " + another_typed + result: 84 = typed_param: 42 * 2 + text: Hello, world = "Hello, " + another_typed: world # For loop with range sum_value: 10 = 0 @@ -1837,6 +1840,7 @@ def process_data(untyped_param, typed_param: int, another_typed: str): } fn python_lang() -> Language { + let debug_variables_query = include_str!("../../../languages/src/python/debugger.scm"); Language::new( LanguageConfig { name: "Python".into(), @@ -1848,4 +1852,392 @@ fn python_lang() -> Language { }, Some(tree_sitter_python::LANGUAGE.into()), ) + .with_debug_variables_query(debug_variables_query) + .unwrap() +} + +fn go_lang() -> Language { + let debug_variables_query = include_str!("../../../languages/src/go/debugger.scm"); + Language::new( + LanguageConfig { + name: "Go".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["go".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_go::LANGUAGE.into()), + ) + .with_debug_variables_query(debug_variables_query) + .unwrap() +} + +/// Test utility function for inline values testing +/// +/// # Arguments +/// * `variables` - List of tuples containing (variable_name, variable_value) +/// * `before` - Source code before inline values are applied +/// * `after` - Expected source code after inline values are applied +/// * `language` - Language configuration to use for parsing +/// * `executor` - Background executor for async operations +/// * `cx` - Test app context +async fn test_inline_values_util( + local_variables: &[(&str, &str)], + global_variables: &[(&str, &str)], + before: &str, + after: &str, + active_debug_line: Option, + language: Language, + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let lines_count = before.lines().count(); + let stop_line = + active_debug_line.unwrap_or_else(|| if lines_count > 6 { 6 } else { lines_count - 1 }); + + let fs = FakeFs::new(executor.clone()); + fs.insert_tree(path!("/project"), json!({ "main.rs": before.to_string() })) + .await; + + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + workspace + .update(cx, |workspace, window, cx| { + workspace.focus_panel::(window, cx); + }) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); + let client = session.update(cx, |session, _| session.adapter_client().unwrap()); + + client.on_request::(|_, _| { + Ok(dap::ThreadsResponse { + threads: vec![dap::Thread { + id: 1, + name: "main".into(), + }], + }) + }); + + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![dap::StackFrame { + id: 1, + name: "main".into(), + source: Some(dap::Source { + name: Some("main.rs".into()), + path: Some(path!("/project/main.rs").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: stop_line as u64, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }], + total_frames: None, + }) + }); + + let local_vars: Vec = local_variables + .iter() + .map(|(name, value)| Variable { + name: (*name).into(), + value: (*value).into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + declaration_location_reference: None, + value_location_reference: None, + }) + .collect(); + + let global_vars: Vec = global_variables + .iter() + .map(|(name, value)| Variable { + name: (*name).into(), + value: (*value).into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + declaration_location_reference: None, + value_location_reference: None, + }) + .collect(); + + client.on_request::({ + let local_vars = Arc::new(local_vars.clone()); + let global_vars = Arc::new(global_vars.clone()); + move |_, args| { + let variables = match args.variables_reference { + 2 => (*local_vars).clone(), + 3 => (*global_vars).clone(), + _ => vec![], + }; + Ok(dap::VariablesResponse { variables }) + } + }); + + client.on_request::(move |_, _| { + Ok(dap::ScopesResponse { + scopes: vec![ + Scope { + name: "Local".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + Scope { + name: "Global".into(), + presentation_hint: None, + variables_reference: 3, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + ], + }) + }); + + if !global_variables.is_empty() { + let global_evaluate_map: std::collections::HashMap = global_variables + .iter() + .map(|(name, value)| (name.to_string(), value.to_string())) + .collect(); + + client.on_request::(move |_, args| { + let value = global_evaluate_map + .get(&args.expression) + .unwrap_or(&"undefined".to_string()) + .clone(); + + Ok(dap::EvaluateResponse { + result: value, + type_: None, + presentation_hint: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + value_location_reference: None, + }) + }); + } + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + let project_path = Path::new(path!("/project")); + let worktree = project + .update(cx, |project, cx| project.find_worktree(project_path, cx)) + .expect("This worktree should exist in project") + .0; + + let worktree_id = workspace + .update(cx, |_, _, cx| worktree.read(cx).id()) + .unwrap(); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(Arc::new(language)), cx); + }); + + let (editor, cx) = cx.add_window_view(|window, cx| { + Editor::new( + EditorMode::full(), + MultiBuffer::build_from_buffer(buffer, cx), + Some(project), + window, + cx, + ) + }); + + active_debug_session_panel(workspace, cx).update_in(cx, |_, window, cx| { + cx.focus_self(window); + }); + cx.run_until_parked(); + + editor.update(cx, |editor, cx| editor.refresh_inline_values(cx)); + + cx.run_until_parked(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!(after, editor.snapshot(window, cx).text()); + }); +} + +#[gpui::test] +async fn test_inline_values_example(executor: BackgroundExecutor, cx: &mut TestAppContext) { + let variables = [("x", "10"), ("y", "20"), ("result", "30")]; + + let before = r#" +fn main() { + let x = 10; + let y = 20; + let result = x + y; + println!("Result: {}", result); +} +"# + .unindent(); + + let after = r#" +fn main() { + let x: 10 = 10; + let y: 20 = 20; + let result: 30 = x: 10 + y: 20; + println!("Result: {}", result: 30); +} +"# + .unindent(); + + test_inline_values_util( + &variables, + &[], + &before, + &after, + None, + rust_lang(), + executor, + cx, + ) + .await; +} + +#[gpui::test] +async fn test_inline_values_with_globals(executor: BackgroundExecutor, cx: &mut TestAppContext) { + let variables = [("x", "5"), ("y", "10")]; + + let before = r#" +static mut GLOBAL_COUNTER: usize = 42; + +fn main() { + let x = 5; + let y = 10; + unsafe { + GLOBAL_COUNTER += 1; + } + println!("x={}, y={}, global={}", x, y, unsafe { GLOBAL_COUNTER }); +} +"# + .unindent(); + + let after = r#" +static mut GLOBAL_COUNTER: 42: usize = 42; + +fn main() { + let x: 5 = 5; + let y: 10 = 10; + unsafe { + GLOBAL_COUNTER += 1; + } + println!("x={}, y={}, global={}", x, y, unsafe { GLOBAL_COUNTER }); +} +"# + .unindent(); + + test_inline_values_util( + &variables, + &[("GLOBAL_COUNTER", "42")], + &before, + &after, + None, + rust_lang(), + executor, + cx, + ) + .await; +} + +#[gpui::test] +async fn test_go_inline_values(executor: BackgroundExecutor, cx: &mut TestAppContext) { + let variables = [("x", "42"), ("y", "hello")]; + + let before = r#" +package main + +var globalCounter int = 100 + +func main() { + x := 42 + y := "hello" + z := x + 10 + println(x, y, z) +} +"# + .unindent(); + + let after = r#" +package main + +var globalCounter: 100 int = 100 + +func main() { + x: 42 := 42 + y := "hello" + z := x + 10 + println(x, y, z) +} +"# + .unindent(); + + test_inline_values_util( + &variables, + &[("globalCounter", "100")], + &before, + &after, + None, + go_lang(), + executor, + cx, + ) + .await; } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 568e9062c8..6e9a9be0fe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -19167,7 +19167,7 @@ impl Editor { let current_execution_position = self .highlighted_rows .get(&TypeId::of::()) - .and_then(|lines| lines.last().map(|line| line.range.start)); + .and_then(|lines| lines.last().map(|line| line.range.end)); self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| { let inline_values = editor @@ -21553,7 +21553,6 @@ impl SemanticsProvider for Entity { fn inline_values( &self, buffer_handle: Entity, - range: Range, cx: &mut App, ) -> Option>>> { diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 278976d3cd..b0e06c3d65 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -20,6 +20,7 @@ test-support = [ "text/test-support", "tree-sitter-rust", "tree-sitter-python", + "tree-sitter-rust", "tree-sitter-typescript", "settings/test-support", "util/test-support", diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 523efa49dc..90a899f79d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,12 +1,6 @@ -pub use crate::{ - Grammar, Language, LanguageRegistry, - diagnostic_set::DiagnosticSet, - highlight_map::{HighlightId, HighlightMap}, - proto, -}; use crate::{ - LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject, - TreeSitterOptions, + DebuggerTextObject, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, + TextObject, TreeSitterOptions, diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{LanguageSettings, language_settings}, outline::OutlineItem, @@ -17,6 +11,12 @@ use crate::{ task_context::RunnableRange, text_diff::text_diff, }; +pub use crate::{ + Grammar, Language, LanguageRegistry, + diagnostic_set::DiagnosticSet, + highlight_map::{HighlightId, HighlightMap}, + proto, +}; use anyhow::{Context as _, Result}; pub use clock::ReplicaId; use clock::{AGENT_REPLICA_ID, Lamport}; @@ -3848,6 +3848,74 @@ impl BufferSnapshot { .filter(|pair| !pair.newline_only) } + pub fn debug_variables_query( + &self, + range: Range, + ) -> impl Iterator, DebuggerTextObject)> + '_ { + let range = range.start.to_offset(self).saturating_sub(1) + ..self.len().min(range.end.to_offset(self) + 1); + + let mut matches = self.syntax.matches_with_options( + range.clone(), + &self.text, + TreeSitterOptions::default(), + |grammar| grammar.debug_variables_config.as_ref().map(|c| &c.query), + ); + + let configs = matches + .grammars() + .iter() + .map(|grammar| grammar.debug_variables_config.as_ref()) + .collect::>(); + + let mut captures = Vec::<(Range, DebuggerTextObject)>::new(); + + iter::from_fn(move || { + loop { + while let Some(capture) = captures.pop() { + if capture.0.overlaps(&range) { + return Some(capture); + } + } + + let mat = matches.peek()?; + + let Some(config) = configs[mat.grammar_index].as_ref() else { + matches.advance(); + continue; + }; + + for capture in mat.captures { + let Some(ix) = config + .objects_by_capture_ix + .binary_search_by_key(&capture.index, |e| e.0) + .ok() + else { + continue; + }; + let text_object = config.objects_by_capture_ix[ix].1; + let byte_range = capture.node.byte_range(); + + let mut found = false; + for (range, existing) in captures.iter_mut() { + if existing == &text_object { + range.start = range.start.min(byte_range.start); + range.end = range.end.max(byte_range.end); + found = true; + break; + } + } + + if !found { + captures.push((byte_range, text_object)); + } + } + + matches.advance(); + } + }) + } + pub fn text_object_ranges( &self, range: Range, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8b8c411366..f564b54ed5 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1082,6 +1082,7 @@ pub struct Grammar { pub embedding_config: Option, pub(crate) injection_config: Option, pub(crate) override_config: Option, + pub(crate) debug_variables_config: Option, pub(crate) highlight_map: Mutex, } @@ -1104,6 +1105,22 @@ pub struct OutlineConfig { pub annotation_capture_ix: Option, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DebuggerTextObject { + Variable, + Scope, +} + +impl DebuggerTextObject { + pub fn from_capture_name(name: &str) -> Option { + match name { + "debug-variable" => Some(DebuggerTextObject::Variable), + "debug-scope" => Some(DebuggerTextObject::Scope), + _ => None, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum TextObject { InsideFunction, @@ -1206,6 +1223,11 @@ struct BracketsPatternConfig { newline_only: bool, } +pub struct DebugVariablesConfig { + pub query: Query, + pub objects_by_capture_ix: Vec<(u32, DebuggerTextObject)>, +} + impl Language { pub fn new(config: LanguageConfig, ts_language: Option) -> Self { Self::new_with_id(LanguageId::new(), config, ts_language) @@ -1237,6 +1259,7 @@ impl Language { redactions_config: None, runnable_config: None, error_query: Query::new(&ts_language, "(ERROR) @error").ok(), + debug_variables_config: None, ts_language, highlight_map: Default::default(), }) @@ -1307,6 +1330,11 @@ impl Language { .with_text_object_query(query.as_ref()) .context("Error loading textobject query")?; } + if let Some(query) = queries.debugger { + self = self + .with_debug_variables_query(query.as_ref()) + .context("Error loading debug variables query")?; + } Ok(self) } @@ -1425,6 +1453,24 @@ impl Language { Ok(self) } + pub fn with_debug_variables_query(mut self, source: &str) -> Result { + let grammar = self.grammar_mut().context("cannot mutate grammar")?; + let query = Query::new(&grammar.ts_language, source)?; + + let mut objects_by_capture_ix = Vec::new(); + for (ix, name) in query.capture_names().iter().enumerate() { + if let Some(text_object) = DebuggerTextObject::from_capture_name(name) { + objects_by_capture_ix.push((ix as u32, text_object)); + } + } + + grammar.debug_variables_config = Some(DebugVariablesConfig { + query, + objects_by_capture_ix, + }); + Ok(self) + } + pub fn with_brackets_query(mut self, source: &str) -> Result { let grammar = self.grammar_mut().context("cannot mutate grammar")?; let query = Query::new(&grammar.ts_language, source)?; @@ -1930,6 +1976,10 @@ impl Grammar { .capture_index_for_name(name)?; Some(self.highlight_map.lock().get(capture_id)) } + + pub fn debug_variables_config(&self) -> Option<&DebugVariablesConfig> { + self.debug_variables_config.as_ref() + } } impl CodeLabel { diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 4d0837d8e3..c157cd9e73 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -226,7 +226,7 @@ pub const QUERY_FILENAME_PREFIXES: &[( ("overrides", |q| &mut q.overrides), ("redactions", |q| &mut q.redactions), ("runnables", |q| &mut q.runnables), - ("debug_variables", |q| &mut q.debug_variables), + ("debugger", |q| &mut q.debugger), ("textobjects", |q| &mut q.text_objects), ]; @@ -243,7 +243,7 @@ pub struct LanguageQueries { pub redactions: Option>, pub runnables: Option>, pub text_objects: Option>, - pub debug_variables: Option>, + pub debugger: Option>, } #[derive(Clone, Default)] diff --git a/crates/languages/src/go/debugger.scm b/crates/languages/src/go/debugger.scm new file mode 100644 index 0000000000..f22b91f938 --- /dev/null +++ b/crates/languages/src/go/debugger.scm @@ -0,0 +1,26 @@ +(parameter_declaration (identifier) @debug-variable) + +(short_var_declaration (expression_list (identifier) @debug-variable)) + +(var_declaration (var_spec (identifier) @debug-variable)) + +(const_declaration (const_spec (identifier) @debug-variable)) + +(assignment_statement (expression_list (identifier) @debug-variable)) + +(binary_expression (identifier) @debug-variable + (#not-match? @debug-variable "^[A-Z]")) + +(call_expression (argument_list (identifier) @debug-variable + (#not-match? @debug-variable "^[A-Z]"))) + +(return_statement (expression_list (identifier) @debug-variable + (#not-match? @debug-variable "^[A-Z]"))) + +(range_clause (expression_list (identifier) @debug-variable)) + +(parenthesized_expression (identifier) @debug-variable + (#not-match? @debug-variable "^[A-Z]")) + +(block) @debug-scope +(function_declaration) @debug-scope diff --git a/crates/languages/src/python/debugger.scm b/crates/languages/src/python/debugger.scm new file mode 100644 index 0000000000..807d6e865d --- /dev/null +++ b/crates/languages/src/python/debugger.scm @@ -0,0 +1,43 @@ +(identifier) @debug-variable +(#eq? @debug-variable "self") + +(assignment left: (identifier) @debug-variable) +(assignment left: (pattern_list (identifier) @debug-variable)) +(assignment left: (tuple_pattern (identifier) @debug-variable)) + +(augmented_assignment left: (identifier) @debug-variable) + +(for_statement left: (identifier) @debug-variable) +(for_statement left: (pattern_list (identifier) @debug-variable)) +(for_statement left: (tuple_pattern (identifier) @debug-variable)) + +(for_in_clause left: (identifier) @debug-variable) +(for_in_clause left: (pattern_list (identifier) @debug-variable)) +(for_in_clause left: (tuple_pattern (identifier) @debug-variable)) + +(as_pattern (identifier) @debug-variable) + +(binary_operator left: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) +(binary_operator right: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) +(comparison_operator (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(list (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) +(tuple (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) +(set (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(subscript value: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(attribute object: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(return_statement (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(parenthesized_expression (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(argument_list (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(if_statement condition: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(while_statement condition: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]")) + +(block) @debug-scope +(module) @debug-scope diff --git a/crates/languages/src/rust/debugger.scm b/crates/languages/src/rust/debugger.scm new file mode 100644 index 0000000000..5347413f69 --- /dev/null +++ b/crates/languages/src/rust/debugger.scm @@ -0,0 +1,50 @@ +(metavariable) @debug-variable + +(parameter (identifier) @debug-variable) + +(self) @debug-variable + +(static_item (identifier) @debug-variable) +(const_item (identifier) @debug-variable) + +(let_declaration pattern: (identifier) @debug-variable) + +(let_condition (identifier) @debug-variable) + +(match_arm (identifier) @debug-variable) + +(for_expression (identifier) @debug-variable) + +(closure_parameters (identifier) @debug-variable) + +(assignment_expression (identifier) @debug-variable) + +(field_expression (identifier) @debug-variable) + +(binary_expression (identifier) @debug-variable + (#not-match? @debug-variable "^[A-Z]")) + +(reference_expression (identifier) @debug-variable + (#not-match? @debug-variable "^[A-Z]")) + +(array_expression (identifier) @debug-variable) +(tuple_expression (identifier) @debug-variable) +(return_expression (identifier) @debug-variable) +(await_expression (identifier) @debug-variable) +(try_expression (identifier) @debug-variable) +(index_expression (identifier) @debug-variable) +(range_expression (identifier) @debug-variable) +(unary_expression (identifier) @debug-variable) + +(if_expression (identifier) @debug-variable) +(while_expression (identifier) @debug-variable) + +(parenthesized_expression (identifier) @debug-variable) + +(arguments (identifier) @debug-variable + (#not-match? @debug-variable "^[A-Z]")) + +(macro_invocation (token_tree (identifier) @debug-variable + (#not-match? @debug-variable "^[A-Z]"))) + +(block) @debug-scope diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 28cfbe4e4d..be4964bbee 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -588,7 +588,14 @@ impl DapStore { cx: &mut Context, ) -> Task>> { let snapshot = buffer_handle.read(cx).snapshot(); - let all_variables = session.read(cx).variables_by_stack_frame_id(stack_frame_id); + let local_variables = + session + .read(cx) + .variables_by_stack_frame_id(stack_frame_id, false, true); + let global_variables = + session + .read(cx) + .variables_by_stack_frame_id(stack_frame_id, true, false); fn format_value(mut value: String) -> String { const LIMIT: usize = 100; @@ -617,10 +624,20 @@ impl DapStore { match inline_value_location.lookup { VariableLookupKind::Variable => { - let Some(variable) = all_variables - .iter() - .find(|variable| variable.name == inline_value_location.variable_name) - else { + let variable_search = + if inline_value_location.scope + == dap::inline_value::VariableScope::Local + { + local_variables.iter().chain(global_variables.iter()).find( + |variable| variable.name == inline_value_location.variable_name, + ) + } else { + global_variables.iter().find(|variable| { + variable.name == inline_value_location.variable_name + }) + }; + + let Some(variable) = variable_search else { continue; }; diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 917506e523..300c598bfb 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -2171,7 +2171,12 @@ impl Session { .unwrap_or_default() } - pub fn variables_by_stack_frame_id(&self, stack_frame_id: StackFrameId) -> Vec { + pub fn variables_by_stack_frame_id( + &self, + stack_frame_id: StackFrameId, + globals: bool, + locals: bool, + ) -> Vec { let Some(stack_frame) = self.stack_frames.get(&stack_frame_id) else { return Vec::new(); }; @@ -2179,6 +2184,10 @@ impl Session { stack_frame .scopes .iter() + .filter(|scope| { + (scope.name.to_lowercase().contains("local") && locals) + || (scope.name.to_lowercase().contains("global") && globals) + }) .filter_map(|scope| self.variables.get(&scope.variables_reference)) .flatten() .cloned() diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2fc7fbbe76..e8b3814850 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -31,6 +31,8 @@ use git_store::{Repository, RepositoryId}; pub mod search_history; mod yarn; +use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope}; + use crate::git_store::GitStore; pub use git_store::{ ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate, @@ -45,7 +47,7 @@ use client::{ }; use clock::ReplicaId; -use dap::{DapRegistry, client::DebugAdapterClient}; +use dap::client::DebugAdapterClient; use collections::{BTreeSet, HashMap, HashSet}; use debounced_delay::DebouncedDelay; @@ -111,7 +113,7 @@ use std::{ use task_store::TaskStore; use terminals::Terminals; -use text::{Anchor, BufferId}; +use text::{Anchor, BufferId, Point}; use toolchain_store::EmptyToolchainStore; use util::{ ResultExt as _, @@ -3667,35 +3669,15 @@ impl Project { range: Range, cx: &mut Context, ) -> Task>> { - let language_name = buffer_handle - .read(cx) - .language() - .map(|language| language.name().to_string()); - - let Some(inline_value_provider) = language_name - .and_then(|language| DapRegistry::global(cx).inline_value_provider(&language)) - else { - return Task::ready(Err(anyhow::anyhow!("Inline value provider not found"))); - }; - let snapshot = buffer_handle.read(cx).snapshot(); - let Some(root_node) = snapshot.syntax_root_ancestor(range.end) else { - return Task::ready(Ok(vec![])); - }; + let captures = snapshot.debug_variables_query(Anchor::MIN..range.end); let row = snapshot .summary_for_anchor::(&range.end) .row as usize; - let inline_value_locations = inline_value_provider.provide( - root_node, - snapshot - .text_for_range(Anchor::MIN..range.end) - .collect::() - .as_str(), - row, - ); + let inline_value_locations = provide_inline_values(captures, &snapshot, row); let stack_frame_id = active_stack_frame.stack_frame_id; cx.spawn(async move |this, cx| { @@ -5377,3 +5359,69 @@ fn proto_to_prompt(level: proto::language_server_prompt_request::Level) -> gpui: proto::language_server_prompt_request::Level::Critical(_) => gpui::PromptLevel::Critical, } } + +fn provide_inline_values( + captures: impl Iterator, language::DebuggerTextObject)>, + snapshot: &language::BufferSnapshot, + max_row: usize, +) -> Vec { + let mut variables = Vec::new(); + let mut variable_position = HashSet::default(); + let mut scopes = Vec::new(); + + let active_debug_line_offset = snapshot.point_to_offset(Point::new(max_row as u32, 0)); + + for (capture_range, capture_kind) in captures { + match capture_kind { + language::DebuggerTextObject::Variable => { + let variable_name = snapshot + .text_for_range(capture_range.clone()) + .collect::(); + let point = snapshot.offset_to_point(capture_range.end); + + while scopes.last().map_or(false, |scope: &Range<_>| { + !scope.contains(&capture_range.start) + }) { + scopes.pop(); + } + + if point.row as usize > max_row { + break; + } + + let scope = if scopes + .last() + .map_or(true, |scope| !scope.contains(&active_debug_line_offset)) + { + VariableScope::Global + } else { + VariableScope::Local + }; + + if variable_position.insert(capture_range.end) { + variables.push(InlineValueLocation { + variable_name, + scope, + lookup: VariableLookupKind::Variable, + row: point.row as usize, + column: point.column as usize, + }); + } + } + language::DebuggerTextObject::Scope => { + while scopes.last().map_or_else( + || false, + |scope: &Range| { + !(scope.contains(&capture_range.start) + && scope.contains(&capture_range.end)) + }, + ) { + scopes.pop(); + } + scopes.push(capture_range); + } + } + } + + variables +}