1
0
Fork 0
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:
Jean Pierre 2021-08-05 20:46:32 -05:00 committed by GitHub
parent 3f0cf9619f
commit 728d205d9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 882 additions and 93 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

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

View file

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

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