Start working on emacs mode generation
This commit is contained in:
parent
501c2e3fbe
commit
23981f82ce
1 changed files with 120 additions and 0 deletions
120
parser/emacs.py
Normal file
120
parser/emacs.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue