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
2 changes: 2 additions & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from .plugin.documents import DocumentSyncListener
from .plugin.documents import TextChangeListener
from .plugin.edit import LspApplyDocumentEditCommand
from .plugin.edit import LspApplyWorkspaceChangesCommand
from .plugin.execute_command import LspExecuteCommand
from .plugin.folding_range import LspFoldAllCommand
from .plugin.folding_range import LspFoldCommand
Expand All @@ -68,6 +69,7 @@
from .plugin.panels import LspUpdateLogPanelCommand
from .plugin.panels import LspUpdatePanelCommand
from .plugin.references import LspSymbolReferencesCommand
from .plugin.rename import LspHideRenameButtonsCommand
from .plugin.rename import LspSymbolRenameCommand
from .plugin.save_command import LspSaveAllCommand
from .plugin.save_command import LspSaveCommand
Expand Down
3 changes: 3 additions & 0 deletions plugin/core/panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class PanelName:
class PanelManager:
def __init__(self, window: sublime.Window) -> None:
self._window = window
self.rename_panel_buttons = None # type: Optional[sublime.PhantomSet]

def destroy_output_panels(self) -> None:
for field in filter(lambda a: not a.startswith('__'), PanelName.__dict__.keys()):
Expand Down Expand Up @@ -91,6 +92,8 @@ def _create_panel(self, name: str, result_file_regex: str, result_line_regex: st
panel = self.create_output_panel(name)
if not panel:
return None
if name == PanelName.Rename:
self.rename_panel_buttons = sublime.PhantomSet(panel, "lsp_rename_buttons")
settings = panel.settings()
if result_file_regex:
settings.set("result_file_regex", result_file_regex)
Expand Down
7 changes: 4 additions & 3 deletions plugin/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __str__(self) -> str:
return "invalid URI scheme: {}".format(self.uri)


def get_line(window: sublime.Window, file_name: str, row: int) -> str:
def get_line(window: sublime.Window, file_name: str, row: int, strip: bool = True) -> str:
'''
Get the line from the buffer if the view is open, else get line from linecache.
row - is 0 based. If you want to get the first line, you should pass 0.
Expand All @@ -95,11 +95,12 @@ def get_line(window: sublime.Window, file_name: str, row: int) -> str:
if view:
# get from buffer
point = view.text_point(row, 0)
return view.substr(view.line(point)).strip()
line = view.substr(view.line(point))
else:
# get from linecache
# linecache row is not 0 based, so we increment it by 1 to get the correct line.
return linecache.getline(file_name, row + 1).strip()
line = linecache.getline(file_name, row + 1)
return line.strip() if strip else line


def get_storage_path() -> str:
Expand Down
13 changes: 13 additions & 0 deletions plugin/edit.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from .core.edit import parse_range
from .core.edit import WorkspaceChanges
from .core.logging import debug
from .core.protocol import TextEdit
from .core.registry import LspWindowCommand
from .core.typing import List, Optional, Any, Generator, Iterable, Tuple
from contextlib import contextmanager
import operator
Expand All @@ -24,6 +27,16 @@ def temporary_setting(settings: sublime.Settings, key: str, val: Any) -> Generat
settings.set(key, prev_val)


class LspApplyWorkspaceChangesCommand(LspWindowCommand):

def run(self, session_name: str, workspace_changes: WorkspaceChanges) -> None:
session = self.session_by_name(session_name)
if not session:
debug('Could not find session', session_name)
return
session.apply_parsed_workspace_edits(workspace_changes)


class LspApplyDocumentEditCommand(sublime_plugin.TextCommand):
re_placeholder = re.compile(r'\$(0|\{0:([^}]*)\})')

Expand Down
149 changes: 130 additions & 19 deletions plugin/rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,48 @@
import sublime_plugin


BUTTON_HTML = """
<style>
html {{
padding: 0.2rem;
background-color: transparent;
}}
a {{
display: inline;
jwortmann marked this conversation as resolved.
Show resolved Hide resolved
line-height: 1.5rem;
padding-left: 0.6rem;
padding-right: 0.6rem;
border-width: 1px;
border-style: solid;
border-color: #fff4;
border-radius: 4px;
color: #cccccc;
background-color: color({color} min-contrast(white 6.0));
text-decoration: none;
}}
html.light a {{
border-color: #000a;
color: white;
background-color: color({color_light} min-contrast(white 6.0));
}}
</style>
<body id='lsp-button'>
<a href='{command}'>{label}</a>
</body>"""

DISCARD_BUTTON_HTML = BUTTON_HTML.format(
label="Discard",
command=sublime.command_url('chain', {
'commands': [
['hide_panel', {}],
['lsp_hide_rename_buttons', {}]
]
}),
color="#3f3f3f",
color_light="#636363"
)


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

Expand Down Expand Up @@ -134,17 +176,17 @@ def _on_rename_result_async(self, session: Session, response: Optional[Workspace
if not response:
return session.window.status_message('Nothing to rename')
changes = parse_workspace_edit(response)
count = len(changes.keys())
if count == 1:
file_count = len(changes.keys())
if file_count == 1:
session.apply_parsed_workspace_edits(changes)
return
total_changes = sum(map(len, changes.values()))
message = "Replace {} occurrences across {} files?".format(total_changes, count)
choice = sublime.yes_no_cancel_dialog(message, "Replace", "Dry Run")
message = "Replace {} occurrences across {} files?".format(total_changes, file_count)
choice = sublime.yes_no_cancel_dialog(message, "Replace", "Preview", title="Rename")
if choice == sublime.DIALOG_YES:
session.apply_parsed_workspace_edits(changes)
elif choice == sublime.DIALOG_NO:
self._render_rename_panel(changes, total_changes, count)
self._render_rename_panel(changes, total_changes, file_count, session.config.name)

def _on_prepare_result(self, pos: int, response: Optional[PrepareRenameResult]) -> None:
if response is None:
Expand Down Expand Up @@ -172,39 +214,94 @@ def _get_relative_path(self, file_path: str) -> str:
base_dir = wm.get_project_path(file_path)
return os.path.relpath(file_path, base_dir) if base_dir else file_path

def _render_rename_panel(self, changes_per_uri: WorkspaceChanges, total_changes: int, file_count: int) -> None:
def _render_rename_panel(
self,
changes_per_uri: WorkspaceChanges,
total_changes: int,
file_count: int,
session_name: str
) -> None:
wm = windows.lookup(self.view.window())
if not wm:
return
panel = wm.panel_manager and wm.panel_manager.ensure_rename_panel()
pm = wm.panel_manager
if not pm:
return
panel = pm.ensure_rename_panel()
if not panel:
return
to_render = [] # type: List[str]
reference_document = [] # type: List[str]
header_lines = "{} changes across {} files.\n\n \n".format(total_changes, file_count)
to_render.append(header_lines)
reference_document.append(header_lines)
ROWCOL_PREFIX = " {:>4}:{:<4} {}"
for uri, (changes, _) in changes_per_uri.items():
scheme, file = parse_uri(uri)
if scheme == "file":
to_render.append('{}:'.format(self._get_relative_path(file)))
else:
to_render.append('{}:'.format(uri))
filename_line = '{}:'.format(self._get_relative_path(file) if scheme == 'file' else uri)
to_render.append(filename_line)
reference_document.append(filename_line)
for edit in changes:
start = parse_range(edit['range']['start'])
if scheme == "file":
line_content = get_line(wm.window, file, start[0])
start_row, start_col_utf16 = parse_range(edit['range']['start'])
line_content = get_line(wm.window, file, start_row, strip=False) if scheme == 'file' else \
'<no preview available>'
original_line = ROWCOL_PREFIX.format(start_row + 1, start_col_utf16 + 1, line_content.strip() + "\n")
reference_document.append(original_line)
if scheme == "file" and line_content:
end_row, end_col_utf16 = parse_range(edit['range']['end'])
new_text_rows = edit['newText'].split('\n')
# TODO: string slicing is not compatible with UTF-16 column numbers
new_line_content = line_content[:start_col_utf16] + new_text_rows[0]
if start_row == end_row and len(new_text_rows) == 1 and end_col_utf16 < len(line_content):
new_line_content += line_content[end_col_utf16:]
to_render.append(
ROWCOL_PREFIX.format(start_row + 1, start_col_utf16 + 1, new_line_content.strip() + "\n"))
else:
line_content = '<no preview available>'
to_render.append(" {:>4}:{:<4} {}".format(start[0] + 1, start[1] + 1, line_content))
to_render.append("") # this adds a spacing between filenames
to_render.append(original_line)
characters = "\n".join(to_render)
base_dir = wm.get_project_path(self.view.file_name() or "")
panel.settings().set("result_base_dir", base_dir)
panel.run_command("lsp_clear_panel")
wm.window.run_command("show_panel", {"panel": "output.rename"})
fmt = "{} changes across {} files.\n\n{}"
panel.run_command('append', {
'characters': fmt.format(total_changes, file_count, characters),
'characters': characters,
'force': True,
'scroll_to_end': False
})
panel.set_reference_document("\n".join(reference_document))
selection = panel.sel()
selection.add(sublime.Region(0, panel.size()))
panel.run_command('toggle_inline_diff')
selection.clear()
buttons = pm.rename_panel_buttons
rchl marked this conversation as resolved.
Show resolved Hide resolved
if not buttons:
return
buttons_position = len(to_render[0]) - 2
APPLY_BUTTON_HTML = BUTTON_HTML.format(
label="Apply",
command=sublime.command_url('chain', {
'commands': [
[
'lsp_apply_workspace_changes',
{'session_name': session_name, 'workspace_changes': changes_per_uri}
],
[
'hide_panel',
{}
],
[
'lsp_hide_rename_buttons',
{}
]
]
}),
color="var(--accent)",
color_light="var(--accent)"
)
buttons.update([
sublime.Phantom(sublime.Region(buttons_position), APPLY_BUTTON_HTML, sublime.LAYOUT_INLINE),
sublime.Phantom(sublime.Region(buttons_position + 1), DISCARD_BUTTON_HTML, sublime.LAYOUT_INLINE)
])
jwortmann marked this conversation as resolved.
Show resolved Hide resolved


class RenameSymbolInputHandler(sublime_plugin.TextInputHandler):
Expand All @@ -226,3 +323,17 @@ def initial_text(self) -> str:

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


class LspHideRenameButtonsCommand(sublime_plugin.WindowCommand):

def run(self) -> None:
wm = windows.lookup(self.window)
if not wm:
return
pm = wm.panel_manager
if not pm:
return
buttons = pm.rename_panel_buttons
if buttons:
buttons.update([])
rchl marked this conversation as resolved.
Show resolved Hide resolved