summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/lsp/capabilities.rs11
-rw-r--r--cli/lsp/language_server.rs269
-rw-r--r--cli/lsp/mod.rs1
-rw-r--r--cli/lsp/refactor.rs131
-rw-r--r--cli/lsp/tsc.rs213
-rw-r--r--cli/tests/integration/lsp_tests.rs39
-rw-r--r--cli/tests/lsp/code_action_params_refactor.json21
-rw-r--r--cli/tests/lsp/code_action_resolve_params_refactor.json20
-rw-r--r--cli/tests/lsp/code_action_resolve_response_refactor.json58
-rw-r--r--cli/tests/lsp/code_action_response_refactor.json157
-rw-r--r--cli/tsc/99_main_compiler.js38
-rw-r--r--cli/tsc/compiler.d.ts17
12 files changed, 882 insertions, 93 deletions
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs
index 89400d108..a664c296d 100644
--- a/cli/lsp/capabilities.rs
+++ b/cli/lsp/capabilities.rs
@@ -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(),
})
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index 68ce9c58c..8d672e251 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -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(&params));
+ 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());
+ }
+
+ // 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, &params.range));
}
- code_actions.set_preferred_fixes();
- let code_action_response = code_actions.get_response();
+ 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(&params));
- 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(
diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs
index c05241ae1..0404d64e0 100644
--- a/cli/lsp/mod.rs
+++ b/cli/lsp/mod.rs
@@ -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;
diff --git a/cli/lsp/refactor.rs b/cli/lsp/refactor.rs
new file mode 100644
index 000000000..17ab4d5de
--- /dev/null
+++ b/cli/lsp/refactor.rs
@@ -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
+}
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index c3f82fea8..c5c3c08a9 100644
--- a/cli/lsp/tsc.rs
+++ b/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)]
@@ -1058,6 +1104,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 {
description: String,
changes: Vec<FileTextChanges>,
@@ -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,
diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs
index 8bf019aa7..e66b59c2b 100644
--- a/cli/tests/integration/lsp_tests.rs
+++ b/cli/tests/integration/lsp_tests.rs
@@ -2116,6 +2116,45 @@ fn lsp_code_actions_imports() {
}
#[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");
client
diff --git a/cli/tests/lsp/code_action_params_refactor.json b/cli/tests/lsp/code_action_params_refactor.json
new file mode 100644
index 000000000..9fe359498
--- /dev/null
+++ b/cli/tests/lsp/code_action_params_refactor.json
@@ -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"
+ ]
+ }
+}
diff --git a/cli/tests/lsp/code_action_resolve_params_refactor.json b/cli/tests/lsp/code_action_resolve_params_refactor.json
new file mode 100644
index 000000000..d4bb3bd81
--- /dev/null
+++ b/cli/tests/lsp/code_action_resolve_params_refactor.json
@@ -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"
+ }
+}
diff --git a/cli/tests/lsp/code_action_resolve_response_refactor.json b/cli/tests/lsp/code_action_resolve_response_refactor.json
new file mode 100644
index 000000000..721a76a6b
--- /dev/null
+++ b/cli/tests/lsp/code_action_resolve_response_refactor.json
@@ -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"
+ }
+}
diff --git a/cli/tests/lsp/code_action_response_refactor.json b/cli/tests/lsp/code_action_response_refactor.json
new file mode 100644
index 000000000..87f354e37
--- /dev/null
+++ b/cli/tests/lsp/code_action_response_refactor.json
@@ -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"
+ }
+ }
+]
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js
index f5cfe38dd..29a387887 100644
--- a/cli/tsc/99_main_compiler.js
+++ b/cli/tsc/99_main_compiler.js
@@ -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,
diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts
index 949d98ee0..ff2e59e8e 100644
--- a/cli/tsc/compiler.d.ts
+++ b/cli/tsc/compiler.d.ts
@@ -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;