From f35c781f428af5b0538ed166f281cb5457a32a6e Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:32:51 +0800 Subject: [PATCH] fix(refactor): apply server-versioned edits to files that aren't open (#1295) gopls sends a document version for every file of a rename, including files the editor never opened. Applying a workspace edit rejected any versioned file without an open document as changed, so a Go rename touching a closed file always failed with "changed; request the refactoring again". For a file that isn't open, the preconditions captured with the edit now decide; an edit without them is still rejected. --- .../src/documents/workspace_edit.rs | 11 +++++-- crates/ide-workspace/tests/workspace_edits.rs | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/ide-workspace/src/documents/workspace_edit.rs b/crates/ide-workspace/src/documents/workspace_edit.rs index 10543d2da..e74dc9f49 100644 --- a/crates/ide-workspace/src/documents/workspace_edit.rs +++ b/crates/ide-workspace/src/documents/workspace_edit.rs @@ -402,13 +402,18 @@ impl Documents { return Ok(()); } let mut edit = normalize_edit(edit)?; + let captured = !edit.preconditions.is_empty(); self.prepare_workspace_edit(&mut edit)?; self.validate_preconditions(&edit.preconditions)?; for file in &edit.files { if let FileEdit::Edit { path, version: Some(version), .. } = file { - let doc = self.by_path(path).ok_or_else(|| stale(path))?; - if i64::from(self.version(doc)?) != i64::from(*version) { - return Err(stale(path)); + match self.by_path(path) { + Some(doc) if i64::from(self.version(doc)?) != i64::from(*version) => return Err(stale(path)), + Some(_) => {} + // Servers version files the editor never opened (gopls renames). The + // preconditions captured with the edit already cover its disk contents. + None if captured => {} + None => return Err(stale(path)), } } } diff --git a/crates/ide-workspace/tests/workspace_edits.rs b/crates/ide-workspace/tests/workspace_edits.rs index 792e8344a..e88257d97 100644 --- a/crates/ide-workspace/tests/workspace_edits.rs +++ b/crates/ide-workspace/tests/workspace_edits.rs @@ -111,6 +111,37 @@ fn matching_document_version_is_applied() { assert_eq!(std::fs::read_to_string(a).unwrap(), "new\n"); } +#[test] +fn versioned_edit_of_a_closed_file_is_guarded_by_its_preconditions() { + // gopls versions every file of a rename, including ones the editor never opened. + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("closed.txt"); + std::fs::write(&path, "old\n").unwrap(); + let docs = documents(); + let versioned = || { + let mut operation = edit(&path, "new"); + if let FileEdit::Edit { version, .. } = &mut operation { + *version = Some(0); + } + WorkspaceEdit { files: vec![operation], ..Default::default() } + }; + // Without a snapshot of what the server saw, a version for a closed file can't be trusted. + assert_eq!(docs.apply_workspace_edit(&versioned()).unwrap_err().kind, ErrorKind::OutOfSync); + assert_eq!(std::fs::read_to_string(&path).unwrap(), "old\n"); + + let mut changed = versioned(); + docs.prepare_workspace_edit(&mut changed).unwrap(); + std::fs::write(&path, "own\n").unwrap(); + assert_eq!(docs.apply_workspace_edit(&changed).unwrap_err().kind, ErrorKind::OutOfSync); + assert_eq!(std::fs::read_to_string(&path).unwrap(), "own\n"); + + std::fs::write(&path, "old\n").unwrap(); + let mut prepared = versioned(); + docs.prepare_workspace_edit(&mut prepared).unwrap(); + docs.apply_workspace_edit(&prepared).unwrap(); + assert_eq!(std::fs::read_to_string(&path).unwrap(), "new\n"); +} + #[test] fn unversioned_preview_rejects_changed_disk_and_dirty_buffers() { let tmp = tempfile::tempdir().unwrap();