From 773288f497b355e762a1ad399cd1a6fa0cd91d69 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 11 Mar 2024 15:48:31 +0100 Subject: [PATCH] chore: use templating --- README.md | 6 +- grammars/julia.template.json | 1087 ++++++++++++++++++++++++++++++++++ scripts/generate.js | 20 +- 3 files changed, 1107 insertions(+), 6 deletions(-) create mode 100644 grammars/julia.template.json diff --git a/README.md b/README.md index 020c9ee..ecd7059 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Julia grammar definition for Atom, VS Code, and GitHub. -The source of truth in this repo is `grammars/julia.json`; `julia.cson` and `julia_vscode.json` are automatically generated in a pre-commit hook. +The source of truth in this repo is `grammars/julia.template.json`; `julia.json`, `julia.cson`, and `julia_vscode.json` are automatically generated in a pre-commit hook or with `npm run generate`. ## Contributing @@ -13,10 +13,10 @@ We love contributors. Here are the steps we have taken to develop on this packag 0. Install prerequisites: [Node.js](https://nodejs.org/) and `npm` (comes bundled with Node). We recommend using a [Node version manager](https://github.com/search?q=node+version+manager+archived%3Afalse&type=repositories&ref=advsearch). LTS is recommended, but any node version newer than 14 should do. 1. Clone this repo and `cd` into it 2. Run `npm ci` -3. Open `grammars/julia.json` in your favourite editor and fix a bug or implement additional highlighting rules +3. Open `grammars/julia.template.json` in your favourite editor and fix a bug or implement additional highlighting rules 4. Add corresponding tests at the bottom of `test/test.js` 5. Run the updated tests with `npm run test` -6. Once tests pass and you're happy with your changes, commit them and open a PR against this repo. This should automatically run a pre-commit hook that generates derivative grammars for VS Code and Atom from `julia.json`. +6. Once tests pass and you're happy with your changes, commit them and open a PR against this repo. This should automatically run a pre-commit hook that generates derivative grammars for VS Code and Atom from `julia.template.json`. ### Testing the updated grammar in VS Code Follow the [julia-vscode developer instructions](https://github.com/julia-vscode/julia-vscode/blob/main/CONTRIBUTING.md) to get the extension setup. Afterwards, simply copy the updated `julia_vscode.json` from this repo into `julia-vscode/syntaxes` and you should see your changes in the debug editor after reloading it. diff --git a/grammars/julia.template.json b/grammars/julia.template.json new file mode 100644 index 0000000..e515a58 --- /dev/null +++ b/grammars/julia.template.json @@ -0,0 +1,1087 @@ +{ + "comment": "This grammar is used by Atom (Oniguruma), GitHub (PCRE), and VSCode (Oniguruma),\nso all regexps must be compatible with both engines.\n\nSpecs:\n- https://github.com/kkos/oniguruma/blob/master/doc/RE\n- https://www.pcre.org/current/doc/html/", + "fileTypes": [ + "jl" + ], + "firstLineMatch": "^#!.*\\bjulia\\s*$", + "name": "Julia", + "patterns": [ + { + "include": "#operator" + }, + { + "include": "#array" + }, + { + "include": "#string" + }, + { + "include": "#parentheses" + }, + { + "include": "#bracket" + }, + { + "include": "#function_decl" + }, + { + "include": "#function_call" + }, + { + "include": "#for_block" + }, + { + "include": "#keyword" + }, + { + "include": "#number" + }, + { + "include": "#comment" + }, + { + "include": "#type_decl" + }, + { + "include": "#symbol" + }, + { + "include": "#punctuation" + } + ], + "repository": { + "array": { + "patterns": [ + { + "begin": "\\[", + "beginCaptures": { + "0": { + "name": "meta.bracket.julia" + } + }, + "end": "(\\])((?:\\.)?'*)", + "endCaptures": { + "1": { + "name": "meta.bracket.julia" + }, + "2": { + "name": "keyword.operator.transpose.julia" + } + }, + "name": "meta.array.julia", + "patterns": [ + { + "match": "\\bbegin\\b", + "name": "constant.numeric.julia" + }, + { + "match": "\\bend\\b", + "name": "constant.numeric.julia" + }, + { + "include": "#self_no_for_block" + } + ] + } + ] + }, + "parentheses": { + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "meta.bracket.julia" + } + }, + "end": "(\\))((?:\\.)?'*)", + "endCaptures": { + "1": { + "name": "meta.bracket.julia" + }, + "2": { + "name": "keyword.operator.transpose.julia" + } + }, + "patterns": [ + { + "include": "#self_no_for_block" + } + ] + } + ] + }, + "bracket": { + "patterns": [ + { + "begin": "\\{", + "beginCaptures": { + "0": { + "name": "meta.bracket.julia" + } + }, + "end": "(\\})((?:\\.)?'*)", + "endCaptures": { + "1": { + "name": "meta.bracket.julia" + }, + "2": { + "name": "keyword.operator.transpose.julia" + } + }, + "patterns": [ + { + "include": "#self_no_for_block" + } + ] + } + ] + }, + "comment_tags": { + "patterns": [ + { + "match": "\\bTODO\\b", + "name": "keyword.other.comment-annotation.julia" + }, + { + "match": "\\bFIXME\\b", + "name": "keyword.other.comment-annotation.julia" + }, + { + "match": "\\bCHANGED\\b", + "name": "keyword.other.comment-annotation.julia" + }, + { + "match": "\\bXXX\\b", + "name": "keyword.other.comment-annotation.julia" + } + ] + }, + "comment": { + "patterns": [ + { + "include": "#comment_block" + }, + { + "begin": "#", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.julia" + } + }, + "end": "\\n", + "name": "comment.line.number-sign.julia", + "patterns": [ + { + "include": "#comment_tags" + } + ] + } + ] + }, + "comment_block": { + "patterns": [ + { + "begin": "#=", + "beginCaptures": { + "0": { + "name": "punctuation.definition.comment.begin.julia" + } + }, + "end": "=#", + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.end.julia" + } + }, + "name": "comment.block.number-sign-equals.julia", + "patterns": [ + { + "include": "#comment_tags" + }, + { + "include": "#comment_block" + } + ] + } + ] + }, + "function_call": { + "patterns": [ + { + "begin": "({{id}})({(?:[^{}]|{(?:[^{}]|{[^{}]*})*})*})?\\.?(\\()", + "beginCaptures": { + "1": { + "name": "support.function.julia" + }, + "2": { + "name": "support.type.julia" + }, + "3": { + "name": "meta.bracket.julia" + } + }, + "end": "\\)(('|(\\.'))*\\.?')?", + "endCaptures": { + "0": { + "name": "meta.bracket.julia" + }, + "1": { + "name": "keyword.operator.transposed-func.julia" + } + }, + "patterns": [ + { + "include": "#self_no_for_block" + } + ] + } + ] + }, + "function_decl": { + "patterns": [ + { + "captures": { + "1": { + "name": "entity.name.function.julia" + }, + "2": { + "name": "support.type.julia" + } + }, + "match": "({{id}})({(?:[^{}]|{(?:[^{}]|{[^{}]*})*})*})?(?=\\([^#]*\\)(::[^\\s]+)?(\\s*\\bwhere\\b\\s+.+?)?\\s*?=(?![=>]))", + "comment": "first group is function name\nSecond group is type parameters (e.g. {T<:Number, S})\nThen open parens\nThen a lookahead ensures that we are followed by:\n - anything (function arguments)\n - 0 or more spaces\n - Finally an equal sign\nNegative lookahead ensures we don't have another equal sign (not `==`)" + }, + { + "captures": { + "1": { + "name": "keyword.other.julia" + }, + "2": { + "name": "keyword.operator.dots.julia" + }, + "3": { + "name": "entity.name.function.julia" + }, + "4": { + "name": "support.type.julia" + } + }, + "match": "\\b(function|macro)(?:\\s+(?:{{id}}(\\.))?({{id}})({(?:[^{}]|{(?:[^{}]|{[^{}]*})*})*})?|\\s*)(?=\\()", + "comment": "similar regex to previous, but with keyword not 1-line syntax" + } + ] + }, + "for_block": { + "comment": "for blocks need to be special-cased to support tokenizing 'outer' properly", + "patterns": [ + { + "begin": "\\b(for)\\b", + "beginCaptures": { + "0": { + "name": "keyword.control.julia" + } + }, + "end": "(?|<-|-->|=>)", + "name": "keyword.operator.arrow.julia" + }, + { + "match": "(?::=|\\+=|-=|\\*=|//=|/=|\\.//=|\\./=|\\.\\*=|\\\\=|\\.\\\\=|\\^=|\\.\\^=|%=|\\.%=|÷=|\\.÷=|\\|=|&=|\\.&=|⊻=|\\.⊻=|\\$=|<<=|>>=|>>>=|=(?!=))", + "name": "keyword.operator.update.julia" + }, + { + "match": "(?:<<|>>>|>>|\\.>>>|\\.>>|\\.<<)", + "name": "keyword.operator.shift.julia" + }, + { + "match": "(?:\\s*(::|>:|<:)\\s*((?:(?:Union)?\\([^)]*\\)|[[:alpha:]_$∇][[:word:]⁺-ₜ!′\\.]*(?:(?:{(?:[^{}]|{(?:[^{}]|{[^{}]*})*})*})|(?:\".+?(?=|\\.>|\\.<=|\\.<|\\.≤|\\.≥|==|\\.!=|\\.=|\\.!|<:|>:|:>|(?)>=|(?|<|≥|≤)", + "name": "keyword.operator.relation.julia" + }, + { + "match": "(?<=\\s)(?:\\?)(?=\\s)", + "name": "keyword.operator.ternary.julia" + }, + { + "match": "(?<=\\s)(?:\\:)(?=\\s)", + "name": "keyword.operator.ternary.julia" + }, + { + "match": "(?:\\|\\||&&|(?)", + "name": "keyword.operator.applies.julia" + }, + { + "match": "(?:\\||\\.\\||\\&|\\.\\&|~|\\.~|⊻|\\.⊻)", + "name": "keyword.operator.bitwise.julia" + }, + { + "match": "(?:\\+\\+|--|\\+|\\.\\+|-|\\.\\-|\\*|\\.\\*|//(?!=)|\\.//(?!=)|/|\\./|%|\\.%|\\\\|\\.\\\\|\\^|\\.\\^|÷|\\.÷|⋅|\\.⋅|∩|\\.∩|∪|\\.∪|×|√|∛)", + "name": "keyword.operator.arithmetic.julia" + }, + { + "match": "(?:∘)", + "name": "keyword.operator.compose.julia" + }, + { + "match": "(?:::|(?<=\\s)isa(?=\\s))", + "name": "keyword.operator.isa.julia" + }, + { + "match": "(?:(?<=\\s)in(?=\\s))", + "name": "keyword.operator.relation.in.julia" + }, + { + "match": "(?:\\.(?=(?:@|_|\\p{L}))|\\.\\.+)", + "name": "keyword.operator.dots.julia" + }, + { + "match": "(?:\\$)(?=.+)", + "name": "keyword.operator.interpolation.julia" + }, + { + "captures": { + "2": { + "name": "keyword.operator.transposed-variable.julia" + } + }, + "match": "({{id}})(('|(\\.'))*\\.?')" + }, + { + "captures": { + "1": { + "name": "bracket.end.julia" + }, + "2": { + "name": "keyword.operator.transposed-matrix.julia" + } + }, + "match": "(\\])((?:'|(?:\\.'))*\\.?')" + }, + { + "captures": { + "1": { + "name": "bracket.end.julia" + }, + "2": { + "name": "keyword.operator.transposed-parens.julia" + } + }, + "match": "(\\))((?:'|(?:\\.'))*\\.?')" + } + ] + }, + "string": { + "patterns": [ + { + "begin": "(?:(@doc)\\s((?:doc)?\"\"\")|(doc\"\"\"))", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "(\"\"\") ?(->)?", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.julia" + }, + "2": { + "name": "keyword.operator.arrow.julia" + } + }, + "name": "string.docstring.julia", + "patterns": [ + { + "include": "#string_escaped_char" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "begin": "(i?cxx)(\"\"\")", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "\"\"\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.julia" + } + }, + "name": "embed.cxx.julia", + "contentName": "source.cpp", + "patterns": [ + { + "include": "source.cpp" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "begin": "(py)(\"\"\")", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "([\\s\\w]*)(\"\"\")", + "endCaptures": { + "2": { + "name": "punctuation.definition.string.end.julia" + } + }, + "name": "embed.python.julia", + "contentName": "source.python", + "patterns": [ + { + "include": "source.python" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "begin": "(js)(\"\"\")", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "\"\"\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.julia" + } + }, + "name": "embed.js.julia", + "contentName": "source.js", + "patterns": [ + { + "include": "source.js" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "begin": "(R)(\"\"\")", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "\"\"\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.julia" + } + }, + "name": "embed.R.julia", + "contentName": "source.r", + "patterns": [ + { + "include": "source.r" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "begin": "(raw)(\"\"\")", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "\"\"\"", + "name": "string.quoted.other.julia", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.julia" + } + }, + "patterns": [ + { + "include": "#string_escaped_char" + } + ] + }, + { + "begin": "(raw)(\")", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "\"", + "name": "string.quoted.other.julia", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.julia" + } + }, + "patterns": [ + { + "include": "#string_escaped_char" + } + ] + }, + { + "begin": "(sql)(\"\"\")", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "\"\"\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.julia" + } + }, + "name": "embed.sql.julia", + "contentName": "meta.embedded.inline.sql", + "patterns": [ + { + "include": "source.sql" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "begin": "var\"\"\"", + "end": "\"\"\"", + "name": "constant.other.symbol.julia", + "patterns": [ + { + "include": "#string_escaped_char" + } + ] + }, + { + "begin": "var\"", + "end": "\"", + "name": "constant.other.symbol.julia", + "patterns": [ + { + "include": "#string_escaped_char" + } + ] + }, + { + "begin": "^\\s?(doc)?(\"\"\")\\s?$", + "beginCaptures": { + "1": { + "name": "support.function.macro.julia" + }, + "2": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "(\"\"\")", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.julia" + } + }, + "name": "string.docstring.julia", + "comment": "This only matches docstrings that start and end with triple quotes on\ntheir own line in the void", + "patterns": [ + { + "include": "#string_escaped_char" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "begin": "'", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "'(?!')", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.julia" + } + }, + "name": "string.quoted.single.julia", + "patterns": [ + { + "include": "#string_escaped_char" + } + ] + }, + { + "begin": "\"\"\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.multiline.begin.julia" + } + }, + "end": "\"\"\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.multiline.end.julia" + } + }, + "name": "string.quoted.triple.double.julia", + "comment": "multi-line string with triple double quotes", + "patterns": [ + { + "include": "#string_escaped_char" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "name": "string.quoted.double.julia", + "begin": "\"(?!\"\")", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.julia" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.julia" + } + }, + "comment": "String with single pair of double quotes. Regex matches isolated double quote", + "patterns": [ + { + "include": "#string_escaped_char" + }, + { + "include": "#string_dollar_sign_interpolate" + } + ] + }, + { + "begin": "r\"\"\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.regexp.begin.julia" + } + }, + "end": "(\"\"\")([imsx]{0,4})?", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.regexp.end.julia" + }, + "2": { + "comment": "I took this scope name from python regex grammar", + "name": "keyword.other.option-toggle.regexp.julia" + } + }, + "name": "string.regexp.julia", + "patterns": [ + { + "include": "#string_escaped_char" + } + ] + }, + { + "begin": "r\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.regexp.begin.julia" + } + }, + "end": "(\")([imsx]{0,4})?", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.regexp.end.julia" + }, + "2": { + "comment": "I took this scope name from python regex grammar", + "name": "keyword.other.option-toggle.regexp.julia" + } + }, + "name": "string.regexp.julia", + "patterns": [ + { + "include": "#string_escaped_char" + } + ] + }, + { + "begin": "(?!:_)(?:struct|mutable\\s+struct|abstract\\s+type|primitive\\s+type)\\s+({{id}})(\\s*(<:)\\s*{{id}}(?:{.*})?)?", + "name": "meta.type.julia" + } + ] + }, + "self_no_for_block": { + "comment": "Same as $self, but does not contain #for_block. 'outer' is not valid in some contexts (e.g. generators, comprehensions, indexing), so use this when matching those in begin/end patterns. Keep this up-to-date with $self!", + "patterns": [ + { + "include": "#operator" + }, + { + "include": "#array" + }, + { + "include": "#string" + }, + { + "include": "#parentheses" + }, + { + "include": "#bracket" + }, + { + "include": "#function_decl" + }, + { + "include": "#function_call" + }, + { + "include": "#keyword" + }, + { + "include": "#number" + }, + { + "include": "#comment" + }, + { + "include": "#type_decl" + }, + { + "include": "#symbol" + }, + { + "include": "#punctuation" + } + ] + }, + "punctuation": { + "patterns": [ + { + "match": ",", + "name": "punctuation.separator.comma.julia" + }, + { + "match": ";", + "name": "punctuation.separator.semicolon.julia" + } + ] + } + }, + "scopeName": "source.julia" +} \ No newline at end of file diff --git a/scripts/generate.js b/scripts/generate.js index 5af5eed..d774fa7 100644 --- a/scripts/generate.js +++ b/scripts/generate.js @@ -3,10 +3,11 @@ const fs = require('fs') const path = require('path') const outdir = path.join(__dirname, '..', 'grammars') -const grammar = JSON.parse(fs.readFileSync(path.join(outdir, 'julia.json'))) +let grammar = JSON.parse(fs.readFileSync(path.join(outdir, 'julia.template.json'))) -// write normal JSON grammar -fs.writeFileSync(path.join(outdir, 'julia.cson'), CSON.stringify(grammar, null, 2)) +const templateRules = { + '{{id}}': '(?:[[:alpha:]_\\\\p{Lu}\\\\p{Ll}\\\\p{Lt}\\\\p{Lm}\\\\p{Lo}\\\\p{Nl}\\\\p{Sc}⅀-⅄∿⊾⊿⊤⊥∂∅-∇∎∏∐∑∞∟∫-∳⋀-⋃◸-◿♯⟘⟙⟀⟁⦰-⦴⨀-⨆⨉-⨖⨛⨜𝛁𝛛𝛻𝜕𝜵𝝏𝝯𝞉𝞩𝟃ⁱ-⁾₁-₎∠-∢⦛-⦯℘℮゛-゜𝟎-𝟡]|[^\\\\P{So}←-⇿])(?:[[:word:]_!\\\\p{Lu}\\\\p{Ll}\\\\p{Lt}\\\\p{Lm}\\\\p{Lo}\\\\p{Nl}\\\\p{Sc}⅀-⅄∿⊾⊿⊤⊥∂∅-∇∎∏∐∑∞∟∫-∳⋀-⋃◸-◿♯⟘⟙⟀⟁⦰-⦴⨀-⨆⨉-⨖⨛⨜𝛁𝛛𝛻𝜕𝜵𝝏𝝯𝞉𝞩𝟃ⁱ-⁾₁-₎∠-∢⦛-⦯℘℮゛-゜𝟎-𝟡]|[^\\\\P{Mn}\\u0001-¡]|[^\\\\P{Mc}\\u0001-¡]|[^\\\\P{Nd}\\u0001-¡]|[^\\\\P{Pc}\\u0001-¡]|[^\\\\P{Sk}\\u0001-¡]|[^\\\\P{Me}\\u0001-¡]|[^\\\\P{No}\\u0001-¡]|[′-‷⁗]|[^\\\\P{So}←-⇿])*', +} // recurse through grammar and replace values: function recurseAndReplace(obj, key, val, replacement) { @@ -19,6 +20,19 @@ function recurseAndReplace(obj, key, val, replacement) { } } +// templating +let grammarString = JSON.stringify(grammar) +for (const [k, v] of Object.entries(templateRules)) { + grammarString = grammarString.replaceAll(k, v) +} +grammar = JSON.parse(grammarString) + +// write normal JSON grammar +fs.writeFileSync(path.join(outdir, 'julia.json'), JSON.stringify(grammar, null, 2)) + +// write normal CSON grammar +fs.writeFileSync(path.join(outdir, 'julia.cson'), CSON.stringify(grammar, null, 2)) + // fix markdown recurseAndReplace(grammar, 'include', 'source.gfm', 'text.html.markdown.julia')