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

feat(lsp): implement textDocument/prepareCallHierarchy (#10061)

This commit is contained in:
Jean Pierre 2021-04-19 00:11:26 -05:00 committed by GitHub
parent 0552eaf569
commit 65a2a04d3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 747 additions and 3 deletions

View file

@ -5,6 +5,7 @@
///! language server, which helps determine what messages are sent from the ///! language server, which helps determine what messages are sent from the
///! client. ///! client.
///! ///!
use lspower::lsp::CallHierarchyServerCapability;
use lspower::lsp::ClientCapabilities; use lspower::lsp::ClientCapabilities;
use lspower::lsp::CodeActionKind; use lspower::lsp::CodeActionKind;
use lspower::lsp::CodeActionOptions; use lspower::lsp::CodeActionOptions;
@ -114,7 +115,7 @@ pub fn server_capabilities(
document_link_provider: None, document_link_provider: None,
color_provider: None, color_provider: None,
execute_command_provider: None, execute_command_provider: None,
call_hierarchy_provider: None, call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
semantic_tokens_provider: None, semantic_tokens_provider: None,
workspace: None, workspace: None,
experimental: None, experimental: None,

View file

@ -1643,6 +1643,192 @@ impl Inner {
Ok(response) Ok(response)
} }
async fn incoming_calls(
&mut self,
params: CallHierarchyIncomingCallsParams,
) -> LspResult<Option<Vec<CallHierarchyIncomingCall>>> {
if !self.enabled() {
return Ok(None);
}
let mark = self.performance.mark("incoming_calls");
let specifier = self.url_map.normalize_url(&params.item.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::ProvideCallHierarchyIncomingCalls((
specifier.clone(),
line_index.offset_tsc(params.item.selection_range.start)?,
));
let incoming_calls: Vec<tsc::CallHierarchyIncomingCall> = self
.ts_server
.request(self.snapshot(), req)
.await
.map_err(|err| {
error!("Failed to request to tsserver {}", err);
LspError::invalid_request()
})?;
let maybe_root_path_owned = self
.config
.root_uri
.as_ref()
.and_then(|uri| uri.to_file_path().ok());
let mut resolved_items = Vec::<CallHierarchyIncomingCall>::new();
for item in incoming_calls.iter() {
if let Some(resolved) = item
.try_resolve_call_hierarchy_incoming_call(
self,
maybe_root_path_owned.as_deref(),
)
.await
{
resolved_items.push(resolved);
}
}
self.performance.measure(mark);
Ok(Some(resolved_items))
}
async fn outgoing_calls(
&mut self,
params: CallHierarchyOutgoingCallsParams,
) -> LspResult<Option<Vec<CallHierarchyOutgoingCall>>> {
if !self.enabled() {
return Ok(None);
}
let mark = self.performance.mark("outgoing_calls");
let specifier = self.url_map.normalize_url(&params.item.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::ProvideCallHierarchyOutgoingCalls((
specifier.clone(),
line_index.offset_tsc(params.item.selection_range.start)?,
));
let outgoing_calls: Vec<tsc::CallHierarchyOutgoingCall> = self
.ts_server
.request(self.snapshot(), req)
.await
.map_err(|err| {
error!("Failed to request to tsserver {}", err);
LspError::invalid_request()
})?;
let maybe_root_path_owned = self
.config
.root_uri
.as_ref()
.and_then(|uri| uri.to_file_path().ok());
let mut resolved_items = Vec::<CallHierarchyOutgoingCall>::new();
for item in outgoing_calls.iter() {
if let Some(resolved) = item
.try_resolve_call_hierarchy_outgoing_call(
&line_index,
self,
maybe_root_path_owned.as_deref(),
)
.await
{
resolved_items.push(resolved);
}
}
self.performance.measure(mark);
Ok(Some(resolved_items))
}
async fn prepare_call_hierarchy(
&mut self,
params: CallHierarchyPrepareParams,
) -> LspResult<Option<Vec<CallHierarchyItem>>> {
if !self.enabled() {
return Ok(None);
}
let mark = self.performance.mark("prepare_call_hierarchy");
let specifier = self
.url_map
.normalize_url(&params.text_document_position_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::PrepareCallHierarchy((
specifier.clone(),
line_index.offset_tsc(params.text_document_position_params.position)?,
));
let maybe_one_or_many: Option<tsc::OneOrMany<tsc::CallHierarchyItem>> =
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(one_or_many) = maybe_one_or_many {
let maybe_root_path_owned = self
.config
.root_uri
.as_ref()
.and_then(|uri| uri.to_file_path().ok());
let mut resolved_items = Vec::<CallHierarchyItem>::new();
match one_or_many {
tsc::OneOrMany::One(item) => {
if let Some(resolved) = item
.try_resolve_call_hierarchy_item(
self,
maybe_root_path_owned.as_deref(),
)
.await
{
resolved_items.push(resolved)
}
}
tsc::OneOrMany::Many(items) => {
for item in items.iter() {
if let Some(resolved) = item
.try_resolve_call_hierarchy_item(
self,
maybe_root_path_owned.as_deref(),
)
.await
{
resolved_items.push(resolved);
}
}
}
}
Some(resolved_items)
} else {
None
};
self.performance.measure(mark);
Ok(response)
}
async fn rename( async fn rename(
&mut self, &mut self,
params: RenameParams, params: RenameParams,
@ -1971,6 +2157,27 @@ impl lspower::LanguageServer for LanguageServer {
self.0.lock().await.folding_range(params).await self.0.lock().await.folding_range(params).await
} }
async fn incoming_calls(
&self,
params: CallHierarchyIncomingCallsParams,
) -> LspResult<Option<Vec<CallHierarchyIncomingCall>>> {
self.0.lock().await.incoming_calls(params).await
}
async fn outgoing_calls(
&self,
params: CallHierarchyOutgoingCallsParams,
) -> LspResult<Option<Vec<CallHierarchyOutgoingCall>>> {
self.0.lock().await.outgoing_calls(params).await
}
async fn prepare_call_hierarchy(
&self,
params: CallHierarchyPrepareParams,
) -> LspResult<Option<Vec<CallHierarchyItem>>> {
self.0.lock().await.prepare_call_hierarchy(params).await
}
async fn rename( async fn rename(
&self, &self,
params: RenameParams, params: RenameParams,
@ -2471,6 +2678,154 @@ mod tests {
harness.run().await; harness.run().await;
} }
#[tokio::test]
async fn test_call_hierarchy() {
let mut harness = LspTestHarness::new(vec![
("initialize_request.json", LspResponse::RequestAny),
("initialized_notification.json", LspResponse::None),
(
"prepare_call_hierarchy_did_open_notification.json",
LspResponse::None,
),
(
"prepare_call_hierarchy_request.json",
LspResponse::Request(
2,
json!([
{
"name": "baz",
"kind": 6,
"detail": "Bar",
"uri": "file:///a/file.ts",
"range": {
"start": {
"line": 5,
"character": 2
},
"end": {
"line": 7,
"character": 3
}
},
"selectionRange": {
"start": {
"line": 5,
"character": 2
},
"end": {
"line": 5,
"character": 5
}
}
}
]),
),
),
(
"incoming_calls_request.json",
LspResponse::Request(
4,
json!([
{
"from": {
"name": "main",
"kind": 12,
"detail": "",
"uri": "file:///a/file.ts",
"range": {
"start": {
"line": 10,
"character": 0
},
"end": {
"line": 13,
"character": 1
}
},
"selectionRange": {
"start": {
"line": 10,
"character": 9
},
"end": {
"line": 10,
"character": 13
}
}
},
"fromRanges": [
{
"start": {
"line": 12,
"character": 6
},
"end": {
"line": 12,
"character": 9
}
}
]
}
]),
),
),
(
"outgoing_calls_request.json",
LspResponse::Request(
5,
json!([
{
"to": {
"name": "foo",
"kind": 12,
"detail": "",
"uri": "file:///a/file.ts",
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 2,
"character": 1
}
},
"selectionRange": {
"start": {
"line": 0,
"character": 9
},
"end": {
"line": 0,
"character": 12
}
}
},
"fromRanges": [
{
"start": {
"line": 6,
"character": 11
},
"end": {
"line": 6,
"character": 14
}
}
]
}
]),
),
),
(
"shutdown_request.json",
LspResponse::Request(3, json!(null)),
),
("exit_notification.json", LspResponse::None),
]);
harness.run().await;
}
#[tokio::test] #[tokio::test]
async fn test_format_mbc() { async fn test_format_mbc() {
let mut harness = LspTestHarness::new(vec![ let mut harness = LspTestHarness::new(vec![

View file

@ -35,10 +35,10 @@ use log::warn;
use lspower::lsp; use lspower::lsp;
use regex::Captures; use regex::Captures;
use regex::Regex; use regex::Regex;
use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::thread; use std::thread;
use std::{borrow::Cow, cmp}; use std::{borrow::Cow, cmp};
use std::{collections::HashMap, path::Path};
use text_size::TextSize; use text_size::TextSize;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::oneshot; use tokio::sync::oneshot;
@ -283,6 +283,13 @@ fn parse_kind_modifier(kind_modifiers: &str) -> HashSet<&str> {
re.split(kind_modifiers).collect() re.split(kind_modifiers).collect()
} }
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum ScriptElementKind { pub enum ScriptElementKind {
#[serde(rename = "")] #[serde(rename = "")]
@ -411,6 +418,33 @@ impl From<ScriptElementKind> for lsp::CompletionItemKind {
} }
} }
impl From<ScriptElementKind> for lsp::SymbolKind {
fn from(kind: ScriptElementKind) -> Self {
match kind {
ScriptElementKind::ModuleElement => lsp::SymbolKind::Module,
ScriptElementKind::ClassElement => lsp::SymbolKind::Class,
ScriptElementKind::EnumElement => lsp::SymbolKind::Enum,
ScriptElementKind::InterfaceElement => lsp::SymbolKind::Interface,
ScriptElementKind::MemberFunctionElement => lsp::SymbolKind::Method,
ScriptElementKind::MemberVariableElement => lsp::SymbolKind::Property,
ScriptElementKind::MemberGetAccessorElement => lsp::SymbolKind::Property,
ScriptElementKind::MemberSetAccessorElement => lsp::SymbolKind::Property,
ScriptElementKind::VariableElement => lsp::SymbolKind::Variable,
ScriptElementKind::ConstElement => lsp::SymbolKind::Variable,
ScriptElementKind::LocalVariableElement => lsp::SymbolKind::Variable,
ScriptElementKind::FunctionElement => lsp::SymbolKind::Function,
ScriptElementKind::LocalFunctionElement => lsp::SymbolKind::Function,
ScriptElementKind::ConstructSignatureElement => {
lsp::SymbolKind::Constructor
}
ScriptElementKind::ConstructorImplementationElement => {
lsp::SymbolKind::Constructor
}
_ => lsp::SymbolKind::Variable,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TextSpan { pub struct TextSpan {
@ -917,6 +951,182 @@ impl ReferenceEntry {
} }
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyItem {
name: String,
kind: ScriptElementKind,
#[serde(skip_serializing_if = "Option::is_none")]
kind_modifiers: Option<String>,
file: String,
span: TextSpan,
selection_span: TextSpan,
#[serde(skip_serializing_if = "Option::is_none")]
container_name: Option<String>,
}
impl CallHierarchyItem {
pub(crate) async fn try_resolve_call_hierarchy_item(
&self,
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyItem> {
let target_specifier = resolve_url(&self.file).unwrap();
let target_line_index = language_server
.get_line_index(target_specifier)
.await
.ok()?;
Some(self.to_call_hierarchy_item(
&target_line_index,
language_server,
maybe_root_path,
))
}
pub(crate) fn to_call_hierarchy_item(
&self,
line_index: &LineIndex,
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> lsp::CallHierarchyItem {
let target_specifier = resolve_url(&self.file).unwrap();
let uri = language_server
.url_map
.normalize_specifier(&target_specifier)
.unwrap();
let use_file_name = self.is_source_file_item();
let maybe_file_path = if uri.scheme() == "file" {
uri.to_file_path().ok()
} else {
None
};
let name = if use_file_name {
if let Some(file_path) = maybe_file_path.as_ref() {
file_path.file_name().unwrap().to_string_lossy().to_string()
} else {
uri.to_string()
}
} else {
self.name.clone()
};
let detail = if use_file_name {
if let Some(file_path) = maybe_file_path.as_ref() {
// TODO: update this to work with multi root workspaces
let parent_dir = file_path.parent().unwrap();
if let Some(root_path) = maybe_root_path {
parent_dir
.strip_prefix(root_path)
.unwrap_or(parent_dir)
.to_string_lossy()
.to_string()
} else {
parent_dir.to_string_lossy().to_string()
}
} else {
String::new()
}
} else {
self.container_name.as_ref().cloned().unwrap_or_default()
};
let mut tags: Option<Vec<lsp::SymbolTag>> = None;
if let Some(modifiers) = self.kind_modifiers.as_ref() {
let kind_modifiers = parse_kind_modifier(modifiers);
if kind_modifiers.contains("deprecated") {
tags = Some(vec![lsp::SymbolTag::Deprecated]);
}
}
lsp::CallHierarchyItem {
name,
tags,
uri,
detail: Some(detail),
kind: self.kind.clone().into(),
range: self.span.to_range(line_index),
selection_range: self.selection_span.to_range(line_index),
data: None,
}
}
fn is_source_file_item(&self) -> bool {
self.kind == ScriptElementKind::ScriptElement
|| self.kind == ScriptElementKind::ModuleElement
&& self.selection_span.start == 0
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyIncomingCall {
from: CallHierarchyItem,
from_spans: Vec<TextSpan>,
}
impl CallHierarchyIncomingCall {
pub(crate) async fn try_resolve_call_hierarchy_incoming_call(
&self,
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyIncomingCall> {
let target_specifier = resolve_url(&self.from.file).unwrap();
let target_line_index = language_server
.get_line_index(target_specifier)
.await
.ok()?;
Some(lsp::CallHierarchyIncomingCall {
from: self.from.to_call_hierarchy_item(
&target_line_index,
language_server,
maybe_root_path,
),
from_ranges: self
.from_spans
.iter()
.map(|span| span.to_range(&target_line_index))
.collect(),
})
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyOutgoingCall {
to: CallHierarchyItem,
from_spans: Vec<TextSpan>,
}
impl CallHierarchyOutgoingCall {
pub(crate) async fn try_resolve_call_hierarchy_outgoing_call(
&self,
line_index: &LineIndex,
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyOutgoingCall> {
let target_specifier = resolve_url(&self.to.file).unwrap();
let target_line_index = language_server
.get_line_index(target_specifier)
.await
.ok()?;
Some(lsp::CallHierarchyOutgoingCall {
to: self.to.to_call_hierarchy_item(
&target_line_index,
language_server,
maybe_root_path,
),
from_ranges: self
.from_spans
.iter()
.map(|span| span.to_range(&line_index))
.collect(),
})
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CompletionEntryDetails { pub struct CompletionEntryDetails {
@ -1956,6 +2166,12 @@ pub enum RequestMethod {
GetSmartSelectionRange((ModuleSpecifier, u32)), GetSmartSelectionRange((ModuleSpecifier, u32)),
/// Get the diagnostic codes that support some form of code fix. /// Get the diagnostic codes that support some form of code fix.
GetSupportedCodeFixes, GetSupportedCodeFixes,
/// Resolve a call hierarchy item for a specific position.
PrepareCallHierarchy((ModuleSpecifier, u32)),
/// Resolve incoming call hierarchy items for a specific position.
ProvideCallHierarchyIncomingCalls((ModuleSpecifier, u32)),
/// Resolve outgoing call hierarchy items for a specific position.
ProvideCallHierarchyOutgoingCalls((ModuleSpecifier, u32)),
} }
impl RequestMethod { impl RequestMethod {
@ -2092,6 +2308,36 @@ impl RequestMethod {
"id": id, "id": id,
"method": "getSupportedCodeFixes", "method": "getSupportedCodeFixes",
}), }),
RequestMethod::PrepareCallHierarchy((specifier, position)) => {
json!({
"id": id,
"method": "prepareCallHierarchy",
"specifier": specifier,
"position": position
})
}
RequestMethod::ProvideCallHierarchyIncomingCalls((
specifier,
position,
)) => {
json!({
"id": id,
"method": "provideCallHierarchyIncomingCalls",
"specifier": specifier,
"position": position
})
}
RequestMethod::ProvideCallHierarchyOutgoingCalls((
specifier,
position,
)) => {
json!({
"id": id,
"method": "provideCallHierarchyOutgoingCalls",
"specifier": specifier,
"position": position
})
}
} }
} }
} }

View file

@ -0,0 +1,33 @@
{
"jsonrpc": "2.0",
"id": 4,
"method": "callHierarchy/incomingCalls",
"params": {
"item": {
"name": "baz",
"kind": 6,
"detail": "Bar",
"uri": "file:///a/file.ts",
"range": {
"start": {
"line": 5,
"character": 2
},
"end": {
"line": 7,
"character": 3
}
},
"selectionRange": {
"start": {
"line": 5,
"character": 2
},
"end": {
"line": 5,
"character": 5
}
}
}
}
}

View file

@ -0,0 +1,33 @@
{
"jsonrpc": "2.0",
"id": 5,
"method": "callHierarchy/outgoingCalls",
"params": {
"item": {
"name": "baz",
"kind": 6,
"detail": "Bar",
"uri": "file:///a/file.ts",
"range": {
"start": {
"line": 5,
"character": 2
},
"end": {
"line": 7,
"character": 3
}
},
"selectionRange": {
"start": {
"line": 5,
"character": 2
},
"end": {
"line": 5,
"character": 5
}
}
}
}
}

View file

@ -0,0 +1,12 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "function foo() {\n return false;\n}\n\nclass Bar {\n baz() {\n return foo();\n }\n}\n\nfunction main() {\n const bar = new Bar();\n bar.baz();\n}\n\nmain();"
}
}
}

View file

@ -0,0 +1,14 @@
{
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/prepareCallHierarchy",
"params": {
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": {
"line": 5,
"character": 3
}
}
}

View file

@ -726,6 +726,33 @@ delete Object.prototype.__proto__;
ts.getSupportedCodeFixes(), ts.getSupportedCodeFixes(),
); );
} }
case "prepareCallHierarchy": {
return respond(
id,
languageService.prepareCallHierarchy(
request.specifier,
request.position,
),
);
}
case "provideCallHierarchyIncomingCalls": {
return respond(
id,
languageService.provideCallHierarchyIncomingCalls(
request.specifier,
request.position,
),
);
}
case "provideCallHierarchyOutgoingCalls": {
return respond(
id,
languageService.provideCallHierarchyOutgoingCalls(
request.specifier,
request.position,
),
);
}
default: default:
throw new TypeError( throw new TypeError(
// @ts-ignore exhausted case statement sets type to never // @ts-ignore exhausted case statement sets type to never

25
cli/tsc/compiler.d.ts vendored
View file

@ -63,7 +63,10 @@ declare global {
| GetReferencesRequest | GetReferencesRequest
| GetSignatureHelpItemsRequest | GetSignatureHelpItemsRequest
| GetSmartSelectionRange | GetSmartSelectionRange
| GetSupportedCodeFixes; | GetSupportedCodeFixes
| PrepareCallHierarchy
| ProvideCallHierarchyIncomingCalls
| ProvideCallHierarchyOutgoingCalls;
interface BaseLanguageServerRequest { interface BaseLanguageServerRequest {
id: number; id: number;
@ -185,4 +188,24 @@ declare global {
interface GetSupportedCodeFixes extends BaseLanguageServerRequest { interface GetSupportedCodeFixes extends BaseLanguageServerRequest {
method: "getSupportedCodeFixes"; method: "getSupportedCodeFixes";
} }
interface PrepareCallHierarchy extends BaseLanguageServerRequest {
method: "prepareCallHierarchy";
specifier: string;
position: number;
}
interface ProvideCallHierarchyIncomingCalls
extends BaseLanguageServerRequest {
method: "provideCallHierarchyIncomingCalls";
specifier: string;
position: number;
}
interface ProvideCallHierarchyOutgoingCalls
extends BaseLanguageServerRequest {
method: "provideCallHierarchyOutgoingCalls";
specifier: string;
position: number;
}
} }