diff --git a/Cargo.lock b/Cargo.lock index 4b6f74756c..140e81cfd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4068,7 +4068,6 @@ dependencies = [ "http_client", "language", "log", - "lsp-types", "node_runtime", "parking_lot", "paths", @@ -4104,7 +4103,6 @@ dependencies = [ "futures 0.3.31", "gpui", "language", - "lsp-types", "paths", "serde", "serde_json", @@ -4249,6 +4247,7 @@ dependencies = [ "collections", "command_palette_hooks", "dap", + "dap_adapters", "db", "debugger_tools", "editor", @@ -7797,6 +7796,7 @@ dependencies = [ "tree-sitter-html", "tree-sitter-json", "tree-sitter-md", + "tree-sitter-python", "tree-sitter-ruby", "tree-sitter-rust", "tree-sitter-typescript", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index c5e7fec0c1..531276e708 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -36,7 +36,6 @@ gpui.workspace = true http_client.workspace = true language.workspace = true log.workspace = true -lsp-types.workspace = true node_runtime.workspace = true parking_lot.workspace = true paths.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 3cd8bb9fb7..485e5f1b4c 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -402,10 +402,6 @@ pub async fn fetch_latest_adapter_version_from_github( }) } -pub trait InlineValueProvider { - fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec; -} - #[async_trait(?Send)] pub trait DebugAdapter: 'static + Send + Sync { fn name(&self) -> DebugAdapterName; @@ -417,10 +413,6 @@ pub trait DebugAdapter: 'static + Send + Sync { user_installed_path: Option, cx: &mut AsyncApp, ) -> Result; - - fn inline_value_provider(&self) -> Option> { - None - } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/dap/src/dap.rs b/crates/dap/src/dap.rs index 8e06396d6b..df8d915812 100644 --- a/crates/dap/src/dap.rs +++ b/crates/dap/src/dap.rs @@ -1,6 +1,7 @@ pub mod adapters; pub mod client; pub mod debugger_settings; +pub mod inline_value; pub mod proto_conversions; mod registry; pub mod transport; diff --git a/crates/dap/src/inline_value.rs b/crates/dap/src/inline_value.rs new file mode 100644 index 0000000000..7204d985aa --- /dev/null +++ b/crates/dap/src/inline_value.rs @@ -0,0 +1,277 @@ +use std::collections::{HashMap, HashSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VariableLookupKind { + Variable, + Expression, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VariableScope { + Local, + Global, +} + +#[derive(Debug, Clone)] +pub struct InlineValueLocation { + pub variable_name: String, + pub scope: VariableScope, + pub lookup: VariableLookupKind, + 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 { + /// 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 + } +} diff --git a/crates/dap/src/registry.rs b/crates/dap/src/registry.rs index c631452b8c..a03be4860d 100644 --- a/crates/dap/src/registry.rs +++ b/crates/dap/src/registry.rs @@ -5,7 +5,10 @@ use gpui::{App, Global, SharedString}; use parking_lot::RwLock; use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate}; -use crate::adapters::{DebugAdapter, DebugAdapterName}; +use crate::{ + adapters::{DebugAdapter, DebugAdapterName}, + inline_value::InlineValueProvider, +}; use std::{collections::BTreeMap, sync::Arc}; /// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user. @@ -22,6 +25,7 @@ pub trait DapLocator: Send + Sync { struct DapRegistryState { adapters: BTreeMap>, locators: FxHashMap>, + inline_value_providers: FxHashMap>, } #[derive(Clone, Default)] @@ -58,6 +62,22 @@ impl DapRegistry { ); } + 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() } @@ -66,6 +86,10 @@ 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/Cargo.toml b/crates/dap_adapters/Cargo.toml index 794db55feb..77dbe40088 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -27,7 +27,6 @@ dap.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true -lsp-types.workspace = true paths.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index 8acfec1b5e..e0c585e466 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; use anyhow::Result; use async_trait::async_trait; -use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release}; +use dap::adapters::{DebugTaskDefinition, latest_github_release}; use futures::StreamExt; use gpui::AsyncApp; use task::DebugRequest; @@ -159,25 +159,4 @@ impl DebugAdapter for CodeLldbDebugAdapter { connection: None, }) } - - fn inline_value_provider(&self) -> Option> { - Some(Box::new(CodeLldbInlineValueProvider)) - } -} - -struct CodeLldbInlineValueProvider; - -impl InlineValueProvider for CodeLldbInlineValueProvider { - fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec { - variables - .into_iter() - .map(|(variable, range)| { - lsp_types::InlineValue::VariableLookup(lsp_types::InlineValueVariableLookup { - range, - variable_name: Some(variable), - case_sensitive_lookup: true, - }) - }) - .collect() - } } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 93af93c6b9..4c1ed025ad 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -16,6 +16,7 @@ use dap::{ self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, GithubRepo, }, + inline_value::{PythonInlineValueProvider, RustInlineValueProvider}, }; use gdb::GdbDebugAdapter; use go::GoDebugAdapter; @@ -34,6 +35,10 @@ pub fn init(cx: &mut App) { registry.add_adapter(Arc::from(JsDebugAdapter::default())); registry.add_adapter(Arc::from(GoDebugAdapter)); registry.add_adapter(Arc::from(GdbDebugAdapter)); + + registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider)); + registry + .add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider)); }) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index f1eb24c6e8..5c0d805ba4 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,8 +1,5 @@ use crate::*; -use dap::{ - DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition, - adapters::InlineValueProvider, -}; +use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use gpui::AsyncApp; use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock}; use util::ResultExt; @@ -182,34 +179,4 @@ impl DebugAdapter for PythonDebugAdapter { self.get_installed_binary(delegate, &config, user_installed_path, cx) .await } - - fn inline_value_provider(&self) -> Option> { - Some(Box::new(PythonInlineValueProvider)) - } -} - -struct PythonInlineValueProvider; - -impl InlineValueProvider for PythonInlineValueProvider { - fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec { - variables - .into_iter() - .map(|(variable, range)| { - if variable.contains(".") || variable.contains("[") { - lsp_types::InlineValue::EvaluatableExpression( - lsp_types::InlineValueEvaluatableExpression { - range, - expression: Some(variable), - }, - ) - } else { - lsp_types::InlineValue::VariableLookup(lsp_types::InlineValueVariableLookup { - range, - variable_name: Some(variable), - case_sensitive_lookup: true, - }) - } - }) - .collect() - } } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index a195b10801..295d18e4b3 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -15,6 +15,7 @@ doctest = false [features] test-support = [ "dap/test-support", + "dap_adapters/test-support", "editor/test-support", "gpui/test-support", "project/test-support", @@ -31,6 +32,7 @@ client.workspace = true collections.workspace = true command_palette_hooks.workspace = true dap.workspace = true +dap_adapters = { workspace = true, optional = true } db.workspace = true editor.workspace = true feature_flags.workspace = true @@ -63,6 +65,7 @@ unindent = { workspace = true, optional = true } [dev-dependencies] dap = { workspace = true, features = ["test-support"] } +dap_adapters = { workspace = true, features = ["test-support"] } debugger_tools = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index c99ccc12ea..e8e9beef35 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -21,6 +21,8 @@ mod dap_logger; #[cfg(test)] mod debugger_panel; #[cfg(test)] +mod inline_values; +#[cfg(test)] mod module_list; #[cfg(test)] mod persistence; @@ -45,6 +47,7 @@ pub fn init_test(cx: &mut gpui::TestAppContext) { Project::init_settings(cx); editor::init(cx); crate::init(cx); + dap_adapters::init(cx); }); } diff --git a/crates/debugger_ui/src/tests/inline_values.rs b/crates/debugger_ui/src/tests/inline_values.rs new file mode 100644 index 0000000000..76b9436080 --- /dev/null +++ b/crates/debugger_ui/src/tests/inline_values.rs @@ -0,0 +1,1859 @@ +use std::{path::Path, sync::Arc}; + +use dap::{Scope, StackFrame, Variable, requests::Variables}; +use editor::{Editor, EditorMode, MultiBuffer, actions::ToggleInlineValues}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_python, tree_sitter_rust}; +use project::{FakeFs, Project}; +use serde_json::json; +use unindent::Unindent as _; +use util::path; + +use crate::{ + debugger_panel::DebugPanel, + tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session}, +}; + +#[gpui::test] +async fn test_rust_inline_values(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + fn stack_frame_for_line(line: u64) -> dap::StackFrame { + StackFrame { + id: 1, + name: "Stack Frame 1".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, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + } + } + + let fs = FakeFs::new(executor.clone()); + let source_code = r#" +static mut GLOBAL: usize = 1; + +fn main() { + let x = 10; + let value = 42; + let y = 4; + let tester = { + let y = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); +} +"# + .unindent(); + fs.insert_tree(path!("/project"), json!({ "main.rs": source_code })) + .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::(move |_, _| { + Ok(dap::ThreadsResponse { + threads: vec![dap::Thread { + id: 1, + name: "Thread 1".into(), + }], + }) + }); + + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(4)], + total_frames: None, + }) + }); + + client.on_request::(move |_, args| { + assert_eq!("GLOBAL", args.expression); + Ok(dap::EvaluateResponse { + result: "1".into(), + type_: None, + presentation_hint: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + value_location_reference: None, + }) + }); + + let local_variables = vec![ + Variable { + name: "x".into(), + value: "10".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, + }, + Variable { + name: "y".into(), + value: "4".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, + }, + Variable { + name: "value".into(), + value: "42".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, + }, + ]; + + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + + client.on_request::(move |_, _| { + Ok(dap::ScopesResponse { + scopes: vec![Scope { + name: "Locale".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, + }], + }) + }); + + 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(rust_lang())), 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_in(cx, |editor, window, cx| { + if !editor.inline_values_enabled() { + editor.toggle_inline_values(&ToggleInlineValues, window, cx); + } + }); + + cx.run_until_parked(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x = 10; + let value = 42; + let y = 4; + let tester = { + let y = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(5)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x: 10 = 10; + let value = 42; + let y = 4; + let tester = { + let y = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(6)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x: 10 = 10; + let value: 42 = 42; + let y = 4; + let tester = { + let y = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(7)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x: 10 = 10; + let value: 42 = 42; + let y: 4 = 4; + let tester = { + let y = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(8)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x: 10 = 10; + let value: 42 = 42; + let y: 4 = 4; + let tester = { + let y = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + let local_variables = vec![ + Variable { + name: "x".into(), + value: "10".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, + }, + Variable { + name: "y".into(), + value: "10".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, + }, + Variable { + name: "value".into(), + value: "42".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, + }, + ]; + + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(9)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x: 10 = 10; + let value: 42 = 42; + let y = 4; + let tester = { + let y: 10 = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + let local_variables = vec![ + Variable { + name: "x".into(), + value: "10".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, + }, + Variable { + name: "y".into(), + value: "5".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, + }, + Variable { + name: "value".into(), + value: "42".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, + }, + ]; + + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(10)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x: 10 = 10; + let value: 42 = 42; + let y = 4; + let tester = { + let y = 10; + let y: 5 = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + let local_variables = vec![ + Variable { + name: "x".into(), + value: "10".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, + }, + Variable { + name: "y".into(), + value: "5".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, + }, + Variable { + name: "value".into(), + value: "42".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, + }, + Variable { + name: "b".into(), + value: "3".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, + }, + ]; + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(11)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x: 10 = 10; + let value: 42 = 42; + let y = 4; + let tester = { + let y = 10; + let y: 5 = 5; + let b: 3 = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + let local_variables = vec![ + Variable { + name: "x".into(), + value: "10".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, + }, + Variable { + name: "y".into(), + value: "4".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, + }, + Variable { + name: "value".into(), + value: "42".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, + }, + Variable { + name: "tester".into(), + value: "size=3".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, + }, + ]; + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(14)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + 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; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + let local_variables = vec![ + Variable { + name: "x".into(), + value: "10".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, + }, + Variable { + name: "y".into(), + value: "4".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, + }, + Variable { + name: "value".into(), + value: "42".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, + }, + Variable { + name: "tester".into(), + value: "size=3".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, + }, + Variable { + name: "caller".into(), + 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, + }, + ]; + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(19)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + 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; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(15)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x = 10; + let value = 42; + let y = 4; + let tester = { + let y = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + let local_variables = vec![Variable { + name: "x".into(), + value: "3".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, + }]; + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(16)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 1: usize = 1; + + fn main() { + let x = 10; + let value = 42; + let y = 4; + let tester = { + let y = 10; + let y = 5; + let b = 3; + vec![y, 20, 30] + }; + + let caller = || { + let x: 3 = 3; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + let local_variables = vec![ + Variable { + name: "x".into(), + value: "10".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, + }, + Variable { + name: "y".into(), + value: "4".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, + }, + Variable { + name: "value".into(), + value: "42".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, + }, + Variable { + name: "tester".into(), + value: "size=3".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, + }, + Variable { + name: "caller".into(), + 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, + }, + ]; + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + client.on_request::(move |_, args| { + assert_eq!("GLOBAL", args.expression); + Ok(dap::EvaluateResponse { + result: "2".into(), + type_: None, + presentation_hint: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + value_location_reference: None, + }) + }); + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(25)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 2: usize = 1; + + fn main() { + 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; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); + + let local_variables = vec![ + Variable { + name: "x".into(), + value: "10".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, + }, + Variable { + name: "y".into(), + value: "4".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, + }, + Variable { + name: "value".into(), + value: "42".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, + }, + Variable { + name: "tester".into(), + value: "size=3".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, + }, + Variable { + name: "caller".into(), + 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, + }, + Variable { + name: "result".into(), + value: "840".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, + }, + ]; + client.on_request::({ + let local_variables = Arc::new(local_variables.clone()); + move |_, _| { + Ok(dap::VariablesResponse { + variables: (*local_variables).clone(), + }) + } + }); + client.on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: vec![stack_frame_for_line(26)], + total_frames: 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + static mut GLOBAL: 2: usize = 1; + + fn main() { + 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; + println!("x={}", x); + }; + + caller(); + + unsafe { + GLOBAL = 2; + } + + let result: 840 = value * 2 * x; + println!("Simple test executed: value={}, result={}", value, result); + assert!(true); + } + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); +} + +fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) +} + +#[gpui::test] +async fn test_python_inline_values(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + let source_code = r#" +def process_data(untyped_param, typed_param: int, another_typed: str): + # Local variables + x = 10 + result = typed_param * 2 + text = "Hello, " + another_typed + + # For loop with range + sum_value = 0 + for i in range(5): + sum_value += i + + # Final result + final_result = x + result + sum_value + return final_result +"# + .unindent(); + fs.insert_tree(path!("/project"), json!({ "main.py": source_code })) + .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()); + + 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.py"), cx) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(Arc::new(python_lang())), cx); + }); + + let (editor, cx) = cx.add_window_view(|window, cx| { + Editor::new( + EditorMode::full(), + MultiBuffer::build_from_buffer(buffer, cx), + Some(project), + window, + cx, + ) + }); + + editor.update_in(cx, |editor, window, cx| { + if !editor.inline_values_enabled() { + editor.toggle_inline_values(&ToggleInlineValues, window, cx); + } + }); + + client.on_request::(move |_, _| { + Ok(dap::ThreadsResponse { + threads: vec![dap::Thread { + id: 1, + name: "Thread 1".into(), + }], + }) + }); + + client.on_request::(move |_, args| { + assert_eq!(args.thread_id, 1); + Ok(dap::StackTraceResponse { + stack_frames: vec![StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("main.py".into()), + path: Some(path!("/project/main.py").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 12, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }], + total_frames: None, + }) + }); + + client.on_request::(move |_, _| { + Ok(dap::ScopesResponse { + scopes: vec![ + Scope { + name: "Local".into(), + presentation_hint: None, + variables_reference: 1, + 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: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + ], + }) + }); + + client.on_request::(move |_, args| match args.variables_reference { + 1 => Ok(dap::VariablesResponse { + variables: vec![ + Variable { + name: "untyped_param".into(), + value: "test_value".into(), + type_: Some("str".into()), + 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, + }, + Variable { + name: "typed_param".into(), + value: "42".into(), + type_: Some("int".into()), + 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, + }, + Variable { + name: "another_typed".into(), + value: "world".into(), + type_: Some("str".into()), + 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, + }, + Variable { + name: "x".into(), + value: "10".into(), + type_: Some("int".into()), + 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, + }, + Variable { + name: "result".into(), + value: "84".into(), + type_: Some("int".into()), + 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, + }, + Variable { + name: "text".into(), + value: "Hello, world".into(), + type_: Some("str".into()), + 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, + }, + Variable { + name: "sum_value".into(), + value: "10".into(), + type_: Some("int".into()), + 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, + }, + Variable { + name: "i".into(), + value: "4".into(), + type_: Some("int".into()), + 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, + }, + Variable { + name: "final_result".into(), + value: "104".into(), + type_: Some("int".into()), + 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, + }, + ], + }), + _ => Ok(dap::VariablesResponse { variables: vec![] }), + }); + + 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(); + + editor.update_in(cx, |editor, window, cx| { + pretty_assertions::assert_eq!( + r#" + 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 + + # For loop with range + sum_value: 10 = 0 + for i: 4 in range(5): + sum_value += i + + # Final result + final_result = x + result + sum_value + return final_result + "# + .unindent(), + editor.snapshot(window, cx).text() + ); + }); +} + +fn python_lang() -> Language { + Language::new( + LanguageConfig { + name: "Python".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["py".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_python::LANGUAGE.into()), + ) +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1c3318b089..16b259629e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4333,6 +4333,16 @@ impl Editor { self.inline_value_cache.enabled } + #[cfg(any(test, feature = "test-support"))] + pub fn inline_value_inlays(&self, cx: &App) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_))) + .cloned() + .collect() + } + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { if self.semantics_provider.is_none() || !self.mode.is_full() { return; @@ -17665,7 +17675,7 @@ impl Editor { } } - fn refresh_inline_values(&mut self, cx: &mut Context) { + pub fn refresh_inline_values(&mut self, cx: &mut Context) { let Some(project) = self.project.clone() else { return; }; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index cc5a3a2972..407573513d 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ "lsp/test-support", "text/test-support", "tree-sitter-rust", + "tree-sitter-python", "tree-sitter-typescript", "settings/test-support", "util/test-support", @@ -58,6 +59,7 @@ sum_tree.workspace = true task.workspace = true text.workspace = true theme.workspace = true +tree-sitter-python = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } tree-sitter.workspace = true @@ -76,15 +78,16 @@ pretty_assertions.workspace = true rand.workspace = true settings = { workspace = true, features = ["test-support"] } text = { workspace = true, features = ["test-support"] } +http_client = { workspace = true, features = ["test-support"] } tree-sitter-elixir.workspace = true tree-sitter-embedded-template.workspace = true tree-sitter-heex.workspace = true tree-sitter-html.workspace = true tree-sitter-json.workspace = true tree-sitter-md.workspace = true +tree-sitter-python.workspace = true tree-sitter-ruby.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true unindent.workspace = true util = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b40d889d17..700157496e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,6 +1,12 @@ +pub use crate::{ + Grammar, Language, LanguageRegistry, + diagnostic_set::DiagnosticSet, + highlight_map::{HighlightId, HighlightMap}, + proto, +}; use crate::{ - DebugVariableCapture, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, - TextObject, TreeSitterOptions, + LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject, + TreeSitterOptions, diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{LanguageSettings, language_settings}, outline::OutlineItem, @@ -11,12 +17,6 @@ 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, anyhow}; use async_watch as watch; pub use clock::ReplicaId; @@ -69,16 +69,10 @@ use util::RandomCharIter; use util::{RangeExt, debug_panic, maybe}; #[cfg(any(test, feature = "test-support"))] -pub use {tree_sitter_rust, tree_sitter_typescript}; +pub use {tree_sitter_python, tree_sitter_rust, tree_sitter_typescript}; pub use lsp::DiagnosticSeverity; -#[derive(Debug)] -pub struct DebugVariableRanges { - pub buffer_id: BufferId, - pub range: Range, -} - /// A label for the background task spawned by the buffer to compute /// a diff against the contents of its file. pub static BUFFER_DIFF_TASK: LazyLock = LazyLock::new(TaskLabel::new); @@ -3377,6 +3371,36 @@ impl BufferSnapshot { result } + /// Returns the root syntax node within the given row + pub fn syntax_root_ancestor(&self, position: Anchor) -> Option { + let start_offset = position.to_offset(self); + + let row = self.summary_for_anchor::(&position).row as usize; + + let layer = self + .syntax + .layers_for_range(start_offset..start_offset, &self.text, true) + .next()?; + + let mut cursor = layer.node().walk(); + + // Descend to the first leaf that touches the start of the range. + while cursor.goto_first_child_for_byte(start_offset).is_some() { + if cursor.node().end_byte() == start_offset { + cursor.goto_next_sibling(); + } + } + + // Ascend to the root node within the same row. + while cursor.goto_parent() { + if cursor.node().start_position().row != row { + break; + } + } + + return Some(cursor.node()); + } + /// Returns the outline for the buffer. /// /// This method allows passing an optional [`SyntaxTheme`] to @@ -3938,79 +3962,6 @@ impl BufferSnapshot { }) } - pub fn debug_variable_ranges( - &self, - offset_range: Range, - ) -> impl Iterator + '_ { - let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| { - grammar - .debug_variables_config - .as_ref() - .map(|config| &config.query) - }); - - let configs = syntax_matches - .grammars() - .iter() - .map(|grammar| grammar.debug_variables_config.as_ref()) - .collect::>(); - - iter::from_fn(move || { - loop { - let mat = syntax_matches.peek()?; - - let variable_ranges = configs[mat.grammar_index].and_then(|config| { - let full_range = mat.captures.iter().fold( - Range { - start: usize::MAX, - end: 0, - }, - |mut acc, next| { - let byte_range = next.node.byte_range(); - if acc.start > byte_range.start { - acc.start = byte_range.start; - } - if acc.end < byte_range.end { - acc.end = byte_range.end; - } - acc - }, - ); - if full_range.start > full_range.end { - // We did not find a full spanning range of this match. - return None; - } - - let captures = mat.captures.iter().filter_map(|capture| { - Some(( - capture, - config.captures.get(capture.index as usize).cloned()?, - )) - }); - - let mut variable_range = None; - for (query, capture) in captures { - if let DebugVariableCapture::Variable = capture { - let _ = variable_range.insert(query.node.byte_range()); - } - } - - Some(DebugVariableRanges { - buffer_id: self.remote_id(), - range: variable_range?, - }) - }); - - syntax_matches.advance(); - if variable_ranges.is_some() { - // It's fine for us to short-circuit on .peek()? returning None. We don't want to return None from this iter if we - // had a capture that did not contain a run marker, hence we'll just loop around for the next capture. - return variable_ranges; - } - } - }) - } - pub fn runnable_ranges( &self, offset_range: Range, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b7252b13cf..93abc6f49d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1044,7 +1044,6 @@ pub struct Grammar { pub(crate) brackets_config: Option, pub(crate) redactions_config: Option, pub(crate) runnable_config: Option, - pub(crate) debug_variables_config: Option, pub(crate) indents_config: Option, pub outline_config: Option, pub text_object_config: Option, @@ -1145,18 +1144,6 @@ struct RunnableConfig { pub extra_captures: Vec, } -#[derive(Clone, Debug, PartialEq)] -enum DebugVariableCapture { - Named(SharedString), - Variable, -} - -#[derive(Debug)] -struct DebugVariablesConfig { - pub query: Query, - pub captures: Vec, -} - struct OverrideConfig { query: Query, values: HashMap, @@ -1217,7 +1204,6 @@ impl Language { override_config: None, redactions_config: None, runnable_config: None, - debug_variables_config: None, error_query: Query::new(&ts_language, "(ERROR) @error").ok(), ts_language, highlight_map: Default::default(), @@ -1289,11 +1275,6 @@ impl Language { .with_text_object_query(query.as_ref()) .context("Error loading textobject query")?; } - if let Some(query) = queries.debug_variables { - self = self - .with_debug_variables_query(query.as_ref()) - .context("Error loading debug variable query")?; - } Ok(self) } @@ -1389,25 +1370,6 @@ impl Language { Ok(self) } - pub fn with_debug_variables_query(mut self, source: &str) -> Result { - let grammar = self - .grammar_mut() - .ok_or_else(|| anyhow!("cannot mutate grammar"))?; - let query = Query::new(&grammar.ts_language, source)?; - - let mut captures = Vec::new(); - for name in query.capture_names() { - captures.push(if *name == "debug_variable" { - DebugVariableCapture::Variable - } else { - DebugVariableCapture::Named(name.to_string().into()) - }); - } - grammar.debug_variables_config = Some(DebugVariablesConfig { query, captures }); - - Ok(self) - } - pub fn with_embedding_query(mut self, source: &str) -> Result { let grammar = self .grammar_mut() diff --git a/crates/languages/src/python/debug_variables.scm b/crates/languages/src/python/debug_variables.scm deleted file mode 100644 index a434e07bf0..0000000000 --- a/crates/languages/src/python/debug_variables.scm +++ /dev/null @@ -1,5 +0,0 @@ -(assignment - left: (identifier) @debug_variable) - -(function_definition - parameters: (parameters (identifier) @debug_variable)) diff --git a/crates/languages/src/rust/debug_variables.scm b/crates/languages/src/rust/debug_variables.scm deleted file mode 100644 index 9ef51035ef..0000000000 --- a/crates/languages/src/rust/debug_variables.scm +++ /dev/null @@ -1,3 +0,0 @@ -(let_declaration pattern: (identifier) @debug_variable) - -(parameter (identifier) @debug_variable) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 965e0c6e7e..cc9c185e35 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -18,6 +18,7 @@ use dap::{ EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId, adapters::{DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments}, client::SessionId, + inline_value::VariableLookupKind, messages::Message, requests::{Completions, Evaluate}, }; @@ -29,7 +30,7 @@ use futures::{ }; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; use http_client::HttpClient; -use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind, range_from_lsp}; +use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; use remote::SshRemoteClient; @@ -564,56 +565,37 @@ impl DapStore { }) } - pub fn resolve_inline_values( + pub fn resolve_inline_value_locations( &self, session: Entity, stack_frame_id: StackFrameId, buffer_handle: Entity, - inline_values: Vec, + inline_value_locations: Vec, 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); cx.spawn(async move |_, cx| { - let mut inlay_hints = Vec::with_capacity(inline_values.len()); - for inline_value in inline_values.iter() { - match inline_value { - lsp::InlineValue::Text(text) => { - inlay_hints.push(InlayHint { - position: snapshot.anchor_after(range_from_lsp(text.range).end), - label: InlayHintLabel::String(format!(": {}", text.text)), - kind: Some(InlayHintKind::Type), - padding_left: false, - padding_right: false, - tooltip: None, - resolve_state: ResolveState::Resolved, - }); - } - lsp::InlineValue::VariableLookup(variable_lookup) => { - let range = range_from_lsp(variable_lookup.range); + let mut inlay_hints = Vec::with_capacity(inline_value_locations.len()); + for inline_value_location in inline_value_locations.iter() { + let point = snapshot.point_to_point_utf16(language::Point::new( + inline_value_location.row as u32, + inline_value_location.column as u32, + )); + let position = snapshot.anchor_after(point); - let mut variable_name = variable_lookup - .variable_name - .clone() - .unwrap_or_else(|| snapshot.text_for_range(range.clone()).collect()); - - if !variable_lookup.case_sensitive_lookup { - variable_name = variable_name.to_ascii_lowercase(); - } - - let Some(variable) = all_variables.iter().find(|variable| { - if variable_lookup.case_sensitive_lookup { - variable.name == variable_name - } else { - variable.name.to_ascii_lowercase() == variable_name - } - }) else { + match inline_value_location.lookup { + VariableLookupKind::Variable => { + let Some(variable) = all_variables + .iter() + .find(|variable| variable.name == inline_value_location.variable_name) + else { continue; }; inlay_hints.push(InlayHint { - position: snapshot.anchor_after(range.end), + position, label: InlayHintLabel::String(format!(": {}", variable.value)), kind: Some(InlayHintKind::Type), padding_left: false, @@ -622,17 +604,10 @@ impl DapStore { resolve_state: ResolveState::Resolved, }); } - lsp::InlineValue::EvaluatableExpression(expression) => { - let range = range_from_lsp(expression.range); - - let expression = expression - .expression - .clone() - .unwrap_or_else(|| snapshot.text_for_range(range.clone()).collect()); - + VariableLookupKind::Expression => { let Ok(eval_task) = session.update(cx, |session, _| { session.mode.request_dap(EvaluateCommand { - expression, + expression: inline_value_location.variable_name.clone(), frame_id: Some(stack_frame_id), source: None, context: Some(EvaluateArgumentsContext::Variables), @@ -643,7 +618,7 @@ impl DapStore { if let Some(response) = eval_task.await.log_err() { inlay_hints.push(InlayHint { - position: snapshot.anchor_after(range.end), + position, label: InlayHintLabel::String(format!(": {}", response.result)), kind: Some(InlayHintKind::Type), padding_left: false, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 38bfb084c2..9a5be5e09a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3562,52 +3562,43 @@ impl Project { range: Range, cx: &mut Context, ) -> Task>> { - let snapshot = buffer_handle.read(cx).snapshot(); + let language_name = buffer_handle + .read(cx) + .language() + .map(|language| language.name().to_string()); - let adapter = session.read(cx).adapter(); - let Some(inline_value_provider) = DapRegistry::global(cx) - .adapter(&adapter) - .and_then(|adapter| adapter.inline_value_provider()) + 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 mut text_objects = - snapshot.text_object_ranges(range.end..range.end, Default::default()); - let text_object_range = text_objects - .find(|(_, obj)| matches!(obj, language::TextObject::AroundFunction)) - .map(|(range, _)| snapshot.anchor_before(range.start)) - .unwrap_or(range.start); + let snapshot = buffer_handle.read(cx).snapshot(); - let variable_ranges = snapshot - .debug_variable_ranges( - text_object_range.to_offset(&snapshot)..range.end.to_offset(&snapshot), - ) - .filter_map(|range| { - let lsp_range = language::range_to_lsp( - range.range.start.to_point_utf16(&snapshot) - ..range.range.end.to_point_utf16(&snapshot), - ) - .ok()?; + let root_node = snapshot.syntax_root_ancestor(range.end).unwrap(); - Some(( - snapshot.text_for_range(range.range).collect::(), - lsp_range, - )) - }) - .collect::>(); + let row = snapshot + .summary_for_anchor::(&range.end) + .row as usize; - let inline_values = inline_value_provider.provide(variable_ranges); + let inline_value_locations = inline_value_provider.provide( + root_node, + snapshot + .text_for_range(Anchor::MIN..range.end) + .collect::() + .as_str(), + row, + ); let stack_frame_id = active_stack_frame.stack_frame_id; cx.spawn(async move |this, cx| { this.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.resolve_inline_values( + dap_store.resolve_inline_value_locations( session, stack_frame_id, buffer_handle, - inline_values, + inline_value_locations, cx, ) })