fix(lsp): apply each rename edit to its own document

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-23 20:19:21 +02:00
parent cf6bc6987e
commit ded9cabdf4
3 changed files with 60 additions and 6 deletions
+8
View File
@@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
-
### Fixed
- **Rename edits land in their own documents.** A rename collected the
ranges of every reference across the open documents but applied them all
to the document that started it, so renaming a symbol used in a second
file moved that file's text into the first. Each edit now applies to the
document it was collected in.
## [0.35.0] - 2026-09-22
### Added
+6 -6
View File
@@ -229,7 +229,9 @@ func (s *Server) references(p referenceParams) []Location {
return out
}
// rename renames a label across the document.
// rename renames a label across every document the references reach. Each
// edit is grouped under the URI it belongs to: a range collected in another
// file must never be applied to the document that started the rename.
func (s *Server) rename(p renameParams) *WorkspaceEdit {
text := s.docs[p.TextDocument.URI]
word, _ := wordAt(text, p.Position)
@@ -244,13 +246,11 @@ func (s *Server) rename(p renameParams) *WorkspaceEdit {
if len(refs) == 0 {
return nil
}
edits := make([]TextEdit, 0, len(refs))
edits := make(map[string][]TextEdit, len(refs))
for _, loc := range refs {
edits = append(edits, TextEdit{Range: loc.Range, NewText: p.NewName})
}
return &WorkspaceEdit{
Changes: map[string][]TextEdit{p.TextDocument.URI: edits},
edits[loc.URI] = append(edits[loc.URI], TextEdit{Range: loc.Range, NewText: p.NewName})
}
return &WorkspaceEdit{Changes: edits}
}
// documentFormatting returns a single edit that replaces the whole document
+46
View File
@@ -1074,3 +1074,49 @@ func TestCrossFileDefinitionAndReferences(t *testing.T) {
t.Errorf("references by uri = %v, want one in each file", byURI)
}
}
// TestRenameGroupsEditsByDocument pins the grouping contract: every edit is
// applied to the document it was collected in, so a cross-file rename never
// corrupts the origin buffer.
func TestRenameGroupsEditsByDocument(t *testing.T) {
docA := "TEXT \u00b7caller(SB), NOSPLIT, $0\n" +
"\tCALL \u00b7helper(SB)\n" +
"\tRET\n"
docB := "TEXT \u00b7helper(SB), NOSPLIT, $0\n" +
"\tRET\n"
uriA := "file:///rename_a_amd64.s"
uriB := "file:///rename_b_amd64.s"
in := frame(1, "initialize", map[string]any{}) +
frame(nil, "initialized", map[string]any{}) +
frame(nil, "textDocument/didOpen", map[string]any{
"textDocument": map[string]any{"uri": uriA, "languageId": "gasm", "version": 1, "text": docA},
}) +
frame(nil, "textDocument/didOpen", map[string]any{
"textDocument": map[string]any{"uri": uriB, "languageId": "gasm", "version": 1, "text": docB},
}) +
frame(2, "textDocument/rename", map[string]any{
"textDocument": map[string]any{"uri": uriA},
"position": map[string]any{"line": 1, "character": 8}, // on helper in CALL ·helper(SB)
"newName": "renamed",
}) + frame(nil, "exit", nil)
msgs := run(t, in)
resp := findByID(msgs, 2)
if resp == nil {
t.Fatal("no rename response")
}
var edit WorkspaceEdit
if err := json.Unmarshal(mustResult(t, resp), &edit); err != nil {
t.Fatal(err)
}
if len(edit.Changes[uriA]) != 1 || len(edit.Changes[uriB]) != 1 {
t.Fatalf("edits per uri = %d and %d, want one each", len(edit.Changes[uriA]), len(edit.Changes[uriB]))
}
gotA := applyEdits(docA, edit.Changes[uriA])
gotB := applyEdits(docB, edit.Changes[uriB])
if !strings.Contains(gotA, "CALL renamed(SB)") || strings.Contains(gotA, "renamed(SB), NOSPLIT") {
t.Errorf("caller document rename wrong: %q", gotA)
}
if !strings.Contains(gotB, "TEXT renamed(SB)") {
t.Errorf("helper document rename wrong: %q", gotB)
}
}