summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/lsp/capabilities.rs3
-rw-r--r--cli/lsp/language_server.rs454
-rw-r--r--cli/lsp/tsc.rs86
-rw-r--r--cli/tests/lsp/document_symbol_did_open_notification.json12
-rw-r--r--cli/tests/lsp/document_symbol_request.json10
5 files changed, 563 insertions, 2 deletions
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs
index f0312cc6d..677e3921e 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 6edfdc74b..58fcf9975 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<Option<DocumentSymbolResponse>> {
+ if !self.enabled() {
+ return Ok(None);
+ }
+ let mark = self.performance.mark("selection_range");
+ let specifier = self.url_map.normalize_url(&params.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::<DocumentSymbol>::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<Option<DocumentSymbolResponse>> {
+ self.0.lock().await.document_symbol(params).await
+ }
+
async fn formatting(
&self,
params: DocumentFormattingParams,
@@ -3407,6 +3457,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 38f291d03..fef605397 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<lsp::DocumentSymbol>,
+ ) -> 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::<lsp::DocumentSymbol>::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<Vec<lsp::SymbolTag>> = 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 != "<function>" && self.text != "<class>"
+ }
+
pub fn walk<F>(&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 000000000..31c37c152
--- /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 000000000..a31317fc3
--- /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"
+ }
+ }
+}