Skip to content

Commit

Permalink
Fix prepareRename support (#2127)
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl authored Dec 11, 2022
1 parent 29c519f commit 472187b
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 83 deletions.
8 changes: 7 additions & 1 deletion plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from .protocol import LSPObject
from .protocol import MarkupKind
from .protocol import Notification
from .protocol import PrepareSupportDefaultBehavior
from .protocol import PublishDiagnosticsParams
from .protocol import Range
from .protocol import Request
Expand Down Expand Up @@ -345,7 +346,8 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor
},
"rename": {
"dynamicRegistration": True,
"prepareSupport": True
"prepareSupport": True,
"prepareSupportDefaultBehavior": PrepareSupportDefaultBehavior.Identifier,
},
"colorProvider": {
"dynamicRegistration": True # exceptional
Expand Down Expand Up @@ -600,6 +602,10 @@ def sessions_async(self, capability_path: Optional[str] = None) -> Generator['Se
def session_views_async(self) -> Iterable['SessionViewProtocol']:
raise NotImplementedError()

@abstractmethod
def purge_changes_async(self) -> None:
raise NotImplementedError()

@abstractmethod
def on_session_initialized_async(self, session: 'Session') -> None:
raise NotImplementedError()
Expand Down
183 changes: 101 additions & 82 deletions plugin/rename.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,60 @@
from .core.edit import parse_workspace_edit
from .core.edit import TextEditTuple
from .core.protocol import PrepareRenameResult
from .core.protocol import Range
from .core.protocol import Request
from .core.registry import get_position
from .core.registry import LspTextCommand
from .core.registry import windows
from .core.sessions import Session
from .core.typing import Any, Optional, Dict, List
from .core.typing import Any, Optional, Dict, List, TypeGuard
from .core.url import parse_uri
from .core.views import first_selection_region
from .core.views import get_line
from .core.views import range_to_region
from .core.views import text_document_position_params
import functools
from functools import partial
import os
import sublime
import sublime_plugin


class RenameSymbolInputHandler(sublime_plugin.TextInputHandler):
def want_event(self) -> bool:
return False

def __init__(self, view: sublime.View, placeholder: str) -> None:
self.view = view
self._placeholder = placeholder

def name(self) -> str:
return "new_name"

def placeholder(self) -> str:
return self._placeholder

def initial_text(self) -> str:
return self.placeholder()

def validate(self, name: str) -> bool:
return len(name) > 0

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


# The flow of this command is fairly complicated so it deserves some documentation.
#
# When "LSP: Rename" is triggered from the Command Palette, the flow can go one of two ways:
#
# 1. Session doesn't have support for "prepareProvider":
# - input() gets called with empty "args" - returns an instance of "RenameSymbolInputHandler"
# - input overlay triggered
# - user enters new name and confirms
# - run() gets called with "new_name" argument
# - rename is performed
#
# 2. Session has support for "prepareProvider":
# - input() gets called with empty "args" - returns None
# - run() gets called with no arguments
# - "prepare" request is triggered on the session
# - based on the "prepare" response, the "placeholder" value is computed
# - "lsp_symbol_rename" command is re-triggered with computed "placeholder" argument
# - run() gets called with "placeholder" argument set
# - run() manually throws a TypeError
# - input() gets called with "placeholder" argument set - returns an instance of "RenameSymbolInputHandler"
# - input overlay triggered
# - user enters new name and confirms
# - run() gets called with "new_name" argument
# - rename is performed
#
# Note how triggering the command programmatically triggers run() first while when triggering the command from
# the Command Palette the input() gets called first.

class LspSymbolRenameCommand(LspTextCommand):

capability = 'renameProvider'

# mypy: Signature of "is_enabled" incompatible with supertype "LspTextCommand"
def is_enabled( # type: ignore
self,
new_name: str = "",
placeholder: str = "",
position: Optional[int] = None,
event: Optional[dict] = None,
point: Optional[int] = None
) -> bool:
if self.best_session("renameProvider.prepareProvider"):
# The language server will tell us if the selection is on a valid token.
return True
return super().is_enabled(event, point)

def is_visible(
self,
new_name: str = "",
Expand All @@ -65,24 +64,28 @@ def is_visible(
point: Optional[int] = None
) -> bool:
if self.applies_to_context_menu(event):
return self.is_enabled(new_name, placeholder, position, event, point)
return self.is_enabled(event, point)
return True

def input(self, args: dict) -> Optional[sublime_plugin.TextInputHandler]:
if "new_name" not in args:
placeholder = args.get("placeholder", "")
if not placeholder:
point = args.get("point")
# guess the symbol name
if not isinstance(point, int):
region = first_selection_region(self.view)
if region is None:
return None
point = region.b
placeholder = self.view.substr(self.view.word(point))
return RenameSymbolInputHandler(self.view, placeholder)
else:
if "new_name" in args:
# Defer to "run" and trigger rename.
return None
prepare_provider_session = self.best_session("renameProvider.prepareProvider")
if prepare_provider_session and "placeholder" not in args:
# Defer to "run" and trigger "prepare" request.
return None
placeholder = args.get("placeholder", "")
if not placeholder:
point = args.get("point")
# guess the symbol name
if not isinstance(point, int):
region = first_selection_region(self.view)
if region is None:
return None
point = region.b
placeholder = self.view.substr(self.view.word(point))
return RenameSymbolInputHandler(self.view, placeholder)

def run(
self,
Expand All @@ -93,29 +96,23 @@ def run(
event: Optional[dict] = None,
point: Optional[int] = None
) -> None:
if position is None:
tmp_pos = get_position(self.view, event, point)
if tmp_pos is None:
listener = self.get_listener()
if listener:
listener.purge_changes_async()
location = position if position is not None else get_position(self.view, event, point)
prepare_provider_session = self.best_session("renameProvider.prepareProvider")
if new_name or placeholder or not prepare_provider_session:
if location is not None and new_name:
self._do_rename(location, new_name)
return
pos = tmp_pos
if new_name:
return self._do_rename(pos, new_name)
else:
session = self.best_session("{}.prepareProvider".format(self.capability))
if session:
params = text_document_position_params(self.view, pos)
request = Request("textDocument/prepareRename", params, self.view, progress=True)
self.event = event
session.send_request(request, lambda r: self.on_prepare_result(r, pos), self.on_prepare_error)
else:
# trigger InputHandler manually
raise TypeError("required positional argument")
else:
if new_name:
return self._do_rename(position, new_name)
else:
# trigger InputHandler manually
raise TypeError("required positional argument")
# Trigger InputHandler manually.
raise TypeError("required positional argument")
if location is None:
return
params = text_document_position_params(self.view, location)
request = Request("textDocument/prepareRename", params, self.view, progress=True)
prepare_provider_session.send_request(
request, partial(self._on_prepare_result, location), self._on_prepare_error)

def _do_rename(self, position: int, new_name: str) -> None:
session = self.best_session(self.capability)
Expand All @@ -128,7 +125,7 @@ def _do_rename(self, position: int, new_name: str) -> None:
"newName": new_name,
}
request = Request("textDocument/rename", params, self.view, progress=True)
session.send_request(request, functools.partial(self._on_rename_result_async, session))
session.send_request(request, partial(self._on_rename_result_async, session))

def _on_rename_result_async(self, session: Session, response: Any) -> None:
if not response:
Expand All @@ -146,22 +143,23 @@ def _on_rename_result_async(self, session: Session, response: Any) -> None:
elif choice == sublime.DIALOG_NO:
self._render_rename_panel(changes, total_changes, count)

def on_prepare_result(self, response: Any, pos: int) -> None:
def _on_prepare_result(self, pos: int, response: Optional[PrepareRenameResult]) -> None:
if response is None:
sublime.error_message("The current selection cannot be renamed")
return
# It must be a dict at this point.
if "placeholder" in response:
placeholder = response["placeholder"]
r = response["range"]
if is_range_response(response):
r = range_to_region(response, self.view)
placeholder = self.view.substr(r)
pos = r.a
elif "placeholder" in response:
placeholder = response["placeholder"] # type: ignore
pos = range_to_region(response["range"], self.view).a # type: ignore
else:
placeholder = self.view.substr(self.view.word(pos))
r = response
region = range_to_region(r, self.view)
args = {"placeholder": placeholder, "position": region.a, "event": self.event}
args = {"placeholder": placeholder, "position": pos}
self.view.run_command("lsp_symbol_rename", args)

def on_prepare_error(self, error: Any) -> None:
def _on_prepare_error(self, error: Any) -> None:
sublime.error_message("Rename error: {}".format(error["message"]))

def _get_relative_path(self, file_path: str) -> str:
Expand Down Expand Up @@ -209,3 +207,24 @@ def _render_rename_panel(
'force': True,
'scroll_to_end': False
})


class RenameSymbolInputHandler(sublime_plugin.TextInputHandler):
def want_event(self) -> bool:
return False

def __init__(self, view: sublime.View, placeholder: str) -> None:
self.view = view
self._placeholder = placeholder

def name(self) -> str:
return "new_name"

def placeholder(self) -> str:
return self._placeholder

def initial_text(self) -> str:
return self.placeholder()

def validate(self, name: str) -> bool:
return len(name) > 0

0 comments on commit 472187b

Please sign in to comment.