This is an archive of the discontinued LLVM Phabricator instance.

[clangd] a prototype for triggering rename after extracting.
Needs ReviewPublic

Authored by hokein on Jul 25 2019, 1:03 AM.

Details

Reviewers
sammccall
Summary

This is a prototype patch of implementing a generic mechanism to perform a chain of refactorings.

An example (extract + rename) of how it works:

  1. the extract tweak returns a vector of steps ([applyEdit, rename]) during the apply stage.
  2. clangd sends a workspace/applyEdit to the editor and waits for the reply;
  3. the editor applies the edit and sends back a reply;
  4. clangd sends a custom request clangd.triggerRename to the editor (edito needs to implement the command);
  5. the editor executes the clangd.triggerRename request (triggers the LSP rename), and replies to clangd;
  6. clangd receives the reply, and there is no more steps, so the "apply tweak" workflow is finished;

A demo:

Event Timeline

hokein created this revision.Jul 25 2019, 1:03 AM
Herald added a project: Restricted Project. · View Herald TranscriptJul 25 2019, 1:03 AM
hokein edited the summary of this revision. (Show Details)Jul 25 2019, 1:06 AM

This is really cool! Hm, formatting is troublesome, well spotted :-)

As you're defining a generic mechanism, it'd be useful to have more examples to verify that this actually generalizes.
We might have to make them up (e.g. replace-auto-with-type-and-go-to-definition?)

If we can't show it generalizes, then I think this would still be useful enough just to dispense with generality and add a "renameAt" extension to CodeAction or something like that.

clang-tools-extra/clangd/Protocol.cpp
544 ↗(On Diff #211683)

(why would "lsp" be in these names - surely this is just rename?)

clang-tools-extra/clangd/Protocol.h
765 ↗(On Diff #211683)

(If there's precedent for others using a similar protocol, we should link to it)

777 ↗(On Diff #211683)

couldn't we send a range here?
That would allow the client to skip prepareRename and still use its native UI.
(In principle. no need to actually implement this in vscode)

807 ↗(On Diff #211683)

(mark extension here too).

894 ↗(On Diff #211683)

So there's a few places that the command could be specified:

  1. in the code action, as a thing to do at the end (above)
  2. as a parameter, when the client executes a command and the server calls applyWorkspaceEdit (here)
  3. as the return value from executeCommand

This patch implements 1 & 2. Why this choice? (easiest to prototype is a perfectly sensible answer)

1 is restricted to code actions.
2 & 3 are (practically) restricted to executeCommand.
2 is restricted to when edits are applied.

(To me, implementing just 1 or just 3 seem like the strongest options...)

hokein marked 4 inline comments as done.Jul 25 2019, 5:17 AM
hokein added inline comments.
clang-tools-extra/clangd/Protocol.h
777 ↗(On Diff #211683)

rename just uses the position as its parameter, not range.

807 ↗(On Diff #211683)

ah, this is not used, remove.

894 ↗(On Diff #211683)

sorry, this patch actually implements just 2 (the clientCommand was added accidentally in the CodeAction, removing now).

for 1 -- it has a limitation, at the time when clangd sends CodeAction, we only run the tweak::prepare, at this time we might not get enough information (AST etc) to compute a client command.

for 3 -- LSP doesn't say much about how to use return value from executeCommand, it just suggests servers to send applyEdit request to the server. I believe most of LSP clients (at least VSCode) just throw the return value away.

If we are going down that path, could we consider to pre-selecting a name of the extract variable and its usage (using multi-select)?
I.e. the optimal workflow seems to be ([[code]] marks selected regions):

// Input:
foo([[10+10]]);


// 'extract' turns this into the following code (and selects for editors that support this):
int [[dummy]] = 10+10;
foo([[dummy]]);

At this point users can type a new name and get it updated directly.
There are a bunch of reasons why running the full-blown rename is suboptimal:

  • rename can fail, e.g. if you had other variables called dummy in this scope or if we actions happen to produce broken AST,
  • rename UI is generally clunkier than multi-selection (a new window is opened, this moves focus from the code to the editor).
  • rename has to wait for the AST, in general case this might result in high latency for actions that can add an #include or if we any of the included files changed.

The obvious drawback is that this workflow might be hard to implement in some editors, but both Vim and VSCode should be able to provide a decent experience here.

hokein updated this revision to Diff 212345.Jul 30 2019, 7:49 AM
hokein edited the summary of this revision. (Show Details)

reimplement the mechanism based on the offline discussion.

hokein updated this revision to Diff 212347.Jul 30 2019, 7:54 AM

update the diff

hokein edited the summary of this revision. (Show Details)Jul 30 2019, 8:06 AM

If we are going down that path, could we consider to pre-selecting a name of the extract variable and its usage (using multi-select)?

I agree that doing rename after extraction is not an ideal approach (both correctness and latency), there should be other better ways to achieve this.
However as a LSP server, we may want to align with the community and other language servers (typescript, java) in order to provide consistent refactoring experience across languages (vscode prefers to do extract with a default name and rename afterwards). Note that this patch aims to implement a generic mechanism to perform a chain of small refactorings (extract + rename is just one example), we still need this no match which way we're going to perform.

The patch is still missing tests, but should be good for early comments.

I agree that doing rename after extraction is not an ideal approach (both correctness and latency), there should be other better ways to achieve this.
However as a LSP server, we may want to align with the community and other language servers (typescript, java) in order to provide consistent refactoring experience across languages (vscode prefers to do extract with a default name and rename afterwards).

Java and Typescript can pretty much guarantee (local) rename is fast and latency is never an issue for those languages.
Either approach seems to require LSP extensions. Pre-select seems to provide better UX , that's the reason I'm proposing to look in that direction.

Note that this patch aims to implement a generic mechanism to perform a chain of small refactorings (extract + rename is just one example), we still need this no match which way we're going to perform.

Totally agree.