Skip to content

Commit

Permalink
refactor: use pydantic to validate settings
Browse files Browse the repository at this point in the history
Signed-off-by: Jack Cherng <[email protected]>
  • Loading branch information
jfcherng committed Nov 4, 2024
1 parent ad41ac1 commit 3f6a92b
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 125 deletions.
17 changes: 15 additions & 2 deletions AutoSetSyntax.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@

"core_syntax_rules": [
{
"comment": "AutoSetSyntax Debug Information",
"comment": "AutoSetSyntax Python-Like Views",
"syntaxes": "scope:source.python",
"selector": "text.plain",
"rules": [
Expand All @@ -684,6 +684,17 @@
"args": ["^# === [AutoSetSyntax] "]
}
]
},
{
"comment": "AutoSetSyntax JSONC-Like Views",
"syntaxes": "scope:source.autosetsyntax.jsonc",
"selector": "text.plain",
"rules": [
{
"constraint": "first_line_contains_regex",
"args": ["^// [AutoSetSyntax] "]
}
]
}
],

Expand Down Expand Up @@ -715,7 +726,9 @@
"magika.syntax_map.html": ["scope:text.html.basic"],
"magika.syntax_map.ini": ["scope:source.ini"],
"magika.syntax_map.java": ["scope:source.java"],
"magika.syntax_map.javascript": ["scope:source.ts" /* magika can't distinguish between TypeScript and JavaScript */],
"magika.syntax_map.javascript": [
"scope:source.ts" /* magika can't distinguish between TypeScript and JavaScript */
],
"magika.syntax_map.json": ["scope:source.json"],
"magika.syntax_map.latex": ["scope:text.tex.latex"],
"magika.syntax_map.lisp": ["scope:source.lisp"],
Expand Down
30 changes: 11 additions & 19 deletions plugin/commands/auto_set_syntax_debug_information.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

from typing import Any, Mapping

import sublime
import sublime_plugin

from ..constants import PLUGIN_NAME, PY_VERSION, ST_CHANNEL, ST_PLATFORM_ARCH, ST_VERSION, VERSION, VIEW_KEY_IS_CREATED
from ..constants import PLUGIN_NAME, PY_VERSION, ST_CHANNEL, ST_PLATFORM_ARCH, ST_VERSION, VERSION
from ..helpers import create_new_view
from ..rules.constraint import get_constraints
from ..rules.match import get_matches
from ..settings import get_merged_plugin_settings
from ..shared import G
from ..utils import find_syntax_by_syntax_like, get_fqcn, stringify
from ..utils import get_fqcn, stringify

TEMPLATE = f"""
# === [{PLUGIN_NAME}] Debug Information === #
Expand Down Expand Up @@ -51,7 +51,7 @@ class AutoSetSyntaxDebugInformationCommand(sublime_plugin.WindowCommand):
def description(self) -> str:
return f"{PLUGIN_NAME}: Debug Information"

def run(self, *, copy_only: bool = False) -> None:
def run(self) -> None:
info: dict[str, Any] = {}

info["env"] = {
Expand All @@ -68,18 +68,10 @@ def run(self, *, copy_only: bool = False) -> None:

content = TEMPLATE.format_map(_pythonize(info))

if copy_only:
sublime.set_clipboard(content)
sublime.message_dialog(f"[{PLUGIN_NAME}] The result has been copied to the clipboard.")
return

view = self.window.new_file()
view.set_name(self.description())
view.set_scratch(True)
view.run_command("append", {"characters": content})
view.settings().update({
VIEW_KEY_IS_CREATED: True,
})

if syntax_ := find_syntax_by_syntax_like("scope:source.python"):
view.assign_syntax(syntax_)
create_new_view(
name=self.description(),
content=content,
syntax="scope:source.python",
scratch=True,
window=self.window,
)
64 changes: 21 additions & 43 deletions plugin/commands/auto_set_syntax_syntax_rules_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,35 @@
import sublime
import sublime_plugin

from ..constants import PLUGIN_NAME, VIEW_KEY_IS_CREATED
from ..rules import SyntaxRule
from ..constants import PLUGIN_NAME
from ..helpers import create_new_view
from ..shared import G
from ..utils import find_syntax_by_syntax_like, stringify

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 #
########################
{{content}}
""".lstrip()
from ..types import StSyntaxRule


class AutoSetSyntaxSyntaxRulesSummaryCommand(sublime_plugin.WindowCommand):
def description(self) -> str:
return f"{PLUGIN_NAME}: Syntax Rules Summary"

def run(self, *, copy_only: bool = False) -> None:
def run(self) -> None:
if not (rule_collection := G.syntax_rule_collections.get(self.window)):
return

summary: defaultdict[sublime.Syntax, list[SyntaxRule]] = defaultdict(list)
summary: defaultdict[sublime.Syntax, list[StSyntaxRule]] = defaultdict(list)
for rule in rule_collection.rules:
if rule.syntax:
summary[rule.syntax].append(rule)

