rossi-cli 0.1.0

Command-line interface for the Rossi Event-B toolchain
//! VS Code / TextMate grammar (`eventb.tmLanguage.json`).
//!
//! Whole-file: a TextMate grammar is pure token data, so the entire file is
//! generated. Consumed natively by VS Code, GitHub, Monaco and IntelliJ.

use serde_json::{Map, Value, json};

use super::Model;

const SCHEMA: &str =
    "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json";

const NOTICE: &str =
    "Generated by `rossi gen-grammars` from the canonical token tables. Do not edit by hand.";

/// Render the complete `eventb.tmLanguage.json` document.
pub fn render(model: &Model) -> String {
    let mut repository = Map::new();
    let mut includes: Vec<Value> = Vec::new();

    // Comments and strings come first so `//`, `/*` and `"…"` win over the
    // operator patterns (which include `/`, `*`).
    repository.insert("comments".into(), comments());
    includes.push(json!({ "include": "#comments" }));
    repository.insert("strings".into(), strings());
    includes.push(json!({ "include": "#strings" }));

    // Generated token groups, in model order.
    for (i, group) in model.groups.iter().enumerate() {
        let key = format!("tokens_{i}");
        repository.insert(
            key.clone(),
            json!({ "name": group.scope.textmate(), "match": group.regex_oniguruma() }),
        );
        includes.push(json!({ "include": format!("#{key}") }));
    }

    // Numbers, labels and identifiers come last (identifiers would otherwise
    // shadow every keyword).
    repository.insert(
        "numbers".into(),
        single("constant.numeric.eventb", "\\b[0-9]+\\b"),
    );
    includes.push(json!({ "include": "#numbers" }));
    repository.insert(
        "labels".into(),
        single("entity.name.tag.eventb", "@[A-Za-z0-9_]+"),
    );
    includes.push(json!({ "include": "#labels" }));
    repository.insert(
        "identifiers".into(),
        single("variable.other.eventb", "[a-zA-Z_][a-zA-Z0-9_]*'?"),
    );
    includes.push(json!({ "include": "#identifiers" }));

    let doc = json!({
        "$schema": SCHEMA,
        "name": "Event-B",
        "scopeName": "source.eventb",
        "information_for_contributors": [NOTICE],
        "patterns": includes,
        "repository": Value::Object(repository),
    });

    let mut out = serde_json::to_string_pretty(&doc).expect("serializable grammar");
    out.push('\n');
    out
}

fn single(scope: &str, regex: &str) -> Value {
    json!({ "patterns": [ { "name": scope, "match": regex } ] })
}

fn comments() -> Value {
    json!({
        "patterns": [
            { "name": "comment.line.double-slash.eventb", "match": "//.*$" },
            { "name": "comment.block.eventb", "begin": "/\\*", "end": "\\*/" }
        ]
    })
}

fn strings() -> Value {
    json!({
        "patterns": [
            {
                "name": "string.quoted.double.eventb",
                "begin": "\"",
                "end": "\"",
                "patterns": [
                    { "name": "constant.character.escape.eventb", "match": "\\\\." }
                ]
            }
        ]
    })
}