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

Enhancements for the rename panel #2428

Merged
merged 16 commits into from
Mar 12, 2024
Merged

Conversation

jwortmann
Copy link
Member

@jwortmann jwortmann commented Mar 3, 2024

  1. Currently the "Dry Run" functionality for a multi-file rename operation is a bit awkward to use, because after you have looked at the rename locations which are shown in the panel, you need to run the rename command again, including typing of the new name, to actually do the rename. This PR adds "Apply" and "Discard" buttons into the panel to save a bit of time. In theory there could be wrong changes applied if you edit the view(s) first, before clicking the "Apply" button (because the document version is not mandatory in the LSP edits), but in that case I'd consider it to be the user's fault (or does anyone have an idea how to prevent it?). The buttons will automatically hide after they are clicked.

  2. Changes are now displayed in the panel using Sublime's inline diff feature. Perhaps it could be reused later to add a preview for other "refactor" code actions.

    Small drawback: the diff rendering requires more vertical space (an empty line after each line is needed because otherwise ST diff rendering would combine the lines into a single block). But is it really a drawback? I think it's okay and the diff rendering is useful because it shows what will change on the line (admittedly the changes are obvious for a simple rename, but still...)

  3. I've also adjusted the dialog labels:

    Before:

    before

    After:

    after

    Could someone on Mac/Linux confirm that passing the title parameter doesn't throw an error - I assume that it is simply ignored on Mac/Linux (?)

    Title for the dialog. Windows only.

@predragnikolic
Copy link
Member

predragnikolic commented Mar 3, 2024

Could someone on Mac/Linux confirm that passing the title parameter doesn't throw an error - I assume that it is simply ignored on Mac/Linux (?)

I am using OpenBox,
2024-03-03-150018_645x237_scrot

Copy link
Member

@predragnikolic predragnikolic left a comment

Choose a reason for hiding this comment

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

I find this improvement useful.

I saw the todo for the utf16 column but I do not have suggestions for that.

@jwortmann
Copy link
Member Author

jwortmann commented Mar 3, 2024

I am using OpenBox,

Thanks, then it seems to work as expected.

I've also considered to remove the dialog entirely and always show the preview panel, because then it's still only 1 button click to apply the changes. But in the dialog, the "Replace" button is preselected (at least on Windows), so you only need to hit Enter. And this is not possible in the preview panel, so I rejected this idea.

I saw the todo for the utf16 column but I do not have suggestions for that.

Yes, it was basically meant as a note that there is the possibility of the preview not being 100% accurate. But it's probably not so relevant in practice. To fix it, I think we would need to iterate through each character in the string to check if it's not a single UTF-16 character (like an emoji), but I don't think that would be worth it just for the preview. We could maybe reconsider if users start to complain.

Update: it's fixed now.

@rwols
Copy link
Member

rwols commented Mar 3, 2024

In theory there could be wrong changes applied if you edit the view(s) first, before clicking the "Apply" button (because the document version is not mandatory in the LSP edits), but in that case I'd consider it to be the user's fault (or does anyone have an idea how to prevent it?)

Maybe track the relevant view.change_count()?

@jwortmann
Copy link
Member Author

Maybe track the relevant view.change_count()?

Hm... but here the change count of all affected views is relevant, i.e. if the content of any of the views changes, we want to reject the whole rename operation alltogether, and not only ignore the edits for the changed view(s). This is probably not so trivial to implement.

I've tried to add the version information to each of the documents in the WorkspaceEdit like this

expand
diff --git a/plugin/core/edit.py b/plugin/core/edit.py
index 48e509df..2633f5af 100644
--- a/plugin/core/edit.py
+++ b/plugin/core/edit.py
@@ -4,13 +4,14 @@ from .protocol import TextEdit
 from .protocol import UINT_MAX
 from .protocol import WorkspaceEdit
 from .typing import List, Dict, Optional, Tuple
+from .url import parse_uri
 import sublime
 
 
 WorkspaceChanges = Dict[str, Tuple[List[TextEdit], Optional[int]]]
 
 
-def parse_workspace_edit(workspace_edit: WorkspaceEdit) -> WorkspaceChanges:
+def parse_workspace_edit(workspace_edit: WorkspaceEdit, window: Optional[sublime.Window] = None) -> WorkspaceChanges:
     changes = {}  # type: WorkspaceChanges
     document_changes = workspace_edit.get('documentChanges')
     if isinstance(document_changes, list):
@@ -22,16 +23,29 @@ def parse_workspace_edit(workspace_edit: WorkspaceEdit) -> WorkspaceChanges:
             text_document = document_change["textDocument"]
             uri = text_document['uri']
             version = text_document.get('version')
+            if window and version is None:
+                version = _current_view_version(window, uri)
             edits = document_change.get('edits')
             changes.setdefault(uri, ([], version))[0].extend(edits)
     else:
         raw_changes = workspace_edit.get('changes')
         if isinstance(raw_changes, dict):
             for uri, edits in raw_changes.items():
