diff options
-rw-r--r-- | cli/lsp/capabilities.rs | 3 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 355 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 248 | ||||
-rw-r--r-- | cli/tests/lsp/incoming_calls_request.json | 33 | ||||
-rw-r--r-- | cli/tests/lsp/outgoing_calls_request.json | 33 | ||||
-rw-r--r-- | cli/tests/lsp/prepare_call_hierarchy_did_open_notification.json | 12 | ||||
-rw-r--r-- | cli/tests/lsp/prepare_call_hierarchy_request.json | 14 | ||||
-rw-r--r-- | cli/tsc/99_main_compiler.js | 27 | ||||
-rw-r--r-- | cli/tsc/compiler.d.ts | 25 |
9 files changed, 747 insertions, 3 deletions
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index e17f030d3..cce349a5d 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -5,6 +5,7 @@ ///! language server, which helps determine what messages are sent from the ///! client. ///! +use lspower::lsp::CallHierarchyServerCapability; use lspower::lsp::ClientCapabilities; use lspower::lsp::CodeActionKind; use lspower::lsp::CodeActionOptions; @@ -114,7 +115,7 @@ pub fn server_capabilities( document_link_provider: None, color_provider: None, execute_command_provider: None, - call_hierarchy_provider: None, + call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)), semantic_tokens_provider: None, workspace: None, experimental: None, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 70682c41b..d5de93593 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1643,6 +1643,192 @@ impl Inner { 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(¶ms.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(¶ms.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(¶ms.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( &mut self, params: RenameParams, @@ -1971,6 +2157,27 @@ impl lspower::LanguageServer for LanguageServer { 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( &self, params: RenameParams, @@ -2472,6 +2679,154 @@ mod tests { } #[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] async fn test_format_mbc() { let mut harness = LspTestHarness::new(vec![ ("initialize_request.json", LspResponse::RequestAny), diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index aaea82421..128a2ba00 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -35,10 +35,10 @@ use log::warn; use lspower::lsp; use regex::Captures; use regex::Regex; -use std::collections::HashMap; use std::collections::HashSet; use std::thread; use std::{borrow::Cow, cmp}; +use std::{collections::HashMap, path::Path}; use text_size::TextSize; use tokio::sync::mpsc; use tokio::sync::oneshot; @@ -283,6 +283,13 @@ fn parse_kind_modifier(kind_modifiers: &str) -> HashSet<&str> { 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)] pub enum ScriptElementKind { #[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)] #[serde(rename_all = "camelCase")] pub struct TextSpan { @@ -919,6 +953,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)] +#[serde(rename_all = "camelCase")] pub struct CompletionEntryDetails { name: String, kind: ScriptElementKind, @@ -1956,6 +2166,12 @@ pub enum RequestMethod { GetSmartSelectionRange((ModuleSpecifier, u32)), /// Get the diagnostic codes that support some form of code fix. 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 { @@ -2092,6 +2308,36 @@ impl RequestMethod { "id": id, "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 + }) + } } } } diff --git a/cli/tests/lsp/incoming_calls_request.json b/cli/tests/lsp/incoming_calls_request.json new file mode 100644 index 000000000..47af92c1b --- /dev/null +++ b/cli/tests/lsp/incoming_calls_request.json @@ -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 + } + } + } + } +} diff --git a/cli/tests/lsp/outgoing_calls_request.json b/cli/tests/lsp/outgoing_calls_request.json new file mode 100644 index 000000000..a8d224ae8 --- /dev/null +++ b/cli/tests/lsp/outgoing_calls_request.json @@ -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 + } + } + } + } +} diff --git a/cli/tests/lsp/prepare_call_hierarchy_did_open_notification.json b/cli/tests/lsp/prepare_call_hierarchy_did_open_notification.json new file mode 100644 index 000000000..a75bd3a53 --- /dev/null +++ b/cli/tests/lsp/prepare_call_hierarchy_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": "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();" + } + } +} diff --git a/cli/tests/lsp/prepare_call_hierarchy_request.json b/cli/tests/lsp/prepare_call_hierarchy_request.json new file mode 100644 index 000000000..1f469ee8b --- /dev/null +++ b/cli/tests/lsp/prepare_call_hierarchy_request.json @@ -0,0 +1,14 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/prepareCallHierarchy", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 5, + "character": 3 + } + } +} diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index dc2b59533..ad661e087 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -726,6 +726,33 @@ delete Object.prototype.__proto__; 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: throw new TypeError( // @ts-ignore exhausted case statement sets type to never diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index 0b1f5e4de..1488f1b02 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -63,7 +63,10 @@ declare global { | GetReferencesRequest | GetSignatureHelpItemsRequest | GetSmartSelectionRange - | GetSupportedCodeFixes; + | GetSupportedCodeFixes + | PrepareCallHierarchy + | ProvideCallHierarchyIncomingCalls + | ProvideCallHierarchyOutgoingCalls; interface BaseLanguageServerRequest { id: number; @@ -185,4 +188,24 @@ declare global { interface GetSupportedCodeFixes extends BaseLanguageServerRequest { 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; + } } |