mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 15:24:46 -05:00
feat(lsp): implement refactoring code actions (#11555)
Closes: denoland/vscode_deno#433
This commit is contained in:
parent
3f0cf9619f
commit
728d205d9d
12 changed files with 882 additions and 93 deletions
|
@ -30,6 +30,7 @@ use lspower::lsp::WorkDoneProgressOptions;
|
|||
use lspower::lsp::WorkspaceFoldersServerCapabilities;
|
||||
use lspower::lsp::WorkspaceServerCapabilities;
|
||||
|
||||
use super::refactor::ALL_KNOWN_REFACTOR_ACTION_KINDS;
|
||||
use super::semantic_tokens::get_legend;
|
||||
|
||||
fn code_action_capabilities(
|
||||
|
@ -41,8 +42,16 @@ fn code_action_capabilities(
|
|||
.and_then(|it| it.code_action.as_ref())
|
||||
.and_then(|it| it.code_action_literal_support.as_ref())
|
||||
.map_or(CodeActionProviderCapability::Simple(true), |_| {
|
||||
let mut code_action_kinds =
|
||||
vec![CodeActionKind::QUICKFIX, CodeActionKind::REFACTOR];
|
||||
code_action_kinds.extend(
|
||||
ALL_KNOWN_REFACTOR_ACTION_KINDS
|
||||
.iter()
|
||||
.map(|action| action.kind.clone()),
|
||||
);
|
||||
|
||||
CodeActionProviderCapability::Options(CodeActionOptions {
|
||||
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
|
||||
code_action_kinds: Some(code_action_kinds),
|
||||
resolve_provider: Some(true),
|
||||
work_done_progress_options: Default::default(),
|
||||
})
|
||||
|
|
|
@ -41,6 +41,7 @@ use super::documents::LanguageId;
|
|||
use super::lsp_custom;
|
||||
use super::parent_process_checker;
|
||||
use super::performance::Performance;
|
||||
use super::refactor;
|
||||
use super::registries;
|
||||
use super::sources;
|
||||
use super::sources::Sources;
|
||||
|
@ -1156,6 +1157,10 @@ impl Inner {
|
|||
}
|
||||
|
||||
let mark = self.performance.mark("code_action", Some(¶ms));
|
||||
let mut all_actions = CodeActionResponse::new();
|
||||
let line_index = self.get_line_index_sync(&specifier).unwrap();
|
||||
|
||||
// QuickFix
|
||||
let fixable_diagnostics: Vec<&Diagnostic> = params
|
||||
.context
|
||||
.diagnostics
|
||||
|
@ -1183,93 +1188,139 @@ impl Inner {
|
|||
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 mut code_actions = CodeActionCollection::default();
|
||||
let file_diagnostics = self
|
||||
.diagnostics_server
|
||||
.get(&specifier, DiagnosticSource::TypeScript)
|
||||
.await;
|
||||
for diagnostic in &fixable_diagnostics {
|
||||
match diagnostic.source.as_deref() {
|
||||
Some("deno-ts") => {
|
||||
let code = match diagnostic.code.as_ref().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 actions: Vec<tsc::CodeFixAction> =
|
||||
match self.ts_server.request(self.snapshot()?, req).await {
|
||||
Ok(items) => items,
|
||||
Err(err) => {
|
||||
// sometimes tsc reports errors when retrieving code actions
|
||||
// because they don't reflect the current state of the document
|
||||
// so we will log them to the output, but we won't send an error
|
||||
// message back to the client.
|
||||
error!("Error getting actions from TypeScript: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
if !fixable_diagnostics.is_empty() {
|
||||
let mut code_actions = CodeActionCollection::default();
|
||||
let file_diagnostics = self
|
||||
.diagnostics_server
|
||||
.get(&specifier, DiagnosticSource::TypeScript)
|
||||
.await;
|
||||
for diagnostic in &fixable_diagnostics {
|
||||
match diagnostic.source.as_deref() {
|
||||
Some("deno-ts") => {
|
||||
let code = match diagnostic.code.as_ref().unwrap() {
|
||||
NumberOrString::String(code) => code.to_string(),
|
||||
NumberOrString::Number(code) => code.to_string(),
|
||||
};
|
||||
for action in actions {
|
||||
code_actions
|
||||
.add_ts_fix_action(&specifier, &action, diagnostic, self)
|
||||
.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 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 actions: Vec<tsc::CodeFixAction> =
|
||||
match self.ts_server.request(self.snapshot()?, req).await {
|
||||
Ok(items) => items,
|
||||
Err(err) => {
|
||||
// sometimes tsc reports errors when retrieving code actions
|
||||
// because they don't reflect the current state of the document
|
||||
// so we will log them to the output, but we won't send an error
|
||||
// message back to the client.
|
||||
error!("Error getting actions from TypeScript: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
for action in actions {
|
||||
code_actions
|
||||
.add_ts_fix_all_action(&action, &specifier, diagnostic);
|
||||
.add_ts_fix_action(&specifier, &action, diagnostic, self)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Unable to convert fix: {}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
if code_actions.is_fix_all_action(
|
||||
&action,
|
||||
diagnostic,
|
||||
&file_diagnostics,
|
||||
) {
|
||||
code_actions
|
||||
.add_ts_fix_all_action(&action, &specifier, diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("deno") => {
|
||||
code_actions
|
||||
Some("deno") => code_actions
|
||||
.add_deno_fix_action(diagnostic)
|
||||
.map_err(|err| {
|
||||
error!("{}", err);
|
||||
LspError::internal_error()
|
||||
})?
|
||||
})?,
|
||||
Some("deno-lint") => code_actions
|
||||
.add_deno_lint_ignore_action(
|
||||
&specifier,
|
||||
self.documents.docs.get(&specifier),
|
||||
diagnostic,
|
||||
)
|
||||
.map_err(|err| {
|
||||
error!("Unable to fix lint error: {}", err);
|
||||
LspError::internal_error()
|
||||
})?,
|
||||
_ => (),
|
||||
}
|
||||
Some("deno-lint") => code_actions
|
||||
.add_deno_lint_ignore_action(
|
||||
&specifier,
|
||||
self.documents.docs.get(&specifier),
|
||||
diagnostic,
|
||||
)
|
||||
.map_err(|err| {
|
||||
error!("Unable to fix lint error: {}", err);
|
||||
LspError::internal_error()
|
||||
})?,
|
||||
_ => (),
|
||||
}
|
||||
code_actions.set_preferred_fixes();
|
||||
all_actions.extend(code_actions.get_response());
|
||||
}
|
||||
code_actions.set_preferred_fixes();
|
||||
let code_action_response = code_actions.get_response();
|
||||
|
||||
// Refactor
|
||||
let start = line_index.offset_tsc(params.range.start)?;
|
||||
let length = line_index.offset_tsc(params.range.end)? - start;
|
||||
let only =
|
||||
params
|
||||
.context
|
||||
.only
|
||||
.as_ref()
|
||||
.map_or(String::default(), |values| {
|
||||
values
|
||||
.first()
|
||||
.map_or(String::default(), |v| v.as_str().to_owned())
|
||||
});
|
||||
let req = tsc::RequestMethod::GetApplicableRefactors((
|
||||
specifier.clone(),
|
||||
tsc::TextSpan { start, length },
|
||||
only,
|
||||
));
|
||||
let refactor_infos: Vec<tsc::ApplicableRefactorInfo> = self
|
||||
.ts_server
|
||||
.request(self.snapshot()?, req)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to request to tsserver {}", err);
|
||||
LspError::invalid_request()
|
||||
})?;
|
||||
let mut refactor_actions = Vec::<CodeAction>::new();
|
||||
for refactor_info in refactor_infos.iter() {
|
||||
refactor_actions
|
||||
.extend(refactor_info.to_code_actions(&specifier, ¶ms.range));
|
||||
}
|
||||
all_actions.extend(
|
||||
refactor::prune_invalid_actions(&refactor_actions, 5)
|
||||
.into_iter()
|
||||
.map(CodeActionOrCommand::CodeAction),
|
||||
);
|
||||
|
||||
let response = if !all_actions.is_empty() {
|
||||
Some(all_actions)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.performance.measure(mark);
|
||||
Ok(Some(code_action_response))
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn code_action_resolve(
|
||||
&mut self,
|
||||
params: CodeAction,
|
||||
) -> LspResult<CodeAction> {
|
||||
if params.kind.is_none() || params.data.is_none() {
|
||||
return Ok(params);
|
||||
}
|
||||
|
||||
let mark = self.performance.mark("code_action_resolve", Some(¶ms));
|
||||
let result = if let Some(data) = params.data.clone() {
|
||||
let kind = params.kind.clone().unwrap();
|
||||
let data = params.data.clone().unwrap();
|
||||
|
||||
let result = if kind.as_str().starts_with(CodeActionKind::QUICKFIX.as_str())
|
||||
{
|
||||
let code_action_data: CodeActionData =
|
||||
from_value(data).map_err(|err| {
|
||||
error!("Unable to decode code action data: {}", err);
|
||||
|
@ -1289,35 +1340,69 @@ impl Inner {
|
|||
})?;
|
||||
if combined_code_actions.commands.is_some() {
|
||||
error!("Deno does not support code actions with commands.");
|
||||
Err(LspError::invalid_request())
|
||||
} else {
|
||||
let changes = if code_action_data.fix_id == "fixMissingImport" {
|
||||
fix_ts_import_changes(
|
||||
&code_action_data.specifier,
|
||||
&combined_code_actions.changes,
|
||||
self,
|
||||
)
|
||||
.map_err(|err| {
|
||||
error!("Unable to remap changes: {}", err);
|
||||
LspError::internal_error()
|
||||
})?
|
||||
} else {
|
||||
combined_code_actions.changes.clone()
|
||||
};
|
||||
let mut code_action = params.clone();
|
||||
code_action.edit =
|
||||
ts_changes_to_edit(&changes, self).await.map_err(|err| {
|
||||
error!("Unable to convert changes to edits: {}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
Ok(code_action)
|
||||
return Err(LspError::invalid_request());
|
||||
}
|
||||
|
||||
let changes = if code_action_data.fix_id == "fixMissingImport" {
|
||||
fix_ts_import_changes(
|
||||
&code_action_data.specifier,
|
||||
&combined_code_actions.changes,
|
||||
self,
|
||||
)
|
||||
.map_err(|err| {
|
||||
error!("Unable to remap changes: {}", err);
|
||||
LspError::internal_error()
|
||||
})?
|
||||
} else {
|
||||
combined_code_actions.changes.clone()
|
||||
};
|
||||
let mut code_action = params.clone();
|
||||
code_action.edit =
|
||||
ts_changes_to_edit(&changes, self).await.map_err(|err| {
|
||||
error!("Unable to convert changes to edits: {}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
code_action
|
||||
} else if kind.as_str().starts_with(CodeActionKind::REFACTOR.as_str()) {
|
||||
let mut code_action = params.clone();
|
||||
let action_data: refactor::RefactorCodeActionData = from_value(data)
|
||||
.map_err(|err| {
|
||||
error!("Unable to decode code action data: {}", err);
|
||||
LspError::invalid_params("The CodeAction's data is invalid.")
|
||||
})?;
|
||||
let line_index =
|
||||
self.get_line_index_sync(&action_data.specifier).unwrap();
|
||||
let start = line_index.offset_tsc(action_data.range.start)?;
|
||||
let length = line_index.offset_tsc(action_data.range.end)? - start;
|
||||
let req = tsc::RequestMethod::GetEditsForRefactor((
|
||||
action_data.specifier.clone(),
|
||||
tsc::TextSpan { start, length },
|
||||
action_data.refactor_name.clone(),
|
||||
action_data.action_name.clone(),
|
||||
));
|
||||
let refactor_edit_info: tsc::RefactorEditInfo = self
|
||||
.ts_server
|
||||
.request(self.snapshot()?, req)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to request to tsserver {}", err);
|
||||
LspError::invalid_request()
|
||||
})?;
|
||||
code_action.edit = refactor_edit_info
|
||||
.to_workspace_edit(self)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Unable to convert changes to edits: {}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
code_action
|
||||
} else {
|
||||
// The code action doesn't need to be resolved
|
||||
Ok(params)
|
||||
params
|
||||
};
|
||||
|
||||
self.performance.measure(mark);
|
||||
result
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn code_lens(
|
||||
|
|
|
@ -16,6 +16,7 @@ mod lsp_custom;
|
|||
mod parent_process_checker;
|
||||
mod path_to_regex;
|
||||
mod performance;
|
||||
mod refactor;
|
||||
mod registries;
|
||||
mod semantic_tokens;
|
||||
mod sources;
|
||||
|
|
131
cli/lsp/refactor.rs
Normal file
131
cli/lsp/refactor.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
// The logic of this module is heavily influenced by
|
||||
// https://github.com/microsoft/vscode/blob/main/extensions/typescript-language-features/src/languageFeatures/refactor.ts
|
||||
|
||||
use deno_core::serde::Deserialize;
|
||||
use deno_core::serde::Serialize;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use lspower::lsp;
|
||||
|
||||
pub struct RefactorCodeActionKind {
|
||||
pub kind: lsp::CodeActionKind,
|
||||
matches_callback: Box<dyn Fn(&str) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
impl RefactorCodeActionKind {
|
||||
pub fn matches(&self, tag: &str) -> bool {
|
||||
(self.matches_callback)(tag)
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref EXTRACT_FUNCTION: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_EXTRACT.as_str(), "function"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("function_")),
|
||||
};
|
||||
|
||||
pub static ref EXTRACT_CONSTANT: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_EXTRACT.as_str(), "constant"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("constant_")),
|
||||
};
|
||||
|
||||
pub static ref EXTRACT_TYPE: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_EXTRACT.as_str(), "type"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("Extract to type alias")),
|
||||
};
|
||||
|
||||
pub static ref EXTRACT_INTERFACE: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_EXTRACT.as_str(), "interface"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("Extract to interface")),
|
||||
};
|
||||
|
||||
pub static ref MOVE_NEWFILE: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR.as_str(), "move", "newFile"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("Move to a new file")),
|
||||
};
|
||||
|
||||
pub static ref REWRITE_IMPORT: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_REWRITE.as_str(), "import"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("Convert namespace import") || tag.starts_with("Convert named imports")),
|
||||
};
|
||||
|
||||
pub static ref REWRITE_EXPORT: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_REWRITE.as_str(), "export"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("Convert default export") || tag.starts_with("Convert named export")),
|
||||
};
|
||||
|
||||
pub static ref REWRITE_ARROW_BRACES: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_REWRITE.as_str(), "arrow", "braces"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("Add or remove braces in an arrow function")),
|
||||
};
|
||||
|
||||
pub static ref REWRITE_PARAMETERS_TO_DESTRUCTURED: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_REWRITE.as_str(), "parameters", "toDestructured"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("Convert parameters to destructured object")),
|
||||
};
|
||||
|
||||
pub static ref REWRITE_PROPERTY_GENERATEACCESSORS: RefactorCodeActionKind = RefactorCodeActionKind {
|
||||
kind : [lsp::CodeActionKind::REFACTOR_REWRITE.as_str(), "property", "generateAccessors"].join(".").into(),
|
||||
matches_callback: Box::new(|tag: &str| tag.starts_with("Generate 'get' and 'set' accessors")),
|
||||
};
|
||||
|
||||
pub static ref ALL_KNOWN_REFACTOR_ACTION_KINDS: Vec<&'static RefactorCodeActionKind> = vec![
|
||||
&EXTRACT_FUNCTION,
|
||||
&EXTRACT_CONSTANT,
|
||||
&EXTRACT_TYPE,
|
||||
&EXTRACT_INTERFACE,
|
||||
&MOVE_NEWFILE,
|
||||
&REWRITE_IMPORT,
|
||||
&REWRITE_EXPORT,
|
||||
&REWRITE_ARROW_BRACES,
|
||||
&REWRITE_PARAMETERS_TO_DESTRUCTURED,
|
||||
&REWRITE_PROPERTY_GENERATEACCESSORS
|
||||
];
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefactorCodeActionData {
|
||||
pub specifier: ModuleSpecifier,
|
||||
pub range: lsp::Range,
|
||||
pub refactor_name: String,
|
||||
pub action_name: String,
|
||||
}
|
||||
|
||||
pub fn prune_invalid_actions(
|
||||
actions: &[lsp::CodeAction],
|
||||
number_of_invalid: usize,
|
||||
) -> Vec<lsp::CodeAction> {
|
||||
let mut available_actions = Vec::<lsp::CodeAction>::new();
|
||||
let mut invalid_common_actions = Vec::<lsp::CodeAction>::new();
|
||||
let mut invalid_uncommon_actions = Vec::<lsp::CodeAction>::new();
|
||||
for action in actions {
|
||||
if action.disabled.is_none() {
|
||||
available_actions.push(action.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// These are the common refactors that we should always show if applicable.
|
||||
let action_kind =
|
||||
action.kind.as_ref().map(|a| a.as_str()).unwrap_or_default();
|
||||
if action_kind.starts_with(EXTRACT_CONSTANT.kind.as_str())
|
||||
|| action_kind.starts_with(EXTRACT_FUNCTION.kind.as_str())
|
||||
{
|
||||
invalid_common_actions.push(action.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// These are the remaining refactors that we can show if we haven't reached the max limit with just common refactors.
|
||||
invalid_uncommon_actions.push(action.clone());
|
||||
}
|
||||
|
||||
let mut prioritized_actions = Vec::<lsp::CodeAction>::new();
|
||||
prioritized_actions.extend(invalid_common_actions);
|
||||
prioritized_actions.extend(invalid_uncommon_actions);
|
||||
let top_n_invalid = prioritized_actions
|
||||
[0..std::cmp::min(number_of_invalid, prioritized_actions.len())]
|
||||
.to_vec();
|
||||
available_actions.extend(top_n_invalid);
|
||||
available_actions
|
||||
}
|
213
cli/lsp/tsc.rs
213
cli/lsp/tsc.rs
|
@ -6,6 +6,11 @@ use super::code_lens;
|
|||
use super::config;
|
||||
use super::language_server;
|
||||
use super::language_server::StateSnapshot;
|
||||
use super::refactor::RefactorCodeActionData;
|
||||
use super::refactor::ALL_KNOWN_REFACTOR_ACTION_KINDS;
|
||||
use super::refactor::EXTRACT_CONSTANT;
|
||||
use super::refactor::EXTRACT_INTERFACE;
|
||||
use super::refactor::EXTRACT_TYPE;
|
||||
use super::semantic_tokens::SemanticTokensBuilder;
|
||||
use super::semantic_tokens::TsTokenEncodingConsts;
|
||||
use super::text;
|
||||
|
@ -1004,6 +1009,47 @@ impl FileTextChanges {
|
|||
edits,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn to_text_document_change_ops(
|
||||
&self,
|
||||
language_server: &mut language_server::Inner,
|
||||
) -> Result<Vec<lsp::DocumentChangeOperation>, AnyError> {
|
||||
let mut ops = Vec::<lsp::DocumentChangeOperation>::new();
|
||||
let specifier = normalize_specifier(&self.file_name)?;
|
||||
let line_index = if !self.is_new_file.unwrap_or(false) {
|
||||
language_server.get_line_index(specifier.clone()).await?
|
||||
} else {
|
||||
LineIndex::new("")
|
||||
};
|
||||
|
||||
if self.is_new_file.unwrap_or(false) {
|
||||
ops.push(lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(
|
||||
lsp::CreateFile {
|
||||
uri: specifier.clone(),
|
||||
options: Some(lsp::CreateFileOptions {
|
||||
ignore_if_exists: Some(true),
|
||||
overwrite: None,
|
||||
}),
|
||||
annotation_id: None,
|
||||
},
|
||||
)));
|
||||
}
|
||||
|
||||
let edits = self
|
||||
.text_changes
|
||||
.iter()
|
||||
.map(|tc| tc.as_text_edit(&line_index))
|
||||
.collect();
|
||||
ops.push(lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit {
|
||||
text_document: lsp::OptionalVersionedTextDocumentIdentifier {
|
||||
uri: specifier.clone(),
|
||||
version: language_server.document_version(specifier),
|
||||
},
|
||||
edits,
|
||||
}));
|
||||
|
||||
Ok(ops)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -1056,6 +1102,149 @@ impl Classifications {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefactorActionInfo {
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
not_applicable_reason: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
kind: Option<String>,
|
||||
}
|
||||
|
||||
impl RefactorActionInfo {
|
||||
pub fn get_action_kind(&self) -> lsp::CodeActionKind {
|
||||
if let Some(kind) = &self.kind {
|
||||
kind.clone().into()
|
||||
} else {
|
||||
let maybe_match = ALL_KNOWN_REFACTOR_ACTION_KINDS
|
||||
.iter()
|
||||
.find(|action| action.matches(&self.name));
|
||||
maybe_match
|
||||
.map_or(lsp::CodeActionKind::REFACTOR, |action| action.kind.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_preferred(&self, all_actions: &[RefactorActionInfo]) -> bool {
|
||||
if EXTRACT_CONSTANT.matches(&self.name) {
|
||||
let get_scope = |name: &str| -> Option<u32> {
|
||||
let scope_regex = Regex::new(r"scope_(\d)").unwrap();
|
||||
if let Some(captures) = scope_regex.captures(name) {
|
||||
captures[1].parse::<u32>().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
return if let Some(scope) = get_scope(&self.name) {
|
||||
all_actions
|
||||
.iter()
|
||||
.filter(|other| {
|
||||
!std::ptr::eq(&self, other) && EXTRACT_CONSTANT.matches(&other.name)
|
||||
})
|
||||
.all(|other| {
|
||||
if let Some(other_scope) = get_scope(&other.name) {
|
||||
scope < other_scope
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
false
|
||||
};
|
||||
}
|
||||
if EXTRACT_TYPE.matches(&self.name) || EXTRACT_INTERFACE.matches(&self.name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApplicableRefactorInfo {
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
inlineable: Option<bool>,
|
||||
actions: Vec<RefactorActionInfo>,
|
||||
}
|
||||
|
||||
impl ApplicableRefactorInfo {
|
||||
pub fn to_code_actions(
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
range: &lsp::Range,
|
||||
) -> Vec<lsp::CodeAction> {
|
||||
let mut code_actions = Vec::<lsp::CodeAction>::new();
|
||||
// All typescript refactoring actions are inlineable
|
||||
for action in self.actions.iter() {
|
||||
code_actions
|
||||
.push(self.as_inline_code_action(action, specifier, range, &self.name));
|
||||
}
|
||||
code_actions
|
||||
}
|
||||
|
||||
fn as_inline_code_action(
|
||||
&self,
|
||||
action: &RefactorActionInfo,
|
||||
specifier: &ModuleSpecifier,
|
||||
range: &lsp::Range,
|
||||
refactor_name: &str,
|
||||
) -> lsp::CodeAction {
|
||||
let disabled = action.not_applicable_reason.as_ref().map(|reason| {
|
||||
lsp::CodeActionDisabled {
|
||||
reason: reason.clone(),
|
||||
}
|
||||
});
|
||||
|
||||
lsp::CodeAction {
|
||||
title: action.description.to_string(),
|
||||
kind: Some(action.get_action_kind()),
|
||||
is_preferred: Some(action.is_preferred(&self.actions)),
|
||||
disabled,
|
||||
data: Some(
|
||||
serde_json::to_value(RefactorCodeActionData {
|
||||
specifier: specifier.clone(),
|
||||
range: *range,
|
||||
refactor_name: refactor_name.to_owned(),
|
||||
action_name: action.name.clone(),
|
||||
})
|
||||
.unwrap(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefactorEditInfo {
|
||||
edits: Vec<FileTextChanges>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rename_location: Option<u32>,
|
||||
}
|
||||
|
||||
impl RefactorEditInfo {
|
||||
pub(crate) async fn to_workspace_edit(
|
||||
&self,
|
||||
language_server: &mut language_server::Inner,
|
||||
) -> Result<Option<lsp::WorkspaceEdit>, AnyError> {
|
||||
let mut all_ops = Vec::<lsp::DocumentChangeOperation>::new();
|
||||
for edit in self.edits.iter() {
|
||||
let ops = edit.to_text_document_change_ops(language_server).await?;
|
||||
all_ops.extend(ops);
|
||||
}
|
||||
|
||||
Ok(Some(lsp::WorkspaceEdit {
|
||||
document_changes: Some(lsp::DocumentChanges::Operations(all_ops)),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CodeAction {
|
||||
|
@ -2421,6 +2610,10 @@ pub enum RequestMethod {
|
|||
},
|
||||
/// Retrieve the text of an assets that exists in memory in the isolate.
|
||||
GetAsset(ModuleSpecifier),
|
||||
/// Retrieve the possible refactor info for a range of a file.
|
||||
GetApplicableRefactors((ModuleSpecifier, TextSpan, String)),
|
||||
/// Retrieve the refactor edit info for a range.
|
||||
GetEditsForRefactor((ModuleSpecifier, TextSpan, String, String)),
|
||||
/// 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).
|
||||
|
@ -2491,6 +2684,26 @@ impl RequestMethod {
|
|||
"method": "getAsset",
|
||||
"specifier": specifier,
|
||||
}),
|
||||
RequestMethod::GetApplicableRefactors((specifier, span, kind)) => json!({
|
||||
"id": id,
|
||||
"method": "getApplicableRefactors",
|
||||
"specifier": state.denormalize_specifier(specifier),
|
||||
"range": { "pos": span.start, "end": span.start + span.length},
|
||||
"kind": kind,
|
||||
}),
|
||||
RequestMethod::GetEditsForRefactor((
|
||||
specifier,
|
||||
span,
|
||||
refactor_name,
|
||||
action_name,
|
||||
)) => json!({
|
||||
"id": id,
|
||||
"method": "getEditsForRefactor",
|
||||
"specifier": state.denormalize_specifier(specifier),
|
||||
"range": { "pos": span.start, "end": span.start + span.length},
|
||||
"refactorName": refactor_name,
|
||||
"actionName": action_name,
|
||||
}),
|
||||
RequestMethod::GetCodeFixes((
|
||||
specifier,
|
||||
start_pos,
|
||||
|
|
|
@ -2115,6 +2115,45 @@ fn lsp_code_actions_imports() {
|
|||
shutdown(&mut client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_code_actions_refactor() {
|
||||
let mut client = init("initialize_params.json");
|
||||
did_open(
|
||||
&mut client,
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "var x: { a?: number; b?: string } = {};\n"
|
||||
}
|
||||
}),
|
||||
);
|
||||
let (maybe_res, maybe_err) = client
|
||||
.write_request(
|
||||
"textDocument/codeAction",
|
||||
load_fixture("code_action_params_refactor.json"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(maybe_err.is_none());
|
||||
assert_eq!(
|
||||
maybe_res,
|
||||
Some(load_fixture("code_action_response_refactor.json"))
|
||||
);
|
||||
let (maybe_res, maybe_err) = client
|
||||
.write_request(
|
||||
"codeAction/resolve",
|
||||
load_fixture("code_action_resolve_params_refactor.json"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(maybe_err.is_none());
|
||||
assert_eq!(
|
||||
maybe_res,
|
||||
Some(load_fixture("code_action_resolve_response_refactor.json"))
|
||||
);
|
||||
shutdown(&mut client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_code_actions_deadlock() {
|
||||
let mut client = init("initialize_params.json");
|
||||
|
|
21
cli/tests/lsp/code_action_params_refactor.json
Normal file
21
cli/tests/lsp/code_action_params_refactor.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts"
|
||||
},
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"diagnostics": [],
|
||||
"only": [
|
||||
"refactor"
|
||||
]
|
||||
}
|
||||
}
|
20
cli/tests/lsp/code_action_resolve_params_refactor.json
Normal file
20
cli/tests/lsp/code_action_resolve_params_refactor.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"title": "Extract to interface",
|
||||
"kind": "refactor.extract.interface",
|
||||
"isPreferred": true,
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Extract type",
|
||||
"actionName": "Extract to interface"
|
||||
}
|
||||
}
|
58
cli/tests/lsp/code_action_resolve_response_refactor.json
Normal file
58
cli/tests/lsp/code_action_resolve_response_refactor.json
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"title": "Extract to interface",
|
||||
"kind": "refactor.extract.interface",
|
||||
"edit": {
|
||||
"documentChanges": [
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"version": 1
|
||||
},
|
||||
"edits": [
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 0
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 0
|
||||
}
|
||||
},
|
||||
"newText": "interface NewType {\n a?: number;\n b?: string;\n}\n\n"
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"newText": "NewType"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"isPreferred": true,
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Extract type",
|
||||
"actionName": "Extract to interface"
|
||||
}
|
||||
}
|
157
cli/tests/lsp/code_action_response_refactor.json
Normal file
157
cli/tests/lsp/code_action_response_refactor.json
Normal file
|
@ -0,0 +1,157 @@
|
|||
[
|
||||
{
|
||||
"title": "Extract to type alias",
|
||||
"kind": "refactor.extract.type",
|
||||
"isPreferred": true,
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Extract type",
|
||||
"actionName": "Extract to type alias"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Extract to interface",
|
||||
"kind": "refactor.extract.interface",
|
||||
"isPreferred": true,
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Extract type",
|
||||
"actionName": "Extract to interface"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Extract function",
|
||||
"kind": "refactor.extract.function",
|
||||
"isPreferred": false,
|
||||
"disabled": {
|
||||
"reason": "Statement or expression expected."
|
||||
},
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Extract Symbol",
|
||||
"actionName": "Extract Function"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Extract constant",
|
||||
"kind": "refactor.extract.constant",
|
||||
"isPreferred": false,
|
||||
"disabled": {
|
||||
"reason": "Statement or expression expected."
|
||||
},
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Extract Symbol",
|
||||
"actionName": "Extract Constant"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Convert default export to named export",
|
||||
"kind": "refactor.rewrite.export.named",
|
||||
"isPreferred": false,
|
||||
"disabled": {
|
||||
"reason": "Could not find export statement"
|
||||
},
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Convert export",
|
||||
"actionName": "Convert default export to named export"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Convert named export to default export",
|
||||
"kind": "refactor.rewrite.export.default",
|
||||
"isPreferred": false,
|
||||
"disabled": {
|
||||
"reason": "Could not find export statement"
|
||||
},
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Convert export",
|
||||
"actionName": "Convert named export to default export"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Convert namespace import to named imports",
|
||||
"kind": "refactor.rewrite.import.named",
|
||||
"isPreferred": false,
|
||||
"disabled": {
|
||||
"reason": "Selection is not an import declaration."
|
||||
},
|
||||
"data": {
|
||||
"specifier": "file:///a/file.ts",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 0,
|
||||
"character": 7
|
||||
},
|
||||
"end": {
|
||||
"line": 0,
|
||||
"character": 33
|
||||
}
|
||||
},
|
||||
"refactorName": "Convert import",
|
||||
"actionName": "Convert namespace import to named imports"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -584,6 +584,44 @@ delete Object.prototype.__proto__;
|
|||
);
|
||||
return respond(id, sourceFile && sourceFile.text);
|
||||
}
|
||||
case "getApplicableRefactors": {
|
||||
return respond(
|
||||
id,
|
||||
languageService.getApplicableRefactors(
|
||||
request.specifier,
|
||||
request.range,
|
||||
{
|
||||
quotePreference: "double",
|
||||
allowTextChangesInNewFiles: true,
|
||||
provideRefactorNotApplicableReason: true,
|
||||
},
|
||||
undefined,
|
||||
request.kind,
|
||||
),
|
||||
);
|
||||
}
|
||||
case "getEditsForRefactor": {
|
||||
return respond(
|
||||
id,
|
||||
languageService.getEditsForRefactor(
|
||||
request.specifier,
|
||||
{
|
||||
indentSize: 2,
|
||||
indentStyle: ts.IndentStyle.Smart,
|
||||
semicolons: ts.SemicolonPreference.Insert,
|
||||
convertTabsToSpaces: true,
|
||||
insertSpaceBeforeAndAfterBinaryOperators: true,
|
||||
insertSpaceAfterCommaDelimiter: true,
|
||||
},
|
||||
request.range,
|
||||
request.refactorName,
|
||||
request.actionName,
|
||||
{
|
||||
quotePreference: "double",
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
case "getCodeFixes": {
|
||||
return respond(
|
||||
id,
|
||||
|
|
17
cli/tsc/compiler.d.ts
vendored
17
cli/tsc/compiler.d.ts
vendored
|
@ -47,6 +47,8 @@ declare global {
|
|||
| ConfigureRequest
|
||||
| FindRenameLocationsRequest
|
||||
| GetAsset
|
||||
| GetApplicableRefactors
|
||||
| GetEditsForRefactor
|
||||
| GetCodeFixes
|
||||
| GetCombinedCodeFix
|
||||
| GetCompletionDetails
|
||||
|
@ -92,6 +94,21 @@ declare global {
|
|||
specifier: string;
|
||||
}
|
||||
|
||||
interface GetApplicableRefactors extends BaseLanguageServerRequest {
|
||||
method: "getApplicableRefactors";
|
||||
specifier: string;
|
||||
range: ts.TextRange;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
interface GetEditsForRefactor extends BaseLanguageServerRequest {
|
||||
method: "getEditsForRefactor";
|
||||
specifier: string;
|
||||
range: ts.TextRange;
|
||||
refactorName: string;
|
||||
actionName: string;
|
||||
}
|
||||
|
||||
interface GetCodeFixes extends BaseLanguageServerRequest {
|
||||
method: "getCodeFixes";
|
||||
specifier: string;
|
||||
|
|
Loading…
Reference in a new issue