From c54f886b1501875ba1a67a99a18c1efb3a47c5a3 Mon Sep 17 00:00:00 2001 From: Rafael Oliveira Date: Tue, 22 Apr 2025 13:20:33 -0300 Subject: [PATCH] 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. --- helix-core/src/indent.rs | 37 +++++++++++++++++++++++- helix-term/src/commands/typed.rs | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 04ce9a28d..3617ef981 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -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 tree_sitter::{Query, QueryCursor, QueryPredicateArg}; @@ -1040,6 +1040,41 @@ pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<& scopes } +pub fn get_breadcrumbs(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec { + let mut breadcrumb_set: BTreeMap = 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 = breadcrumb_set + .into_iter() + .map(|(_k, v)| v.into()) + // Remove the `source_code` node + .skip(1) + .collect(); + breadcrumbs +} + #[cfg(test)] mod test { use super::*; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index c35ff714a..445160621 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1667,6 +1667,43 @@ fn tree_sitter_scopes( 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( cx: &mut compositor::Context, _args: Args, @@ -3195,6 +3232,17 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ ..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 { name: "tree-sitter-highlight-name", aliases: &[],