Add a document level syntax symbol picker

This commit is contained in:
Michael Davis 2024-11-08 09:57:20 -05:00
parent 02c5df9031
commit 5d6071f182
No known key found for this signature in database
5 changed files with 226 additions and 2 deletions

View file

@ -103,7 +103,7 @@
| `code_action` | Perform code action | normal: `` <space>a ``, select: `` <space>a `` |
| `buffer_picker` | Open buffer picker | normal: `` <space>b ``, select: `` <space>b `` |
| `jumplist_picker` | Open jumplist picker | normal: `` <space>j ``, select: `` <space>j `` |
| `symbol_picker` | Open symbol picker | normal: `` <space>s ``, select: `` <space>s `` |
| `symbol_picker` | Open symbol picker | |
| `changed_file_picker` | Open changed file picker | normal: `` <space>g ``, select: `` <space>g `` |
| `select_references_to_symbol_under_cursor` | Select symbol references | normal: `` <space>h ``, select: `` <space>h `` |
| `workspace_symbol_picker` | Open workspace symbol picker | normal: `` <space>S ``, select: `` <space>S `` |
@ -294,3 +294,5 @@
| `extend_to_word` | Extend to a two-character label | select: `` gw `` |
| `goto_next_tabstop` | goto next snippet placeholder | |
| `goto_prev_tabstop` | goto next snippet placeholder | |
| `syntax_symbol_picker` | Open a picker of symbols from the syntax tree | |
| `lsp_or_syntax_symbol_picker` | Open an LSP symbol picker if available, or syntax otherwise | normal: `` <space>s ``, select: `` <space>s `` |

View file

@ -154,6 +154,8 @@ pub struct LanguageConfiguration {
#[serde(skip)]
pub(crate) indent_query: OnceCell<Option<Query>>,
#[serde(skip)]
symbols_query: OnceCell<Option<Query>>,
#[serde(skip)]
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debugger: Option<DebugAdapterConfig>,
@ -798,6 +800,12 @@ impl LanguageConfiguration {
.as_ref()
}
pub fn symbols_query(&self) -> Option<&Query> {
self.symbols_query
.get_or_init(|| self.load_query("symbols.scm"))
.as_ref()
}
pub fn textobject_query(&self) -> Option<&TextObjectQuery> {
self.textobject_query
.get_or_init(|| {
@ -1412,6 +1420,51 @@ impl Syntax {
self.layers[self.root].tree()
}
pub fn captures<'a>(
&'a self,
query: &'a Query,
source: RopeSlice<'a>,
range: Option<std::ops::Range<usize>>,
) -> impl Iterator<Item = (QueryMatch<'a, 'a>, usize)> + 'a {
struct Captures<'a> {
// The query cursor must live as long as the captures iterator so
// we need to bind them together in this struct.
_cursor: QueryCursor,
captures: QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>,
}
impl<'a> Iterator for Captures<'a> {
type Item = (QueryMatch<'a, 'a>, usize);
fn next(&mut self) -> Option<Self::Item> {
self.captures.next()
}
}
let mut cursor = PARSER.with(|ts_parser| {
let highlighter = &mut ts_parser.borrow_mut();
highlighter.cursors.pop().unwrap_or_default()
});
// The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
// prevents them from being moved. But both of these values are really just
// pointers, so it's actually ok to move them.
let cursor_ref = unsafe {
mem::transmute::<&mut tree_sitter::QueryCursor, &mut tree_sitter::QueryCursor>(
&mut cursor,
)
};
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT);
let captures = cursor_ref.captures(query, self.tree().root_node(), RopeProvider(source));
Captures {
_cursor: cursor,
captures,
}
}
/// Iterate over the highlighted regions for a given slice of source code.
pub fn highlight_iter<'a>(
&'a self,

View file

@ -1,5 +1,6 @@
pub(crate) mod dap;
pub(crate) mod lsp;
pub(crate) mod syntax;
pub(crate) mod typed;
pub use dap::*;
@ -11,6 +12,7 @@ use helix_stdx::{
};
use helix_vcs::{FileChange, Hunk};
pub use lsp::*;
pub use syntax::*;
use tui::text::Span;
pub use typed::*;
@ -587,6 +589,8 @@ impl MappableCommand {
extend_to_word, "Extend to a two-character label",
goto_next_tabstop, "goto next snippet placeholder",
goto_prev_tabstop, "goto next snippet placeholder",
syntax_symbol_picker, "Open a picker of symbols from the syntax tree",
lsp_or_syntax_symbol_picker, "Open an LSP symbol picker if available, or syntax otherwise",
);
}
@ -6495,3 +6499,24 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) {
}
jump_to_label(cx, words, behaviour)
}
fn lsp_or_syntax_symbol_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
if doc
.language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
.next()
.is_some()
{
lsp::symbol_picker(cx);
} else if doc.syntax().is_some()
&& doc
.language_config()
.is_some_and(|config| config.symbols_query().is_some())
{
syntax_symbol_picker(cx);
} else {
cx.editor
.set_error("No language server supporting document symbols or syntax info available");
}
}

