feat(core): add `tree-sitter-breadcrumbs` command

Using this command, the user will be able to see a popup
showing all parent lines for cursor. This is good to get
context on the editor on big files.

This implementation is not ideal, as breadcrumbs should maybe
appear all the time, but is a simple hack to suppress the need
for the time being.
pull/13492/head
Rafael Oliveira 2025-04-22 13:20:33 -03:00
parent 63a1a94d92
commit c54f886b15
2 changed files with 84 additions and 1 deletions

View File

@ -1,4 +1,4 @@
use std::{borrow::Cow, collections::HashMap, iter}; use std::{borrow::Cow, collections::BTreeMap, collections::HashMap, iter};
use helix_stdx::rope::RopeSliceExt; use helix_stdx::rope::RopeSliceExt;
use tree_sitter::{Query, QueryCursor, QueryPredicateArg}; use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
@ -1040,6 +1040,41 @@ pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&
scopes scopes
} }
pub fn get_breadcrumbs(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<String> {
let mut breadcrumb_set: BTreeMap<usize, String> = BTreeMap::new();
if let Some(syntax) = syntax {
let pos = text.char_to_byte(pos);
let mut node = match syntax
.tree()
.root_node()
.descendant_for_byte_range(pos, pos)
{
Some(node) => node,
None => return vec![],
};
while let Some(parent) = node.parent() {
if node.is_extra() {
continue;
}
if node.is_named() {
let line_idx = text.byte_to_line(parent.start_byte());
let line: String = text.line(line_idx).into();
breadcrumb_set.insert(line_idx, format!("{}: {}", line_idx + 1, line.trim_end()));
}
node = parent;
}
}
let breadcrumbs: Vec<String> = breadcrumb_set
.into_iter()
.map(|(_k, v)| v.into())
// Remove the `source_code` node
.skip(1)
.collect();
breadcrumbs
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -1667,6 +1667,43 @@ fn tree_sitter_scopes(
Ok(()) Ok(())
} }
fn tree_sitter_breadcrumbs(
cx: &mut compositor::Context,
_args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let pos = doc.selection(view.id).primary().cursor(text);
let breadcrumbs = indent::get_breadcrumbs(doc.syntax(), text, pos);
let language = match doc.language_config() {
Some(l) => &l.language_id,
None => "txt",
};
let contents = format!("```{}\n{}\n```\n", language, breadcrumbs.join("\n"));
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let popup = Popup::new("hover", contents).auto_close(true);
compositor.replace_or_push("hover", popup);
},
));
Ok(call)
};
cx.jobs.callback(callback);
Ok(())
}
fn tree_sitter_highlight_name( fn tree_sitter_highlight_name(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: Args, _args: Args,
@ -3195,6 +3232,17 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
..Signature::DEFAULT ..Signature::DEFAULT
}, },
}, },
TypableCommand {
name: "tree-sitter-breadcrumbs",
aliases: &["breadcrumbs", "bread"],
doc: "Display lines of parents to reach the token under the cursor. Useful to know where you are in the program.",
fun: tree_sitter_breadcrumbs,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
},
TypableCommand { TypableCommand {
name: "tree-sitter-highlight-name", name: "tree-sitter-highlight-name",
aliases: &[], aliases: &[],