Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
139 commits
Select commit Hold shift + click to select a range
f5ec6c5
add willRename and didRename fileOperations
predragnikolic Jun 25, 2024
e26a48e
always enable
predragnikolic Jun 25, 2024
e6304c2
add renaming from the sidebar
predragnikolic Jun 25, 2024
e5e0722
changes
predragnikolic Jun 25, 2024
5c91301
fix styles
predragnikolic Jun 25, 2024
075e50e
add new line
predragnikolic Jun 25, 2024
814b0bb
hande if new path directory doesn't exist
predragnikolic Jun 25, 2024
fb42ffc
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Jun 25, 2024
d488b06
handle renaming buffers
predragnikolic Jun 26, 2024
d1a10cf
ahh...
predragnikolic Jun 26, 2024
08e3af1
add initial_selection
predragnikolic Jun 26, 2024
6f65462
improve initial_selection and rename file_name to path
predragnikolic Jun 26, 2024
c9e8772
Split "Lsp: Rename" to "Lsp: Rename Folder" and "Lsp: Rename File"
predragnikolic Jun 27, 2024
e0c9c82
new line
predragnikolic Jun 27, 2024
a52b5c4
when renaming a directory, it would be good to retarget all open view…
predragnikolic Jun 27, 2024
2acceb6
save some lines
predragnikolic Jun 27, 2024
9ab9dc5
remove more lines
predragnikolic Jun 27, 2024
f5c26a6
few less lines
predragnikolic Jun 27, 2024
437659f
simpler conditions
predragnikolic Jun 27, 2024
3647f92
avoid multiple statements on one line
predragnikolic Jun 27, 2024
3389f66
implement FileOperationFilter
predragnikolic Jul 2, 2024
4785d49
remomve few lines
predragnikolic Jul 2, 2024
2f5af31
fix flake
predragnikolic Jul 2, 2024
890667c
fix pyright
predragnikolic Jul 2, 2024
af04c56
remove is_visible code
predragnikolic Jul 2, 2024
178adae
remove LSP: Rename File and Rename folder in favor of LSP: Rename... …
predragnikolic Jul 2, 2024
8ef443c
rename LspRenameFileCommand to LspRenamePathCommand
predragnikolic Jul 2, 2024
d1342b9
remove FileOperationFilterChecker, FileOperationFilterMatcher in favo…
predragnikolic Jul 2, 2024
057eaff
flake8 fixes
predragnikolic Jul 2, 2024
cb82086
cannot use an input handler to rename folder because it always displa…
predragnikolic Jul 2, 2024
728c6ea
Update plugin/core/types.py
predragnikolic Jul 3, 2024
93b024a
remove unnecessary if
predragnikolic Jul 3, 2024
1ff4648
remove LSP: Rename... from sidebar in favor of overriding the existin…
predragnikolic Jul 3, 2024
1ae7ec4
always enable LspRenamePathCommand
predragnikolic Jul 3, 2024
6181644
handle OS errors
predragnikolic Jul 3, 2024
181f17c
except Exception
predragnikolic Jul 4, 2024
18e8549
Remove "LSP: Rename File" commands, instead override Default ST commands
predragnikolic Aug 18, 2024
51962f1
remove unused import
predragnikolic Aug 18, 2024
21f1bf7
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Aug 20, 2024
814cb6b
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Feb 17, 2025
0e0daa7
prevent save prompt to show up
predragnikolic Feb 17, 2025
51670f9
Introduce "LSP: Rename Path" and rename "LSP: Rename" to "LSP: Rename…
predragnikolic Feb 17, 2025
f530756
fix flake
predragnikolic Feb 17, 2025
5df5b43
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Nov 20, 2025
defedc7
Update plugin/rename_file.py
predragnikolic Nov 21, 2025
848e6cd
Update plugin/rename_file.py
predragnikolic Nov 21, 2025
7cea663
return early in case of exception
predragnikolic Nov 21, 2025
d198d3f
rename handle to _handle_response_async
predragnikolic Nov 21, 2025
d1c2ba8
move session closer to where it is used
predragnikolic Nov 21, 2025
26f53d3
add return types
predragnikolic Nov 21, 2025
f292c0c
use filename_to_uri
predragnikolic Nov 21, 2025
2ffe948
dont use abbreviations
predragnikolic Nov 21, 2025
50a80aa
pattern and filters are required fields in the LSP spec
predragnikolic Nov 22, 2025
54626f4
import from ..protocol
predragnikolic Nov 22, 2025
9c608fe
remove view arg in match_file_operation_filters and update code
predragnikolic Nov 22, 2025
6b8fd30
trigger rename_path and notify_did_rename after apply_workspace_edit_…
predragnikolic Nov 22, 2025
672cdf2
Update boot.py
predragnikolic Nov 22, 2025
4fcac0e
use path resolve and inline run method signature
predragnikolic Nov 22, 2025
ca12477
make it more obvious that this is the file_name
predragnikolic Nov 22, 2025
c7f26ff
fix rename folder from sidebar
predragnikolic Nov 22, 2025
fb9a710
remove if for pattern
predragnikolic Nov 22, 2025
06accb2
Update plugin/core/types.py
predragnikolic Nov 22, 2025
28b1e1b
Update plugin/rename_file.py
predragnikolic Nov 22, 2025
add6233
fix flake
predragnikolic Nov 22, 2025
e004c0b
early return if new_name is like old name
predragnikolic Nov 28, 2025
d0b7d49
have same check for is_case_change as ST native rename command
predragnikolic Nov 28, 2025
a5c0609
be consistent, this was the only method that started with _
predragnikolic Nov 28, 2025
9f667a3
notify_did_rename only after a successful rename
predragnikolic Nov 28, 2025
7f1d829
Update plugin/rename_file.py
predragnikolic Nov 29, 2025
a83b956
Update plugin/rename_file.py
predragnikolic Nov 29, 2025
4089bfe
Update plugin/rename_file.py
predragnikolic Nov 29, 2025
02bbbd8
Update plugin/rename_file.py
predragnikolic Nov 29, 2025
55dbdd7
Use Path instead of os.path
predragnikolic Nov 29, 2025
ca52462
flake
predragnikolic Nov 29, 2025
dc23125
move a bit closer to the if branch
predragnikolic Nov 29, 2025
092a179
rename file_operation_options to file_operations for consistency
predragnikolic Nov 29, 2025
4af0ae6
move notify did rename later
predragnikolic Nov 29, 2025
be87d19
check if old_path is a directory before we rename
predragnikolic Nov 29, 2025
6afa7c2
fix flake
predragnikolic Nov 29, 2025
72a162d
don't consider matching if filters is an empty array
predragnikolic Nov 29, 2025
fbcbec5
send request to all sessions
predragnikolic Nov 29, 2025
140ffca
fix flake
predragnikolic Nov 29, 2025
91c34cf
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Nov 29, 2025
057391a
schedule the requests to be done on the async thread
predragnikolic Nov 29, 2025
48650c5
fix flake by renaming the method
predragnikolic Nov 29, 2025
7e87b26
visual indent
predragnikolic Nov 29, 2025
0db107e
Update plugin/core/types.py
predragnikolic Nov 30, 2025
a485fef
this check makes more sense
predragnikolic Nov 30, 2025
9cd3a8b
slightly move line down
predragnikolic Nov 30, 2025
211d151
Update plugin/rename_file.py
predragnikolic Nov 30, 2025
cd97fa3
create_request_async -> create_request
predragnikolic Nov 30, 2025
cfa5799
call self.run_async from set_timeout_async
predragnikolic Nov 30, 2025
083245b
Apply Rafal's suggestions
predragnikolic Nov 30, 2025
23d745e
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Dec 1, 2025
0521ed2
Revert "Merge branch 'main' into add-will-rename-and-did-rename" beca…
predragnikolic Dec 1, 2025
8a93340
use functools.partial
predragnikolic Dec 1, 2025
5dde553
set parents=True to prevent exception that can be thrown if renaming …
predragnikolic Dec 1, 2025
cf95964
be consistent - Rename LspRenameFromSidebarOverride to LspRenameFromS…
predragnikolic Dec 1, 2025
630b29e
sort
predragnikolic Dec 1, 2025
9149454
notify_did_rename after all renames
predragnikolic Dec 1, 2025
2028af2
notify_did_rename after all renames - but a bit differently
predragnikolic Dec 1, 2025
4616c1a
Reapply "Merge branch 'main' into add-will-rename-and-did-rename" bec…
predragnikolic Dec 1, 2025
1f36a76
Update plugin/rename_file.py
predragnikolic Dec 1, 2025
0a3fe16
apply all edit promises, then call rename file, then send didRename n…
predragnikolic Dec 1, 2025
37fe35f
simplifications
rchl Dec 1, 2025
f1af519
Revert "simplifications"
predragnikolic Dec 1, 2025
08ae452
Make sure to send didClose even when we rename a folder
predragnikolic Dec 1, 2025
0d31e49
await promise chain
rchl Dec 5, 2025
43ef16b
simpler
rchl Dec 5, 2025
e96b382
silly lint
rchl Dec 5, 2025
13d2169
Merge remote-tracking branch 'origin/main' into add-will-rename-and-d…
rchl Dec 5, 2025
ad57ceb
Update plugin/rename_file.py
predragnikolic Dec 5, 2025
59c982a
remove FileName and Group types
predragnikolic Dec 5, 2025
8f85694
replace only one occurrence
predragnikolic Dec 5, 2025
4e707c0
only save view if dirty
predragnikolic Dec 5, 2025
3fd0a0e
extract restore_view to method
predragnikolic Dec 5, 2025
1b0f2c0
fix lint
predragnikolic Dec 5, 2025
84a4123
fix bug where the filename_to_uri will set with "res:"
predragnikolic Dec 6, 2025
2e7c60b
Update plugin/rename_file.py
predragnikolic Dec 8, 2025
0303148
make errors messages consistent
predragnikolic Dec 8, 2025
3332ab5
remove unused import
predragnikolic Dec 8, 2025
5c347fb
rename group_index to group
predragnikolic Dec 8, 2025
627cd92
rename group_index to group
predragnikolic Dec 8, 2025
4c36739
Update plugin/rename_file.py
predragnikolic Dec 8, 2025
c672e2b
rename group_index to group on one missed place
predragnikolic Dec 8, 2025
10fce17
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Dec 8, 2025
a625909
Bug, compare new_path with old_path instead of new_name with old_path
predragnikolic Dec 8, 2025
69a5dfb
make sure to restore the views in the original left-to-right order.
predragnikolic Dec 8, 2025
fce6991
focus the last active view after restore
predragnikolic Dec 8, 2025
af48156
reorder methods by the order they are called in the code
predragnikolic Dec 9, 2025
42c701f
is_case_change as staticmethod
predragnikolic Dec 9, 2025
288c6cc
make it a bit easier to see what happens
predragnikolic Dec 9, 2025
b27aa61
remove the LspRenameFromSidebarOverrideCommand command
predragnikolic Dec 9, 2025
7f72ce2
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Dec 13, 2025
44dd412
only apply workspace edits from one server
predragnikolic Dec 13, 2025
3267e02
send will rename request to only one session
predragnikolic Dec 13, 2025
9cdde00
Revert "send will rename request to only one session"
predragnikolic Dec 13, 2025
58b76e4
send the request to all sessions, but then only apply one of the resp…
predragnikolic Dec 13, 2025
01bcb71
Merge branch 'main' into add-will-rename-and-did-rename
predragnikolic Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Default.sublime-commands
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@
"command": "lsp_show_diagnostics_panel"
},
{
"caption": "LSP: Rename",
"caption": "LSP: Rename Path",
"command": "lsp_rename_path"
},
{
"caption": "LSP: Rename Symbol",
"command": "lsp_symbol_rename"
},
{
Expand Down
6 changes: 6 additions & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from .plugin.references import LspSymbolReferencesCommand
from .plugin.rename import LspHideRenameButtonsCommand
from .plugin.rename import LspSymbolRenameCommand
from .plugin.rename_file import LspRenamePathCommand
from .plugin.save_command import LspSaveAllCommand
from .plugin.save_command import LspSaveCommand
from .plugin.selection_range import LspExpandSelectionCommand
Expand Down Expand Up @@ -130,6 +131,7 @@
"LspParseVscodePackageJson",
"LspPrevDiagnosticCommand",
"LspRefactorCommand",
"LspRenamePathCommand",
"LspResolveDocsCommand",
"LspRestartServerCommand",
"LspRunTextCommandHelperCommand",
Expand Down Expand Up @@ -270,6 +272,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)
Expand Down
8 changes: 8 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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)
Expand Down Expand Up @@ -249,6 +253,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)
Expand Down
5 changes: 5 additions & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,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
},
Expand Down
25 changes: 25 additions & 0 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations
from ...protocol import FileOperationFilter
from ...protocol import FileOperationPatternKind
from ...protocol import ServerCapabilities
from ...protocol import TextDocumentSyncKind
from ...protocol import TextDocumentSyncOptions
from ...protocol import URI
from .collections import DottedDict
from .constants import LANGUAGE_IDENTIFIERS
from .file_watcher import FileWatcherEventType
Expand All @@ -13,6 +16,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
Expand Down Expand Up @@ -457,6 +461,27 @@ def matches(self, view: sublime.View) -> bool:
return any(f(view) for f in self.filters) if self.filters else True


