Skip to content

Expand rename to cover services, RPCs, fields, reference sites, and rpc/request/response chains#131

Open
skagedal wants to merge 13 commits into
coder3101:mainfrom
skagedal:rename-services-and-references
Open

Expand rename to cover services, RPCs, fields, reference sites, and rpc/request/response chains#131
skagedal wants to merge 13 commits into
coder3101:mainfrom
skagedal:rename-services-and-references

Conversation

@skagedal
Copy link
Copy Markdown

@skagedal skagedal commented May 14, 2026

I would like the "rename" action to just work everywhere for the symbol under the cursor. This is wired into my muscle-memory from using other editors and I tend to do it even when it's a bit silly, like in cases where the symbol just would never be referenced from anywhere else (such as a service name in Protobuf). I like to just not have to think about it.

I filed an issue before (#129) about wanting to be able to rename a message from a referenced site. This PR includes that, but also some other rename improvements. If you would rather want me to split it into separate PRs, let me know.

I find the rpc/Request/Response chain rename especially useful. I realize all protobuf users may not follow this convention, but it should (hopefully) not get in the way for people who don't. Let me know if you would rather like this to be configurable.

Additions

Service and RPC symbol rename. can_rename was gated on message_name/enum_name parents only, so invoking rename on a service or RPC method was a silent no-op. A new is_renameable predicate accepts service_name and rpc_name, and rename_tree's empty-ancestor path returns a single in-file edit (services/RPCs have no cross-file references to propagate).

Rename from a type-reference site. Previously you had to navigate to the declaration first. Now invoking rename on a MyRequest reference inside rpc DoThing(MyRequest) — or on the inner segment of a qualified type like Outer.Inner — works: the LSP resolves the partial qualified path under the cursor to the declaration via the existing definition machinery and runs the regular rename from there. The user's "rename the segment under cursor" intent is honored: cursor on Outer renames Outer, cursor on Inner renames Inner.

Field name, oneof, and enum-value rename. Identifiers whose parent is field, map_field, oneof_field, oneof, or enum_field are now renameable. Each is single-site — these don't appear as type references in other .proto files — so the workspace pass is intentionally a no-op for them, avoiding accidental renames if a lowercase type happens to share a name.

Chained rpc/Request/Response rename. When an rpc follows the rpc <Name>(<Name>Request) returns (<Name>Response) convention from the Google API design guide (AIPs 131–136), renaming any one of the three triggers a chained rename of the other two. The chain only kicks in when all of these hold:

  • the matching message name follows the convention exactly (trailing segment of its qualified text),
  • the request/response is used by exactly one rpc in the workspace, and
  • the user's new name preserves the convention (e.g. renaming a request, the new name still ends with Request).

If any check fails, only the symbol the user invoked rename on is renamed — the primary rename is never blocked by chain-detection failures.

Implementation

This implementation has been assisted by Claude Code using Opus 4.7. I have read and generally understood the code and sign off on it as a human being. I have previous experience with Rust, but would not consider myself an expert. I have no previous experience with writing LSP:s.

skagedal and others added 10 commits May 13, 2026 20:44
Rename was previously gated on message_name/enum_name parents in
can_rename, so invoking rename on a service or RPC method silently did
nothing. Add a focused is_renameable predicate that also accepts
service_name and rpc_name, and handle the empty-ancestor case in
rename_tree by passing the unqualified identifier to the workspace
pass — services and RPCs have no cross-file references, so that pass
becomes a harmless no-op for them.
Previously rename was only available at the declaration of a message
or enum. Invoking Rename Symbol on a type reference (e.g. the
`MyRequest` in `rpc DoThing(MyRequest)`, or the `Author` in
`Book.Author field = 1;`) was rejected by can_rename, forcing users to
first navigate to the declaration.

The LSP rename handler now detects when the cursor is on an identifier
inside a `message_or_enum_type` node and pivots: it builds the partial
qualified path up to and including the segment under cursor
(`Book.Author` vs `Book`, depending on which segment the cursor is
on), resolves it to the declaration via the existing definition
machinery, and runs the regular rename from there. The workspace pass
then propagates the rename to all references — including the site the
user was standing on.

Existing can_rename snapshots that previously returned None at
reference positions are updated to reflect the new behavior.
Extend is_renameable to accept identifiers whose parent is a
field-like declaration: regular fields, map fields, oneof fields,
the oneof itself, and enum values. These are single-site renames —
they aren't referenced as types from other .proto files — so they
follow the empty-ancestor path in rename_tree.

Refine the empty-ancestor handling to only propagate the identifier
to the workspace pass for top-level enums (which can be referenced as
types from other files). Services, RPCs, fields, oneofs, and enum
values now produce a clean single-site edit with no risk of
inadvertently renaming an unrelated type that happens to share the
name.
When an rpc follows the convention
  rpc <Name>(<Name>Request) returns (<Name>Response);
(enforced by buf lint's RPC_REQUEST_STANDARD_NAME /
RPC_RESPONSE_STANDARD_NAME), renaming any one of the trio now triggers
a chained rename of the other two. The chain only kicks in when:

  - the matching request/response message is named exactly per the
    convention (trailing segment of its qualified text),
  - it is used by exactly one rpc in the workspace, and
  - the user's new name preserves the convention (i.e. when renaming a
    request, the new name still ends with `Request`).

