ADR 0132: Renaming a symbol¶
Status¶
Accepted
Context¶
The last of the language server's useful half. It waited on ADR 0131, because a rename and a project-wide replace want the same two answers — what does undo mean, and does anything touch disk — and answering them twice differently would have been the wrong shape.
With those settled, rename is a second producer of an edit set the
preview and apply path already take. What was left was the protocol and
the reading of a WorkspaceEdit.
Decision¶
F2. The F<n> layer is "ask the language server"
(ADR 0120), and F2 is what every
other editor binds rename to, which is the whole argument for the layer.
The prompt is the find bar with one field, prefilled with the name under the caret, so the common edit is a few keystrokes rather than retyping it.
The core sends and does not interpret¶
ase_lsp_client_request_rename() matches every other request in that
header: it sends, and hands the reply back as raw JSON. Nothing is
edited there. A rename to an empty name is refused before anything is
sent.
Reading a WorkspaceEdit is its own unit¶
lsp::replacementsFrom() in gui/src/lsp_workspace_edit.h, because it
is the part with edge cases rather than the part with plumbing:
- Two shapes.
changesis an object keyed by file URI;documentChangesis an array of{textDocument, edits}. Servers pick, so both are read. Reading only one works until a server that picks the other is used. - A multi-line range is dropped, not approximated. Rename does not produce one for a name, and guessing at what a server meant by one is how a refactor eats a function body.
- A URI that is not a local file — a built-in, something inside an archive — is skipped.
- An entry with no range, no
newText, or an empty range is skipped.
Iterating changes needed something the JSON layer did not have: every
accessor took a key, and these keys are data. ase_json_object_size(),
ase_json_object_key() and ase_json_object_value() are new.
Every edit says what must already be there¶
LSP counts characters in UTF-16 code units; this editor reads bytes. ADR 0067 accepted that for a caret, where being a little early on a line of wide characters is recoverable. For a rename it is not: a wrong length eats whatever is next to the name.
So TextEdit gained expected — the bytes that must already be at that
position — and an edit whose bytes do not match is skipped. That closes
the same hole for replace, where the file may have changed between the
search and the apply, and it is what makes an edit checkable rather than
trusted.
Consequences¶
project::Replacement now carries its own length and replacement text
rather than the operation carrying one of each. A WorkspaceEdit need
not use the same length or the same text everywhere, and search-and-
replace is the special case where it does.
Verified against real clangd, not only the fake server: renaming
widget_total across a header and an implementation produced four
changes in two files, including both occurrences on one line, and applied
them correctly.
That run also caught the preview drawing the same picture for both of
those occurrences. textColumn was being found with indexOf, which
always finds the first one. It is now derived from how much indentation
was trimmed, so each row previews its own change.
The docgen guard from ADR 0126
refused the build when F2 was added: its "function keys" layer matched
c.contains("f1"), which claimed shift+f12 by accident and rejected
f2 outright. The matcher now tests the key rather than a substring.
It refused to ship a reference page that was quietly one row short,
which is what it was written to do.
What rename still does not do: a server that answers with a
documentChanges carrying file creations, deletions or renames has
those ignored — only text edits are read. Nothing in C or C++ produces
them for a symbol rename.