Skip to content

Commit

Permalink
implement FileOperationFilter
Browse files Browse the repository at this point in the history
  • Loading branch information
predragnikolic committed Jul 2, 2024
1 parent 3647f92 commit 3389f66
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 13 deletions.
67 changes: 66 additions & 1 deletion plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
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
from typing import cast
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 +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]] = {
Expand Down
36 changes: 24 additions & 12 deletions plugin/rename_file.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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] = []
Expand All @@ -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))

0 comments on commit 3389f66

Please sign in to comment.