From 218496744ce03c040438907bdb632aa37ced118f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 24 Apr 2025 00:27:27 +0200 Subject: [PATCH] debugger: Add support for inline value hints (#28656) This PR uses Tree Sitter to show inline values while a user is in a debug session. We went with Tree Sitter over the LSP Inline Values request because the LSP request isn't widely supported. Tree Sitter is easy for languages/extensions to add support to. Tree Sitter can compute the inline values locally, so there's no need to add extra RPC messages for Collab. Tree Sitter also gives Zed more control over how we want to show variables. There's still more work to be done after this PR, namely differentiating between global/local scoped variables, but it's a great starting point to start iteratively improving it. Release Notes: - N/A --------- Co-authored-by: Piotr Osiewicz Co-authored-by: Anthony Eid Co-authored-by: Cole Miller Co-authored-by: Anthony Co-authored-by: Kirill --- Cargo.lock | 4 +- Cargo.toml | 1 + crates/collab/src/tests/editor_tests.rs | 4 + crates/dap/Cargo.toml | 1 + crates/dap/src/adapters.rs | 9 + crates/dap_adapters/Cargo.toml | 1 + crates/dap_adapters/src/codelldb.rs | 23 +- crates/dap_adapters/src/python.rs | 32 ++- crates/debugger_ui/src/debugger_ui.rs | 2 +- .../src/session/running/console.rs | 18 +- .../src/session/running/stack_frame_list.rs | 14 +- crates/editor/src/actions.rs | 1 + crates/editor/src/display_map/inlay_map.rs | 9 + crates/editor/src/editor.rs | 203 ++++++++++++++++-- crates/editor/src/hover_links.rs | 1 + crates/editor/src/hover_popover.rs | 1 + crates/editor/src/inlay_hint_cache.rs | 18 ++ crates/editor/src/proposed_changes_editor.rs | 9 + crates/language/src/buffer.rs | 95 +++++++- crates/language/src/language.rs | 38 ++++ crates/language/src/language_registry.rs | 2 + crates/language/src/language_settings.rs | 5 + .../languages/src/python/debug_variables.scm | 5 + crates/languages/src/rust/debug_variables.scm | 3 + crates/lsp/Cargo.toml | 2 +- .../project/src/debugger/breakpoint_store.rs | 26 ++- crates/project/src/debugger/dap_store.rs | 106 ++++++++- crates/project/src/debugger/session.rs | 28 ++- crates/project/src/project.rs | 79 ++++++- crates/zed/src/zed/quick_action_bar.rs | 23 ++ 30 files changed, 709 insertions(+), 54 deletions(-) create mode 100644 crates/languages/src/python/debug_variables.scm create mode 100644 crates/languages/src/rust/debug_variables.scm diff --git a/Cargo.lock b/Cargo.lock index e1a99c19c2..9f24b3d82c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4039,6 +4039,7 @@ dependencies = [ "http_client", "language", "log", + "lsp-types", "node_runtime", "parking_lot", "paths", @@ -4073,6 +4074,7 @@ dependencies = [ "dap", "gpui", "language", + "lsp-types", "paths", "serde", "serde_json", @@ -8363,7 +8365,7 @@ dependencies = [ [[package]] name = "lsp-types" version = "0.95.1" -source = "git+https://github.com/zed-industries/lsp-types?rev=1fff0dd12e2071c5667327394cfec163d2a466ab#1fff0dd12e2071c5667327394cfec163d2a466ab" +source = "git+https://github.com/zed-industries/lsp-types?rev=c9c189f1c5dd53c624a419ce35bc77ad6a908d18#c9c189f1c5dd53c624a419ce35bc77ad6a908d18" dependencies = [ "bitflags 1.3.2", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4dcd268965..ba9fc3673d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -296,6 +296,7 @@ livekit_api = { path = "crates/livekit_api" } livekit_client = { path = "crates/livekit_client" } lmstudio = { path = "crates/lmstudio" } lsp = { path = "crates/lsp" } +lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" } markdown = { path = "crates/markdown" } markdown_preview = { path = "crates/markdown_preview" } media = { path = "crates/media" } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 719b8643f2..ec66e593d5 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1544,6 +1544,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + show_value_hints: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, show_type_hints: true, @@ -1559,6 +1560,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -1778,6 +1780,7 @@ async fn test_inlay_hint_refresh_is_forwarded( SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: false, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -1794,6 +1797,7 @@ async fn test_inlay_hint_refresh_is_forwarded( SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 531276e708..c5e7fec0c1 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -36,6 +36,7 @@ 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 7b4294dccd..1b927d0051 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -284,6 +284,10 @@ 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; @@ -373,7 +377,12 @@ 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"))] pub struct FakeAdapter {} diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 0a11724aa2..ba461c3e68 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -26,6 +26,7 @@ async-trait.workspace = true dap.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 e1b34e2d73..e6e27b25b6 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, bail}; use async_trait::async_trait; -use dap::adapters::latest_github_release; +use dap::adapters::{InlineValueProvider, latest_github_release}; use gpui::AsyncApp; use task::{DebugRequest, DebugTaskDefinition}; @@ -150,4 +150,25 @@ 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/python.rs b/crates/dap_adapters/src/python.rs index 3c3f587ecc..cde4df9109 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,5 +1,5 @@ use crate::*; -use dap::{DebugRequest, StartDebuggingRequestArguments}; +use dap::{StartDebuggingRequestArguments, adapters::InlineValueProvider}; use gpui::AsyncApp; use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; use task::DebugTaskDefinition; @@ -160,4 +160,34 @@ impl DebugAdapter for PythonDebugAdapter { request_args: self.request_args(config), }) } + + 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/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index ce3300de5f..1630e27d23 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -247,7 +247,7 @@ pub fn init(cx: &mut App) { let stack_id = state.selected_stack_frame_id(cx); state.session().update(cx, |session, cx| { - session.evaluate(text, None, stack_id, None, cx); + session.evaluate(text, None, stack_id, None, cx).detach(); }); }); Some(()) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index ad765496c2..86be2269e4 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -141,14 +141,16 @@ impl Console { expression }); - self.session.update(cx, |state, cx| { - state.evaluate( - expression, - Some(dap::EvaluateArgumentsContext::Variables), - self.stack_frame_list.read(cx).selected_stack_frame_id(), - None, - cx, - ); + self.session.update(cx, |session, cx| { + session + .evaluate( + expression, + Some(dap::EvaluateArgumentsContext::Variables), + self.stack_frame_list.read(cx).selected_stack_frame_id(), + None, + cx, + ) + .detach(); }); } diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index c9e7a9b11c..b60262f2d7 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -10,6 +10,7 @@ use gpui::{ }; use language::PointUtf16; +use project::debugger::breakpoint_store::ActiveStackFrame; use project::debugger::session::{Session, SessionEvent, StackFrame}; use project::{ProjectItem, ProjectPath}; use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*}; @@ -265,6 +266,7 @@ impl StackFrameList { return Task::ready(Err(anyhow!("Project path not found"))); }; + let stack_frame_id = stack_frame.id; cx.spawn_in(window, async move |this, cx| { let (worktree, relative_path) = this .update(cx, |this, cx| { @@ -313,12 +315,22 @@ impl StackFrameList { .await?; this.update(cx, |this, cx| { + let Some(thread_id) = this.state.read_with(cx, |state, _| state.thread_id)? else { + return Err(anyhow!("No selected thread ID found")); + }; + this.workspace.update(cx, |workspace, cx| { let breakpoint_store = workspace.project().read(cx).breakpoint_store(); breakpoint_store.update(cx, |store, cx| { store.set_active_position( - (this.session.read(cx).session_id(), abs_path, position), + ActiveStackFrame { + session_id: this.session.read(cx).session_id(), + thread_id, + stack_frame_id, + path: abs_path, + position, + }, cx, ); }) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 9726740f17..d79461641d 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -419,6 +419,7 @@ actions!( OpenGitBlameCommit, ToggleIndentGuides, ToggleInlayHints, + ToggleInlineValues, ToggleInlineDiagnostics, ToggleEditPrediction, ToggleLineNumbers, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 29a0dcd273..ec3bc4865c 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -64,6 +64,14 @@ impl Inlay { text: text.into(), } } + + pub fn debugger_hint>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::DebuggerValue(id), + position, + text: text.into(), + } + } } impl sum_tree::Item for Transform { @@ -287,6 +295,7 @@ impl<'a> Iterator for InlayChunks<'a> { }) } InlayId::Hint(_) => self.highlight_styles.inlay_hint, + InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint, }; let next_inlay_highlight_endpoint; let offset_in_inlay = self.output_offset - self.transforms.start().0; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3a98b5950d..aae7a6a859 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -121,8 +121,11 @@ use mouse_context_menu::MouseContextMenu; use persistence::DB; use project::{ ProjectPath, - debugger::breakpoint_store::{ - BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent, + debugger::{ + breakpoint_store::{ + BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent, + }, + session::{Session, SessionEvent}, }, }; @@ -248,10 +251,27 @@ const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers { function: false, }; +struct InlineValueCache { + enabled: bool, + inlays: Vec, + refresh_task: Task>, +} + +impl InlineValueCache { + fn new(enabled: bool) -> Self { + Self { + enabled, + inlays: Vec::new(), + refresh_task: Task::ready(None), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InlayId { InlineCompletion(usize), Hint(usize), + DebuggerValue(usize), } impl InlayId { @@ -259,6 +279,7 @@ impl InlayId { match self { Self::InlineCompletion(id) => *id, Self::Hint(id) => *id, + Self::DebuggerValue(id) => *id, } } } @@ -923,6 +944,7 @@ pub struct Editor { mouse_cursor_hidden: bool, hide_mouse_mode: HideMouseMode, pub change_list: ChangeList, + inline_value_cache: InlineValueCache, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -1517,6 +1539,8 @@ impl Editor { if editor.go_to_active_debug_line(window, cx) { cx.stop_propagation(); } + + editor.refresh_inline_values(cx); } _ => {} }, @@ -1659,6 +1683,7 @@ impl Editor { released_too_fast: false, }, inline_diagnostics_enabled: mode.is_full(), + inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, @@ -1788,6 +1813,33 @@ impl Editor { }, )); + if let Some(dap_store) = this + .project + .as_ref() + .map(|project| project.read(cx).dap_store()) + { + let weak_editor = cx.weak_entity(); + + this._subscriptions + .push( + cx.observe_new::(move |_, _, cx| { + let session_entity = cx.entity(); + weak_editor + .update(cx, |editor, cx| { + editor._subscriptions.push( + cx.subscribe(&session_entity, Self::on_debug_session_event), + ); + }) + .ok(); + }), + ); + + for session in dap_store.read(cx).sessions().cloned().collect::>() { + this._subscriptions + .push(cx.subscribe(&session, Self::on_debug_session_event)); + } + } + this.end_selection(window, cx); this.scroll_manager.show_scrollbars(window, cx); jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx); @@ -4195,6 +4247,17 @@ impl Editor { } } + pub fn toggle_inline_values( + &mut self, + _: &ToggleInlineValues, + _: &mut Window, + cx: &mut Context, + ) { + self.inline_value_cache.enabled = !self.inline_value_cache.enabled; + + self.refresh_inline_values(cx); + } + pub fn toggle_inlay_hints( &mut self, _: &ToggleInlayHints, @@ -4211,6 +4274,10 @@ impl Editor { self.inlay_hint_cache.enabled } + pub fn inline_values_enabled(&self) -> bool { + self.inline_value_cache.enabled + } + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { if self.semantics_provider.is_none() || !self.mode.is_full() { return; @@ -16343,34 +16410,33 @@ impl Editor { maybe!({ let breakpoint_store = self.breakpoint_store.as_ref()?; - let Some((_, _, active_position)) = - breakpoint_store.read(cx).active_position().cloned() + let Some(active_stack_frame) = breakpoint_store.read(cx).active_position().cloned() else { self.clear_row_highlights::(); return None; }; + let position = active_stack_frame.position; + let buffer_id = position.buffer_id?; let snapshot = self .project .as_ref()? .read(cx) - .buffer_for_id(active_position.buffer_id?, cx)? + .buffer_for_id(buffer_id, cx)? .read(cx) .snapshot(); let mut handled = false; - for (id, ExcerptRange { context, .. }) in self - .buffer - .read(cx) - .excerpts_for_buffer(active_position.buffer_id?, cx) + for (id, ExcerptRange { context, .. }) in + self.buffer.read(cx).excerpts_for_buffer(buffer_id, cx) { - if context.start.cmp(&active_position, &snapshot).is_ge() - || context.end.cmp(&active_position, &snapshot).is_lt() + if context.start.cmp(&position, &snapshot).is_ge() + || context.end.cmp(&position, &snapshot).is_lt() { continue; } let snapshot = self.buffer.read(cx).snapshot(cx); - let multibuffer_anchor = snapshot.anchor_in_excerpt(id, active_position)?; + let multibuffer_anchor = snapshot.anchor_in_excerpt(id, position)?; handled = true; self.clear_row_highlights::(); @@ -16383,6 +16449,7 @@ impl Editor { cx.notify(); } + handled.then_some(()) }) .is_some() @@ -17374,6 +17441,87 @@ impl Editor { cx.notify(); } + fn on_debug_session_event( + &mut self, + _session: Entity, + event: &SessionEvent, + cx: &mut Context, + ) { + match event { + SessionEvent::InvalidateInlineValue => { + self.refresh_inline_values(cx); + } + _ => {} + } + } + + fn refresh_inline_values(&mut self, cx: &mut Context) { + let Some(project) = self.project.clone() else { + return; + }; + let Some(buffer) = self.buffer.read(cx).as_singleton() else { + return; + }; + if !self.inline_value_cache.enabled { + let inlays = std::mem::take(&mut self.inline_value_cache.inlays); + self.splice_inlays(&inlays, Vec::new(), cx); + return; + } + + let current_execution_position = self + .highlighted_rows + .get(&TypeId::of::()) + .and_then(|lines| lines.last().map(|line| line.range.start)); + + self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| { + let snapshot = editor + .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) + .ok()?; + + let inline_values = editor + .update(cx, |_, cx| { + let Some(current_execution_position) = current_execution_position else { + return Some(Task::ready(Ok(Vec::new()))); + }; + + // todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text + // anchor is in the same buffer + let range = + buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor; + project.inline_values(buffer, range, cx) + }) + .ok() + .flatten()? + .await + .context("refreshing debugger inlays") + .log_err()?; + + let (excerpt_id, buffer_id) = snapshot + .excerpts() + .next() + .map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?; + editor + .update(cx, |editor, cx| { + let new_inlays = inline_values + .into_iter() + .map(|debugger_value| { + Inlay::debugger_hint( + post_inc(&mut editor.next_inlay_id), + Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position), + debugger_value.text(), + ) + }) + .collect::>(); + let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect(); + std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids); + + editor.splice_inlays(&inlay_ids, new_inlays, cx); + }) + .ok()?; + Some(()) + }); + } + fn on_buffer_event( &mut self, multibuffer: &Entity, @@ -18909,6 +19057,13 @@ pub trait SemanticsProvider { cx: &mut App, ) -> Option>>; + fn inline_values( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>>; + fn inlay_hints( &self, buffer_handle: Entity, @@ -19366,13 +19521,33 @@ impl SemanticsProvider for Entity { fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { // TODO: make this work for remote projects - self.update(cx, |this, cx| { + self.update(cx, |project, cx| { + if project + .active_debug_session(cx) + .is_some_and(|(session, _)| session.read(cx).any_stopped_thread()) + { + return true; + } + buffer.update(cx, |buffer, cx| { - this.any_language_server_supports_inlay_hints(buffer, cx) + project.any_language_server_supports_inlay_hints(buffer, cx) }) }) } + fn inline_values( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>> { + self.update(cx, |project, cx| { + let (session, active_stack_frame) = project.active_debug_session(cx)?; + + Some(project.inline_values(session, active_stack_frame, buffer_handle, range, cx)) + }) + } + fn inlay_hints( &self, buffer_handle: Entity, diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index fbab3fd8ff..bb3b12fb89 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -1280,6 +1280,7 @@ mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + show_value_hints: false, edit_debounce_ms: 0, scroll_debounce_ms: 0, show_type_hints: true, diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 3ab6c95d5e..e86070a205 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1614,6 +1614,7 @@ mod tests { async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1e22487500..b24bd7944a 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -989,6 +989,7 @@ fn fetch_and_update_hints( } let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?; + if !editor.registered_buffers.contains_key(&query.buffer_id) { if let Some(project) = editor.project.as_ref() { project.update(cx, |project, cx| { @@ -999,6 +1000,7 @@ fn fetch_and_update_hints( }) } } + editor .semantics_provider .as_ref()? @@ -1324,6 +1326,7 @@ pub mod tests { let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -1430,6 +1433,7 @@ pub mod tests { async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -1535,6 +1539,7 @@ pub mod tests { async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -1760,6 +1765,7 @@ pub mod tests { let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -1919,6 +1925,7 @@ pub mod tests { ] { update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -1962,6 +1969,7 @@ pub mod tests { let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: false, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -2017,6 +2025,7 @@ pub mod tests { let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -2090,6 +2099,7 @@ pub mod tests { async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -2222,6 +2232,7 @@ pub mod tests { async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -2521,6 +2532,7 @@ pub mod tests { async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -2829,6 +2841,7 @@ pub mod tests { async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -3005,6 +3018,7 @@ pub mod tests { update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -3037,6 +3051,7 @@ pub mod tests { async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -3129,6 +3144,7 @@ pub mod tests { async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: false, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -3205,6 +3221,7 @@ pub mod tests { update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, @@ -3265,6 +3282,7 @@ pub mod tests { async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { + show_value_hints: true, enabled: true, edit_debounce_ms: 0, scroll_debounce_ms: 0, diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 90cf6605ed..0eebddb640 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -455,6 +455,15 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { self.0.inlay_hints(buffer, range, cx) } + fn inline_values( + &self, + _: Entity, + _: Range, + _: &mut App, + ) -> Option>>> { + None + } + fn resolve_inlay_hint( &self, hint: project::InlayHint, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5097a0bab0..aac6df01b2 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, + DebugVariableCapture, 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, anyhow}; use async_watch as watch; use clock::Lamport; @@ -73,6 +73,12 @@ pub use {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); @@ -3888,6 +3894,79 @@ 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 85195bfe7a..86e6b8cae9 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1015,6 +1015,7 @@ 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, @@ -1115,6 +1116,18 @@ 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, @@ -1175,6 +1188,7 @@ 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(), @@ -1246,6 +1260,11 @@ 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) } @@ -1341,6 +1360,25 @@ 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/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 7ba3f3b0ae..d87c6db5dd 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -214,6 +214,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), ("textobjects", |q| &mut q.text_objects), ]; @@ -230,6 +231,7 @@ pub struct LanguageQueries { pub redactions: Option>, pub runnables: Option>, pub text_objects: Option>, + pub debug_variables: Option>, } #[derive(Clone, Default)] diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 96610846bd..ce7019bd85 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -971,6 +971,11 @@ pub struct InlayHintSettings { /// Default: false #[serde(default)] pub enabled: bool, + /// Global switch to toggle inline values on and off. + /// + /// Default: false + #[serde(default)] + pub show_value_hints: bool, /// Whether type hints should be shown. /// /// Default: true diff --git a/crates/languages/src/python/debug_variables.scm b/crates/languages/src/python/debug_variables.scm new file mode 100644 index 0000000000..a434e07bf0 --- /dev/null +++ b/crates/languages/src/python/debug_variables.scm @@ -0,0 +1,5 @@ +(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 new file mode 100644 index 0000000000..9ef51035ef --- /dev/null +++ b/crates/languages/src/rust/debug_variables.scm @@ -0,0 +1,3 @@ +(let_declaration pattern: (identifier) @debug_variable) + +(parameter (identifier) @debug_variable) diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index d661e4c159..715049c59d 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -22,7 +22,7 @@ collections.workspace = true futures.workspace = true gpui.workspace = true log.workspace = true -lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "1fff0dd12e2071c5667327394cfec163d2a466ab" } +lsp-types.workspace = true parking_lot.workspace = true postage.workspace = true serde.workspace = true diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index ee0511ca01..c47b3ea8fa 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow}; use breakpoints_in_file::BreakpointsInFile; use collections::BTreeMap; -use dap::client::SessionId; +use dap::{StackFrameId, client::SessionId}; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task}; use itertools::Itertools; use language::{Buffer, BufferSnapshot, proto::serialize_anchor as serialize_text_anchor}; @@ -17,6 +17,8 @@ use text::{Point, PointUtf16}; use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore}; +use super::session::ThreadId; + mod breakpoints_in_file { use language::{BufferEvent, DiskState}; @@ -108,10 +110,20 @@ enum BreakpointStoreMode { Local(LocalBreakpointStore), Remote(RemoteBreakpointStore), } + +#[derive(Clone)] +pub struct ActiveStackFrame { + pub session_id: SessionId, + pub thread_id: ThreadId, + pub stack_frame_id: StackFrameId, + pub path: Arc, + pub position: text::Anchor, +} + pub struct BreakpointStore { breakpoints: BTreeMap, BreakpointsInFile>, downstream_client: Option<(AnyProtoClient, u64)>, - active_stack_frame: Option<(SessionId, Arc, text::Anchor)>, + active_stack_frame: Option, // E.g ssh mode: BreakpointStoreMode, } @@ -493,7 +505,7 @@ impl BreakpointStore { }) } - pub fn active_position(&self) -> Option<&(SessionId, Arc, text::Anchor)> { + pub fn active_position(&self) -> Option<&ActiveStackFrame> { self.active_stack_frame.as_ref() } @@ -504,7 +516,7 @@ impl BreakpointStore { ) { if let Some(session_id) = session_id { self.active_stack_frame - .take_if(|(id, _, _)| *id == session_id); + .take_if(|active_stack_frame| active_stack_frame.session_id == session_id); } else { self.active_stack_frame.take(); } @@ -513,11 +525,7 @@ impl BreakpointStore { cx.notify(); } - pub fn set_active_position( - &mut self, - position: (SessionId, Arc, text::Anchor), - cx: &mut Context, - ) { + pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context) { self.active_stack_frame = Some(position); cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged); cx.notify(); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index f5fddb75d9..c0bf4bb1dd 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -4,7 +4,7 @@ use super::{ session::{self, Session, SessionStateEvent}, }; use crate::{ - ProjectEnvironment, + InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, project_settings::ProjectSettings, terminals::{SshCommand, wrap_for_ssh}, worktree_store::WorktreeStore, @@ -15,7 +15,7 @@ use collections::HashMap; use dap::{ Capabilities, CompletionItem, CompletionsArguments, DapRegistry, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, Source, - StartDebuggingRequestArguments, + StackFrameId, StartDebuggingRequestArguments, adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName, TcpArguments}, client::SessionId, messages::Message, @@ -28,7 +28,10 @@ use futures::{ }; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; use http_client::HttpClient; -use language::{BinaryStatus, LanguageRegistry, LanguageToolchainStore}; +use language::{ + BinaryStatus, Buffer, LanguageRegistry, LanguageToolchainStore, + language_settings::InlayHintKind, range_from_lsp, +}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; @@ -763,6 +766,103 @@ impl DapStore { }) } + pub fn resolve_inline_values( + &self, + session: Entity, + stack_frame_id: StackFrameId, + buffer_handle: Entity, + inline_values: 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 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 { + continue; + }; + + inlay_hints.push(InlayHint { + position: snapshot.anchor_after(range.end), + label: InlayHintLabel::String(format!(": {}", variable.value)), + kind: Some(InlayHintKind::Type), + padding_left: false, + padding_right: false, + tooltip: None, + 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()); + + let Ok(eval_task) = session.update(cx, |session, cx| { + session.evaluate( + expression, + Some(EvaluateArgumentsContext::Variables), + Some(stack_frame_id), + None, + cx, + ) + }) else { + continue; + }; + + if let Some(response) = eval_task.await { + inlay_hints.push(InlayHint { + position: snapshot.anchor_after(range.end), + label: InlayHintLabel::String(format!(": {}", response.result)), + kind: Some(InlayHintKind::Type), + padding_left: false, + padding_right: false, + tooltip: None, + resolve_state: ResolveState::Resolved, + }); + }; + } + }; + } + + Ok(inlay_hints) + }) + } + pub fn shutdown_sessions(&mut self, cx: &mut Context) -> Task<()> { let mut tasks = vec![]; for session_id in self.sessions.keys().cloned().collect::>() { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 6a147bbb4e..dde153d9b0 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -20,7 +20,9 @@ use dap::{ client::{DebugAdapterClient, SessionId}, messages::{Events, Message}, }; -use dap::{ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory}; +use dap::{ + EvaluateResponse, ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory, +}; use futures::channel::oneshot; use futures::{FutureExt, future::Shared}; use gpui::{ @@ -649,6 +651,7 @@ pub enum SessionEvent { StackTrace, Variables, Threads, + InvalidateInlineValue, CapabilitiesLoaded, } @@ -1060,6 +1063,7 @@ impl Session { .map(Into::into) .filter(|_| !event.preserve_focus_hint.unwrap_or(false)), )); + cx.emit(SessionEvent::InvalidateInlineValue); cx.notify(); } @@ -1281,6 +1285,10 @@ impl Session { }); } + pub fn any_stopped_thread(&self) -> bool { + self.thread_states.any_stopped_thread() + } + pub fn thread_status(&self, thread_id: ThreadId) -> ThreadStatus { self.thread_states.thread_status(thread_id) } @@ -1802,6 +1810,20 @@ impl Session { .unwrap_or_default() } + pub fn variables_by_stack_frame_id(&self, stack_frame_id: StackFrameId) -> Vec { + let Some(stack_frame) = self.stack_frames.get(&stack_frame_id) else { + return Vec::new(); + }; + + stack_frame + .scopes + .iter() + .filter_map(|scope| self.variables.get(&scope.variables_reference)) + .flatten() + .cloned() + .collect() + } + pub fn variables( &mut self, variables_reference: VariableReference, @@ -1867,7 +1889,7 @@ impl Session { frame_id: Option, source: Option, cx: &mut Context, - ) { + ) -> Task> { self.request( EvaluateCommand { expression, @@ -1896,7 +1918,6 @@ impl Session { }, cx, ) - .detach(); } pub fn location( @@ -1915,6 +1936,7 @@ impl Session { ); self.locations.get(&reference).cloned() } + pub fn disconnect_client(&mut self, cx: &mut Context) { let command = DisconnectCommand { restart: Some(false), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3c4b9e9658..1d950ed651 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -41,13 +41,14 @@ use client::{ }; use clock::ReplicaId; -use dap::client::DebugAdapterClient; +use dap::{DapRegistry, client::DebugAdapterClient}; use collections::{BTreeSet, HashMap, HashSet}; use debounced_delay::DebouncedDelay; use debugger::{ - breakpoint_store::BreakpointStore, + breakpoint_store::{ActiveStackFrame, BreakpointStore}, dap_store::{DapStore, DapStoreEvent}, + session::Session, }; pub use environment::ProjectEnvironment; #[cfg(test)] @@ -63,7 +64,7 @@ use image_store::{ImageItemEvent, ImageStoreEvent}; use ::git::{blame::Blame, status::FileStatus}; use gpui::{ AnyEntity, App, AppContext, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Hsla, - SharedString, Task, WeakEntity, Window, + SharedString, Task, WeakEntity, Window, prelude::FluentBuilder, }; use itertools::Itertools; use language::{ @@ -1551,6 +1552,15 @@ impl Project { self.breakpoint_store.clone() } + pub fn active_debug_session(&self, cx: &App) -> Option<(Entity, ActiveStackFrame)> { + let active_position = self.breakpoint_store.read(cx).active_position()?; + let session = self + .dap_store + .read(cx) + .session_by_id(active_position.session_id)?; + Some((session, active_position.clone())) + } + pub fn lsp_store(&self) -> Entity { self.lsp_store.clone() } @@ -3484,6 +3494,69 @@ impl Project { }) } + pub fn inline_values( + &mut self, + session: Entity, + active_stack_frame: ActiveStackFrame, + buffer_handle: Entity, + range: Range, + cx: &mut Context, + ) -> Task>> { + let snapshot = buffer_handle.read(cx).snapshot(); + + let Some(inline_value_provider) = session + .read(cx) + .adapter_name() + .map(|adapter_name| DapRegistry::global(cx).adapter(&adapter_name)) + .and_then(|adapter| adapter.inline_value_provider()) + 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 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()?; + + Some(( + snapshot.text_for_range(range.range).collect::(), + lsp_range, + )) + }) + .collect::>(); + + let inline_values = inline_value_provider.provide(variable_ranges); + + 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( + session, + stack_frame_id, + buffer_handle, + inline_values, + cx, + ) + }) + })? + .await + }) + } + pub fn inlay_hints( &mut self, buffer_handle: Entity, diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 6f8b6efa70..8a89b1ecc7 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -90,6 +90,7 @@ impl Render for QuickActionBar { let editor_value = editor.read(cx); let selection_menu_enabled = editor_value.selection_menu_enabled(cx); let inlay_hints_enabled = editor_value.inlay_hints_enabled(); + let inline_values_enabled = editor_value.inline_values_enabled(); let inline_diagnostics_enabled = editor_value.show_inline_diagnostics(); let supports_inline_diagnostics = editor_value.inline_diagnostics_enabled(); let git_blame_inline_enabled = editor_value.git_blame_inline_enabled(); @@ -224,6 +225,28 @@ impl Render for QuickActionBar { } }, ); + + menu = menu.toggleable_entry( + "Inline Values", + inline_values_enabled, + IconPosition::Start, + Some(editor::actions::ToggleInlineValues.boxed_clone()), + { + let editor = editor.clone(); + move |window, cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_inline_values( + &editor::actions::ToggleInlineValues, + window, + cx, + ); + }) + .ok(); + } + } + ); + } if supports_inline_diagnostics {