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 <peterosiewicz@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Kirill <kirill@zed.dev>
This commit is contained in:
Remco Smits 2025-04-24 00:27:27 +02:00 committed by GitHub
parent d095bab8ad
commit 218496744c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 709 additions and 54 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -1544,6 +1544,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
store.update_user_settings::<AllLanguageSettings>(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::<AllLanguageSettings>(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::<AllLanguageSettings>(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::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
show_value_hints: true,
enabled: true,
edit_debounce_ms: 0,
scroll_debounce_ms: 0,

View file

@ -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

View file

@ -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<lsp_types::InlineValue>;
}
#[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<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary>;
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
None
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeAdapter {}

View file

@ -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

View file

@ -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<Box<dyn InlineValueProvider>> {
Some(Box::new(CodeLldbInlineValueProvider))
}
}
struct CodeLldbInlineValueProvider;
impl InlineValueProvider for CodeLldbInlineValueProvider {
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue> {
variables
.into_iter()
.map(|(variable, range)| {
lsp_types::InlineValue::VariableLookup(lsp_types::InlineValueVariableLookup {
range,
variable_name: Some(variable),
case_sensitive_lookup: true,
})
})
.collect()
}
}

View file

@ -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<Box<dyn InlineValueProvider>> {
Some(Box::new(PythonInlineValueProvider))
}
}
struct PythonInlineValueProvider;
impl InlineValueProvider for PythonInlineValueProvider {
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue> {
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()
}
}

View file

@ -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(())

View file

@ -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();
});
}

View file

@ -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,
);
})

View file

@ -419,6 +419,7 @@ actions!(
OpenGitBlameCommit,
ToggleIndentGuides,
ToggleInlayHints,
ToggleInlineValues,
ToggleInlineDiagnostics,
ToggleEditPrediction,
ToggleLineNumbers,

View file

@ -64,6 +64,14 @@ impl Inlay {
text: text.into(),
}
}
pub fn debugger_hint<T: Into<Rope>>(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;

View file

@ -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<InlayId>,
refresh_task: Task<Option<()>>,
}
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::<project::debugger::session::Session>(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::<Vec<_>>() {
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>,
) {
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<Self>) {
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::<DebugCurrentRowHighlight>();
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::<DebugCurrentRowHighlight>();
@ -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<Session>,
event: &SessionEvent,
cx: &mut Context<Self>,
) {
match event {
SessionEvent::InvalidateInlineValue => {
self.refresh_inline_values(cx);
}
_ => {}
}
}
fn refresh_inline_values(&mut self, cx: &mut Context<Self>) {
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::<DebugCurrentRowHighlight>())
.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::<Vec<_>>();
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<MultiBuffer>,
@ -18909,6 +19057,13 @@ pub trait SemanticsProvider {
cx: &mut App,
) -> Option<Task<Vec<project::Hover>>>;
fn inline_values(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>>;
fn inlay_hints(
&self,
buffer_handle: Entity<Buffer>,
@ -19366,13 +19521,33 @@ impl SemanticsProvider for Entity<Project> {
fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, 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<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {
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<Buffer>,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -455,6 +455,15 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
self.0.inlay_hints(buffer, range, cx)
}
fn inline_values(
&self,
_: Entity<Buffer>,
_: Range<text::Anchor>,
_: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
None
}
fn resolve_inlay_hint(
&self,
hint: project::InlayHint,

View file

@ -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<usize>,
}
/// 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<TaskLabel> = LazyLock::new(TaskLabel::new);
@ -3888,6 +3894,79 @@ impl BufferSnapshot {
})
}
pub fn debug_variable_ranges(
&self,
offset_range: Range<usize>,
) -> impl Iterator<Item = DebugVariableRanges> + '_ {
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::<Vec<_>>();
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<usize>,

View file

@ -1015,6 +1015,7 @@ pub struct Grammar {
pub(crate) brackets_config: Option<BracketsConfig>,
pub(crate) redactions_config: Option<RedactionConfig>,
pub(crate) runnable_config: Option<RunnableConfig>,
pub(crate) debug_variables_config: Option<DebugVariablesConfig>,
pub(crate) indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>,
pub text_object_config: Option<TextObjectConfig>,
@ -1115,6 +1116,18 @@ struct RunnableConfig {
pub extra_captures: Vec<RunnableCapture>,
}
#[derive(Clone, Debug, PartialEq)]
enum DebugVariableCapture {
Named(SharedString),
Variable,
}
#[derive(Debug)]
struct DebugVariablesConfig {
pub query: Query,
pub captures: Vec<DebugVariableCapture>,
}
struct OverrideConfig {
query: Query,
values: HashMap<u32, OverrideEntry>,
@ -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<Self> {
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<Self> {
let grammar = self
.grammar_mut()

View file

@ -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<Cow<'static, str>>,
pub runnables: Option<Cow<'static, str>>,
pub text_objects: Option<Cow<'static, str>>,
pub debug_variables: Option<Cow<'static, str>>,
}
#[derive(Clone, Default)]

View file

@ -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

View file

@ -0,0 +1,5 @@
(assignment
left: (identifier) @debug_variable)
(function_definition
parameters: (parameters (identifier) @debug_variable))

View file

@ -0,0 +1,3 @@
(let_declaration pattern: (identifier) @debug_variable)
(parameter (identifier) @debug_variable)

View file

@ -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

View file

@ -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<Path>,
pub position: text::Anchor,
}
pub struct BreakpointStore {
breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,
downstream_client: Option<(AnyProtoClient, u64)>,
active_stack_frame: Option<(SessionId, Arc<Path>, text::Anchor)>,
active_stack_frame: Option<ActiveStackFrame>,
// E.g ssh
mode: BreakpointStoreMode,
}
@ -493,7 +505,7 @@ impl BreakpointStore {
})
}
pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, 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<Path>, text::Anchor),
cx: &mut Context<Self>,
) {
pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context<Self>) {
self.active_stack_frame = Some(position);
cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
cx.notify();

View file

@ -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<Session>,
stack_frame_id: StackFrameId,
buffer_handle: Entity<Buffer>,
inline_values: Vec<lsp::InlineValue>,
cx: &mut Context<Self>,
) -> Task<Result<Vec<InlayHint>>> {
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<Self>) -> Task<()> {
let mut tasks = vec![];
for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {

View file

@ -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<dap::Variable> {
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<u64>,
source: Option<Source>,
cx: &mut Context<Self>,
) {
) -> Task<Option<EvaluateResponse>> {
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<Self>) {
let command = DisconnectCommand {
restart: Some(false),

View file

@ -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<Session>, 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<LspStore> {
self.lsp_store.clone()
}
@ -3484,6 +3494,69 @@ impl Project {
})
}
pub fn inline_values(
&mut self,
session: Entity<Session>,
active_stack_frame: ActiveStackFrame,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<Vec<InlayHint>>> {
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::<String>(),
lsp_range,
))
})
.collect::<Vec<_>>();
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<T: ToOffset>(
&mut self,
buffer_handle: Entity<Buffer>,

View file

@ -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 {