summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/lsp/README.md18
-rw-r--r--cli/lsp/analysis.rs289
-rw-r--r--cli/lsp/capabilities.rs25
-rw-r--r--cli/lsp/language_server.rs207
-rw-r--r--cli/lsp/tsc.rs115
-rw-r--r--cli/tests/lsp/code_action_request.json44
-rw-r--r--cli/tests/lsp/code_action_response.json150
-rw-r--r--cli/tests/lsp/did_open_notification_code_action.json12
-rw-r--r--cli/tests/lsp/initialize_request.json16
-rw-r--r--cli/tsc/99_main_compiler.js45
-rw-r--r--cli/tsc/compiler.d.ts24
11 files changed, 940 insertions, 5 deletions
diff --git a/cli/lsp/README.md b/cli/lsp/README.md
index 87a662fc3..c43590f60 100644
--- a/cli/lsp/README.md
+++ b/cli/lsp/README.md
@@ -14,3 +14,21 @@ integrated into the command line and can be started via the `lsp` sub-command.
When the language server is started, a `LanguageServer` instance is created
which holds all of the state of the language server. It also defines all of the
methods that the client calls via the Language Server RPC protocol.
+
+## Custom requests
+
+The LSP currently supports the following custom requests. A client should
+implement these in order to have a fully functioning client that integrates well
+with Deno:
+
+- `deno/cache` - This command will instruct Deno to attempt to cache a module
+ and all of its dependencies. It expects an argument of
+ `{ textDocument: TextDocumentIdentifier }` to be passed.
+- `deno/performance` - Requests the return of the timing averages for the
+ internal instrumentation of Deno.
+- `deno/virtualTextDocument` - Requests a virtual text document from the LSP,
+ which is a read only document that can be displayed in the client. This allows
+ clients to access documents in the Deno cache, like remote modules and
+ TypeScript library files built into Deno. It also supports a special URL of
+ `deno:/status.md` which provides a markdown formatted text document that
+ contains details about the status of the LSP for display to a user.
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
index 95c0e95ff..1584ca79d 100644
--- a/cli/lsp/analysis.rs
+++ b/cli/lsp/analysis.rs
@@ -1,5 +1,8 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+use super::text::LineIndex;
+use super::tsc;
+
use crate::ast;
use crate::import_map::ImportMap;
use crate::media_type::MediaType;
@@ -8,7 +11,9 @@ use crate::module_graph::parse_ts_reference;
use crate::module_graph::TypeScriptReference;
use crate::tools::lint::create_linter;
+use deno_core::error::custom_error;
use deno_core::error::AnyError;
+use deno_core::futures::Future;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::ModuleSpecifier;
@@ -16,9 +21,40 @@ use deno_lint::rules;
use lspower::lsp;
use lspower::lsp::Position;
use lspower::lsp::Range;
+use std::cmp::Ordering;
use std::collections::HashMap;
use std::rc::Rc;
+lazy_static! {
+ /// Diagnostic error codes which actually are the same, and so when grouping
+ /// fixes we treat them the same.
+ static ref FIX_ALL_ERROR_CODES: HashMap<&'static str, &'static str> =
+ [("2339", "2339"), ("2345", "2339"),]
+ .iter()
+ .copied()
+ .collect();
+
+ /// Fixes which help determine if there is a preferred fix when there are
+ /// multiple fixes available.
+ static ref PREFERRED_FIXES: HashMap<&'static str, (u32, bool)> = [
+ ("annotateWithTypeFromJSDoc", (1, false)),
+ ("constructorForDerivedNeedSuperCall", (1, false)),
+ ("extendsInterfaceBecomesImplements", (1, false)),
+ ("awaitInSyncFunction", (1, false)),
+ ("classIncorrectlyImplementsInterface", (3, false)),
+ ("classDoesntImplementInheritedAbstractMember", (3, false)),
+ ("unreachableCode", (1, false)),
+ ("unusedIdentifier", (1, false)),
+ ("forgottenThisPropertyAccess", (1, false)),
+ ("spelling", (2, false)),
+ ("addMissingAwait", (1, false)),
+ ("fixImport", (0, true)),
+ ]
+ .iter()
+ .copied()
+ .collect();
+}
+
/// Category of self-generated diagnostic messages (those not coming from)
/// TypeScript.
pub enum Category {
@@ -264,6 +300,259 @@ pub struct CodeLensData {
pub specifier: ModuleSpecifier,
}
+fn code_as_string(code: &Option<lsp::NumberOrString>) -> String {
+ match code {
+ Some(lsp::NumberOrString::String(str)) => str.clone(),
+ Some(lsp::NumberOrString::Number(num)) => num.to_string(),
+ _ => "".to_string(),
+ }
+}
+
+/// Determines if two TypeScript diagnostic codes are effectively equivalent.
+fn is_equivalent_code(
+ a: &Option<lsp::NumberOrString>,
+ b: &Option<lsp::NumberOrString>,
+) -> bool {
+ let a_code = code_as_string(a);
+ let b_code = code_as_string(b);
+ FIX_ALL_ERROR_CODES.get(a_code.as_str())
+ == FIX_ALL_ERROR_CODES.get(b_code.as_str())
+}
+
+/// Return a boolean flag to indicate if the specified action is the preferred
+/// action for a given set of actions.
+fn is_preferred(
+ action: &tsc::CodeFixAction,
+ actions: &[(lsp::CodeAction, tsc::CodeFixAction)],
+ fix_priority: u32,
+ only_one: bool,
+) -> bool {
+ actions.iter().all(|(_, a)| {
+ if action == a {
+ return true;
+ }
+ if a.fix_id.is_some() {
+ return true;
+ }
+ if let Some((other_fix_priority, _)) =
+ PREFERRED_FIXES.get(a.fix_name.as_str())
+ {
+ match other_fix_priority.cmp(&fix_priority) {
+ Ordering::Less => return true,
+ Ordering::Greater => return false,
+ Ordering::Equal => (),
+ }
+ if only_one && action.fix_name == a.fix_name {
+ return false;
+ }
+ }
+ true
+ })
+}
+
+/// Convert changes returned from a TypeScript quick fix action into edits
+/// for an LSP CodeAction.
+async fn ts_changes_to_edit<F, Fut, V>(
+ changes: &[tsc::FileTextChanges],
+ index_provider: &F,
+ version_provider: &V,
+) -> Result<Option<lsp::WorkspaceEdit>, AnyError>
+where
+ F: Fn(ModuleSpecifier) -> Fut + Clone,
+ Fut: Future<Output = Result<LineIndex, AnyError>>,
+ V: Fn(ModuleSpecifier) -> Option<i32>,
+{
+ let mut text_document_edits = Vec::new();
+ for change in changes {
+ let text_document_edit = change
+ .to_text_document_edit(index_provider, version_provider)
+ .await?;
+ text_document_edits.push(text_document_edit);
+ }
+ Ok(Some(lsp::WorkspaceEdit {
+ changes: None,
+ document_changes: Some(lsp::DocumentChanges::Edits(text_document_edits)),
+ change_annotations: None,
+ }))
+}
+
+#[derive(Debug, Default)]
+pub struct CodeActionCollection {
+ actions: Vec<(lsp::CodeAction, tsc::CodeFixAction)>,
+ fix_all_actions: HashMap<String, (lsp::CodeAction, tsc::CodeFixAction)>,
+}
+
+impl CodeActionCollection {
+ /// Add a TypeScript code fix action to the code actions collection.
+ pub async fn add_ts_fix_action<F, Fut, V>(
+ &mut self,
+ action: &tsc::CodeFixAction,
+ diagnostic: &lsp::Diagnostic,
+ index_provider: &F,
+ version_provider: &V,
+ ) -> Result<(), AnyError>
+ where
+ F: Fn(ModuleSpecifier) -> Fut + Clone,
+ Fut: Future<Output = Result<LineIndex, AnyError>>,
+ V: Fn(ModuleSpecifier) -> Option<i32>,
+ {
+ if action.commands.is_some() {
+ // In theory, tsc can return actions that require "commands" to be applied
+ // back into TypeScript. Currently there is only one command, `install
+ // package` but Deno doesn't support that. The problem is that the
+ // `.applyCodeActionCommand()` returns a promise, and with the current way
+ // we wrap tsc, we can't handle the asynchronous response, so it is
+ // actually easier to return errors if we ever encounter one of these,
+ // which we really wouldn't expect from the Deno lsp.
+ return Err(custom_error(
+ "UnsupportedFix",
+ "The action returned from TypeScript is unsupported.",
+ ));
+ }
+ let edit =
+ ts_changes_to_edit(&action.changes, index_provider, version_provider)
+ .await?;
+ let code_action = lsp::CodeAction {
+ title: action.description.clone(),
+ kind: Some(lsp::CodeActionKind::QUICKFIX),
+ diagnostics: Some(vec![diagnostic.clone()]),
+ edit,
+ command: None,
+ is_preferred: None,
+ disabled: None,
+ data: None,
+ };
+ self.actions.retain(|(c, a)| {
+ !(action.fix_name == a.fix_name && code_action.edit == c.edit)
+ });
+ self.actions.push((code_action, action.clone()));
+
+ if let Some(fix_id) = &action.fix_id {
+ if let Some((existing_fix_all, existing_action)) =
+ self.fix_all_actions.get(fix_id)
+ {
+ self.actions.retain(|(c, _)| c != existing_fix_all);
+ self
+ .actions
+ .push((existing_fix_all.clone(), existing_action.clone()));
+ }
+ }
+ Ok(())
+ }
+
+ /// 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<F, Fut, V>(
+ &mut self,
+ action: &tsc::CodeFixAction,
+ diagnostic: &lsp::Diagnostic,
+ combined_code_actions: &tsc::CombinedCodeActions,
+ index_provider: &F,
+ version_provider: &V,
+ ) -> Result<(), AnyError>
+ where
+ F: Fn(ModuleSpecifier) -> Fut + Clone,
+ Fut: Future<Output = Result<LineIndex, AnyError>>,
+ V: Fn(ModuleSpecifier) -> Option<i32>,
+ {
+ 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 title = if let Some(description) = &action.fix_all_description {
+ description.clone()
+ } else {
+ format!("{} (Fix all in file)", action.description)
+ };
+
+ let code_action = lsp::CodeAction {
+ title,
+ kind: Some(lsp::CodeActionKind::QUICKFIX),
+ diagnostics: Some(vec![diagnostic.clone()]),
+ edit,
+ command: None,
+ is_preferred: None,
+ disabled: None,
+ data: None,
+ };
+ if let Some((existing, _)) =
+ self.fix_all_actions.get(&action.fix_id.clone().unwrap())
+ {
+ self.actions.retain(|(c, _)| c != existing);
+ }
+ self.actions.push((code_action.clone(), action.clone()));
+ self.fix_all_actions.insert(
+ action.fix_id.clone().unwrap(),
+ (code_action, action.clone()),
+ );
+ Ok(())
+ }
+
+ /// Move out the code actions and return them as a `CodeActionResponse`.
+ pub fn get_response(self) -> lsp::CodeActionResponse {
+ self
+ .actions
+ .into_iter()
+ .map(|(c, _)| lsp::CodeActionOrCommand::CodeAction(c))
+ .collect()
+ }
+
+ /// Determine if a action can be converted into a "fix all" action.
+ pub fn is_fix_all_action(
+ &self,
+ action: &tsc::CodeFixAction,
+ diagnostic: &lsp::Diagnostic,
+ file_diagnostics: &[&lsp::Diagnostic],
+ ) -> bool {
+ // If the action does not have a fix id (indicating it can be "bundled up")
+ // or if the collection already contains a "bundled" action return false
+ if action.fix_id.is_none()
+ || self
+ .fix_all_actions
+ .contains_key(&action.fix_id.clone().unwrap())
+ {
+ false
+ } else {
+ // else iterate over the diagnostic in the file and see if there are any
+ // other diagnostics that could be bundled together in a "fix all" code
+ // action
+ file_diagnostics.iter().any(|d| {
+ if d == &diagnostic || d.code.is_none() || diagnostic.code.is_none() {
+ false
+ } else {
+ d.code == diagnostic.code
+ || is_equivalent_code(&d.code, &diagnostic.code)
+ }
+ })
+ }
+ }
+
+ /// Set the `.is_preferred` flag on code actions, this should be only executed
+ /// when all actions are added to the collection.
+ pub fn set_preferred_fixes(&mut self) {
+ let actions = self.actions.clone();
+ for (code_action, action) in self.actions.iter_mut() {
+ if action.fix_id.is_some() {
+ continue;
+ }
+ if let Some((fix_priority, only_one)) =
+ PREFERRED_FIXES.get(action.fix_name.as_str())
+ {
+ code_action.is_preferred =
+ Some(is_preferred(action, &actions, *fix_priority, *only_one));
+ }
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs
index 6e8082ee8..93afbce86 100644
--- a/cli/lsp/capabilities.rs
+++ b/cli/lsp/capabilities.rs
@@ -6,6 +6,9 @@
///! client.
///!
use lspower::lsp::ClientCapabilities;
+use lspower::lsp::CodeActionKind;
+use lspower::lsp::CodeActionOptions;
+use lspower::lsp::CodeActionProviderCapability;
use lspower::lsp::CodeLensOptions;
use lspower::lsp::CompletionOptions;
use lspower::lsp::HoverProviderCapability;
@@ -18,9 +21,27 @@ use lspower::lsp::TextDocumentSyncKind;
use lspower::lsp::TextDocumentSyncOptions;
use lspower::lsp::WorkDoneProgressOptions;
+fn code_action_capabilities(
+ client_capabilities: &ClientCapabilities,
+) -> CodeActionProviderCapability {
+ client_capabilities
+ .text_document
+ .as_ref()
+ .and_then(|it| it.code_action.as_ref())
+ .and_then(|it| it.code_action_literal_support.as_ref())
+ .map_or(CodeActionProviderCapability::Simple(true), |_| {
+ CodeActionProviderCapability::Options(CodeActionOptions {
+ code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
+ resolve_provider: None,
+ work_done_progress_options: Default::default(),
+ })
+ })
+}
+
pub fn server_capabilities(
- _client_capabilities: &ClientCapabilities,
+ client_capabilities: &ClientCapabilities,
) -> ServerCapabilities {
+ let code_action_provider = code_action_capabilities(client_capabilities);
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
@@ -59,7 +80,7 @@ pub fn server_capabilities(
document_highlight_provider: Some(OneOf::Left(true)),
document_symbol_provider: None,
workspace_symbol_provider: None,
- code_action_provider: None,
+ code_action_provider: Some(code_action_provider),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(true),
}),
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index 52022632c..d7d034db0 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -16,6 +16,7 @@ use lspower::lsp::request::*;
use lspower::lsp::*;
use lspower::Client;
use regex::Regex;
+use serde_json::from_value;
use std::cell::RefCell;
use std::collections::HashMap;
use std::env;
@@ -29,6 +30,7 @@ use crate::import_map::ImportMap;
use crate::tsc_config::parse_config;
use crate::tsc_config::TsConfig;
+use super::analysis::CodeActionCollection;
use super::analysis::CodeLensData;
use super::analysis::CodeLensSource;
use super::capabilities;
@@ -63,16 +65,31 @@ pub struct StateSnapshot {
#[derive(Debug)]
struct Inner {
+ /// Cached versions of "fixed" assets that can either be inlined in Rust or
+ /// are part of the TypeScript snapshot and have to be fetched out.
assets: HashMap<ModuleSpecifier, Option<AssetDocument>>,
+ /// The LSP client that this LSP server is connected to.
client: Client,
+ /// Configuration information.
config: Config,
+ /// A collection of diagnostics from different sources.
diagnostics: DiagnosticCollection,
+ /// The "in-memory" documents in the editor which can be updated and changed.
documents: DocumentCache,
+ /// An optional URL which provides the location of a TypeScript configuration
+ /// file which will be used by the Deno LSP.
maybe_config_uri: Option<Url>,
+ /// An optional import map which is used to resolve modules.
maybe_import_map: Option<ImportMap>,
+ /// The URL for the import map which is used to determine relative imports.
maybe_import_map_uri: Option<Url>,
+ /// A collection of measurements which instrument that performance of the LSP.
performance: Performance,
+ /// Cached sources that are read-only.
sources: Sources,
+ /// A memoized version of fixable diagnostic codes retrieved from TypeScript.
+ ts_fixable_diagnostics: Vec<String>,
+ /// An abstraction that handles interactions with TypeScript.
ts_server: TsServer,
}
@@ -101,6 +118,7 @@ impl Inner {
maybe_import_map_uri: Default::default(),
performance: Default::default(),
sources,
+ ts_fixable_diagnostics: Default::default(),
ts_server: TsServer::new(),
}
}
@@ -177,7 +195,9 @@ impl Inner {
specifier: &ModuleSpecifier,
) -> Result<tsc::NavigationTree, AnyError> {
if self.documents.contains(specifier) {
+ let mark = self.performance.mark("get_navigation_tree");
if let Some(navigation_tree) = self.documents.navigation_tree(specifier) {
+ self.performance.measure(mark);
Ok(navigation_tree)
} else {
let res = self
@@ -193,6 +213,7 @@ impl Inner {
self
.documents
.set_navigation_tree(specifier, navigation_tree.clone())?;
+ self.performance.measure(mark);
Ok(navigation_tree)
}
} else {
@@ -485,6 +506,7 @@ impl Inner {
params: InitializeParams,
) -> LspResult<InitializeResult> {
info!("Starting Deno language server...");
+ let mark = self.performance.mark("initialize");
let capabilities = capabilities::server_capabilities(&params.capabilities);
@@ -522,6 +544,24 @@ impl Inner {
warn!("Updating tsconfig has errored: {}", err);
}
+ if capabilities.code_action_provider.is_some() {
+ let res = self
+ .ts_server
+ .request(self.snapshot(), tsc::RequestMethod::GetSupportedCodeFixes)
+ .await
+ .map_err(|err| {
+ error!("Unable to get fixable diagnostics: {}", err);
+ LspError::internal_error()
+ })?;
+ let fixable_diagnostics: Vec<String> =
+ from_value(res).map_err(|err| {
+ error!("Unable to get fixable diagnostics: {}", err);
+ LspError::internal_error()
+ })?;
+ self.ts_fixable_diagnostics = fixable_diagnostics;
+ }
+
+ self.performance.measure(mark);
Ok(InitializeResult {
capabilities,
server_info: Some(server_info),
@@ -818,6 +858,129 @@ impl Inner {
}
}
+ async fn code_action(
+ &mut self,
+ params: CodeActionParams,
+ ) -> LspResult<Option<CodeActionResponse>> {
+ if !self.enabled() {
+ return Ok(None);
+ }
+
+ let mark = self.performance.mark("code_action");
+ let specifier = utils::normalize_url(params.text_document.uri);
+ let fixable_diagnostics: Vec<&Diagnostic> = params
+ .context
+ .diagnostics
+ .iter()
+ .filter(|d| match &d.source {
+ Some(source) => match source.as_str() {
+ "deno-ts" => match &d.code {
+ Some(NumberOrString::String(code)) => {
+ self.ts_fixable_diagnostics.contains(code)
+ }
+ Some(NumberOrString::Number(code)) => {
+ self.ts_fixable_diagnostics.contains(&code.to_string())
+ }
+ _ => false,
+ },
+ // currently only processing `deno-ts` quick fixes
+ _ => false,
+ },
+ None => false,
+ })
+ .collect();
+ if fixable_diagnostics.is_empty() {
+ self.performance.measure(mark);
+ return Ok(None);
+ }
+ let line_index = self.get_line_index_sync(&specifier).unwrap();
+ let file_diagnostics: Vec<&Diagnostic> = self
+ .diagnostics
+ .diagnostics_for(&specifier, &DiagnosticSource::TypeScript)
+ .collect();
+ let mut code_actions = CodeActionCollection::default();
+ for diagnostic in &fixable_diagnostics {
+ let code = match &diagnostic.code.clone().unwrap() {
+ NumberOrString::String(code) => code.to_string(),
+ NumberOrString::Number(code) => code.to_string(),
+ };
+ let codes = vec![code];
+ let req = tsc::RequestMethod::GetCodeFixes((
+ specifier.clone(),
+ line_index.offset_tsc(diagnostic.range.start)?,
+ line_index.offset_tsc(diagnostic.range.end)?,
+ codes,
+ ));
+ let res =
+ self
+ .ts_server
+ .request(self.snapshot(), req)
+ .await
+ .map_err(|err| {
+ error!("Error getting actions from TypeScript: {}", err);
+ LspError::internal_error()
+ })?;
+ let actions: Vec<tsc::CodeFixAction> =
+ from_value(res).map_err(|err| {
+ error!("Cannot decode actions from TypeScript: {}", err);
+ LspError::internal_error()
+ })?;
+ for action in actions {
+ code_actions
+ .add_ts_fix_action(
+ &action,
+ diagnostic,
+ &|s| self.get_line_index(s),
+ &|s| self.documents.version(&s),
+ )
+ .await
+ .map_err(|err| {
+ error!("Unable to convert fix: {}", err);
+ LspError::internal_error()
+ })?;
+ if code_actions.is_fix_all_action(
+ &action,
+ 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.set_preferred_fixes();
+ let code_action_response = code_actions.get_response();
+ self.performance.measure(mark);
+ Ok(Some(code_action_response))
+ }
+
async fn code_lens(
&mut self,
params: CodeLensParams,
@@ -1438,6 +1601,13 @@ impl lspower::LanguageServer for LanguageServer {
self.0.lock().await.hover(params).await
}
+ async fn code_action(
+ &self,
+ params: CodeActionParams,
+ ) -> LspResult<Option<CodeActionResponse>> {
+ self.0.lock().await.code_action(params).await
+ }
+
async fn code_lens(
&self,
params: CodeLensParams,
@@ -1512,6 +1682,7 @@ struct VirtualTextDocumentParams {
text_document: TextDocumentIdentifier,
}
+// These are implementations of custom commands supported by the LSP
impl Inner {
async fn cache(&mut self, params: CacheParams) -> LspResult<bool> {
let mark = self.performance.mark("cache");
@@ -1623,6 +1794,7 @@ mod tests {
RequestAny,
Request(u64, Value),
RequestAssert(V),
+ RequestFixture(u64, String),
}
type LspTestHarnessRequest = (&'static str, LspResponse<fn(Value)>);
@@ -1667,6 +1839,20 @@ mod tests {
Some(jsonrpc::Outgoing::Response(resp)) => assert(json!(resp)),
_ => panic!("unexpected result: {:?}", result),
},
+ LspResponse::RequestFixture(id, res_path_str) => {
+ let res_path = fixtures_path.join(res_path_str);
+ let res_str = fs::read_to_string(res_path).unwrap();
+ match result {
+ Some(jsonrpc::Outgoing::Response(resp)) => assert_eq!(
+ resp,
+ jsonrpc::Response::ok(
+ jsonrpc::Id::Number(*id),
+ serde_json::from_str(&res_str).unwrap()
+ )
+ ),
+ _ => panic!("unexpected result: {:?}", result),
+ }
+ }
},
Err(err) => panic!("Error result: {}", err),
}
@@ -2121,6 +2307,25 @@ mod tests {
harness.run().await;
}
+ #[tokio::test]
+ async fn test_code_actions() {
+ let mut harness = LspTestHarness::new(vec![
+ ("initialize_request.json", LspResponse::RequestAny),
+ ("initialized_notification.json", LspResponse::None),
+ ("did_open_notification_code_action.json", LspResponse::None),
+ (
+ "code_action_request.json",
+ LspResponse::RequestFixture(2, "code_action_response.json".to_string()),
+ ),
+ (
+ "shutdown_request.json",
+ LspResponse::Request(3, json!(null)),
+ ),
+ ("exit_notification.json", LspResponse::None),
+ ]);
+ harness.run().await;
+ }
+
#[derive(Deserialize)]
struct PerformanceAverages {
averages: Vec<PerformanceAverage>,
@@ -2166,7 +2371,7 @@ mod tests {
LspResponse::RequestAssert(|value| {
let resp: PerformanceResponse =
serde_json::from_value(value).unwrap();
- assert_eq!(resp.result.averages.len(), 9);
+ assert_eq!(resp.result.averages.len(), 10);
}),
),
(
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index 3fee900c6..579979b06 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -354,7 +354,7 @@ impl From<ScriptElementKind> for lsp::CompletionItemKind {
}
}
-#[derive(Debug, Clone, Deserialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TextSpan {
pub start: u32,
@@ -710,6 +710,90 @@ impl DocumentHighlights {
}
}
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct TextChange {
+ span: TextSpan,
+ new_text: String,
+}
+
+impl TextChange {
+ pub fn as_text_edit(
+ &self,
+ line_index: &LineIndex,
+ ) -> lsp::OneOf<lsp::TextEdit, lsp::AnnotatedTextEdit> {
+ lsp::OneOf::Left(lsp::TextEdit {
+ range: self.span.to_range(line_index),
+ new_text: self.new_text.clone(),
+ })
+ }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct FileTextChanges {
+ file_name: String,
+ text_changes: Vec<TextChange>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ is_new_file: Option<bool>,
+}
+
+impl FileTextChanges {
+ pub async fn to_text_document_edit<F, Fut, V>(
+ &self,
+ index_provider: &F,
+ version_provider: &V,
+ ) -> Result<lsp::TextDocumentEdit, AnyError>
+ where
+ F: Fn(ModuleSpecifier) -> Fut + Clone,
+ Fut: Future<Output = Result<LineIndex, AnyError>>,
+ V: Fn(ModuleSpecifier) -> Option<i32>,
+ {
+ let specifier = ModuleSpecifier::resolve_url(&self.file_name)?;
+ let line_index = index_provider(specifier.clone()).await?;
+ let edits = self
+ .text_changes
+ .iter()
+ .map(|tc| tc.as_text_edit(&line_index))
+ .collect();
+ Ok(lsp::TextDocumentEdit {
+ text_document: lsp::OptionalVersionedTextDocumentIdentifier {
+ uri: specifier.as_url().clone(),
+ version: version_provider(specifier),
+ },
+ edits,
+ })
+ }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct CodeFixAction {
+ pub description: String,
+ pub changes: Vec<FileTextChanges>,
+ // These are opaque types that should just be passed back when applying the
+ // action.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub commands: Option<Vec<Value>>,
+ pub fix_name: String,
+ // It appears currently that all fixIds are strings, but the protocol
+ // specifies an opaque type, the problem is that we need to use the id as a
+ // hash key, and `Value` does not implement hash (and it could provide a false
+ // positive depending on JSON whitespace, so we deserialize it but it might
+ // break in the future)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub fix_id: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub fix_all_description: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CombinedCodeActions {
+ pub changes: Vec<FileTextChanges>,
+ pub commands: Option<Vec<Value>>,
+}
+
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferenceEntry {
@@ -1215,8 +1299,12 @@ pub enum RequestMethod {
FindRenameLocations((ModuleSpecifier, u32, bool, bool, bool)),
/// Retrieve the text of an assets that exists in memory in the isolate.
GetAsset(ModuleSpecifier),
+ /// Retrieve code fixes for a range of a file with the provided error codes.
+ GetCodeFixes((ModuleSpecifier, u32, u32, Vec<String>)),
/// Get completion information at a given position (IntelliSense).
GetCompletions((ModuleSpecifier, u32, UserPreferences)),
+ /// Retrieve the combined code fixes for a fix id for a module.
+ GetCombinedCodeFix((ModuleSpecifier, Value)),
/// Get declaration information for a specific position.
GetDefinition((ModuleSpecifier, u32)),
/// Return diagnostics for given file.
@@ -1231,6 +1319,8 @@ pub enum RequestMethod {
GetQuickInfo((ModuleSpecifier, u32)),
/// Get document references for a specific position.
GetReferences((ModuleSpecifier, u32)),
+ /// Get the diagnostic codes that support some form of code fix.
+ GetSupportedCodeFixes,
}
impl RequestMethod {
@@ -1263,6 +1353,25 @@ impl RequestMethod {
"method": "getAsset",
"specifier": specifier,
}),
+ RequestMethod::GetCodeFixes((
+ specifier,
+ start_pos,
+ end_pos,
+ error_codes,
+ )) => json!({
+ "id": id,
+ "method": "getCodeFixes",
+ "specifier": specifier,
+ "startPosition": start_pos,
+ "endPosition": end_pos,
+ "errorCodes": error_codes,
+ }),
+ RequestMethod::GetCombinedCodeFix((specifier, fix_id)) => json!({
+ "id": id,
+ "method": "getCombinedCodeFix",
+ "specifier": specifier,
+ "fixId": fix_id,
+ }),
RequestMethod::GetCompletions((specifier, position, preferences)) => {
json!({
"id": id,
@@ -1317,6 +1426,10 @@ impl RequestMethod {
"specifier": specifier,
"position": position,
}),
+ RequestMethod::GetSupportedCodeFixes => json!({
+ "id": id,
+ "method": "getSupportedCodeFixes",
+ }),
}
}
}
diff --git a/cli/tests/lsp/code_action_request.json b/cli/tests/lsp/code_action_request.json
new file mode 100644
index 000000000..af6cbee8b
--- /dev/null
+++ b/cli/tests/lsp/code_action_request.json
@@ -0,0 +1,44 @@
+{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "method": "textDocument/codeAction",
+ "params": {
+ "textDocument": {
+ "uri": "file:///a/file.ts"
+ },
+ "range": {
+ "start": {
+ "line": 1,
+ "character": 2
+ },
+ "end": {
+ "line": 1,
+ "character": 7
+ }
+ },
+ "context": {
+ "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": []
+ }
+ ],
+ "only": [
+ "quickfix"
+ ]
+ }
+ }
+}
diff --git a/cli/tests/lsp/code_action_response.json b/cli/tests/lsp/code_action_response.json
new file mode 100644
index 000000000..5af45ba7f
--- /dev/null
+++ b/cli/tests/lsp/code_action_response.json
@@ -0,0 +1,150 @@
+[
+ {
+ "title": "Add async modifier to containing function",
+ "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<void>"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "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<void>"
+ },
+ {
+ "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<void>"
+ }
+ ]
+ }
+ ]
+ }
+ }
+]
diff --git a/cli/tests/lsp/did_open_notification_code_action.json b/cli/tests/lsp/did_open_notification_code_action.json
new file mode 100644
index 000000000..57559cf3c
--- /dev/null
+++ b/cli/tests/lsp/did_open_notification_code_action.json
@@ -0,0 +1,12 @@
+{
+ "jsonrpc": "2.0",
+ "method": "textDocument/didOpen",
+ "params": {
+ "textDocument": {
+ "uri": "file:///a/file.ts",
+ "languageId": "typescript",
+ "version": 1,
+ "text": "export function a(): void {\n await Promise.resolve(\"a\");\n}\n\nexport function b(): void {\n await Promise.resolve(\"b\");\n}\n"
+ }
+ }
+}
diff --git a/cli/tests/lsp/initialize_request.json b/cli/tests/lsp/initialize_request.json
index 46f96a2c5..eaea00a18 100644
--- a/cli/tests/lsp/initialize_request.json
+++ b/cli/tests/lsp/initialize_request.json
@@ -20,6 +20,22 @@
},
"capabilities": {
"textDocument": {
+ "codeAction": {
+ "codeActionLiteralSupport": {
+ "codeActionKind": {
+ "valueSet": [
+ "quickfix"
+ ]
+ }
+ },
+ "isPreferredSupport": true,
+ "dataSupport": true,
+ "resolveSupport": {
+ "properties": [
+ "edit"
+ ]
+ }
+ },
"synchronization": {
"dynamicRegistration": true,
"willSave": true,
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js
index fa25b207f..50631e83f 100644
--- a/cli/tsc/99_main_compiler.js
+++ b/cli/tsc/99_main_compiler.js
@@ -555,6 +555,45 @@ delete Object.prototype.__proto__;
);
return respond(id, sourceFile && sourceFile.text);
}
+ case "getCodeFixes": {
+ return respond(
+ id,
+ languageService.getCodeFixesAtPosition(
+ request.specifier,
+ request.startPosition,
+ request.endPosition,
+ request.errorCodes.map((v) => Number(v)),
+ {
+ indentSize: 2,
+ indentStyle: ts.IndentStyle.Block,
+ semicolons: ts.SemicolonPreference.Insert,
+ },
+ {
+ quotePreference: "double",
+ },
+ ),
+ );
+ }
+ case "getCombinedCodeFix": {
+ return respond(
+ id,
+ languageService.getCombinedCodeFix(
+ {
+ type: "file",
+ fileName: request.specifier,
+ },
+ request.fixId,
+ {
+ indentSize: 2,
+ indentStyle: ts.IndentStyle.Block,
+ semicolons: ts.SemicolonPreference.Insert,
+ },
+ {
+ quotePreference: "double",
+ },
+ ),
+ );
+ }
case "getCompletions": {
return respond(
id,
@@ -638,6 +677,12 @@ delete Object.prototype.__proto__;
),
);
}
+ case "getSupportedCodeFixes": {
+ return respond(
+ id,
+ ts.getSupportedCodeFixes(),
+ );
+ }
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 17d6ddb38..4e5dcdb96 100644
--- a/cli/tsc/compiler.d.ts
+++ b/cli/tsc/compiler.d.ts
@@ -44,6 +44,8 @@ declare global {
| ConfigureRequest
| FindRenameLocationsRequest
| GetAsset
+ | GetCodeFixes
+ | GetCombinedCodeFix
| GetCompletionsRequest
| GetDefinitionRequest
| GetDiagnosticsRequest
@@ -51,7 +53,8 @@ declare global {
| GetImplementationRequest
| GetNavigationTree
| GetQuickInfoRequest
- | GetReferencesRequest;
+ | GetReferencesRequest
+ | GetSupportedCodeFixes;
interface BaseLanguageServerRequest {
id: number;
@@ -78,6 +81,21 @@ declare global {
specifier: string;
}
+ interface GetCodeFixes extends BaseLanguageServerRequest {
+ method: "getCodeFixes";
+ specifier: string;
+ startPosition: number;
+ endPosition: number;
+ errorCodes: string[];
+ }
+
+ interface GetCombinedCodeFix extends BaseLanguageServerRequest {
+ method: "getCombinedCodeFix";
+ specifier: string;
+ // deno-lint-ignore ban-types
+ fixId: {};
+ }
+
interface GetCompletionsRequest extends BaseLanguageServerRequest {
method: "getCompletions";
specifier: string;
@@ -125,4 +143,8 @@ declare global {
specifier: string;
position: number;
}
+
+ interface GetSupportedCodeFixes extends BaseLanguageServerRequest {
+ method: "getSupportedCodeFixes";
+ }
}