Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger VSCode to rename after extract variable assist is applied #17587

Merged
merged 2 commits into from
Jul 15, 2024

Conversation

joshka
Copy link
Contributor

@joshka joshka commented Jul 13, 2024

When the user applies the "Extract Variable" assist, the cursor is
positioned at the newly inserted variable. This commit adds a command
to the assist that triggers the rename action in VSCode. This way, the
user can quickly rename the variable after applying the assist.

Fixes part of: #17579

Screen.Recording.2024-07-12.at.6.56.43.PM.mov

I haven't yet looked at the module or function extraction assists yet.

When the user applies the "Extract Variable" assist, the cursor is
positioned at the newly inserted variable. This commit adds a command
to the assist that triggers the rename action in VSCode. This way, the
user can quickly rename the variable after applying the assist.

Fixes part of: rust-lang#17579
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 13, 2024
Copy link
Contributor

@DropDemBits DropDemBits left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally all of the extract & generate assists should still be using the add_placeholder_snippet_group for better out-of-the box support for other editor extensions, but that's an entirely separate issue 🙂

crates/rust-analyzer/src/lsp/to_proto.rs Outdated Show resolved Hide resolved
crates/ide-db/src/assists.rs Show resolved Hide resolved
crates/ide-assists/src/handlers/extract_variable.rs Outdated Show resolved Hide resolved
editors/code/src/client.ts Show resolved Hide resolved
@joshka
Copy link
Contributor Author

joshka commented Jul 14, 2024

Ideally all of the extract & generate assists should still be using the add_placeholder_snippet_group for better out-of-the box support for other editor extensions, but that's an entirely separate issue 🙂

I experimented with that first and it seemed like it was a less than ideal experience. Agree that this is a separate issue.

- move `edit.rename()` to the end of the function
- use a match statement to set `res.command`
@Veykril
Copy link
Member

Veykril commented Jul 15, 2024

Thanks!
@bors r+

@bors
Copy link
Contributor

bors commented Jul 15, 2024

📌 Commit bfa6a5c has been approved by Veykril

It is now in the queue for this repository.

@bors
Copy link
Contributor

bors commented Jul 15, 2024

⌛ Testing commit bfa6a5c with merge e961b1a...

@bors
Copy link
Contributor

bors commented Jul 15, 2024

☀️ Test successful - checks-actions
Approved by: Veykril
Pushing e961b1a to master...

@bors bors merged commit e961b1a into rust-lang:master Jul 15, 2024
11 checks passed
@joshka joshka deleted the jm/edit-name-after-refactor branch July 15, 2024 21:03
@nemethf
Copy link
Contributor

nemethf commented Jul 19, 2024

@joshka, @Veykril, shouldn't RA check whether the LSP client announced its support for the command "rust-analyzer.rename" before sending it in a codeAction/resolve reply?

Also, I'm not sure asking the client to do a rename after the the extract is the best approach since RA can send "disjoint" snippets as well. I have not tested it, but instead of sending this:

{
  "jsonrpc": "2.0", "id": 7,
  "result": {
    "title": "Extract into variable",
    "kind": "refactor.extract",
    "command": { "title": "rename",  "command": "rust-analyzer.rename" },
    "edit": {
      "documentChanges": [
        {
          "textDocument": { "uri": "file:///tmp/rust-rename-17587/src/main.rs", "version": 0 },
          "edits": [
            {
              "range": {
                "start": { "line": 1, "character": 8 },
                "end": { "line": 1, "character": 9  }
              },
              "newText": "$0var_name",
              "insertTextFormat": 2
            },
            {
              "range": {
                "start": { "line": 1, "character": 18 },
                "end": {  "line": 1,  "character": 18 }
              },
              "newText": "\n    let a = var_name;"
            }
          ]
        }
      ]
    }
  }
}

RA could send something like this (edited):

{
  "jsonrpc": "2.0", "id": 7,
  "result": {
    "title": "Extract into variable",
    "kind": "refactor.extract",
    "edit": {
      "documentChanges": [
        {
          "textDocument": { "uri": "file:///tmp/rust-rename-17587/src/main.rs", "version": 0 },
          "edits": [
            {
              "range": {
                "start": { "line": 1, "character": 8 },
                "end": { "line": 1, "character": 9  }
              },
              "newText": "$0${1:var_name}",
              "insertTextFormat": 2
            },
            {
              "range": {
                "start": { "line": 1, "character": 18 },
                "end": {  "line": 1,  "character": 18 }
              },
              "newText": "\n    let a = $1;"
              "insertTextFormat": 2
            }
          ]
        }
      ]
    }
  }
}

I.e., the "var_name" is a just default value for the mirrored tab-stop field. The commit log of #15876 directly says:

