1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 15:24:46 -05:00

feat(lsp): add implementations code lens (#9441)

This commit is contained in:
Kitson Kelly 2021-02-08 21:45:10 +11:00 committed by GitHub
parent 09b79463d7
commit e368c5d0f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 286 additions and 3 deletions

View file

@ -289,6 +289,8 @@ pub fn analyze_dependencies(
#[derive(Debug, Deserialize, Serialize)]
pub enum CodeLensSource {
#[serde(rename = "implementations")]
Implementations,
#[serde(rename = "references")]
References,
}

View file

@ -18,7 +18,10 @@ pub struct ClientCapabilities {
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeLensSettings {
/// Flag for providing reference code lens.
/// Flag for providing implementation code lenses.
#[serde(default)]
pub implementations: bool,
/// Flag for providing reference code lenses.
#[serde(default)]
pub references: bool,
/// Flag for providing reference code lens on all functions. For this to have
@ -47,7 +50,15 @@ impl WorkspaceSettings {
pub fn enabled_code_lens(&self) -> bool {
if let Some(code_lens) = &self.code_lens {
// This should contain all the "top level" code lens references
code_lens.references
code_lens.implementations || code_lens.references
} else {
false
}
}
pub fn enabled_code_lens_implementations(&self) -> bool {
if let Some(code_lens) = &self.code_lens {
code_lens.implementations
} else {
false
}

View file

@ -52,6 +52,7 @@ use super::tsc::TsServer;
use super::utils;
lazy_static! {
static ref ABSTRACT_MODIFIER: Regex = Regex::new(r"\babstract\b").unwrap();
static ref EXPORT_MODIFIER: Regex = Regex::new(r"\bexport\b").unwrap();
}
@ -1037,6 +1038,30 @@ impl Inner {
navigation_tree.walk(&|i, mp| {
let mut code_lenses = cl.borrow_mut();
// TSC Implementations Code Lens
if self.config.settings.enabled_code_lens_implementations() {
let source = CodeLensSource::Implementations;
match i.kind {
tsc::ScriptElementKind::InterfaceElement => {
code_lenses.push(i.to_code_lens(&line_index, &specifier, &source));
}
tsc::ScriptElementKind::ClassElement
| tsc::ScriptElementKind::MemberFunctionElement
| tsc::ScriptElementKind::MemberVariableElement
| tsc::ScriptElementKind::MemberGetAccessorElement
| tsc::ScriptElementKind::MemberSetAccessorElement => {
if ABSTRACT_MODIFIER.is_match(&i.kind_modifiers) {
code_lenses.push(i.to_code_lens(
&line_index,
&specifier,
&source,
));
}
}
_ => (),
}
}
// TSC References Code Lens
if self.config.settings.enabled_code_lens_references() {
let source = CodeLensSource::References;
@ -1124,6 +1149,83 @@ impl Inner {
let code_lens_data: CodeLensData = serde_json::from_value(data)
.map_err(|err| LspError::invalid_params(err.to_string()))?;
let code_lens = match code_lens_data.source {
CodeLensSource::Implementations => {
let line_index =
self.get_line_index_sync(&code_lens_data.specifier).unwrap();
let req = tsc::RequestMethod::GetImplementation((
code_lens_data.specifier.clone(),
line_index.offset_tsc(params.range.start)?,
));
let res =
self.ts_server.request(self.snapshot(), req).await.map_err(
|err| {
error!("Error processing TypeScript request: {}", err);
LspError::internal_error()
},
)?;
let maybe_implementations: Option<Vec<tsc::ImplementationLocation>> =
serde_json::from_value(res).map_err(|err| {
error!("Error deserializing response: {}", err);
LspError::internal_error()
})?;
if let Some(implementations) = maybe_implementations {
let mut locations = Vec::new();
for implementation in implementations {
let implementation_specifier = ModuleSpecifier::resolve_url(
&implementation.document_span.file_name,
)
.map_err(|err| {
error!("Invalid specifier returned from TypeScript: {}", err);
LspError::internal_error()
})?;
let implementation_location =
implementation.to_location(&line_index);
if !(implementation_specifier == code_lens_data.specifier
&& implementation_location.range.start == params.range.start)
{
locations.push(implementation_location);
}
}
let command = if !locations.is_empty() {
let title = if locations.len() > 1 {
format!("{} implementations", locations.len())
} else {
"1 implementation".to_string()
};
Command {
title,
command: "deno.showReferences".to_string(),
arguments: Some(vec![
serde_json::to_value(code_lens_data.specifier).unwrap(),
serde_json::to_value(params.range.start).unwrap(),
serde_json::to_value(locations).unwrap(),
]),
}
} else {
Command {
title: "0 implementations".to_string(),
command: "".to_string(),
arguments: None,
}
};
CodeLens {
range: params.range,
command: Some(command),
data: None,
}
} else {
let command = Command {
title: "0 implementations".to_string(),
command: "".to_string(),
arguments: None,
};
CodeLens {
range: params.range,
command: Some(command),
data: None,
}
}
}
CodeLensSource::References => {
let line_index =
self.get_line_index_sync(&code_lens_data.specifier).unwrap();
@ -2372,6 +2474,121 @@ mod tests {
harness.run().await;
}
#[tokio::test]
async fn test_code_lens_impl_request() {
let mut harness = LspTestHarness::new(vec![
("initialize_request.json", LspResponse::RequestAny),
("initialized_notification.json", LspResponse::None),
("did_open_notification_cl_impl.json", LspResponse::None),
(
"code_lens_request.json",
LspResponse::Request(
2,
json!([
{
"range": {
"start": {
"line": 0,
"character": 10,
},
"end": {
"line": 0,
"character": 11,
}
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations",
},
},
{
"range": {
"start": {
"line": 0,
"character": 10,
},
"end": {
"line": 0,
"character": 11,
}
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references",
},
},
{
"range": {
"start": {
"line": 4,
"character": 6,
},
"end": {
"line": 4,
"character": 7,
}
},
"data": {
"specifier": "file:///a/file.ts",
"source": "references",
},
},
]),
),
),
(
"code_lens_resolve_request_impl.json",
LspResponse::Request(
4,
json!({
"range": {
"start": {
"line": 0,
"character": 10,
},
"end": {
"line": 0,
"character": 11,
}
},
"command": {
"title": "1 implementation",
"command": "deno.showReferences",
"arguments": [
"file:///a/file.ts",
{
"line": 0,
"character": 10,
},
[
{
"uri": "file:///a/file.ts",
"range": {
"start": {
"line": 4,
"character": 6,
},
"end": {
"line": 4,
"character": 7,
}
}
}
],
]
}
}),
),
),
(
"shutdown_request.json",
LspResponse::Request(3, json!(null)),
),
("exit_notification.json", LspResponse::None),
]);
harness.run().await;
}
#[tokio::test]
async fn test_code_actions() {
let mut harness = LspTestHarness::new(vec![

View file

@ -497,8 +497,16 @@ impl NavigationTree {
specifier: &ModuleSpecifier,
source: &CodeLensSource,
) -> lsp::CodeLens {
let range = if let Some(name_span) = &self.name_span {
name_span.to_range(line_index)
} else if !self.spans.is_empty() {
let span = &self.spans[0];
span.to_range(line_index)
} else {
lsp::Range::default()
};
lsp::CodeLens {
range: self.name_span.clone().unwrap().to_range(line_index),
range,
command: None,
data: Some(json!({
"specifier": specifier,
@ -542,6 +550,17 @@ pub struct ImplementationLocation {
display_parts: Vec<SymbolDisplayPart>,
}
impl ImplementationLocation {
pub fn to_location(&self, line_index: &LineIndex) -> lsp::Location {
let uri =
utils::normalize_file_name(&self.document_span.file_name).unwrap();
lsp::Location {
uri,
range: self.document_span.text_span.to_range(line_index),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameLocation {

View file

@ -0,0 +1,21 @@
{
"jsonrpc": "2.0",
"id": 4,
"method": "codeLens/resolve",
"params": {
"range": {
"start": {
"line": 0,
"character": 10
},
"end": {
"line": 0,
"character": 11
}
},
"data": {
"specifier": "file:///a/file.ts",
"source": "implementations"
}
}
}

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 A {\n b(): void;\n}\n\nclass B implements A {\n b() {\n console.log(\"b\");\n }\n}\n"
}
}
}

View file

@ -12,6 +12,7 @@
"initializationOptions": {
"enable": true,
"codeLens": {
"implementations": true,
"references": true
},
"lint": true,