Add support for renaming local variables#1232
Conversation
233e283 to
0bead42
Compare
| /// word) returns as-is. Anything else gets wrapped in backticks. Empty | ||
| /// names, reserved words, and names containing a literal backtick return | ||
| /// `Err` (a backtick can't appear inside a backtick-quoted identifier). | ||
| pub fn to_identifier_text(name: &str) -> anyhow::Result<String> { |
There was a problem hiding this comment.
I slightly question the decision to use anyhow in our lowest level crate
Typically anyhow isn't used in a "library" style crate, but our use case is a bit blurry because our only consumers won't care much about what type of error this is.
If you care about this, I would not be against seeing a structured IdentifierError or something
There was a problem hiding this comment.
ACTUALLY I now see that these errors actually make their way to the USER when they type in an invalid name!
In that case I now feel even stronger that they should be a structured error type, with a note that they display to the user as is, and maybe even some tests to validate their behavior if you dont have any yet
(GREAT user experience though!)
There was a problem hiding this comment.
hmm I'm very skeptical about the benefits of proactively creating error types unless we need specific handling somewhere in the stack, and quite confident about the costs.
| /// A name that parses as a plain R identifier (and isn't a reserved | ||
| /// word) returns as-is. Anything else gets wrapped in backticks. Empty | ||
| /// names, reserved words, and names containing a literal backtick return | ||
| /// `Err` (a backtick can't appear inside a backtick-quoted identifier). |
There was a problem hiding this comment.
Can we return a Cow<str>? I feel like 95% of the time we will have a valid identifier
There was a problem hiding this comment.
(I see it won't matter much for our current usage, but it still seems useful in case we use this a lot)
There was a problem hiding this comment.
Returning Cow only helps if RenameTargets::new_text is also Cow (so the caller doesn't .into_owned() and re-allocate). But that would require adding a lifetime to RenameTargets, which doesn't feel great right now.
| pub fn is_valid_identifier(name: &str) -> bool { | ||
| let mut chars = name.chars(); | ||
| let Some(first) = chars.next() else { | ||
| return false; | ||
| }; | ||
| if !(first.is_ascii_alphabetic() || first == '.') { | ||
| return false; | ||
| } | ||
| if first == '.' { | ||
| if let Some(second) = name.chars().nth(1) { | ||
| if second.is_ascii_digit() { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| chars.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_') | ||
| } | ||
|
|
||
| /// R reserved words that cannot be used as identifier names. Source: | ||
| /// `?Reserved` in R. Note that `return` is a function, not a reserved | ||
| /// word, so it's missing from this list (`return <- 1` is valid R). | ||
| /// `_` became reserved in R 4.2 for use as the `|>` pipe placeholder. | ||
| pub fn is_reserved(name: &str) -> bool { |
There was a problem hiding this comment.
are you sure you want these to be pub?
There was a problem hiding this comment.
I'm agnostic about it since it's an internal crate.
| let Some(first) = chars.next() else { | ||
| return false; | ||
| }; | ||
| if !(first.is_ascii_alphabetic() || first == '.') { |
There was a problem hiding this comment.
i think this will fail for valid non ascii identifiers, like μ <- 1
There was a problem hiding this comment.
getting it fully correct might be Extremely Hard™
| } | ||
|
|
||
| /// R reserved words that cannot be used as identifier names. Source: | ||
| /// `?Reserved` in R. Note that `return` is a function, not a reserved |
There was a problem hiding this comment.
funny how we both touched this this week r-lib/tree-sitter-r#197
| match ident { | ||
| Identifier::Definition { def, name, .. } => Some((def.range(), name.to_string())), | ||
| Identifier::Use { use_site, name, .. } => Some((use_site.range(), name.to_string())), | ||
| Identifier::NamespaceAccess { .. } => None, |
There was a problem hiding this comment.
I'm not entirely sure what this should even do with NamespaceAccesses
There was a problem hiding this comment.
Would only work when the package is in the workspace. Sometimes you've got to qualify your own package for reason x or y (I had to do it in rlang). Sometimes you're working on two packages at a time and one of them calls the other with qualified names.
| params: TextDocumentPositionParams, | ||
| state: &WorldState, | ||
| ) -> LspResult<Option<PrepareRenameResponse>> { | ||
| // Don't propagate errors to the frontend |
There was a problem hiding this comment.
Would you mention why? Because the protocol definitely encourages it
error: code and message set in case the element can’t be renamed. Clients should show the information in their user interface.
IIUC, it is because prepare_rename() isn't set up to return actionable errors to the user right now
There was a problem hiding this comment.
I copied ty's behaviour which doesn't propagate. And then I got super confused when I demo'd rename to Hadley because I was expecting an error right away but instead had to pick a name first! We now propagate errors as recommended in the spec, good catch.
| if is_valid_identifier(name) { | ||
| return Ok(name.to_string()); | ||
| } | ||
| if name.contains('`') { |
There was a problem hiding this comment.
I think you should add a special case before this of "starts with and ends with a backtick"
I tried renaming a symbol to
`i know what im doing`
and it refused to allow me to do so, but i think this should be allowed. i know i can drop the backticks here, but i feel like i shouldnt have to because im an "expert" and gave you the right thing to begin with
e150326 to
2bc2610
Compare
0bead42 to
d34bb18
Compare
82d3822 to
014546a
Compare
d34bb18 to
3b0ed35
Compare
Branched from #1231
Addresses posit-dev/positron#13749
Part of #1149
Implements rename for local definitions. If a symbol is unbound locally, an error is emitted. Cross-file renaming will come along with the Salsa infrastructure.
See usage in screencast. The most surprising behaviour is with nested lazy contexts where the enclosing snapshot reaches definitions and uses below the context. That's a consequence of the over-approximation we inherit from ty. The alternative would be to perform full call analysis to determine exactly what a nested symbol can reach (intractable in general).
Screen.Recording.2026-05-26.at.08.56.49.mov
See user-facing errors in second screencast, when trying to rename something not renamable, or to an invalid identifier. There should be no backtraces in user visible popups.
Screen.Recording.2026-05-26.at.08.57.32.mov