def match_file_operation_filters(filters: list[FileOperationFilter], uri: URI) -> bool:
def matches(file_operation_filter: FileOperationFilter) -> bool:
uri_scheme, file_name = parse_uri(uri)
pattern = file_operation_filter['pattern']
scheme = file_operation_filter.get('scheme')
if scheme and uri_scheme != scheme:
return False
matches = pattern.get('matches')
if matches == FileOperationPatternKind.File and not os.path.isfile(file_name):
return False
if matches == FileOperationPatternKind.Folder and not os.path.isdir(file_name):
return False
options = pattern.get('options', {})
flags = GLOBSTAR | BRACE
if options.get('ignoreCase', False):
flags |= IGNORECASE
return globmatch(file_name, pattern['glob'], flags=flags)

return any(matches(_filter) for _filter in filters)


# 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]] = {
Expand Down
152 changes: 152 additions & 0 deletions plugin/rename_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from __future__ import annotations
from .core.open import open_file_uri
from .core.promise import Promise
from .core.protocol import Notification, Request
from .core.registry import LspWindowCommand
from .core.types import match_file_operation_filters
from .core.url import filename_to_uri
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING
import sublime
import sublime_plugin
import weakref

if TYPE_CHECKING:
from ..protocol import FileRename
from ..protocol import WorkspaceEdit
from .core.sessions import Session
from collections.abc import Generator


class RenamePathInputHandler(sublime_plugin.TextInputHandler):
def __init__(self, path: str) -> None:
self.path = Path(path)

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

def placeholder(self) -> str:
return self.path.name

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

def initial_selection(self) -> list[tuple[int, int]]:
return [(0, len(self.path.stem))]

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


class LspRenamePathCommand(LspWindowCommand):
capability = 'workspace.fileOperations.willRename'

def is_enabled(self) -> bool:
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
if paths := args.get('paths'): # command was called from side bar context menu
return RenamePathInputHandler(paths[0])
if (view := self.window.active_view()) and (file_name := view.file_name()):
return RenamePathInputHandler(file_name)
return RenamePathInputHandler("")

def run(self, new_name: str, paths: list[str] | None = None) -> None:
old_path = paths[0] if paths else None
view = self.window.active_view()
if old_path is None and view:
old_path = view.file_name()
if old_path is None: # handle renaming buffers
if view:
view.set_name(new_name)
return
# new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy
resolved_new_path = (Path(old_path).parent / new_name).resolve()
new_path = str(resolved_new_path)
if new_path == old_path:
return
if resolved_new_path.exists() and not self.is_case_change(old_path, new_path):
self.window.status_message('Rename error: Target already exists')
return
sublime.set_timeout_async(lambda: self.run_async(old_path, new_path))

@staticmethod
def is_case_change(path_a: str, path_b: str) -> bool:
return path_a.lower() == path_b.lower() and Path(path_a).stat().st_ino == Path(path_b).stat().st_ino

def run_async(self, old_path: str, new_path: str) -> None:
file_rename: FileRename = {
"newUri": filename_to_uri(new_path),
"oldUri": filename_to_uri(old_path)
}
Promise.all(list(self.create_will_rename_requests_async(file_rename))) \
.then(lambda responses: self.handle_rename_async(responses)) \
.then(lambda _: self.rename_path(old_path, new_path)) \
.then(lambda success: self.notify_did_rename(file_rename) if success else None)

def create_will_rename_requests_async(
self, file_rename: FileRename
) -> Generator[Promise[tuple[WorkspaceEdit | None, weakref.ref[Session]]]]:
for session in self.sessions():
filters = session.get_capability('workspace.fileOperations.willRename.filters') or []
if match_file_operation_filters(filters, file_rename['oldUri']):
yield session.send_request_task(Request.willRenameFiles({'files': [file_rename]})) \
.then(partial(lambda weak_session, response: (response, weak_session), weakref.ref(session)))

def handle_rename_async(self, responses: list[tuple[WorkspaceEdit | None, weakref.ref[Session]]]) -> Promise[None]:
for response, weak_session in responses:
if (session := weak_session()) and response:
return session.apply_workspace_edit_async(response, is_refactoring=True)
return Promise.resolve(None)

def rename_path(self, old: str, new: str) -> Promise[bool]:
old_path = Path(old)
new_path = Path(new)
restore_files: list[tuple[str, tuple[int, int], list[sublime.Region]]] = []
active_view = self.window.active_view()
last_active_view: str | None = active_view.file_name() if active_view else None
for view in reversed(self.window.views()):
if (file_name := view.file_name()) and file_name.startswith(str(old_path)):
new_file_name = file_name.replace(str(old_path), str(new_path), 1)
if view == active_view:
last_active_view = new_file_name
restore_files.append((new_file_name, self.window.get_view_index(view), list(view.sel())))
if view.is_dirty():
view.run_command('save', {'async': False})
view.close() # LSP spec - send didClose for the old file
if (new_dir := new_path.parent) and not new_dir.exists():
new_dir.mkdir(parents=True)
try:
old_path.rename(new_path)
except Exception as error:
sublime.status_message(f"Rename error: {error}")
return Promise.resolve(False)
return Promise.all([
open_file_uri(self.window, file_name, group=group[0]).then(partial(self.restore_view, selection, group))
for file_name, group, selection in reversed(restore_files)
]).then(lambda _: self.focus_view(last_active_view)).then(lambda _: True)

def notify_did_rename(self, file_rename: FileRename) -> None:
for session in self.sessions():
filters = session.get_capability('workspace.fileOperations.didRename.filters') or []
if filters and match_file_operation_filters(filters, file_rename['oldUri']):
session.send_notification(Notification.didRenameFiles({'files': [file_rename]}))

def restore_view(self, selection: list[sublime.Region], group: tuple[int, int], view: sublime.View | None) -> None:
if not view:
return
group_index, tab_index = group
self.window.set_view_index(view, group_index, tab_index)
if selection:
view.sel().clear()
view.sel().add_all(selection)

def focus_view(self, path: str | None) -> None:
if path and (view := self.window.find_open_file(path)):
self.window.focus_view(view)
1 change: 1 addition & 0 deletions stubs/wcmatch/glob.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ from typing import Any, Optional

BRACE: int = ...
GLOBSTAR: int = ...
IGNORECASE: int = ...


def globmatch(
Expand Down
Loading