Skip to content

Commit 5d6071f

Browse files
committed
Add a document level syntax symbol picker
1 parent 02c5df9 commit 5d6071f

File tree

5 files changed

+226
-2
lines changed

5 files changed

+226
-2
lines changed

book/src/generated/static-cmd.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
| `code_action` | Perform code action | normal: `` <space>a ``, select: `` <space>a `` |
104104
| `buffer_picker` | Open buffer picker | normal: `` <space>b ``, select: `` <space>b `` |
105105
| `jumplist_picker` | Open jumplist picker | normal: `` <space>j ``, select: `` <space>j `` |
106-
| `symbol_picker` | Open symbol picker | normal: `` <space>s ``, select: `` <space>s `` |
106+
| `symbol_picker` | Open symbol picker | |
107107
| `changed_file_picker` | Open changed file picker | normal: `` <space>g ``, select: `` <space>g `` |
108108
| `select_references_to_symbol_under_cursor` | Select symbol references | normal: `` <space>h ``, select: `` <space>h `` |
109109
| `workspace_symbol_picker` | Open workspace symbol picker | normal: `` <space>S ``, select: `` <space>S `` |
@@ -294,3 +294,5 @@
294294
| `extend_to_word` | Extend to a two-character label | select: `` gw `` |
295295
| `goto_next_tabstop` | goto next snippet placeholder | |
296296
| `goto_prev_tabstop` | goto next snippet placeholder | |
297+
| `syntax_symbol_picker` | Open a picker of symbols from the syntax tree | |
298+
| `lsp_or_syntax_symbol_picker` | Open an LSP symbol picker if available, or syntax otherwise | normal: `` <space>s ``, select: `` <space>s `` |

helix-core/src/syntax.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ pub struct LanguageConfiguration {
154154
#[serde(skip)]
155155
pub(crate) indent_query: OnceCell<Option<Query>>,
156156
#[serde(skip)]
157+
symbols_query: OnceCell<Option<Query>>,
158+
#[serde(skip)]
157159
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
158160
#[serde(skip_serializing_if = "Option::is_none")]
159161
pub debugger: Option<DebugAdapterConfig>,
@@ -798,6 +800,12 @@ impl LanguageConfiguration {
798800
.as_ref()
799801
}
800802

803+
pub fn symbols_query(&self) -> Option<&Query> {
804+
self.symbols_query
805+
.get_or_init(|| self.load_query("symbols.scm"))
806+
.as_ref()
807+
}
808+
801809
pub fn textobject_query(&self) -> Option<&TextObjectQuery> {
802810
self.textobject_query
803811
.get_or_init(|| {
@@ -1412,6 +1420,51 @@ impl Syntax {
14121420
self.layers[self.root].tree()
14131421
}
14141422

1423+
pub fn captures<'a>(
1424+
&'a self,
1425+
query: &'a Query,
1426+
source: RopeSlice<'a>,
1427+
range: Option<std::ops::Range<usize>>,
1428+
) -> impl Iterator<Item = (QueryMatch<'a, 'a>, usize)> + 'a {
1429+
struct Captures<'a> {
1430+
// The query cursor must live as long as the captures iterator so
1431+
// we need to bind them together in this struct.
1432+
_cursor: QueryCursor,
1433+
captures: QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>,
1434+
}
1435+
1436+
impl<'a> Iterator for Captures<'a> {
1437+
type Item = (QueryMatch<'a, 'a>, usize);
1438+
1439+
fn next(&mut self) -> Option<Self::Item> {
1440+
self.captures.next()
1441+
}
1442+
}
1443+
1444+
let mut cursor = PARSER.with(|ts_parser| {
1445+
let highlighter = &mut ts_parser.borrow_mut();
1446+
highlighter.cursors.pop().unwrap_or_default()
1447+
});
1448+
1449+
// The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
1450+
// prevents them from being moved. But both of these values are really just
1451+
// pointers, so it's actually ok to move them.
1452+
let cursor_ref = unsafe {
1453+
mem::transmute::<&mut tree_sitter::QueryCursor, &mut tree_sitter::QueryCursor>(
1454+
&mut cursor,
1455+
)
1456+
};
1457+
1458+
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
1459+
cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT);
1460+
1461+
let captures = cursor_ref.captures(query, self.tree().root_node(), RopeProvider(source));
1462+
Captures {
1463+
_cursor: cursor,
1464+
captures,
1465+
}
1466+
}
1467+
14151468
/// Iterate over the highlighted regions for a given slice of source code.
14161469
pub fn highlight_iter<'a>(
14171470
&'a self,

helix-term/src/commands.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub(crate) mod dap;
22
pub(crate) mod lsp;
3+
pub(crate) mod syntax;
34
pub(crate) mod typed;
45

56
pub use dap::*;
@@ -11,6 +12,7 @@ use helix_stdx::{
1112
};
1213
use helix_vcs::{FileChange, Hunk};
1314
pub use lsp::*;
15+
pub use syntax::*;
1416
use tui::text::Span;
1517
pub use typed::*;
1618

@@ -587,6 +589,8 @@ impl MappableCommand {
587589
extend_to_word, "Extend to a two-character label",
588590
goto_next_tabstop, "goto next snippet placeholder",
589591
goto_prev_tabstop, "goto next snippet placeholder",
592+
syntax_symbol_picker, "Open a picker of symbols from the syntax tree",
593+
lsp_or_syntax_symbol_picker, "Open an LSP symbol picker if available, or syntax otherwise",
590594
);
591595
}
592596

@@ -6495,3 +6499,24 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) {
64956499
}
64966500
jump_to_label(cx, words, behaviour)
64976501
}
6502+
6503+
fn lsp_or_syntax_symbol_picker(cx: &mut Context) {
6504+
let doc = doc!(cx.editor);
6505+
6506+
if doc
6507+
.language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
6508+
.next()
6509+
.is_some()
6510+
{
6511+
lsp::symbol_picker(cx);
6512+
} else if doc.syntax().is_some()
6513+
&& doc
6514+
.language_config()
6515+
.is_some_and(|config| config.symbols_query().is_some())
6516+
{
6517+
syntax_symbol_picker(cx);
6518+
} else {
6519+
cx.editor
6520+
.set_error("No language server supporting document symbols or syntax info available");
6521+
}
6522+
}

helix-term/src/commands/syntax.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
use helix_core::{tree_sitter::Query, Selection, Uri};
2+
use helix_view::{align_view, Align, DocumentId};
3+
4+
use crate::ui::{overlay::overlaid, picker::PathOrId, Picker, PickerColumn};
5+
6+
use super::Context;
7+
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
9+
enum SymbolKind {
10+
Function,
11+
Macro,
12+
Module,
13+
Constant,
14+
Struct,
15+
Interface,
16+
Type,
17+
Class,
18+
}
19+
20+
impl SymbolKind {
21+
fn as_str(&self) -> &'static str {
22+
match self {
23+
Self::Function => "function",
24+
Self::Macro => "macro",
25+
Self::Module => "module",
26+
Self::Constant => "constant",
27+
Self::Struct => "struct",
28+
Self::Interface => "interface",
29+
Self::Type => "type",
30+
Self::Class => "class",
31+
}
32+
}
33+
}
34+
35+
fn definition_symbol_kind_for_capture(symbols: &Query, capture_index: usize) -> Option<SymbolKind> {
36+
match symbols.capture_names()[capture_index] {
37+
"definition.function" => Some(SymbolKind::Function),
38+
"definition.macro" => Some(SymbolKind::Macro),
39+
"definition.module" => Some(SymbolKind::Module),
40+
"definition.constant" => Some(SymbolKind::Constant),
41+
"definition.struct" => Some(SymbolKind::Struct),
42+
"definition.interface" => Some(SymbolKind::Interface),
43+
"definition.type" => Some(SymbolKind::Type),
44+
"definition.class" => Some(SymbolKind::Class),
45+
_ => None,
46+
}
47+
}
48+
49+
// NOTE: Uri is cheap to clone and DocumentId is Copy
50+
#[derive(Debug, Clone)]
51+
enum UriOrDocumentId {
52+
// TODO: the workspace symbol picker will take advantage of this.
53+
#[allow(dead_code)]
54+
Uri(Uri),
55+
Id(DocumentId),
56+
}
57+
58+
impl UriOrDocumentId {
59+
fn path_or_id(&self) -> Option<PathOrId<'_>> {
60+
match self {
61+
Self::Id(id) => Some(PathOrId::Id(*id)),
62+
Self::Uri(uri) => uri.as_path().map(PathOrId::Path),
63+
}
64+
}
65+
}
66+
67+
#[derive(Debug)]
68+
struct Symbol {
69+
kind: SymbolKind,
70+
name: String,
71+
start: usize,
72+
end: usize,
73+
start_line: usize,
74+
end_line: usize,
75+
doc: UriOrDocumentId,
76+
}
77+
78+
pub fn syntax_symbol_picker(cx: &mut Context) {
79+
let doc = doc!(cx.editor);
80+
let Some((syntax, lang_config)) = doc.syntax().zip(doc.language_config()) else {
81+
cx.editor
82+
.set_error("Syntax tree is not available on this buffer");
83+
return;
84+
};
85+
let Some(symbols_query) = lang_config.symbols_query() else {
86+
cx.editor
87+
.set_error("Syntax-based symbols information not available for this language");
88+
return;
89+
};
90+
91+
let doc_id = doc.id();
92+
let text = doc.text();
93+
94+
let columns = vec![
95+
PickerColumn::new("kind", |symbol: &Symbol, _| symbol.kind.as_str().into()),
96+
PickerColumn::new("name", |symbol: &Symbol, _| symbol.name.as_str().into()),
97+
];
98+
99+
let symbols = syntax
100+
.captures(symbols_query, text.slice(..), None)
101+
.filter_map(move |(match_, capture_index)| {
102+
let capture = match_.captures[capture_index];
103+
let kind = definition_symbol_kind_for_capture(symbols_query, capture.index as usize)?;
104+
let node = capture.node;
105+
let start = text.byte_to_char(node.start_byte());
106+
let end = text.byte_to_char(node.end_byte());
107+
108+
Some(Symbol {
109+
kind,
110+
name: text.slice(start..end).to_string(),
111+
start,
112+
end,
113+
114+
start_line: text.char_to_line(start),
115+
end_line: text.char_to_line(end),
116+
doc: UriOrDocumentId::Id(doc_id),
117+
})
118+
});
119+
120+
let picker = Picker::new(
121+
columns,
122+
1, // name
123+
symbols,
124+
(),
125+
move |cx, symbol, action| {
126+
cx.editor.switch(doc_id, action);
127+
let view = view_mut!(cx.editor);
128+
let doc = doc_mut!(cx.editor, &doc_id);
129+
doc.set_selection(view.id, Selection::single(symbol.start, symbol.end));
130+
if action.align_view(view, doc.id()) {
131+
align_view(doc, view, Align::Center)
132+
}
133+
},
134+
)
135+
.with_preview(|_editor, symbol| {
136+
Some((
137+
symbol.doc.path_or_id()?,
138+
Some((symbol.start_line, symbol.end_line)),
139+
))
140+
})
141+
.truncate_start(false);
142+
143+
cx.push_layer(Box::new(overlaid(picker)));
144+
}

helix-term/src/keymap/default.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
224224
"F" => file_picker_in_current_directory,
225225
"b" => buffer_picker,
226226
"j" => jumplist_picker,
227-
"s" => symbol_picker,
227+
"s" => lsp_or_syntax_symbol_picker,
228228
"S" => workspace_symbol_picker,
229229
"d" => diagnostics_picker,
230230
"D" => workspace_diagnostics_picker,

0 commit comments

Comments
 (0)