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:
parent
d095bab8ad
commit
218496744c
30 changed files with 709 additions and 54 deletions
|
@ -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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue