From 92bd8477aa335e36b8376ab4201ff03ae65901a0 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 | 18 +++--- ...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/shared.py | 61 +++++++++---------- plugin/snapshot.py | 60 ++++++++---------- plugin/types.py | 41 ++++++++++++- 11 files changed, 166 insertions(+), 137 deletions(-) diff --git a/plugin/commands/auto_set_syntax.py b/plugin/commands/auto_set_syntax.py index ce988581..6f275a42 100644 --- a/plugin/commands/auto_set_syntax.py +++ b/plugin/commands/auto_set_syntax.py @@ -20,7 +20,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, @@ -147,11 +147,11 @@ def _view_snapshot_context(view: sublime.View) -> Generator[ViewSnapshot, None, try: settings.set(VIEW_RUN_ID_SETTINGS_KEY, run_id) - ViewSnapshotCollection.add(run_id, view) - yield ViewSnapshotCollection.get(run_id) # type: ignore + G.view_snapshot_collection.add(run_id, view) + yield G.view_snapshot_collection.get(run_id) # type: ignore finally: settings.erase(VIEW_RUN_ID_SETTINGS_KEY) - ViewSnapshotCollection.pop(run_id) + G.view_snapshot_collection.pop(run_id) def _snapshot_view(failed_ret: Any = None) -> Callable[[_T_Callable], _T_Callable]: @@ -184,7 +184,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 +258,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 +288,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 +359,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 +392,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/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..9e427bb6 100644 --- a/plugin/snapshot.py +++ b/plugin/snapshot.py @@ -1,7 +1,9 @@ from __future__ import annotations +from collections import UserDict from dataclasses import dataclass from pathlib import Path +from typing import TYPE_CHECKING import sublime @@ -34,26 +36,30 @@ 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: ... - # 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 +70,10 @@ 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)) 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