Explicitly allow a single TextDocumentEdit to have multiple SnippetTextEdits. This allows things like renaming extracted variables and functions without having to go through a separate rename step.

@DropDemBits
Copy link
Contributor

@nemethf

shouldn't RA check whether the LSP client announced its support for the command "rust-analyzer.rename" before sending it in a codeAction/resolve reply?

Hmm, it should be respecting the client commands list that the client declares and only emitting it if editor.action.rename is specified there.

Also, I'm not sure asking the client to do a rename after the the extract is the best approach since RA can send "disjoint" snippets as well.

This would also be better for editors like Helix, where I forsee code action snippet support being available before a plugin system is ready.

It is possible to do it today like is done for generate_trait_from_impl, but for extract_variable there's one case where it's annoying to grab the nodes to mark as a placeholder group. Once we have something like #9649 it'll be nicer to find these nodes again, so hopefully I can scrounge up enough free time and look into that again.

@nemethf
Copy link
Contributor

nemethf commented Jul 19, 2024

shouldn't RA check whether the LSP client announced its support for the command "rust-analyzer.rename" before sending it in a codeAction/resolve reply?

Hmm, it should be respecting the client commands list that the client declares and only emitting it if editor.action.rename is specified there.

That is not what happens in my case. The client only states that it supports the most basic rename action in its RenameClientCapabilities and that's all. And yet the server sends this in its textDocument/rename reply: "command": { "title": "rename", "command": "rust-analyzer.rename" },

$ rust-analyzer --version
rust-analyzer 0.4.2041-standalone
This is the initialize request my LSP client sends:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "processId": 1602814,
    "clientInfo": { "name": "Eglot",  "version": "1.17" },
    "rootPath": "/tmp/rust-rename-17587/",
    "rootUri": "file:///tmp/rust-rename-17587",
    "initializationOptions": {},
    "capabilities": {
      "workspace": {
        "applyEdit": true,
        "executeCommand": { "dynamicRegistration": false },
        "workspaceEdit": { "documentChanges": true },
        "didChangeWatchedFiles": { "dynamicRegistration": true },
        "symbol": { "dynamicRegistration": false },
        "configuration": true,
        "workspaceFolders": true
      },
      "textDocument": {
        "synchronization": {
          "dynamicRegistration": false,
          "willSave": true,
          "willSaveWaitUntil": true,
          "didSave": true
        },
        "completion": {
          "dynamicRegistration": false,
          "completionItem": {
            "snippetSupport": true,
            "deprecatedSupport": true,
            "resolveSupport": {
              "properties": [ "documentation", "details", "additionalTextEdits" ]
            },
            "tagSupport": { "valueSet": [ 1 ] }
          },
          "contextSupport": true
        },
        "hover": {
          "dynamicRegistration": false,
          "contentFormat": [ "plaintext" ]
        },
        "signatureHelp": {
          "dynamicRegistration": false,
          "signatureInformation": {
            "parameterInformation": { "labelOffsetSupport": true },
            "documentationFormat": [ "plaintext" ],
            "activeParameterSupport": true
          }
        },
        "references": { "dynamicRegistration": false },
        "definition": {
          "dynamicRegistration": false,
          "linkSupport": true
        },
        "declaration": {
          "dynamicRegistration": false,
          "linkSupport": true
        },
        "implementation": {
          "dynamicRegistration": false,
          "linkSupport": true
        },
        "typeDefinition": {
          "dynamicRegistration": false,
          "linkSupport": true
        },
        "documentSymbol": {
          "dynamicRegistration": false,
          "hierarchicalDocumentSymbolSupport": true,
          "symbolKind": {
            "valueSet": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 ]
          }
        },
        "documentHighlight": { "dynamicRegistration": false },
        "codeAction": {
          "dynamicRegistration": false,
          "resolveSupport": {
            "properties": [ "edit", "command" ]
          },
          "dataSupport": true,
          "codeActionLiteralSupport": {
            "codeActionKind": {
              "valueSet": [
                "quickfix",
                "refactor",
                "refactor.extract",
                "refactor.inline",
                "refactor.rewrite",
                "source",
                "source.organizeImports"
              ]
            }
          },
          "isPreferredSupport": true
        },
        "formatting": { "dynamicRegistration": false },
        "rangeFormatting": { "dynamicRegistration": false },
        "rename": { "dynamicRegistration": false },
        "inlayHint": { "dynamicRegistration": false },
        "publishDiagnostics": { 
          "relatedInformation": false,
          "codeDescriptionSupport": false,
          "tagSupport": {
            "valueSet": [ 1, 2 ]
          }
        }
      },
      "window": {
        "showDocument": { "support": true },
        "workDoneProgress": true
      },
      "general": {
        "positionEncodings": [ "utf-32", "utf-8", "utf-16" ]
      },
      "experimental": {
        "snippetTextEdit": true,
        "serverStatusNotification": true,
        "colorDiagnosticOutput": true,
        "openServerLogs": true,
        "testExplorer": true,
        "localDocs": true
      },
      "offsetEncoding": [ "utf-32", "utf-16" ]
    },
    "workspaceFolders": [
      {
        "uri": "file:///tmp/rust-rename-17587",
        "name": "/tmp/rust-rename-17587/"
      }
    ]
  }
}

