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] 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)) +