Introduce vscode extension with syntax highlighting (#314)

This commit is contained in:
Nick Guerrera 2021-03-01 13:35:48 -08:00 коммит произвёл GitHub
Родитель dbe9ffd544
Коммит d0dbe22e0c
5 изменённых файлов: 508 добавлений и 2 удалений

Просмотреть файл

@ -0,0 +1,421 @@
// TextMate-based syntax highlighting is implemented in this file.
// adl.tmLanguage.json is generated by running this script.
import { writeFileSync } from "fs";
import { OnigRegExp } from "oniguruma";
const schema = "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json";
// Special scope that indicates a larger construct that doesn't get a single color.
// Expanded to meta.<key>.adl when we emit.
const meta = "<meta>";
/**
* The TextMate scope that gets assigned to a match and colored by a theme.
* See https://macromates.com/manual/en/language_grammars#naming_conventions
*/
type Scope =
| typeof meta
| "comment.block.adl"
| "comment.line.double-slash.adl"
| "constant.character.escape.adl"
| "constant.numeric.adl"
| "constant.language.adl"
| "entity.name.type.adl"
| "entity.name.function.adl"
| "keyword.other.adl"
| "string.quoted.double.adl"
| "variable.name.adl";
interface RuleKey {
/** Rule's unique key through which identifies the rule in the repository. */
key: string;
}
interface RuleScope {
scope: Scope;
}
interface RulePatterns {
patterns: Rule[];
}
type Captures = Record<string, RuleScope | RulePatterns>;
type Rule = MatchRule | BeginEndRule | IncludeRule;
interface MatchRule extends RuleScope, RuleKey {
match: string;
captures?: Captures;
}
interface BeginEndRule extends RuleKey, RuleScope, Partial<RulePatterns> {
begin: string;
end: string;
beginCaptures?: Captures;
endCaptures?: Captures;
}
interface IncludeRule extends RuleKey, RulePatterns {}
interface Grammar extends RulePatterns {
$schema: typeof schema;
name: string;
scopeName: string;
fileTypes: string[];
}
const identifierStart = "[_$[:alpha:]]";
const identifierContinue = "[_$[:alnum:]]";
const beforeIdentifier = `(?=${identifierStart})`;
const identifier = `\\b${identifierStart}${identifierContinue}*\\b`;
const stringPattern = '\\"(?:[^\\"\\\\]|\\\\.)*\\"';
const statementKeyword = `\\b(?:namespace|model|op)\\b`;
const universalEnd = `(?=,|;|@|\\)|\\}|${statementKeyword})`;
const hexNumber = "\\b(?<!\\$)0(?:x|X)[0-9a-fA-F][0-9a-fA-F_]*(n)?\\b(?!\\$)";
const binaryNumber = "\\b(?<!\\$)0(?:b|B)[01][01_]*(n)?\\b(?!\\$)";
const decimalNumber =
"(?<!\\$)(?:" +
"(?:\\b[0-9][0-9_]*(\\.)[0-9][0-9_]*[eE][+-]?[0-9][0-9_]*(n)?\\b)|" + // 1.1E+3
"(?:\\b[0-9][0-9_]*(\\.)[eE][+-]?[0-9][0-9_]*(n)?\\b)|" + // 1.E+3
"(?:\\B(\\.)[0-9][0-9_]*[eE][+-]?[0-9][0-9_]*(n)?\\b)|" + // .1E+3
"(?:\\b[0-9][0-9_]*[eE][+-]?[0-9][0-9_]*(n)?\\b)|" + // 1E+3
"(?:\\b[0-9][0-9_]*(\\.)[0-9][0-9_]*(n)?\\b)|" + // 1.1
"(?:\\b[0-9][0-9_]*(\\.)(n)?\\B)|" + // 1.
"(?:\\B(\\.)[0-9][0-9_]*(n)?\\b)|" + // .1
"(?:\\b[0-9][0-9_]*(n)?\\b(?!\\.))" + // 1
")(?!\\$)";
const anyNumber = `(?:${hexNumber}|${binaryNumber}|${decimalNumber})`;
const expression: IncludeRule = {
key: "expression",
patterns: [
/* placeholder filled later due to cycle*/
],
};
const statement: IncludeRule = {
key: "statement",
patterns: [
/*placeholder filled later due to cycle*/
],
};
const booleanLiteral: MatchRule = {
key: "boolean-literal",
scope: "constant.language.adl",
match: `\\b(true|false)\\b`,
};
const escapeChar: MatchRule = {
key: "escape-character",
scope: "constant.character.escape.adl",
match: "\\\\.",
};
// TODO: Triple-quoted """X""" currently matches as three string literals
// ("" "X" "") but should be its own thing.
const stringLiteral: BeginEndRule = {
key: "string-literal",
scope: "string.quoted.double.adl",
begin: '"',
end: '"',
patterns: [escapeChar],
};
const numericLiteral: MatchRule = {
key: "numeric-literal",
scope: "constant.numeric.adl",
match: anyNumber,
};
const lineComment: MatchRule = {
key: "line-comment",
scope: "comment.line.double-slash.adl",
match: "//.*$",
};
const blockComment: BeginEndRule = {
key: "block-comment",
scope: "comment.block.adl",
begin: "/\\*",
end: "\\*/",
};
// Tokens that match standing alone in any context: literals and comments
const token: IncludeRule = {
key: "token",
patterns: [lineComment, blockComment, stringLiteral, booleanLiteral, numericLiteral],
};
const parenthesizedExpression: BeginEndRule = {
key: "parenthesized-expression",
scope: meta,
begin: "\\(",
end: "\\)",
patterns: [expression],
};
const decorator: BeginEndRule = {
key: "decorator",
scope: meta,
begin: `@(${identifier})`,
beginCaptures: {
"1": { scope: "entity.name.function.adl" },
},
end: `${beforeIdentifier}|${universalEnd}`,
patterns: [token, parenthesizedExpression],
};
const identifierExpression: MatchRule = {
key: "type-name",
scope: "entity.name.type.adl",
match: identifier,
};
const typeAnnotation: BeginEndRule = {
key: "type-annotation",
scope: meta,
begin: "\\s*:",
end: universalEnd,
patterns: [expression],
};
const modelProperty: BeginEndRule = {
key: "model-property",
scope: meta,
begin: `(?:(${identifier})|(${stringPattern}))`,
beginCaptures: {
"1": { scope: "variable.name.adl" },
"2": { scope: "string.quoted.double.adl" },
},
end: universalEnd,
patterns: [token, typeAnnotation],
};
const modelSpreadProperty: BeginEndRule = {
key: "model-spread-property",
scope: meta,
begin: "\\.\\.\\.",
end: universalEnd,
patterns: [expression],
};
const modelExpression: BeginEndRule = {
key: "model-expression",
scope: meta,
begin: "\\{",
end: "\\}",
patterns: [token, decorator, modelProperty, modelSpreadProperty],
};
const modelHeritage: BeginEndRule = {
key: "model-heritage",
scope: meta,
begin: "\\b(extends)\\b",
beginCaptures: {
"1": { scope: "keyword.other.adl" },
},
end: universalEnd,
patterns: [expression],
};
const modelStatement: BeginEndRule = {
key: "model-statement",
scope: meta,
begin: "\\b(model)\\b",
beginCaptures: {
"1": { scope: "keyword.other.adl" },
},
end: `(?<=\\})|${universalEnd}`,
patterns: [
token,
modelHeritage, // before expression or`extends` will looki like type name
expression, // enough to match name, type parameters, and body and assignment.
],
};
const namespaceName: BeginEndRule = {
key: "namespace-name",
scope: meta,
begin: beforeIdentifier,
end: `((?=\\{)|${universalEnd})`,
patterns: [token, identifierExpression],
};
const namespaceBody: BeginEndRule = {
key: "namespace-body",
scope: meta,
begin: "\\{",
end: "\\}",
patterns: [statement],
};
const namespaceStatement: BeginEndRule = {
key: "namespace-statement",
scope: meta,
begin: "\\b(namespace)\\b",
beginCaptures: {
"1": { scope: "keyword.other.adl" },
},
end: `((?<=\\})|${universalEnd})`,
patterns: [token, namespaceName, namespaceBody],
};
const functionName: MatchRule = {
key: "function-name",
scope: "entity.name.function.adl",
match: identifier,
};
const operationName: BeginEndRule = {
key: "operation-name",
scope: meta,
begin: beforeIdentifier,
end: `((?=\\()|${universalEnd})`,
patterns: [token, functionName],
};
const operationParameters: BeginEndRule = {
key: "operation-parameters",
scope: meta,
begin: "\\(",
end: "\\)",
patterns: [token, decorator, modelProperty, modelSpreadProperty],
};
const operationStatement: BeginEndRule = {
key: "operation-statement",
scope: meta,
begin: "\\b(op)\\b",
beginCaptures: {
"1": { scope: "keyword.other.adl" },
},
end: universalEnd,
patterns: [
operationName,
operationParameters,
typeAnnotation, // return type
],
};
// NOTE: We don't actually classify all the different expression types and their
// punctuation yet. For now, at least, we only deal with the ones that would
// break coloring due to breaking out of context inappropriately with parens or
// braces that weren't handled with appropriate precedence. The other
// expressions color acceptably as unclassified punctuation around those we do
// handle here.
expression.patterns = [token, parenthesizedExpression, modelExpression, identifierExpression];
statement.patterns = [token, decorator, modelStatement, namespaceStatement, operationStatement];
const grammar: Grammar = {
$schema: schema,
name: "ADL",
scopeName: "source.adl",
fileTypes: [".adl"],
patterns: [statement],
};
/** Entry point, write the grammar to disk. */
function main() {
const filePath = "./dist/adl.tmlanguage.json";
const json = emit(grammar);
writeFileSync(filePath, json);
}
/** Emit the grammar to a JSON string matching tmlanguage.json schema. */
function emit(grammar: Grammar): string {
const indent = 2;
const processed = processGrammar(grammar);
return JSON.stringify(processed, undefined, indent);
}
/**
* Convert the grammar from our more convenient representation to the
* tmlanguage.json schema. Perform some validation in the process.
*/
function processGrammar(grammar: Grammar): any {
// key is rule.key, value is [unprocessed rule, processed rule]. unprocessed
// rule is used for its identity to check for duplicates and deal with cycles.
const repository = new Map<string, [Rule, any]>();
const output = processNode(grammar);
output.repository = processRepository();
return output;
function processNode(node: any): any {
if (typeof node !== "object") {
return node;
}
if (Array.isArray(node)) {
return node.map(processNode);
}
const output: any = {};
for (const key in node) {
const value = node[key];
switch (key) {
case "key":
// Drop it. It was used to place the node in the repository, and does
// not need to be retained on the node in the final structure.
break;
case "scope":
// tmlanguage uses "name" confusingly for scope. We avoid "name" which
// can be confused with the repository key.
output.name = value === meta ? `meta.${node.key}.adl` : value;
break;
case "begin":
case "end":
case "match":
validateRegexp(value, node, key);
output[key] = value;
break;
case "patterns":
output[key] = processPatterns(value);
break;
default:
output[key] = processNode(value);
break;
}
}
return output;
}
function processPatterns(rules: Rule[]) {
for (const rule of rules) {
if (!repository.has(rule.key)) {
// put placeholder first to prevent cycles
const entry: [Rule, any] = [rule, undefined];
repository.set(rule.key, entry);
// fill placeholder with processed node.
entry[1] = processNode(rule);
} else if (repository.get(rule.key)![0] !== rule) {
throw new Error("Duplicate key: " + rule.key);
}
}
return rules.map((r) => ({ include: `#${r.key}` }));
}
function processRepository() {
const output: any = {};
for (const key of [...repository.keys()].sort()) {
output[key] = repository.get(key)![1];
}
return output;
}
function validateRegexp(regexp: string, node: any, prop: string) {
try {
new OnigRegExp(regexp);
} catch (err) {
console.error(`Error: Bad regex: ${JSON.stringify({ [prop]: regexp })}`);
console.error(`Error: ${err.message}`);
console.error();
console.error("Context:");
console.dir(node);
console.error();
throw err;
}
}
}
main();

Просмотреть файл

@ -0,0 +1,23 @@
{
"comments": {
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"]
// NOTE: quotes omitted here intentionally for now as they interfere with typing """
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""]
]
}