-                changes[uri] = (edits, None)
+                version = None
+                if window:
+                    version = _current_view_version(window, uri)
+                changes[uri] = (edits, version)
     return changes
 
 
+def _current_view_version(window: sublime.Window, uri: str) -> Optional[int]:
+    scheme, filepath = parse_uri(uri)
+    if scheme == 'file':
+        view = window.find_open_file(filepath)
+        return view.change_count() if view else 0
+    return None
+
+
 def parse_range(range: Position) -> Tuple[int, int]:
     return range['line'], min(UINT_MAX, range['character'])
 
diff --git a/plugin/rename.py b/plugin/rename.py
index d50f9d26..16822a9f 100644
--- a/plugin/rename.py
+++ b/plugin/rename.py
@@ -175,7 +175,7 @@ class LspSymbolRenameCommand(LspTextCommand):
     def _on_rename_result_async(self, session: Session, response: Optional[WorkspaceEdit]) -> None:
         if not response:
             return session.window.status_message('Nothing to rename')
-        changes = parse_workspace_edit(response)
+        changes = parse_workspace_edit(response, self.view.window())
         file_count = len(changes.keys())
         if file_count == 1:
             session.apply_parsed_workspace_edits(changes)

but it's useless because it will still apply the edits to the views where the version is matching. This means that the code will be broken, because only some of the symbols were renamed.

So this looks like it would need a few bigger changes/refactorings and is probably better suitable for another PR.

And to be fair, the same problem is also present in the current code, because the dialog is not a modal dialog (at least on Windows). If you make changes to an affected view before clicking on "Replace" in the dialog, you still end up with messed up edits.

@jwortmann
Copy link
Member Author

@rchl I wonder if you want to take a look too / have any opinions about it, or can we merge this?

I have a few other ideas and refactorings on top of this PR in mind (e.g. convert LspApplyWorkspaceChangesCommand -> LspApplyWorkspaceEditCommand taking a regular LSP WorkspaceEdit instead of our custom WorkspaceChanges), but I'd rather leave that for another PR.

@rchl
Copy link
Member

rchl commented Mar 5, 2024

Hey. Would like to check it out but I'm busy with life at the moment. Will try checking it out soon.

@jwortmann
Copy link
Member Author

No problem, we can leave the PR open for a while 👍

Copy link

netlify bot commented Mar 6, 2024

Deploy Preview for sublime-lsp ready!

Name Link
🔨 Latest commit cb9fd8f
🔍 Latest deploy log https://app.netlify.com/sites/sublime-lsp/deploys/65e8cfe925aa14000933a6d7
😎 Deploy Preview https://deploy-preview-2428--sublime-lsp.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

def is_range_response(result: PrepareRenameResult) -> TypeGuard[Range]:
return 'start' in result


def utf16_to_code_points(s: str, col: int) -> int:
Copy link
Member

Choose a reason for hiding this comment

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

maybe the naming could be a bit more clear that it's about position like utf_16_pos_to_code_point_pos?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't really feel utf_16_pos_to_code_point_pos. Could we keep the current name? I have reworded the docstring a little bit, and I think it's clear from the docstring and from usage what the function does.

Btw, the parameter is named col because that's the name given in the View.text_point_utf16 method.

Copy link
Member

@rchl rchl Mar 10, 2024

Choose a reason for hiding this comment

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

The name to me strongly suggests something that feels like charset conversion which I think is kinda confusing.

Can we do utf16_offset_to_code_point_offset? Or if we want to match ST more then utf16_point_to_code_point?

Copy link
Member

Choose a reason for hiding this comment

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

I'm willing to give up if you insist. I'm after all nit picking a bit.

Copy link
Member Author

@jwortmann jwortmann Mar 11, 2024

Choose a reason for hiding this comment

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

Well, I'd say "code points" already implies that it's about the position, because there is no character encoding named "code points". If we want to have the word "position" in the name, then I would suggest utf16_to_utf32_position.

If we want to mach ST closely, then it should probably be code_point_utf16 or even the same name text_point_utf16 (but the latter would be confusing I guess). The only difference between the builtin and this function here is that the builtin counts from the beginning of the view's content, while this one takes the string as an explicit parameter, and it always uses clamp=True.

So I would prefer one of these:

  • utf16_to_code_points (current)
  • utf16_to_utf32_position
  • code_point_utf16

Copy link
Member

Choose a reason for hiding this comment

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

kinda like utf16_to_utf32_position but choose yourself whether to rename it or not later

plugin/rename.py Outdated Show resolved Hide resolved
plugin/rename.py Outdated Show resolved Hide resolved
plugin/rename.py Outdated Show resolved Hide resolved
plugin/rename.py Outdated Show resolved Hide resolved
plugin/rename.py Outdated Show resolved Hide resolved
plugin/rename.py Outdated Show resolved Hide resolved
plugin/rename.py Show resolved Hide resolved
plugin/rename.py Outdated Show resolved Hide resolved
@rchl rchl merged commit 983df8b into sublimelsp:main Mar 12, 2024
8 checks passed
@jwortmann jwortmann deleted the rename-preview branch March 12, 2024 21:21
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.

4 participants