Skip to content

Commit 5427b3b

Browse files
authored
feat: add a document_symbols support (#9)
1 parent a7ed80b commit 5427b3b

File tree

3 files changed

+279
-14
lines changed

3 files changed

+279
-14
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A Language Server for **proto3** files. It uses tree-sitter parser for all opera
77
- [x] Hover
88
- [x] Go to definition
99
- [x] Diagnostics
10+
- [x] Document symbols outline for message and enums
1011

1112
## Installation
1213

src/lsp.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ use tracing::{debug, info};
33

44
use async_lsp::lsp_types::{
55
DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
6-
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
7-
HoverProviderCapability, InitializeParams, InitializeResult, OneOf, ServerCapabilities,
8-
ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind,
6+
DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse,
7+
Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
8+
OneOf, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind,
99
};
1010
use async_lsp::{ErrorCode, LanguageClient, LanguageServer, ResponseError};
1111
use futures::future::BoxFuture;
@@ -39,6 +39,7 @@ impl LanguageServer for ServerState {
3939
)),
4040
definition_provider: Some(OneOf::Left(true)),
4141
hover_provider: Some(HoverProviderCapability::Simple(true)),
42+
document_symbol_provider: Some(OneOf::Left(true)),
4243
..ServerCapabilities::default()
4344
},
4445
server_info: Some(ServerInfo {
@@ -167,4 +168,35 @@ impl LanguageServer for ServerState {
167168
}
168169
ControlFlow::Continue(())
169170
}
171+
172+
fn document_symbol(
173+
&mut self,
174+
params: DocumentSymbolParams,
175+
) -> BoxFuture<'static, Result<Option<DocumentSymbolResponse>, Self::Error>> {
176+
let uri = params.text_document.uri;
177+
178+
let Some(contents) = self.documents.get(&uri) else {
179+
return Box::pin(async move {
180+
Err(ResponseError::new(
181+
ErrorCode::INVALID_REQUEST,
182+
"uri was never opened",
183+
))
184+
});
185+
};
186+
187+
let Some(parsed) = self.parser.parse(contents.as_bytes()) else {
188+
return Box::pin(async move {
189+
Err(ResponseError::new(
190+
ErrorCode::REQUEST_FAILED,
191+
"ts failed to parse contents",
192+
))
193+
});
194+
};
195+
196+
let locations = parsed.find_document_locations(contents.as_bytes());
197+
198+
let response = DocumentSymbolResponse::Nested(locations);
199+
200+
Box::pin(async move { Ok(Some(response)) })
201+
}
170202
}

src/parser.rs

Lines changed: 243 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::unreachable;
2+
13
use async_lsp::lsp_types::{
2-
Diagnostic, DiagnosticSeverity, Location, MarkedString, Position, PublishDiagnosticsParams,
3-
Range, Url,
4+
Diagnostic, DiagnosticSeverity, DocumentSymbol, Location, MarkedString, Position,
5+
PublishDiagnosticsParams, Range, SymbolKind, Url,
46
};
57
use tracing::info;
68
use tree_sitter::{Node, Tree, TreeCursor};
@@ -15,6 +17,35 @@ pub struct ParsedTree {
1517
tree: Tree,
1618
}
1719

