From c4ff071e26f99f964431b873889b051d6d88c98c Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 13 Aug 2026 03:53:35 +0000 Subject: [PATCH 1/9] fix: procedure syntax highlighting --- packages/language/src/zmodel-semantic.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/language/src/zmodel-semantic.ts b/packages/language/src/zmodel-semantic.ts index 7910c78af..b0b10d266 100644 --- a/packages/language/src/zmodel-semantic.ts +++ b/packages/language/src/zmodel-semantic.ts @@ -21,6 +21,8 @@ import { isPluginField, isReferenceExpr, isTypeDef, + isProcedure, + isFunctionParamType, type AstNode, } from './ast'; @@ -62,7 +64,7 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'name', type: SemanticTokenTypes.variable, }); - } else if (isDataFieldType(node)) { + } else if (isDataFieldType(node) || isFunctionParamType(node)) { if (node.type) { acceptor({ node, @@ -88,7 +90,7 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'function', type: SemanticTokenTypes.function, }); - } else if (isFunctionDecl(node) || isAttribute(node)) { + } else if (isFunctionDecl(node) || isAttribute(node) || isProcedure(node)) { acceptor({ node, property: 'name', From d47fa2b70e4b9a6b913aa7360d4b5578e5f912bd Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 13 Aug 2026 22:42:04 +0000 Subject: [PATCH 2/9] fix: attribute param highlighting --- packages/language/src/zmodel-semantic.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/language/src/zmodel-semantic.ts b/packages/language/src/zmodel-semantic.ts index b0b10d266..0d70b0edd 100644 --- a/packages/language/src/zmodel-semantic.ts +++ b/packages/language/src/zmodel-semantic.ts @@ -23,6 +23,7 @@ import { isTypeDef, isProcedure, isFunctionParamType, + isAttributeParamType, type AstNode, } from './ast'; @@ -64,7 +65,7 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'name', type: SemanticTokenTypes.variable, }); - } else if (isDataFieldType(node) || isFunctionParamType(node)) { + } else if (isDataFieldType(node) || isFunctionParamType(node) || isAttributeParamType(node)) { if (node.type) { acceptor({ node, From aea4ad52aadab6494f3fe6c654fbec960599acec Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 14 Aug 2026 02:33:08 +0000 Subject: [PATCH 3/9] fix: binary, unary, boolean, number, null, collection predicate, and params --- packages/language/src/zmodel-semantic.ts | 91 ++++++++++++++++++++---- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/packages/language/src/zmodel-semantic.ts b/packages/language/src/zmodel-semantic.ts index 0d70b0edd..5f6254f36 100644 --- a/packages/language/src/zmodel-semantic.ts +++ b/packages/language/src/zmodel-semantic.ts @@ -24,6 +24,15 @@ import { isProcedure, isFunctionParamType, isAttributeParamType, + isUnaryExpr, + isBinaryExpr, + isBooleanLiteral, + isNumberLiteral, + isNullExpr, + isCollectionPredicateBinding, + isFunctionParam, + isAttributeParam, + isProcedureParam, type AstNode, } from './ast'; @@ -47,23 +56,17 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'baseModel', type: SemanticTokenTypes.type, }); - } else if (isDataSource(node) || isGeneratorDecl(node) || isPlugin(node) || isEnum(node) || isTypeDef(node)) { + } else if (isDataSource(node) || isGeneratorDecl(node) || isPlugin(node) || isTypeDef(node)) { acceptor({ node, property: 'name', type: SemanticTokenTypes.type, }); - } else if ( - isDataField(node) || - isConfigField(node) || - isAttributeArg(node) || - isPluginField(node) || - isEnumField(node) - ) { + } else if (isDataField(node) || isConfigField(node) || isPluginField(node)) { acceptor({ node, property: 'name', - type: SemanticTokenTypes.variable, + type: SemanticTokenTypes.property, }); } else if (isDataFieldType(node) || isFunctionParamType(node) || isAttributeParamType(node)) { if (node.type) { @@ -83,7 +86,21 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { acceptor({ node, property: 'decl', - type: SemanticTokenTypes.function, + type: SemanticTokenTypes.decorator, + }); + + if (node.decl.$refText === '@regex' && node.args[0]) { + acceptor({ + node: node.args[0], + property: 'value', + type: SemanticTokenTypes.regexp, + }); + } + } else if (isAttribute(node)) { + acceptor({ + node, + property: 'name', + type: SemanticTokenTypes.decorator, }); } else if (isInvocationExpr(node)) { acceptor({ @@ -91,17 +108,25 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'function', type: SemanticTokenTypes.function, }); - } else if (isFunctionDecl(node) || isAttribute(node) || isProcedure(node)) { + } else if (isFunctionDecl(node) || isProcedure(node)) { acceptor({ node, property: 'name', type: SemanticTokenTypes.function, }); + + if ('mutation' in node && node.mutation) { + acceptor({ + node, + property: 'mutation', + type: SemanticTokenTypes.modifier, + }); + } } else if (isReferenceExpr(node)) { acceptor({ node, property: 'target', - type: SemanticTokenTypes.variable, + type: SemanticTokenTypes.property, }); } else if (isMemberAccessExpr(node)) { acceptor({ @@ -109,6 +134,48 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'member', type: SemanticTokenTypes.property, }); + } else if (isNumberLiteral(node)) { + acceptor({ + node, + property: 'value', + type: SemanticTokenTypes.number, + }); + } else if (isBooleanLiteral(node) || isNullExpr(node)) { + acceptor({ + node, + property: 'value', + type: SemanticTokenTypes.keyword, + }); + } else if (isUnaryExpr(node) || isBinaryExpr(node)) { + acceptor({ + node, + property: 'operator', + type: SemanticTokenTypes.operator, + }); + } else if (isEnumField(node)) { + acceptor({ + node, + property: 'name', + type: SemanticTokenTypes.enumMember, + }); + } else if (isEnum(node)) { + acceptor({ + node, + property: 'name', + type: SemanticTokenTypes.enum, + }); + } else if (isFunctionParam(node) || isAttributeArg(node) || isAttributeParam(node) || isProcedureParam(node)) { + acceptor({ + node, + property: 'name', + type: SemanticTokenTypes.parameter, + }); + } else if (isCollectionPredicateBinding(node)) { + acceptor({ + node, + property: 'name', + type: SemanticTokenTypes.variable, + }); } } } From 1ba8e9559fff6f2ef739fb924b538cf7ad76d054 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 14 Aug 2026 04:17:46 +0000 Subject: [PATCH 4/9] fix: reference exprs --- packages/language/src/zmodel-semantic.ts | 30 ++++++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/language/src/zmodel-semantic.ts b/packages/language/src/zmodel-semantic.ts index 5f6254f36..5e3b8c188 100644 --- a/packages/language/src/zmodel-semantic.ts +++ b/packages/language/src/zmodel-semantic.ts @@ -123,11 +123,31 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { }); } } else if (isReferenceExpr(node)) { - acceptor({ - node, - property: 'target', - type: SemanticTokenTypes.property, - }); + if (isEnumField(node.target.ref)) { + acceptor({ + node, + property: 'target', + type: SemanticTokenTypes.enumMember, + }); + } else if (isFunctionParam(node.target.ref)) { + acceptor({ + node, + property: 'target', + type: SemanticTokenTypes.parameter, + }); + } else if (isCollectionPredicateBinding(node.target.ref)) { + acceptor({ + node, + property: 'target', + type: SemanticTokenTypes.variable, + }); + } else { + acceptor({ + node, + property: 'target', + type: SemanticTokenTypes.property, + }); + } } else if (isMemberAccessExpr(node)) { acceptor({ node, From 521f3f61acf55616223c2138c155a7f9dc478a51 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 14 Aug 2026 04:18:58 +0000 Subject: [PATCH 5/9] fix: `in` operator --- packages/language/src/zmodel-semantic.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/language/src/zmodel-semantic.ts b/packages/language/src/zmodel-semantic.ts index 5e3b8c188..eb90c0cf3 100644 --- a/packages/language/src/zmodel-semantic.ts +++ b/packages/language/src/zmodel-semantic.ts @@ -167,11 +167,19 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { type: SemanticTokenTypes.keyword, }); } else if (isUnaryExpr(node) || isBinaryExpr(node)) { - acceptor({ - node, - property: 'operator', - type: SemanticTokenTypes.operator, - }); + if (node.operator === 'in') { + acceptor({ + node, + property: 'operator', + type: SemanticTokenTypes.keyword, + }); + } else { + acceptor({ + node, + property: 'operator', + type: SemanticTokenTypes.operator, + }); + } } else if (isEnumField(node)) { acceptor({ node, From d55b1cb6e7ece6159c50d9484525d9ac63c5a8e1 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 14 Aug 2026 04:29:29 +0000 Subject: [PATCH 6/9] fix: data field params --- packages/language/src/zmodel-semantic.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/language/src/zmodel-semantic.ts b/packages/language/src/zmodel-semantic.ts index eb90c0cf3..a2d84386f 100644 --- a/packages/language/src/zmodel-semantic.ts +++ b/packages/language/src/zmodel-semantic.ts @@ -34,6 +34,7 @@ import { isAttributeParam, isProcedureParam, type AstNode, + isDataFieldParam, } from './ast'; export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { @@ -192,7 +193,13 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'name', type: SemanticTokenTypes.enum, }); - } else if (isFunctionParam(node) || isAttributeArg(node) || isAttributeParam(node) || isProcedureParam(node)) { + } else if ( + isFunctionParam(node) || + isAttributeArg(node) || + isAttributeParam(node) || + isProcedureParam(node) || + isDataFieldParam(node) + ) { acceptor({ node, property: 'name', From 3380aae746068c48a1339a9929e9d7c3dfada951 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 14 Aug 2026 23:24:22 +0000 Subject: [PATCH 7/9] fix: highlighting for constants, `in`, and `this` --- packages/language/package.json | 4 +- .../language/scripts/patch-generated-tm.ts | 37 +++++++++++++++++++ packages/language/src/zmodel-semantic.ts | 16 +------- 3 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 packages/language/scripts/patch-generated-tm.ts diff --git a/packages/language/package.json b/packages/language/package.json index e515552dd..05f23687b 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -23,8 +23,8 @@ "watch": "tsdown --watch", "lint": "eslint src --ext ts", "test": "vitest run", - "langium:generate": "langium generate", - "langium:generate:production": "langium generate --mode=production", + "langium:generate": "langium generate && tsx scripts/patch-generated-tm.ts", + "langium:generate:production": "langium generate --mode=production && tsx scripts/patch-generated-tm.ts", "pack": "pnpm pack" }, "exports": { diff --git a/packages/language/scripts/patch-generated-tm.ts b/packages/language/scripts/patch-generated-tm.ts new file mode 100644 index 000000000..fa2c56caa --- /dev/null +++ b/packages/language/scripts/patch-generated-tm.ts @@ -0,0 +1,37 @@ +import tm from '../syntaxes/zmodel.tmLanguage.json'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; + +const tmPath = join(import.meta.dirname, '../syntaxes/zmodel.tmLanguage.json'); + +const control = tm.patterns.find((pattern) => pattern.name === 'keyword.control.zmodel-v3'); + +if (!control?.match) { + throw new Error('Could not find control pattern/match'); +} + +const keywordsToRemove = new Set(['this', 'null', 'true', 'false', 'in']); + +const pattern = /\((.+)\)/g; + +const [, keywordsMatch] = pattern.exec(control.match) as RegExpExecArray; +const keywordsArray = keywordsMatch.split('|').filter((keyword) => !keywordsToRemove.has(keyword)); + +control.match = control.match.replace(keywordsMatch, keywordsArray.join('|')); + +tm.patterns.push({ + name: 'constant.language', + match: '\\b(true|false|null)\\b', +}); + +tm.patterns.push({ + name: 'variable.language.this', + match: '\\b(this)\\b', +}); + +tm.patterns.push({ + name: 'keyword.operator.expression.in', + match: '\\b(in)\\b', +}); + +writeFileSync(tmPath, JSON.stringify(tm, null, 2)); diff --git a/packages/language/src/zmodel-semantic.ts b/packages/language/src/zmodel-semantic.ts index a2d84386f..6baadcc93 100644 --- a/packages/language/src/zmodel-semantic.ts +++ b/packages/language/src/zmodel-semantic.ts @@ -26,9 +26,7 @@ import { isAttributeParamType, isUnaryExpr, isBinaryExpr, - isBooleanLiteral, isNumberLiteral, - isNullExpr, isCollectionPredicateBinding, isFunctionParam, isAttributeParam, @@ -161,20 +159,8 @@ export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { property: 'value', type: SemanticTokenTypes.number, }); - } else if (isBooleanLiteral(node) || isNullExpr(node)) { - acceptor({ - node, - property: 'value', - type: SemanticTokenTypes.keyword, - }); } else if (isUnaryExpr(node) || isBinaryExpr(node)) { - if (node.operator === 'in') { - acceptor({ - node, - property: 'operator', - type: SemanticTokenTypes.keyword, - }); - } else { + if (node.operator !== 'in') { acceptor({ node, property: 'operator', From f14d631050536ed53e6da3953363451bbda0209a Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 15 Aug 2026 01:13:18 +0000 Subject: [PATCH 8/9] chore: log after patching --- packages/language/scripts/patch-generated-tm.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/language/scripts/patch-generated-tm.ts b/packages/language/scripts/patch-generated-tm.ts index fa2c56caa..92ed83785 100644 --- a/packages/language/scripts/patch-generated-tm.ts +++ b/packages/language/scripts/patch-generated-tm.ts @@ -3,7 +3,6 @@ import { writeFileSync } from 'fs'; import { join } from 'path'; const tmPath = join(import.meta.dirname, '../syntaxes/zmodel.tmLanguage.json'); - const control = tm.patterns.find((pattern) => pattern.name === 'keyword.control.zmodel-v3'); if (!control?.match) { @@ -11,10 +10,8 @@ if (!control?.match) { } const keywordsToRemove = new Set(['this', 'null', 'true', 'false', 'in']); - -const pattern = /\((.+)\)/g; - -const [, keywordsMatch] = pattern.exec(control.match) as RegExpExecArray; +const matchPattern = /\((.+)\)/g; +const [, keywordsMatch] = matchPattern.exec(control.match) as RegExpExecArray; const keywordsArray = keywordsMatch.split('|').filter((keyword) => !keywordsToRemove.has(keyword)); control.match = control.match.replace(keywordsMatch, keywordsArray.join('|')); @@ -35,3 +32,4 @@ tm.patterns.push({ }); writeFileSync(tmPath, JSON.stringify(tm, null, 2)); +console.log('Patched syntaxes/zmodel.tmLanguage.json'); From 03d0af8a7626f55621fcbdab6586b353f5ac98cc Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 15 Aug 2026 01:14:40 +0000 Subject: [PATCH 9/9] chore: add comment description --- packages/language/scripts/patch-generated-tm.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/language/scripts/patch-generated-tm.ts b/packages/language/scripts/patch-generated-tm.ts index 92ed83785..25e9a33a1 100644 --- a/packages/language/scripts/patch-generated-tm.ts +++ b/packages/language/scripts/patch-generated-tm.ts @@ -1,3 +1,8 @@ +/** + * Post-processes syntaxes/zmodel.tmLanguage.json after `langium generate` to correct + * highlighting for some keywords. + */ + import tm from '../syntaxes/zmodel.tmLanguage.json'; import { writeFileSync } from 'fs'; import { join } from 'path';