1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-03 04:48:52 -05:00

feat(lsp): add workspace symbol provider (#12787)

This commit is contained in:
Kitson Kelly 2021-11-23 11:08:56 +11:00 committed by GitHub
parent 3abe31252e
commit bf5657cd59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 303 additions and 24 deletions

View file

@ -12,6 +12,7 @@ use lspower::lsp::CodeActionOptions;
use lspower::lsp::CodeActionProviderCapability;
use lspower::lsp::CodeLensOptions;
use lspower::lsp::CompletionOptions;
use lspower::lsp::DocumentSymbolOptions;
use lspower::lsp::FoldingRangeProviderCapability;
use lspower::lsp::HoverProviderCapability;
use lspower::lsp::ImplementationProviderCapability;
@ -114,9 +115,13 @@ pub fn server_capabilities(
)),
references_provider: Some(OneOf::Left(true)),
document_highlight_provider: Some(OneOf::Left(true)),
// 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,
document_symbol_provider: Some(OneOf::Right(DocumentSymbolOptions {
label: Some("Deno".to_string()),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
})),
workspace_symbol_provider: Some(OneOf::Left(true)),
code_action_provider: Some(code_action_provider),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(true),

View file

@ -2270,6 +2270,44 @@ impl Inner {
Ok(None)
}
}
async fn symbol(
&mut self,
params: WorkspaceSymbolParams,
) -> LspResult<Option<Vec<SymbolInformation>>> {
let mark = self.performance.mark("symbol", Some(&params));
let req = tsc::RequestMethod::GetNavigateToItems {
search: params.query,
// this matches vscode's hard coded result count
max_result_count: Some(256),
file: None,
};
let navigate_to_items: Vec<tsc::NavigateToItem> = self
.ts_server
.request(self.snapshot()?, req)
.await
.map_err(|err| {
error!("Failed request to tsserver: {}", err);
LspError::invalid_request()
})?;
let maybe_symbol_information = if navigate_to_items.is_empty() {
None
} else {
let mut symbol_information = Vec::new();
for item in navigate_to_items {
if let Some(info) = item.to_symbol_information(self).await {
symbol_information.push(info);
}
}
Some(symbol_information)
};
self.performance.measure(mark);
Ok(maybe_symbol_information)
}
}
#[lspower::async_trait]
@ -2481,6 +2519,13 @@ impl lspower::LanguageServer for LanguageServer {
) -> LspResult<Option<SignatureHelp>> {
self.0.lock().await.signature_help(params).await
}
async fn symbol(
&self,
params: WorkspaceSymbolParams,
) -> LspResult<Option<Vec<SymbolInformation>>> {
self.0.lock().await.symbol(params).await
}
}
// These are implementations of custom commands supported by the LSP

View file

