tinymist/syntaxes/textmate/main.ts
Myriad-Dreamin c22f70b49e
dev: refactor def and use cache for improving definition (#179)
* chore: add a notes to do.

* dev: cache def and use information

* dev: move signature cache to analysis level

* dev: refactor a bit for improving definition

* dev: more appropriate definition discover

* fix: clippy error
2024-04-11 20:45:02 +08:00

1680 lines
35 KiB
TypeScript

import * as textmate from "./textmate";
// JS-Snippet to generate pattern
function generatePattern(maxDepth: number, lb: string, rb: string) {
const NOT_BRACE_PATTERN = `[^${rb}${lb}]`;
// Unrolled Pattern variants: 0=default, 1=unrolled (more efficient)
let p = [`${lb}${NOT_BRACE_PATTERN}*(?:`, `${NOT_BRACE_PATTERN}*)*${rb}`];
// Generate and display the pattern
return (
p[0].repeat(maxDepth) +
`${lb}${NOT_BRACE_PATTERN}*${rb}` +
p[1].repeat(maxDepth)
);
}
function lookAhead(pattern: RegExp) {
return new RegExp(`(?=(?:${pattern.source}))`);
}
function braceMatch(pattern: RegExp) {
return ("(?x)" + pattern.source) as unknown as RegExp;
}
const PAREN_BLOCK = generatePattern(6, "\\(", "\\)");
const CODE_BLOCK = generatePattern(6, "\\{", "\\}");
const BRACE_FREE_EXPR = /[^\s\}\{\[\]][^\}\{\[\]]*/.source;
// todo: This is invocable
const codeBlock: textmate.Pattern = {
// name: "meta.block.continuous.typst",
begin: /\{/,
end: /\}/,
beginCaptures: {
"0": {
name: "meta.brace.curly.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.curly.typst",
},
},
patterns: [
{
include: "#code",
},
],
};
const contentBlock: textmate.Pattern = {
// name: "meta.block.content.typst",
begin: /\[/,
end: /\]/,
beginCaptures: {
"0": {
name: "meta.brace.square.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.square.typst",
},
},
patterns: [
{
include: "#markup",
},
],
};
const primitiveColors: textmate.Pattern = {
match:
/\b(red|blue|green|black|white|gray|silver|eastern|navy|aqua|teal|purple|fuchsia|maroon|orange|yellow|olive|lime|ltr|rtl|ttb|btt|start|left|center|right|end|top|horizon|bottom)\b(?!-)/,
name: "support.type.builtin.typst",
};
const primitiveFunctions = {
match: /\b(?:luma|oklab|oklch|rgb|cmyk|range)\b(?!-)/,
name: "support.function.builtin.typst",
};
const primitiveTypes: textmate.PatternMatch = {
match:
/\b(auto|any|none|false|true|str|int|float|bool|length|content)\b(?!-)/,
name: "entity.name.type.primitive.typst",
};
const IDENTIFIER_BARE = /[\p{XID_Start}_][\p{XID_Continue}_\-]*/;
const IDENTIFIER = /(?<!\)|\]|\})\b[\p{XID_Start}_][\p{XID_Continue}_\-]*/;
// todo: distinguish type and variable
const identifier: textmate.PatternMatch = {
match: IDENTIFIER,
name: "variable.other.readwrite.typst",
};
const markupLabel: textmate.PatternMatch = {
name: "entity.other.label.typst",
match: /<[\p{XID_Start}_][\p{XID_Continue}_\-\.\:]*>/,
};
const markupReference: textmate.PatternMatch = {
name: "entity.other.reference.typst",
match:
/(@)[\p{XID_Start}_](?:[\p{XID_Continue}_\-]|[\.\:](?!:[\.\:]*[^\p{XID_Continue}_\-\.\:]))*/,
captures: {
"1": {
name: "punctuation.definition.reference.typst",
},
},
};
const markupEscape: textmate.PatternMatch = {
name: "constant.character.escape.content.typst",
match: /\\(?:[^u]|u\{?[0-9a-zA-Z]*\}?)/,
};
const stringLiteral: textmate.PatternBeginEnd = {
name: "string.quoted.double.typst",
begin: /"/,
end: /"/,
beginCaptures: {
"0": {
name: "punctuation.definition.string.typst",
},
},
endCaptures: {
"0": {
name: "punctuation.definition.string.typst",
},
},
patterns: [
{
match: /(\\(?:[^u]|u\{?[0-9a-zA-Z]*\}?))|[^\\"]+/,
captures: {
"1": {
name: "constant.character.escape.string.typst",
},
},
},
],
};
// todo: math mode
const markupMath: textmate.Pattern = {
name: "markup.math.typst",
begin: /\$/,
end: /\$/,
beginCaptures: {
"0": {
name: "punctuation.definition.string.math.typst",
},
},
endCaptures: {
"0": {
name: "punctuation.definition.string.math.typst",
},
},
patterns: [
{
include: "#markupEscape",
},
{
include: "#markupEnterCode",
},
],
};
const markupHeading: textmate.Pattern = {
name: "markup.heading.typst",
begin: /^\s*=+\s+/,
end: /\n|(?=<)/,
beginCaptures: {
"0": {
name: "punctuation.definition.heading.typst",
},
},
patterns: [
{
include: "#markup",
},
],
};
const common: textmate.Pattern = {
patterns: [
{
include: "#strictComments",
},
{
include: "#blockRaw",
},
{
include: "#inlineRaw",
},
],
};
const markup: textmate.Pattern = {
patterns: [
{
include: "#common",
},
{
include: "#markupEnterCode",
},
{
include: "#markupEscape",
},
{
name: "punctuation.definition.linebreak.typst",
match: /\\/,
},
{
name: "punctuation.definition.nonbreaking-space.typst",
match: /\~/,
},
{
name: "punctuation.definition.shy.typst",
match: /-\?/,
},
{
name: "punctuation.definition.em-dash.typst",
match: /---/,
},
{
name: "punctuation.definition.en-dash.typst",
match: /--/,
},
{
name: "punctuation.definition.ellipsis.typst",
match: /\.\.\./,
},
// what is it?
// {
// name: "constant.symbol.typst",
// match: /:([a-zA-Z0-9]+:)+/,
// },
// # These two markup are buggy
// # - include: '#markupBold'
// # - include: '#markupItalic'
{
name: "markup.underline.link.typst",
match: /https?:\/\/[0-9a-zA-Z~\/%#&='',;\.\+\?]*/,
},
{
include: "#markupMath",
},
{
include: "#markupHeading",
},
{
name: "punctuation.definition.list.unnumbered.typst",
match: /^\s*-\s+/,
},
{
name: "punctuation.definition.list.numbered.typst",
match: /^\s*([0-9]+\.|\+)\s+/,
},
{
match: /^\s*(\/)\s+([^:]*)(:)/,
captures: {
"1": {
name: "punctuation.definition.list.description.typst",
},
"2": {
patterns: [
{
include: "#markup",
},
],
},
"3": {
name: "markup.list.term.typst",
},
},
},
{
include: "#markupLabel",
},
{
include: "#markupReference",
},
],
};
const enterExpression = (kind: string, seek: RegExp): textmate.Pattern => {
return {
/// name: 'markup.expr.typst'
begin: new RegExp("#" + seek.source),
end: /(?<=;)|(?<=[\)\]\}])(?![;\(\[])|(?=[\s\]\}\)]|$)|(;)/,
beginCaptures: {
"0": {
name: kind,
},
},
endCaptures: {
"1": {
name: "punctuation.terminator.statement.typst",
},
},
patterns: [
{
include: "#expression",
},
],
};
};
const markupEnterCode: textmate.Pattern = {
patterns: [
/// hash and follows a space
{
match: /(#)\s/,
captures: {
"1": {
name: "punctuation.definition.hash.typst",
},
},
},
/// hash and follows a empty
{
match: /(#)(;)/,
captures: {
"1": {
name: "punctuation.definition.hash.typst",
},
"2": {
name: "punctuation.terminator.statement.typst",
},
},
},
enterExpression(
"keyword.control.hash.typst",
/(?=(?:break|continue|and|or|not|return|as|in|include|import|let|else|if|for|while|context|set|show)\b(?!-))/
),
enterExpression(
"entity.name.type.primitive.hash.typst",
/(?=(?:auto|any|none|false|true|str|int|float|bool|length|content)\b(?!-))/
),
enterExpression(
"entity.name.function.hash.typst",
/(?=[\p{XID_Start}_][\p{XID_Continue}_\-]*\()/
),
enterExpression(
"variable.other.readwrite.hash.typst",
/(?=[\p{XID_Start}_])/
),
enterExpression("string.hash.hash.typst", /(?=\")/),
enterExpression("constant.numeric.hash.typst", /(?=\d)/),
enterExpression("keyword.control.hash.typst", new RegExp("")),
],
};
const code: textmate.Pattern = {
patterns: [
{
include: "#common",
},
{
include: "#comments",
},
{
name: "punctuation.separator.colon.typst",
match: /;/,
},
{
include: "#expression",
},
],
};
const FLOAT_OR_INT =
/(?<!\)|\]|\}\d)(^|(?<=\s)|\b)(?:(\d*)?\.?\d+([eE][+-]?\d+)?|\d+\.)/;
const floatUnit = (unit: RegExp, canDotSuff: boolean) =>
new RegExp(
FLOAT_OR_INT.source + (canDotSuff ? "" : "(?<!\\.)") + unit.source
);
const constants: textmate.Pattern = {
patterns: [
{
name: "constant.language.none.typst",
match: /(?<!\)|\]|\})\bnone\b(?!-)/,
},
{
name: "constant.language.auto.typst",
match: /(?<!\)|\]|\})\bauto\b(?!-)/,
},
{
name: "constant.language.boolean.typst",
match: /(?<!\)|\]|\})\b(true|false)\b(?!-)/,
},
{
name: "constant.numeric.length.typst",
match: floatUnit(/(mm|pt|cm|in|em)($|\b)/, false),
},
{
name: "constant.numeric.angle.typst",
match: floatUnit(/(rad|deg)($|\b)/, false),
},
{
name: "constant.numeric.percentage.typst",
match: floatUnit(/%/, true),
},
{
name: "constant.numeric.fr.typst",
match: floatUnit(/fr/, false),
},
{
name: "constant.numeric.integer.typst",
match: /(?<!\)|\]|\})(^|(?<=\s)|\b)\d+\b(?![\.eE])/,
},
{
name: "constant.numeric.float.typst",
match: floatUnit(/($|\b)/, true),
},
{
include: "#stringLiteral",
},
{
include: "#markupMath",
},
],
};
const expression = (): textmate.Grammar => {
const expression: textmate.Pattern = {
patterns: [
{ include: "#comments" },
{ include: "#arrowFunc" },
{ include: "#arrayOrDict" },
{ include: "#contentBlock" },
{
match: /\b(break|continue)\b(?!-)/,
name: "keyword.control.loop.typst",
},
{
match: /\b(and|or|not)\b(?!-)/,
name: "keyword.other.logical.typst",
},
{
match: /\b(return)\b(?!-)/,
name: "keyword.control.flow.typst",
},
{ include: "#markupLabel" },
{ include: "#blockRaw" },
{ include: "#inlineRaw" },
{ include: "#codeBlock" },
{ include: "#letStatement" },
{ include: "#showStatement" },
{ include: "#contextStatement" },
{ include: "#setStatement" },
{ include: "#forStatement" },
{ include: "#whileStatement" },
{ include: "#ifStatement" },
{ include: "#importStatement" },
{ include: "#includeStatement" },
{ include: "#strictFuncCallOrPropAccess" },
{ include: "#primitiveColors" },
{ include: "#primitiveFunctions" },
{ include: "#primitiveTypes" },
// todo: enable if only if for completely right grammar
{ include: "#identifier" },
{ include: "#constants" },
{
match: /(as)\b(?!-)/,
name: "keyword.control.typst",
},
{
match: /(in)\b(?!-)/,
name: "keyword.operator.range.typst",
},
{
match: /\./,
name: "keyword.operator.accessor.typst",
},
{
match:
/\+|\\|\/|(?<![[:alpha:]])(?<!\w)(?<!\d)-(?![[:alnum:]-][[:alpha:]_])/,
name: "keyword.operator.arithmetic.typst",
},
{
match: /==|!=|<=|<|>=|>/,
name: "keyword.operator.relational.typst",
},
{
begin: /(\+=|-=|\*=|\/=|=)/,
end: /(?=[\n;\)\]\}])/,
beginCaptures: {
"1": {
name: "keyword.operator.assignment.typst",
},
},
patterns: [
{
include: "#expression",
},
],
},
],
};
const arrayOrDict: textmate.Pattern = {
patterns: [
/// empty array ()
{
match: /(\()\s*(\))/,
captures: {
"1": {
name: "meta.brace.round.typst",
},
"2": {
name: "meta.brace.round.typst",
},
},
},
/// empty dictionary (:)
{
match: /(\()\s*(:)\s*(\))/,
captures: {
"1": {
name: "meta.brace.round.typst",
},
"2": {
name: "punctuation.separator.colon.typst",
},
"3": {
name: "meta.brace.round.typst",
},
},
},
/// parentheisized expressions: (...)
// todo: This is invocable
{
begin: /\(/,
end: /\)/,
beginCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
patterns: [
{
include: "#literalContent",
},
],
},
],
};
const literalContent: textmate.Pattern = {
patterns: [
{
name: "punctuation.separator.colon.typst",
match: /:/,
},
{
name: "punctuation.separator.comma.typst",
match: /,/,
},
{
include: "#expression",
},
],
};
return {
repository: {
expression,
arrayOrDict,
literalContent,
},
};
};
const blockComment: textmate.Pattern = {
name: "comment.block.typst",
begin: /\/\*/,
end: /\*\//,
beginCaptures: {
"0": {
name: "punctuation.definition.comment.typst",
},
},
patterns: [
{
include: "#blockComment",
},
],
};
const lineCommentInner = (strict: boolean): textmate.Pattern => {
return {
name: "comment.line.double-slash.typst",
begin: strict ? /(?<!:)\/\// : /\/\//,
end: /(?=$|\n)/,
beginCaptures: {
"0": {
name: "punctuation.definition.comment.typst",
},
},
};
};
const strictLineComment = lineCommentInner(true);
const lineComment = lineCommentInner(false);
const strictComments: textmate.Pattern = {
patterns: [{ include: "#blockComment" }, { include: "#strictLineComment" }],
};
const comments: textmate.Pattern = {
patterns: [{ include: "#blockComment" }, { include: "#lineComment" }],
};
const inlineRaw: textmate.Pattern = {
name: "markup.raw.inline.typst",
begin: /`/,
end: /`/,
beginCaptures: {
"0": {
name: "punctuation.definition.raw.inline.typst",
},
},
endCaptures: {
"0": {
name: "punctuation.definition.raw.inline.typst",
},
},
};
const blockRaw: textmate.Pattern = {
patterns: [
{
include: "#blockRawGeneral",
},
],
};
const blockRawGeneral: textmate.Pattern = {
name: "markup.raw.block.typst",
begin: new RegExp(/(`{3,})/.source + `(${IDENTIFIER_BARE.source}\\b)?`),
beginCaptures: {
"1": {
name: "punctuation.definition.raw.begin.typst",
},
"2": {
name: "fenced_code.block.language.typst",
},
},
end: /(\1)/,
endCaptures: {
"1": {
name: "punctuation.definition.raw.end.typst",
},
},
};
const markupAnnotate = (ch: string, style: string): textmate.Pattern => {
const MARKUP_BOUNDARY = /[\W_\p{Han}\p{Hangul}\p{Katakana}\p{Hiragana}]/;
const notationAtBound = `(^${ch}|${ch}$|((?<=${MARKUP_BOUNDARY.source})${ch})|(${ch}(?=${MARKUP_BOUNDARY.source})))`;
return {
name: `markup.${style}.typst`,
begin: new RegExp(notationAtBound),
end: new RegExp(notationAtBound + `\\n|(?=\\])`),
captures: {
"0": {
name: `punctuation.definition.${style}.typst`,
},
},
patterns: [
{
include: "#markup",
},
],
};
};
const markupBold = markupAnnotate("\\*", "bold");
const markupItalic = markupAnnotate("_", "italic");
const includeStatement: textmate.Pattern = {
name: "meta.expr.include.typst",
begin: /(\binclude\b(?!-))\s*/,
end: /(?=[\n;\}\]\)])/,
beginCaptures: {
"1": {
name: "keyword.control.import.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
// todo: sometimes eat a character
const importStatement = (): textmate.Grammar => {
const importStatement: textmate.Pattern = {
name: "meta.expr.import.typst",
begin: /(\bimport\b(?!-))\s*/,
end: /(?=[\n;\}\]\)])/,
beginCaptures: {
"1": {
name: "keyword.control.import.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#importPathClause",
},
{
match: /\:/,
name: "punctuation.separator.colon.typst",
},
{
match: /\*/,
name: "keyword.operator.wildcard.typst",
},
{
match: /\,/,
name: "punctuation.separator.comma.typst",
},
{
include: "#importAsClause",
},
{
include: "#expression",
},
],
};
/// import expression until as|:
const importPathClause: textmate.Pattern = {
begin: /(\bimport\b(?!-))\s*/,
// todo import as
end: /(?=\:|as)/,
beginCaptures: {
"1": {
name: "keyword.control.import.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
/// as expression
const importAsClause: textmate.Pattern = {
// todo: as...
begin: /(\bas\b)\s*/,
end: /(?=[\s;\}\]\)])/,
beginCaptures: {
"1": {
name: "keyword.control.import.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#identifier",
},
],
};
return {
repository: {
importStatement,
importPathClause,
importAsClause,
},
};
};
const letStatement = (): textmate.Grammar => {
const letStatement: textmate.Pattern = {
name: "meta.expr.let.typst",
begin: lookAhead(/(let\b(?!-))/),
end: /(?!\()(?=[\s;\}\]\)])/,
patterns: [
/// Matches any comments
{
include: "#comments",
},
/// Matches binding clause
{
include: "#letBindingClause",
},
/// Matches init assignment clause
{
include: "#letInitClause",
},
],
};
const letBindingClause: textmate.Pattern = {
// name: "meta.let.binding.typst",
begin: /(let\b(?!-))\s*/,
end: /(?=[=;\]}\n])/,
beginCaptures: {
"1": {
name: "storage.type.typst",
},
},
patterns: [
{
include: "#comments",
},
/// Matches a func call after the set clause
{
begin: /(\b[\p{XID_Start}_][\p{XID_Continue}_\-]*)(\()/,
end: /\)/,
beginCaptures: {
"1": {
name: "entity.name.function.typst",
patterns: [
{
include: "#primitiveFunctions",
},
],
},
"2": {
name: "meta.brace.round.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
patterns: [
{
include: "#funcParams",
},
],
},
{
begin: /\(/,
end: /\)/,
beginCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
patterns: [{ include: "#patternBindingItems" }],
},
{
include: "#identifier",
},
],
};
const letInitClause: textmate.Pattern = {
// name: "meta.let.init.typst",
begin: /=\s*/,
end: /(?<!\s*=)(?=[;\]})\n])/,
beginCaptures: {
"0": {
name: "keyword.operator.assignment.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
return {
repository: {
letStatement,
letBindingClause,
letInitClause,
},
};
};
// todo: #if [] == [] [] {}
/**
* Matches a (strict grammar) if in markup context.
*/
const ifStatement = (): textmate.Grammar => {
const ifStatement: textmate.Pattern = {
name: "meta.expr.if.typst",
begin: lookAhead(/(else\s+)?(if\b(?!-))/),
end: /(?<=\}|\])(?!\s*else\b(?!-))|(?=[;\}\]\)\n]|$)/,
patterns: [
/// Matches any comments
{
include: "#comments",
},
// todo
/// Matches if clause with a code block expression
/// Matches if clause
{
include: "#ifClause",
},
/// Matches else clause
{
include: "#elseClause",
},
/// Matches else content clause
{
include: "#elseContentClause",
},
/// Matches a code block after the if clause
{
include: "#codeBlock",
},
/// Matches a content block after the if clause
{
include: "#contentBlock",
},
],
};
const ifClause: textmate.Pattern = {
// name: "meta.if.clause.typst",
begin: /(?:(\belse)\s+)?(\bif)\s+/,
end: /(?<!(?:if|and|or|not|in|!=|==|<=|>=|<|>|\+|-|\*|\/|=|\+=|-=|\*=|\/=)\s*)(?=[\[\{\n])|(?=[;\n\]}]|$)/,
beginCaptures: {
"1": {
name: "keyword.control.conditional.typst",
},
"2": {
name: "keyword.control.conditional.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
const elseClause: textmate.Pattern = {
// name: "meta.else.clause.typst",
begin: /(\belse)\s*(\{)/,
end: /\}/,
beginCaptures: {
"1": {
name: "keyword.control.conditional.typst",
},
"2": {
name: "meta.brace.curly.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.curly.typst",
},
},
patterns: [
{
include: "#code",
},
],
};
const elseContentClause: textmate.Pattern = {
// name: "meta.else.clause.typst",
begin: /(\belse)\s*(\[)/,
end: /\]/,
beginCaptures: {
"1": {
name: "keyword.control.conditional.typst",
},
"2": {
name: "meta.brace.square.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.square.typst",
},
},
patterns: [
{
include: "#code",
},
],
};
return {
repository: {
ifStatement,
ifClause,
elseClause,
elseContentClause,
},
};
};
const forStatement = (): textmate.Grammar => {
// for v in expr { ... }
const forStatement: textmate.Pattern = {
name: "meta.expr.for.typst",
begin: lookAhead(/(for\b(?!-))\s*/),
end: /(?<=[\}\]])(?=\s*[\n\S;\}\]\)])(?!\s*[\{\[])|(?=[;\}\]\)\n]|$)/,
patterns: [
/// Matches any comments
{
include: "#comments",
},
/// Matches for clause
{
include: "#forClause",
},
/// Matches a code block after the for clause
{
include: "#codeBlock",
},
/// Matches a content block after the for clause
{
include: "#contentBlock",
},
],
};
const forClause: textmate.Pattern = {
// name: "meta.for.clause.bind.typst",
// todo: consider comment in for /* {} */ in .. {}
begin: new RegExp(
/(for\b)\s*/.source + `(${BRACE_FREE_EXPR}|${CODE_BLOCK})\\s*(in)\\s*`
),
end: /(?=[;{\[\}\]\)\n]|$)/,
beginCaptures: {
"1": {
name: "keyword.control.loop.typst",
},
"2": {
patterns: [
{
include: "#comments",
},
// todo: reuse pattern binding
{
begin: /\(/,
end: /\)/,
beginCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
patterns: [{ include: "#patternBindingItems" }],
},
{
include: "#identifier",
},
],
},
"3": {
name: "keyword.operator.range.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
return {
repository: {
forStatement,
forClause,
},
};
};
const whileStatement = (): textmate.Grammar => {
// for v in expr { ... }
const whileStatement: textmate.Pattern = {
name: "meta.expr.while.typst",
begin: lookAhead(/(while\b(?!-))/),
end: /(?<=\}|\])|(?=[;\}\]\)\n]|$)/,
patterns: [
/// Matches any comments
{
include: "#comments",
},
/// Matches while clause
{
include: "#whileClause",
},
/// Matches a code block after the while clause
{
include: "#codeBlock",
},
/// Matches a content block after the while clause
{
include: "#contentBlock",
},
],
};
const whileClause: textmate.Pattern = {
// name: "meta.while.clause.bind.typst",
begin: /(while\b)\s*/,
end: /(?<!(?:if|and|or|not|in|!=|==|<=|>=|<|>|\+|-|\*|\/|=|\+=|-=|\*=|\/=)\s+)(?=[\[\{])|(?=[;\}\]\)\n]|$)/,
beginCaptures: {
"1": {
name: "keyword.control.loop.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
return {
repository: {
whileStatement,
whileClause,
},
};
};
const contextStatement: textmate.Pattern = {
name: "meta.expr.context.typst",
begin: /(context\b(?!-))\s*/,
end: /(?=[\n;\}\]\)])/,
beginCaptures: {
"1": {
name: "keyword.control.other.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
const setStatement = (): textmate.Grammar => {
const setStatement: textmate.Pattern = {
name: "meta.expr.set.typst",
begin: lookAhead(new RegExp(/(set\b(?!-))\s*/.source + IDENTIFIER.source)),
end: /(?<=\))(?!\s*if\b)|(?=[\s;\{\[\}\]\)])/,
patterns: [
/// Matches any comments
{
include: "#comments",
},
/// Matches binding clause
{
include: "#setClause",
},
/// Matches condition after the set clause
{
include: "#setIfClause",
},
],
};
const setClause: textmate.Pattern = {
// name: "meta.set.clause.bind.typst",
begin: /(set\b)\s*/,
end: /(?=if)|(?=[\n;\]}])/,
beginCaptures: {
"1": {
name: "keyword.control.other.typst",
},
},
patterns: [
{
include: "#comments",
},
/// Matches a func call after the set clause
{
include: "#strictFuncCallOrPropAccess",
},
{
include: "#identifier",
},
],
};
const setIfClause: textmate.Pattern = {
// name: "meta.set.if.clause.cond.typst",
begin: /(if\b(?!-))\s*/,
end: /(?<=\S)(?<!and|or|not|in|!=|==|<=|>=|<|>|\+|-|\*|\/|=|\+=|-=|\*=|\/=)(?!\s*(?:and|or|not|in|!=|==|<=|>=|<|>|\+|-|\*|\/|=|\+=|-=|\*=|\/=|\.))|(?=[\n;\}\]\)])/,
beginCaptures: {
"1": {
name: "keyword.control.conditional.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
return {
repository: {
setStatement,
setClause,
setIfClause,
},
};
};
const showStatement = (): textmate.Grammar => {
const showStatement: textmate.Pattern = {
name: "meta.expr.show.typst",
begin: lookAhead(/(show\b(?!-))/),
end: /(?=[\s;\{\[\}\]\)])/,
patterns: [
/// Matches any comments
{
include: "#comments",
},
/// Matches show any clause
{
include: "#showAnyClause",
},
/// Matches select clause
{
include: "#showSelectClause",
},
/// Matches substitution clause
{
include: "#showSubstClause",
},
],
};
const showAnyClause: textmate.Pattern = {
// name: "meta.show.clause.select.typst",
match: /(show\b)\s*(?=\:)/,
captures: {
"1": {
name: "keyword.control.other.typst",
},
},
};
const showSelectClause: textmate.Pattern = {
// name: "meta.show.clause.select.typst",
begin: /(show\b)\s*/,
end: /(?=[:;\}\]\n])/,
beginCaptures: {
"1": {
name: "keyword.control.other.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#markupLabel",
},
/// Matches a func call after the set clause
{
include: "#expression",
},
],
};
const showSubstClause: textmate.Pattern = {
// name: "meta.show.clause.subst.typst",
begin: /(\:)\s*/,
end: /(?<!:)(?<=\S)(?!\S)|(?=[\n;\}\]\)])/,
beginCaptures: {
"1": {
name: "punctuation.separator.colon.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
};
return {
repository: {
showStatement,
showAnyClause,
showSelectClause,
showSubstClause,
},
};
};
// todo: { f }(..args)
// todo: ( f )(..args)
const callArgs: textmate.Pattern = {
// name: "meta.call.args.typst",
begin: /\(/,
end: /\)/,
beginCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
patterns: [
{
match: /\.\./,
name: "keyword.operator.spread.typst",
},
{
match: /:/,
name: "punctuation.separator.colon.typst",
},
{
match: /,/,
name: "punctuation.separator.comma.typst",
},
{
include: "#expression",
},
],
};
const funcRestParam: textmate.Pattern = {
match: /(\.\.)(\b[\p{XID_Start}_][\p{XID_Continue}_\-]*)?/,
// debugging
// - name: meta.parameter.binding.typst
captures: {
"1": {
name: "keyword.operator.spread.typst",
},
"2": {
name: "variable.other.readwrite.typst",
},
},
};
const patternBindingItems: textmate.Pattern = {
patterns: [
{ include: "#comments" },
/// rest binding
{
include: "#funcRestParam",
},
/// recursive binding
{
begin: /\(/,
end: /\)/,
beginCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
patterns: [
{
include: "#patternBindingItems",
},
],
},
/// parameter binding
{
include: "#primitiveTypes",
},
{
include: "#identifier",
},
{
match: /:/,
name: "punctuation.separator.colon.typst",
},
{
match: /,/,
name: "punctuation.separator.comma.typst",
},
],
};
const funcParams: textmate.Pattern = {
patterns: [
{
include: "#patternBindingItems",
},
{
match: /:/,
name: "punctuation.separator.colon.typst",
},
{
match: /,/,
name: "punctuation.separator.comma.typst",
},
],
};
const funcCallOrPropAccess = (strict: boolean): textmate.Pattern => {
return {
name: "meta.expr.call.typst",
begin: lookAhead(
strict
? new RegExp(/(\.)?/.source + IDENTIFIER.source + /(?=\(|\[)/.source)
: new RegExp(
/(\.\s*)?/.source + IDENTIFIER.source + /\s*(?=\(|\[)/.source
)
),
end: /(?:(?<=\)|\])(?:(?![\[\(\.])|$))|(?=[\n;\}\]\)]|$)/,
patterns: [
// todo: comments?
// {
// include: "#comments",
// },
{
match: /\./,
name: "keyword.operator.accessor.typst",
},
{
match: new RegExp(
IDENTIFIER.source +
(strict ? /(?=\(|\[)/.source : /\s*(?=\(|\[)/.source)
),
name: "entity.name.function.typst",
patterns: [
{
include: "#primitiveFunctions",
},
],
},
{
include: "#identifier",
},
// empty args
{
// name: "meta.call.args.typst",
match: /(\()\s*(\))/,
captures: {
"1": {
name: "meta.brace.round.typst",
},
"2": {
name: "meta.brace.round.typst",
},
},
},
{
include: "#callArgs",
},
{
include: "#contentBlock",
},
],
};
};
// todo: #x => y should be parsed as |#x|=>|y
// https://github.com/microsoft/vscode-textmate/blob/main/test-cases/themes/syntaxes/TypeScript.tmLanguage.json
const arrowFunc: textmate.Pattern = {
name: "meta.expr.arrow-function.typst",
patterns: [
{
match: new RegExp(`(${IDENTIFIER.source})` + /\s*(?==>)/.source),
captures: {
"1": {
name: "variable.parameter.typst",
},
},
},
{
begin: braceMatch(lookAhead(new RegExp(PAREN_BLOCK + /\s*=>/.source))),
end: /(?==>)/,
patterns: [
{
include: "#comments",
},
{
begin: /\(/,
end: /\)/,
beginCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
endCaptures: {
"0": {
name: "meta.brace.round.typst",
},
},
patterns: [
{
include: "#funcParams",
},
],
},
],
},
{
begin: /=>/,
end: /(?<=\}|\])|(?:(?!\{|\[)(?=[\n\S;]))/,
beginCaptures: {
"0": {
name: "storage.type.function.arrow.typst",
},
},
patterns: [
{
include: "#comments",
},
{
include: "#expression",
},
],
},
],
};
export const typst: textmate.Grammar = {
repository: {
common,
markup,
markupEnterCode,
code,
constants,
primitiveColors,
primitiveFunctions,
primitiveTypes,
identifier,
markupLabel,
markupReference,
markupEscape,
stringLiteral,
comments,
strictComments,
blockComment,
lineComment,
strictLineComment,
inlineRaw,
blockRaw,
blockRawGeneral,
markupBold,
markupItalic,
markupMath,
markupHeading,
...expression().repository,
includeStatement,
...importStatement().repository,
...letStatement().repository,
...ifStatement().repository,
...forStatement().repository,
...whileStatement().repository,
contextStatement,
...setStatement().repository,
...showStatement().repository,
strictFuncCallOrPropAccess: funcCallOrPropAccess(true),
// funcCallOrPropAccess: funcCallOrPropAccess(false),
callArgs,
funcRestParam,
funcParams,
patternBindingItems,
codeBlock,
contentBlock,
arrowFunc,
},
};
function generate() {
const fs = require("fs");
const path = require("path");
const typstPath = path.join(__dirname, "../typst.tmLanguage");
const compiled = textmate.compile(typst);
const repository = JSON.parse(compiled).repository;
// dump to file
fs.writeFileSync(
path.join(__dirname, "../typst.tmLanguage.json"),
JSON.stringify({
$schema:
"https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
scopeName: "source.typst",
name: "typst",
patterns: [
{
include: "#markup",
},
],
repository,
})
);
// dump to file
fs.writeFileSync(
path.join(__dirname, "../typst-code.tmLanguage.json"),
JSON.stringify({
$schema:
"https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
scopeName: "source.typst-code",
name: "typst-code",
patterns: [
{
include: "#code",
},
],
repository,
})
);
}
// console.log(typst!.repository!.forStatement);
generate();
// todo: this is fixed in v0.11.0
// #code(```typ
// #let a = 1; #let b = 2;
// #(a, b) = (4, 5)
// #a, #b
// ```)