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
|
@ -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>,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue