From f5ec6c59f179c1dff93c6b2a25f9286201600e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 25 Jun 2024 15:50:17 +0200 Subject: [PATCH 01/37] add willRename and didRename fileOperations --- Default.sublime-commands | 4 ++ boot.py | 2 + plugin/core/protocol.py | 8 ++++ plugin/core/sessions.py | 5 +++ plugin/rename_file.py | 89 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 plugin/rename_file.py diff --git a/Default.sublime-commands b/Default.sublime-commands index 75bb04d95..2f64684d2 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -151,6 +151,10 @@ "caption": "LSP: Rename", "command": "lsp_symbol_rename" }, + { + "caption": "LSP: Rename File", + "command": "lsp_rename_file" + }, { "caption": "LSP: Code Action", "command": "lsp_code_actions" diff --git a/boot.py b/boot.py index 56ae078db..6ff25cec8 100644 --- a/boot.py +++ b/boot.py @@ -69,6 +69,7 @@ from .plugin.references import LspSymbolReferencesCommand from .plugin.rename import LspHideRenameButtonsCommand from .plugin.rename import LspSymbolRenameCommand +from .plugin.rename_file import LspRenameFileCommand from .plugin.save_command import LspSaveAllCommand from .plugin.save_command import LspSaveCommand from .plugin.selection_range import LspExpandSelectionCommand @@ -146,6 +147,7 @@ "LspSymbolImplementationCommand", "LspSymbolReferencesCommand", "LspSymbolRenameCommand", + "LspRenameFileCommand", "LspSymbolTypeDefinitionCommand", "LspToggleCodeLensesCommand", "LspToggleHoverPopupsCommand", diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 9cf429aec..759fe99ba 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -6064,6 +6064,10 @@ def colorPresentation(cls, params: ColorPresentationParams, view: sublime.View) def willSaveWaitUntil(cls, params: WillSaveTextDocumentParams, view: sublime.View) -> Request: return Request("textDocument/willSaveWaitUntil", params, view) + @classmethod + def willRenameFiles(cls, params: RenameFilesParams) -> Request: + return Request("workspace/willRenameFiles", params) + @classmethod def documentSymbols(cls, params: DocumentSymbolParams, view: sublime.View) -> Request: return Request("textDocument/documentSymbol", params, view, progress=True) @@ -6256,6 +6260,10 @@ def didSave(cls, params: DidSaveTextDocumentParams) -> Notification: def didClose(cls, params: DidCloseTextDocumentParams) -> Notification: return Notification("textDocument/didClose", params) + @classmethod + def didRenameFiles(cls, params: RenameFilesParams) -> Notification: + return Notification("workspace/didRenameFiles", params) + @classmethod def didChangeConfiguration(cls, params: DidChangeConfigurationParams) -> Notification: return Notification("workspace/didChangeConfiguration", params) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index ccb30b429..04e7fa41f 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -493,6 +493,11 @@ def get_initialize_params(variables: dict[str, str], workspace_folders: list[Wor "codeLens": { "refreshSupport": True }, + "fileOperations": { + "dynamicRegistration": True, + "willRename": True, + "didRename": True + }, "inlayHint": { "refreshSupport": True }, diff --git a/plugin/rename_file.py b/plugin/rename_file.py new file mode 100644 index 000000000..06a948df1 --- /dev/null +++ b/plugin/rename_file.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from .core.open import open_file_uri +from .core.protocol import Notification, RenameFilesParams, Request, WorkspaceEdit +from .core.registry import LspTextCommand +from .core.url import parse_uri +from .core.views import did_open_text_document_params, uri_from_view +from pathlib import Path +from urllib.parse import urljoin +import os +import sublime +import sublime_plugin + + +class RenameFileInputHandler(sublime_plugin.TextInputHandler): + def want_event(self) -> bool: + return False + + def __init__(self, file_name: str) -> None: + self.file_name = file_name + + def name(self) -> str: + return "new_name" + + def placeholder(self) -> str: + return self.file_name + + def initial_text(self) -> str: + return self.placeholder() + + def validate(self, name: str) -> bool: + return len(name) > 0 + +class LspRenameFileCommand(LspTextCommand): + capability = 'workspace.fileOperations.willRename' + + def want_event(self) -> bool: + return False + + def input(self, args: dict) -> sublime_plugin.TextInputHandler | None: + if "new_name" in args: + return None + return RenameFileInputHandler(Path(self.view.file_name() or "").name) + + def run( + self, + _edit: sublime.Edit, + new_name: str = "", # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy + ) -> None: + session = self.best_session("workspace.fileOperations.willRename") + if not session: + return + current_file_path = self.view.file_name() or '' + new_file_path = os.path.normpath(Path(current_file_path).parent / new_name) + window = self.view.window() + if os.path.exists(new_file_path) and window: + window.status_message(f'Unable to Rename. File already exists') + return + rename_file_params: RenameFilesParams = { + "files": [{ + "newUri": urljoin("file:", new_file_path), + "oldUri": uri_from_view(self.view), + }] + } + request = Request.willRenameFiles(rename_file_params) + session.send_request(request, lambda res: self.handle(res, session.config.name, new_name, rename_file_params)) + + def handle(self, res: WorkspaceEdit | None, session_name: str, new_name: str, rename_file_params: RenameFilesParams) -> None: + window = self.view.window() + session = self.session_by_name(session_name) + if session and window: + # LSP spec - Apply WorkspaceEdit before the files are renamed + if res: + session.apply_workspace_edit_async(res, is_refactoring=True) + renamed_file = rename_file_params['files'][0] + old_regions = [region for region in self.view.sel()] + self.view.close() # LSP spec - send didClose for old file + # actally rename the file, this will create a new file + os.rename( + parse_uri(renamed_file['oldUri'])[1], + parse_uri(renamed_file['newUri'])[1] + ) + # LSP spec - send didOpen for the new file + open_file_uri(window, renamed_file['newUri']) \ + .then(lambda v: v and v.sel().add_all(old_regions)) + for session in self.sessions('workspace.fileOperations.didRename'): + session.send_notification(Notification.didRenameFiles(rename_file_params)) + + From e26a48e2608d6b60fa6f84802b72d6fd002d0e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 25 Jun 2024 16:10:13 +0200 Subject: [PATCH 02/37] always enable --- plugin/rename_file.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 06a948df1..15c8c4bde 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -34,6 +34,9 @@ def validate(self, name: str) -> bool: class LspRenameFileCommand(LspTextCommand): capability = 'workspace.fileOperations.willRename' + def is_enabled(self): + return True + def want_event(self) -> bool: return False @@ -46,7 +49,10 @@ def run( self, _edit: sublime.Edit, new_name: str = "", # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy + paths: str | None = None ) -> None: + print('paths', paths) + print('new_name', new_name) session = self.best_session("workspace.fileOperations.willRename") if not session: return From e6304c23c260084ee47debbefa8cec2688475309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 25 Jun 2024 17:58:21 +0200 Subject: [PATCH 03/37] add renaming from the sidebar --- Side Bar.sublime-menu | 8 ++++ plugin/rename_file.py | 87 ++++++++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 Side Bar.sublime-menu diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu new file mode 100644 index 000000000..d9bbc76ab --- /dev/null +++ b/Side Bar.sublime-menu @@ -0,0 +1,8 @@ +[ + { + "caption": "Lsp: Rename", + "mnemonic": "l", + "command": "lsp_rename_file", + "args": {"paths": []} + } +] \ No newline at end of file diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 15c8c4bde..387780e10 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -1,10 +1,7 @@ from __future__ import annotations - from .core.open import open_file_uri from .core.protocol import Notification, RenameFilesParams, Request, WorkspaceEdit -from .core.registry import LspTextCommand -from .core.url import parse_uri -from .core.views import did_open_text_document_params, uri_from_view +from .core.registry import LspWindowCommand from pathlib import Path from urllib.parse import urljoin import os @@ -31,7 +28,8 @@ def initial_text(self) -> str: def validate(self, name: str) -> bool: return len(name) > 0 -class LspRenameFileCommand(LspTextCommand): + +class LspRenameFileCommand(LspWindowCommand): capability = 'workspace.fileOperations.willRename' def is_enabled(self): @@ -43,53 +41,66 @@ def want_event(self) -> bool: def input(self, args: dict) -> sublime_plugin.TextInputHandler | None: if "new_name" in args: return None - return RenameFileInputHandler(Path(self.view.file_name() or "").name) + old_path = self.get_old_path(args.get('paths'), self.window.active_view()) + return RenameFileInputHandler(Path(old_path).name) def run( self, - _edit: sublime.Edit, - new_name: str = "", # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy - paths: str | None = None + new_name: str, # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy + paths: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename" ) -> None: - print('paths', paths) - print('new_name', new_name) - session = self.best_session("workspace.fileOperations.willRename") - if not session: - return - current_file_path = self.view.file_name() or '' - new_file_path = os.path.normpath(Path(current_file_path).parent / new_name) - window = self.view.window() - if os.path.exists(new_file_path) and window: - window.status_message(f'Unable to Rename. File already exists') + session = self.session() + old_path = self.get_old_path(paths, self.window.active_view()) + new_path = os.path.normpath(Path(old_path).parent / new_name) + if os.path.exists(new_path): + self.window.status_message(f'Unable to Rename. Already exists') return rename_file_params: RenameFilesParams = { "files": [{ - "newUri": urljoin("file:", new_file_path), - "oldUri": uri_from_view(self.view), + "newUri": urljoin("file:", new_path), + "oldUri": urljoin("file:", old_path), }] } + if not session: + self.rename_path(old_path, new_path) + self.notify_did_rename(rename_file_params) + return request = Request.willRenameFiles(rename_file_params) - session.send_request(request, lambda res: self.handle(res, session.config.name, new_name, rename_file_params)) + session.send_request(request, lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params)) + + def get_old_path(self, paths: list[str] | None, view: sublime.View | None) -> str: + if paths: + return paths[0] + if view: + return view.file_name() or "" + return "" - def handle(self, res: WorkspaceEdit | None, session_name: str, new_name: str, rename_file_params: RenameFilesParams) -> None: - window = self.view.window() + def handle(self, res: WorkspaceEdit | None, session_name: str, old_path: str, new_path: str, rename_file_params: RenameFilesParams) -> None: session = self.session_by_name(session_name) - if session and window: + if session: # LSP spec - Apply WorkspaceEdit before the files are renamed if res: session.apply_workspace_edit_async(res, is_refactoring=True) - renamed_file = rename_file_params['files'][0] - old_regions = [region for region in self.view.sel()] - self.view.close() # LSP spec - send didClose for old file - # actally rename the file, this will create a new file - os.rename( - parse_uri(renamed_file['oldUri'])[1], - parse_uri(renamed_file['newUri'])[1] - ) + self.rename_path(old_path, new_path) + self.notify_did_rename(rename_file_params) + + def rename_path(self, old_path: str, new_path: str) -> None: + old_regions: list[sublime.Region] = [] + view = self.window.find_open_file(old_path) + if view: + old_regions = [region for region in view.sel()] + view.close() # LSP spec - send didClose for old file + # actally rename the file, this will create a new file + os.rename( + old_path, + new_path + ) + if os.path.isfile(new_path): # LSP spec - send didOpen for the new file - open_file_uri(window, renamed_file['newUri']) \ - .then(lambda v: v and v.sel().add_all(old_regions)) - for session in self.sessions('workspace.fileOperations.didRename'): - session.send_notification(Notification.didRenameFiles(rename_file_params)) - + open_file_uri(self.window, new_path) \ + .then(lambda v: v and old_regions and v.sel().add_all(old_regions)) + def notify_did_rename(self, rename_file_params: RenameFilesParams): + sessions = [s for s in self.sessions() if s.has_capability('workspace.fileOperations.didRename')] + for session in sessions: + session.send_notification(Notification.didRenameFiles(rename_file_params)) From e5e07221bfbd2a1def57d8f63c6a6c0eebf0161c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 25 Jun 2024 18:09:54 +0200 Subject: [PATCH 04/37] changes --- plugin/rename_file.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 387780e10..4856993b5 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -66,7 +66,10 @@ def run( self.notify_did_rename(rename_file_params) return request = Request.willRenameFiles(rename_file_params) - session.send_request(request, lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params)) + session.send_request( + request, + lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params + )) def get_old_path(self, paths: list[str] | None, view: sublime.View | None) -> str: if paths: @@ -75,7 +78,8 @@ def get_old_path(self, paths: list[str] | None, view: sublime.View | None) -> st return view.file_name() or "" return "" - def handle(self, res: WorkspaceEdit | None, session_name: str, old_path: str, new_path: str, rename_file_params: RenameFilesParams) -> None: + def handle(self, res: WorkspaceEdit | None, session_name: str, + old_path: str, new_path: str, rename_file_params: RenameFilesParams) -> None: session = self.session_by_name(session_name) if session: # LSP spec - Apply WorkspaceEdit before the files are renamed @@ -89,18 +93,20 @@ def rename_path(self, old_path: str, new_path: str) -> None: view = self.window.find_open_file(old_path) if view: old_regions = [region for region in view.sel()] - view.close() # LSP spec - send didClose for old file + view.close() # LSP spec - send didClose for the old file # actally rename the file, this will create a new file - os.rename( - old_path, - new_path - ) + os.rename(old_path, new_path) if os.path.isfile(new_path): + def restore_regions(v: sublime.View | None) -> None: + if not v: + return + v.sel().clear() + v.sel().add_all(old_regions) + # LSP spec - send didOpen for the new file - open_file_uri(self.window, new_path) \ - .then(lambda v: v and old_regions and v.sel().add_all(old_regions)) + open_file_uri(self.window, new_path).then(restore_regions) def notify_did_rename(self, rename_file_params: RenameFilesParams): sessions = [s for s in self.sessions() if s.has_capability('workspace.fileOperations.didRename')] - for session in sessions: - session.send_notification(Notification.didRenameFiles(rename_file_params)) + for s in sessions: + s.send_notification(Notification.didRenameFiles(rename_file_params)) From 5c91301a1f73f1b6e3995f815c3f241c270a4ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 25 Jun 2024 18:22:24 +0200 Subject: [PATCH 05/37] fix styles --- plugin/rename_file.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 4856993b5..09abb2363 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -46,14 +46,14 @@ def input(self, args: dict) -> sublime_plugin.TextInputHandler | None: def run( self, - new_name: str, # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy - paths: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename" + new_name: str, # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy + paths: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename" ) -> None: session = self.session() old_path = self.get_old_path(paths, self.window.active_view()) new_path = os.path.normpath(Path(old_path).parent / new_name) if os.path.exists(new_path): - self.window.status_message(f'Unable to Rename. Already exists') + self.window.status_message('Unable to Rename. Already exists') return rename_file_params: RenameFilesParams = { "files": [{ @@ -68,8 +68,8 @@ def run( request = Request.willRenameFiles(rename_file_params) session.send_request( request, - lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params - )) + lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params) + ) def get_old_path(self, paths: list[str] | None, view: sublime.View | None) -> str: if paths: @@ -93,7 +93,7 @@ def rename_path(self, old_path: str, new_path: str) -> None: view = self.window.find_open_file(old_path) if view: old_regions = [region for region in view.sel()] - view.close() # LSP spec - send didClose for the old file + view.close() # LSP spec - send didClose for the old file # actally rename the file, this will create a new file os.rename(old_path, new_path) if os.path.isfile(new_path): From 075e50e535ccdb2aeca9ebfb811c77e8bf8bbe43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 25 Jun 2024 18:29:38 +0200 Subject: [PATCH 06/37] add new line --- Side Bar.sublime-menu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu index d9bbc76ab..62795b026 100644 --- a/Side Bar.sublime-menu +++ b/Side Bar.sublime-menu @@ -5,4 +5,4 @@ "command": "lsp_rename_file", "args": {"paths": []} } -] \ No newline at end of file +] From 814b0bbc9d466107118e216d18e46f38b928dab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 25 Jun 2024 18:47:12 +0200 Subject: [PATCH 07/37] hande if new path directory doesn't exist --- plugin/rename_file.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 09abb2363..9d3cbe252 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -95,6 +95,9 @@ def rename_path(self, old_path: str, new_path: str) -> None: old_regions = [region for region in view.sel()] view.close() # LSP spec - send didClose for the old file # actally rename the file, this will create a new file + new_dir = Path(new_path).parent + if not os.path.exists(new_dir): + os.makedirs(new_dir) os.rename(old_path, new_path) if os.path.isfile(new_path): def restore_regions(v: sublime.View | None) -> None: From d488b0609291a1a03257db02f7b8a1d9fe53d1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Wed, 26 Jun 2024 11:56:40 +0200 Subject: [PATCH 08/37] handle renaming buffers --- plugin/rename_file.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 9d3cbe252..f2de9575b 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -50,11 +50,15 @@ def run( paths: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename" ) -> None: session = self.session() - old_path = self.get_old_path(paths, self.window.active_view()) + view = self.window.active_view() + old_path = self.get_old_path(paths, view) new_path = os.path.normpath(Path(old_path).parent / new_name) if os.path.exists(new_path): self.window.status_message('Unable to Rename. Already exists') return + if old_path == '' and view: # handle renaming buffers + view.set_name(Path(new_path).name) + return rename_file_params: RenameFilesParams = { "files": [{ "newUri": urljoin("file:", new_path), From d1a10cfdc5ce2c8b402bc46c4f781570e63f8248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Wed, 26 Jun 2024 12:03:03 +0200 Subject: [PATCH 09/37] ahh... --- plugin/rename_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index f2de9575b..623a675f8 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -56,7 +56,7 @@ def run( if os.path.exists(new_path): self.window.status_message('Unable to Rename. Already exists') return - if old_path == '' and view: # handle renaming buffers + if old_path == '' and view: # handle renaming buffers view.set_name(Path(new_path).name) return rename_file_params: RenameFilesParams = { From 08e3af11a9cdeda1fbc302606f375dff42a971fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Wed, 26 Jun 2024 12:10:56 +0200 Subject: [PATCH 10/37] add initial_selection --- plugin/rename_file.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 623a675f8..b109bf6a3 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -25,6 +25,10 @@ def placeholder(self) -> str: def initial_text(self) -> str: return self.placeholder() + def initial_selection(self) -> list[tuple[int, int]]: + end_point = self.file_name.rfind('.') + return [(0, end_point)] + def validate(self, name: str) -> bool: return len(name) > 0 From 6f654623cbedfd7f4dada7269783861d68c5b344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Wed, 26 Jun 2024 12:13:56 +0200 Subject: [PATCH 11/37] improve initial_selection and rename file_name to path --- plugin/rename_file.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index b109bf6a3..fe2e59ac6 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -13,24 +13,26 @@ class RenameFileInputHandler(sublime_plugin.TextInputHandler): def want_event(self) -> bool: return False - def __init__(self, file_name: str) -> None: - self.file_name = file_name + def __init__(self, path: str) -> None: + self.path = path def name(self) -> str: return "new_name" def placeholder(self) -> str: - return self.file_name + return self.path def initial_text(self) -> str: return self.placeholder() def initial_selection(self) -> list[tuple[int, int]]: - end_point = self.file_name.rfind('.') + end_point = self.path.rfind('.') + if end_point == -1: + end_point = len(self.path) return [(0, end_point)] - def validate(self, name: str) -> bool: - return len(name) > 0 + def validate(self, path: str) -> bool: + return len(path) > 0 class LspRenameFileCommand(LspWindowCommand): From c9e877283b3cd26cb2299d301717fe2a469e1d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 27 Jun 2024 12:12:00 +0200 Subject: [PATCH 12/37] Split "Lsp: Rename" to "Lsp: Rename Folder" and "Lsp: Rename File" --- Side Bar.sublime-menu | 14 ++++++++++---- plugin/rename_file.py | 25 ++++++++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu index 62795b026..45a7f6bf9 100644 --- a/Side Bar.sublime-menu +++ b/Side Bar.sublime-menu @@ -1,8 +1,14 @@ [ { - "caption": "Lsp: Rename", + "caption": "Lsp: Rename File", "mnemonic": "l", "command": "lsp_rename_file", - "args": {"paths": []} - } -] + "args": {"files": []} + }, + { + "caption": "Lsp: Rename Folder", + "mnemonic": "l", + "command": "lsp_rename_file", + "args": {"dirs": []} + }, +] \ No newline at end of file diff --git a/plugin/rename_file.py b/plugin/rename_file.py index fe2e59ac6..9f673d3d4 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -41,23 +41,33 @@ class LspRenameFileCommand(LspWindowCommand): def is_enabled(self): return True + def is_visible(self, dirs=None, files=None): + if dirs is None and files is None: + return True # show 'LSP: Rename File' in command palette + if dirs is not None: + return len(dirs) == 1 # show 'LSP: Rename Folder' in sidebar + if files is not None: + return len(files) == 1 # show 'LSP: Rename File' in sidebar + return False + def want_event(self) -> bool: return False def input(self, args: dict) -> sublime_plugin.TextInputHandler | None: if "new_name" in args: return None - old_path = self.get_old_path(args.get('paths'), self.window.active_view()) + old_path = self.get_old_path(args.get('dirs'), args.get('files'), self.window.active_view()) return RenameFileInputHandler(Path(old_path).name) def run( self, new_name: str, # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy - paths: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename" + dirs: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename Folder" + files: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename File" ) -> None: session = self.session() view = self.window.active_view() - old_path = self.get_old_path(paths, view) + old_path = self.get_old_path(dirs, files, view) new_path = os.path.normpath(Path(old_path).parent / new_name) if os.path.exists(new_path): self.window.status_message('Unable to Rename. Already exists') @@ -81,9 +91,11 @@ def run( lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params) ) - def get_old_path(self, paths: list[str] | None, view: sublime.View | None) -> str: - if paths: - return paths[0] + def get_old_path(self, dirs: list[str] | None, files: list[str] | None, view: sublime.View | None) -> str: + if dirs: + return dirs[0] + if files: + return files[0] if view: return view.file_name() or "" return "" @@ -104,7 +116,6 @@ def rename_path(self, old_path: str, new_path: str) -> None: if view: old_regions = [region for region in view.sel()] view.close() # LSP spec - send didClose for the old file - # actally rename the file, this will create a new file new_dir = Path(new_path).parent if not os.path.exists(new_dir): os.makedirs(new_dir) From e0c9c8264a249988e91f69462ba06145d4029d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 27 Jun 2024 12:14:55 +0200 Subject: [PATCH 13/37] new line --- Side Bar.sublime-menu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu index 45a7f6bf9..67b0316cf 100644 --- a/Side Bar.sublime-menu +++ b/Side Bar.sublime-menu @@ -11,4 +11,4 @@ "command": "lsp_rename_file", "args": {"dirs": []} }, -] \ No newline at end of file +] From a52b5c4151597238aab457621323810fbd13eeef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 27 Jun 2024 19:07:15 +0200 Subject: [PATCH 14/37] when renaming a directory, it would be good to retarget all open views 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 --- plugin/rename_file.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 9f673d3d4..671b9ccc0 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -119,7 +119,15 @@ def rename_path(self, old_path: str, new_path: str) -> None: new_dir = Path(new_path).parent if not os.path.exists(new_dir): os.makedirs(new_dir) + isdir = os.path.isdir(old_path) os.rename(old_path, new_path) + if isdir: + for v in self.window.views(): + file_name = v.file_name() + if not file_name: + continue + if file_name.startswith(old_path): + v.retarget(file_name.replace(old_path, new_path)) if os.path.isfile(new_path): def restore_regions(v: sublime.View | None) -> None: if not v: From 2acceb6ca7df114b71428d3859615d99d420c53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 27 Jun 2024 21:59:30 +0200 Subject: [PATCH 15/37] save some lines --- plugin/rename_file.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 671b9ccc0..b829040ad 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -124,9 +124,7 @@ def rename_path(self, old_path: str, new_path: str) -> None: if isdir: for v in self.window.views(): file_name = v.file_name() - if not file_name: - continue - if file_name.startswith(old_path): + if file_name and file_name.startswith(old_path): v.retarget(file_name.replace(old_path, new_path)) if os.path.isfile(new_path): def restore_regions(v: sublime.View | None) -> None: From 9ab9dc5e9e90db6edc453467e942330164a7be6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 27 Jun 2024 22:08:36 +0200 Subject: [PATCH 16/37] remove more lines --- plugin/rename_file.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index b829040ad..ba5c27bdf 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -10,9 +10,6 @@ class RenameFileInputHandler(sublime_plugin.TextInputHandler): - def want_event(self) -> bool: - return False - def __init__(self, path: str) -> None: self.path = path @@ -26,10 +23,8 @@ def initial_text(self) -> str: return self.placeholder() def initial_selection(self) -> list[tuple[int, int]]: - end_point = self.path.rfind('.') - if end_point == -1: - end_point = len(self.path) - return [(0, end_point)] + name, _ext = os.path.splitext(self.path) + return [(0, len(name))] def validate(self, path: str) -> bool: return len(path) > 0 From f5c26a663436c2f9bf1d8e1a30db0268d49c81aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 27 Jun 2024 23:35:39 +0200 Subject: [PATCH 17/37] few less lines --- plugin/rename_file.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index ba5c27bdf..d40ee50d5 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -52,7 +52,7 @@ def input(self, args: dict) -> sublime_plugin.TextInputHandler | None: if "new_name" in args: return None old_path = self.get_old_path(args.get('dirs'), args.get('files'), self.window.active_view()) - return RenameFileInputHandler(Path(old_path).name) + return RenameFileInputHandler(Path(old_path or "").name) def run( self, @@ -63,13 +63,13 @@ def run( session = self.session() view = self.window.active_view() old_path = self.get_old_path(dirs, files, view) + if old_path is None: # handle renaming buffers + if view: view.set_name(new_name) + return new_path = os.path.normpath(Path(old_path).parent / new_name) if os.path.exists(new_path): self.window.status_message('Unable to Rename. Already exists') return - if old_path == '' and view: # handle renaming buffers - view.set_name(Path(new_path).name) - return rename_file_params: RenameFilesParams = { "files": [{ "newUri": urljoin("file:", new_path), @@ -86,19 +86,14 @@ def run( lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params) ) - def get_old_path(self, dirs: list[str] | None, files: list[str] | None, view: sublime.View | None) -> str: - if dirs: - return dirs[0] - if files: - return files[0] - if view: - return view.file_name() or "" - return "" + def get_old_path(self, dirs: list[str] | None, files: list[str] | None, view: sublime.View | None) -> str | None: + if dirs: return dirs[0] + if files: return files[0] + if view: return view.file_name() def handle(self, res: WorkspaceEdit | None, session_name: str, old_path: str, new_path: str, rename_file_params: RenameFilesParams) -> None: - session = self.session_by_name(session_name) - if session: + if session := self.session_by_name(session_name): # LSP spec - Apply WorkspaceEdit before the files are renamed if res: session.apply_workspace_edit_async(res, is_refactoring=True) @@ -107,8 +102,7 @@ def handle(self, res: WorkspaceEdit | None, session_name: str, def rename_path(self, old_path: str, new_path: str) -> None: old_regions: list[sublime.Region] = [] - view = self.window.find_open_file(old_path) - if view: + if view := self.window.find_open_file(old_path): old_regions = [region for region in view.sel()] view.close() # LSP spec - send didClose for the old file new_dir = Path(new_path).parent From 437659fd4e865f7e5c4c3e22f72d93ea788f8fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 27 Jun 2024 23:39:40 +0200 Subject: [PATCH 18/37] simpler conditions --- plugin/rename_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index d40ee50d5..d183b81d4 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -39,9 +39,9 @@ def is_enabled(self): def is_visible(self, dirs=None, files=None): if dirs is None and files is None: return True # show 'LSP: Rename File' in command palette - if dirs is not None: + if dirs: return len(dirs) == 1 # show 'LSP: Rename Folder' in sidebar - if files is not None: + if files: return len(files) == 1 # show 'LSP: Rename File' in sidebar return False From 3647f92a3c0b1b53e9f11d01606eeab0e27ac2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 27 Jun 2024 23:41:02 +0200 Subject: [PATCH 19/37] avoid multiple statements on one line --- plugin/rename_file.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index d183b81d4..b886109e0 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -64,7 +64,8 @@ def run( view = self.window.active_view() old_path = self.get_old_path(dirs, files, view) if old_path is None: # handle renaming buffers - if view: view.set_name(new_name) + if view: + view.set_name(new_name) return new_path = os.path.normpath(Path(old_path).parent / new_name) if os.path.exists(new_path): @@ -87,9 +88,12 @@ def run( ) def get_old_path(self, dirs: list[str] | None, files: list[str] | None, view: sublime.View | None) -> str | None: - if dirs: return dirs[0] - if files: return files[0] - if view: return view.file_name() + if dirs: + return dirs[0] + if files: + return files[0] + if view: + return view.file_name() def handle(self, res: WorkspaceEdit | None, session_name: str, old_path: str, new_path: str, rename_file_params: RenameFilesParams) -> None: From 3389f66824dfca1439ce233daf5c194ce2981993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 14:52:50 +0200 Subject: [PATCH 20/37] implement FileOperationFilter --- plugin/core/types.py | 67 ++++++++++++++++++++++++++++++++++++++++++- plugin/rename_file.py | 36 +++++++++++++++-------- 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/plugin/core/types.py b/plugin/core/types.py index 46d7efa7b..5a44ba19c 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -2,7 +2,7 @@ from .collections import DottedDict from .file_watcher import FileWatcherEventType from .logging import debug, set_debug_logging -from .protocol import TextDocumentSyncKind +from .protocol import FileOperationFilter, FileOperationPattern, FileOperationPatternKind, TextDocumentSyncKind from .url import filename_to_uri from .url import parse_uri from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, TypedDict, TypeVar, Union @@ -10,6 +10,7 @@ from wcmatch.glob import BRACE from wcmatch.glob import globmatch from wcmatch.glob import GLOBSTAR +from wcmatch.glob import IGNORECASE import contextlib import fnmatch import os @@ -440,6 +441,70 @@ def matches(self, view: sublime.View) -> bool: return any(f(view) for f in self.filters) if self.filters else True +class FileOperationFilterChecker: + """ + A file operation filter denotes a view or path through properties like scheme or pattern. An example is a filter + that applies to TypeScript files on disk. Another example is a filter that applies to JSON files with name + package.json: + { + "scheme": "file", + "pattern": { + "glob": "**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}", + "matches": "file" + } + } + """ + + __slots__ = ("scheme", "pattern") + + def __init__( + self, + scheme: str | None = None, + pattern: FileOperationPattern | None = None + ) -> None: + self.scheme = scheme + self.pattern = pattern + + def __call__(self, path: str, view: sublime.View | None) -> bool: + if self.scheme and view: + uri = view.settings().get("lsp_uri") + if isinstance(uri, str) and parse_uri(uri)[0] != self.scheme: + return False + if self.pattern: + matches = self.pattern.get('matches') + if matches: + if matches == FileOperationPatternKind.File and os.path.isdir(path): + return False + if matches == FileOperationPatternKind.Folder and os.path.isfile(path): + return False + options = self.pattern.get('options', {}) + flags = GLOBSTAR | BRACE + if options.get('ignoreCase', False): + flags |= IGNORECASE + if not globmatch(path, self.pattern['glob'], flags=flags): + return False + return True + + +class FileOperationFilterMatcher: + """ + A FileOperationFilterMatcher is a list of FileOperationFilterChecker. + Provides logic to see if a path/view matches the specified FileOperationFilter's. + """ + + __slots__ = ("filters",) + + def __init__(self, file_filters: list[FileOperationFilter]) -> None: + self.filters = [FileOperationFilterChecker(**file_filter) for file_filter in file_filters] + + def __bool__(self) -> bool: + return bool(self.filters) + + def matches(self, new_path: str, view: sublime.View | None) -> bool: + """Does this selector match the view? A selector with no filters matches all views.""" + return any(f(new_path, view) for f in self.filters) if self.filters else True + + # method -> (capability dotted path, optional registration dotted path) # these are the EXCEPTIONS. The general rule is: method foo/bar --> (barProvider, barProvider.id) _METHOD_TO_CAPABILITY_EXCEPTIONS: dict[str, tuple[str, str | None]] = { diff --git a/plugin/rename_file.py b/plugin/rename_file.py index b886109e0..aa82a054f 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -1,4 +1,6 @@ from __future__ import annotations + +from .core.types import FileOperationFilterMatcher from .core.open import open_file_uri from .core.protocol import Notification, RenameFilesParams, Request, WorkspaceEdit from .core.registry import LspWindowCommand @@ -79,13 +81,18 @@ def run( } if not session: self.rename_path(old_path, new_path) - self.notify_did_rename(rename_file_params) + self.notify_did_rename(rename_file_params, new_path, view) + return + capability = session.get_capability('workspace.fileOperations.willRename') + if not capability: return - request = Request.willRenameFiles(rename_file_params) - session.send_request( - request, - lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params) - ) + filters = FileOperationFilterMatcher(capability.get('filters')) + if filters.matches(old_path, view): + request = Request.willRenameFiles(rename_file_params) + session.send_request( + request, + lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params, view) + ) def get_old_path(self, dirs: list[str] | None, files: list[str] | None, view: sublime.View | None) -> str | None: if dirs: @@ -96,13 +103,13 @@ def get_old_path(self, dirs: list[str] | None, files: list[str] | None, view: su return view.file_name() def handle(self, res: WorkspaceEdit | None, session_name: str, - old_path: str, new_path: str, rename_file_params: RenameFilesParams) -> None: + old_path: str, new_path: str, rename_file_params: RenameFilesParams, view: sublime.View | None) -> None: if session := self.session_by_name(session_name): # 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) - self.notify_did_rename(rename_file_params) + self.notify_did_rename(rename_file_params, new_path, view) def rename_path(self, old_path: str, new_path: str) -> None: old_regions: list[sublime.Region] = [] @@ -129,7 +136,12 @@ def restore_regions(v: sublime.View | None) -> None: # LSP spec - send didOpen for the new file open_file_uri(self.window, new_path).then(restore_regions) - def notify_did_rename(self, rename_file_params: RenameFilesParams): - sessions = [s for s in self.sessions() if s.has_capability('workspace.fileOperations.didRename')] - for s in sessions: - s.send_notification(Notification.didRenameFiles(rename_file_params)) + def notify_did_rename(self, rename_file_params: RenameFilesParams, path: str, view: sublime.View | None): + for s in self.sessions(): + capability = s.get_capability('workspace.fileOperations.didRename') + if not capability: + continue + filters = FileOperationFilterMatcher(capability.get('filters')) + if filters.matches(path, view): + s.send_notification(Notification.didRenameFiles(rename_file_params)) + From 4785d4938c2b4abc715a4e6f3291b0ca323f534e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 15:03:52 +0200 Subject: [PATCH 21/37] remomve few lines --- plugin/rename_file.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index aa82a054f..8ef5b770a 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -83,16 +83,16 @@ def run( self.rename_path(old_path, new_path) self.notify_did_rename(rename_file_params, new_path, view) return - capability = session.get_capability('workspace.fileOperations.willRename') - if not capability: - return - filters = FileOperationFilterMatcher(capability.get('filters')) - if filters.matches(old_path, view): + filters = (session.get_capability('workspace.fileOperations.willRename') or {}).get('filters') + if filters and FileOperationFilterMatcher(filters).matches(old_path, view): request = Request.willRenameFiles(rename_file_params) session.send_request( request, lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params, view) ) + else: + self.rename_path(old_path, new_path) + self.notify_did_rename(rename_file_params, new_path, view) def get_old_path(self, dirs: list[str] | None, files: list[str] | None, view: sublime.View | None) -> str | None: if dirs: @@ -138,10 +138,9 @@ def restore_regions(v: sublime.View | None) -> None: def notify_did_rename(self, rename_file_params: RenameFilesParams, path: str, view: sublime.View | None): for s in self.sessions(): - capability = s.get_capability('workspace.fileOperations.didRename') - if not capability: + filters = (s.get_capability('workspace.fileOperations.didRename') or {}).get('filters') + if not filters: continue - filters = FileOperationFilterMatcher(capability.get('filters')) - if filters.matches(path, view): + if FileOperationFilterMatcher(filters).matches(path, view): s.send_notification(Notification.didRenameFiles(rename_file_params)) From 2f5af3173253cb49946315c7031f52f12c667aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 15:04:24 +0200 Subject: [PATCH 22/37] fix flake --- plugin/rename_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 8ef5b770a..9d0015391 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -143,4 +143,3 @@ def notify_did_rename(self, rename_file_params: RenameFilesParams, path: str, vi continue if FileOperationFilterMatcher(filters).matches(path, view): s.send_notification(Notification.didRenameFiles(rename_file_params)) - From 890667caf4a152c5f11560673957050f9dad42e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 15:12:18 +0200 Subject: [PATCH 23/37] fix pyright --- stubs/wcmatch/glob.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/stubs/wcmatch/glob.pyi b/stubs/wcmatch/glob.pyi index 9d5a7a6f6..0cc199577 100644 --- a/stubs/wcmatch/glob.pyi +++ b/stubs/wcmatch/glob.pyi @@ -2,6 +2,7 @@ from typing import Any, Optional BRACE: int = ... GLOBSTAR: int = ... +IGNORECASE: int = ... def globmatch( From af04c56692d61c1c2e8755dd535833d8fef545db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 15:17:14 +0200 Subject: [PATCH 24/37] remove is_visible code It is not necessary --- plugin/rename_file.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 9d0015391..ad12cdd7c 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -38,14 +38,6 @@ class LspRenameFileCommand(LspWindowCommand): def is_enabled(self): return True - def is_visible(self, dirs=None, files=None): - if dirs is None and files is None: - return True # show 'LSP: Rename File' in command palette - if dirs: - return len(dirs) == 1 # show 'LSP: Rename Folder' in sidebar - if files: - return len(files) == 1 # show 'LSP: Rename File' in sidebar - return False def want_event(self) -> bool: return False From 178adae6e086f9edc3600bdbbd1f64d98c4c2337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 15:18:06 +0200 Subject: [PATCH 25/37] remove LSP: Rename File and Rename folder in favor of LSP: Rename... in the sidebar context menu --- Side Bar.sublime-menu | 12 +++--------- plugin/rename_file.py | 16 ++++++---------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu index 67b0316cf..5538942d2 100644 --- a/Side Bar.sublime-menu +++ b/Side Bar.sublime-menu @@ -1,14 +1,8 @@ [ { - "caption": "Lsp: Rename File", + "caption": "LSP: Rename...", "mnemonic": "l", "command": "lsp_rename_file", - "args": {"files": []} - }, - { - "caption": "Lsp: Rename Folder", - "mnemonic": "l", - "command": "lsp_rename_file", - "args": {"dirs": []} - }, + "args": {"paths": []} + } ] diff --git a/plugin/rename_file.py b/plugin/rename_file.py index ad12cdd7c..9c785422e 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -38,25 +38,23 @@ class LspRenameFileCommand(LspWindowCommand): def is_enabled(self): return True - def want_event(self) -> bool: return False def input(self, args: dict) -> sublime_plugin.TextInputHandler | None: if "new_name" in args: return None - old_path = self.get_old_path(args.get('dirs'), args.get('files'), self.window.active_view()) + old_path = self.get_old_path(args.get('paths'), self.window.active_view()) return RenameFileInputHandler(Path(old_path or "").name) def run( self, new_name: str, # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy - dirs: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename Folder" - files: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename File" + paths: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename..." ) -> None: session = self.session() view = self.window.active_view() - old_path = self.get_old_path(dirs, files, view) + old_path = self.get_old_path(paths, view) if old_path is None: # handle renaming buffers if view: view.set_name(new_name) @@ -86,11 +84,9 @@ def run( self.rename_path(old_path, new_path) self.notify_did_rename(rename_file_params, new_path, view) - def get_old_path(self, dirs: list[str] | None, files: list[str] | None, view: sublime.View | None) -> str | None: - if dirs: - return dirs[0] - if files: - return files[0] + def get_old_path(self, paths: list[str] | None, view: sublime.View | None) -> str | None: + if paths: + return paths[0] if view: return view.file_name() From 8ef443c2f1ebb27f52dc145b795c04b10a97d110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 15:19:19 +0200 Subject: [PATCH 26/37] rename LspRenameFileCommand to LspRenamePathCommand --- Default.sublime-commands | 2 +- Side Bar.sublime-menu | 2 +- boot.py | 4 ++-- plugin/rename_file.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Default.sublime-commands b/Default.sublime-commands index 2f64684d2..631920f88 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -153,7 +153,7 @@ }, { "caption": "LSP: Rename File", - "command": "lsp_rename_file" + "command": "lsp_rename_path" }, { "caption": "LSP: Code Action", diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu index 5538942d2..a82bbd17c 100644 --- a/Side Bar.sublime-menu +++ b/Side Bar.sublime-menu @@ -2,7 +2,7 @@ { "caption": "LSP: Rename...", "mnemonic": "l", - "command": "lsp_rename_file", + "command": "lsp_rename_path", "args": {"paths": []} } ] diff --git a/boot.py b/boot.py index 6ff25cec8..32de129c4 100644 --- a/boot.py +++ b/boot.py @@ -69,7 +69,7 @@ from .plugin.references import LspSymbolReferencesCommand from .plugin.rename import LspHideRenameButtonsCommand from .plugin.rename import LspSymbolRenameCommand -from .plugin.rename_file import LspRenameFileCommand +from .plugin.rename_file import LspRenamePathCommand from .plugin.save_command import LspSaveAllCommand from .plugin.save_command import LspSaveCommand from .plugin.selection_range import LspExpandSelectionCommand @@ -147,7 +147,7 @@ "LspSymbolImplementationCommand", "LspSymbolReferencesCommand", "LspSymbolRenameCommand", - "LspRenameFileCommand", + "LspRenamePathCommand", "LspSymbolTypeDefinitionCommand", "LspToggleCodeLensesCommand", "LspToggleHoverPopupsCommand", diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 9c785422e..fd8bec49c 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -32,7 +32,7 @@ def validate(self, path: str) -> bool: return len(path) > 0 -class LspRenameFileCommand(LspWindowCommand): +class LspRenamePathCommand(LspWindowCommand): capability = 'workspace.fileOperations.willRename' def is_enabled(self): From d1342b96deff642522b9ff0541277ff0d0e4c396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 16:16:22 +0200 Subject: [PATCH 27/37] remove FileOperationFilterChecker, FileOperationFilterMatcher in favor of match_file_operation_filters --- plugin/core/types.py | 64 +++++++++---------------------------------- plugin/rename_file.py | 12 ++++---- 2 files changed, 18 insertions(+), 58 deletions(-) diff --git a/plugin/core/types.py b/plugin/core/types.py index 5a44ba19c..8fd01f11f 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -2,7 +2,7 @@ from .collections import DottedDict from .file_watcher import FileWatcherEventType from .logging import debug, set_debug_logging -from .protocol import FileOperationFilter, FileOperationPattern, FileOperationPatternKind, TextDocumentSyncKind +from .protocol import FileOperationFilter, FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions, TextDocumentSyncKind from .url import filename_to_uri from .url import parse_uri from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, TypedDict, TypeVar, Union @@ -440,69 +440,31 @@ def matches(self, view: sublime.View) -> bool: """Does this selector match the view? A selector with no filters matches all views.""" return any(f(view) for f in self.filters) if self.filters else True - -class FileOperationFilterChecker: - """ - A file operation filter denotes a view or path through properties like scheme or pattern. An example is a filter - that applies to TypeScript files on disk. Another example is a filter that applies to JSON files with name - package.json: - { - "scheme": "file", - "pattern": { - "glob": "**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}", - "matches": "file" - } - } - """ - - __slots__ = ("scheme", "pattern") - - def __init__( - self, - scheme: str | None = None, - pattern: FileOperationPattern | None = None - ) -> None: - self.scheme = scheme - self.pattern = pattern - - def __call__(self, path: str, view: sublime.View | None) -> bool: - if self.scheme and view: +def match_file_operation_filters(file_operation_options: FileOperationRegistrationOptions, path: str, view: sublime.View | None) -> bool: + def matches(file_operation_filter: FileOperationFilter) -> bool: + pattern = file_operation_filter.get('pattern') + scheme = file_operation_filter.get('scheme') + if scheme and view: uri = view.settings().get("lsp_uri") - if isinstance(uri, str) and parse_uri(uri)[0] != self.scheme: + if isinstance(uri, str) and parse_uri(uri)[0] != scheme: return False - if self.pattern: - matches = self.pattern.get('matches') + if pattern: + matches = pattern.get('matches') if matches: if matches == FileOperationPatternKind.File and os.path.isdir(path): return False if matches == FileOperationPatternKind.Folder and os.path.isfile(path): return False - options = self.pattern.get('options', {}) + options = pattern.get('options', {}) flags = GLOBSTAR | BRACE if options.get('ignoreCase', False): flags |= IGNORECASE - if not globmatch(path, self.pattern['glob'], flags=flags): + if not globmatch(path, pattern['glob'], flags=flags): return False return True - -class FileOperationFilterMatcher: - """ - A FileOperationFilterMatcher is a list of FileOperationFilterChecker. - Provides logic to see if a path/view matches the specified FileOperationFilter's. - """ - - __slots__ = ("filters",) - - def __init__(self, file_filters: list[FileOperationFilter]) -> None: - self.filters = [FileOperationFilterChecker(**file_filter) for file_filter in file_filters] - - def __bool__(self) -> bool: - return bool(self.filters) - - def matches(self, new_path: str, view: sublime.View | None) -> bool: - """Does this selector match the view? A selector with no filters matches all views.""" - return any(f(new_path, view) for f in self.filters) if self.filters else True + filters = [matches(file_operation_filter) for file_operation_filter in file_operation_options.get('filters')] + return any(filters) if filters else True # method -> (capability dotted path, optional registration dotted path) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index fd8bec49c..9098f9774 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .core.types import FileOperationFilterMatcher +from .core.types import match_file_operation_filters from .core.open import open_file_uri from .core.protocol import Notification, RenameFilesParams, Request, WorkspaceEdit from .core.registry import LspWindowCommand @@ -73,8 +73,8 @@ def run( self.rename_path(old_path, new_path) self.notify_did_rename(rename_file_params, new_path, view) return - filters = (session.get_capability('workspace.fileOperations.willRename') or {}).get('filters') - if filters and FileOperationFilterMatcher(filters).matches(old_path, view): + file_operation_options = session.get_capability('workspace.fileOperations.willRename') + if file_operation_options and match_file_operation_filters(file_operation_options, old_path, view): request = Request.willRenameFiles(rename_file_params) session.send_request( request, @@ -126,8 +126,6 @@ def restore_regions(v: sublime.View | None) -> None: def notify_did_rename(self, rename_file_params: RenameFilesParams, path: str, view: sublime.View | None): for s in self.sessions(): - filters = (s.get_capability('workspace.fileOperations.didRename') or {}).get('filters') - if not filters: - continue - if FileOperationFilterMatcher(filters).matches(path, view): + file_operation_options = s.get_capability('workspace.fileOperations.didRename') + if file_operation_options and match_file_operation_filters(file_operation_options, path, view): s.send_notification(Notification.didRenameFiles(rename_file_params)) From 057eaff2cab6cd0e270ee8a44ab3d0330aed2e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 16:23:11 +0200 Subject: [PATCH 28/37] flake8 fixes --- plugin/core/types.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugin/core/types.py b/plugin/core/types.py index 8fd01f11f..76a775d20 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -2,7 +2,10 @@ from .collections import DottedDict from .file_watcher import FileWatcherEventType from .logging import debug, set_debug_logging -from .protocol import FileOperationFilter, FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions, TextDocumentSyncKind +from .protocol import FileOperationFilter +from .protocol import FileOperationPatternKind +from .protocol import FileOperationRegistrationOptions +from .protocol import TextDocumentSyncKind from .url import filename_to_uri from .url import parse_uri from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, TypedDict, TypeVar, Union @@ -440,7 +443,10 @@ def matches(self, view: sublime.View) -> bool: """Does this selector match the view? A selector with no filters matches all views.""" return any(f(view) for f in self.filters) if self.filters else True -def match_file_operation_filters(file_operation_options: FileOperationRegistrationOptions, path: str, view: sublime.View | None) -> bool: + +def match_file_operation_filters( + file_operation_options: FileOperationRegistrationOptions, path: str, view: sublime.View | None +) -> bool: def matches(file_operation_filter: FileOperationFilter) -> bool: pattern = file_operation_filter.get('pattern') scheme = file_operation_filter.get('scheme') From cb82086537649f152a298e2dbf1ac4eba054ab48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 2 Jul 2024 20:32:47 +0200 Subject: [PATCH 29/37] cannot use an input handler to rename folder because it always displays LSP: Rename File I cannot tweak the text inside the TextInputHandler use show_input_panel instead... --- Side Bar.sublime-menu | 2 +- boot.py | 2 ++ plugin/rename_file.py | 52 +++++++++++++++++++++++++++---------------- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu index a82bbd17c..ae55009a7 100644 --- a/Side Bar.sublime-menu +++ b/Side Bar.sublime-menu @@ -2,7 +2,7 @@ { "caption": "LSP: Rename...", "mnemonic": "l", - "command": "lsp_rename_path", + "command": "lsp_rename_path_sidebar", "args": {"paths": []} } ] diff --git a/boot.py b/boot.py index 32de129c4..020add216 100644 --- a/boot.py +++ b/boot.py @@ -70,6 +70,7 @@ from .plugin.rename import LspHideRenameButtonsCommand from .plugin.rename import LspSymbolRenameCommand from .plugin.rename_file import LspRenamePathCommand +from .plugin.rename_file import LspRenamePathSidebarCommand from .plugin.save_command import LspSaveAllCommand from .plugin.save_command import LspSaveCommand from .plugin.selection_range import LspExpandSelectionCommand @@ -148,6 +149,7 @@ "LspSymbolReferencesCommand", "LspSymbolRenameCommand", "LspRenamePathCommand", + "LspRenamePathSidebarCommand", "LspSymbolTypeDefinitionCommand", "LspToggleCodeLensesCommand", "LspToggleHoverPopupsCommand", diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 9098f9774..ff791089e 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -9,9 +9,32 @@ import os import sublime import sublime_plugin - - -class RenameFileInputHandler(sublime_plugin.TextInputHandler): +import functools + + +class LspRenamePathSidebarCommand(LspWindowCommand): + def run(self, paths: list[str] | None = None) -> None: + old_path = paths[0] if paths else None + path_name = Path(old_path or "").name + v = self.window.show_input_panel( + "(LSP) New Name:", + path_name, + functools.partial(self.on_done, old_path), + None, + None) + v.sel().clear() + name, _ext = os.path.splitext(path_name) + v.sel().add(sublime.Region(0, len(name))) + + def on_done(self, old_path: str | None, new_name: str) -> None: + if new_name: + self.window.run_command('lsp_rename_path', { + "new_name": new_name, + "old_path": old_path + }) + + +class RenamePathInputHandler(sublime_plugin.TextInputHandler): def __init__(self, path: str) -> None: self.path = path @@ -44,17 +67,18 @@ def want_event(self) -> bool: def input(self, args: dict) -> sublime_plugin.TextInputHandler | None: if "new_name" in args: return None - old_path = self.get_old_path(args.get('paths'), self.window.active_view()) - return RenameFileInputHandler(Path(old_path or "").name) + view = self.window.active_view() + old_path = view.file_name() if view else None + return RenamePathInputHandler(Path(old_path or "").name) def run( self, new_name: str, # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy - paths: list[str] | None = None, # exist when invoked from the sidebar with "LSP: Rename..." + old_path: str | None = None, ) -> None: session = self.session() view = self.window.active_view() - old_path = self.get_old_path(paths, view) + old_path = old_path or view.file_name() if view else None if old_path is None: # handle renaming buffers if view: view.set_name(new_name) @@ -69,12 +93,8 @@ def run( "oldUri": urljoin("file:", old_path), }] } - if not session: - self.rename_path(old_path, new_path) - self.notify_did_rename(rename_file_params, new_path, view) - return - file_operation_options = session.get_capability('workspace.fileOperations.willRename') - if file_operation_options and match_file_operation_filters(file_operation_options, old_path, view): + file_operation_options = session.get_capability('workspace.fileOperations.willRename') if session else None + if session and file_operation_options and match_file_operation_filters(file_operation_options, old_path, view): request = Request.willRenameFiles(rename_file_params) session.send_request( request, @@ -84,12 +104,6 @@ def run( self.rename_path(old_path, new_path) self.notify_did_rename(rename_file_params, new_path, view) - def get_old_path(self, paths: list[str] | None, view: sublime.View | None) -> str | None: - if paths: - return paths[0] - if view: - return view.file_name() - def handle(self, res: WorkspaceEdit | None, session_name: str, old_path: str, new_path: str, rename_file_params: RenameFilesParams, view: sublime.View | None) -> None: if session := self.session_by_name(session_name): From 728c6eac9a8dff9d073ab4ef71f3185461e6462a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 4 Jul 2024 00:19:23 +0200 Subject: [PATCH 30/37] Update plugin/core/types.py Co-authored-by: jwortmann --- plugin/core/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/core/types.py b/plugin/core/types.py index 76a775d20..3b49d070c 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -469,8 +469,8 @@ def matches(file_operation_filter: FileOperationFilter) -> bool: return False return True - filters = [matches(file_operation_filter) for file_operation_filter in file_operation_options.get('filters')] - return any(filters) if filters else True + filters = file_operation_options.get('filters') + return any(matches(_filter) for _filter in filters) if filters else True # method -> (capability dotted path, optional registration dotted path) From 93b024ab80717824bf2ff4ae23e0e2bae5a64858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 4 Jul 2024 00:18:18 +0200 Subject: [PATCH 31/37] remove unnecessary if --- plugin/core/types.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugin/core/types.py b/plugin/core/types.py index 3b49d070c..e35bb76f5 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -456,11 +456,10 @@ def matches(file_operation_filter: FileOperationFilter) -> bool: return False if pattern: matches = pattern.get('matches') - if matches: - if matches == FileOperationPatternKind.File and os.path.isdir(path): - return False - if matches == FileOperationPatternKind.Folder and os.path.isfile(path): - return False + if matches == FileOperationPatternKind.File and os.path.isdir(path): + return False + if matches == FileOperationPatternKind.Folder and os.path.isfile(path): + return False options = pattern.get('options', {}) flags = GLOBSTAR | BRACE if options.get('ignoreCase', False): From 1ff4648380a11baee7cb4168c9f4e88562054799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 4 Jul 2024 00:26:53 +0200 Subject: [PATCH 32/37] remove LSP: Rename... from sidebar in favor of overriding the existing ST rename_path command --- Default.sublime-commands | 2 +- Side Bar.sublime-menu | 8 -------- boot.py | 8 ++++++-- plugin/rename_file.py | 6 +++--- 4 files changed, 10 insertions(+), 14 deletions(-) delete mode 100644 Side Bar.sublime-menu diff --git a/Default.sublime-commands b/Default.sublime-commands index 631920f88..2f64684d2 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -153,7 +153,7 @@ }, { "caption": "LSP: Rename File", - "command": "lsp_rename_path" + "command": "lsp_rename_file" }, { "caption": "LSP: Code Action", diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu deleted file mode 100644 index ae55009a7..000000000 --- a/Side Bar.sublime-menu +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "caption": "LSP: Rename...", - "mnemonic": "l", - "command": "lsp_rename_path_sidebar", - "args": {"paths": []} - } -] diff --git a/boot.py b/boot.py index 020add216..16ce854b0 100644 --- a/boot.py +++ b/boot.py @@ -69,8 +69,8 @@ from .plugin.references import LspSymbolReferencesCommand from .plugin.rename import LspHideRenameButtonsCommand from .plugin.rename import LspSymbolRenameCommand +from .plugin.rename_file import LspRenameFileCommand from .plugin.rename_file import LspRenamePathCommand -from .plugin.rename_file import LspRenamePathSidebarCommand from .plugin.save_command import LspSaveAllCommand from .plugin.save_command import LspSaveCommand from .plugin.selection_range import LspExpandSelectionCommand @@ -148,8 +148,8 @@ "LspSymbolImplementationCommand", "LspSymbolReferencesCommand", "LspSymbolRenameCommand", + "LspRenameFileCommand", "LspRenamePathCommand", - "LspRenamePathSidebarCommand", "LspSymbolTypeDefinitionCommand", "LspToggleCodeLensesCommand", "LspToggleHoverPopupsCommand", @@ -265,6 +265,10 @@ def on_pre_close(self, view: sublime.View) -> None: tup[1](None) break + def on_window_command(self, window: sublime.Window, command_name: str, args: dict) -> tuple[str, dict] | None: + if command_name == "rename_path": + return ("lsp_rename_path", args) + def on_post_window_command(self, window: sublime.Window, command_name: str, args: dict[str, Any] | None) -> None: if command_name == "show_panel": wm = windows.lookup(window) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index ff791089e..799bb7a69 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -12,7 +12,7 @@ import functools -class LspRenamePathSidebarCommand(LspWindowCommand): +class LspRenamePathCommand(LspWindowCommand): def run(self, paths: list[str] | None = None) -> None: old_path = paths[0] if paths else None path_name = Path(old_path or "").name @@ -28,7 +28,7 @@ def run(self, paths: list[str] | None = None) -> None: def on_done(self, old_path: str | None, new_name: str) -> None: if new_name: - self.window.run_command('lsp_rename_path', { + self.window.run_command('lsp_rename_file', { "new_name": new_name, "old_path": old_path }) @@ -55,7 +55,7 @@ def validate(self, path: str) -> bool: return len(path) > 0 -class LspRenamePathCommand(LspWindowCommand): +class LspRenameFileCommand(LspWindowCommand): capability = 'workspace.fileOperations.willRename' def is_enabled(self): From 1ae7ec45a7dc1e5241e1366f3d350ea90aa86546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 4 Jul 2024 00:46:02 +0200 Subject: [PATCH 33/37] always enable LspRenamePathCommand --- plugin/rename_file.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 799bb7a69..4c6ec1b52 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -13,6 +13,9 @@ class LspRenamePathCommand(LspWindowCommand): + def is_enabled(self): + return True + def run(self, paths: list[str] | None = None) -> None: old_path = paths[0] if paths else None path_name = Path(old_path or "").name From 61816445e486bcec50a1546767f61cb56612011e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 4 Jul 2024 00:46:33 +0200 Subject: [PATCH 34/37] handle OS errors --- plugin/rename_file.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 4c6ec1b52..b47b37c45 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -125,7 +125,10 @@ def rename_path(self, old_path: str, new_path: str) -> None: if not os.path.exists(new_dir): os.makedirs(new_dir) isdir = os.path.isdir(old_path) - os.rename(old_path, new_path) + try: + os.rename(old_path, new_path) + except: + sublime.status_message("Unable to rename") if isdir: for v in self.window.views(): file_name = v.file_name() From 181f17c25e92f9c43d0845eec6b9df7919a71383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Thu, 4 Jul 2024 12:23:59 +0200 Subject: [PATCH 35/37] except Exception --- plugin/rename_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index b47b37c45..399828671 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -127,7 +127,7 @@ def rename_path(self, old_path: str, new_path: str) -> None: isdir = os.path.isdir(old_path) try: os.rename(old_path, new_path) - except: + except Exception: sublime.status_message("Unable to rename") if isdir: for v in self.window.views(): From 18e854902c8f92e3731707e2d9340ab79b528069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Sun, 18 Aug 2024 12:27:12 +0200 Subject: [PATCH 36/37] Remove "LSP: Rename File" commands, instead override Default ST commands --- Default.sublime-commands | 4 ---- boot.py | 6 +++--- plugin/rename_file.py | 37 ++++++++----------------------------- 3 files changed, 11 insertions(+), 36 deletions(-) diff --git a/Default.sublime-commands b/Default.sublime-commands index 2f64684d2..75bb04d95 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -151,10 +151,6 @@ "caption": "LSP: Rename", "command": "lsp_symbol_rename" }, - { - "caption": "LSP: Rename File", - "command": "lsp_rename_file" - }, { "caption": "LSP: Code Action", "command": "lsp_code_actions" diff --git a/boot.py b/boot.py index 16ce854b0..c4a791485 100644 --- a/boot.py +++ b/boot.py @@ -70,7 +70,7 @@ from .plugin.rename import LspHideRenameButtonsCommand from .plugin.rename import LspSymbolRenameCommand from .plugin.rename_file import LspRenameFileCommand -from .plugin.rename_file import LspRenamePathCommand +from .plugin.rename_file import RenameFileCommand from .plugin.save_command import LspSaveAllCommand from .plugin.save_command import LspSaveCommand from .plugin.selection_range import LspExpandSelectionCommand @@ -149,7 +149,7 @@ "LspSymbolReferencesCommand", "LspSymbolRenameCommand", "LspRenameFileCommand", - "LspRenamePathCommand", + "RenameFileCommand", "LspSymbolTypeDefinitionCommand", "LspToggleCodeLensesCommand", "LspToggleHoverPopupsCommand", @@ -267,7 +267,7 @@ def on_pre_close(self, view: sublime.View) -> None: def on_window_command(self, window: sublime.Window, command_name: str, args: dict) -> tuple[str, dict] | None: if command_name == "rename_path": - return ("lsp_rename_path", args) + return ("rename_file", args) def on_post_window_command(self, window: sublime.Window, command_name: str, args: dict[str, Any] | None) -> None: if command_name == "show_panel": diff --git a/plugin/rename_file.py b/plugin/rename_file.py index 399828671..e24824010 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -12,13 +12,20 @@ import functools -class LspRenamePathCommand(LspWindowCommand): +# It is bad that this command is named RenameFileCommand, same as the command in Default/rename.py +# ST has a bug that prevents the RenameFileCommand to be override in on_window_command: +# https://github.com/sublimehq/sublime_text/issues/2234 +# So naming this command "RenameFileCommand" is one BAD way to override the rename behavior. +class RenameFileCommand(LspWindowCommand): def is_enabled(self): return True def run(self, paths: list[str] | None = None) -> None: old_path = paths[0] if paths else None path_name = Path(old_path or "").name + view = self.window.active_view() + if path_name == "" and view: + path_name = Path(view.file_name() or "").name v = self.window.show_input_panel( "(LSP) New Name:", path_name, @@ -37,27 +44,6 @@ def on_done(self, old_path: str | None, new_name: str) -> None: }) -class RenamePathInputHandler(sublime_plugin.TextInputHandler): - def __init__(self, path: str) -> None: - self.path = path - - def name(self) -> str: - return "new_name" - - def placeholder(self) -> str: - return self.path - - def initial_text(self) -> str: - return self.placeholder() - - def initial_selection(self) -> list[tuple[int, int]]: - name, _ext = os.path.splitext(self.path) - return [(0, len(name))] - - def validate(self, path: str) -> bool: - return len(path) > 0 - - class LspRenameFileCommand(LspWindowCommand): capability = 'workspace.fileOperations.willRename' @@ -67,13 +53,6 @@ def is_enabled(self): def want_event(self) -> bool: return False - def input(self, args: dict) -> sublime_plugin.TextInputHandler | None: - if "new_name" in args: - return None - view = self.window.active_view() - old_path = view.file_name() if view else None - return RenamePathInputHandler(Path(old_path or "").name) - def run( self, new_name: str, # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy From 51962f1fdcd138ae0647fe688a5065a169029164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Sun, 18 Aug 2024 12:28:34 +0200 Subject: [PATCH 37/37] remove unused import --- plugin/rename_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/rename_file.py b/plugin/rename_file.py index e24824010..a73447207 100644 --- a/plugin/rename_file.py +++ b/plugin/rename_file.py @@ -8,7 +8,6 @@ from urllib.parse import urljoin import os import sublime -import sublime_plugin import functools