From 84a18314e3ad5a2d0e066925a07649bc8397c795 Mon Sep 17 00:00:00 2001 From: Jack Cherng Date: Wed, 6 Mar 2024 03:20:44 +0800 Subject: [PATCH] refactor: retire ViewSnapshotCollection It has some issues that are tricky to solve because we are using multithreads... Instead, we use a view snapshot in the function call chain. Signed-off-by: Jack Cherng --- plugin/commands/auto_set_syntax.py | 141 ++++++++---------- plugin/rules/constraint.py | 19 +-- plugin/rules/constraints/contains.py | 9 +- plugin/rules/constraints/contains_regex.py | 7 +- .../rules/constraints/first_line_contains.py | 8 +- .../constraints/first_line_contains_regex.py | 7 +- plugin/rules/constraints/is_arch.py | 5 +- plugin/rules/constraints/is_extension.py | 9 +- plugin/rules/constraints/is_hidden_syntax.py | 9 +- plugin/rules/constraints/is_in_git_repo.py | 7 +- plugin/rules/constraints/is_in_hg_repo.py | 7 +- .../is_in_python_django_project.py | 7 +- .../is_in_ruby_on_rails_project.py | 7 +- plugin/rules/constraints/is_in_svn_repo.py | 7 +- plugin/rules/constraints/is_interpreter.py | 7 +- plugin/rules/constraints/is_line_count.py | 7 +- plugin/rules/constraints/is_magika_enabled.py | 9 +- plugin/rules/constraints/is_name.py | 7 +- plugin/rules/constraints/is_platform.py | 5 +- plugin/rules/constraints/is_platform_arch.py | 5 +- plugin/rules/constraints/is_size.py | 7 +- plugin/rules/constraints/is_syntax.py | 9 +- plugin/rules/constraints/name_contains.py | 7 +- .../rules/constraints/name_contains_regex.py | 7 +- plugin/rules/constraints/path_contains.py | 7 +- .../rules/constraints/path_contains_regex.py | 7 +- plugin/rules/constraints/relative_exists.py | 7 +- plugin/rules/constraints/selector_matches.py | 7 +- plugin/rules/match.py | 15 +- plugin/rules/matches/all.py | 7 +- plugin/rules/matches/any.py | 7 +- plugin/rules/matches/ratio.py | 7 +- plugin/rules/matches/some.py | 7 +- plugin/rules/syntax.py | 14 +- plugin/shared.py | 4 - plugin/snapshot.py | 46 +----- 36 files changed, 180 insertions(+), 274 deletions(-) diff --git a/plugin/commands/auto_set_syntax.py b/plugin/commands/auto_set_syntax.py index e71600dd..b9a30ea1 100644 --- a/plugin/commands/auto_set_syntax.py +++ b/plugin/commands/auto_set_syntax.py @@ -1,10 +1,9 @@ from __future__ import annotations import re -from functools import wraps from itertools import chain from pathlib import Path -from typing import Any, Callable, TypeVar, cast +from typing import Any import sublime import sublime_plugin @@ -27,8 +26,6 @@ stringify, ) -_T_Callable = TypeVar("_T_Callable", bound=Callable[..., Any]) - class AutoSetSyntaxCommand(sublime_plugin.TextCommand): def description(self) -> str: @@ -38,35 +35,20 @@ def run(self, edit: sublime.Edit) -> None: run_auto_set_syntax_on_view(self.view, ListenerEvent.COMMAND, must_plaintext=False) -def _snapshot_view(failed_ret: Any = None) -> Callable[[_T_Callable], _T_Callable]: - def decorator(func: _T_Callable) -> _T_Callable: - @wraps(func) - def wrapped(view: sublime.View, *args: Any, **kwargs: Any) -> Any: - if not ((window := view.window()) and G.is_plugin_ready(window) and view.is_valid()): - Logger.log("⏳ Calm down! View has gone or the plugin is not ready yet.") - return failed_ret - - with G.view_snapshot_collection.snapshot_context(view): - return func(view, *args, **kwargs) - - return cast(_T_Callable, wrapped) - - return decorator - - -@_snapshot_view(failed_ret=False) def run_auto_set_syntax_on_view( view: sublime.View, event: ListenerEvent | None = None, *, must_plaintext: bool = False, ) -> bool: - # multithread guard... - if not G.view_snapshot_collection.get_by_view(view): + if not ((window := view.window()) and G.is_plugin_ready(window) and view.is_valid()): + Logger.log("⏳ Calm down! View has gone or the plugin is not ready yet.") return False + view_snapshot = ViewSnapshot.from_view(view) + if event is ListenerEvent.EXEC: - return _assign_syntax_for_exec_output(view, event) + return _assign_syntax_for_exec_output(view_snapshot, event) # prerequsites if not ( @@ -77,15 +59,15 @@ def run_auto_set_syntax_on_view( return False if event is ListenerEvent.NEW: - return _assign_syntax_for_new_view(view, event) + return _assign_syntax_for_new_view(view_snapshot, event) - if _assign_syntax_for_st_syntax_test(view, event): + if _assign_syntax_for_st_syntax_test(view_snapshot, event): return True - if _assign_syntax_with_plugin_rules(view, syntax_rule_collection, event): + if _assign_syntax_with_plugin_rules(view_snapshot, syntax_rule_collection, event): return True - if _assign_syntax_with_first_line(view, event): + if _assign_syntax_with_first_line(view_snapshot, event): return True if event in { @@ -94,7 +76,7 @@ def run_auto_set_syntax_on_view( ListenerEvent.LOAD, ListenerEvent.SAVE, ListenerEvent.UNTRANSIENTIZE, - } and _assign_syntax_with_trimmed_filename(view, event): + } and _assign_syntax_with_trimmed_filename(view_snapshot, event): return True if event in { @@ -105,18 +87,19 @@ def run_auto_set_syntax_on_view( ListenerEvent.PASTE, ListenerEvent.SAVE, ListenerEvent.UNTRANSIENTIZE, - } and _assign_syntax_with_magika(view, event): + } and _assign_syntax_with_magika(view_snapshot, event): return True - if _assign_syntax_with_heuristics(view, event): + if _assign_syntax_with_heuristics(view_snapshot, event): return True return _sorry_cannot_help(view, event) -def _assign_syntax_for_exec_output(view: sublime.View, event: ListenerEvent | None = None) -> bool: +def _assign_syntax_for_exec_output(view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> bool: if ( - (window := view.window()) + (view := view_snapshot.valid_view) + and (window := view.window()) and (not (syntax_old := view.syntax()) or syntax_old.scope == "text.plain") and (exec_file_syntax := get_merged_plugin_setting("exec_file_syntax", window=window)) and (syntax := find_syntax_by_syntax_like(exec_file_syntax, include_hidden=True)) @@ -129,9 +112,10 @@ def _assign_syntax_for_exec_output(view: sublime.View, event: ListenerEvent | No return False -def _assign_syntax_for_new_view(view: sublime.View, event: ListenerEvent | None = None) -> bool: +def _assign_syntax_for_new_view(view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> bool: if ( - (window := view.window()) + (view := view_snapshot.valid_view) + and (window := view.window()) and (new_file_syntax := get_merged_plugin_setting("new_file_syntax", window=window)) and (syntax := find_syntax_by_syntax_like(new_file_syntax, include_plaintext=False)) ): @@ -143,9 +127,9 @@ def _assign_syntax_for_new_view(view: sublime.View, event: ListenerEvent | None return False -def _assign_syntax_for_st_syntax_test(view: sublime.View, event: ListenerEvent | None = None) -> bool: +def _assign_syntax_for_st_syntax_test(view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> bool: if ( - (view_snapshot := G.view_snapshot_collection.get_by_view(view)) + (view := view_snapshot.valid_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") @@ -160,11 +144,11 @@ def _assign_syntax_for_st_syntax_test(view: sublime.View, event: ListenerEvent | def _assign_syntax_with_plugin_rules( - view: sublime.View, + view_snapshot: ViewSnapshot, syntax_rule_collection: SyntaxRuleCollection, event: ListenerEvent | None = None, ) -> bool: - if syntax_rule := syntax_rule_collection.test(view, event): + if (view := view_snapshot.valid_view) and (syntax_rule := syntax_rule_collection.test(view_snapshot, event)): assert syntax_rule.syntax # otherwise it should be dropped during optimizing return assign_syntax_to_view( view, @@ -174,7 +158,7 @@ def _assign_syntax_with_plugin_rules( return False -def _assign_syntax_with_first_line(view: sublime.View, event: ListenerEvent | None = None) -> bool: +def _assign_syntax_with_first_line(view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> bool: # Note that this only works for files under some circumstances. # This is to prevent from, for example, changing a ".erb" (Rails HTML template) file into HTML syntax. # But we want to change a file whose name is "cpp" with a Python shebang into Python syntax. @@ -202,7 +186,7 @@ def _prefer_vim_modeline(view_snapshot: ViewSnapshot) -> sublime.Syntax | None: return syntax return None - if not (view_snapshot := G.view_snapshot_collection.get_by_view(view)): + if not (view := view_snapshot.valid_view): return False # It's potentially that a first line of a syntax is a prefix of another syntax's. @@ -221,9 +205,10 @@ def _prefer_vim_modeline(view_snapshot: ViewSnapshot) -> sublime.Syntax | None: return False -def _assign_syntax_with_trimmed_filename(view: sublime.View, event: ListenerEvent | None = None) -> bool: +def _assign_syntax_with_trimmed_filename(view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> bool: if not ( - (filepath := view.file_name()) + (view := view_snapshot.valid_view) + and (filepath := view.file_name()) and (window := view.window()) and (syntax_old := view.syntax()) and is_plaintext_syntax(syntax_old) @@ -256,46 +241,12 @@ def _assign_syntax_with_trimmed_filename(view: sublime.View, event: ListenerEven return False -def _assign_syntax_with_heuristics(view: sublime.View, event: ListenerEvent | None = None) -> bool: - if not ( - (view_snapshot := G.view_snapshot_collection.get_by_view(view)) - and view_snapshot.syntax - and is_plaintext_syntax(view_snapshot.syntax) - ): - return False - - def is_large_file(view_snapshot: ViewSnapshot) -> bool: - return view_snapshot.char_count >= 10 * 1024 # 10KB - - def is_json(view_snapshot: ViewSnapshot) -> bool: - text_begin = re.sub(r"^\s+", "", view_snapshot.content[:10]) - text_end = re.sub(r"\s+$", "", view_snapshot.content[-10:]) - - # XSSI protection prefix (https://security.stackexchange.com/q/110539) - if text_begin.startswith((")]}'\n", ")]}',\n")): - return True - - return is_large_file(view_snapshot) and bool( - # map - (re.search(r'^\{"', text_begin) and re.search(r'(?:[\d"\]}]|true|false|null)\}$', text_end)) - # array - or (re.search(r'^\["', text_begin) and re.search(r'(?:[\d"\]}]|true|false|null)\]$', text_end)) - or (text_begin.startswith("[[") and text_end.endswith("]]")) - or (text_begin.startswith("[{") and text_end.endswith("}]")) - ) - - if is_json(view_snapshot) and (syntax := find_syntax_by_syntax_like("scope:source.json")): - return assign_syntax_to_view(view, syntax, details={"event": event, "reason": "heuristics"}) - - return False - - -def _assign_syntax_with_magika(view: sublime.View, event: ListenerEvent | None = None) -> bool: +def _assign_syntax_with_magika(view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> bool: if not ( - (window := view.window()) + (view := view_snapshot.valid_view) + and (window := view.window()) and (settings := get_merged_plugin_settings(window=window)) and settings.get("magika.enabled") - 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 @@ -338,6 +289,36 @@ def _assign_syntax_with_magika(view: sublime.View, event: ListenerEvent | None = return assign_syntax_to_view(view, syntax, details={"event": event, "reason": "Magika (Deep Learning)"}) +def _assign_syntax_with_heuristics(view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> bool: + def is_large_file(view_snapshot: ViewSnapshot) -> bool: + return view_snapshot.char_count >= 10 * 1024 # 10KB + + def is_json(view_snapshot: ViewSnapshot) -> bool: + text_begin = re.sub(r"^\s+", "", view_snapshot.content[:10]) + text_end = re.sub(r"\s+$", "", view_snapshot.content[-10:]) + + # XSSI protection prefix (https://security.stackexchange.com/q/110539) + if text_begin.startswith((")]}'\n", ")]}',\n")): + return True + + return is_large_file(view_snapshot) and bool( + # map + (re.search(r'^\{"', text_begin) and re.search(r'(?:[\d"\]}]|true|false|null)\}$', text_end)) + # array + or (re.search(r'^\["', text_begin) and re.search(r'(?:[\d"\]}]|true|false|null)\]$', text_end)) + or (text_begin.startswith("[[") and text_end.endswith("]]")) + or (text_begin.startswith("[{") and text_end.endswith("}]")) + ) + + if not ((view := view_snapshot.valid_view) and view_snapshot.syntax and is_plaintext_syntax(view_snapshot.syntax)): + return False + + if is_json(view_snapshot) and (syntax := find_syntax_by_syntax_like("scope:source.json")): + return assign_syntax_to_view(view, syntax, details={"event": event, "reason": "heuristics"}) + + return False + + def _sorry_cannot_help(view: sublime.View, event: ListenerEvent | None = None) -> bool: details = {"event": event, "reason": "no matching rule"} Logger.log(f"❌ Cannot help {stringify(view)} because {stringify(details)}", window=view.window()) diff --git a/plugin/rules/constraint.py b/plugin/rules/constraint.py index 08528f34..f77eba7a 100644 --- a/plugin/rules/constraint.py +++ b/plugin/rules/constraint.py @@ -6,11 +6,8 @@ from pathlib import Path from typing import Any, Callable, Generator, Iterable, Pattern, TypeVar, final -import sublime - from ..cache import clearable_lru_cache from ..constants import PLUGIN_NAME, ST_PLATFORM -from ..shared import G from ..snapshot import ViewSnapshot from ..types import Optimizable, ST_ConstraintRule from ..utils import ( @@ -54,11 +51,11 @@ def optimize(self) -> Generator[Optimizable, None, None]: return yield - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: assert self.constraint try: - result = self.constraint.test(view) + result = self.constraint.test(view_snapshot) except AlwaysTruthyException: return True except AlwaysFalsyException: @@ -117,8 +114,8 @@ def is_droppable(self) -> bool: return False @abstractmethod - def test(self, view: sublime.View) -> bool: - """Tests whether the `view` passes this constraint.""" + def test(self, view_snapshot: ViewSnapshot) -> bool: + """Tests whether the `view_snapshot` passes this constraint.""" @final def _handled_args(self, normalizer: Callable[[T], T] | None = None) -> tuple[T, ...]: @@ -161,14 +158,6 @@ def _handled_case_insensitive(kwargs: dict[str, Any]) -> bool: """Returns `case_insensitive` in `kwars`. Defaulted to platform's specification.""" return bool(kwargs.get("case_insensitive", ST_PLATFORM in {"windows", "osx"})) - @final - @staticmethod - def get_view_snapshot(view: sublime.View) -> ViewSnapshot: - """Gets the cached information for the `view`.""" - snapshot = G.view_snapshot_collection.get_by_view(view) - assert snapshot # our workflow guarantees this won't be None - return snapshot - @final @staticmethod def find_parent_with_sibling(base: str | Path, sibling: str, *, use_exists: bool = False) -> Path | None: diff --git a/plugin/rules/constraints/contains.py b/plugin/rules/constraints/contains.py index 816f37dc..f0381cdd 100644 --- a/plugin/rules/constraints/contains.py +++ b/plugin/rules/constraints/contains.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ...utils import nth, str_finditer from ..constraint import AbstractConstraint @@ -19,15 +18,13 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not (self.needles and isinstance(self.threshold, (int, float))) - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: if self.threshold <= 0: return True - content = self.get_view_snapshot(view).content - return ( nth( - (_ for needle in self.needles for _ in str_finditer(content, needle)), + (_ for needle in self.needles for _ in str_finditer(view_snapshot.content, needle)), self.threshold - 1, ) is not None diff --git a/plugin/rules/constraints/contains_regex.py b/plugin/rules/constraints/contains_regex.py index 9dc4b0c1..27663484 100644 --- a/plugin/rules/constraints/contains_regex.py +++ b/plugin/rules/constraints/contains_regex.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ...utils import nth from ..constraint import AbstractConstraint @@ -19,13 +18,13 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not isinstance(self.threshold, (int, float)) - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: if self.threshold <= 0: return True return ( nth( - self.regex.finditer(self.get_view_snapshot(view).content), + self.regex.finditer(view_snapshot.content), self.threshold - 1, ) is not None diff --git a/plugin/rules/constraints/first_line_contains.py b/plugin/rules/constraints/first_line_contains.py index 815f0fee..941c8a84 100644 --- a/plugin/rules/constraints/first_line_contains.py +++ b/plugin/rules/constraints/first_line_contains.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint @@ -17,6 +16,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.needles - def test(self, view: sublime.View) -> bool: - first_line = self.get_view_snapshot(view).first_line - return any((needle in first_line) for needle in self.needles) + def test(self, view_snapshot: ViewSnapshot) -> bool: + return any((needle in view_snapshot.first_line) for needle in self.needles) diff --git a/plugin/rules/constraints/first_line_contains_regex.py b/plugin/rules/constraints/first_line_contains_regex.py index 8d5cb6e3..20fed9ec 100644 --- a/plugin/rules/constraints/first_line_contains_regex.py +++ b/plugin/rules/constraints/first_line_contains_regex.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint @@ -14,5 +13,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.regex = self._handled_regex(self.args, self.kwargs) - def test(self, view: sublime.View) -> bool: - return bool(self.regex.search(self.get_view_snapshot(view).first_line)) + def test(self, view_snapshot: ViewSnapshot) -> bool: + return bool(self.regex.search(view_snapshot.first_line)) diff --git a/plugin/rules/constraints/is_arch.py b/plugin/rules/constraints/is_arch.py index 8e9e224e..7dc938ee 100644 --- a/plugin/rules/constraints/is_arch.py +++ b/plugin/rules/constraints/is_arch.py @@ -2,9 +2,8 @@ from typing import Any, final -import sublime - from ...constants import ST_ARCH +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint @@ -19,5 +18,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.names - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: return self.result diff --git a/plugin/rules/constraints/is_extension.py b/plugin/rules/constraints/is_extension.py index 64f41d1e..f2b8291c 100644 --- a/plugin/rules/constraints/is_extension.py +++ b/plugin/rules/constraints/is_extension.py @@ -2,9 +2,8 @@ from typing import Any, final -import sublime - from ...settings import pref_trim_suffixes +from ...snapshot import ViewSnapshot from ...utils import list_trimmed_strings from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -26,8 +25,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.exts - def test(self, view: sublime.View) -> bool: - if not (window := view.window()): + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not ((view := view_snapshot.valid_view) and (window := view.window())): raise AlwaysFalsyException("view has been closed") return any( @@ -35,7 +34,7 @@ def test(self, view: sublime.View) -> bool: for filename in map( self.fix_case, list_trimmed_strings( - self.get_view_snapshot(view).file_name, + view_snapshot.file_name, pref_trim_suffixes(window=window), ), ) diff --git a/plugin/rules/constraints/is_hidden_syntax.py b/plugin/rules/constraints/is_hidden_syntax.py index 615bafd1..0cab1933 100644 --- a/plugin/rules/constraints/is_hidden_syntax.py +++ b/plugin/rules/constraints/is_hidden_syntax.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -12,8 +11,8 @@ class IsHiddenSyntaxConstraint(AbstractConstraint): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - def test(self, view: sublime.View) -> bool: - if not (syntax := self.get_view_snapshot(view).syntax): - raise AlwaysFalsyException(f"View({view.id()}) has no syntax") + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not (syntax := view_snapshot.syntax): + raise AlwaysFalsyException(f"{view_snapshot.view} has no syntax") return syntax.hidden diff --git a/plugin/rules/constraints/is_in_git_repo.py b/plugin/rules/constraints/is_in_git_repo.py index 4d44a75f..24879900 100644 --- a/plugin/rules/constraints/is_in_git_repo.py +++ b/plugin/rules/constraints/is_in_git_repo.py @@ -3,8 +3,7 @@ from pathlib import Path from typing import final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -15,11 +14,11 @@ class IsInGitRepoConstraint(AbstractConstraint): _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: cls = self.__class__ # file not on disk, maybe just a buffer - if not (_file_path := self.get_view_snapshot(view).file_path): + if not (_file_path := view_snapshot.file_path): raise AlwaysFalsyException("file not on disk") file_path = Path(_file_path) diff --git a/plugin/rules/constraints/is_in_hg_repo.py b/plugin/rules/constraints/is_in_hg_repo.py index 94568859..6b2d8449 100644 --- a/plugin/rules/constraints/is_in_hg_repo.py +++ b/plugin/rules/constraints/is_in_hg_repo.py @@ -3,8 +3,7 @@ from pathlib import Path from typing import final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -15,11 +14,11 @@ class IsInHgRepoConstraint(AbstractConstraint): _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: cls = self.__class__ # file not on disk, maybe just a buffer - if not (_file_path := self.get_view_snapshot(view).file_path): + if not (_file_path := view_snapshot.file_path): raise AlwaysFalsyException("file not on disk") file_path = Path(_file_path) diff --git a/plugin/rules/constraints/is_in_python_django_project.py b/plugin/rules/constraints/is_in_python_django_project.py index 330e16ea..f8fcdf74 100644 --- a/plugin/rules/constraints/is_in_python_django_project.py +++ b/plugin/rules/constraints/is_in_python_django_project.py @@ -3,8 +3,7 @@ from pathlib import Path from typing import final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -15,11 +14,11 @@ class IsInPythonDjangoProjectConstraint(AbstractConstraint): _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: cls = self.__class__ # file not on disk, maybe just a buffer - if not (_file_path := self.get_view_snapshot(view).file_path): + if not (_file_path := view_snapshot.file_path): raise AlwaysFalsyException("no filename") file_path = Path(_file_path) 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 961bc3a3..8f70fd73 100644 --- a/plugin/rules/constraints/is_in_ruby_on_rails_project.py +++ b/plugin/rules/constraints/is_in_ruby_on_rails_project.py @@ -3,8 +3,7 @@ from pathlib import Path from typing import final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -15,11 +14,11 @@ class IsInRubyOnRailsProjectConstraint(AbstractConstraint): _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: cls = self.__class__ # file not on disk, maybe just a buffer - if not (_file_path := self.get_view_snapshot(view).file_path): + if not (_file_path := view_snapshot.file_path): raise AlwaysFalsyException("no filename") file_path = Path(_file_path) diff --git a/plugin/rules/constraints/is_in_svn_repo.py b/plugin/rules/constraints/is_in_svn_repo.py index 8c6207aa..b3e6c8f2 100644 --- a/plugin/rules/constraints/is_in_svn_repo.py +++ b/plugin/rules/constraints/is_in_svn_repo.py @@ -3,8 +3,7 @@ from pathlib import Path from typing import final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -15,11 +14,11 @@ class IsInSvnRepoConstraint(AbstractConstraint): _success_dirs: set[Path] = set() """Cached directories which make the result `True`.""" - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: cls = self.__class__ # file not on disk, maybe just a buffer - if not (_file_path := self.get_view_snapshot(view).file_path): + if not (_file_path := view_snapshot.file_path): raise AlwaysFalsyException("file not on disk") file_path = Path(_file_path) diff --git a/plugin/rules/constraints/is_interpreter.py b/plugin/rules/constraints/is_interpreter.py index afc26515..0d39ad94 100644 --- a/plugin/rules/constraints/is_interpreter.py +++ b/plugin/rules/constraints/is_interpreter.py @@ -2,8 +2,7 @@ from typing import Any, Pattern, final -import sublime - +from ...snapshot import ViewSnapshot from ...utils import compile_regex, merge_literals_to_regex, merge_regexes from ..constraint import AbstractConstraint @@ -32,5 +31,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.first_line_regex - def test(self, view: sublime.View) -> bool: - return bool(self.first_line_regex.search(self.get_view_snapshot(view).first_line)) + def test(self, view_snapshot: ViewSnapshot) -> bool: + return bool(self.first_line_regex.search(view_snapshot.first_line)) diff --git a/plugin/rules/constraints/is_line_count.py b/plugin/rules/constraints/is_line_count.py index 5bc9ff03..8af3535e 100644 --- a/plugin/rules/constraints/is_line_count.py +++ b/plugin/rules/constraints/is_line_count.py @@ -2,8 +2,7 @@ from typing import Any, Callable, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint Comparator = Callable[[Any, Any], bool] @@ -28,6 +27,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not (self.comparator and self.threshold is not None) - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: assert self.comparator - return self.comparator(self.get_view_snapshot(view).line_count, self.threshold) + return self.comparator(view_snapshot.line_count, self.threshold) diff --git a/plugin/rules/constraints/is_magika_enabled.py b/plugin/rules/constraints/is_magika_enabled.py index 4db8a44d..fcc43bc0 100644 --- a/plugin/rules/constraints/is_magika_enabled.py +++ b/plugin/rules/constraints/is_magika_enabled.py @@ -2,13 +2,14 @@ from typing import final -import sublime - from ...settings import get_merged_plugin_setting +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint @final class IsMagikaEnabledConstraint(AbstractConstraint): - def test(self, view: sublime.View) -> bool: - return bool(get_merged_plugin_setting("magika.enabled", False, window=view.window())) + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not ((view := view_snapshot.valid_view) and (window := view.window())): + return False + return bool(get_merged_plugin_setting("magika.enabled", False, window=window)) diff --git a/plugin/rules/constraints/is_name.py b/plugin/rules/constraints/is_name.py index 3e0f7a71..d2001f8f 100644 --- a/plugin/rules/constraints/is_name.py +++ b/plugin/rules/constraints/is_name.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -20,8 +19,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.names - def test(self, view: sublime.View) -> bool: - if not (file_name := self.get_view_snapshot(view).file_name): + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not (file_name := view_snapshot.file_name): raise AlwaysFalsyException("file not on disk") if self.case_insensitive: diff --git a/plugin/rules/constraints/is_platform.py b/plugin/rules/constraints/is_platform.py index b66ed6c4..1f31cf21 100644 --- a/plugin/rules/constraints/is_platform.py +++ b/plugin/rules/constraints/is_platform.py @@ -2,9 +2,8 @@ from typing import Any, final -import sublime - from ...constants import ST_PLATFORM +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint @@ -19,5 +18,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.names - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: return self.result diff --git a/plugin/rules/constraints/is_platform_arch.py b/plugin/rules/constraints/is_platform_arch.py index f4c740af..80784f92 100644 --- a/plugin/rules/constraints/is_platform_arch.py +++ b/plugin/rules/constraints/is_platform_arch.py @@ -2,9 +2,8 @@ from typing import Any, final -import sublime - from ...constants import ST_PLATFORM_ARCH +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint @@ -19,5 +18,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.names - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: return self.result diff --git a/plugin/rules/constraints/is_size.py b/plugin/rules/constraints/is_size.py index c6c91017..75f764a9 100644 --- a/plugin/rules/constraints/is_size.py +++ b/plugin/rules/constraints/is_size.py @@ -2,8 +2,7 @@ from typing import Any, Callable, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException Comparator = Callable[[Any, Any], bool] @@ -28,8 +27,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not (self.comparator and self.threshold is not None) - def test(self, view: sublime.View) -> bool: - if (file_size := self.get_view_snapshot(view).file_size) < 0: + def test(self, view_snapshot: ViewSnapshot) -> bool: + if (file_size := view_snapshot.file_size) < 0: raise AlwaysFalsyException("file not on disk") assert self.comparator diff --git a/plugin/rules/constraints/is_syntax.py b/plugin/rules/constraints/is_syntax.py index bb875a16..2670af41 100644 --- a/plugin/rules/constraints/is_syntax.py +++ b/plugin/rules/constraints/is_syntax.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ...utils import find_syntaxes_by_syntax_likes from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -18,8 +17,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.candidates - def test(self, view: sublime.View) -> bool: - if not (syntax := self.get_view_snapshot(view).syntax): - raise AlwaysFalsyException(f"View({view.id()}) has no syntax") + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not (syntax := view_snapshot.syntax): + raise AlwaysFalsyException(f"{view_snapshot.view} has no syntax") return syntax in find_syntaxes_by_syntax_likes(self.candidates) diff --git a/plugin/rules/constraints/name_contains.py b/plugin/rules/constraints/name_contains.py index d216d042..87deb6f3 100644 --- a/plugin/rules/constraints/name_contains.py +++ b/plugin/rules/constraints/name_contains.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -17,8 +16,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.needles - def test(self, view: sublime.View) -> bool: - if not (file_name := self.get_view_snapshot(view).file_name): + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not (file_name := view_snapshot.file_name): raise AlwaysFalsyException("file not on disk") return any((needle in file_name) for needle in self.needles) diff --git a/plugin/rules/constraints/name_contains_regex.py b/plugin/rules/constraints/name_contains_regex.py index 49175029..927b723f 100644 --- a/plugin/rules/constraints/name_contains_regex.py +++ b/plugin/rules/constraints/name_contains_regex.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -14,8 +13,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.regex = self._handled_regex(self.args, self.kwargs) - def test(self, view: sublime.View) -> bool: - if not (file_name := self.get_view_snapshot(view).file_name): + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not (file_name := view_snapshot.file_name): raise AlwaysFalsyException("file not on disk") return bool(self.regex.search(file_name)) diff --git a/plugin/rules/constraints/path_contains.py b/plugin/rules/constraints/path_contains.py index 06abb225..f7e8d31b 100644 --- a/plugin/rules/constraints/path_contains.py +++ b/plugin/rules/constraints/path_contains.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -17,8 +16,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.needles - def test(self, view: sublime.View) -> bool: - if not (file_path := self.get_view_snapshot(view).file_path): + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not (file_path := view_snapshot.file_path): raise AlwaysFalsyException("file not on disk") return any((needle in file_path) for needle in self.needles) diff --git a/plugin/rules/constraints/path_contains_regex.py b/plugin/rules/constraints/path_contains_regex.py index ce3d3e39..ba3de226 100644 --- a/plugin/rules/constraints/path_contains_regex.py +++ b/plugin/rules/constraints/path_contains_regex.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -14,8 +13,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.regex = self._handled_regex(self.args, self.kwargs) - def test(self, view: sublime.View) -> bool: - if not (file_path := self.get_view_snapshot(view).file_path): + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not (file_path := view_snapshot.file_path): raise AlwaysFalsyException("file not on disk") return bool(self.regex.search(file_path)) diff --git a/plugin/rules/constraints/relative_exists.py b/plugin/rules/constraints/relative_exists.py index b46c78f1..8c5e127e 100644 --- a/plugin/rules/constraints/relative_exists.py +++ b/plugin/rules/constraints/relative_exists.py @@ -3,8 +3,7 @@ from pathlib import Path from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -20,9 +19,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.relatives - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: # file not on disk, maybe just a buffer - if not (file_path := self.get_view_snapshot(view).file_path): + if not (file_path := view_snapshot.file_path): raise AlwaysFalsyException("no filename") folder = Path(file_path).parent diff --git a/plugin/rules/constraints/selector_matches.py b/plugin/rules/constraints/selector_matches.py index 5e2ee045..3c53535d 100644 --- a/plugin/rules/constraints/selector_matches.py +++ b/plugin/rules/constraints/selector_matches.py @@ -4,6 +4,7 @@ import sublime +from ...snapshot import ViewSnapshot from ..constraint import AbstractConstraint, AlwaysFalsyException @@ -40,9 +41,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def is_droppable(self) -> bool: return not self.candidates - def test(self, view: sublime.View) -> bool: - if not (syntax := self.get_view_snapshot(view).syntax): - raise AlwaysFalsyException(f"View({view.id()}) has no syntax") + def test(self, view_snapshot: ViewSnapshot) -> bool: + if not (syntax := view_snapshot.syntax): + raise AlwaysFalsyException(f"{view_snapshot.view} has no syntax") return any( # ... diff --git a/plugin/rules/match.py b/plugin/rules/match.py index 5e0a7bdd..cfca3e00 100644 --- a/plugin/rules/match.py +++ b/plugin/rules/match.py @@ -4,9 +4,8 @@ from dataclasses import dataclass, field from typing import Any, Generator, Union, final -import sublime - from ..cache import clearable_lru_cache +from ..snapshot import ViewSnapshot from ..types import Optimizable, ST_MatchRule from ..utils import camel_to_snake, first_true, list_all_subclasses, remove_suffix from .constraint import ConstraintRule @@ -51,9 +50,9 @@ def optimize(self) -> Generator[Optimizable, None, None]: rules.append(rule) self.rules = tuple(rules) - def test(self, view: sublime.View) -> bool: + def test(self, view_snapshot: ViewSnapshot) -> bool: assert self.match - return self.match.test(view, self.rules) + return self.match.test(view_snapshot, self.rules) @classmethod def make(cls, match_rule: ST_MatchRule) -> MatchRule: @@ -115,12 +114,12 @@ def is_droppable(self, rules: tuple[MatchableRule, ...]) -> bool: return False @abstractmethod - def test(self, view: sublime.View, rules: tuple[MatchableRule, ...]) -> bool: - """Tests whether the `view` passes this `match` with those `rules`.""" + def test(self, view_snapshot: ViewSnapshot, rules: tuple[MatchableRule, ...]) -> bool: + """Tests whether the `view_snapshot` passes this `match` with those `rules`.""" @final @staticmethod - def test_count(view: sublime.View, rules: tuple[MatchableRule, ...], goal: float) -> bool: + def test_count(view_snapshot: ViewSnapshot, rules: tuple[MatchableRule, ...], goal: float) -> bool: """Tests whether the amount of passing `rules` is greater than or equal to `goal`.""" if goal <= 0: return True @@ -129,7 +128,7 @@ def test_count(view: sublime.View, rules: tuple[MatchableRule, ...], goal: float for rule in rules: if tolerance < 0: return False - if rule.test(view): + if rule.test(view_snapshot): goal -= 1 if goal == 0: return True diff --git a/plugin/rules/matches/all.py b/plugin/rules/matches/all.py index c68668cd..eaf9b283 100644 --- a/plugin/rules/matches/all.py +++ b/plugin/rules/matches/all.py @@ -2,8 +2,7 @@ from typing import final -import sublime - +from ...snapshot import ViewSnapshot from ..match import AbstractMatch, MatchableRule @@ -14,5 +13,5 @@ class AllMatch(AbstractMatch): def is_droppable(self, rules: tuple[MatchableRule, ...]) -> bool: return len(rules) == 0 - def test(self, view: sublime.View, rules: tuple[MatchableRule, ...]) -> bool: - return all(rule.test(view) for rule in rules) + def test(self, view_snapshot: ViewSnapshot, rules: tuple[MatchableRule, ...]) -> bool: + return all(rule.test(view_snapshot) for rule in rules) diff --git a/plugin/rules/matches/any.py b/plugin/rules/matches/any.py index bd3e47c6..63154b60 100644 --- a/plugin/rules/matches/any.py +++ b/plugin/rules/matches/any.py @@ -2,8 +2,7 @@ from typing import final -import sublime - +from ...snapshot import ViewSnapshot from ..match import AbstractMatch, MatchableRule @@ -14,5 +13,5 @@ class AnyMatch(AbstractMatch): def is_droppable(self, rules: tuple[MatchableRule, ...]) -> bool: return len(rules) == 0 - def test(self, view: sublime.View, rules: tuple[MatchableRule, ...]) -> bool: - return any(rule.test(view) for rule in rules) + def test(self, view_snapshot: ViewSnapshot, rules: tuple[MatchableRule, ...]) -> bool: + return any(rule.test(view_snapshot) for rule in rules) diff --git a/plugin/rules/matches/ratio.py b/plugin/rules/matches/ratio.py index 6d6e7085..a988ea28 100644 --- a/plugin/rules/matches/ratio.py +++ b/plugin/rules/matches/ratio.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ...utils import nth from ..match import AbstractMatch, MatchableRule @@ -22,5 +21,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: 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: - return self.test_count(view, rules, self.ratio * len(rules)) + def test(self, view_snapshot: ViewSnapshot, rules: tuple[MatchableRule, ...]) -> bool: + return self.test_count(view_snapshot, rules, self.ratio * len(rules)) diff --git a/plugin/rules/matches/some.py b/plugin/rules/matches/some.py index df450858..22f683d4 100644 --- a/plugin/rules/matches/some.py +++ b/plugin/rules/matches/some.py @@ -2,8 +2,7 @@ from typing import Any, final -import sublime - +from ...snapshot import ViewSnapshot from ...utils import nth from ..match import AbstractMatch, MatchableRule @@ -20,5 +19,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: 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: - return self.test_count(view, rules, self.count) + def test(self, view_snapshot: ViewSnapshot, rules: tuple[MatchableRule, ...]) -> bool: + return self.test_count(view_snapshot, rules, self.count) diff --git a/plugin/rules/syntax.py b/plugin/rules/syntax.py index 47992d08..4c55cfbd 100644 --- a/plugin/rules/syntax.py +++ b/plugin/rules/syntax.py @@ -6,6 +6,7 @@ import sublime from ..constants import VERSION +from ..snapshot import ViewSnapshot from ..types import ListenerEvent, Optimizable, ST_SyntaxRule from ..utils import find_syntax_by_syntax_likes, first_true from .match import MatchRule @@ -35,16 +36,19 @@ def optimize(self) -> Generator[Optimizable, None, None]: yield self.root_rule self.root_rule = None - def test(self, view: sublime.View, event: ListenerEvent | None = None) -> bool: + def test(self, view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> bool: if event and self.on_events is not None and event not in self.on_events: return False + if not view_snapshot.syntax: + return False + # note that an empty selector matches anything - if not view.match_selector(0, self.selector): + if sublime.score_selector(view_snapshot.syntax.scope, self.selector) == 0: return False assert self.root_rule - return self.root_rule.test(view) + return self.root_rule.test(view_snapshot) @classmethod def make(cls, syntax_rule: ST_SyntaxRule) -> SyntaxRule: @@ -97,8 +101,8 @@ def optimize(self) -> Generator[Optimizable, None, None]: rules.append(rule) self.rules = tuple(rules) - def test(self, view: sublime.View, event: ListenerEvent | None = None) -> SyntaxRule | None: - return first_true(self.rules, pred=lambda rule: rule.test(view, event)) + def test(self, view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) -> SyntaxRule | None: + return first_true(self.rules, pred=lambda rule: rule.test(view_snapshot, event)) @classmethod def make(cls, syntax_rules: Iterable[ST_SyntaxRule]) -> SyntaxRuleCollection: diff --git a/plugin/shared.py b/plugin/shared.py index 610ea739..8d38cedc 100644 --- a/plugin/shared.py +++ b/plugin/shared.py @@ -5,7 +5,6 @@ import sublime from .settings import get_merged_plugin_settings -from .snapshot import ViewSnapshotCollection from .types import Optimizable, WindowKeyedDict if TYPE_CHECKING: @@ -43,9 +42,6 @@ class G: dropped_rules_collection = DroppedRulesCollection() """Those per-window rules which are dropped after doing optimizations.""" - view_snapshot_collection = ViewSnapshotCollection() - """Caches of view attributes.""" - @classmethod 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 a9873ba3..bf2145ab 100644 --- a/plugin/snapshot.py +++ b/plugin/snapshot.py @@ -1,24 +1,18 @@ 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 -from .constants import VIEW_RUN_ID_SETTINGS_KEY from .settings import get_merged_plugin_setting -from .utils import get_view_by_id, head_tail_content_st +from .utils import head_tail_content_st @dataclass class ViewSnapshot: - id: int - """View ID.""" + view: sublime.View + """The view object.""" char_count: int """Character count.""" content: str @@ -60,8 +54,8 @@ def file_size(self) -> int: return self.path_obj.stat().st_size if self.path_obj else -1 @property - def view(self) -> sublime.View | None: - return get_view_by_id(self.id) + def valid_view(self) -> sublime.View | None: + return self.view if self.view.is_valid() else None @classmethod def from_view(cls, view: sublime.View) -> ViewSnapshot: @@ -74,7 +68,7 @@ def from_view(cls, view: sublime.View) -> ViewSnapshot: path = None return cls( - id=view.id(), + view=view, char_count=view.size(), content=get_view_pseudo_content(view, window), first_line=get_view_pseudo_first_line(view, window), @@ -85,34 +79,6 @@ def from_view(cls, view: sublime.View) -> ViewSnapshot: ) -# `UserDict` is not subscriptable until Python 3.9... -if TYPE_CHECKING: - _UserDict_ViewSnapshot = UserDict[str, ViewSnapshot] -else: - _UserDict_ViewSnapshot = UserDict - - -class ViewSnapshotCollection(_UserDict_ViewSnapshot): - def add(self, cache_id: str, view: sublime.View) -> None: - self[cache_id] = ViewSnapshot.from_view(view) - - 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: return head_tail_content_st(view, get_merged_plugin_setting("trim_file_size", window=window))