debugger: Add inline value tests (#29815)

## Context

This PR improves the accuracy of our inline values for Rust/Python. It
does this by only adding inline value hints to the last valid use of a
variable and checking whether variables are valid within a given scope
or not.

We also added tests for Rust/Python inline values and inline values
refreshing when stepping in a debug session.

### Future tasks
1. Handle functions that have inner functions defined within them.
2. Add inline values to variables that were used in inner scopes but not
defined in them.
3. Move the inline value provider trait and impls to the language trait
(or somewhere else).
4. Use Semantic tokens as the first inline value provider and fall back
to tree sitter
5. add let some variable statement, for loops, and function inline value
hints to Rust.
6. Make writing tests more streamlined. 
6.1 We should be able to write a test by only passing in variables,
language, source file, expected result, and stop position to a function.
7. Write a test that has coverage for selecting different stack frames. 

co-authored-by: Remco Smits \<djsmits12@gmail.com\>

Release Notes:

- N/A

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
This commit is contained in:
Anthony Eid 2025-05-07 14:39:35 +02:00 committed by GitHub
parent 7bc3f74cab
commit 1a520990cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 2273 additions and 281 deletions

4
Cargo.lock generated
View file

@ -4068,7 +4068,6 @@ dependencies = [
"http_client",
"language",
"log",
"lsp-types",
"node_runtime",
"parking_lot",
"paths",
@ -4104,7 +4103,6 @@ dependencies = [
"futures 0.3.31",
"gpui",
"language",
"lsp-types",
"paths",
"serde",
"serde_json",
@ -4249,6 +4247,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"dap",
"dap_adapters",
"db",
"debugger_tools",
"editor",
@ -7797,6 +7796,7 @@ dependencies = [
"tree-sitter-html",
"tree-sitter-json",
"tree-sitter-md",
"tree-sitter-python",
"tree-sitter-ruby",
"tree-sitter-rust",
"tree-sitter-typescript",

View file

@ -36,7 +36,6 @@ 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

@ -402,10 +402,6 @@ 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;
@ -417,10 +413,6 @@ 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"))]

View file

@ -1,6 +1,7 @@
pub mod adapters;
pub mod client;
pub mod debugger_settings;
pub mod inline_value;
pub mod proto_conversions;
mod registry;
pub mod transport;

View file

@ -0,0 +1,277 @@
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariableLookupKind {
Variable,
Expression,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariableScope {
Local,
Global,
}
#[derive(Debug, Clone)]
pub struct InlineValueLocation {
pub variable_name: String,
pub scope: VariableScope,
pub lookup: VariableLookupKind,
pub row: usize,
pub column: usize,
}
/// A trait for providing inline values for debugging purposes.
///
/// Implementors of this trait are responsible for analyzing a given node in the
/// source code and extracting variable information, including their names,
/// scopes, and positions. This information is used to display inline values
/// during debugging sessions. Implementors must also handle variable scoping
/// themselves by traversing the syntax tree upwards to determine whether a
/// variable is local or global.
pub trait InlineValueProvider {
/// Provides a list of inline value locations based on the given node and source code.
///
/// # Parameters
/// - `node`: The root node of the active debug line. Implementors should traverse
/// upwards from this node to gather variable information and determine their scope.
/// - `source`: The source code as a string slice, used to extract variable names.
/// - `max_row`: The maximum row to consider when collecting variables. Variables
/// declared beyond this row should be ignored.
///
/// # Returns
/// A vector of `InlineValueLocation` instances, each representing a variable's
/// name, scope, and the position of the inline value should be shown.
fn provide(
&self,
node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation>;
}
pub struct RustInlineValueProvider;
impl InlineValueProvider for RustInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local && child.kind() == "let_declaration" {
if let Some(identifier) = child.child_by_field_name("pattern") {
let variable_name = source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) = variable_names_in_scope.get(&variable_name) {
variables.remove(*index);
}
variable_names_in_scope.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
} else if child.kind() == "static_item" {
if let Some(name) = child.child_by_field_name("name") {
let variable_name = source[name.byte_range()].to_string();
variables.push(InlineValueLocation {
variable_name,
scope: scope.clone(),
lookup: VariableLookupKind::Expression,
row: name.end_position().row,
column: name.end_position().column,
});
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_item" | "closure_expression") {
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}
pub struct PythonInlineValueProvider;
impl InlineValueProvider for PythonInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local {
match child.kind() {
"expression_statement" => {
if let Some(expr) = child.child(0) {
if expr.kind() == "assignment" {
if let Some(param) = expr.child(0) {
let param_identifier = if param.kind() == "identifier" {
Some(param)
} else if param.kind() == "typed_parameter" {
param.child(0)
} else {
None
};
if let Some(identifier) = param_identifier {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
}
"function_definition" => {
if let Some(params) = child.child_by_field_name("parameters") {
for param in params.named_children(&mut params.walk()) {
let param_identifier = if param.kind() == "identifier" {
Some(param)
} else if param.kind() == "typed_parameter" {
param.child(0)
} else {
None
};
if let Some(identifier) = param_identifier {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
"for_statement" => {
if let Some(target) = child.child_by_field_name("left") {
if target.kind() == "identifier" {
let variable_name = source[target.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) = variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: target.end_position().row,
column: target.end_position().column,
});
}
}
}
_ => {}
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_definition" | "module")
&& node.range().end_point.row < max_row
{
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}

View file

@ -5,7 +5,10 @@ use gpui::{App, Global, SharedString};
use parking_lot::RwLock;
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
use crate::adapters::{DebugAdapter, DebugAdapterName};
use crate::{
adapters::{DebugAdapter, DebugAdapterName},
inline_value::InlineValueProvider,
};
use std::{collections::BTreeMap, sync::Arc};
/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
@ -22,6 +25,7 @@ pub trait DapLocator: Send + Sync {
struct DapRegistryState {
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
locators: FxHashMap<SharedString, Arc<dyn DapLocator>>,
inline_value_providers: FxHashMap<String, Arc<dyn InlineValueProvider>>,
}
#[derive(Clone, Default)]
@ -58,6 +62,22 @@ impl DapRegistry {
);
}
pub fn add_inline_value_provider(
&self,
language: String,
provider: Arc<dyn InlineValueProvider>,
) {
let _previous_value = self
.0
.write()
.inline_value_providers
.insert(language, provider);
debug_assert!(
_previous_value.is_none(),
"Attempted to insert a new inline value provider when one is already registered"
);
}
pub fn locators(&self) -> FxHashMap<SharedString, Arc<dyn DapLocator>> {
self.0.read().locators.clone()
}
@ -66,6 +86,10 @@ impl DapRegistry {
self.0.read().adapters.get(name).cloned()
}
pub fn inline_value_provider(&self, language: &str) -> Option<Arc<dyn InlineValueProvider>> {
self.0.read().inline_value_providers.get(language).cloned()
}
pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
self.0.read().adapters.keys().cloned().collect()
}

View file

@ -27,7 +27,6 @@ dap.workspace = true
futures.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;
use async_trait::async_trait;
use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release};
use dap::adapters::{DebugTaskDefinition, latest_github_release};
use futures::StreamExt;
use gpui::AsyncApp;
use task::DebugRequest;
@ -159,25 +159,4 @@ 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

@ -16,6 +16,7 @@ use dap::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
},
inline_value::{PythonInlineValueProvider, RustInlineValueProvider},
};
use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
@ -34,6 +35,10 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GdbDebugAdapter));
registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
registry
.add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
})
}

View file

@ -1,8 +1,5 @@
use crate::*;
use dap::{
DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition,
adapters::InlineValueProvider,
};
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util::ResultExt;
@ -182,34 +179,4 @@ impl DebugAdapter for PythonDebugAdapter {
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
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

@ -15,6 +15,7 @@ doctest = false
[features]
test-support = [
"dap/test-support",
"dap_adapters/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
@ -31,6 +32,7 @@ client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
dap.workspace = true
dap_adapters = { workspace = true, optional = true }
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
@ -63,6 +65,7 @@ unindent = { workspace = true, optional = true }
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }
dap_adapters = { workspace = true, features = ["test-support"] }
debugger_tools = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true

View file

@ -21,6 +21,8 @@ mod dap_logger;
#[cfg(test)]
mod debugger_panel;
#[cfg(test)]
mod inline_values;
#[cfg(test)]
mod module_list;
#[cfg(test)]
mod persistence;
@ -45,6 +47,7 @@ pub fn init_test(cx: &mut gpui::TestAppContext) {
Project::init_settings(cx);
editor::init(cx);
crate::init(cx);
dap_adapters::init(cx);
});
}

File diff suppressed because it is too large Load diff

View file

@ -4333,6 +4333,16 @@ impl Editor {
self.inline_value_cache.enabled
}
#[cfg(any(test, feature = "test-support"))]
pub fn inline_value_inlays(&self, cx: &App) -> Vec<Inlay> {
self.display_map
.read(cx)
.current_inlays()
.filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_)))
.cloned()
.collect()
}
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context<Self>) {
if self.semantics_provider.is_none() || !self.mode.is_full() {
return;
@ -17665,7 +17675,7 @@ impl Editor {
}
}
fn refresh_inline_values(&mut self, cx: &mut Context<Self>) {
pub fn refresh_inline_values(&mut self, cx: &mut Context<Self>) {
let Some(project) = self.project.clone() else {
return;
};

View file

@ -19,6 +19,7 @@ test-support = [
"lsp/test-support",
"text/test-support",
"tree-sitter-rust",
"tree-sitter-python",
"tree-sitter-typescript",
"settings/test-support",
"util/test-support",
@ -58,6 +59,7 @@ sum_tree.workspace = true
task.workspace = true
text.workspace = true
theme.workspace = true
tree-sitter-python = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
tree-sitter.workspace = true
@ -76,15 +78,16 @@ pretty_assertions.workspace = true
rand.workspace = true
settings = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
tree-sitter-elixir.workspace = true
tree-sitter-embedded-template.workspace = true
tree-sitter-heex.workspace = true
tree-sitter-html.workspace = true
tree-sitter-json.workspace = true
tree-sitter-md.workspace = true
tree-sitter-python.workspace = true
tree-sitter-ruby.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }

View file

@ -1,6 +1,12 @@
pub use crate::{
Grammar, Language, LanguageRegistry,
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
proto,
};
use crate::{
DebugVariableCapture, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
TextObject, TreeSitterOptions,
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject,
TreeSitterOptions,
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{LanguageSettings, language_settings},
outline::OutlineItem,
@ -11,12 +17,6 @@ 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;
pub use clock::ReplicaId;
@ -69,16 +69,10 @@ use util::RandomCharIter;
use util::{RangeExt, debug_panic, maybe};
#[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript};
pub use {tree_sitter_python, 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);
@ -3377,6 +3371,36 @@ impl BufferSnapshot {
result
}
/// Returns the root syntax node within the given row
pub fn syntax_root_ancestor(&self, position: Anchor) -> Option<tree_sitter::Node> {
let start_offset = position.to_offset(self);
let row = self.summary_for_anchor::<text::PointUtf16>(&position).row as usize;
let layer = self
.syntax
.layers_for_range(start_offset..start_offset, &self.text, true)
.next()?;
let mut cursor = layer.node().walk();
// Descend to the first leaf that touches the start of the range.
while cursor.goto_first_child_for_byte(start_offset).is_some() {
if cursor.node().end_byte() == start_offset {
cursor.goto_next_sibling();
}
}
// Ascend to the root node within the same row.
while cursor.goto_parent() {
if cursor.node().start_position().row != row {
break;
}
}
return Some(cursor.node());
}
/// Returns the outline for the buffer.
///
/// This method allows passing an optional [`SyntaxTheme`] to
@ -3938,79 +3962,6 @@ 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

@ -1044,7 +1044,6 @@ 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>,
@ -1145,18 +1144,6 @@ 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>,
@ -1217,7 +1204,6 @@ 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(),
@ -1289,11 +1275,6 @@ 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)
}
@ -1389,25 +1370,6 @@ 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

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

View file

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

View file

@ -18,6 +18,7 @@ use dap::{
EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
adapters::{DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments},
client::SessionId,
inline_value::VariableLookupKind,
messages::Message,
requests::{Completions, Evaluate},
};
@ -29,7 +30,7 @@ use futures::{
};
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
use http_client::HttpClient;
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind, range_from_lsp};
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
use node_runtime::NodeRuntime;
use remote::SshRemoteClient;
@ -564,56 +565,37 @@ impl DapStore {
})
}
pub fn resolve_inline_values(
pub fn resolve_inline_value_locations(
&self,
session: Entity<Session>,
stack_frame_id: StackFrameId,
buffer_handle: Entity<Buffer>,
inline_values: Vec<lsp::InlineValue>,
inline_value_locations: Vec<dap::inline_value::InlineValueLocation>,
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 inlay_hints = Vec::with_capacity(inline_value_locations.len());
for inline_value_location in inline_value_locations.iter() {
let point = snapshot.point_to_point_utf16(language::Point::new(
inline_value_location.row as u32,
inline_value_location.column as u32,
));
let position = snapshot.anchor_after(point);
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 {
match inline_value_location.lookup {
VariableLookupKind::Variable => {
let Some(variable) = all_variables
.iter()
.find(|variable| variable.name == inline_value_location.variable_name)
else {
continue;
};
inlay_hints.push(InlayHint {
position: snapshot.anchor_after(range.end),
position,
label: InlayHintLabel::String(format!(": {}", variable.value)),
kind: Some(InlayHintKind::Type),
padding_left: false,
@ -622,17 +604,10 @@ impl DapStore {
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());
VariableLookupKind::Expression => {
let Ok(eval_task) = session.update(cx, |session, _| {
session.mode.request_dap(EvaluateCommand {
expression,
expression: inline_value_location.variable_name.clone(),
frame_id: Some(stack_frame_id),
source: None,
context: Some(EvaluateArgumentsContext::Variables),
@ -643,7 +618,7 @@ impl DapStore {
if let Some(response) = eval_task.await.log_err() {
inlay_hints.push(InlayHint {
position: snapshot.anchor_after(range.end),
position,
label: InlayHintLabel::String(format!(": {}", response.result)),
kind: Some(InlayHintKind::Type),
padding_left: false,

View file

@ -3562,52 +3562,43 @@ impl Project {
range: Range<text::Anchor>,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<Vec<InlayHint>>> {
let snapshot = buffer_handle.read(cx).snapshot();
let language_name = buffer_handle
.read(cx)
.language()
.map(|language| language.name().to_string());
let adapter = session.read(cx).adapter();
let Some(inline_value_provider) = DapRegistry::global(cx)
.adapter(&adapter)
.and_then(|adapter| adapter.inline_value_provider())
let Some(inline_value_provider) = language_name
.and_then(|language| DapRegistry::global(cx).inline_value_provider(&language))
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 snapshot = buffer_handle.read(cx).snapshot();
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()?;
let root_node = snapshot.syntax_root_ancestor(range.end).unwrap();
Some((
snapshot.text_for_range(range.range).collect::<String>(),
lsp_range,
))
})
.collect::<Vec<_>>();
let row = snapshot
.summary_for_anchor::<text::PointUtf16>(&range.end)
.row as usize;
let inline_values = inline_value_provider.provide(variable_ranges);
let inline_value_locations = inline_value_provider.provide(
root_node,
snapshot
.text_for_range(Anchor::MIN..range.end)
.collect::<String>()
.as_str(),
row,
);
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(
dap_store.resolve_inline_value_locations(
session,
stack_frame_id,
buffer_handle,
inline_values,
inline_value_locations,
cx,
)
})