@ -12,6 +12,9 @@ use lspower::lsp::SemanticTokens;
use lspower::lsp::SemanticTokensLegend;
use std::ops::{Index, IndexMut};
pub(crate) const MODIFIER_MASK: u32 = 255;
pub(crate) const TYPE_OFFSET: u32 = 8;
enum TokenType {
Class = 0,
Enum = 1,
@ -78,12 +81,12 @@ pub fn get_legend() -> SemanticTokensLegend {
token_types[TokenType::Method] = "method".into();
let mut token_modifiers = vec![SemanticTokenModifier::from(""); 6];
token_modifiers[TokenModifier::Declaration] = "declaration".into();
token_modifiers[TokenModifier::Static] = "static".into();
token_modifiers[TokenModifier::Async] = "async".into();
token_modifiers[TokenModifier::Declaration] = "declaration".into();
token_modifiers[TokenModifier::Readonly] = "readonly".into();
token_modifiers[TokenModifier::DefaultLibrary] = "defaultLibrary".into();
token_modifiers[TokenModifier::Static] = "static".into();
token_modifiers[TokenModifier::Local] = "local".into();
token_modifiers[TokenModifier::DefaultLibrary] = "defaultLibrary".into();
SemanticTokensLegend {
token_types,
@ -91,11 +94,6 @@ pub fn get_legend() -> SemanticTokensLegend {
}
}
pub enum TsTokenEncodingConsts {
TypeOffset = 8,
ModifierMask = 255,
}
pub struct SemanticTokensBuilder {
prev_line: u32,
prev_char: u32,

View file

@ -9,8 +9,8 @@ use super::refactor::ALL_KNOWN_REFACTOR_ACTION_KINDS;
use super::refactor::EXTRACT_CONSTANT;
use super::refactor::EXTRACT_INTERFACE;
use super::refactor::EXTRACT_TYPE;
use super::semantic_tokens;
use super::semantic_tokens::SemanticTokensBuilder;
use super::semantic_tokens::TsTokenEncodingConsts;
use super::text;
use super::text::LineIndex;
use super::urls::INVALID_SPECIFIER;
@ -507,6 +507,9 @@ impl From<ScriptElementKind> for lsp::SymbolKind {
fn from(kind: ScriptElementKind) -> Self {
match kind {
ScriptElementKind::ModuleElement => Self::Module,
// this is only present in `getSymbolKind` in `workspaceSymbols` in
// vscode, but seems strange it isn't consistent.
ScriptElementKind::TypeElement => Self::Class,
ScriptElementKind::ClassElement => Self::Class,
ScriptElementKind::EnumElement => Self::Enum,
ScriptElementKind::EnumMemberElement => Self::EnumMember,
@ -514,9 +517,12 @@ impl From<ScriptElementKind> for lsp::SymbolKind {
ScriptElementKind::IndexSignatureElement => Self::Method,
ScriptElementKind::CallSignatureElement => Self::Method,
ScriptElementKind::MemberFunctionElement => Self::Method,
ScriptElementKind::MemberVariableElement => Self::Property,
ScriptElementKind::MemberGetAccessorElement => Self::Property,
ScriptElementKind::MemberSetAccessorElement => Self::Property,
// workspaceSymbols in vscode treats them as fields, which does seem more
// semantically correct while `fromProtocolScriptElementKind` treats them
// as properties.
ScriptElementKind::MemberVariableElement => Self::Field,
ScriptElementKind::MemberGetAccessorElement => Self::Field,
ScriptElementKind::MemberSetAccessorElement => Self::Field,
ScriptElementKind::VariableElement => Self::Variable,
ScriptElementKind::LetElement => Self::Variable,
ScriptElementKind::ConstElement => Self::Variable,
@ -672,6 +678,71 @@ impl DocumentSpan {
}
}
#[derive(Debug, Clone, Deserialize)]
pub enum MatchKind {
#[serde(rename = "exact")]
Exact,
#[serde(rename = "prefix")]
Prefix,
#[serde(rename = "substring")]
Substring,
#[serde(rename = "camelCase")]
CamelCase,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigateToItem {
name: String,
kind: ScriptElementKind,
kind_modifiers: String,
match_kind: MatchKind,
is_case_sensitive: bool,
file_name: String,
text_span: TextSpan,
container_name: Option<String>,
container_kind: ScriptElementKind,
}
impl NavigateToItem {
pub(crate) async fn to_symbol_information(
&self,
language_server: &mut language_server::Inner,
) -> Option<lsp::SymbolInformation> {
let specifier = normalize_specifier(&self.file_name).ok()?;
let asset_or_doc = language_server
.get_asset_or_document(&specifier)
.await
.ok()?;
let line_index = asset_or_doc.line_index();
let uri = language_server
.url_map
.normalize_specifier(&specifier)
.ok()?;
let range = self.text_span.to_range(line_index);
let location = lsp::Location { uri, range };
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]);
}
// The field `deprecated` is deprecated but SymbolInformation does not have
// a default, therefore we have to supply the deprecated deprecated
// field. It is like a bad version of Inception.
#[allow(deprecated)]
Some(lsp::SymbolInformation {
name: self.name.clone(),
kind: self.kind.clone().into(),
tags,
deprecated: None,
location,
container_name: self.container_name.clone(),
})
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigationTree {
@ -752,6 +823,16 @@ impl NavigationTree {
}
}
let name = match self.kind {
ScriptElementKind::MemberGetAccessorElement => {
format!("(get) {}", self.text)
}
ScriptElementKind::MemberSetAccessorElement => {
format!("(set) {}", self.text)
}
_ => self.text.clone(),
};
let mut tags: Option<Vec<lsp::SymbolTag>> = None;
let kind_modifiers = parse_kind_modifier(&self.kind_modifiers);
if kind_modifiers.contains("deprecated") {
@ -769,7 +850,7 @@ impl NavigationTree {
// field. It is like a bad version of Inception.
#[allow(deprecated)]
document_symbols.push(lsp::DocumentSymbol {
name: self.text.clone(),
name,
kind: self.kind.clone().into(),
range: span.to_range(line_index.clone()),
selection_range: selection_span.to_range(line_index.clone()),
@ -1166,11 +1247,12 @@ impl Classifications {
}
fn get_token_type_from_classification(ts_classification: u32) -> u32 {
(ts_classification >> (TsTokenEncodingConsts::TypeOffset as u32)) - 1
assert!(ts_classification > semantic_tokens::MODIFIER_MASK);
(ts_classification >> semantic_tokens::TYPE_OFFSET) - 1
}
fn get_token_modifier_from_classification(ts_classification: u32) -> u32 {
ts_classification & (TsTokenEncodingConsts::ModifierMask as u32)
ts_classification & semantic_tokens::MODIFIER_MASK
}
}
@ -2667,6 +2749,12 @@ pub enum RequestMethod {
GetEncodedSemanticClassifications((ModuleSpecifier, TextSpan)),
/// Get implementation information for a specific position.
GetImplementation((ModuleSpecifier, u32)),
/// Get "navigate to" items, which are converted to workspace symbols
GetNavigateToItems {
search: String,
max_result_count: Option<u32>,
file: Option<String>,
},
/// Get a "navigation tree" for a specifier.
GetNavigationTree(ModuleSpecifier),
/// Get outlining spans for a specifier.
@ -2808,6 +2896,17 @@ impl RequestMethod {
"specifier": state.denormalize_specifier(specifier),
"position": position,
}),
RequestMethod::GetNavigateToItems {
search,
max_result_count,
file,
} => json!({
"id": id,
"method": "getNavigateToItems",
"search": search,
"maxResultCount": max_result_count,
"file": file,
}),
RequestMethod::GetNavigationTree(specifier) => json!({
"id": id,
"method": "getNavigationTree",

View file

@ -3775,6 +3775,120 @@ fn lsp_configuration_did_change() {
shutdown(&mut client);
}
#[test]
fn lsp_workspace_symbol() {
let mut client = init("initialize_params.json");
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "export class A {\n fieldA: string;\n fieldB: string;\n}\n",
}
}),
);
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/file_01.ts",
"languageId": "typescript",
"version": 1,
"text": "export class B {\n fieldC: string;\n fieldD: string;\n}\n",
}
}),
);
let (maybe_res, maybe_err) = client
.write_request(
"workspace/symbol",
json!({
"query": "field"
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
maybe_res,
Some(json!([
{
"name": "fieldA",
"kind": 8,
"location": {
"uri": "file:///a/file.ts",
"range": {
"start": {
"line": 1,
"character": 2
},
"end": {
"line": 1,
"character": 17
}
}
},
"containerName": "A"
},
{
"name": "fieldB",
"kind": 8,
"location": {
"uri": "file:///a/file.ts",
"range": {
"start": {
"line": 2,
"character": 2
},
"end": {
"line": 2,
"character": 17
}
}
},
"containerName": "A"
},
{
"name": "fieldC",
"kind": 8,
"location": {
"uri": "file:///a/file_01.ts",
"range": {
"start": {
"line": 1,
"character": 2
},
"end": {
"line": 1,
"character": 17
}
}
},
"containerName": "B"
},
{
"name": "fieldD",
"kind": 8,
"location": {
"uri": "file:///a/file_01.ts",
"range": {
"start": {
"line": 2,
"character": 2
},
"end": {
"line": 2,
"character": 17
}
}
},
"containerName": "B"
}
]))
);
shutdown(&mut client);
}
#[test]
fn lsp_code_actions_ignore_lint() {
let mut client = init("initialize_params.json");

View file

@ -148,7 +148,7 @@
},
{
"name": "staticBar",
"kind": 7,
"kind": 8,
"range": {
"start": {
"line": 11,
@ -171,8 +171,8 @@
}
},
{
"name": "value",
"kind": 7,
"name": "(get) value",
"kind": 8,
"range": {
"start": {
"line": 9,
@ -195,8 +195,8 @@
}
},
{
"name": "value",
"kind": 7,
"name": "(set) value",
"kind": 8,
"range": {
"start": {
"line": 10,
@ -220,7 +220,7 @@
},
{
"name": "x",
"kind": 7,
"kind": 8,
"range": {
"start": {
"line": 5,

View file

@ -743,6 +743,16 @@ delete Object.prototype.__proto__;
),
);
}
case "getNavigateToItems": {
return respond(
id,
languageService.getNavigateToItems(
request.search,
request.maxResultCount,
request.fileName,
),
);
}
case "getNavigationTree": {
return respond(
id,

View file

@ -58,6 +58,7 @@ declare global {
| GetDocumentHighlightsRequest
| GetEncodedSemanticClassifications
| GetImplementationRequest
| GetNavigateToItems
| GetNavigationTree
| GetOutliningSpans
| GetQuickInfoRequest
@ -173,6 +174,13 @@ declare global {
position: number;
}
interface GetNavigateToItems extends BaseLanguageServerRequest {
method: "getNavigateToItems";
search: string;
maxResultCount?: number;
fileName?: string;
}
interface GetNavigationTree extends BaseLanguageServerRequest {
method: "getNavigationTree";
specifier: string;