Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add willRename and didRename fileOperations #2498

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
39 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
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
4 changes: 4 additions & 0 deletions Default.sublime-commands
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@
"caption": "LSP: Rename",
"command": "lsp_symbol_rename"
},
{
"caption": "LSP: Rename File",
"command": "lsp_rename_path"
jwortmann marked this conversation as resolved.
Show resolved Hide resolved
},
{
"caption": "LSP: Code Action",
"command": "lsp_code_actions"
Expand Down
8 changes: 8 additions & 0 deletions Side Bar.sublime-menu
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
jwortmann marked this conversation as resolved.
Show resolved Hide resolved
{
"caption": "LSP: Rename...",
"mnemonic": "l",
"command": "lsp_rename_path_sidebar",
"args": {"paths": []}
}
]
4 changes: 4 additions & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
from .plugin.references import LspSymbolReferencesCommand
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
Expand Down Expand Up @@ -146,6 +148,8 @@
"LspSymbolImplementationCommand",
"LspSymbolReferencesCommand",
"LspSymbolRenameCommand",
"LspRenamePathCommand",
"LspRenamePathSidebarCommand",
"LspSymbolTypeDefinitionCommand",
"LspToggleCodeLensesCommand",
"LspToggleHoverPopupsCommand",
Expand Down
8 changes: 8 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
33 changes: 33 additions & 0 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from .collections import DottedDict
from .file_watcher import FileWatcherEventType
from .logging import debug, set_debug_logging
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
Expand All @@ -10,6 +13,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 @@ -440,6 +444,35 @@ 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(
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] != scheme:
return False
if pattern:
matches = pattern.get('matches')
if matches:
jwortmann marked this conversation as resolved.
Show resolved Hide resolved
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):
flags |= IGNORECASE
if not globmatch(path, pattern['glob'], flags=flags):
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
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved


# 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
145 changes: 145 additions & 0 deletions plugin/rename_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from __future__ import annotations

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
from pathlib import Path
from urllib.parse import urljoin
import os
import sublime
import sublime_plugin
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

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 LspRenamePathCommand(LspWindowCommand):
capability = 'workspace.fileOperations.willRename'

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
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
old_path: str | None = None,
) -> None:
session = self.session()
view = self.window.active_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)
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
rename_file_params: RenameFilesParams = {
"files": [{
"newUri": urljoin("file:", new_path),
"oldUri": urljoin("file:", old_path),
}]
}
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,
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 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):
# 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, new_path, view)

def rename_path(self, old_path: str, new_path: str) -> None:
old_regions: list[sublime.Region] = []
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
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 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:
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(restore_regions)

def notify_did_rename(self, rename_file_params: RenameFilesParams, path: str, view: sublime.View | None):
for s in self.sessions():
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))
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