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": [
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
},
{
"edits": [
{
@ -21,6 +16,11 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path.typ
"uri": "s1.typ",
"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": [
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
},
{
"edits": [
{
@ -21,6 +16,11 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_alias.typ
"uri": "s1.typ",
"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": [
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
},
{
"edits": [
{
@ -21,6 +16,11 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_non_cano.typ
"uri": "s1.typ",
"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": [
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
},
{
"edits": [
{
@ -21,6 +16,11 @@ input_file: crates/tinymist-query/src/fixtures/rename/module_path_star.typ
"uri": "s1.typ",
"version": null
}
},
{
"kind": "rename",
"newUri": "new_name.typ",
"oldUri": "variable.typ"
}
]
}

View file

@ -51,6 +51,8 @@ mod inlay_hint;
pub use inlay_hint::*;
mod jump;
pub use jump::*;
mod will_rename_files;
pub use will_rename_files::*;
mod rename;
pub use rename::*;
mod selection_range;
@ -251,6 +253,7 @@ mod polymorphic {
Completion(CompletionRequest),
SignatureHelp(SignatureHelpRequest),
Rename(RenameRequest),
WillRenameFiles(WillRenameFilesRequest),
PrepareRename(PrepareRenameRequest),
DocumentSymbol(DocumentSymbolRequest),
Symbol(SymbolRequest),
@ -286,6 +289,7 @@ mod polymorphic {
Self::Completion(..) => Mergeable,
Self::SignatureHelp(..) => PinnedFirst,
Self::Rename(..) => Mergeable,
Self::WillRenameFiles(..) => Mergeable,
Self::PrepareRename(..) => Mergeable,
Self::DocumentSymbol(..) => ContextFreeUnique,
Self::WorkspaceLabel(..) => Mergeable,
@ -320,6 +324,7 @@ mod polymorphic {
Self::Completion(req) => &req.path,
Self::SignatureHelp(req) => &req.path,
Self::Rename(req) => &req.path,
Self::WillRenameFiles(..) => return None,
Self::PrepareRename(req) => &req.path,
Self::DocumentSymbol(req) => &req.path,
Self::Symbol(..) => return None,
@ -356,6 +361,7 @@ mod polymorphic {
SignatureHelp(Option<SignatureHelp>),
PrepareRename(Option<PrepareRenameResponse>),
Rename(Option<WorkspaceEdit>),
WillRenameFiles(Option<WorkspaceEdit>),
DocumentSymbol(Option<DocumentSymbolResponse>),
Symbol(Option<Vec<SymbolInformation>>),
WorkspaceLabel(Option<Vec<SymbolInformation>>),
@ -390,6 +396,7 @@ mod polymorphic {
Self::SignatureHelp(res) => serde_json::to_value(res),
Self::PrepareRename(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::Symbol(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 lsp_types::{
DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier, RenameFile, TextDocumentEdit,
DocumentChangeOperation, DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier,
RenameFile, TextDocumentEdit,
};
use reflexo::path::{unix_slash, PathClean};
use typst::foundations::{Repr, Str};
@ -56,9 +57,6 @@ impl StatefulRequest for RenameRequest {
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 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 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(
lsp_types::ResourceOp::Rename(RenameFile {
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
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 {
document_changes: Some(DocumentChanges::Operations(document_changes)),
..Default::default()
@ -125,7 +95,7 @@ impl StatefulRequest for RenameRequest {
_ => {
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_loc = {
@ -147,17 +117,17 @@ impl StatefulRequest for RenameRequest {
for i in (Some(def_loc).into_iter()).chain(references) {
let uri = i.uri;
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 {
range,
new_text: self.new_name.clone(),
});
}
log::info!("rename editions: {editions:?}");
log::info!("rename edits: {edits:?}");
Some(WorkspaceEdit {
changes: Some(editions),
changes: Some(edits),
..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(
ctx: &AnalysisContext,
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,
};
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 {
capabilities: ServerCapabilities {
// todo: respect position_encoding
@ -226,7 +242,7 @@ impl Initializer for SuperInit {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
..Default::default()
file_operations,
}),
document_formatting_provider,
inlay_hint_provider: Some(OneOf::Left(true)),
@ -357,6 +373,8 @@ pub struct ConstConfig {
pub position_encoding: PositionEncoding,
/// Allow dynamic registration of configuration changes.
pub cfg_change_registration: bool,
/// Allow notifying workspace/didRenameFiles
pub notify_will_rename_files: bool,
/// Allow dynamic registration of semantic tokens.
pub tokens_dynamic_registration: bool,
/// Allow overlapping tokens.
@ -392,6 +410,7 @@ impl From<&InitializeParams> for ConstConfig {
};
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 sema = try_(|| doc?.semantic_tokens.as_ref());
let fold = try_(|| doc?.folding_range.as_ref());
@ -400,6 +419,7 @@ impl From<&InitializeParams> for ConstConfig {
Self {
position_encoding,
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_overlapping_token_support: try_or(|| sema?.overlapping_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_::<WorkspaceSymbolRequest>(State::symbol)
.with_request_::<OnEnter>(State::on_enter)
.with_request_::<WillRenameFiles>(State::will_rename_files)
// notifications
.with_notification::<Initialized>(State::initialized)
.with_notification::<DidOpenTextDocument>(State::did_open)
@ -790,6 +791,27 @@ impl LanguageState {
let (path, position) = as_path_pos(params);
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 {
@ -1047,6 +1069,7 @@ impl LanguageState {
Completion(req) => handle.run_stateful(snap, req, R::Completion),
SignatureHelp(req) => handle.run_semantic(snap, req, R::SignatureHelp),
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),
Symbol(req) => handle.run_semantic(snap, req, R::Symbol),
WorkspaceLabel(req) => handle.run_semantic(snap, req, R::WorkspaceLabel),