From 23981f82cea41ceb3fffbd40449b15697a945e33 Mon Sep 17 00:00:00 2001 From: John Doty Date: Fri, 6 Sep 2024 10:20:34 -0700 Subject: [PATCH] Start working on emacs mode generation --- parser/emacs.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 parser/emacs.py diff --git a/parser/emacs.py b/parser/emacs.py new file mode 100644 index 0000000..2f51c0b --- /dev/null +++ b/parser/emacs.py @@ -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)