120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
# https://www.masteringemacs.org/article/lets-write-a-treesitter-major-mode
|
|
import dataclasses
|
|
import itertools
|
|
import pathlib
|
|
import textwrap
|
|
|
|
from parser.tree_sitter import terminal_name
|
|
|
|
from . import parser
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True, order=True)
|
|
class FaceQuery:
|
|
feature: str # Important to be first!
|
|
face: str
|
|
node: str
|
|
field: str | None
|
|
|
|
|
|
def gather_faces(grammar: parser.Grammar):
|
|
nts = {nt.name: nt for nt in grammar.non_terminals()}
|
|
|
|
def scoop(node: str, input: parser.FlattenedWithMetadata, visited: set[str]) -> list[FaceQuery]:
|
|
parts = []
|
|
for item in input:
|
|
if isinstance(item, tuple):
|
|
meta, sub = item
|
|
parts.extend(scoop(node, sub, visited))
|
|
|
|
highlight = meta.get("highlight")
|
|
if isinstance(highlight, parser.HighlightMeta):
|
|
field_name = meta.get("field")
|
|
if not isinstance(field_name, str):
|
|
raise Exception("Highlight must come with a field name") # TODO
|
|
|
|
feature = highlight.font_lock_feature
|
|
face = highlight.font_lock_face
|
|
if feature and face:
|
|
parts.append(
|
|
FaceQuery(
|
|
node=node,
|
|
field=field_name,
|
|
feature=feature,
|
|
face=face,
|
|
)
|
|
)
|
|
|
|
elif isinstance(item, str):
|
|
nt = nts[item]
|
|
if nt.transparent:
|
|
if nt.name in visited:
|
|
continue
|
|
visited.add(nt.name)
|
|
body = nt.fn(grammar)
|
|
for production in body.flatten(with_metadata=True):
|
|
parts.extend(scoop(node, production, visited))
|
|
|
|
return parts
|
|
|
|
queries: list[FaceQuery] = []
|
|
for rule in grammar.non_terminals():
|
|
if rule.transparent:
|
|
continue
|
|
|
|
body = rule.fn(grammar)
|
|
for production in body.flatten(with_metadata=True):
|
|
queries.extend(scoop(rule.name, production, set()))
|
|
|
|
for rule in grammar.terminals():
|
|
highlight = rule.meta.get("highlight")
|
|
if isinstance(highlight, parser.HighlightMeta):
|
|
feature = highlight.font_lock_feature
|
|
face = highlight.font_lock_face
|
|
if feature and face:
|
|
queries.append(
|
|
FaceQuery(
|
|
node=terminal_name(rule),
|
|
field=None,
|
|
feature=feature,
|
|
face=face,
|
|
)
|
|
)
|
|
|
|
# Remove duplicates, which happen.
|
|
queries = list(set(queries))
|
|
queries.sort()
|
|
|
|
# Group by feature.
|
|
features = []
|
|
for feature, qs in itertools.groupby(queries, key=lambda x: x.feature):
|
|
feature_group = f":language {grammar.name}\n:override t\n:feature {feature}\n"
|
|
|
|
face_queries = []
|
|
for query in qs:
|
|
if query.field:
|
|
fq = f"({query.node} {query.field}: _ @{query.face})"
|
|
else:
|
|
fq = f"({query.node}) @{query.face}"
|
|
face_queries.append(fq)
|
|
|
|
face_queries_str = "\n ".join(face_queries)
|
|
feature_group += f"({face_queries_str})\n"
|
|
|
|
features.append(feature_group)
|
|
|
|
feature_string = "\n".join(features)
|
|
feature_string = textwrap.indent(feature_string, " ")
|
|
feature_string = feature_string.strip()
|
|
|
|
feature_string = f"""
|
|
(defvar {grammar.name}-font-lock-rules
|
|
'({feature_string})
|
|
"Tree-sitter font lock rules for {grammar.name}.")
|
|
""".strip()
|
|
|
|
return feature_string
|
|
|
|
|
|
def emit_emacs_major_mode(grammar: parser.Grammar, path: pathlib.Path | str):
|
|
face_var = gather_faces(grammar)
|