Skip to content

Conversation

@predragnikolic
Copy link
Member

@predragnikolic predragnikolic commented Jun 25, 2024

closes #2199

Here is a example video:

Kapture.2024-06-25.at.18.24.36.mp4

Notice the import changing in file a.ts when we rename the b.ts file.

example.zip

@predragnikolic predragnikolic force-pushed the add-will-rename-and-did-rename branch from 11b3618 to 6f65462 Compare June 26, 2024 10:15
@predragnikolic
Copy link
Member Author

I will highlight things implemented from the LSP spec.

Renaming a document

Document renames should be signaled to a server sending a document close notification with the document’s old name followed by an open notification using the document’s new name.

That part of the spec can be seen on these two lines:

Note

The user could rename main.py to main.ts. It is much easer to just close the view and open it, than to keep the file open and try modify the view to switch the syntax, language id, and a few more things... which I initially did, but concluded that requires more code, so I switched to just closing and opening the view.

WillRenameFiles Request

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.
The request can return a WorkspaceEdit which will be applied to the workspace before the files are renamed.

That can be seen here:

# LSP spec - Apply WorkspaceEdit before the files are renamed
if res:
session.apply_workspace_edit_async(res, is_refactoring=True)
self.rename_path(old_path, new_path)

Please note that clients might drop results if computing the edit took too long or if a server constantly fails on this request. This is done to keep renames fast and reliable.

I didn't implement this.

DidRenameFiles Notification

When a rename happened, all sessions who have the workspace.fileOperations.didRename capability will get notified.

@predragnikolic
Copy link
Member Author

predragnikolic commented Jun 26, 2024

Here is what to expect from this PR.

  • There is a LSP: Rename File command in the command palette
    and LSP: Rename LSP: Rename File and LSP: Rename Folder in the sidebar.
  • Uses can rename file and folders. (folders can only be renamed from the sidebar).
  • When remaining a file user can rename a file example.py to hello.py, or to ./hello.py, or to ../../hello.py or to ./newDir/hello.py or to ./existingDir/hello.py. And it will work as expected.
  • The LSP: Rename File is always enabled, even if a session(with a 'workspace.fileOperations.willRename') doesn't exist.
    def is_enabled(self):
    return True

For session that don't support the 'workspace.fileOperations.willRename',
we will still rename the file and notify all sessions that have 'workspace.fileOperations.didRename'

@predragnikolic predragnikolic marked this pull request as ready for review June 27, 2024 10:15
@jwortmann
Copy link
Member

The built-in rename_file and rename_path commands are both implemented in Python in Default/rename.py and Default/side_bar.py. I wonder wouldn't it be better to just replace these commands, like LSP already does with show_scope_name? Here we could add on_window_command in

LSP/boot.py

Line 209 in 293f4a4

class Listener(sublime_plugin.EventListener):
to retarget the commands if a language server is running.

While for other features like document symbols or goto symbol, the similar built-in commands are not overridden because they can return different results as the LSP commands, so it might be useful to have both variants. But I think for the rename this is not necessary and to add two more LSP variants into the menus would not be optimal for usability. What do you think?

@predragnikolic
Copy link
Member Author

@jwortmann I tried to implement your suggestion on a different branch,
but I hit a bug with ST in which ST never emits a rename_file to on_window_command, although rename_file is a window command. It works fine for the rename_path command. My assumption is maybe because rename_file requires a TextInputHandler, but only ST core code can say whats missing.
You can see my attempt here

@predragnikolic
Copy link
Member Author

predragnikolic commented Jun 27, 2024

I will try a workaround sublimehq/sublime_text#2234 (comment)
EDIT: I can't apply the workaround, because the workaround needs to be added to Default/rename.py.

@predragnikolic
Copy link
Member Author

@jwortmann I liked the idea, that way we do not introducing new commands, instead relying on existing ones.
But because the on_window_command approach is buggy and needs a fix on ST side, I will quit from that idea now and stick with the approach in this PR.
I've subscribed to the ST issue, so once that is fixed I could rethink the approach.

…s from that folder to new location

so we do not lose changes

The ST default rename_path command doesn't do this
VS Code does this
LSP will do this
Copy link
Member

@rchl rchl left a comment

Choose a reason for hiding this comment

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

After all the years of work, I think it looks good now :)

@predragnikolic
Copy link
Member Author

There are two minor things worth addressing in this PR.

  1. If we rename a folder, while having many views open. The active view might change when restoring. We could add code to focus the last active view.

  2. If we rename a folder, when views are restored, views do not have the correct original order.

old_path is a full path - "/full/path/to/hello.txt"
new_name is not a full path - "./../hello.txt"
new_path is a full path - "/full/path/to/hello.txt"
self.window.views() returns views ordered from left to right. When we iterate to close the views in the first loop, because closing a view shifts the indices of all subsequent views (to its right), we must iterate and grab the views from right to left (in reversed order). This prevents the shifting indices from causing errors when calling get_view_index for the remaining views.

When we restore the views, we do the opposite: we iterate from left to right (in original order).
@predragnikolic
Copy link
Member Author

Thank you Rafal, Janos for the comments.
I think the PR is in a usable state.

and ignore the rest because required_view_version might throw an error
Initially it was implemented to be send to only one server

later in fbcbec5 it was changed to be send to all servers
the benefit is that multiple server can receive WorkspaceEdit,
however the downside is that apply workspace edits in the LSP can fail
if the document version changes.

So for now I will only send the request to one server
…onses. The response type for the request is WorkspaceEdit | null, so maybe there could be a case where only one of the servers responds with a WorkspaceEdit, and this way would increase the chance that we get at least one response which is not null
@predragnikolic
Copy link
Member Author

predragnikolic commented Dec 13, 2025

Here is a video that could be used to showcase this feature in the release notes:

  • Add support for willRename and didRename fileOperations
demo.mp4

Notice the import changing in file a.ts when we rename the b.ts file.

@predragnikolic
Copy link
Member Author

I will wait one day to see if there are some additional comments
after that I do plan to squash and merge this.

@jwortmann
Copy link
Member

Another idea that I just had:

it might not be obvious to users why there is a special "LSP: Rename Path" command for a simple rename operation and even when mentioned in the release notes, people might not remember it later. So maybe after applying the WorkspaceEdit, we can display a window.status_message() with a text like "Updated X references to new_name.ts in Y files." Then it would be more clear to users that the rename also affected the content of other files.

It might also be nice to add another section "Rename Path" with a short description in the docs after https://lsp.sublimetext.io/features/#rename.

@jwortmann
Copy link
Member

"Updated X references to new_name.ts in Y files."

Here X would be the total number of TextEdits and Y the number of different URIs in the WorkspaceEdit.
Currently session.apply_workspace_edit_asnc() returns a Promise[None]. We could refactor that method to return a Promise[tuple[int, int]] instead, where the two numbers are the TextEdit and URI counts. Ideally, these numbers should only count the edits/URIs where the changes were actually applied, i.e. not skipped due to

LSP/plugin/core/edit.py

Lines 61 to 63 in d5aefde

if not view.is_valid():
print('LSP: ignoring edits due to view not being open')
return Promise.resolve(None)

Though I'm not sure if it is possible to evaluate the correct number, because this method calls a TextCommand which then can also ignore edits at

LSP/plugin/edit.py

Lines 62 to 64 in d5aefde

if required_view_version is not None and required_view_version != view_version:
print('LSP: ignoring edit due to non-matching document version')
return

and I think there is no way to report the actual number of applied changes back to the caller.
Maybe for now it would be sufficient to just count the TextEdits and URIs that are in the WorkspaceEdit (i.e. regardless of whether they were applied or not).

Could be done in a different PR (just throwing in some ideas here).

@predragnikolic
Copy link
Member Author

+1 for adding docs


For the other improvements that were suggested by Janos, I do have something to share.

I experimented the previous week with adding the same prompt that we have for rename symbol.

prompt.mp4

I did not push the "prompt" code changes to Github, they will probably be introduced in a follow up PR.

The same goes true for the docs.


That said, I am fine if this PR stays open until I create the other two changes.

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.

Support workspace/willRenameFiles

7 participants