Skip to content

Make workspace/willRenameFiles working #2531

Open
przepompownia wants to merge 1 commit intophpactor:masterfrom
przepompownia:fix-will-rename
Open

Make workspace/willRenameFiles working #2531
przepompownia wants to merge 1 commit intophpactor:masterfrom
przepompownia:fix-will-rename

Conversation

@przepompownia
Copy link
Contributor

@przepompownia przepompownia commented Feb 4, 2024

Fixes #2522
Fixes #2523

@przepompownia
Copy link
Contributor Author

przepompownia commented Feb 4, 2024

@dantleech at the moment it seems to work on simple cases but

  • we need to verify if we need the part described in workspace/willRenameFiles - destination file must exist before #2522 (currently commented out)
  • I'm aware that it's badly designed at the moment - I mean using WorkspaceEdit directly in FileRenamer. Probably we need to define some equivalent of Phpactor\Rename\Model\LocatedTextEditsMap instead and convert it to WorkspaceEdit in the handler.

@przepompownia
Copy link
Contributor Author

To test it with Neovim (master) I use

local function willRenameFilesHandler(response)
  for clientId, clientResponse in pairs(response) do
    local client = vim.lsp.get_client_by_id(clientId)
    if nil == client then
      vim.notify(('Client %s does not exist'):format(clientId), vim.log.levels.ERROR, {title = 'LSP: workspace/willRenameFiles'})

      return
    end

    vim.lsp.util.apply_workspace_edit(clientResponse.result, client.offset_encoding)
    -- vim.print(clientResponse.result)
  end
end

local function willRenameCurrentBuffer(newPath)
  if not vim.startswith(newPath, '/') then
    newPath = vim.fs.joinpath(vim.uv.cwd(), newPath)
  end
  vim.lsp.buf_request_all(
    0,
    'workspace/willRenameFiles', 
    {
      files = { { oldUri = vim.uri_from_bufnr(0), newUri = vim.uri_from_fname(newPath) } },
    },
    willRenameFilesHandler
  )
end
vim.api.nvim_create_user_command('WillRenameCurrentBuffer', function (opts)
  willRenameCurrentBuffer(opts.args)
end, {nargs = 1, complete = 'file'})

@przepompownia przepompownia marked this pull request as ready for review February 25, 2024 03:49
@przepompownia
Copy link
Contributor Author

przepompownia commented Feb 25, 2024

@dantleech at the moment it seems to work on simple cases but

* we need to verify if we need the part described in [`workspace/willRenameFiles` - destination file must exist before #2522](https://github.com/phpactor/phpactor/issues/2522) (currently commented out)

* I'm aware that it's badly designed at the moment - I mean using `WorkspaceEdit` directly in `FileRenamer`. Probably we need to define some equivalent of `Phpactor\Rename\Model\LocatedTextEditsMap` instead and convert it to `WorkspaceEdit` in the handler.

At the moment both above things are done:

  • replaceDefinition() is needed to edit the source file itself before moving it (in the meaning of WorkspaceEdit changes)
  • to avoid reinvention of models and use RenameResult in handler I changed both the adapter and handler into a pattern used in ClassRenamer.

@przepompownia
Copy link
Contributor Author

Composing the summarized WorkspaceEdit instance from component instances can be controversial. It assumes (relying on LocatedTextEditConverter::toWorkspaceEdit implementation) that only documentChanges field is not empty in each of them and we can ignore other fields.

https://github.com/phpactor/phpactor/pull/2531/files#diff-39ca8889abb439b22964fc403e2e98d48192ef428529ede2bee62f84221faedfR60-R71

* @return Promise<LocatedTextEditsMap>
*/
public function renameFile(TextDocumentUri $from, TextDocumentUri $to): Promise
public function renameFile(TextDocumentUri $from, TextDocumentUri $to): Generator
Copy link
Collaborator

@dantleech dantleech Feb 26, 2024

Choose a reason for hiding this comment

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

why using a generator? in general public APIs should use Promise? can we switch back? that would also make the diff easier to read :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I switched to using generator in similar way like in ClassRenamer (please compare what happens there) to smuggle RenameResult through getReturn() and avoid changes in models (as I mentioned above).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using Promise would require some new class (Phpactor\Rename\Model\RenameEdit?) composed of LocatedTextEditsMap and RenameResult.

Copy link
Contributor Author

@przepompownia przepompownia Feb 27, 2024

Choose a reason for hiding this comment

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

RenameEdit class couldn't simply look like

<?php

namespace Phpactor\Rename\Model;

class RenameEdit
{
    public function __construct(
        public readonly LocatedTextEditsMap $locatedTextEditsMap,
        public readonly RenameResult $renameResult,
    ) {
    } 
}

because it'd loss the order of edits. Such class should be similar to WorkspaceEdit which allows ordered edits of different types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I converted the adapter to return Promise and created RenameEdit - initially as iterable class.

@dantleech
Copy link
Collaborator

dantleech commented Mar 2, 2024

I;d really need to test this against a real client implementation, I initially developed it against the CoC implementation - what implementation is there for NVIM LSP? Is it supported by VS code? What is the end state of this feature from your perspective? how would you use it?

@przepompownia
Copy link
Contributor Author

Honestly, at the moment I couldn't think of anything more than that using a simple prompt with path completion to send such request (like the example above) but as a nvim-tree user I could extend it like here

https://github.com/przepompownia/nvim-arctgx/blob/ef3fc1f4221bd82382abf1b5ff973debb415f651/lua/arctgx/pluginConfigs/nvim-tree.lua#L88-L101

I haven't tested it on nvim-oil yet maybe because built-in nvim feature vim.lsp.util.apply_workspace_edit (after recent fixes in master) performs WorkspaceEdit correctly.

Maybe I'll find some free time to install oil.nvim and CoC in separate environments. It's possible that some other clients do not apply RenameResult (but perform it independently) but it seems to be not a reason do skip it in in the response from the server.

The will rename files request is sent from the client to the server before files are actually renamed as long as the rename is triggered from within the client either by a user action or by applying a workspace edit.

https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_willRenameFiles

@przepompownia
Copy link
Contributor Author

przepompownia commented Mar 18, 2024

Intentionally I didn't merge the recent master here yet because of #2607.

@przepompownia
Copy link
Contributor Author

*/
class RenameEdit extends ArrayIterator
{
public function __construct(LocatedTextEditsMap|RenameResult|null ...$edits)
Copy link
Collaborator

Choose a reason for hiding this comment

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

why is this a variadic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only for enforcing types without using docblock.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, let's use the docblock then :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No problem, regardless, I'd like to know why you think this is probably bad practice (especially that in practice the constructor gets max two arguments).

Copy link
Collaborator

@dantleech dantleech Mar 30, 2024

Choose a reason for hiding this comment

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

variadics are fine but i'm confused: why is it an array at all and not LocatedTextEdits $edits, RenameResult $result ?

Copy link
Collaborator

@dantleech dantleech Apr 6, 2024

Choose a reason for hiding this comment

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

I'm still confused. The result of the Promise in lib/Rename/Adapter/ClassMover/FileRenamer.php is:

            return new WorkspaceRenameEdits([
                LocatedTextEditsMap::fromLocatedEdits($locatedEdits),
                new RenameResult($from, $to),
            ]);

i.e. why not:

            return new WorkspaceRenameEdits(
                LocatedTextEditsMap::fromLocatedEdits($locatedEdits),
                new RenameResult($from, $to),
            );

It doesn't need to be an array?

Copy link
Contributor Author

@przepompownia przepompownia Apr 6, 2024

Choose a reason for hiding this comment

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

It doesn't need to be an array?

It's (similarly to WorkspaceEdit's documentChanges elements) the only way to keep the order of operations here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just to clarify though, it's not needed now?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I mean, we are also passing an aggregate of located text edits, rather than the individual edits. So it's a bit ... weird. If this is gojg to be a list of operations then we should or bably not use the LocatedTextEditsMap here, as that' not an atomic operation.

Copy link
Contributor Author

@przepompownia przepompownia Apr 6, 2024

Choose a reason for hiding this comment

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

I agree that this separation is far from the best and even weird. I would like to leave some design assumptions to you, but I realize that this may mean a lot of reconstruction (and, in effect, wait probably long time). Please feel free to replace it with something better. I'm not attached in any way the current form of solution. It would be good to have working such LSP request.

@przepompownia
Copy link
Contributor Author

przepompownia commented Mar 30, 2024

I'm confused about why phpstan is complaining about changes unrelated to this PR after merge #2614

Edit: most of them were probably secondary to the few unsilenced.

$previous = $error->getPrevious();

$this->clientApi->window()->showMessage()->error(sprintf(
$error->getMessage() . ($previous?->getTraceAsString() ?? '')
Copy link
Collaborator

Choose a reason for hiding this comment

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

is sprintf missing a format string? %s %s why do we get the trace from $previous?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I copied catching CouldNotRename from RenameHandler to have the same way of error handling and don't know what the trace was needed for in the original place:

https://github.com/phpactor/phpactor/blame/a23130918b61c3f45f4a403abe56b5a06be80de9/lib/Extension/LanguageServerRename/Handler/RenameHandler.php#L93

@dantleech
Copy link
Collaborator

I've made a PR on your PR: przepompownia#1 would be great if you can verify that it's all working.

@z01cbrag
Copy link

Any updates on this? I'd love to have this feature ❤️

@dantleech
Copy link
Collaborator

Which client are you using that can take advantage of it?

@dantleech
Copy link
Collaborator

So there's this plugin for neovim at least which integrates with neo-tree and nvim-tree: https://github.com/antosha417/nvim-lsp-file-operations will try this out soon, sorry for the delay.

@z01cbrag
Copy link

There's also https://github.com/stevearc/oil.nvim, that's what I'm using and it has support for this.

@przepompownia przepompownia force-pushed the fix-will-rename branch 7 times, most recently from 6fc4bca to 1546a97 Compare December 26, 2025 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

workspace/willRenameFiles doesn't contain RenameFile part workspace/willRenameFiles - destination file must exist before

3 participants