diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index f0312cc6d6..677e3921ee 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -103,7 +103,8 @@ pub fn server_capabilities( )), references_provider: Some(OneOf::Left(true)), document_highlight_provider: Some(OneOf::Left(true)), - document_symbol_provider: None, + // TODO: Provide a label once https://github.com/gluon-lang/lsp-types/pull/207 is merged + document_symbol_provider: Some(OneOf::Left(true)), workspace_symbol_provider: None, code_action_provider: Some(code_action_provider), code_lens_provider: Some(CodeLensOptions { diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 6edfdc74b6..58fcf99759 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -718,6 +718,49 @@ impl Inner { self.performance.measure(mark); } + async fn document_symbol( + &self, + params: DocumentSymbolParams, + ) -> LspResult> { + if !self.enabled() { + return Ok(None); + } + let mark = self.performance.mark("selection_range"); + let specifier = self.url_map.normalize_url(¶ms.text_document.uri); + + let line_index = + if let Some(line_index) = self.get_line_index_sync(&specifier) { + line_index + } else { + return Err(LspError::invalid_params(format!( + "An unexpected specifier ({}) was provided.", + specifier + ))); + }; + + let req = tsc::RequestMethod::GetNavigationTree(specifier); + let navigation_tree: tsc::NavigationTree = self + .ts_server + .request(self.snapshot(), req) + .await + .map_err(|err| { + error!("Failed to request to tsserver {}", err); + LspError::invalid_request() + })?; + + let response = if let Some(child_items) = navigation_tree.child_items { + let mut document_symbols = Vec::::new(); + for item in child_items { + item.collect_document_symbols(&line_index, &mut document_symbols); + } + Some(DocumentSymbolResponse::Nested(document_symbols)) + } else { + None + }; + self.performance.measure(mark); + Ok(response) + } + async fn formatting( &self, params: DocumentFormattingParams, @@ -2165,6 +2208,13 @@ impl lspower::LanguageServer for LanguageServer { self.0.lock().await.did_change_watched_files(params).await } + async fn document_symbol( + &self, + params: DocumentSymbolParams, + ) -> LspResult> { + self.0.lock().await.document_symbol(params).await + } + async fn formatting( &self, params: DocumentFormattingParams, @@ -3406,6 +3456,410 @@ mod tests { ); } + #[tokio::test] + async fn test_document_symbol() { + let mut harness = LspTestHarness::new(vec![ + ( + LspFixture::Path("initialize_request.json"), + LspResponse::RequestAny, + ), + ( + LspFixture::Path("initialized_notification.json"), + LspResponse::None, + ), + ( + LspFixture::Path("document_symbol_did_open_notification.json"), + LspResponse::None, + ), + ( + LspFixture::Path("document_symbol_request.json"), + LspResponse::Request( + 2, + json!([ + { + "name": "bar", + "kind": 13, + "range": { + "start": { + "line": 17, + "character": 4 + }, + "end": { + "line": 17, + "character": 26 + } + }, + "selectionRange": { + "start": { + "line": 17, + "character": 4 + }, + "end": { + "line": 17, + "character": 7 + } + } + }, + { + "name": "Bar", + "kind": 5, + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 13, + "character": 1 + } + }, + "selectionRange": { + "start": { + "line": 4, + "character": 6 + }, + "end": { + "line": 4, + "character": 9 + } + }, + "children": [ + { + "name": "constructor", + "kind": 9, + "range": { + "start": { + "line": 5, + "character": 2 + }, + "end": { + "line": 5, + "character": 35 + } + }, + "selectionRange": { + "start": { + "line": 5, + "character": 2 + }, + "end": { + "line": 5, + "character": 35 + } + } + }, + { + "name": "baz", + "kind": 6, + "tags": [ + 1 + ], + "range": { + "start": { + "line": 8, + "character": 2 + }, + "end": { + "line": 8, + "character": 25 + } + }, + "selectionRange": { + "start": { + "line": 8, + "character": 2 + }, + "end": { + "line": 8, + "character": 5 + } + } + }, + { + "name": "foo", + "kind": 6, + "range": { + "start": { + "line": 6, + "character": 2 + }, + "end": { + "line": 6, + "character": 24 + } + }, + "selectionRange": { + "start": { + "line": 6, + "character": 2 + }, + "end": { + "line": 6, + "character": 5 + } + } + }, + { + "name": "getStaticBar", + "kind": 6, + "range": { + "start": { + "line": 12, + "character": 2 + }, + "end": { + "line": 12, + "character": 57 + } + }, + "selectionRange": { + "start": { + "line": 12, + "character": 17 + }, + "end": { + "line": 12, + "character": 29 + } + } + }, + { + "name": "staticBar", + "kind": 7, + "range": { + "start": { + "line": 11, + "character": 2 + }, + "end": { + "line": 11, + "character": 32 + } + }, + "selectionRange": { + "start": { + "line": 11, + "character": 9 + }, + "end": { + "line": 11, + "character": 18 + } + } + }, + { + "name": "value", + "kind": 7, + "range": { + "start": { + "line": 9, + "character": 2 + }, + "end": { + "line": 9, + "character": 35 + } + }, + "selectionRange": { + "start": { + "line": 9, + "character": 6 + }, + "end": { + "line": 9, + "character": 11 + } + } + }, + { + "name": "value", + "kind": 7, + "range": { + "start": { + "line": 10, + "character": 2 + }, + "end": { + "line": 10, + "character": 42 + } + }, + "selectionRange": { + "start": { + "line": 10, + "character": 6 + }, + "end": { + "line": 10, + "character": 11 + } + } + }, + { + "name": "x", + "kind": 7, + "range": { + "start": { + "line": 5, + "character": 14 + }, + "end": { + "line": 5, + "character": 30 + } + }, + "selectionRange": { + "start": { + "line": 5, + "character": 21 + }, + "end": { + "line": 5, + "character": 22 + } + } + } + ] + }, + { + "name": "IFoo", + "kind": 11, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 2, + "character": 1 + } + }, + "selectionRange": { + "start": { + "line": 0, + "character": 10 + }, + "end": { + "line": 0, + "character": 14 + } + }, + "children": [ + { + "name": "foo", + "kind": 6, + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 17 + } + }, + "selectionRange": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 5 + } + } + } + ] + }, + { + "name": "Values", + "kind": 10, + "range": { + "start": { + "line": 15, + "character": 0 + }, + "end": { + "line": 15, + "character": 30 + } + }, + "selectionRange": { + "start": { + "line": 15, + "character": 5 + }, + "end": { + "line": 15, + "character": 11 + } + }, + "children": [ + { + "name": "value1", + "kind": 13, + "range": { + "start": { + "line": 15, + "character": 14 + }, + "end": { + "line": 15, + "character": 20 + } + }, + "selectionRange": { + "start": { + "line": 15, + "character": 14 + }, + "end": { + "line": 15, + "character": 20 + } + } + }, + { + "name": "value2", + "kind": 13, + "range": { + "start": { + "line": 15, + "character": 22 + }, + "end": { + "line": 15, + "character": 28 + } + }, + "selectionRange": { + "start": { + "line": 15, + "character": 22 + }, + "end": { + "line": 15, + "character": 28 + } + } + } + ] + } + ]), + ), + ), + ( + LspFixture::Path("shutdown_request.json"), + LspResponse::Request(3, json!(null)), + ), + ( + LspFixture::Path("exit_notification.json"), + LspResponse::None, + ), + ]); + harness.run().await; + } + #[tokio::test] async fn test_folding_range() { let mut harness = LspTestHarness::new(vec![ diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 38f291d036..fef6053976 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -41,7 +41,7 @@ use std::collections::HashSet; use std::thread; use std::{borrow::Cow, cmp}; use std::{collections::HashMap, path::Path}; -use text_size::TextSize; +use text_size::{TextRange, TextSize}; use tokio::sync::mpsc; use tokio::sync::oneshot; @@ -621,6 +621,90 @@ impl NavigationTree { } } + pub fn collect_document_symbols( + &self, + line_index: &LineIndex, + document_symbols: &mut Vec, + ) -> bool { + let mut should_include = self.should_include_entry(); + if !should_include + && self.child_items.as_ref().map_or(true, |v| v.is_empty()) + { + return false; + } + + let children = self + .child_items + .as_ref() + .map_or(&[] as &[NavigationTree], |v| v.as_slice()); + for span in self.spans.iter() { + let range = TextRange::at(span.start.into(), span.length.into()); + let mut symbol_children = Vec::::new(); + for child in children.iter() { + let should_traverse_child = child + .spans + .iter() + .map(|child_span| { + TextRange::at(child_span.start.into(), child_span.length.into()) + }) + .any(|child_range| range.intersect(child_range).is_some()); + if should_traverse_child { + let included_child = + child.collect_document_symbols(line_index, &mut symbol_children); + should_include = should_include || included_child; + } + } + + if should_include { + let mut selection_span = span; + if let Some(name_span) = self.name_span.as_ref() { + let name_range = + TextRange::at(name_span.start.into(), name_span.length.into()); + if range.contains_range(name_range) { + selection_span = name_span; + } + } + + let mut tags: Option> = None; + let kind_modifiers = parse_kind_modifier(&self.kind_modifiers); + if kind_modifiers.contains("deprecated") { + tags = Some(vec![lsp::SymbolTag::Deprecated]); + } + + let children = if !symbol_children.is_empty() { + Some(symbol_children) + } else { + None + }; + + // The field `deprecated` is deprecated but DocumentSymbol does not have + // a default, therefore we have to supply the deprecated deprecated + // field. It is like a bad version of Inception. + #[allow(deprecated)] + document_symbols.push(lsp::DocumentSymbol { + name: self.text.clone(), + kind: self.kind.clone().into(), + range: span.to_range(line_index), + selection_range: selection_span.to_range(line_index), + tags, + children, + detail: None, + deprecated: None, + }) + } + } + + should_include + } + + fn should_include_entry(&self) -> bool { + if let ScriptElementKind::Alias = self.kind { + return false; + } + + !self.text.is_empty() && self.text != "" && self.text != "" + } + pub fn walk(&self, callback: &F) where F: Fn(&NavigationTree, Option<&NavigationTree>), diff --git a/cli/tests/lsp/document_symbol_did_open_notification.json b/cli/tests/lsp/document_symbol_did_open_notification.json new file mode 100644 index 0000000000..31c37c152b --- /dev/null +++ b/cli/tests/lsp/document_symbol_did_open_notification.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "interface IFoo {\n foo(): boolean;\n}\n\nclass Bar implements IFoo {\n constructor(public x: number) { }\n foo() { return true; }\n /** @deprecated */\n baz() { return false; }\n get value(): number { return 0; }\n set value(newVavlue: number) { return; }\n static staticBar = new Bar(0);\n private static getStaticBar() { return Bar.staticBar; }\n}\n\nenum Values { value1, value2 }\n\nvar bar: IFoo = new Bar(3);" + } + } +} diff --git a/cli/tests/lsp/document_symbol_request.json b/cli/tests/lsp/document_symbol_request.json new file mode 100644 index 0000000000..a31317fc35 --- /dev/null +++ b/cli/tests/lsp/document_symbol_request.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/documentSymbol", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + } + } +}