feat(hover): show symbol/field IDs

This commit is contained in:
Riley Bruins 2025-10-12 16:10:58 -07:00
parent dbe8584676
commit 4e44959c6d
4 changed files with 161 additions and 28 deletions

View file

@ -21,11 +21,21 @@
"]"
] @alternation
(field_definition
(identifier) @field)
(negated_field
(identifier) @field)
(negated_field
"!" @negation)
(definition/named_node) @capture
(anonymous_node
(string
(string_content)) @anonymous)
(named_node
(identifier) @identifier.node)

View file

@ -27,7 +27,10 @@ use ts_query_ls::{
use crate::{
Backend, DocumentData, ImportedUri, LanguageData, LspClient, QUERY_LANGUAGE, SymbolInfo,
util::{CAPTURES_QUERY, NodeUtil as _, TextProviderRope, uri_to_basename},
util::{
CAPTURES_QUERY, NodeUtil as _, TextProviderRope, remove_unnecessary_escapes,
uri_to_basename,
},
};
use super::code_action::CodeActions;
@ -462,12 +465,12 @@ async fn get_diagnostics_recursively(
remove_unnecessary_escapes(&capture_text)
};
let sym = SymbolInfo {
label: capture_text.clone(),
label: capture_text,
named,
};
if !symbols.contains(&sym) {
diagnostics.push(Diagnostic {
message: format!("Invalid node type: \"{capture_text}\""),
message: format!("Invalid node type: \"{}\"", sym.label),
severity: ERROR_SEVERITY,
range,
code: DiagnosticCode::InvalidNode.into(),
@ -860,30 +863,6 @@ async fn get_imported_query_diagnostics(
items
}
fn remove_unnecessary_escapes(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some(char @ ('\"' | '\\' | 'n' | 'r' | 't' | '0')) => {
result.push('\\');
result.push(char);
}
Some(char) => {
result.push(char);
}
None => {}
}
} else {
result.push(c);
}
}
result
}
fn validate_predicate<'a>(
diagnostics: &mut Vec<Diagnostic>,
tree_cursor: &mut TreeCursor<'a>,

View file

@ -12,7 +12,7 @@ use crate::{
Backend, LspClient, QUERY_LANGUAGE, SymbolInfo,
util::{
FORMAT_IGNORE_REGEX, INHERITS_REGEX, NodeUtil, PosUtil, capture_at_pos,
get_imported_module_under_cursor, uri_to_basename,
get_imported_module_under_cursor, remove_unnecessary_escapes, uri_to_basename,
},
};
@ -130,6 +130,93 @@ pub async fn hover<C: LspClient>(
value,
}),
})
} else if let Some(language) = language_data.as_ref().map(|ld| &ld.language) {
let syms = (0..language.node_kind_count() as u16)
.filter(|&id| {
if !(language.node_kind_is_visible(id)
|| language.node_kind_is_supertype(id))
|| !language.node_kind_is_named(id)
{
return false;
}
language
.node_kind_for_id(id)
.is_some_and(|kind| kind == sym.label)
})
.collect::<Vec<_>>();
if syms.is_empty() {
None
} else {
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: format!(
"Symbol IDs: {}",
syms.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
),
}),
range,
})
}
} else {
None
}
}
"anonymous" => {
if let Some(language) = language_data.as_ref().map(|ld| &ld.language) {
let string_content =
remove_unnecessary_escapes(&capture_text[1..capture_text.len() - 1]);
let syms = (0..language.node_kind_count() as u16)
.filter(|&id| {
if !language.node_kind_is_visible(id)
|| language.node_kind_is_supertype(id)
|| language.node_kind_is_named(id)
{
return false;
}
language
.node_kind_for_id(id)
.is_some_and(|kind| kind == string_content)
})
.collect::<Vec<_>>();
if syms.is_empty() {
None
} else {
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: format!(
"Symbol IDs: {}",
syms.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
),
}),
range,
})
}
} else {
None
}
}
"field" => {
if let Some(language) = language_data.as_ref().map(|ld| &ld.language) {
let sym = (1..=language.field_count() as u16).find(|&id| {
language
.field_name_for_id(id)
.is_some_and(|name| name == capture_text)
});
sym.map(|sym| Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("Field ID: {sym}"),
}),
range,
})
} else {
None
}
@ -448,6 +535,38 @@ An error node", BTreeMap::from([(String::from("error"), String::from("An error n
end: Position::new(0, 21)
},
"*Document not found*", BTreeMap::default())]
#[case("(named_node)", Position { line: 0, character: 4 }, Range {
start: Position::new(0, 1),
end: Position::new(0, 11)
}, "Symbol IDs: 39", BTreeMap::default())]
#[case("(MISSING identifier)", Position { line: 0, character: 12 }, Range {
start: Position::new(0, 9),
end: Position::new(0, 19)
}, "Symbol IDs: 6, 7", BTreeMap::default())]
#[case("(definition/named_node)", Position { line: 0, character: 12 }, Range {
start: Position::new(0, 12),
end: Position::new(0, 22)
}, "Symbol IDs: 39", BTreeMap::default())]
#[case("\"MISSING\"", Position { line: 0, character: 0 }, Range {
start: Position::new(0, 0),
end: Position::new(0, 9)
}, "Symbol IDs: 18", BTreeMap::default())]
#[case(r#""MISSING""#, Position { line: 0, character: 2 }, Range {
start: Position::new(0, 0),
end: Position::new(0, 9)
}, "Symbol IDs: 18", BTreeMap::default())]
#[case(r#""MIS\SING""#, Position { line: 0, character: 4 }, Range {
start: Position::new(0, 0),
end: Position::new(0, 10)
}, "Symbol IDs: 18", BTreeMap::default())]
#[case("(missing_node name: (identifier) @variable !type)", Position { line: 0, character: 15 }, Range {
start: Position::new(0, 14),
end: Position::new(0, 18)
}, "Field ID: 1", BTreeMap::default())]
#[case("(missing_node name: (identifier) @variable !type)", Position { line: 0, character: 46 }, Range {
start: Position::new(0, 44),
end: Position::new(0, 48)
}, "Field ID: 5", BTreeMap::default())]
#[tokio::test(flavor = "current_thread")]
async fn hover(
#[case] source: &str,

View file

@ -727,3 +727,28 @@ pub async fn get_work_done_token<C: LspClient>(
None
}
}
/// Remove unnecessary backslashes from the given string content.
pub fn remove_unnecessary_escapes(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some(char @ ('\"' | '\\' | 'n' | 'r' | 't' | '0')) => {
result.push('\\');
result.push(char);
}
Some(char) => {
result.push(char);
}
None => {}
}
} else {
result.push(c);
}
}
result
}