Skip to content

Commit c05fce3

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 c05fce3

File tree

2 files changed

+265
-0
lines changed

2 files changed

+265
-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

+236
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,233 @@ 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| get_document_symbol_for_parameter(codemap, param))
226+
.chain(get_document_symbols(codemap, &def.body))
227+
.collect(),
228+
),
229+
));
230+
}
231+
StmtP::Load(load) => {
232+
symbols.push(make_document_symbol(
233+
load.module.node.clone(),
234+
LspSymbolKind::MODULE,
235+
ast.span,
236+
load.module.span,
237+
codemap,
238+
Some(
239+
load.args
240+
.iter()
241+
.map(|loaded_symbol| {
242+
make_document_symbol(
243+
loaded_symbol.local.ident.clone(),
244+
LspSymbolKind::METHOD,
245+
loaded_symbol.span(),
246+
loaded_symbol.local.span,
247+
codemap,
248+
None,
249+
)
250+
})
251+
.collect(),
252+
),
253+
));
254+
}
255+
256+
// These don't produce any symbols.
257+
StmtP::Break
258+
| StmtP::Continue
259+
| StmtP::Pass
260+
| StmtP::Return(_)
261+
| StmtP::AssignModify(_, _, _) => {}
262+
}
263+
264+
symbols
265+
}
266+
267+
fn get_document_symbol_for_parameter<P: AstPayload>(
268+
codemap: &CodeMap,
269+
param: &ParameterP<P>,
270+
) -> Option<DocumentSymbol> {
271+
match param {
272+
ParameterP::NoArgs => None,
273+
ParameterP::Normal(p, _)
274+
| ParameterP::WithDefaultValue(p, _, _)
275+
| ParameterP::Args(p, _)
276+
| ParameterP::KwArgs(p, _) => Some(make_document_symbol(
277+
p.ident.clone(),
278+
LspSymbolKind::VARIABLE,
279+
p.span,
280+
p.span,
281+
codemap,
282+
None,
283+
)),
284+
}
285+
}
286+
287+
fn get_document_symbol_for_expr<P: AstPayload>(
288+
codemap: &CodeMap,
289+
name: Option<&AstAssignIdentP<P>>,
290+
expr: &AstExprP<P>,
291+
outer_range: Span,
292+
) -> Option<DocumentSymbol> {
293+
match &expr.node {
294+
ExprP::Call(call, args) => {
295+
if let ExprP::Identifier(func_name) = &call.node {
296+
// Look for a call to `struct`. We'll require passing in a name from the assignment
297+
// expression. The outer range is the range of the entire assignment expression.
298+
if &func_name.node.ident == "struct" {
299+
name.map(|name| {
300+
make_document_symbol(
301+
name.ident.clone(),
302+
LspSymbolKind::STRUCT,
303+
outer_range,
304+
name.span,
305+
codemap,
306+
Some(
307+
args.iter()
308+
.filter_map(|arg| match &arg.node {
309+
ArgumentP::Named(name, _) => Some(make_document_symbol(
310+
name.node.clone(),
311+
LspSymbolKind::FIELD,
312+
arg.span,
313+
name.span,
314+
codemap,
315+
None,
316+
)),
317+
_ => None,
318+
})
319+
.collect(),
320+
),
321+
)
322+
})
323+
} else {
324+
// Check if this call has a named argument called "name". If so, we'll assume
325+
// that this is a buildable target, and expose it.
326+
args.iter()
327+
.find_map(|arg| match &arg.node {
328+
ArgumentP::Named(name, value) => match (name, &value.node) {
329+
(name, ExprP::Literal(AstLiteral::String(value)))
330+
if &name.node == "name" =>
331+
{
332+
Some(value)
333+
}
334+
_ => None,
335+
},
336+
_ => None,
337+
})
338+
.map(|target_name| {
339+
make_document_symbol(
340+
target_name.node.clone(),
341+
LspSymbolKind::CONSTANT,
342+
expr.span,
343+
target_name.span,
344+
codemap,
345+
None,
346+
)
347+
})
348+
}
349+
} else {
350+
None
351+
}
352+
}
353+
ExprP::Lambda(lambda) => name.map(|name| {
354+
make_document_symbol(
355+
name.ident.clone(),
356+
LspSymbolKind::FUNCTION,
357+
expr.span,
358+
expr.span,
359+
codemap,
360+
Some(
361+
lambda
362+
.params
363+
.iter()
364+
.filter_map(|param| get_document_symbol_for_parameter(codemap, param))
365+
.chain(get_document_symbol_for_expr(
366+
codemap,
367+
None,
368+
&lambda.body,
369+
lambda.body.span,
370+
))
371+
.collect(),
372+
),
373+
)
374+
}),
375+
376+
_ => None,
377+
}
378+
}
379+
380+
fn make_document_symbol(
381+
name: String,
382+
kind: LspSymbolKind,
383+
range: Span,
384+
selection_range: Span,
385+
codemap: &CodeMap,
386+
children: Option<Vec<DocumentSymbol>>,
387+
) -> DocumentSymbol {
388+
DocumentSymbol {
389+
name,
390+
detail: None,
391+
kind,
392+
tags: None,
393+
deprecated: None,
394+
range: codemap.resolve_span(range).into(),
395+
selection_range: codemap.resolve_span(selection_range).into(),
396+
children,
397+
}
398+
}
399+
164400
#[cfg(test)]
165401
mod tests {
166402
use std::collections::HashMap;

0 commit comments

Comments
 (0)