1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

feat(lsp): Implement textDocument/documentSymbol (#9981)

Co-authored-by: Kitson Kelly <me@kitsonkelly.com>
This commit is contained in:
Jean Pierre 2021-04-19 20:29:27 -05:00 committed by GitHub
parent 6d404ec54b
commit 2079da0f1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 563 additions and 2 deletions

View file

@ -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 {

View file

@ -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,
@ -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![

View file

@ -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>),

View file

@ -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);"
}
}
}

View file

@ -0,0 +1,10 @@
{
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/documentSymbol",
"params": {
"textDocument": {
"uri": "file:///a/file.ts"
}
}
}