Skip to content

Commit 28bbf76

Browse files
committed
feat: support document symbols requests in lsp
This allows requesting a list/tree of symbols in the current document. The following items are currently exposed: - `load` statements: - The module path - The loaded symbol names - `def` statements: - The function node - Argument names - Closures, when assigned to a named variable - Structs (calls to `struct`), when assigned to a named variable - Build system targets (function calls containing a `name` argument)
1 parent 4419c9d commit 28bbf76

File tree

2 files changed

+246
-0
lines changed

2 files changed

+246
-0
lines changed

starlark_lsp/src/server.rs

+29
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use lsp_types::notification::DidOpenTextDocument;
4343
use lsp_types::notification::LogMessage;
4444
use lsp_types::notification::PublishDiagnostics;
4545
use lsp_types::request::Completion;
46+
use lsp_types::request::DocumentSymbolRequest;
4647
use lsp_types::request::GotoDefinition;
4748
use lsp_types::request::HoverRequest;
4849
use lsp_types::CompletionItem;
@@ -55,6 +56,8 @@ use lsp_types::Diagnostic;
5556
use lsp_types::DidChangeTextDocumentParams;
5657
use lsp_types::DidCloseTextDocumentParams;
5758
use lsp_types::DidOpenTextDocumentParams;
59+
use lsp_types::DocumentSymbolParams;
60+
use lsp_types::DocumentSymbolResponse;
5861
use lsp_types::Documentation;
5962
use lsp_types::GotoDefinitionParams;
6063
use lsp_types::GotoDefinitionResponse;
@@ -107,6 +110,7 @@ use crate::definition::IdentifierDefinition;
107110
use crate::definition::LspModule;
108111
use crate::inspect::AstModuleInspect;
109112
use crate::inspect::AutocompleteType;
113+
use crate::symbols;
110114
use crate::symbols::find_symbols_at_location;
111115

112116
/// The request to get the file contents for a starlark: URI
@@ -408,6 +412,7 @@ impl<T: LspContext> Backend<T> {
408412
definition_provider,
409413
completion_provider: Some(CompletionOptions::default()),
410414
hover_provider: Some(HoverProviderCapability::Simple(true)),
415+
document_symbol_provider: Some(OneOf::Left(true)),
411416
..ServerCapabilities::default()
412417
}
413418
}
@@ -506,6 +511,11 @@ impl<T: LspContext> Backend<T> {
506511
self.send_response(new_response(id, self.hover_info(params, initialize_params)));
507512
}
508513

514+
/// Offer an overview of symbols in the current document.
515+
fn document_symbols(&self, id: RequestId, params: DocumentSymbolParams) {
516+
self.send_response(new_response(id, self.get_document_symbols(params)));
517+
}
518+
509519
/// Get the file contents of a starlark: URI.
510520
fn get_starlark_file_contents(&self, id: RequestId, params: StarlarkFileContentsParams) {
511521
let response: anyhow::Result<_> = match params.uri {
@@ -1166,6 +1176,23 @@ impl<T: LspContext> Backend<T> {
11661176
})
11671177
}
11681178

