Use Gitea Releases for installation and ship VS Code highlighting
This commit is contained in:
1 parent
a46bd6e132
commit
201bdee372
243 files changed
+3289
-12521
No files matched your search
@@ -0,0 +1,264 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createRequire } = require('node:module');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const extension = path.join(root, 'editor/vscode');
|
||||
const extensionRequire = createRequire(path.join(extension, 'package.json'));
|
||||
const textmate = extensionRequire('vscode-textmate');
|
||||
const oniguruma = extensionRequire('vscode-oniguruma');
|
||||
const cliArguments = process.argv.slice(2);
|
||||
assert.equal(cliArguments.length, 2, 'pass --markdown-grammar followed by the real VS Code grammar path');
|
||||
assert.equal(cliArguments[0], '--markdown-grammar');
|
||||
const markdownPath = path.resolve(cliArguments[1]);
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(extension, 'package.json'), 'utf8'));
|
||||
const configuration = JSON.parse(fs.readFileSync(path.join(extension, 'language-configuration.json'), 'utf8'));
|
||||
|
||||
async function loadGrammars() {
|
||||
const wasm = fs.readFileSync(extensionRequire.resolve('vscode-oniguruma/release/onig.wasm'));
|
||||
await oniguruma.loadWASM(wasm.buffer.slice(wasm.byteOffset, wasm.byteOffset + wasm.byteLength));
|
||||
const catalog = new Map();
|
||||
for (const contribution of manifest.contributes.grammars) {
|
||||
const filename = path.join(extension, contribution.path);
|
||||
catalog.set(contribution.scopeName, textmate.parseRawGrammar(fs.readFileSync(filename, 'utf8'), filename));
|
||||
}
|
||||
const markdown = textmate.parseRawGrammar(fs.readFileSync(markdownPath, 'utf8'), markdownPath);
|
||||
assert.equal(markdown.scopeName, 'text.html.markdown');
|
||||
catalog.set(markdown.scopeName, markdown);
|
||||
const registry = new textmate.Registry({
|
||||
onigLib: Promise.resolve({
|
||||
createOnigScanner: patterns => new oniguruma.OnigScanner(patterns),
|
||||
createOnigString: value => new oniguruma.OnigString(value)
|
||||
}),
|
||||
loadGrammar: scope => catalog.get(scope) || null,
|
||||
getInjections: scope => manifest.contributes.grammars
|
||||
.filter(contribution => contribution.injectTo?.includes(scope))
|
||||
.map(contribution => contribution.scopeName)
|
||||
});
|
||||
return {
|
||||
source: await registry.loadGrammarWithConfiguration('source.dever', 1, {}),
|
||||
markdown: await registry.loadGrammarWithConfiguration('text.html.markdown', 2, {
|
||||
embeddedLanguages: { 'meta.embedded.block.dever': 1 },
|
||||
tokenTypes: { 'meta.embedded.block.dever': 0 }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const grammars = loadGrammars();
|
||||
|
||||
function tokenize(grammar, document) {
|
||||
let state = textmate.INITIAL;
|
||||
return document.split(/\r\n|\r|\n/).map(line => {
|
||||
const encoded = grammar.tokenizeLine2(line, state);
|
||||
const result = grammar.tokenizeLine(line, state);
|
||||
state = result.ruleStack;
|
||||
return { line, tokens: result.tokens, encoded: encoded.tokens };
|
||||
});
|
||||
}
|
||||
|
||||
function tokenAt(rows, line, fragment) {
|
||||
const row = rows[line];
|
||||
const offset = row.line.indexOf(fragment);
|
||||
assert.ok(offset >= 0, `line ${line + 1} does not contain ${fragment}`);
|
||||
const token = row.tokens.find(candidate => candidate.startIndex <= offset && candidate.endIndex > offset);
|
||||
assert.ok(token, `no token for ${fragment} on line ${line + 1}`);
|
||||
return token;
|
||||
}
|
||||
|
||||
function hasScope(rows, line, fragment, scope) {
|
||||
const actual = tokenAt(rows, line, fragment).scopes;
|
||||
assert.ok(actual.includes(scope), `${fragment}: expected ${scope}, received ${actual.join(' ')}`);
|
||||
}
|
||||
|
||||
function sourceFiles(directory, suffix) {
|
||||
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
||||
const filename = path.join(directory, entry.name);
|
||||
return entry.isDirectory() ? sourceFiles(filename, suffix) : filename.endsWith(suffix) ? [filename] : [];
|
||||
});
|
||||
}
|
||||
|
||||
test('types, fields and named function inputs/outputs receive Dever scopes', async () => {
|
||||
const rows = tokenize((await grammars).source, [
|
||||
'type Profile {',
|
||||
' id: Int?',
|
||||
' name: Text',
|
||||
'}',
|
||||
'greet(name: Text) (message: Text) {',
|
||||
' message = app.format(name)',
|
||||
'}'
|
||||
].join('\n'));
|
||||
hasScope(rows, 0, 'type', 'storage.type.dever');
|
||||
hasScope(rows, 0, 'Profile', 'entity.name.type.dever');
|
||||
hasScope(rows, 1, 'id', 'variable.other.member.dever');
|
||||
hasScope(rows, 1, 'Int', 'support.type.dever');
|
||||
hasScope(rows, 4, 'greet', 'entity.name.function.dever');
|
||||
hasScope(rows, 4, 'name', 'variable.other.member.dever');
|
||||
hasScope(rows, 4, 'message', 'variable.other.member.dever');
|
||||
hasScope(rows, 5, 'app', 'entity.name.namespace.dever');
|
||||
hasScope(rows, 5, 'format', 'entity.name.function.dever');
|
||||
});
|
||||
|
||||
test('API, Model, Job and external Adapter declarations are covered', async () => {
|
||||
const source = (await grammars).source;
|
||||
for (const method of ['get', 'post', 'put', 'delete', 'cmd', 'rest']) {
|
||||
const rows = tokenize(source, `${method} profile = app.profile`);
|
||||
hasScope(rows, 0, method, 'keyword.control.api.dever');
|
||||
}
|
||||
const rows = tokenize(source, [
|
||||
'public get profile = app.profile',
|
||||
' private owner user_id: user.account.model.id = dever.auth.user_id()',
|
||||
'index(user_id, created_at)',
|
||||
'sql owned_articles(id: Int) (articles: List<Article>) {',
|
||||
'job send(message: Text) () fails app.Error {',
|
||||
'schedule send_every_minute {',
|
||||
'external npm "worker.js" {}'
|
||||
].join('\n'));
|
||||
hasScope(rows, 0, 'public', 'storage.modifier.dever');
|
||||
hasScope(rows, 1, 'owner', 'storage.modifier.dever');
|
||||
hasScope(rows, 2, 'index', 'keyword.other.declaration.dever');
|
||||
hasScope(rows, 3, 'sql', 'keyword.other.declaration.dever');
|
||||
hasScope(rows, 4, 'job', 'storage.modifier.dever');
|
||||
hasScope(rows, 4, 'fails', 'storage.modifier.dever');
|
||||
hasScope(rows, 5, 'schedule', 'keyword.other.declaration.dever');
|
||||
hasScope(rows, 6, 'npm', 'keyword.other.external.dever');
|
||||
});
|
||||
|
||||
test('hash comments, quoted text and escapes do not leak into subsequent lines', async () => {
|
||||
const rows = tokenize((await grammars).source, [
|
||||
'# type Fake and return',
|
||||
String.raw`message = "中文 # and type \n \u{4E2D} \""`,
|
||||
String.raw`invalid = "\q"`,
|
||||
'broken = "unfinished',
|
||||
'type Next { value: Text }'
|
||||
].join('\n'));
|
||||
hasScope(rows, 0, 'type', 'comment.line.number-sign.dever');
|
||||
hasScope(rows, 1, '#', 'string.quoted.double.dever');
|
||||
hasScope(rows, 1, '\\n', 'constant.character.escape.dever');
|
||||
hasScope(rows, 1, '\\u{4E2D}', 'constant.character.escape.dever');
|
||||
hasScope(rows, 2, '\\q', 'invalid.illegal.escape.dever');
|
||||
hasScope(rows, 4, 'type', 'storage.type.dever');
|
||||
assert.ok(!tokenAt(rows, 4, 'type').scopes.includes('string.quoted.double.dever'));
|
||||
});
|
||||
|
||||
test('integer division is an operator, not a JavaScript line comment', async () => {
|
||||
const rows = tokenize((await grammars).source, 'count = 12 // 3\namount = 1.25e-2\nvalid = true and not false');
|
||||
hasScope(rows, 0, '//', 'keyword.operator.dever');
|
||||
hasScope(rows, 0, '3', 'constant.numeric.dever');
|
||||
assert.ok(!tokenAt(rows, 0, '3').scopes.some(scope => scope.startsWith('comment.')));
|
||||
hasScope(rows, 1, '1.25e-2', 'constant.numeric.dever');
|
||||
hasScope(rows, 2, 'true', 'constant.language.dever');
|
||||
hasScope(rows, 2, 'and', 'keyword.operator.logical.dever');
|
||||
});
|
||||
|
||||
test('excluded control-flow syntax is not presented as a valid function', async () => {
|
||||
const rows = tokenize((await grammars).source, 'if(value)\nreturn value\nimport other\npackage main');
|
||||
for (const [line, keyword] of ['if', 'return', 'import', 'package'].entries()) {
|
||||
hasScope(rows, line, keyword, 'invalid.illegal.keyword.dever');
|
||||
}
|
||||
});
|
||||
|
||||
test('Markdown fences reuse source.dever while prose keeps Markdown scopes', async () => {
|
||||
for (const marker of ['```', '~~~']) {
|
||||
for (const language of ['dever', 'typescript dever']) {
|
||||
const rows = tokenize((await grammars).markdown, [
|
||||
'# 文档', '', '## 类型', `${marker}${language}`,
|
||||
'type Profile { name: Text }', marker, '', '正文 type Profile'
|
||||
].join('\n'));
|
||||
hasScope(rows, 4, 'type', 'storage.type.dever');
|
||||
hasScope(rows, 4, 'Text', 'support.type.dever');
|
||||
hasScope(rows, 4, 'Profile', 'meta.embedded.block.dever');
|
||||
assert.ok(!tokenAt(rows, 7, 'type').scopes.includes('source.dever'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('embedded tokens carry the Dever language ID and are not string tokens', async () => {
|
||||
const rows = tokenize((await grammars).markdown, '```dever\ntype Profile {}\n```');
|
||||
const metadata = rows[1].encoded[1];
|
||||
assert.equal(metadata & 0xff, 1);
|
||||
assert.equal((metadata >>> 8) & 3, 0);
|
||||
});
|
||||
|
||||
test('fence closing honors its character and minimum opening length', async () => {
|
||||
const rows = tokenize((await grammars).markdown, [
|
||||
'````dever', 'type First {}', '```', 'type Second {}',
|
||||
'~~~~', 'type Third {}', '`````', '正文 type Outside'
|
||||
].join('\n'));
|
||||
for (const line of [1, 3, 5]) hasScope(rows, line, 'type', 'storage.type.dever');
|
||||
assert.ok(!tokenAt(rows, 7, 'type').scopes.includes('source.dever'));
|
||||
const waves = tokenize((await grammars).markdown, '~~~dever\ntype Inside {}\n~~~~\n正文 type Outside');
|
||||
assert.ok(!tokenAt(waves, 3, 'type').scopes.includes('source.dever'));
|
||||
});
|
||||
|
||||
test('valid indentation and CRLF work without taking over other language fences', async () => {
|
||||
const rows = tokenize((await grammars).markdown, ' ```dever\r\ntype Profile {}\r\n ```\r\n正文');
|
||||
hasScope(rows, 1, 'type', 'storage.type.dever');
|
||||
for (const language of ['typescript', 'javascript', 'dever extra', 'typescript dever']) {
|
||||
const other = tokenize((await grammars).markdown, `\`\`\`${language}\ntype Profile {}\n\`\`\``);
|
||||
assert.ok(!tokenAt(other, 1, 'type').scopes.includes('source.dever'), language);
|
||||
}
|
||||
});
|
||||
|
||||
test('quotes, lists, showcase fences, indented code and comments retain Markdown scopes', async () => {
|
||||
for (const [document, line] of [
|
||||
['> ```dever\n> type Quoted {}\n> ```', 1],
|
||||
['- 示例\n\n ```dever\n type Nested {}\n ```', 3],
|
||||
['````markdown\n```dever\ntype Showcase {}\n```\n````', 2],
|
||||
[' ```dever\n type Indented {}\n ```', 1],
|
||||
['<!--\n```dever\ntype Comment {}\n```\n-->', 2]
|
||||
]) {
|
||||
const rows = tokenize((await grammars).markdown, document);
|
||||
assert.ok(!tokenAt(rows, line, 'type').scopes.includes('source.dever'), document);
|
||||
}
|
||||
});
|
||||
|
||||
test('language configuration uses Dever comments, pairs and indentation', () => {
|
||||
assert.equal(configuration.comments.lineComment, '#');
|
||||
assert.equal(configuration.comments.blockComment, undefined);
|
||||
assert.deepEqual(configuration.brackets, [['{', '}'], ['[', ']'], ['(', ')']]);
|
||||
const increase = new RegExp(configuration.indentationRules.increaseIndentPattern);
|
||||
const decrease = new RegExp(configuration.indentationRules.decreaseIndentPattern);
|
||||
for (const line of ['greet(name: Text) (message: Text) {', ' values = [', 'type Profile { # 字段']) {
|
||||
assert.ok(increase.test(line), line);
|
||||
}
|
||||
for (const line of ['# example {', 'message = "literal {"', 'message = "value" # {']) {
|
||||
assert.ok(!increase.test(line), line);
|
||||
}
|
||||
assert.ok(decrease.test(' }'));
|
||||
assert.ok(decrease.test(' ]'));
|
||||
assert.ok(configuration.autoClosingPairs.every(pair => pair.notIn.includes('string') && pair.notIn.includes('comment')));
|
||||
assert.deepEqual(manifest.contributes.languages[0].extensions, ['.dever']);
|
||||
const injection = manifest.contributes.grammars.find(contribution => contribution.injectTo?.includes('text.html.markdown'));
|
||||
assert.deepEqual(injection.embeddedLanguages, { 'meta.embedded.block.dever': 'dever' });
|
||||
assert.deepEqual(injection.tokenTypes, { 'meta.embedded.block.dever': 'other' });
|
||||
assert.equal(manifest.main, undefined);
|
||||
assert.equal(manifest.dependencies, undefined);
|
||||
});
|
||||
|
||||
test('maintained plain and Markdown CMS files tokenize without illegal tokens', async () => {
|
||||
const loaded = await grammars;
|
||||
for (const [directory, suffix, grammar] of [
|
||||
['examples/cms/dever', '.dever', loaded.source],
|
||||
['examples/cms/md', '.dever.md', loaded.markdown]
|
||||
]) {
|
||||
const files = sourceFiles(path.join(root, directory), suffix);
|
||||
assert.ok(files.length > 0, directory);
|
||||
let highlighted = 0;
|
||||
for (const filename of files) {
|
||||
const rows = tokenize(grammar, fs.readFileSync(filename, 'utf8'));
|
||||
let fileHighlighted = 0;
|
||||
for (const row of rows) {
|
||||
for (const token of row.tokens) {
|
||||
assert.ok(!token.scopes.some(scope => scope.startsWith('invalid.illegal.')), `${filename}: ${row.line}`);
|
||||
if (token.scopes.includes('source.dever')) fileHighlighted += 1;
|
||||
}
|
||||
}
|
||||
assert.ok(fileHighlighted > 0, `${filename} must actually enter the Dever grammar`);
|
||||
highlighted += fileHighlighted;
|
||||
}
|
||||
assert.ok(highlighted > files.length, `${directory} must actually enter the Dever grammar`);
|
||||
process.stdout.write(`CMS ${directory}: ${files.length} files, ${highlighted} Dever tokens\n`);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user