Introduce vscode extension with syntax highlighting (#314)
This commit is contained in:
Родитель
dbe9ffd544
Коммит
d0dbe22e0c
|
@ -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"
|
||||
},
|
||||
|
|
Загрузка…
Ссылка в новой задаче