From 7a86879cd7a2d1654ddb07622f8c80913a4d19a0 Mon Sep 17 00:00:00 2001 From: Jack Cherng Date: Wed, 1 Nov 2023 22:28:46 +0800 Subject: [PATCH] refactor: tidy codes Signed-off-by: Jack Cherng --- plugin/commands/auto_set_syntax.py | 35 +++----- ...to_set_syntax_create_new_implementation.py | 10 +-- .../auto_set_syntax_debug_information.py | 4 +- plugin/guesslang/client.py | 12 ++- plugin/guesslang/server.py | 56 +++++++------ plugin/listener.py | 10 +-- plugin/logger.py | 22 +++-- plugin/rules/constraint.py | 9 ++- plugin/rules/constraints/contains.py | 2 +- .../rules/constraints/first_line_contains.py | 2 +- plugin/rules/constraints/is_extension.py | 2 +- plugin/rules/constraints/is_in_git_repo.py | 2 +- plugin/rules/constraints/is_in_hg_repo.py | 2 +- .../is_in_python_django_project.py | 2 +- .../is_in_ruby_on_rails_project.py | 2 +- plugin/rules/constraints/is_in_svn_repo.py | 2 +- plugin/rules/constraints/is_interpreter.py | 2 +- plugin/rules/constraints/is_line_count.py | 4 +- plugin/rules/constraints/is_name.py | 2 +- plugin/rules/constraints/is_size.py | 4 +- plugin/rules/constraints/is_syntax.py | 2 +- plugin/rules/constraints/name_contains.py | 2 +- plugin/rules/constraints/path_contains.py | 2 +- plugin/rules/constraints/relative_exists.py | 2 +- plugin/rules/constraints/selector_matches.py | 2 +- plugin/rules/matches/all.py | 4 +- plugin/rules/matches/any.py | 4 +- plugin/rules/matches/ratio.py | 4 +- plugin/rules/matches/some.py | 4 +- plugin/shared.py | 61 +++++++------- plugin/snapshot.py | 80 +++++++++++-------- plugin/types.py | 41 +++++++++- plugin/utils.py | 2 +- 33 files changed, 214 insertions(+), 182 deletions(-) diff --git a/plugin/commands/auto_set_syntax.py b/plugin/commands/auto_set_syntax.py index ce988581..f141b1b0 100644 --- a/plugin/commands/auto_set_syntax.py +++ b/plugin/commands/auto_set_syntax.py @@ -1,18 +1,17 @@ from __future__ import annotations +from collections.abc import Callable import re -import uuid -from contextlib import contextmanager from functools import wraps from itertools import chain from operator import itemgetter from pathlib import Path -from typing import Any, Callable, Generator, Iterable, Mapping, TypeVar, cast +from typing import Any, Mapping, TypeVar, cast import sublime import sublime_plugin -from ..constants import PLUGIN_NAME, RE_ST_SYNTAX_TEST_LINE, RE_VIM_SYNTAX_LINE, VIEW_RUN_ID_SETTINGS_KEY +from ..constants import PLUGIN_NAME, RE_ST_SYNTAX_TEST_LINE, RE_VIM_SYNTAX_LINE from ..guesslang.types import GuesslangServerPredictionItem, GuesslangServerResponse from ..helpers import is_syntaxable_view from ..libs import websocket @@ -20,7 +19,7 @@ from ..rules import SyntaxRuleCollection from ..settings import get_merged_plugin_setting, get_merged_plugin_settings, pref_trim_suffixes from ..shared import G -from ..snapshot import ViewSnapshot, ViewSnapshotCollection +from ..snapshot import ViewSnapshot from ..types import ListenerEvent from ..utils import ( find_syntax_by_syntax_like, @@ -140,20 +139,6 @@ def _status_msg_and_log(msg: str, window: sublime.Window | None = None) -> None: sublime.status_message(msg) -@contextmanager -def _view_snapshot_context(view: sublime.View) -> Generator[ViewSnapshot, None, None]: - run_id = str(uuid.uuid4()) - settings = view.settings() - - try: - settings.set(VIEW_RUN_ID_SETTINGS_KEY, run_id) - ViewSnapshotCollection.add(run_id, view) - yield ViewSnapshotCollection.get(run_id) # type: ignore - finally: - settings.erase(VIEW_RUN_ID_SETTINGS_KEY) - ViewSnapshotCollection.pop(run_id) - - def _snapshot_view(failed_ret: Any = None) -> Callable[[_T_Callable], _T_Callable]: def decorator(func: _T_Callable) -> _T_Callable: @wraps(func) @@ -162,7 +147,7 @@ def wrapped(view: sublime.View, *args: Any, **kwargs: Any) -> Any: Logger.log("⏳ Calm down! View has gone or the plugin is not ready yet.") return failed_ret - with _view_snapshot_context(view): + with G.view_snapshot_collection.snapshot_context(view): return func(view, *args, **kwargs) return cast(_T_Callable, wrapped) @@ -184,7 +169,7 @@ def run_auto_set_syntax_on_view( if not ( (window := view.window()) and is_syntaxable_view(view, must_plaintext) - and (syntax_rule_collection := G.get_syntax_rule_collection(window)) + and (syntax_rule_collection := G.syntax_rule_collections.get(window)) ): return False @@ -258,7 +243,7 @@ def _assign_syntax_for_new_view(view: sublime.View, event: ListenerEvent | None def _assign_syntax_for_st_syntax_test(view: sublime.View, event: ListenerEvent | None = None) -> bool: if ( - (view_snapshot := ViewSnapshotCollection.get_by_view(view)) + (view_snapshot := G.view_snapshot_collection.get_by_view(view)) and (not view_snapshot.syntax or is_plaintext_syntax(view_snapshot.syntax)) and (m := RE_ST_SYNTAX_TEST_LINE.search(view_snapshot.first_line)) and (new_syntax := m.group("syntax")).endswith(".sublime-syntax") @@ -288,7 +273,7 @@ def _assign_syntax_with_plugin_rules( def _assign_syntax_with_first_line(view: sublime.View, event: ListenerEvent | None = None) -> bool: - if not (view_snapshot := ViewSnapshotCollection.get_by_view(view)): + if not (view_snapshot := G.view_snapshot_collection.get_by_view(view)): return False # Note that this only works for files under some circumstances. @@ -359,7 +344,7 @@ def _assign_syntax_with_trimmed_filename(view: sublime.View, event: ListenerEven def _assign_syntax_with_special_cases(view: sublime.View, event: ListenerEvent | None = None) -> bool: if not ( - (view_snapshot := ViewSnapshotCollection.get_by_view(view)) + (view_snapshot := G.view_snapshot_collection.get_by_view(view)) and view_snapshot.syntax and is_plaintext_syntax(view_snapshot.syntax) ): @@ -392,7 +377,7 @@ def is_json(view: sublime.View) -> bool: def _assign_syntax_with_guesslang_async(view: sublime.View, event: ListenerEvent | None = None) -> None: if not ( G.guesslang_client - and (view_snapshot := ViewSnapshotCollection.get_by_view(view)) + and (view_snapshot := G.view_snapshot_collection.get_by_view(view)) # don't apply on those have an extension and (event == ListenerEvent.COMMAND or "." not in view_snapshot.file_name_unhidden) # only apply on plain text syntax diff --git a/plugin/commands/auto_set_syntax_create_new_implementation.py b/plugin/commands/auto_set_syntax_create_new_implementation.py index c8c6693d..76851715 100644 --- a/plugin/commands/auto_set_syntax_create_new_implementation.py +++ b/plugin/commands/auto_set_syntax_create_new_implementation.py @@ -62,12 +62,10 @@ def _clone_file_as_template( new = window.new_file() new.run_command("append", {"characters": template}) - new.settings().update( - { - "default_dir": save_dir, - "is_auto_set_syntax_template_buffer": True, - } - ) + new.settings().update({ + "default_dir": save_dir, + "is_auto_set_syntax_template_buffer": True, + }) if syntax and (syntax := find_syntax_by_syntax_like(syntax)): new.assign_syntax(syntax) diff --git a/plugin/commands/auto_set_syntax_debug_information.py b/plugin/commands/auto_set_syntax_debug_information.py index c4ddc29c..c816173b 100644 --- a/plugin/commands/auto_set_syntax_debug_information.py +++ b/plugin/commands/auto_set_syntax_debug_information.py @@ -63,8 +63,8 @@ def run(self) -> None: }, } info["plugin_settings"] = get_merged_plugin_settings(window=self.window) - info["syntax_rule_collection"] = G.get_syntax_rule_collection(self.window) - info["dropped_rules"] = G.get_dropped_rules(self.window) + info["syntax_rule_collection"] = G.syntax_rule_collections.get(self.window) + info["dropped_rules"] = G.dropped_rules_collection.get(self.window, []) msg = TEMPLATE.format_map(_pythonize(info)) diff --git a/plugin/guesslang/client.py b/plugin/guesslang/client.py index 059bb4b2..261fa9e4 100644 --- a/plugin/guesslang/client.py +++ b/plugin/guesslang/client.py @@ -77,11 +77,9 @@ def request_guess_snapshot( if self.is_connected(): assert self._ws self._ws.send( - sublime.encode_value( - { - "id": view_snapshot.id, - "content": view_snapshot.content, - "event_name": event.value if event else None, - } - ) + sublime.encode_value({ + "id": view_snapshot.id, + "content": view_snapshot.content, + "event_name": event.value if event else None, + }) ) diff --git a/plugin/guesslang/server.py b/plugin/guesslang/server.py index 7925cd03..2b5761d1 100644 --- a/plugin/guesslang/server.py +++ b/plugin/guesslang/server.py @@ -14,18 +14,21 @@ class GuesslangServer: SERVER_DIR: Final[Path] = PLUGIN_STORAGE_DIR / "guesslang-server" - SERVER_FILE: Final[Path] = PLUGIN_STORAGE_DIR / "guesslang-server/websocket.js" + SERVER_FILE: Final[Path] = SERVER_DIR / "websocket.js" def __init__(self, host: str, port: int) -> None: self.host = host self.port = port - # background server process(es) - self._subprocesses: set[subprocess.Popen] = set() + self._proc: subprocess.Popen | None = None + """The server process.""" def start(self) -> bool: """Starts the guesslang server and return whether it starts.""" + if self._proc: + Logger.log("⚠️ Server is already running.") + return True if not (node_info := parse_node_path_args()): - Logger.log("❌ Node.js binary is not found or not executable") + Logger.log("❌ Node.js binary is not found or not executable.") return False node_path, node_args = node_info Logger.log(f"✔ Use Node.js binary ({node_path}) and args ({node_args})") @@ -42,35 +45,35 @@ def start(self) -> bool: }, ) except Exception as e: - Logger.log(f"❌ Failed starting guesslang server because {e}") + Logger.log(f"❌ Failed starting guesslang server: {e}") return False if process.stdout and process.stdout.read(2) == "OK": - self._subprocesses.add(process) + self._proc = process return True Logger.log("❌ Failed starting guesslang server.") return False def stop(self) -> None: - for p in self._subprocesses: - try: - p.kill() - except Exception: - pass - for p in self._subprocesses: - try: - p.wait() - except Exception: - pass - self._subprocesses.clear() + if not self._proc: + return + try: + self._proc.kill() + except Exception: + pass + try: + self._proc.wait() + except Exception: + pass + self._proc = None def restart(self) -> bool: self.stop() return self.start() def is_running(self) -> bool: - return len(self._subprocesses) > 0 + return self._proc is not None @staticmethod def _start_process( @@ -85,6 +88,9 @@ def _start_process( else: startupinfo = None # type: ignore + if isinstance(cmd, (str, Path)): + kwargs["shell"] = True + return subprocess.Popen( cmd, startupinfo=startupinfo, @@ -105,16 +111,16 @@ def parse_node_path_args() -> tuple[str, list[str]] | None: get_merged_plugin_setting("guesslang.node_bin_args"), ), ("${lsp_utils_node_bin}", []), - (shutil.which("electron"), []), - (shutil.which("node"), []), - (shutil.which("code"), ["--ms-enable-electron-run-as-node"]), # VSCode - (shutil.which("codium"), []), # VSCodium (non-Windows) - (shutil.which("VSCodium"), []), # VSCodium (Windows) + ("electron", []), + ("node", []), + ("code", ["--ms-enable-electron-run-as-node"]), # VSCode + ("codium", []), # VSCodium (non-Windows) + ("VSCodium", []), # VSCodium (Windows) ): - if (node := expand_variables(node)) and is_executable(node): + if (node := shutil.which(expand_variables(node)) or "") and is_executable(node): return (node, args) return None def is_executable(path: str | Path) -> bool: - return bool((os.path.isfile(path) and os.access(path, os.X_OK))) + return os.path.isfile(path) and os.access(path, os.X_OK) diff --git a/plugin/listener.py b/plugin/listener.py index 660aba9b..78bc33ac 100644 --- a/plugin/listener.py +++ b/plugin/listener.py @@ -39,8 +39,8 @@ def set_up_window(window: sublime.Window) -> None: def tear_down_window(window: sublime.Window) -> None: - G.clear_syntax_rule_collection(window) - G.clear_dropped_rules(window) + G.syntax_rule_collections.pop(window, None) + G.dropped_rules_collection.pop(window, None) Logger.log("👋 Bye!", window=window) Logger.destroy(window=window) @@ -59,11 +59,11 @@ def names_as_str(items: Iterable[Any], *, sep: str = ", ") -> str: Logger.log(f'🔍 Found "Constraint" implementations: {names_as_str(get_constraints())}', window=window) syntax_rule_collection = SyntaxRuleCollection.make(pref_syntax_rules(window=window)) - G.set_syntax_rule_collection(window, syntax_rule_collection) + G.syntax_rule_collections[window] = syntax_rule_collection Logger.log(f"📜 Compiled syntax rule collection: {stringify(syntax_rule_collection)}", window=window) - dropped_rules = tuple(syntax_rule_collection.optimize()) - G.set_dropped_rules(window, dropped_rules) + dropped_rules = list(syntax_rule_collection.optimize()) + G.dropped_rules_collection[window] = dropped_rules Logger.log(f"✨ Optimized syntax rule collection: {stringify(syntax_rule_collection)}", window=window) Logger.log(f"💀 Dropped rules during optimizing: {stringify(dropped_rules)}", window=window) diff --git a/plugin/logger.py b/plugin/logger.py index e47678d3..ad200406 100644 --- a/plugin/logger.py +++ b/plugin/logger.py @@ -34,18 +34,16 @@ def _create_log_panel(window: sublime.Window) -> sublime.View: panel.assign_syntax(Logger.SYNTAX_FILE) panel.set_read_only(True) panel.set_scratch(True) - panel.settings().update( - { - "draw_white_space": "none", - "gutter": False, - "is_widget": True, # ST 3 convention for a non-normal view - "line_numbers": False, - "scroll_context_lines": 0, - "scroll_past_end": False, - "spell_check": False, - "word_wrap": False, - } - ) + panel.settings().update({ + "draw_white_space": "none", + "gutter": False, + "is_widget": True, # ST 3 convention for a non-normal view + "line_numbers": False, + "scroll_context_lines": 0, + "scroll_past_end": False, + "spell_check": False, + "word_wrap": False, + }) return panel diff --git a/plugin/rules/constraint.py b/plugin/rules/constraint.py index 8efeed46..08528f34 100644 --- a/plugin/rules/constraint.py +++ b/plugin/rules/constraint.py @@ -10,7 +10,8 @@ from ..cache import clearable_lru_cache from ..constants import PLUGIN_NAME, ST_PLATFORM -from ..snapshot import ViewSnapshot, ViewSnapshotCollection +from ..shared import G +from ..snapshot import ViewSnapshot from ..types import Optimizable, ST_ConstraintRule from ..utils import ( camel_to_snake, @@ -164,7 +165,7 @@ def _handled_case_insensitive(kwargs: dict[str, Any]) -> bool: @staticmethod def get_view_snapshot(view: sublime.View) -> ViewSnapshot: """Gets the cached information for the `view`.""" - snapshot = ViewSnapshotCollection.get_by_view(view) + snapshot = G.view_snapshot_collection.get_by_view(view) assert snapshot # our workflow guarantees this won't be None return snapshot @@ -176,8 +177,10 @@ def find_parent_with_sibling(base: str | Path, sibling: str, *, use_exists: bool if use_exists: checker = Path.exists + elif sibling.endswith(("\\", "/")): + checker = Path.is_dir # type: ignore else: - checker = Path.is_dir if sibling.endswith(("\\", "/")) else Path.is_file + checker = Path.is_file # type: ignore return first_true(path.parents, pred=lambda p: checker(p / sibling)) diff --git a/plugin/rules/constraints/contains.py b/plugin/rules/constraints/contains.py index edbaa84e..bbe84adc 100644 --- a/plugin/rules/constraints/contains.py +++ b/plugin/rules/constraints/contains.py @@ -13,7 +13,7 @@ class ContainsConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.needles: Tuple[str, ...] = self._handled_args() + self.needles: tuple[str, ...] = self._handled_args() self.threshold: int = kwargs.get("threshold", 1) def is_droppable(self) -> bool: diff --git a/plugin/rules/constraints/first_line_contains.py b/plugin/rules/constraints/first_line_contains.py index 518b4731..560ceae5 100644 --- a/plugin/rules/constraints/first_line_contains.py +++ b/plugin/rules/constraints/first_line_contains.py @@ -12,7 +12,7 @@ class FirstLineContainsConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.needles: Tuple[str, ...] = self._handled_args() + self.needles: tuple[str, ...] = self._handled_args() def is_droppable(self) -> bool: return not self.needles diff --git a/plugin/rules/constraints/is_extension.py b/plugin/rules/constraints/is_extension.py index f7c21f67..33aec9b9 100644 --- a/plugin/rules/constraints/is_extension.py +++ b/plugin/rules/constraints/is_extension.py @@ -20,7 +20,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.case_insensitive = self._handled_case_insensitive(kwargs) - self.exts: Tuple[str, ...] = self._handled_args(_extensionize) + self.exts: tuple[str, ...] = self._handled_args(_extensionize) self.exts = tuple(map(self.fix_case, self.exts)) def is_droppable(self) -> bool: diff --git a/plugin/rules/constraints/is_in_git_repo.py b/plugin/rules/constraints/is_in_git_repo.py index 28fdb822..e08204d7 100644 --- a/plugin/rules/constraints/is_in_git_repo.py +++ b/plugin/rules/constraints/is_in_git_repo.py @@ -12,7 +12,7 @@ class IsInGitRepoConstraint(AbstractConstraint): """Check whether this file is in a git repo.""" - _success_dirs: Set[Path] = set() + _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" def test(self, view: sublime.View) -> bool: diff --git a/plugin/rules/constraints/is_in_hg_repo.py b/plugin/rules/constraints/is_in_hg_repo.py index 69e1d9b5..3998b197 100644 --- a/plugin/rules/constraints/is_in_hg_repo.py +++ b/plugin/rules/constraints/is_in_hg_repo.py @@ -12,7 +12,7 @@ class IsInHgRepoConstraint(AbstractConstraint): """Check whether this file is in a Mercurial repo.""" - _success_dirs: Set[Path] = set() + _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" def test(self, view: sublime.View) -> bool: diff --git a/plugin/rules/constraints/is_in_python_django_project.py b/plugin/rules/constraints/is_in_python_django_project.py index 8c44b1eb..aa1d09b6 100644 --- a/plugin/rules/constraints/is_in_python_django_project.py +++ b/plugin/rules/constraints/is_in_python_django_project.py @@ -12,7 +12,7 @@ class IsInPythonDjangoProjectConstraint(AbstractConstraint): """Check whether this file is in a (Python) Django project.""" - _success_dirs: Set[Path] = set() + _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" def test(self, view: sublime.View) -> bool: diff --git a/plugin/rules/constraints/is_in_ruby_on_rails_project.py b/plugin/rules/constraints/is_in_ruby_on_rails_project.py index 1b7c7def..60a3f157 100644 --- a/plugin/rules/constraints/is_in_ruby_on_rails_project.py +++ b/plugin/rules/constraints/is_in_ruby_on_rails_project.py @@ -12,7 +12,7 @@ class IsInRubyOnRailsProjectConstraint(AbstractConstraint): """Check whether this file is in a Ruby on Rails project.""" - _success_dirs: Set[Path] = set() + _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" def test(self, view: sublime.View) -> bool: diff --git a/plugin/rules/constraints/is_in_svn_repo.py b/plugin/rules/constraints/is_in_svn_repo.py index 7c6c61cc..0cf86ce5 100644 --- a/plugin/rules/constraints/is_in_svn_repo.py +++ b/plugin/rules/constraints/is_in_svn_repo.py @@ -12,7 +12,7 @@ class IsInSvnRepoConstraint(AbstractConstraint): """Check whether this file is in a SVN repo.""" - _success_dirs: Set[Path] = set() + _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" def test(self, view: sublime.View) -> bool: diff --git a/plugin/rules/constraints/is_interpreter.py b/plugin/rules/constraints/is_interpreter.py index 119230dc..e04dedcb 100644 --- a/plugin/rules/constraints/is_interpreter.py +++ b/plugin/rules/constraints/is_interpreter.py @@ -13,7 +13,7 @@ class IsInterpreterConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.interpreters: Tuple[str, ...] = self._handled_args() + self.interpreters: tuple[str, ...] = self._handled_args() self.loosy_version = bool(self.kwargs.get("loosy_version", False)) interpreters_regex = merge_literals_to_regex(self.interpreters) diff --git a/plugin/rules/constraints/is_line_count.py b/plugin/rules/constraints/is_line_count.py index c459a505..37cbe315 100644 --- a/plugin/rules/constraints/is_line_count.py +++ b/plugin/rules/constraints/is_line_count.py @@ -14,8 +14,8 @@ class IsLineCountConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.comparator: Optional[Comparator] = None - self.threshold: Optional[float] = None + self.comparator: Comparator | None = None + self.threshold: float | None = None if len(self.args) != 2: return diff --git a/plugin/rules/constraints/is_name.py b/plugin/rules/constraints/is_name.py index a5392bf6..5d9a86c7 100644 --- a/plugin/rules/constraints/is_name.py +++ b/plugin/rules/constraints/is_name.py @@ -12,7 +12,7 @@ class IsNameConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - names: Tuple[str, ...] = self._handled_args() + names: tuple[str, ...] = self._handled_args() self.case_insensitive = self._handled_case_insensitive(kwargs) self.names = set(map(str.lower, names) if self.case_insensitive else names) diff --git a/plugin/rules/constraints/is_size.py b/plugin/rules/constraints/is_size.py index dff6a806..eb5027a9 100644 --- a/plugin/rules/constraints/is_size.py +++ b/plugin/rules/constraints/is_size.py @@ -14,8 +14,8 @@ class IsSizeConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.comparator: Optional[Comparator] = None - self.threshold: Optional[float] = None + self.comparator: Comparator | None = None + self.threshold: float | None = None if len(self.args) != 2: return diff --git a/plugin/rules/constraints/is_syntax.py b/plugin/rules/constraints/is_syntax.py index c5fd04eb..249293bc 100644 --- a/plugin/rules/constraints/is_syntax.py +++ b/plugin/rules/constraints/is_syntax.py @@ -13,7 +13,7 @@ class IsSyntaxConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.candidates: Tuple[str, ...] = self._handled_args() + self.candidates: tuple[str, ...] = self._handled_args() def is_droppable(self) -> bool: return not self.candidates diff --git a/plugin/rules/constraints/name_contains.py b/plugin/rules/constraints/name_contains.py index a5861922..496fd6ed 100644 --- a/plugin/rules/constraints/name_contains.py +++ b/plugin/rules/constraints/name_contains.py @@ -12,7 +12,7 @@ class NameContainsConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.needles: Tuple[str, ...] = self._handled_args() + self.needles: tuple[str, ...] = self._handled_args() def is_droppable(self) -> bool: return not self.needles diff --git a/plugin/rules/constraints/path_contains.py b/plugin/rules/constraints/path_contains.py index 4e339f6e..accd458f 100644 --- a/plugin/rules/constraints/path_contains.py +++ b/plugin/rules/constraints/path_contains.py @@ -12,7 +12,7 @@ class PathContainsConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.needles: Tuple[str, ...] = self._handled_args() + self.needles: tuple[str, ...] = self._handled_args() def is_droppable(self) -> bool: return not self.needles diff --git a/plugin/rules/constraints/relative_exists.py b/plugin/rules/constraints/relative_exists.py index a613353d..8a43f8bf 100644 --- a/plugin/rules/constraints/relative_exists.py +++ b/plugin/rules/constraints/relative_exists.py @@ -13,7 +13,7 @@ class RelativeExistsConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.relatives: Tuple[str, ...] = self._handled_args() + self.relatives: tuple[str, ...] = self._handled_args() self.match: str = kwargs.get("match", "any").lower() self.matcher = all if self.match == "all" else any diff --git a/plugin/rules/constraints/selector_matches.py b/plugin/rules/constraints/selector_matches.py index 3e677002..880a69ba 100644 --- a/plugin/rules/constraints/selector_matches.py +++ b/plugin/rules/constraints/selector_matches.py @@ -18,7 +18,7 @@ class SelectorMatchesConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.candidates: Tuple[str, ...] = self._handled_args() + self.candidates: tuple[str, ...] = self._handled_args() def is_droppable(self) -> bool: return not self.candidates diff --git a/plugin/rules/matches/all.py b/plugin/rules/matches/all.py index 8b2bade9..8d78a4ef 100644 --- a/plugin/rules/matches/all.py +++ b/plugin/rules/matches/all.py @@ -11,8 +11,8 @@ class AllMatch(AbstractMatch): """Matches when all rules are matched.""" - def is_droppable(self, rules: Tuple[MatchableRule, ...]) -> bool: + def is_droppable(self, rules: tuple[MatchableRule, ...]) -> bool: return len(rules) == 0 - def test(self, view: sublime.View, rules: Tuple[MatchableRule, ...]) -> bool: + def test(self, view: sublime.View, rules: tuple[MatchableRule, ...]) -> bool: return all(rule.test(view) for rule in rules) diff --git a/plugin/rules/matches/any.py b/plugin/rules/matches/any.py index 47cb3dfc..a3519688 100644 --- a/plugin/rules/matches/any.py +++ b/plugin/rules/matches/any.py @@ -11,8 +11,8 @@ class AnyMatch(AbstractMatch): """Matches when any rule is matched.""" - def is_droppable(self, rules: Tuple[MatchableRule, ...]) -> bool: + def is_droppable(self, rules: tuple[MatchableRule, ...]) -> bool: return len(rules) == 0 - def test(self, view: sublime.View, rules: Tuple[MatchableRule, ...]) -> bool: + def test(self, view: sublime.View, rules: tuple[MatchableRule, ...]) -> bool: return any(rule.test(view) for rule in rules) diff --git a/plugin/rules/matches/ratio.py b/plugin/rules/matches/ratio.py index 83a6a561..ea011e63 100644 --- a/plugin/rules/matches/ratio.py +++ b/plugin/rules/matches/ratio.py @@ -19,8 +19,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.denominator: float = nth(self.args, 1) or 0 self.ratio: float = (self.numerator / self.denominator) if self.denominator else -1 - def is_droppable(self, rules: Tuple[MatchableRule, ...]) -> bool: + def is_droppable(self, rules: tuple[MatchableRule, ...]) -> bool: return not (self.denominator > 0 and 0 <= self.ratio <= 1) - def test(self, view: sublime.View, rules: Tuple[MatchableRule, ...]) -> bool: + def test(self, view: sublime.View, rules: tuple[MatchableRule, ...]) -> bool: return self.test_count(view, rules, self.ratio * len(rules)) diff --git a/plugin/rules/matches/some.py b/plugin/rules/matches/some.py index cda9ac70..00307f68 100644 --- a/plugin/rules/matches/some.py +++ b/plugin/rules/matches/some.py @@ -17,8 +17,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.count: float = nth(self.args, 0) or -1 - def is_droppable(self, rules: Tuple[MatchableRule, ...]) -> bool: + def is_droppable(self, rules: tuple[MatchableRule, ...]) -> bool: return not (0 <= self.count <= len(rules)) - def test(self, view: sublime.View, rules: Tuple[MatchableRule, ...]) -> bool: + def test(self, view: sublime.View, rules: tuple[MatchableRule, ...]) -> bool: return self.test_count(view, rules, self.count) diff --git a/plugin/shared.py b/plugin/shared.py index 915d25fa..43e532af 100644 --- a/plugin/shared.py +++ b/plugin/shared.py @@ -1,21 +1,37 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterable, Tuple +from typing import TYPE_CHECKING, Iterable, List import sublime from .guesslang.server import GuesslangServer from .settings import get_merged_plugin_settings -from .types import Optimizable +from .snapshot import ViewSnapshotCollection +from .types import Optimizable, WindowKeyedDict if TYPE_CHECKING: from .guesslang.client import GuesslangClient from .rules import SyntaxRuleCollection -WindowId = int -DroppedRules = Tuple[Optimizable, ...] +DroppedRules = List[Optimizable] DroppedRulesArg = Iterable[Optimizable] +# `UserDict` is not subscriptable until Python 3.9... +if TYPE_CHECKING: + _WindowKeyedDict_DroppedRules = WindowKeyedDict[DroppedRules] + _WindowKeyedDict_SyntaxRuleCollection = WindowKeyedDict[SyntaxRuleCollection] +else: + _WindowKeyedDict_DroppedRules = WindowKeyedDict + _WindowKeyedDict_SyntaxRuleCollection = WindowKeyedDict + + +class DroppedRulesCollection(_WindowKeyedDict_DroppedRules): + pass + + +class SyntaxRuleCollections(_WindowKeyedDict_SyntaxRuleCollection): + pass + class G: """This class holds "G"lobal variables as its class variables.""" @@ -29,36 +45,15 @@ class G: startup_views: set[sublime.View] = set() """Views exist before this plugin is loaded when Sublime Text just starts.""" - windows_syntax_rule_collection: dict[WindowId, SyntaxRuleCollection] = {} - """(Per window) The compiled top-level plugin rules.""" + syntax_rule_collections = SyntaxRuleCollections() + """The compiled per-window top-level plugin rules.""" - windows_dropped_rules: dict[WindowId, DroppedRules] = {} - """(Per window) Those rules which are dropped after doing optimizations.""" + dropped_rules_collection = DroppedRulesCollection() + """Those per-window rules which are dropped after doing optimizations.""" - @classmethod - def is_plugin_ready(cls, window: sublime.Window) -> bool: - return bool(get_merged_plugin_settings(window=window) and cls.get_syntax_rule_collection(window)) - - @classmethod - def set_syntax_rule_collection(cls, window: sublime.Window, value: SyntaxRuleCollection) -> None: - cls.windows_syntax_rule_collection[window.id()] = value + view_snapshot_collection = ViewSnapshotCollection() + """Caches of view attributes.""" @classmethod - def get_syntax_rule_collection(cls, window: sublime.Window) -> SyntaxRuleCollection | None: - return cls.windows_syntax_rule_collection.get(window.id()) - - @classmethod - def clear_syntax_rule_collection(cls, window: sublime.Window) -> SyntaxRuleCollection | None: - return cls.windows_syntax_rule_collection.pop(window.id(), None) - - @classmethod - def set_dropped_rules(cls, window: sublime.Window, value: DroppedRulesArg) -> None: - cls.windows_dropped_rules[window.id()] = tuple(value) - - @classmethod - def get_dropped_rules(cls, window: sublime.Window) -> DroppedRules: - return cls.windows_dropped_rules.get(window.id()) or tuple() - - @classmethod - def clear_dropped_rules(cls, window: sublime.Window) -> DroppedRules | None: - return cls.windows_dropped_rules.pop(window.id(), None) + def is_plugin_ready(cls, window: sublime.Window) -> bool: + return bool(get_merged_plugin_settings(window=window) and cls.syntax_rule_collections.get(window)) diff --git a/plugin/snapshot.py b/plugin/snapshot.py index 6e99add7..ed42e771 100644 --- a/plugin/snapshot.py +++ b/plugin/snapshot.py @@ -1,7 +1,12 @@ from __future__ import annotations +import uuid +from collections import UserDict +from collections.abc import Generator +from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path +from typing import TYPE_CHECKING import sublime @@ -34,26 +39,32 @@ class ViewSnapshot: """The syntax object. Note that the value is as-is when it's cached.""" -class ViewSnapshotCollection: - _snapshots: dict[str, ViewSnapshot] = {} +# `UserDict` is not subscriptable until Python 3.9... +if TYPE_CHECKING: - @classmethod - def add(cls, cache_id: str, view: sublime.View) -> None: - window = view.window() or sublime.active_window() + class ViewSnapshotCollection(UserDict[str, ViewSnapshot]): + def add(self, cache_id: str, view: sublime.View) -> None: ... + def get_by_view(self, view: sublime.View) -> ViewSnapshot | None: ... + @contextmanager + def snapshot_context(self, view: sublime.View) -> Generator[ViewSnapshot, None, None]: ... - # is real file on a disk? - if (_path := view.file_name()) and (path := Path(_path).resolve()).is_file(): - file_name = path.name - file_path = path.as_posix() - file_size = path.stat().st_size - else: - file_name = "" - file_path = "" - file_size = -1 +else: - cls.set( - cache_id, - ViewSnapshot( + class ViewSnapshotCollection(UserDict): + def add(self, cache_id: str, view: sublime.View) -> None: + window = view.window() or sublime.active_window() + + # is real file on a disk? + if (_path := view.file_name()) and (path := Path(_path).resolve()).is_file(): + file_name = path.name + file_path = path.as_posix() + file_size = path.stat().st_size + else: + file_name = "" + file_path = "" + file_size = -1 + + self[cache_id] = ViewSnapshot( id=view.id(), char_count=view.size(), content=get_view_pseudo_content(view, window), @@ -64,24 +75,23 @@ def add(cls, cache_id: str, view: sublime.View) -> None: first_line=get_view_pseudo_first_line(view, window), line_count=view.rowcol(view.size())[0] + 1, syntax=view.syntax(), - ), - ) - - @classmethod - def get(cls, cache_id: str) -> ViewSnapshot | None: - return cls._snapshots.get(cache_id, None) - - @classmethod - def get_by_view(cls, view: sublime.View) -> ViewSnapshot | None: - return cls.get(view.settings().get(VIEW_RUN_ID_SETTINGS_KEY)) - - @classmethod - def set(cls, cache_id: str, snapshot: ViewSnapshot) -> None: - cls._snapshots[cache_id] = snapshot - - @classmethod - def pop(cls, cache_id: str) -> ViewSnapshot | None: - return cls._snapshots.pop(cache_id, None) + ) + + def get_by_view(self, view: sublime.View) -> ViewSnapshot | None: + return self.get(view.settings().get(VIEW_RUN_ID_SETTINGS_KEY)) + + @contextmanager + def snapshot_context(self, view: sublime.View) -> Generator[ViewSnapshot, None, None]: + run_id = str(uuid.uuid4()) + settings = view.settings() + + try: + settings.set(VIEW_RUN_ID_SETTINGS_KEY, run_id) + self.add(run_id, view) + yield self.get(run_id) # type: ignore + finally: + settings.erase(VIEW_RUN_ID_SETTINGS_KEY) + self.pop(run_id) def get_view_pseudo_content(view: sublime.View, window: sublime.Window) -> str: diff --git a/plugin/types.py b/plugin/types.py index 2013e061..ccc78c2c 100644 --- a/plugin/types.py +++ b/plugin/types.py @@ -1,13 +1,18 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections import UserDict from dataclasses import dataclass from enum import Enum -from typing import Any, Generator, TypedDict, Union +from typing import TYPE_CHECKING, Any, Generator, KeysView, TypedDict, TypeVar, Union import sublime SyntaxLike = Union[str, sublime.Syntax] +WindowId = int +WindowIdAble = Union[WindowId, sublime.Window] + +_T = TypeVar("_T") class ListenerEvent(Enum): @@ -76,6 +81,40 @@ class ST_SyntaxRule(ST_MatchRule): on_events: str | list[str] | None +# `UserDict` is not subscriptable until Python 3.9... +if TYPE_CHECKING: + + class WindowKeyedDict(UserDict[WindowIdAble, _T]): + def __setitem__(self, key: WindowIdAble, value: _T) -> None: ... + def __getitem__(self, key: WindowIdAble) -> _T: ... + def __delitem__(self, key: WindowIdAble) -> None: ... + def keys(self) -> KeysView[WindowId]: ... + @staticmethod + def _to_window_id(value: WindowIdAble) -> WindowId: ... + +else: + + class WindowKeyedDict(UserDict): + def __setitem__(self, key: WindowIdAble, value: _T) -> None: + key = self._to_window_id(key) + super().__setitem__(key, value) + + def __getitem__(self, key: WindowIdAble) -> _T: + key = self._to_window_id(key) + return super().__getitem__(key) + + def __delitem__(self, key: WindowIdAble) -> None: + key = self._to_window_id(key) + super().__delitem__(key) + + def keys(self) -> KeysView[WindowId]: + return super().keys() + + @staticmethod + def _to_window_id(value: WindowIdAble) -> WindowId: + return value.id() if isinstance(value, sublime.Window) else value + + @dataclass class SemanticVersion: major: int diff --git a/plugin/utils.py b/plugin/utils.py index 48f60915..a7be7fae 100644 --- a/plugin/utils.py +++ b/plugin/utils.py @@ -466,7 +466,7 @@ def rmtree_ex(path: str | Path, ignore_errors: bool = False, **kwargs: Any) -> N @see https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation """ if os.name == "nt" and (path := Path(path)).is_absolute(): - path = R"\\?\{}".format(path) + path = fR"\\?\{path}" shutil.rmtree(path, ignore_errors, **kwargs)