View file

@ -0,0 +1,144 @@
use helix_core::{tree_sitter::Query, Selection, Uri};
use helix_view::{align_view, Align, DocumentId};
use crate::ui::{overlay::overlaid, picker::PathOrId, Picker, PickerColumn};
use super::Context;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum SymbolKind {
Function,
Macro,
Module,
Constant,
Struct,
Interface,
Type,
Class,
}
impl SymbolKind {
fn as_str(&self) -> &'static str {
match self {
Self::Function => "function",
Self::Macro => "macro",
Self::Module => "module",
Self::Constant => "constant",
Self::Struct => "struct",
Self::Interface => "interface",
Self::Type => "type",
Self::Class => "class",
}
}
}
fn definition_symbol_kind_for_capture(symbols: &Query, capture_index: usize) -> Option<SymbolKind> {
match symbols.capture_names()[capture_index] {
"definition.function" => Some(SymbolKind::Function),
"definition.macro" => Some(SymbolKind::Macro),
"definition.module" => Some(SymbolKind::Module),
"definition.constant" => Some(SymbolKind::Constant),
"definition.struct" => Some(SymbolKind::Struct),
"definition.interface" => Some(SymbolKind::Interface),
"definition.type" => Some(SymbolKind::Type),
"definition.class" => Some(SymbolKind::Class),
_ => None,
}
}
// NOTE: Uri is cheap to clone and DocumentId is Copy
#[derive(Debug, Clone)]
enum UriOrDocumentId {
// TODO: the workspace symbol picker will take advantage of this.
#[allow(dead_code)]
Uri(Uri),
Id(DocumentId),
}
impl UriOrDocumentId {
fn path_or_id(&self) -> Option<PathOrId<'_>> {
match self {
Self::Id(id) => Some(PathOrId::Id(*id)),
Self::Uri(uri) => uri.as_path().map(PathOrId::Path),
}
}
}
#[derive(Debug)]
struct Symbol {
kind: SymbolKind,
name: String,
start: usize,
end: usize,
start_line: usize,
end_line: usize,
doc: UriOrDocumentId,
}
pub fn syntax_symbol_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
let Some((syntax, lang_config)) = doc.syntax().zip(doc.language_config()) else {
cx.editor
.set_error("Syntax tree is not available on this buffer");
return;
};
let Some(symbols_query) = lang_config.symbols_query() else {
cx.editor
.set_error("Syntax-based symbols information not available for this language");
return;
};
let doc_id = doc.id();
let text = doc.text();
let columns = vec![
PickerColumn::new("kind", |symbol: &Symbol, _| symbol.kind.as_str().into()),
PickerColumn::new("name", |symbol: &Symbol, _| symbol.name.as_str().into()),
];
let symbols = syntax
.captures(symbols_query, text.slice(..), None)
.filter_map(move |(match_, capture_index)| {
let capture = match_.captures[capture_index];
let kind = definition_symbol_kind_for_capture(symbols_query, capture.index as usize)?;
let node = capture.node;
let start = text.byte_to_char(node.start_byte());
let end = text.byte_to_char(node.end_byte());
Some(Symbol {
kind,
name: text.slice(start..end).to_string(),
start,
end,
start_line: text.char_to_line(start),
end_line: text.char_to_line(end),
doc: UriOrDocumentId::Id(doc_id),
})
});
let picker = Picker::new(
columns,
1, // name
symbols,
(),
move |cx, symbol, action| {
cx.editor.switch(doc_id, action);
let view = view_mut!(cx.editor);
let doc = doc_mut!(cx.editor, &doc_id);
doc.set_selection(view.id, Selection::single(symbol.start, symbol.end));
if action.align_view(view, doc.id()) {
align_view(doc, view, Align::Center)
}
},
)
.with_preview(|_editor, symbol| {
Some((
symbol.doc.path_or_id()?,
Some((symbol.start_line, symbol.end_line)),
))
})
.truncate_start(false);
cx.push_layer(Box::new(overlaid(picker)));
}

View file

@ -224,7 +224,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"F" => file_picker_in_current_directory,
"b" => buffer_picker,
"j" => jumplist_picker,
"s" => symbol_picker,
"s" => lsp_or_syntax_symbol_picker,
"S" => workspace_symbol_picker,
"d" => diagnostics_picker,
"D" => workspace_diagnostics_picker,