feat(lsp): update imports on file rename (#20245)

Closes https://github.com/denoland/vscode_deno/issues/410.
This commit is contained in:
Nayeem Rahman 2023-08-26 01:50:47 +01:00 committed by GitHub
parent a526cff0a9
commit 6f077ebb07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 221 additions and 31 deletions

View File

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

View File

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

View File

@ -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,
@ -4880,6 +4936,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![

View File

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

View File

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