20+
#[derive(Default)]
21+
struct DocumentSymbolTreeBuilder {
22+
// The stack are things we're still in the process of building/parsing.
23+
stack: Vec<(usize, DocumentSymbol)>,
24+
// The found are things we've finished processing/parsing, at the top level of the stack.
25+
found: Vec<DocumentSymbol>,
26+
}
27+
impl DocumentSymbolTreeBuilder {
28+
fn push(&mut self, node: usize, symbol: DocumentSymbol) {
29+
self.stack.push((node, symbol));
30+
}
31+
32+
fn maybe_pop(&mut self, node: usize) {
33+
let should_pop = self.stack.last().map_or(false, |(n, _)| *n == node);
34+
if should_pop {
35+
let (_, explored) = self.stack.pop().unwrap();
36+
if let Some((_, parent)) = self.stack.last_mut() {
37+
parent.children.as_mut().unwrap().push(explored);
38+
} else {
39+
self.found.push(explored);
40+
}
41+
}
42+
}
43+
44+
fn build(self) -> Vec<DocumentSymbol> {
45+
self.found
46+
}
47+
}
48+
1849
impl ProtoParser {
1950
pub fn new() -> Self {
2051
let mut parser = tree_sitter::Parser::new();
@@ -32,10 +63,7 @@ impl ProtoParser {
3263
}
3364

3465
impl ParsedTree {
35-
fn walk_and_collect_kinds<'a>(
36-
cursor: &mut TreeCursor<'a>,
37-
kinds: &[&str],
38-
) -> Vec<Node<'a>> {
66+
fn walk_and_collect_kinds<'a>(cursor: &mut TreeCursor<'a>, kinds: &[&str]) -> Vec<Node<'a>> {
3967
let mut v = vec![];
4068

4169
loop {
@@ -76,12 +104,10 @@ impl ParsedTree {
76104
}
77105
}
78106

79-
fn find_preceeding_comments(&self, nid: usize, content: impl AsRef<[u8]>) -> Option<String> {
107+
fn find_preceding_comments(&self, nid: usize, content: impl AsRef<[u8]>) -> Option<String> {
80108
let root = self.tree.root_node();
81109
let mut cursor = root.walk();
82110

83-
info!("Looking for node with id: {nid}");
84-
85111
Self::advance_cursor_to(&mut cursor, nid);
86112
if !cursor.goto_parent() {
87113
return None;
@@ -134,6 +160,69 @@ impl ParsedTree {
134160
Self::walk_and_collect_kinds(&mut cursor, kinds)
135161
}
136162

163+
pub fn find_document_locations(&self, content: impl AsRef<[u8]>) -> Vec<DocumentSymbol> {
164+
let mut builder = DocumentSymbolTreeBuilder::default();
165+
let content = content.as_ref();
166+
167+
let mut cursor = self.tree.root_node().walk();
168+
169+
self.find_document_locations_inner(&mut builder, &mut cursor, content);
170+
171+
builder.build()
172+
}
173+
174+
fn find_document_locations_inner(
175+
&self,
176+
builder: &mut DocumentSymbolTreeBuilder,
177+
cursor: &'_ mut TreeCursor,
178+
content: &[u8],
179+
) {
180+
let kinds = &["message_name", "enum_name"];
181+
loop {
182+
let node = cursor.node();
183+
184+
if kinds.contains(&node.kind()) {
185+
let name = node.utf8_text(content).unwrap();
186+
let kind = match node.kind() {
187+
"message_name" => SymbolKind::STRUCT,
188+
"enum_name" => SymbolKind::ENUM,
189+
_ => unreachable!("unsupported symbol kind"),
190+
};
191+
let detail = self.find_preceding_comments(node.id(), content);
192+
let message = node.parent().unwrap();
193+
194+
let new_symbol = DocumentSymbol {
195+
name: name.to_string(),
196+
detail,
197+
kind,
198+
tags: None,
199+
deprecated: None,
200+
range: Range {
201+
start: ts_to_lsp_position(&message.start_position()),
202+
end: ts_to_lsp_position(&message.end_position()),
203+
},
204+
selection_range: Range {
205+
start: ts_to_lsp_position(&node.start_position()),
206+
end: ts_to_lsp_position(&node.end_position()),
207+
},
208+
children: Some(vec![]),
209+
};
210+
211+
builder.push(message.id(), new_symbol);
212+
}
213+
214+
if cursor.goto_first_child() {
215+
self.find_document_locations_inner(builder, cursor, content);
216+
builder.maybe_pop(node.id());
217+
cursor.goto_parent();
218+
}
219+
220+
if !cursor.goto_next_sibling() {
221+
break;
222+
}
223+
}
224+
}
225+
137226
pub fn definition(
138227
&self,
139228
pos: &Position,
@@ -168,7 +257,7 @@ impl ParsedTree {
168257
.find_childrens_by_kinds(&["message_name", "enum_name", "service_name", "rpc_name"])
169258
.into_iter()
170259
.filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text)
171-
.filter_map(|n| self.find_preceeding_comments(n.id(), content.as_ref()))
260+
.filter_map(|n| self.find_preceding_comments(n.id(), content.as_ref()))
172261
.map(MarkedString::String)
173262
.collect(),
174263
None => vec![],
@@ -200,7 +289,9 @@ impl ParsedTree {
200289

201290
#[cfg(test)]
202291
mod test {
203-
use async_lsp::lsp_types::{DiagnosticSeverity, MarkedString, Position, Range, Url};
292+
use async_lsp::lsp_types::{
293+
DiagnosticSeverity, DocumentSymbol, MarkedString, Position, Range, SymbolKind, Url,
294+
};
204295

205296
use super::ProtoParser;
206297

@@ -335,6 +426,147 @@ Author has a name and a country where they were born"#
335426
);
336427
}
337428

429+
#[test]
430+
fn test_document_symbols() {
431+
let contents = r#"syntax = "proto3";
432+
433+
package com.symbols;
434+
435+
// outer 1 comment
436+
message Outer1 {
437+
message Inner1 {
438+
string name = 1;
439+
};
440+
441+
Inner1 i = 1;
442+
}
443+
444+
message Outer2 {
445+
message Inner2 {
446+
string name = 1;
447+
};
448+
// Inner 3 comment here
449+
message Inner3 {
450+
string name = 1;
451+
452+
enum X {
453+
a = 1;
454+
b = 2;
455+
}
456+
}
457+
Inner1 i = 1;
458+
Inner2 y = 2;
459+
}
460+
461+
"#;
462+
let parsed = ProtoParser::new().parse(contents);
463+
assert!(parsed.is_some());
464+
let tree = parsed.unwrap();
465+
let res = tree.find_document_locations(contents);
466+
467+
assert_eq!(res.len(), 2);
468+
assert_eq!(
469+
res,
470+
vec!(
471+
DocumentSymbol {
472+
name: "Outer1".to_string(),
473+
detail: Some("outer 1 comment".to_string()),
474+
kind: SymbolKind::STRUCT,
475+
tags: None,
476+
range: Range {
477+
start: Position::new(5, 0),
478+
end: Position::new(11, 1),
479+
},
480+
selection_range: Range {
481+
start: Position::new(5, 8),
482+
end: Position::new(5, 14),
483+
},
484+
children: Some(vec!(DocumentSymbol {
485+
name: "Inner1".to_string(),
486+
detail: None,
487+
kind: SymbolKind::STRUCT,
488+
tags: None,
489+
deprecated: None,
490+
range: Range {
491+
start: Position::new(6, 4),
492+
end: Position::new(8, 5),
493+
},
494+
selection_range: Range {
495+
start: Position::new(6, 12),
496+
end: Position::new(6, 18),
497+
},
498+
children: Some(vec!()),
499+
},)),
500+
deprecated: None,
501+
},
502+
DocumentSymbol {
503+
name: "Outer2".to_string(),
504+
detail: None,
505+
kind: SymbolKind::STRUCT,
506+
tags: None,
507+
range: Range {
508+
start: Position::new(13, 0),
509+
end: Position::new(28, 1),
510+
},
511+
selection_range: Range {
512+
start: Position::new(13, 8),
513+
end: Position::new(13, 14),
514+
},
515+
children: Some(vec!(
516+
DocumentSymbol {
517+
name: "Inner2".to_string(),
518+
detail: None,
519+
kind: SymbolKind::STRUCT,
520+
tags: None,
521+
deprecated: None,
522+
range: Range {
523+
start: Position::new(14, 4),
524+
end: Position::new(16, 5),
525+
},
526+
selection_range: Range {
527+
start: Position::new(14, 12),
528+
end: Position::new(14, 18),
529+
},
530+
children: Some(vec!()),
531+
},
532+
DocumentSymbol {
533+
name: "Inner3".to_string(),
534+
detail: Some("Inner 3 comment here".to_string()),
535+
kind: SymbolKind::STRUCT,
536+
tags: None,
537+
deprecated: None,
538+
range: Range {
539+
start: Position::new(18, 4),
540+
end: Position::new(25, 5),
541+
},
542+
selection_range: Range {
543+
start: Position::new(18, 12),
544+
end: Position::new(18, 18),
545+
},
546+
children: Some(vec!(DocumentSymbol {
547+
name: "X".to_string(),
548+
detail: None,
549+
kind: SymbolKind::ENUM,
550+
tags: None,
551+
deprecated: None,
552+
range: Range {
553+
start: Position::new(21, 8),
554+
end: Position::new(24, 9),
555+
},
556+
selection_range: Range {
557+
start: Position::new(21, 13),
558+
end: Position::new(21, 14),
559+
},
560+
children: Some(vec!()),
561+
})),
562+
}
563+
)),
564+
deprecated: None,
565+
},
566+
)
567+
);
568+
}
569+
338570
#[test]
339571
fn test_goto_definition() {
340572
let url = "file://foo/bar.proto";

0 commit comments

Comments
 (0)