content = ""
for syntax_, rules in sorted(summary.items(), key=lambda x: x[0].name.casefold()):
content += f"# Syntax: {syntax_.name}\n"
for rule in rules:
content += f"{stringify(rule)}\n"
content += "\n"
content = TEMPLATE.format(content=content)

if copy_only:
sublime.set_clipboard(content)
sublime.message_dialog(f"[{PLUGIN_NAME}] The result has been copied to the clipboard.")
return

view = self.window.new_file()
view.set_name(self.description())
view.set_scratch(True)
view.run_command("append", {"characters": content})
view.settings().update({
VIEW_KEY_IS_CREATED: True,
})

if syntax := find_syntax_by_syntax_like("scope:source.python"):
view.assign_syntax(syntax)
if rule.syntax and rule.src_setting:
summary[rule.syntax].append(rule.src_setting)

content = f"// [{PLUGIN_NAME}] Syntax Rules Summary\n\n"
for syntax, rules in sorted(summary.items(), key=lambda x: x[0].name.casefold()):
content += "/" * 80 + f"\n// Syntax: {syntax.name}\n" + "/" * 80 + "\n\n"
content += "\n".join(st_rule.model_dump_json(indent=4) for st_rule in rules) + "\n\n"

create_new_view(
name=self.description(),
content=content,
syntax="scope:source.autosetsyntax.jsonc",
include_hidden_syntax=True,
scratch=True,
window=self.window,
)
35 changes: 34 additions & 1 deletion plugin/helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import annotations

from collections import deque
from typing import Any

import sublime

from .constants import VIEW_KEY_IS_CREATED
from .settings import get_st_setting
from .utils import is_plaintext_syntax, is_transient_view
from .utils import find_syntax_by_syntax_like, is_plaintext_syntax, is_transient_view


def is_syntaxable_view(view: sublime.View, *, must_plaintext: bool = False) -> bool:
Expand Down Expand Up @@ -44,3 +46,34 @@ def resolve_magika_label_with_syntax_map(label: str, syntax_map: dict[str, list[
res[scope] = True # parsed

return [scope for scope, is_parsed in res.items() if is_parsed]


def create_new_view(
*,
name: str = "Untitled",
content: str = "",
syntax: str | sublime.Syntax | None = None,
include_hidden_syntax: bool = False,
scratch: bool = False,
read_only: bool = False,
settings: dict[str, Any] | None = None,
window: sublime.Window | None = None,
) -> sublime.View:
"""Copies the content to a new view."""
window = window or sublime.active_window()

view = window.new_file()
view.set_name(name)
view.set_scratch(scratch)
view.set_read_only(read_only)
view.settings().update(settings or {})
view.settings().update({
VIEW_KEY_IS_CREATED: True,
})

if syntax and (syntax := find_syntax_by_syntax_like(syntax, include_hidden=include_hidden_syntax)):
view.assign_syntax(syntax)

view.run_command("append", {"characters": content})

return view
23 changes: 12 additions & 11 deletions plugin/rules/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -46,7 +47,10 @@ 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

src_setting: StConstraintRule | None = None
"""The source setting object."""

def is_droppable(self) -> bool:
return not (self.constraint and not self.constraint.is_droppable())
Expand Down Expand Up @@ -74,21 +78,18 @@ def test(self, view_snapshot: ViewSnapshot) -> bool:
def make(cls, constraint_rule: StConstraintRule) -> Self:
"""Build this object with the `constraint_rule`."""
obj = cls()
obj.src_setting = constraint_rule

if args := constraint_rule.get("args"):
# make sure args is always a tuple
obj.args = tuple(args) if isinstance(args, list) else (args,)

if kwargs := constraint_rule.get("kwargs"):
obj.kwargs = kwargs

if (inverted := constraint_rule.get("inverted")) is not None:
obj.inverted = bool(inverted)
obj.args = tuple(constraint_rule.args)
obj.kwargs = constraint_rule.kwargs
obj.inverted = constraint_rule.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

Expand Down
27 changes: 14 additions & 13 deletions plugin/rules/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,14 +31,15 @@ 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()
kwargs: dict[str, Any] = field(default_factory=dict)
rules: tuple[MatchableRule, ...] = tuple()

src_setting: StMatchRule | None = None
"""The source setting object."""

def is_droppable(self) -> bool:
return not (self.rules and self.match and not self.match.is_droppable(self.rules))

Expand All @@ -62,25 +64,24 @@ def test(self, view_snapshot: ViewSnapshot) -> bool:
def make(cls, match_rule: StMatchRule) -> Self:
"""Build this object with the `match_rule`."""
obj = cls()
obj.src_setting = match_rule

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)
Expand Down
21 changes: 8 additions & 13 deletions plugin/rules/syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class SyntaxRule(Optimizable):
"""`None` = no restriction, empty = no event = never triggered."""
root_rule: MatchRule | None = None

src_setting: StSyntaxRule | None = None
"""The source 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)

Expand Down Expand Up @@ -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.src_setting = 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):
Expand Down
Loading

0 comments on commit 3f6a92b

Please sign in to comment.