1179+
fn get_document_symbols(
1180+
&self,
1181+
params: DocumentSymbolParams,
1182+
) -> anyhow::Result<DocumentSymbolResponse> {
1183+
let uri = params.text_document.uri.try_into()?;
1184+
1185+
let document = match self.get_ast(&uri) {
1186+
Some(document) => document,
1187+
None => return Ok(DocumentSymbolResponse::Nested(vec![])),
1188+
};
1189+
1190+
let result =
1191+
symbols::get_document_symbols(document.ast.codemap(), document.ast.statement());
1192+
1193+
Ok(result.into())
1194+
}
1195+
11691196
fn get_workspace_root(
11701197
workspace_roots: Option<&Vec<WorkspaceFolder>>,
11711198
target: &LspUrl,
@@ -1223,6 +1250,8 @@ impl<T: LspContext> Backend<T> {
12231250
self.completion(req.id, params, &initialize_params);
12241251
} else if let Some(params) = as_request::<HoverRequest>(&req) {
12251252
self.hover(req.id, params, &initialize_params);
1253+
} else if let Some(params) = as_request::<DocumentSymbolRequest>(&req) {
1254+
self.document_symbols(req.id, params);
12261255
} else if self.connection.handle_shutdown(&req)? {
12271256
return Ok(());
12281257
}

starlark_lsp/src/symbols.rs

+217
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@
1818
//! Find which symbols are in scope at a particular point.
1919
2020
use std::collections::HashMap;
21+
use std::ops::Deref;
2122

23+
use lsp_types::DocumentSymbol;
24+
use lsp_types::SymbolKind as LspSymbolKind;
2225
use starlark::codemap::CodeMap;
26+
use starlark::codemap::Span;
2327
use starlark::docs::DocItem;
2428
use starlark::docs::DocParam;
2529
use starlark_syntax::codemap::ResolvedPos;
30+
use starlark_syntax::syntax::ast::ArgumentP;
2631
use starlark_syntax::syntax::ast::AssignP;
32+
use starlark_syntax::syntax::ast::AssignTargetP;
33+
use starlark_syntax::syntax::ast::AstAssignIdentP;
34+
use starlark_syntax::syntax::ast::AstExprP;
35+
use starlark_syntax::syntax::ast::AstLiteral;
2736
use starlark_syntax::syntax::ast::AstPayload;
2837
use starlark_syntax::syntax::ast::AstStmtP;
2938
use starlark_syntax::syntax::ast::ExprP;
@@ -161,6 +170,214 @@ pub(crate) fn find_symbols_at_location<P: AstPayload>(
161170
symbols
162171
}
163172

173+
pub fn get_document_symbols<P: AstPayload>(
174+
codemap: &CodeMap,
175+
ast: &AstStmtP<P>,
176+
) -> Vec<DocumentSymbol> {
177+
let mut symbols = Vec::new();
178+
match &ast.node {
179+
StmtP::Expression(expr) => {
180+
if let Some(symbol) = get_document_symbol_for_expr(codemap, None, expr, ast.span) {
181+
symbols.push(symbol);
182+
}
183+
}
184+
StmtP::Assign(assign) => {
185+
if let Some(symbol) = get_document_symbol_for_expr(
186+
codemap,
187+
match &assign.lhs.node {
188+
AssignTargetP::Tuple(_)
189+
| AssignTargetP::Index(_)
190+
| AssignTargetP::Dot(_, _) => None,
191+
AssignTargetP::Identifier(ident) => Some(ident),
192+
},
193+
&assign.rhs,
194+
ast.span,
195+
) {
196+
symbols.push(symbol);
197+
}
198+
}
199+
StmtP::Statements(statements) => {
200+
for stmt in statements {
201+
symbols.extend(get_document_symbols(codemap, stmt));
202+
}
203+
}
204+
StmtP::If(_, body) => {
205+
symbols.extend(get_document_symbols(codemap, body));
206+
}
207+
StmtP::IfElse(_, bodies) => {
208+
let (if_body, else_body) = bodies.deref();
209+
symbols.extend(get_document_symbols(codemap, if_body));
210+
symbols.extend(get_document_symbols(codemap, else_body));
211+
}
212+
StmtP::For(for_) => {
213+
symbols.extend(get_document_symbols(codemap, &for_.body));
214+
}
215+
StmtP::Def(def) => {
216+
symbols.push(make_document_symbol(
217+
def.name.ident.clone(),
218+
LspSymbolKind::FUNCTION,
219+
ast.span,
220+
def.name.span,
221+
codemap,
222+
Some(
223+
def.params
224+
.iter()
225+
.filter_map(|param| match &param.node {
226+
ParameterP::NoArgs => None,
227+
ParameterP::Normal(p, _)
228+
| ParameterP::WithDefaultValue(p, _, _)
229+
| ParameterP::Args(p, _)
230+
| ParameterP::KwArgs(p, _) => Some(make_document_symbol(
231+
p.ident.clone(),
232+
LspSymbolKind::VARIABLE,
233+
p.span,
234+
p.span,
235+
codemap,
236+
None,
237+
)),
238+
})
239+
.chain(get_document_symbols(codemap, &def.body))
240+
.collect(),
241+
),
242+
));
243+
}
244+
StmtP::Load(load) => {
245+
symbols.push(make_document_symbol(
246+
load.module.node.clone(),
247+
LspSymbolKind::MODULE,
248+
ast.span,
249+
load.module.span,
250+
codemap,
251+
Some(
252+
load.args
253+
.iter()
254+
.map(|loaded_symbol| {
255+
make_document_symbol(
256+
loaded_symbol.local.ident.clone(),
257+
LspSymbolKind::METHOD,
258+
loaded_symbol.span(),
259+
loaded_symbol.local.span,
260+
codemap,
261+
None,
262+
)
263+
})
264+
.collect(),
265+
),
266+
));
267+
}
268+
269+
// These don't produce any symbols.
270+
StmtP::Break
271+
| StmtP::Continue
272+
| StmtP::Pass
273+
| StmtP::Return(_)
274+
| StmtP::AssignModify(_, _, _) => {}
275+
}
276+
277+
symbols
278+
}
279+
280+
fn get_document_symbol_for_expr<P: AstPayload>(
281+
codemap: &CodeMap,
282+
name: Option<&AstAssignIdentP<P>>,
283+
expr: &AstExprP<P>,
284+
outer_range: Span,
285+
) -> Option<DocumentSymbol> {
286+
match &expr.node {
287+
ExprP::Call(call, args) => {
288+
if let ExprP::Identifier(func_name) = &call.node {
289+
// Look for a call to `struct`. We'll require passing in a name from the assignment
290+
// expression. The outer range is the range of the entire assignment expression.
291+
if &func_name.node.ident == "struct" {
292+
name.map(|name| {
293+
make_document_symbol(
294+
name.ident.clone(),
295+
LspSymbolKind::STRUCT,
296+
outer_range,
297+
name.span,
298+
codemap,
299+
Some(
300+
args.iter()
301+
.filter_map(|arg| match &arg.node {
302+
ArgumentP::Named(name, _) => Some(make_document_symbol(
303+
name.node.clone(),
304+
LspSymbolKind::FIELD,
305+
arg.span,
306+
name.span,
307+
codemap,
308+
None,
309+
)),
310+
_ => None,
311+
})
312+
.collect(),
313+
),
314+
)
315+
})
316+
} else {
317+
// Check if this call has a named argument called "name". If so, we'll assume
318+
// that this is a buildable target, and expose it.
319+
args.iter()
320+
.find_map(|arg| match &arg.node {
321+
ArgumentP::Named(name, value) => match (name, &value.node) {
322+
(name, ExprP::Literal(AstLiteral::String(value)))
323+
if &name.node == "name" =>
324+
{
325+
Some(value)
326+
}
327+
_ => None,
328+
},
329+
_ => None,
330+
})
331+
.map(|target_name| {
332+
make_document_symbol(
333+
target_name.node.clone(),
334+
LspSymbolKind::CONSTANT,
335+
expr.span,
336+
target_name.span,
337+
codemap,
338+
None,
339+
)
340+
})
341+
}
342+
} else {
343+
None
344+
}
345+
}
346+
ExprP::Lambda(_) => name.map(|name| {
347+
make_document_symbol(
348+
name.ident.clone(),
349+
LspSymbolKind::FUNCTION,
350+
expr.span,
351+
expr.span,
352+
codemap,
353+
None,
354+
)
355+
}),
356+
357+
_ => None,
358+
}
359+
}
360+
361+
fn make_document_symbol(
362+
name: String,
363+
kind: LspSymbolKind,
364+
range: Span,
365+
selection_range: Span,
366+
codemap: &CodeMap,
367+
children: Option<Vec<DocumentSymbol>>,
368+
) -> DocumentSymbol {
369+
DocumentSymbol {
370+
name,
371+
detail: None,
372+
kind,
373+
tags: None,
374+
deprecated: None,
375+
range: codemap.resolve_span(range).into(),
376+
selection_range: codemap.resolve_span(selection_range).into(),
377+
children,
378+
}
379+
}
380+
164381
#[cfg(test)]
165382
mod tests {
166383
use std::collections::HashMap;

0 commit comments

Comments
 (0)