feat: issue import changes request during willRenameFiles (#648)

* feat: issue import changes request during `willRenameFiles`

* test: update snapshot

* fix: snapshot
This commit is contained in:
Myriad-Dreamin 2024-10-09 14:53:19 +08:00 committed by GitHub
parent c9846b1d0d
commit 7b0fb6036d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 193 additions and 63 deletions

View file

@ -5,11 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path.typ
--- ---
{ {
"documentChanges": [ "documentChanges": [
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
},
{ {
"edits": [ "edits": [
{ {
@ -21,6 +16,11 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path.typ
"uri": "s1.typ", "uri": "s1.typ",
"version": null "version": null
} }
},
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
} }
] ]
} }

View file

@ -5,11 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_alias.typ
--- ---
{ {
"documentChanges": [ "documentChanges": [
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
},
{ {
"edits": [ "edits": [
{ {
@ -21,6 +16,11 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_alias.typ
"uri": "s1.typ", "uri": "s1.typ",
"version": null "version": null
} }
},
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
} }
] ]
} }

View file

@ -5,11 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_non_cano.typ
--- ---
{ {
"documentChanges": [ "documentChanges": [
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
},
{ {
"edits": [ "edits": [
{ {
@ -21,6 +16,11 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_non_cano.typ
"uri": "s1.typ", "uri": "s1.typ",
"version": null "version": null
} }
},
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
} }
] ]
} }

View file

@ -5,11 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_star.typ
--- ---
{ {
"documentChanges": [ "documentChanges": [
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
},
{ {
"edits": [ "edits": [
{ {
@ -21,6 +16,11 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_star.typ
"uri": "s1.typ", "uri": "s1.typ",
"version": null "version": null
} }
},
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
} }
] ]
} }

View file

@ -51,6 +51,8 @@ mod inlay_hint;
pub use inlay_hint::*; pub use inlay_hint::*;
mod jump; mod jump;
pub use jump::*; pub use jump::*;
mod will_rename_files;
pub use will_rename_files::*;
mod rename; mod rename;
pub use rename::*; pub use rename::*;
mod selection_range; mod selection_range;
@ -251,6 +253,7 @@ mod polymorphic {
Completion(CompletionRequest), Completion(CompletionRequest),
SignatureHelp(SignatureHelpRequest), SignatureHelp(SignatureHelpRequest),
Rename(RenameRequest), Rename(RenameRequest),
WillRenameFiles(WillRenameFilesRequest),
PrepareRename(PrepareRenameRequest), PrepareRename(PrepareRenameRequest),
DocumentSymbol(DocumentSymbolRequest), DocumentSymbol(DocumentSymbolRequest),
Symbol(SymbolRequest), Symbol(SymbolRequest),
@ -286,6 +289,7 @@ mod polymorphic {
Self::Completion(..) => Mergeable, Self::Completion(..) => Mergeable,
Self::SignatureHelp(..) => PinnedFirst, Self::SignatureHelp(..) => PinnedFirst,
Self::Rename(..) => Mergeable, Self::Rename(..) => Mergeable,
Self::WillRenameFiles(..) => Mergeable,
Self::PrepareRename(..) => Mergeable, Self::PrepareRename(..) => Mergeable,
Self::DocumentSymbol(..) => ContextFreeUnique, Self::DocumentSymbol(..) => ContextFreeUnique,
Self::WorkspaceLabel(..) => Mergeable, Self::WorkspaceLabel(..) => Mergeable,
@ -320,6 +324,7 @@ mod polymorphic {
Self::Completion(req) => &req.path, Self::Completion(req) => &req.path,
Self::SignatureHelp(req) => &req.path, Self::SignatureHelp(req) => &req.path,
Self::Rename(req) => &req.path, Self::Rename(req) => &req.path,
Self::WillRenameFiles(..) => return None,
Self::PrepareRename(req) => &req.path, Self::PrepareRename(req) => &req.path,
Self::DocumentSymbol(req) => &req.path, Self::DocumentSymbol(req) => &req.path,
Self::Symbol(..) => return None, Self::Symbol(..) => return None,
@ -356,6 +361,7 @@ mod polymorphic {
SignatureHelp(Option<SignatureHelp>), SignatureHelp(Option<SignatureHelp>),
PrepareRename(Option<PrepareRenameResponse>), PrepareRename(Option<PrepareRenameResponse>),
Rename(Option<WorkspaceEdit>), Rename(Option<WorkspaceEdit>),
WillRenameFiles(Option<WorkspaceEdit>),
DocumentSymbol(Option<DocumentSymbolResponse>), DocumentSymbol(Option<DocumentSymbolResponse>),
Symbol(Option<Vec<SymbolInformation>>), Symbol(Option<Vec<SymbolInformation>>),
WorkspaceLabel(Option<Vec<SymbolInformation>>), WorkspaceLabel(Option<Vec<SymbolInformation>>),
@ -390,6 +396,7 @@ mod polymorphic {
Self::SignatureHelp(res) => serde_json::to_value(res), Self::SignatureHelp(res) => serde_json::to_value(res),
Self::PrepareRename(res) => serde_json::to_value(res), Self::PrepareRename(res) => serde_json::to_value(res),
Self::Rename(res) => serde_json::to_value(res), Self::Rename(res) => serde_json::to_value(res),
Self::WillRenameFiles(res) => serde_json::to_value(res),
Self::DocumentSymbol(res) => serde_json::to_value(res), Self::DocumentSymbol(res) => serde_json::to_value(res),
Self::Symbol(res) => serde_json::to_value(res), Self::Symbol(res) => serde_json::to_value(res),
Self::WorkspaceLabel(res) => serde_json::to_value(res), Self::WorkspaceLabel(res) => serde_json::to_value(res),

View file

@ -1,7 +1,8 @@
use std::ops::Range; use std::ops::Range;
use lsp_types::{ use lsp_types::{
DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier, RenameFile, TextDocumentEdit, DocumentChangeOperation, DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier,
RenameFile, TextDocumentEdit,
}; };
use reflexo::path::{unix_slash, PathClean}; use reflexo::path::{unix_slash, PathClean};
use typst::foundations::{Repr, Str}; use typst::foundations::{Repr, Str};
@ -56,9 +57,6 @@ impl StatefulRequest for RenameRequest {
self.new_name self.new_name
}; };
let mut editions: HashMap<Url, Vec<TextEdit>> = HashMap::new();
let mut document_changes = vec![];
let def_fid = lnk.def_at?.0; let def_fid = lnk.def_at?.0;
let old_path = ctx.path_for_id(def_fid).ok()?; let old_path = ctx.path_for_id(def_fid).ok()?;
@ -74,6 +72,11 @@ impl StatefulRequest for RenameRequest {
let old_uri = path_to_url(&old_path).ok()?; let old_uri = path_to_url(&old_path).ok()?;
let new_uri = path_to_url(&new_path).ok()?; let new_uri = path_to_url(&new_path).ok()?;
let mut edits: HashMap<Url, Vec<TextEdit>> = HashMap::new();
do_rename_file(ctx, def_fid, diff, &mut edits)?;
let mut document_changes = edits_to_document_changes(edits);
document_changes.push(lsp_types::DocumentChangeOperation::Op( document_changes.push(lsp_types::DocumentChangeOperation::Op(
lsp_types::ResourceOp::Rename(RenameFile { lsp_types::ResourceOp::Rename(RenameFile {
old_uri, old_uri,
@ -83,40 +86,7 @@ impl StatefulRequest for RenameRequest {
}), }),
)); ));
let dep = ctx.module_dependencies().get(&def_fid)?.clone();
for ref_fid in dep.dependents.iter() {
let ref_src = ctx.source_by_id(*ref_fid).ok()?;
let uri = ctx.uri_for_id(*ref_fid).ok()?;
let Some(import_info) = ctx.import_info(ref_src.clone()) else {
continue;
};
let edits = editions.entry(uri).or_default();
for (rng, importing_src) in &import_info.imports {
let importing = importing_src.as_ref().map(|s| s.id());
if importing.map_or(true, |i| i != def_fid) {
continue;
}
log::debug!("import: {rng:?} -> {importing:?} v.s. {def_fid:?}");
rename_importer(ctx, &ref_src, rng.clone(), &diff, edits);
}
}
// todo: validate: workspace.workspaceEdit.resourceOperations // todo: validate: workspace.workspaceEdit.resourceOperations
for edition in editions.into_iter() {
document_changes.push(lsp_types::DocumentChangeOperation::Edit(
TextDocumentEdit {
text_document: OptionalVersionedTextDocumentIdentifier {
uri: edition.0,
version: None,
},
edits: edition.1.into_iter().map(OneOf::Left).collect(),
},
));
}
Some(WorkspaceEdit { Some(WorkspaceEdit {
document_changes: Some(DocumentChanges::Operations(document_changes)), document_changes: Some(DocumentChanges::Operations(document_changes)),
..Default::default() ..Default::default()
@ -125,7 +95,7 @@ impl StatefulRequest for RenameRequest {
_ => { _ => {
let references = find_references(ctx, source.clone(), doc.as_ref(), deref_target)?; let references = find_references(ctx, source.clone(), doc.as_ref(), deref_target)?;
let mut editions = HashMap::new(); let mut edits = HashMap::new();
let (def_fid, _def_range) = lnk.def_at?; let (def_fid, _def_range) = lnk.def_at?;
let def_loc = { let def_loc = {
@ -147,17 +117,17 @@ impl StatefulRequest for RenameRequest {
for i in (Some(def_loc).into_iter()).chain(references) { for i in (Some(def_loc).into_iter()).chain(references) {
let uri = i.uri; let uri = i.uri;
let range = i.range; let range = i.range;
let edits = editions.entry(uri).or_insert_with(Vec::new); let edits = edits.entry(uri).or_insert_with(Vec::new);
edits.push(TextEdit { edits.push(TextEdit {
range, range,
new_text: self.new_name.clone(), new_text: self.new_name.clone(),
}); });
} }
log::info!("rename editions: {editions:?}"); log::info!("rename edits: {edits:?}");
Some(WorkspaceEdit { Some(WorkspaceEdit {
changes: Some(editions), changes: Some(edits),
..Default::default() ..Default::default()
}) })
} }
@ -165,6 +135,51 @@ impl StatefulRequest for RenameRequest {
} }
} }
pub(crate) fn do_rename_file(
ctx: &mut AnalysisContext,
def_fid: TypstFileId,
diff: PathBuf,
edits: &mut HashMap<Url, Vec<TextEdit>>,
) -> Option<()> {
let dep = ctx.module_dependencies().get(&def_fid)?.clone();
for ref_fid in dep.dependents.iter() {
let ref_src = ctx.source_by_id(*ref_fid).ok()?;
let uri = ctx.uri_for_id(*ref_fid).ok()?;
let Some(import_info) = ctx.import_info(ref_src.clone()) else {
continue;
};
let edits = edits.entry(uri).or_default();
for (rng, importing_src) in &import_info.imports {
let importing = importing_src.as_ref().map(|s| s.id());
if importing.map_or(true, |i| i != def_fid) {
continue;
}
log::debug!("import: {rng:?} -> {importing:?} v.s. {def_fid:?}");
rename_importer(ctx, &ref_src, rng.clone(), &diff, edits);
}
}
Some(())
}
pub(crate) fn edits_to_document_changes(
edits: HashMap<Url, Vec<TextEdit>>,
) -> Vec<DocumentChangeOperation> {
let mut document_changes = vec![];
for (uri, edits) in edits {
document_changes.push(lsp_types::DocumentChangeOperation::Edit(TextDocumentEdit {
text_document: OptionalVersionedTextDocumentIdentifier { uri, version: None },
edits: edits.into_iter().map(OneOf::Left).collect(),
}));
}
document_changes
}
fn rename_importer( fn rename_importer(
ctx: &AnalysisContext, ctx: &AnalysisContext,
src: &Source, src: &Source,

View file

@ -0,0 +1,65 @@
use lsp_types::ChangeAnnotation;
use crate::{do_rename_file, edits_to_document_changes, prelude::*};
/// Handle [`workspace/willRenameFiles`] request is sent from the client to the
/// server.
///
/// [`workspace/willRenameFiles`]: https://microsoft.github.io/language-server-protocol/specification#workspace_willRenameFiles
#[derive(Debug, Clone)]
pub struct WillRenameFilesRequest {
/// rename paths from `left` to `right`
pub paths: Vec<(PathBuf, PathBuf)>,
}
impl StatefulRequest for WillRenameFilesRequest {
type Response = WorkspaceEdit;
fn request(
self,
ctx: &mut AnalysisContext,
_doc: Option<VersionedDocument>,
) -> Option<Self::Response> {
let mut edits: HashMap<Url, Vec<TextEdit>> = HashMap::new();
self.paths
.into_iter()
.map(|(left, right)| {
let diff = pathdiff::diff_paths(&right, &left)?;
log::info!("did rename diff: {diff:?}");
if diff.is_absolute() {
log::info!(
"bad rename: absolute path, base: {left:?}, new: {right:?}, diff: {diff:?}"
);
return None;
}
let def_fid = ctx.file_id_by_path(&left).ok()?;
log::info!("did rename def_fid: {def_fid:?}");
do_rename_file(ctx, def_fid, diff, &mut edits)
})
.collect::<Option<Vec<()>>>()?;
log::info!("did rename edits: {edits:?}");
let document_changes = edits_to_document_changes(edits);
if document_changes.is_empty() {
return None;
}
let mut change_annotations = HashMap::new();
change_annotations.insert(
"Typst Rename Files".to_string(),
ChangeAnnotation {
label: "Typst Rename Files".to_string(),
needs_confirmation: Some(true),
description: Some("Rename files should update imports".to_string()),
},
);
Some(WorkspaceEdit {
changes: None,
document_changes: Some(lsp_types::DocumentChanges::Operations(document_changes)),
change_annotations: Some(change_annotations),
})
}
}

View file

@ -164,6 +164,22 @@ impl Initializer for SuperInit {
_ => None, _ => None,
}; };
let file_operations = const_config.notify_will_rename_files.then(|| {
WorkspaceFileOperationsServerCapabilities {
will_rename: Some(FileOperationRegistrationOptions {
filters: vec![FileOperationFilter {
scheme: Some("file".to_string()),
pattern: FileOperationPattern {
glob: "**/*.typ".to_string(),
matches: Some(FileOperationPatternKind::File),
options: None,
},
}],
}),
..Default::default()
}
});
let res = InitializeResult { let res = InitializeResult {
capabilities: ServerCapabilities { capabilities: ServerCapabilities {
// todo: respect position_encoding // todo: respect position_encoding
@ -226,7 +242,7 @@ impl Initializer for SuperInit {
supported: Some(true), supported: Some(true),
change_notifications: Some(OneOf::Left(true)), change_notifications: Some(OneOf::Left(true)),
}), }),
..Default::default() file_operations,
}), }),
document_formatting_provider, document_formatting_provider,
inlay_hint_provider: Some(OneOf::Left(true)), inlay_hint_provider: Some(OneOf::Left(true)),
@ -357,6 +373,8 @@ pub struct ConstConfig {
pub position_encoding: PositionEncoding, pub position_encoding: PositionEncoding,
/// Allow dynamic registration of configuration changes. /// Allow dynamic registration of configuration changes.
pub cfg_change_registration: bool, pub cfg_change_registration: bool,
/// Allow notifying workspace/didRenameFiles
pub notify_will_rename_files: bool,
/// Allow dynamic registration of semantic tokens. /// Allow dynamic registration of semantic tokens.
pub tokens_dynamic_registration: bool, pub tokens_dynamic_registration: bool,
/// Allow overlapping tokens. /// Allow overlapping tokens.
@ -392,6 +410,7 @@ impl From<&InitializeParams> for ConstConfig {
}; };
let workspace = params.capabilities.workspace.as_ref(); let workspace = params.capabilities.workspace.as_ref();
let file_operations = try_(|| workspace?.file_operations.as_ref());
let doc = params.capabilities.text_document.as_ref(); let doc = params.capabilities.text_document.as_ref();
let sema = try_(|| doc?.semantic_tokens.as_ref()); let sema = try_(|| doc?.semantic_tokens.as_ref());
let fold = try_(|| doc?.folding_range.as_ref()); let fold = try_(|| doc?.folding_range.as_ref());
@ -400,6 +419,7 @@ impl From<&InitializeParams> for ConstConfig {
Self { Self {
position_encoding, position_encoding,
cfg_change_registration: try_or(|| workspace?.configuration, false), cfg_change_registration: try_or(|| workspace?.configuration, false),
notify_will_rename_files: try_or(|| file_operations?.will_rename, false),
tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false), tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false),
tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false), tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false),
tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false), tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false),

View file

@ -233,6 +233,7 @@ impl LanguageState {
.with_request_::<References>(State::references) .with_request_::<References>(State::references)
.with_request_::<WorkspaceSymbolRequest>(State::symbol) .with_request_::<WorkspaceSymbolRequest>(State::symbol)
.with_request_::<OnEnter>(State::on_enter) .with_request_::<OnEnter>(State::on_enter)
.with_request_::<WillRenameFiles>(State::will_rename_files)
// notifications // notifications
.with_notification::<Initialized>(State::initialized) .with_notification::<Initialized>(State::initialized)
.with_notification::<DidOpenTextDocument>(State::did_open) .with_notification::<DidOpenTextDocument>(State::did_open)
@ -790,6 +791,27 @@ impl LanguageState {
let (path, position) = as_path_pos(params); let (path, position) = as_path_pos(params);
run_query!(req_id, self.OnEnter(path, position)) run_query!(req_id, self.OnEnter(path, position))
} }
fn will_rename_files(
&mut self,
req_id: RequestId,
params: RenameFilesParams,
) -> ScheduledResult {
log::info!("will rename files {params:?}");
let paths = params
.files
.iter()
.map(|f| {
Some((
as_path_(Url::parse(&f.old_uri).ok()?),
as_path_(Url::parse(&f.new_uri).ok()?),
))
})
.collect::<Option<Vec<_>>>()
.ok_or_else(|| invalid_params("invalid urls"))?;
run_query!(req_id, self.WillRenameFiles(paths))
}
} }
impl LanguageState { impl LanguageState {
@ -1047,6 +1069,7 @@ impl LanguageState {
Completion(req) => handle.run_stateful(snap, req, R::Completion), Completion(req) => handle.run_stateful(snap, req, R::Completion),
SignatureHelp(req) => handle.run_semantic(snap, req, R::SignatureHelp), SignatureHelp(req) => handle.run_semantic(snap, req, R::SignatureHelp),
Rename(req) => handle.run_stateful(snap, req, R::Rename), Rename(req) => handle.run_stateful(snap, req, R::Rename),
WillRenameFiles(req) => handle.run_stateful(snap, req, R::WillRenameFiles),
PrepareRename(req) => handle.run_stateful(snap, req, R::PrepareRename), PrepareRename(req) => handle.run_stateful(snap, req, R::PrepareRename),
Symbol(req) => handle.run_semantic(snap, req, R::Symbol), Symbol(req) => handle.run_semantic(snap, req, R::Symbol),
WorkspaceLabel(req) => handle.run_semantic(snap, req, R::WorkspaceLabel), WorkspaceLabel(req) => handle.run_semantic(snap, req, R::WorkspaceLabel),

View file

@ -385,7 +385,7 @@ fn e2e() {
}); });
let hash = replay_log(&tinymist_binary, &root.join("vscode")); let hash = replay_log(&tinymist_binary, &root.join("vscode"));
insta::assert_snapshot!(hash, @"siphash128_13:427c0fe870e3acd4e53a9e101bc6dc29"); insta::assert_snapshot!(hash, @"siphash128_13:5f3b961e94db34a9d7e9f6a405617c0d");
} }
} }