If any check fails, only the symbol the user invoked rename on is
renamed — the primary rename is never blocked by chain-detection
failures. New parser-level helpers `rpc_at_position`,
`message_name_at_position`, and `all_rpc_signatures` expose the
information the orchestrator needs; new workspace-level helpers
`find_rpc_decl` and `count_rpc_uses_of_type` handle the cross-file
lookups.

The orchestration lives in `lsp.rs::rename`, which now factors out
`run_single_rename` so multiple rename ops can share the existing
per-file + workspace machinery and merge their edits.
- Move trailing_segment into utils.rs with rsplit_once-style impl
  to dedupe the helper from lsp.rs and workspace/rename.rs.
- Add NodeKind::is_rpc_name and use it in find_rpc_decl to walk
  rpc_name nodes directly instead of every identifier in every file.
- Document rename_pivot_identifier's dependency on the tree-sitter-proto
  grammar emitting dot separators as anonymous children.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the rpc/request/response chain detection out of the LSP handler
and onto ProtoLanguageState as `compute_rename_ops` + `apply_rename_ops`,
with RenameOp as a public struct and per-case helpers
(`chain_from_rpc_cursor`, `chain_from_message_cursor`,
`sibling_message_ops`, `strip_convention_suffix`). The LSP layer now
just pivots from reference to declaration and delegates the rest.

Restores logging on workspace lookup and primary-rename failures.
No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user renames a `<Rpc>Request` / `<Rpc>Response` message, the
chain previously looked up the matching rpc by simple name and took
the first hit. If another package happened to declare an rpc with the
same simple name, iteration order alone determined whether the chain
fired or silently no-op'd.

Replace `find_rpc_decl` (Option) with `find_rpc_decls` (Vec), enumerate
all candidates, and keep only the unique one whose request/response
slot resolves (through the workspace's name-resolution rules) back to
the user's primary message. Comparison uses a span-inclusive
`position_in_range` so a mid-identifier cursor still matches the
definition's full-range Location.

Adds collision_foo.proto / collision_bar.proto fixtures and a
regression test demonstrating the chain stays inside foo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end coverage of the rpc/request/response chain logic without
needing a ClientSocket:

- chain from rpc cursor (primary + 2 sibling ops)
- chain from request cursor (primary + rpc + response)
- shared request blocks chain via uniqueness check
- new name not preserving convention blocks chain
- reference-site cursor: documents that the LSP layer must pivot first
- apply_rename_ops merges edits across primary + siblings

Pulls out `make_state` and `op` helpers to keep the new tests terse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@skagedal skagedal marked this pull request as ready for review May 14, 2026 07:22
@coder3101
Copy link
Copy Markdown
Owner

I find the rpc/Request/Response chain rename especially useful. I realize all protobuf users may not follow this convention, but it should (hopefully) not get in the way for people who don't. Let me know if you would rather like this to be configurable.

Agreed this seems useful but I think it should be behind some configuration.

@skagedal
Copy link
Copy Markdown
Author

Agreed this seems useful but I think it should be behind some configuration.

Cool, I'll have a look at that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants