fix(lsp): apply each rename edit to its own document
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -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
|
## [0.35.0] - 2026-09-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+6
-6
@@ -229,7 +229,9 @@ func (s *Server) references(p referenceParams) []Location {
|
|||||||
return out
|
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 {
|
func (s *Server) rename(p renameParams) *WorkspaceEdit {
|
||||||
text := s.docs[p.TextDocument.URI]
|
text := s.docs[p.TextDocument.URI]
|
||||||
word, _ := wordAt(text, p.Position)
|
word, _ := wordAt(text, p.Position)
|
||||||
@@ -244,13 +246,11 @@ func (s *Server) rename(p renameParams) *WorkspaceEdit {
|
|||||||
if len(refs) == 0 {
|
if len(refs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
edits := make([]TextEdit, 0, len(refs))
|
edits := make(map[string][]TextEdit, len(refs))
|
||||||
for _, loc := range refs {
|
for _, loc := range refs {
|
||||||
edits = append(edits, TextEdit{Range: loc.Range, NewText: p.NewName})
|
edits[loc.URI] = append(edits[loc.URI], TextEdit{Range: loc.Range, NewText: p.NewName})
|
||||||
}
|
|
||||||
return &WorkspaceEdit{
|
|
||||||
Changes: map[string][]TextEdit{p.TextDocument.URI: edits},
|
|
||||||
}
|
}
|
||||||
|
return &WorkspaceEdit{Changes: edits}
|
||||||
}
|
}
|
||||||
|
|
||||||
// documentFormatting returns a single edit that replaces the whole document
|
// documentFormatting returns a single edit that replaces the whole document
|
||||||
|
|||||||
@@ -1074,3 +1074,49 @@ func TestCrossFileDefinitionAndReferences(t *testing.T) {
|
|||||||
t.Errorf("references by uri = %v, want one in each file", byURI)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user