@Veykril
Copy link
Member

Veykril commented Jul 19, 2024

There is a weird lens_forceCustomCommands config that is incorrectly set to to true by default, when that is turned on we ignored the capability 🙃 That would explain this, unsure what the point of this is, we should remove it fully.

@joshka
Copy link
Contributor Author

joshka commented Jul 19, 2024

Also, I'm not sure asking the client to do a rename after the the extract is the best approach since RA can send "disjoint" snippets as well. I have not tested it, but instead of sending this:

The rename UX flow seems (subjectively) nicer than the disjoint snippets flow to me as there's no good intuitive way to finish the disjoint edit (neither Tab or Enter work, only Escape), also there's some editing weirdness:

Changing extract_variable.rs:131 (and removing the rename call):

                    if let Some(cap) = ctx.config.snippet_cap {
                        if let Some(ast::Pat::IdentPat(ident_pat)) = let_stmt.pat() {
                            if let Some(name) = ident_pat.name() {
                                edit.add_placeholder_snippet_group(
                                    cap,
                                    vec![name.syntax().clone(), name_expr.syntax().clone()],
                                );
                            }
                        }
                    }
Screen.Recording.2024-07-19.at.12.35.01.PM.mov

Regarding the client commands, the server side execution of the rename command is a noop if the editor.action.rename command is not enabled (as commands.rename == false in that case). I don't think the assists have access to the any client command config, so there's no way to filter this out.

let commands = snap.config.client_commands();
res.command = match assist.command {
Some(assists::Command::TriggerSignatureHelp) if commands.trigger_parameter_hints => {
Some(command::trigger_parameter_hints())
}
Some(assists::Command::Rename) if commands.rename => Some(command::rename()),
_ => None,
};

That is not what happens in my case. The client only states that it supports the most basic rename action in its RenameClientCapabilities and that's all. And yet the server sends this in its textDocument/rename reply: "command": { "title": "rename", "command": "rust-analyzer.rename" },

Is there any impact of this other than an unnecessary round trip? I'm trying to understand what the error / problem is here, but I'm not seeing it.

@Veykril
Copy link
Member

Veykril commented Jul 19, 2024

Is there any impact of this other than an unnecessary round trip? I'm trying to understand what the error / problem is here, but I'm not seeing it.

That depends on the client, we are violating the LSP (or in this case the lsp extension I suppose), we shouldn't be doing that.

The problem is this config being enabled by default for some reason https://github.com/veykril/rust-analyzer/blob/09ef75c717e9ffb4cbc71ce1cfec7694244742c2/crates/rust-analyzer/src/config.rs#L564 which then force enables all of the commands here https://github.com/veykril/rust-analyzer/blob/09ef75c717e9ffb4cbc71ce1cfec7694244742c2/crates/rust-analyzer/src/config.rs#L1896-L1911
We should just get rid of lens_forceCustomCommands, I see no value in it for debugging.

@joshka
Copy link
Contributor Author

joshka commented Jul 19, 2024

Ah got it

@nemethf
Copy link
Contributor

nemethf commented Jul 20, 2024

There is one additional thing that I don't understand. The client needs to declare support for editor.action.rename, but the server sends rust-analyzer.rename instead of editor.action.rename. Is this intentional?

Thanks.

@Veykril
Copy link
Member

Veykril commented Jul 20, 2024

Same goes for the "editor.action.triggerParameterHints", capability. Thanks for calling that out I think we should move those over to rust-analyzer.* or similar. The names are based on the equivalent vscode commands but we should clearly not use the exact same names for those.

nemethf added a commit to nemethf/eglot-x that referenced this pull request Jul 30, 2024
The motivation is to trigger a rename after extract variable assist is
applied: rust-lang/rust-analyzer#17587

The implementation might need to be updated later:
rust-lang/rust-analyzer#17644

* eglot-x.el (eglot-x-client-commands): New defcustom.
(eglot-client-capabilities): Set experimental/commands based on the
defcustom.
(eglot-x--apply-text-edits): Sync with upstream version of
eglot--apply-text-edits, and remove heuristic to detect a snippet,
which is no longer needed.
(eglot-x-execute-command): New defun.  Handle the client command
"rust-analyzer.rename" locally.
(elgot-execute): New cl-defmethod.  Extend the default implementation
by calling eglot-x-execute-command for commands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants