feat(lsp): add TS quick fix code actions (#9396)

This commit is contained in:
Kitson Kelly 2021-02-05 05:53:02 +11:00 committed by GitHub
parent 644a7ff2d7
commit b77fcbc518
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 944 additions and 8 deletions

View file

@ -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.

View file

@ -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::*;

View file

@ -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),
}),

View file

@ -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);
}),
),
(

View file

@ -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",
}),
}
}
}

View file

@ -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"
]
}
}
}

View file

@ -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>"
}
]
}
]
}
}
]

View file

@ -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"
}
}
}

View file

@ -20,6 +20,22 @@
},
"capabilities": {
"textDocument": {
"codeAction": {
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"quickfix"
]
}
},
"isPreferredSupport": true,
"dataSupport": true,
"resolveSupport": {
"properties": [
"edit"
]
}
},
"synchronization": {
"dynamicRegistration": true,
"willSave": true,

View file

@ -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

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

@ -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";
}
}

View file

@ -37,6 +37,7 @@ use std::cell::RefCell;
use std::convert::From;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::pin::Pin;
use std::rc::Rc;
@ -82,7 +83,7 @@ pub fn init(isolate: &mut JsRuntime) {
pub trait FetchPermissions {
fn check_net_url(&self, _url: &Url) -> Result<(), AnyError>;
fn check_read(&self, _p: &PathBuf) -> Result<(), AnyError>;
fn check_read(&self, _p: &Path) -> Result<(), AnyError>;
}
/// For use with `op_fetch` when the user does not want permissions.
@ -93,7 +94,7 @@ impl FetchPermissions for NoFetchPermissions {
Ok(())
}
fn check_read(&self, _p: &PathBuf) -> Result<(), AnyError> {
fn check_read(&self, _p: &Path) -> Result<(), AnyError> {
Ok(())
}
}

View file

@ -624,7 +624,7 @@ impl deno_fetch::FetchPermissions for Permissions {
Permissions::check_net_url(self, url)
}
fn check_read(&self, p: &PathBuf) -> Result<(), AnyError> {
fn check_read(&self, p: &Path) -> Result<(), AnyError> {
Permissions::check_read(self, p)
}
}