diff --git a/starlark_lsp/src/server.rs b/starlark_lsp/src/server.rs index faac435fd..d479275c1 100644 --- a/starlark_lsp/src/server.rs +++ b/starlark_lsp/src/server.rs @@ -43,6 +43,7 @@ use lsp_types::notification::DidOpenTextDocument; use lsp_types::notification::LogMessage; use lsp_types::notification::PublishDiagnostics; use lsp_types::request::Completion; +use lsp_types::request::DocumentSymbolRequest; use lsp_types::request::GotoDefinition; use lsp_types::request::HoverRequest; use lsp_types::CompletionItem; @@ -55,6 +56,8 @@ use lsp_types::Diagnostic; use lsp_types::DidChangeTextDocumentParams; use lsp_types::DidCloseTextDocumentParams; use lsp_types::DidOpenTextDocumentParams; +use lsp_types::DocumentSymbolParams; +use lsp_types::DocumentSymbolResponse; use lsp_types::Documentation; use lsp_types::GotoDefinitionParams; use lsp_types::GotoDefinitionResponse; @@ -107,6 +110,7 @@ use crate::definition::IdentifierDefinition; use crate::definition::LspModule; use crate::inspect::AstModuleInspect; use crate::inspect::AutocompleteType; +use crate::symbols; use crate::symbols::find_symbols_at_location; /// The request to get the file contents for a starlark: URI @@ -408,6 +412,7 @@ impl Backend { definition_provider, completion_provider: Some(CompletionOptions::default()), hover_provider: Some(HoverProviderCapability::Simple(true)), + document_symbol_provider: Some(OneOf::Left(true)), ..ServerCapabilities::default() } } @@ -506,6 +511,11 @@ impl Backend { self.send_response(new_response(id, self.hover_info(params, initialize_params))); } + /// Offer an overview of symbols in the current document. + fn document_symbols(&self, id: RequestId, params: DocumentSymbolParams) { + self.send_response(new_response(id, self.get_document_symbols(params))); + } + /// Get the file contents of a starlark: URI. fn get_starlark_file_contents(&self, id: RequestId, params: StarlarkFileContentsParams) { let response: anyhow::Result<_> = match params.uri { @@ -1166,6 +1176,23 @@ impl Backend { }) } + fn get_document_symbols( + &self, + params: DocumentSymbolParams, + ) -> anyhow::Result { + let uri = params.text_document.uri.try_into()?; + + let document = match self.get_ast(&uri) { + Some(document) => document, + None => return Ok(DocumentSymbolResponse::Nested(vec![])), + }; + + let result = + symbols::get_document_symbols(document.ast.codemap(), document.ast.statement()); + + Ok(result.into()) + } + fn get_workspace_root( workspace_roots: Option<&Vec>, target: &LspUrl, @@ -1223,6 +1250,8 @@ impl Backend { self.completion(req.id, params, &initialize_params); } else if let Some(params) = as_request::(&req) { self.hover(req.id, params, &initialize_params); + } else if let Some(params) = as_request::(&req) { + self.document_symbols(req.id, params); } else if self.connection.handle_shutdown(&req)? { return Ok(()); } diff --git a/starlark_lsp/src/symbols.rs b/starlark_lsp/src/symbols.rs index 0f63052c9..0138d2df9 100644 --- a/starlark_lsp/src/symbols.rs +++ b/starlark_lsp/src/symbols.rs @@ -18,12 +18,21 @@ //! Find which symbols are in scope at a particular point. use std::collections::HashMap; +use std::ops::Deref; +use lsp_types::DocumentSymbol; +use lsp_types::SymbolKind as LspSymbolKind; use starlark::codemap::CodeMap; +use starlark::codemap::Span; use starlark::docs::DocItem; use starlark::docs::DocParam; use starlark_syntax::codemap::ResolvedPos; +use starlark_syntax::syntax::ast::ArgumentP; use starlark_syntax::syntax::ast::AssignP; +use starlark_syntax::syntax::ast::AssignTargetP; +use starlark_syntax::syntax::ast::AstAssignIdentP; +use starlark_syntax::syntax::ast::AstExprP; +use starlark_syntax::syntax::ast::AstLiteral; use starlark_syntax::syntax::ast::AstPayload; use starlark_syntax::syntax::ast::AstStmtP; use starlark_syntax::syntax::ast::ExprP; @@ -161,16 +170,250 @@ pub(crate) fn find_symbols_at_location( symbols } +pub fn get_document_symbols( + codemap: &CodeMap, + ast: &AstStmtP

, +) -> Vec { + let mut symbols = Vec::new(); + match &ast.node { + StmtP::Expression(expr) => { + if let Some(symbol) = get_document_symbol_for_expr(codemap, None, expr, ast.span) { + symbols.push(symbol); + } + } + StmtP::Assign(assign) => { + if let Some(symbol) = get_document_symbol_for_expr( + codemap, + match &assign.lhs.node { + AssignTargetP::Tuple(_) + | AssignTargetP::Index(_) + | AssignTargetP::Dot(_, _) => None, + AssignTargetP::Identifier(ident) => Some(ident), + }, + &assign.rhs, + ast.span, + ) { + symbols.push(symbol); + } + } + StmtP::Statements(statements) => { + for stmt in statements { + symbols.extend(get_document_symbols(codemap, stmt)); + } + } + StmtP::If(_, body) => { + symbols.extend(get_document_symbols(codemap, body)); + } + StmtP::IfElse(_, bodies) => { + let (if_body, else_body) = bodies.deref(); + symbols.extend(get_document_symbols(codemap, if_body)); + symbols.extend(get_document_symbols(codemap, else_body)); + } + StmtP::For(for_) => { + symbols.extend(get_document_symbols(codemap, &for_.body)); + } + StmtP::Def(def) => { + symbols.push(make_document_symbol( + def.name.ident.clone(), + LspSymbolKind::FUNCTION, + ast.span, + def.name.span, + codemap, + Some( + def.params + .iter() + .filter_map(|param| get_document_symbol_for_parameter(codemap, param)) + .chain(get_document_symbols(codemap, &def.body)) + .collect(), + ), + )); + } + StmtP::Load(load) => { + symbols.push(make_document_symbol( + load.module.node.clone(), + LspSymbolKind::MODULE, + ast.span, + load.module.span, + codemap, + Some( + load.args + .iter() + .map(|loaded_symbol| { + make_document_symbol( + loaded_symbol.local.ident.clone(), + LspSymbolKind::METHOD, + loaded_symbol.span(), + loaded_symbol.local.span, + codemap, + None, + ) + }) + .collect(), + ), + )); + } + + // These don't produce any symbols. + StmtP::Break + | StmtP::Continue + | StmtP::Pass + | StmtP::Return(_) + | StmtP::AssignModify(_, _, _) => {} + } + + symbols +} + +fn get_document_symbol_for_parameter( + codemap: &CodeMap, + param: &ParameterP

, +) -> Option { + match param { + ParameterP::NoArgs => None, + ParameterP::Normal(p, _) + | ParameterP::WithDefaultValue(p, _, _) + | ParameterP::Args(p, _) + | ParameterP::KwArgs(p, _) => Some(make_document_symbol( + p.ident.clone(), + LspSymbolKind::VARIABLE, + p.span, + p.span, + codemap, + None, + )), + } +} + +fn get_document_symbol_for_expr( + codemap: &CodeMap, + name: Option<&AstAssignIdentP

>, + expr: &AstExprP

, + outer_range: Span, +) -> Option { + match &expr.node { + ExprP::Call(call, args) => { + if let ExprP::Identifier(func_name) = &call.node { + // Look for a call to `struct`. We'll require passing in a name from the assignment + // expression. The outer range is the range of the entire assignment expression. + if &func_name.node.ident == "struct" { + name.map(|name| { + make_document_symbol( + name.ident.clone(), + LspSymbolKind::STRUCT, + outer_range, + name.span, + codemap, + Some( + args.iter() + .filter_map(|arg| match &arg.node { + ArgumentP::Named(name, _) => Some(make_document_symbol( + name.node.clone(), + LspSymbolKind::FIELD, + arg.span, + name.span, + codemap, + None, + )), + _ => None, + }) + .collect(), + ), + ) + }) + } else { + // Check if this call has a named argument called "name". If so, we'll assume + // that this is a buildable target, and expose it. + args.iter() + .find_map(|arg| match &arg.node { + ArgumentP::Named(name, value) => match (name, &value.node) { + (name, ExprP::Literal(AstLiteral::String(value))) + if &name.node == "name" => + { + Some(value) + } + _ => None, + }, + _ => None, + }) + .map(|target_name| { + make_document_symbol( + target_name.node.clone(), + LspSymbolKind::CONSTANT, + expr.span, + target_name.span, + codemap, + None, + ) + }) + } + } else { + None + } + } + ExprP::Lambda(lambda) => name.map(|name| { + make_document_symbol( + name.ident.clone(), + LspSymbolKind::FUNCTION, + expr.span, + expr.span, + codemap, + Some( + lambda + .params + .iter() + .filter_map(|param| get_document_symbol_for_parameter(codemap, param)) + .chain(get_document_symbol_for_expr( + codemap, + None, + &lambda.body, + lambda.body.span, + )) + .collect(), + ), + ) + }), + + _ => None, + } +} + +fn make_document_symbol( + name: String, + kind: LspSymbolKind, + range: Span, + selection_range: Span, + codemap: &CodeMap, + children: Option>, +) -> DocumentSymbol { + #[allow(deprecated)] + DocumentSymbol { + name, + detail: None, + kind, + tags: None, + deprecated: None, + range: codemap.resolve_span(range).into(), + selection_range: codemap.resolve_span(selection_range).into(), + children, + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; + use itertools::Itertools; + use lsp_types::DocumentSymbol; + use lsp_types::Position; + use lsp_types::Range; + use lsp_types::SymbolKind as LspSymbolKind; use starlark::syntax::AstModule; use starlark::syntax::Dialect; use starlark_syntax::codemap::ResolvedPos; use starlark_syntax::syntax::module::AstModuleFields; use super::find_symbols_at_location; + use super::get_document_symbols; use super::Symbol; use super::SymbolKind; @@ -317,4 +560,72 @@ my_var = True ]) ); } + + #[test] + fn document_symbols() { + let ast_module = AstModule::parse( + "t.star", + r#"load("foo.star", "exported_a", renamed = "exported_b") + +def method(param): + foo = struct(field = "value") + bar = lambda x: x + 1 + return lambda y: y + 1 + +baz = struct(field = "value") + +some_rule(name = "qux") + "# + .to_owned(), + &Dialect::Standard, + ) + .unwrap(); + + fn format_document_symbol(symbol: DocumentSymbol, indent_level: usize) -> String { + use std::borrow::Cow; + + let inner_indent = " ".repeat(indent_level); + let outer_indent = " ".repeat(indent_level - 1); + let children = match symbol.children { + Some(children) => Cow::Owned(format!( + " [\n{}{}\n{}]", + &inner_indent, + children + .into_iter() + .map(|symbol| format_document_symbol(symbol, indent_level + 1)) + .join(&format!("\n{}", &inner_indent)), + &outer_indent + )), + None => Cow::Borrowed(""), + }; + + format!("{:?} {}{children}", symbol.kind, symbol.name,) + } + + let symbols = get_document_symbols(ast_module.codemap(), ast_module.statement()) + .into_iter() + .map(|symbol| format_document_symbol(symbol, 1)) + .join("\n"); + + assert_eq!( + symbols, + r##"Module foo.star [ + Method exported_a + Method renamed +] +Function method [ + Variable param + Struct foo [ + Field field + ] + Function bar [ + Variable x + ] +] +Struct baz [ + Field field +] +Constant qux"## + ); + } }