summaryrefslogtreecommitdiff
path: root/cli/lsp/tsc.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/lsp/tsc.rs')
-rw-r--r--cli/lsp/tsc.rs213
1 files changed, 213 insertions, 0 deletions
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,