diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 1584ca79d4..ad43966169 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -16,6 +16,7 @@ use deno_core::error::AnyError; use deno_core::futures::Future; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; +use deno_core::serde_json::json; use deno_core::ModuleSpecifier; use deno_lint::rules; use lspower::lsp; @@ -352,7 +353,7 @@ fn is_preferred( /// Convert changes returned from a TypeScript quick fix action into edits /// for an LSP CodeAction. -async fn ts_changes_to_edit( +pub async fn ts_changes_to_edit( changes: &[tsc::FileTextChanges], index_provider: &F, version_provider: &V, @@ -376,6 +377,13 @@ where })) } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionData { + pub specifier: ModuleSpecifier, + pub fix_id: String, +} + #[derive(Debug, Default)] pub struct CodeActionCollection { actions: Vec<(lsp::CodeAction, tsc::CodeFixAction)>, @@ -442,31 +450,16 @@ impl CodeActionCollection { /// Add a TypeScript action to the actions as a "fix all" action, where it /// will fix all occurrences of the diagnostic in the file. - pub async fn add_ts_fix_all_action( + pub fn add_ts_fix_all_action( &mut self, action: &tsc::CodeFixAction, + specifier: &ModuleSpecifier, diagnostic: &lsp::Diagnostic, - combined_code_actions: &tsc::CombinedCodeActions, - index_provider: &F, - version_provider: &V, - ) -> Result<(), AnyError> - where - F: Fn(ModuleSpecifier) -> Fut + Clone, - Fut: Future>, - V: Fn(ModuleSpecifier) -> Option, - { - if combined_code_actions.commands.is_some() { - return Err(custom_error( - "UnsupportedFix", - "The action returned from TypeScript is unsupported.", - )); - } - let edit = ts_changes_to_edit( - &combined_code_actions.changes, - index_provider, - version_provider, - ) - .await?; + ) { + let data = Some(json!({ + "specifier": specifier, + "fixId": action.fix_id, + })); let title = if let Some(description) = &action.fix_all_description { description.clone() } else { @@ -477,11 +470,11 @@ impl CodeActionCollection { title, kind: Some(lsp::CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), - edit, + edit: None, command: None, is_preferred: None, disabled: None, - data: None, + data, }; if let Some((existing, _)) = self.fix_all_actions.get(&action.fix_id.clone().unwrap()) @@ -493,7 +486,6 @@ impl CodeActionCollection { action.fix_id.clone().unwrap(), (code_action, action.clone()), ); - Ok(()) } /// Move out the code actions and return them as a `CodeActionResponse`. diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index 93afbce868..9eed85b730 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -32,7 +32,7 @@ fn code_action_capabilities( .map_or(CodeActionProviderCapability::Simple(true), |_| { CodeActionProviderCapability::Options(CodeActionOptions { code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]), - resolve_provider: None, + resolve_provider: Some(true), work_done_progress_options: Default::default(), }) }) diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index fa613f6962..838ca27254 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -30,7 +30,9 @@ use crate::import_map::ImportMap; use crate::tsc_config::parse_config; use crate::tsc_config::TsConfig; +use super::analysis::ts_changes_to_edit; use super::analysis::CodeActionCollection; +use super::analysis::CodeActionData; use super::analysis::CodeLensData; use super::analysis::CodeLensSource; use super::capabilities; @@ -945,35 +947,7 @@ impl Inner { diagnostic, &file_diagnostics, ) { - let req = tsc::RequestMethod::GetCombinedCodeFix(( - specifier.clone(), - json!(action.fix_id.clone().unwrap()), - )); - let res = - self.ts_server.request(self.snapshot(), req).await.map_err( - |err| { - error!("Unable to get combined fix from TypeScript: {}", err); - LspError::internal_error() - }, - )?; - let combined_code_actions: tsc::CombinedCodeActions = from_value(res) - .map_err(|err| { - error!("Cannot decode combined actions from TypeScript: {}", err); - LspError::internal_error() - })?; - code_actions - .add_ts_fix_all_action( - &action, - diagnostic, - &combined_code_actions, - &|s| self.get_line_index(s), - &|s| self.documents.version(&s), - ) - .await - .map_err(|err| { - error!("Unable to add fix all: {}", err); - LspError::internal_error() - })?; + code_actions.add_ts_fix_all_action(&action, &specifier, diagnostic); } } } @@ -983,6 +957,59 @@ impl Inner { Ok(Some(code_action_response)) } + async fn code_action_resolve( + &self, + params: CodeAction, + ) -> LspResult { + let mark = self.performance.mark("code_action_resolve"); + let result = + if let Some(data) = params.data.clone() { + let code_action_data: CodeActionData = + from_value(data).map_err(|err| { + error!("Unable to decode code action data: {}", err); + LspError::invalid_params("The CodeAction's data is invalid.") + })?; + let req = tsc::RequestMethod::GetCombinedCodeFix(( + code_action_data.specifier, + json!(code_action_data.fix_id.clone()), + )); + let res = self.ts_server.request(self.snapshot(), req).await.map_err( + |err| { + error!("Unable to get combined fix from TypeScript: {}", err); + LspError::internal_error() + }, + )?; + let combined_code_actions: tsc::CombinedCodeActions = + from_value(res).map_err(|err| { + error!("Cannot decode combined actions from TypeScript: {}", err); + LspError::internal_error() + })?; + if combined_code_actions.commands.is_some() { + error!("Deno does not support code actions with commands."); + Err(LspError::invalid_request()) + } else { + let mut code_action = params.clone(); + code_action.edit = ts_changes_to_edit( + &combined_code_actions.changes, + &|s| self.get_line_index(s), + &|s| self.documents.version(&s), + ) + .await + .map_err(|err| { + error!("Unable to convert changes to edits: {}", err); + LspError::internal_error() + })?; + Ok(code_action) + } + } else { + Err(LspError::invalid_params( + "The CodeAction's data is missing.", + )) + }; + self.performance.measure(mark); + result + } + async fn code_lens( &mut self, params: CodeLensParams, @@ -1610,6 +1637,13 @@ impl lspower::LanguageServer for LanguageServer { self.0.lock().await.code_action(params).await } + async fn code_action_resolve( + &self, + params: CodeAction, + ) -> LspResult { + self.0.lock().await.code_action_resolve(params).await + } + async fn code_lens( &self, params: CodeLensParams, @@ -2319,6 +2353,13 @@ mod tests { "code_action_request.json", LspResponse::RequestFixture(2, "code_action_response.json".to_string()), ), + ( + "code_action_resolve_request.json", + LspResponse::RequestFixture( + 4, + "code_action_resolve_request_response.json".to_string(), + ), + ), ( "shutdown_request.json", LspResponse::Request(3, json!(null)), diff --git a/cli/tests/lsp/code_action_resolve_request.json b/cli/tests/lsp/code_action_resolve_request.json new file mode 100644 index 0000000000..48a2eea3bc --- /dev/null +++ b/cli/tests/lsp/code_action_resolve_request.json @@ -0,0 +1,32 @@ +{ + "jsonrpc": "2.0", + "id": 4, + "method": "codeAction/resolve", + "params": { + "title": "Add all missing 'async' modifiers", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 7 + } + }, + "severity": 1, + "code": 1308, + "source": "deno-ts", + "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", + "relatedInformation": [] + } + ], + "data": { + "specifier": "file:///a/file.ts", + "fixId": "fixAwaitInSyncFunction" + } + } +} diff --git a/cli/tests/lsp/code_action_resolve_request_response.json b/cli/tests/lsp/code_action_resolve_request_response.json new file mode 100644 index 0000000000..e3f5b3f0ed --- /dev/null +++ b/cli/tests/lsp/code_action_resolve_request_response.json @@ -0,0 +1,91 @@ +{ + "title": "Add all missing 'async' modifiers", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 7 + } + }, + "severity": 1, + "code": 1308, + "source": "deno-ts", + "message": "'await' expressions are only allowed within async functions and at the top levels of modules.", + "relatedInformation": [] + } + ], + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///a/file.ts", + "version": 1 + }, + "edits": [ + { + "range": { + "start": { + "line": 0, + "character": 7 + }, + "end": { + "line": 0, + "character": 7 + } + }, + "newText": "async " + }, + { + "range": { + "start": { + "line": 0, + "character": 21 + }, + "end": { + "line": 0, + "character": 25 + } + }, + "newText": "Promise" + }, + { + "range": { + "start": { + "line": 4, + "character": 7 + }, + "end": { + "line": 4, + "character": 7 + } + }, + "newText": "async " + }, + { + "range": { + "start": { + "line": 4, + "character": 21 + }, + "end": { + "line": 4, + "character": 25 + } + }, + "newText": "Promise" + } + ] + } + ] + }, + "data": { + "specifier": "file:///a/file.ts", + "fixId": "fixAwaitInSyncFunction" + } +} diff --git a/cli/tests/lsp/code_action_response.json b/cli/tests/lsp/code_action_response.json index 5af45ba7f5..ab30898f8e 100644 --- a/cli/tests/lsp/code_action_response.json +++ b/cli/tests/lsp/code_action_response.json @@ -82,69 +82,9 @@ "relatedInformation": [] } ], - "edit": { - "documentChanges": [ - { - "textDocument": { - "uri": "file:///a/file.ts", - "version": 1 - }, - "edits": [ - { - "range": { - "start": { - "line": 0, - "character": 7 - }, - "end": { - "line": 0, - "character": 7 - } - }, - "newText": "async " - }, - { - "range": { - "start": { - "line": 0, - "character": 21 - }, - "end": { - "line": 0, - "character": 25 - } - }, - "newText": "Promise" - }, - { - "range": { - "start": { - "line": 4, - "character": 7 - }, - "end": { - "line": 4, - "character": 7 - } - }, - "newText": "async " - }, - { - "range": { - "start": { - "line": 4, - "character": 21 - }, - "end": { - "line": 4, - "character": 25 - } - }, - "newText": "Promise" - } - ] - } - ] + "data": { + "specifier": "file:///a/file.ts", + "fixId": "fixAwaitInSyncFunction" } } ]