feat: upgrade `tree-sitter-injection` to show injections for entire file

pull/12759/head
Nikita Revenco 2025-02-03 14:58:52 +00:00 committed by Nik Revenco
parent 92469b431a
commit 37f8cbed3c
4 changed files with 83 additions and 44 deletions

View File

@ -56,7 +56,7 @@
| `:lsp-stop` | Stops the given language servers, or all language servers that are used by the current file if no arguments are supplied |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:tree-sitter-highlight-name` | Display name of tree-sitter highlight scope under the cursor. |
| `:tree-sitter-injection` | Display injected language for the primary range. |
| `:tree-sitter-injections` | Display injected languages for the file. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
| `:debug-eval` | Evaluate expression in current debug context. |

View File

@ -1,6 +1,7 @@
//! This module contains the functionality toggle comments on lines over the selection
//! using the comment character defined in the user's `languages.toml`
use slotmap::DefaultKey as LayerId;
use smallvec::SmallVec;
use crate::{syntax::BlockCommentToken, Change, Range, Rope, RopeSlice, Syntax, Tendril};
@ -25,6 +26,38 @@ pub fn get_comment_token<'a, S: AsRef<str>>(
.max_by_key(|token| token.len())
}
/// For a given range in the document, get the most tightly encompassing
/// injection layer corresponding to that range.
pub fn injection_for_range(syntax: &Syntax, from: usize, to: usize) -> Option<LayerId> {
let mut best_fit = None;
let mut min_gap = usize::MAX;
for (layer_id, layer) in &syntax.layers {
for ts_range in &layer.ranges {
let is_encompassing = ts_range.start_byte <= from && ts_range.end_byte >= to;
if is_encompassing {
let this_gap = ts_range.end_byte - ts_range.start_byte;
let config = syntax.layer_config(layer_id);
let has_comment_tokens =
config.comment_tokens.is_some() || config.block_comment_tokens.is_some();
if this_gap < min_gap
// ignore the language family for which it won't make
// sense to consider their comment.
//
// This includes, for instance, `comment`, `jsdoc`, `regex`
&& has_comment_tokens
{
best_fit = Some(layer_id);
min_gap = this_gap;
}
}
}
}
best_fit
}
pub fn get_injected_tokens(
syntax: Option<&Syntax>,
start: usize,
@ -33,8 +66,7 @@ pub fn get_injected_tokens(
// Find the injection with the most tightly encompassing range.
syntax
.and_then(|syntax| {
syntax
.injection_for_range(start, end)
injection_for_range(syntax, start, end)
.map(|language_id| syntax.layer_config(language_id))
.map(|config| {
(

View File

@ -1437,36 +1437,6 @@ impl Syntax {
Arc::clone(&loader.language_configs[language_id])
}
/// For a given range in the document, get the most tightly encompassing
/// injection layer corresponding to that range.
pub fn injection_for_range(&self, from: usize, to: usize) -> Option<LayerId> {
let mut best_fit = None;
let mut min_gap = usize::MAX;
for (layer_id, layer) in &self.layers {
for ts_range in &layer.ranges {
let is_encompassing = ts_range.start_byte <= from && ts_range.end_byte >= to;
if is_encompassing {
let this_gap = ts_range.end_byte - ts_range.start_byte;
if this_gap < min_gap
// ignore the language family for which it won't make
// sense to consider their comment.
//
// Since uncommenting would attempt to use the comment
// language's non-existing comment tokens
// TODO: add this as a language configuration key?
&& !matches!(self.layer_config(layer_id).language_name.as_ref(), "jsdoc" | "comment" | "regex")
{
best_fit = Some(layer_id);
min_gap = this_gap;
}
}
}
}
best_fit
}
pub fn tree(&self) -> &Tree {
self.layers[self.root].tree()
}

View File

@ -1604,7 +1604,7 @@ fn tree_sitter_scopes(
Ok(())
}
fn tree_sitter_injection(
fn tree_sitter_injections(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
@ -1613,20 +1613,57 @@ fn tree_sitter_injection(
return Ok(());
}
let (view, doc) = current!(cx.editor);
let doc = doc!(cx.editor);
let syntax = doc
.syntax()
.context("No tree-sitter grammar found for this file.")?;
let range = doc.selection(view.id).primary();
let mut ranges = vec![];
let language_name = syntax
.injection_for_range(range.from(), range.to())
.map(|language_id| syntax.layer_config(language_id).language_name.clone())
.context("No injection layer found for the current range.")?;
for (language_id, layer) in &syntax.layers {
let language_name = &syntax.layer_config(language_id).language_name;
for range in &layer.ranges {
ranges.push((range, language_name.clone()));
}
}
cx.editor.set_status(language_name);
if ranges.is_empty() {
bail!("No injections found for the current file");
}
ranges.sort_unstable_by(|(range_a, _), (range_b, _)| {
range_a
.start_byte
.cmp(&range_b.start_byte)
.then(range_a.end_byte.cmp(&range_b.end_byte))
});
let char_count = doc.text().len_chars();
let mut contents = String::new();
for (range, language_name) in ranges {
let range = if range.end_byte < char_count {
format!("`{}` - `{}`", range.start_byte, range.end_byte)
} else {
"full file".into()
};
writeln!(contents, "**{}**: {}", language_name, range)?;
}
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(())
}
@ -3162,10 +3199,10 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
},
},
TypableCommand {
name: "tree-sitter-injection",
name: "tree-sitter-injections",
aliases: &[],
doc: "Display injected language for the primary range.",
fun: tree_sitter_injection,
doc: "Display injected languages for the file.",
fun: tree_sitter_injections,
signature: CommandSignature::none(),
},
TypableCommand {