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 e1a99c19c2464cb212a8db1028e656bc990f3859..9f24b3d82cc58204aed78140b5cd8e3c7bcd5eb6 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 4dcd268965d6b7c6f78e08ef8d1605fd7333e5b1..ba9fc3673dbb21637c06b69018fdd423dcdf7919 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 719b8643f2bded9d911746a7e038513f05a5771d..ec66e593d5a2340d605b3a3a8f28d4c35944b6fc 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 531276e7088da42578e788e5b8eed48aecea07e4..c5e7fec0c126f478685df98999808b5c7feb8196 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 7b4294dccd6512710bb76988638bf5cdf182b41d..1b927d00513cd1b2238be6fe6e2788e73cfe2686 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 0a11724aa2a130f215b3584daf18894078b3c509..ba461c3e68b5448d48def34fba0d66f460be13f6 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 e1b34e2d738b588d916f9e74f761e3da7998eeb9..e6e27b25b647089d218dc5ae3a16084a030076b7 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 3c3f587eccb9bbd6cb695978320321baef0e434e..cde4df9109d471462c9c421a5aa8898b136907b1 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 ce3300de5fc0ce48ea2c5733a77b68a863c7e16e..1630e27d23f176c5070c0e87c417accd41b7f7e4 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 ad765496c22ca2dd70529109e55baeca55e84e32..86be2269e4edaba6e9cd35ec283beab6264e8009 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 c9e7a9b11c848cf2cf2102aa511ff1533d475022..b60262f2d7002cb5b121fefb6b682a825244dfd5 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 9726740f1776ae68987273537a9d849041df0f56..d79461641dc45c6a025975232ec41f0338958441 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 29a0dcd27373806acf05707f042d690638a42f4e..ec3bc4865c4f6dd5194aac016c94fb05f3413f50 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 3a98b5950dccb1d90df1c930a03cc8bd7caddd69..aae7a6a859118ed0e253d017ce1a7ca9c0593b80 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 fbab3fd8ff3b1567dc7995640f0eecfde1af93e6..bb3b12fb89b22a6abc7abc80009b3d7377122b2c 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 3ab6c95d5e0bfc15823bd5b837d39b1061ff7b3e..e86070a20565b83c1b0d9bebafe635d73cdc21c7 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 1e224875008815cac5c1b10dfa00a8793f60df88..b24bd7944a8327229eea3e46bacbde10b10489c4 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 90cf6605edae88e65873bcf8901de8e460a9ff35..0eebddb640e5c8301eb1597b5c1599c7bdacc074 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 5097a0bab0f11730a0dd354aacf3c165df8a0dee..aac6df01b25ff17ec17a78b00266c434fd684e71 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 85195bfe7ae19d3540675758147d3487d144b02e..86e6b8cae9d97eb9c1c7b77cf1bba267940f11b1 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 7ba3f3b0aeb64d4a9321fe8fcd61e5a27228c16d..d87c6db5ddc87c3ee9fc533c46d2e708bda64f10 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 96610846bd6b58f9bf2c98a1b15c27760a3f5870..ce7019bd85f01a1f67f61fdb6f52989fa2de9c2b 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 0000000000000000000000000000000000000000..a434e07bf0a8eaa71b2689354976023335c530b2 --- /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 0000000000000000000000000000000000000000..9ef51035efba9d9b8e4958ae75e63b415229f4c1 --- /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 d661e4c159226d8dd075b6a321234987b57146de..715049c59d6f9244056a974553f311388a600a19 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 ee0511ca0151ad92fcb7c08a6123e65b46077678..c47b3ea8fae30776d73f7a1ad603f5a1de22f512 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 f5fddb75d95f90c0650acff9ad250b8fc5fe4706..c0bf4bb1dd5efe513bc547e82722e8fa003ba7ea 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 6a147bbb4e0f69f6533d3b6523c8edc2a1eb577d..dde153d9b0b349162af04eba72e9958c5f3cdce9 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 3c4b9e9658f9e1b556c9b83bbd1c6142b17480bd..1d950ed651d10137e112b26ea60948bff79d3fb2 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 6f8b6efa70ea0ad704b196a168fdebb5082e3e5d..8a89b1ecc7fa9511e115957e204a99c075e3ffda 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 {