diff options
-rw-r--r-- | cli/lsp/config.rs | 7 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 87 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 138 | ||||
-rw-r--r-- | cli/tsc/99_main_compiler.js | 11 | ||||
-rw-r--r-- | cli/tsc/compiler.d.ts | 9 |
5 files changed, 221 insertions, 31 deletions
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 13bfdd75d..292f07e47 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -34,6 +34,7 @@ pub struct ClientCapabilities { pub testing_api: bool, pub workspace_configuration: bool, pub workspace_did_change_watched_files: bool, + pub workspace_will_rename_files: bool, } fn is_true() -> bool { @@ -664,6 +665,12 @@ impl Config { .did_change_watched_files .and_then(|it| it.dynamic_registration) .unwrap_or(false); + if let Some(file_operations) = &workspace.file_operations { + if let Some(true) = file_operations.dynamic_registration { + self.client_capabilities.workspace_will_rename_files = + file_operations.will_rename.unwrap_or(false); + } + } } if let Some(text_document) = &capabilities.text_document { diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index c29817919..9137092e1 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -94,6 +94,7 @@ use crate::factory::CliFactory; use crate::file_fetcher::FileFetcher; use crate::graph_util; use crate::http_util::HttpClient; +use crate::lsp::tsc::file_text_changes_to_workspace_edit; use crate::lsp::urls::LspUrlKind; use crate::npm::create_npm_fs_resolver; use crate::npm::CliNpmRegistryApi; @@ -2060,13 +2061,7 @@ impl Inner { action_data.action_name, ) .await?; - 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.edit = refactor_edit_info.to_workspace_edit(self).await?; code_action } else { // The code action doesn't need to be resolved @@ -2934,6 +2929,37 @@ impl Inner { } } + async fn will_rename_files( + &self, + params: RenameFilesParams, + ) -> LspResult<Option<WorkspaceEdit>> { + let mut changes = vec![]; + for rename in params.files { + changes.extend( + self + .ts_server + .get_edits_for_file_rename( + self.snapshot(), + self.url_map.normalize_url( + &resolve_url(&rename.old_uri).unwrap(), + LspUrlKind::File, + ), + self.url_map.normalize_url( + &resolve_url(&rename.new_uri).unwrap(), + LspUrlKind::File, + ), + (&self.fmt_options.options).into(), + tsc::UserPreferences { + allow_text_changes_in_new_files: Some(true), + ..Default::default() + }, + ) + .await?, + ); + } + file_text_changes_to_workspace_edit(&changes, self) + } + async fn symbol( &self, params: WorkspaceSymbolParams, @@ -3004,7 +3030,7 @@ impl tower_lsp::LanguageServer for LanguageServer { } async fn initialized(&self, _: InitializedParams) { - let mut maybe_registration = None; + let mut registrations = Vec::with_capacity(2); let client = { let mut ls = self.0.write().await; if ls @@ -3015,19 +3041,33 @@ impl tower_lsp::LanguageServer for LanguageServer { // we are going to watch all the JSON files in the workspace, and the // notification handler will pick up any of the changes of those files we // are interested in. - let watch_registration_options = - DidChangeWatchedFilesRegistrationOptions { - watchers: vec![FileSystemWatcher { - glob_pattern: "**/*.{json,jsonc,lock}".to_string(), - kind: Some(WatchKind::Change), - }], - }; - maybe_registration = Some(Registration { + let options = DidChangeWatchedFilesRegistrationOptions { + watchers: vec![FileSystemWatcher { + glob_pattern: "**/*.{json,jsonc,lock}".to_string(), + kind: Some(WatchKind::Change), + }], + }; + registrations.push(Registration { id: "workspace/didChangeWatchedFiles".to_string(), method: "workspace/didChangeWatchedFiles".to_string(), - register_options: Some( - serde_json::to_value(watch_registration_options).unwrap(), - ), + register_options: Some(serde_json::to_value(options).unwrap()), + }); + } + if ls.config.client_capabilities.workspace_will_rename_files { + let options = FileOperationRegistrationOptions { + filters: vec![FileOperationFilter { + scheme: Some("file".to_string()), + pattern: FileOperationPattern { + glob: "**/*".to_string(), + matches: None, + options: None, + }, + }], + }; + registrations.push(Registration { + id: "workspace/willRenameFiles".to_string(), + method: "workspace/willRenameFiles".to_string(), + register_options: Some(serde_json::to_value(options).unwrap()), }); } @@ -3042,7 +3082,7 @@ impl tower_lsp::LanguageServer for LanguageServer { ls.client.clone() }; - if let Some(registration) = maybe_registration { + for registration in registrations { if let Err(err) = client .when_outside_lsp_lock() .register_capability(vec![registration]) @@ -3376,6 +3416,13 @@ impl tower_lsp::LanguageServer for LanguageServer { self.0.read().await.signature_help(params).await } + async fn will_rename_files( + &self, + params: RenameFilesParams, + ) -> LspResult<Option<WorkspaceEdit>> { + self.0.read().await.will_rename_files(params).await + } + async fn symbol( &self, params: WorkspaceSymbolParams, diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 99dd92193..a051e8c2a 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -51,6 +51,7 @@ use deno_core::OpState; use deno_core::RuntimeOptions; use deno_runtime::tokio_util::create_basic_runtime; use lazy_regex::lazy_regex; +use log::error; use once_cell::sync::Lazy; use regex::Captures; use regex::Regex; @@ -317,6 +318,26 @@ impl TsServer { }) } + pub async fn get_edits_for_file_rename( + &self, + snapshot: Arc<StateSnapshot>, + old_specifier: ModuleSpecifier, + new_specifier: ModuleSpecifier, + format_code_settings: FormatCodeSettings, + user_preferences: UserPreferences, + ) -> Result<Vec<FileTextChanges>, LspError> { + let req = RequestMethod::GetEditsForFileRename(( + old_specifier, + new_specifier, + format_code_settings, + user_preferences, + )); + self.request(snapshot, req).await.map_err(|err| { + log::error!("Failed to request to tsserver {}", err); + LspError::invalid_request() + }) + } + pub async fn get_document_highlights( &self, snapshot: Arc<StateSnapshot>, @@ -2067,6 +2088,28 @@ impl ApplicableRefactorInfo { } } +pub fn file_text_changes_to_workspace_edit( + changes: &[FileTextChanges], + language_server: &language_server::Inner, +) -> LspResult<Option<lsp::WorkspaceEdit>> { + let mut all_ops = Vec::<lsp::DocumentChangeOperation>::new(); + for change in changes { + let ops = match change.to_text_document_change_ops(language_server) { + Ok(op) => op, + Err(err) => { + error!("Unable to convert changes to edits: {}", err); + return Err(LspError::internal_error()); + } + }; + 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 RefactorEditInfo { @@ -2079,17 +2122,8 @@ impl RefactorEditInfo { pub async fn to_workspace_edit( &self, language_server: &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)?; - all_ops.extend(ops); - } - - Ok(Some(lsp::WorkspaceEdit { - document_changes: Some(lsp::DocumentChanges::Operations(all_ops)), - ..Default::default() - })) + ) -> LspResult<Option<lsp::WorkspaceEdit>> { + file_text_changes_to_workspace_edit(&self.edits, language_server) } } @@ -3644,6 +3678,15 @@ enum RequestMethod { String, ), ), + /// Retrieve the refactor edit info for a range. + GetEditsForFileRename( + ( + ModuleSpecifier, + ModuleSpecifier, + FormatCodeSettings, + UserPreferences, + ), + ), /// Retrieve code fixes for a range of a file with the provided error codes. GetCodeFixes((ModuleSpecifier, u32, u32, Vec<String>, FormatCodeSettings)), /// Get completion information at a given position (IntelliSense). @@ -3757,6 +3800,19 @@ impl RequestMethod { "refactorName": refactor_name, "actionName": action_name, }), + RequestMethod::GetEditsForFileRename(( + old_specifier, + new_specifier, + format_code_settings, + preferences, + )) => json!({ + "id": id, + "method": "getEditsForFileRename", + "oldSpecifier": state.denormalize_specifier(old_specifier), + "newSpecifier": state.denormalize_specifier(new_specifier), + "formatCodeSettings": format_code_settings, + "preferences": preferences, + }), RequestMethod::GetCodeFixes(( specifier, start_pos, @@ -4881,6 +4937,66 @@ mod tests { } #[test] + fn test_get_edits_for_file_rename() { + let temp_dir = TempDir::new(); + let (mut runtime, state_snapshot, _) = setup( + &temp_dir, + false, + json!({ + "target": "esnext", + "module": "esnext", + "lib": ["deno.ns", "deno.window"], + "noEmit": true, + }), + &[ + ( + "file:///a.ts", + r#"import "./b.ts";"#, + 1, + LanguageId::TypeScript, + ), + ("file:///b.ts", r#""#, 1, LanguageId::TypeScript), + ], + ); + let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); + let result = request( + &mut runtime, + state_snapshot.clone(), + RequestMethod::GetDiagnostics(vec![specifier.clone()]), + Default::default(), + ); + assert!(result.is_ok()); + let changes = request( + &mut runtime, + state_snapshot.clone(), + RequestMethod::GetEditsForFileRename(( + resolve_url("file:///b.ts").unwrap(), + resolve_url("file:///c.ts").unwrap(), + Default::default(), + Default::default(), + )), + Default::default(), + ) + .unwrap(); + let changes: Vec<FileTextChanges> = + serde_json::from_value(changes).unwrap(); + assert_eq!( + changes, + vec![FileTextChanges { + file_name: "file:///a.ts".to_string(), + text_changes: vec![TextChange { + span: TextSpan { + start: 8, + length: 6, + }, + new_text: "./c.ts".to_string(), + }], + is_new_file: None, + }] + ); + } + + #[test] fn test_update_import_statement() { let fixtures = vec![ ( diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index f2ccec466..451e36b96 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -1049,6 +1049,17 @@ delete Object.prototype.__proto__; ), ); } + case "getEditsForFileRename": { + return respond( + id, + languageService.getEditsForFileRename( + request.oldSpecifier, + request.newSpecifier, + request.formatCodeSettings, + request.preferences, + ), + ); + } case "getCodeFixes": { return respond( id, diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index da713a1bd..7b8340093 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -64,6 +64,7 @@ declare global { | GetAssets | GetApplicableRefactors | GetEditsForRefactor + | GetEditsForFileRename | GetCodeFixes | GetCombinedCodeFix | GetCompletionDetails @@ -127,6 +128,14 @@ declare global { actionName: string; } + interface GetEditsForFileRename extends BaseLanguageServerRequest { + method: "getEditsForFileRename"; + old_specifier: string; + new_specifier: string; + formatCodeSettings: ts.FormatCodeSettings; + preferences?: ts.UserPreferences; + } + interface GetCodeFixes extends BaseLanguageServerRequest { method: "getCodeFixes"; specifier: string; |