Просмотреть файл

@ -0,0 +1,54 @@
{
"name": "adl-vscode",
"displayName": "ADL Language Support for VS Code",
"repository": {
"type": "git",
"url": "git+https://github.com/azure/adl"
},
"publisher": "Microsoft",
"description": "",
"version": "0.1.0",
"engines": {
"vscode": "^1.53.0"
},
"categories": [
"Programming Languages"
],
"scripts": {
"build": "tsc -p . && npm run tmlanguage-gen",
"watch": "tsc -p . --watch",
"tmlanguage-gen": "node dist/adl.tmlanguage.js",
"package": "vsce package",
"check-format": "prettier --list-different --config ../../.prettierrc.json --ignore-path ../../.prettierignore \"**/*.ts\" \"*.{js,json}\"",
"format": "prettier --write --config ../../.prettierrc.json --ignore-path ../../.prettierignore \"**/*.ts\" \"*.{js,json}\""
},
"devDependencies": {
"@types/node": "14.0.27",
"@types/oniguruma": "~7.0.1",
"oniguruma": "~7.2.1",
"typescript": "~4.1.5",
"vsce": "^1.85.1"
},
"contributes": {
"languages": [
{
"id": "adl",
"aliases": [
"ADL",
"adl"
],
"extensions": [
".adl"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "adl",
"scopeName": "source.adl",
"path": "./dist/adl.tmlanguage.json"
}
]
}
}

Просмотреть файл

@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "."
},
"include": ["./**/*.ts"],
"exclude": ["dist", "test/scenarios/**", "resources", "node_modules", "**/*.d.ts", "**/*.adl.ts"]
}

Просмотреть файл

@ -46,8 +46,7 @@
"@types/node": "14.0.27",
"grammarkdown": "^3.1.2",
"mocha": "7.1.2",
"ts-node": "^8.10.2",
"typescript": "~3.9.5",
"typescript": "~4.1.5",
"@types/yargs": "~15.0.12",
"prettier": "~2.2.1"
},