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:
parent
3abe31252e
commit
bf5657cd59
8 changed files with 303 additions and 24 deletions
|
@ -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),
|
||||
|
|
|
@ -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(¶ms));
|
||||
|
||||
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
|
||||
|
|
|
@ -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,
|
||||
|
|
113
cli/lsp/tsc.rs
113
cli/lsp/tsc.rs
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
8
cli/tsc/compiler.d.ts
vendored
8
cli/tsc/compiler.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue