diff --git a/plugin/commands/auto_set_syntax_syntax_rules_summary.py b/plugin/commands/auto_set_syntax_syntax_rules_summary.py index e4c9c361..cdd6f14c 100644 --- a/plugin/commands/auto_set_syntax_syntax_rules_summary.py +++ b/plugin/commands/auto_set_syntax_syntax_rules_summary.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from collections import defaultdict import sublime @@ -8,16 +9,10 @@ from ..constants import PLUGIN_NAME, VIEW_KEY_IS_CREATED from ..rules import SyntaxRule from ..shared import G -from ..utils import find_syntax_by_syntax_like, stringify +from ..utils import drop_falsy, find_syntax_by_syntax_like TEMPLATE = f""" -# === [{PLUGIN_NAME}] Syntax Rules Summary === # -# You may use the following website to beautify this debug information. -# @link https://play.ruff.rs/?secondary=Format - -######################## -# Syntax Rules Summary # -######################## +// [{PLUGIN_NAME}] Syntax Rules Summary {{content}} """.lstrip() @@ -38,9 +33,12 @@ def run(self, *, copy_only: bool = False) -> None: content = "" for syntax_, rules in sorted(summary.items(), key=lambda x: x[0].name.casefold()): - content += f"# Syntax: {syntax_.name}\n" + content += "/" * 80 + "\n" + content += f"// Syntax: {syntax_.name}\n" + content += "/" * 80 + "\n" for rule in rules: - content += f"{stringify(rule)}\n" + assert rule.st_syntax_rule + content += rule.st_syntax_rule.model_dump_json() + "\n" content += "\n" content = TEMPLATE.format(content=content) @@ -57,5 +55,5 @@ def run(self, *, copy_only: bool = False) -> None: VIEW_KEY_IS_CREATED: True, }) - if syntax := find_syntax_by_syntax_like("scope:source.python"): + if syntax := find_syntax_by_syntax_like("scope:source.json"): view.assign_syntax(syntax) diff --git a/plugin/rules/constraint.py b/plugin/rules/constraint.py index 55cff708..afa70e47 100644 --- a/plugin/rules/constraint.py +++ b/plugin/rules/constraint.py @@ -12,6 +12,7 @@ from ..cache import clearable_lru_cache from ..constants import PLUGIN_NAME, ST_PLATFORM +from ..logger import Logger from ..snapshot import ViewSnapshot from ..types import Optimizable, StConstraintRule from ..utils import ( @@ -46,7 +47,7 @@ class ConstraintRule(Optimizable): constraint_name: str = "" args: tuple[Any, ...] = tuple() kwargs: dict[str, Any] = field(default_factory=dict) - inverted: bool = False # whether the test result should be inverted + inverted: bool = False def is_droppable(self) -> bool: return not (self.constraint and not self.constraint.is_droppable()) @@ -75,20 +76,16 @@ def make(cls, constraint_rule: StConstraintRule) -> Self: """Build this object with the `constraint_rule`.""" obj = cls() - if args := constraint_rule.get("args"): - # make sure args is always a tuple - obj.args = tuple(args) if isinstance(args, list) else (args,) + obj.args = tuple(constraint_rule.args) + obj.kwargs = constraint_rule.kwargs + obj.inverted = constraint_rule.inverted - if kwargs := constraint_rule.get("kwargs"): - obj.kwargs = kwargs - - if (inverted := constraint_rule.get("inverted")) is not None: - obj.inverted = bool(inverted) - - if constraint := constraint_rule.get("constraint"): + if constraint := constraint_rule.constraint: obj.constraint_name = constraint if constraint_class := find_constraint(constraint): obj.constraint = constraint_class(*obj.args, **obj.kwargs) + else: + Logger.log(f"Unsupported constraint rule: {constraint}") return obj diff --git a/plugin/rules/match.py b/plugin/rules/match.py index f68befe6..5b434ab9 100644 --- a/plugin/rules/match.py +++ b/plugin/rules/match.py @@ -9,8 +9,9 @@ from typing_extensions import Self from ..cache import clearable_lru_cache +from ..logger import Logger from ..snapshot import ViewSnapshot -from ..types import Optimizable, StMatchRule +from ..types import Optimizable, StConstraintRule, StMatchRule from ..utils import camel_to_snake, list_all_subclasses, remove_suffix from .constraint import ConstraintRule @@ -30,8 +31,6 @@ def list_matches() -> Generator[type[AbstractMatch], None, None]: @dataclass class MatchRule(Optimizable): - DEFAULT_MATCH_NAME = "any" - match: AbstractMatch | None = None match_name: str = "" args: tuple[Any, ...] = tuple() @@ -63,24 +62,22 @@ def make(cls, match_rule: StMatchRule) -> Self: """Build this object with the `match_rule`.""" obj = cls() - if args := match_rule.get("args"): - # make sure args is always a tuple - obj.args = tuple(args) if isinstance(args, list) else (args,) - - if kwargs := match_rule.get("kwargs"): - obj.kwargs = kwargs + obj.args = tuple(match_rule.args) + obj.kwargs = match_rule.kwargs - match = match_rule.get("match", cls.DEFAULT_MATCH_NAME) + match = match_rule.match if match_class := find_match(match): obj.match_name = match obj.match = match_class(*obj.args, **obj.kwargs) + else: + Logger.log(f"Unsupported match rule: {match}") rules_compiled: list[MatchableRule] = [] - for rule in match_rule.get("rules", []): + for rule in match_rule.rules: rule_class: type[MatchableRule] | None = None - if "constraint" in rule: + if isinstance(rule, StConstraintRule): rule_class = ConstraintRule - elif "rules" in rule: # nested MatchRule + elif isinstance(rule, StMatchRule): rule_class = MatchRule if rule_class and (rule_compiled := rule_class.make(rule)): # type: ignore rules_compiled.append(rule_compiled) diff --git a/plugin/rules/syntax.py b/plugin/rules/syntax.py index f395751c..c85871c6 100644 --- a/plugin/rules/syntax.py +++ b/plugin/rules/syntax.py @@ -24,6 +24,9 @@ class SyntaxRule(Optimizable): """`None` = no restriction, empty = no event = never triggered.""" root_rule: MatchRule | None = None + st_syntax_rule: StSyntaxRule | None = None + """The original syntax rule setting object.""" + def is_droppable(self) -> bool: return not (self.syntax and (self.on_events is None or self.on_events) and self.root_rule) @@ -56,24 +59,16 @@ def test(self, view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) def make(cls, syntax_rule: StSyntaxRule) -> Self: """Build this object with the `syntax_rule`.""" obj = cls() + obj.st_syntax_rule = syntax_rule - if comment := syntax_rule.get("comment"): - obj.comment = str(comment) - - syntaxes = syntax_rule.get("syntaxes", []) - if isinstance(syntaxes, str): - syntaxes = [syntaxes] - obj.syntaxes_name = tuple(syntaxes) - if target_syntax := find_syntax_by_syntax_likes(syntaxes): + obj.syntaxes_name = tuple(syntax_rule.syntaxes) + if target_syntax := find_syntax_by_syntax_likes(syntax_rule.syntaxes): obj.syntax = target_syntax # note that an empty string selector should match any scope - if (selector := syntax_rule.get("selector")) is not None: - obj.selector = selector + obj.selector = syntax_rule.selector - if (on_events := syntax_rule.get("on_events")) is not None: - if isinstance(on_events, str): - on_events = [on_events] + if (on_events := syntax_rule.on_events) is not None: obj.on_events = set(drop_falsy(map(ListenerEvent.from_value, on_events))) if match_rule_compiled := MatchRule.make(syntax_rule): diff --git a/plugin/settings.py b/plugin/settings.py index 78da692e..b8932745 100644 --- a/plugin/settings.py +++ b/plugin/settings.py @@ -2,11 +2,12 @@ from collections import ChainMap from itertools import chain -from typing import Any, Callable, Mapping, MutableMapping +from typing import Any, Callable, List, Mapping, MutableMapping import sublime import sublime_plugin from more_itertools import unique_everseen +from pydantic import TypeAdapter from .types import StSyntaxRule from .utils import drop_falsy @@ -34,7 +35,7 @@ def get_st_settings() -> sublime.Settings: def pref_syntax_rules(*, window: sublime.Window | None = None) -> list[StSyntaxRule]: - return get_merged_plugin_setting("syntax_rules", [], window=window) + return TypeAdapter(List[StSyntaxRule]).validate_python(get_merged_plugin_setting("syntax_rules", [], window=window)) def pref_trim_suffixes(*, window: sublime.Window | None = None) -> tuple[str]: diff --git a/plugin/types.py b/plugin/types.py index 9078e6b7..e23eb0b3 100644 --- a/plugin/types.py +++ b/plugin/types.py @@ -5,9 +5,10 @@ from collections import UserDict as BuiltinUserDict from collections.abc import Generator, Hashable, Iterator, KeysView from enum import Enum -from typing import Any, Generic, TypedDict, TypeVar, Union, overload +from typing import Any, Generic, TypeVar, Union, overload import sublime +from pydantic import BaseModel, Field, field_validator from typing_extensions import Self SyntaxLike = Union[str, sublime.Syntax] @@ -95,31 +96,48 @@ def optimize(self) -> Generator[Any, None, None]: """Does optimizations and returns a generator for dropped objects.""" -class StConstraintRule(TypedDict): - """Typed dict for corresponding ST settings.""" +class StConstraintRule(BaseModel): + """Model for a "constraint rule" in settings.""" constraint: str - args: list[Any] | Any | None - kwargs: dict[str, Any] | None - inverted: bool + """The name of the "constraint".""" + args: list[Any] = Field(default_factory=list) + """Positional arguments for the "constraint".""" + kwargs: dict[str, Any] = Field(default_factory=dict) + """Keyword arguments for the "constraint".""" + inverted: bool = False + """Whether the test result should be inverted.""" -class StMatchRule(TypedDict): - """Typed dict for corresponding ST settings.""" +class StMatchRule(BaseModel): + """Model for a "match rule" in settings.""" - match: str - args: list[Any] | Any | None - kwargs: dict[str, Any] | None - rules: list[StMatchRule | StConstraintRule] + match: str = "any" + """The name of the "match".""" + args: list[Any] = Field(default_factory=list) + """Positional arguments for the "match".""" + kwargs: dict[str, Any] = Field(default_factory=dict) + """Keyword arguments for the "match".""" + rules: list[StConstraintRule | StMatchRule] = Field(default_factory=list) + """Rules to match against.""" class StSyntaxRule(StMatchRule): - """Typed dict for corresponding ST settings.""" - - comment: str - selector: str - syntaxes: str | list[str] - on_events: str | list[str] | None + """Model for a "syntax rule" in settings.""" + + comment: str = "" + """A comment for the rule.""" + selector: str = "text.plain" + """To constrain the syntax scope of the current view. An empty string matches any scope.""" + syntaxes: list[str] = Field(default_factory=list) + """Syntaxes to be used. The first available one will be used.""" + on_events: list[str] | None = None + """Events to listen to, or `None` for all events.""" + + @field_validator("syntaxes", "on_events", mode="before") + @classmethod + def str_to_list_str(cls, v: Any) -> list[str]: + return [v] if isinstance(v, str) else v class WindowKeyedDict(UserDict[WindowIdAble, _T]):