diff --git a/README.md b/README.md index 8d2d3879d..e3b910b38 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ + + + diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 1f4b25b84..7463fb9da 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -14,12 +14,6 @@ export function isAbstractDeclaration(item: unknown): item is AbstractDeclaratio return reflection.isInstance(item, AbstractDeclaration); } -export type AttributeName = DataModelAttributeName | DataModelFieldAttributeName | InternalAttributeName; - -export function isAttributeName(item: unknown): item is AttributeName { - return isDataModelAttributeName(item) || isDataModelFieldAttributeName(item) || isInternalAttributeName(item); -} - export type Boolean = boolean; export function isBoolean(item: unknown): item is Boolean { @@ -40,18 +34,6 @@ export function isConfigExpr(item: unknown): item is ConfigExpr { return reflection.isInstance(item, ConfigExpr); } -export type DataModelAttributeName = string; - -export function isDataModelAttributeName(item: unknown): item is DataModelAttributeName { - return typeof item === 'string'; -} - -export type DataModelFieldAttributeName = string; - -export function isDataModelFieldAttributeName(item: unknown): item is DataModelFieldAttributeName { - return typeof item === 'string'; -} - export type Expression = ArrayExpr | BinaryExpr | InvocationExpr | LiteralExpr | MemberAccessExpr | NullExpr | ObjectExpr | ReferenceExpr | ThisExpr | UnaryExpr; export const Expression = 'Expression'; @@ -66,12 +48,6 @@ export function isExpressionType(item: unknown): item is ExpressionType { return item === 'String' || item === 'Int' || item === 'Float' || item === 'Boolean' || item === 'DateTime' || item === 'Null' || item === 'Object' || item === 'Any' || item === 'Unsupported'; } -export type InternalAttributeName = string; - -export function isInternalAttributeName(item: unknown): item is InternalAttributeName { - return typeof item === 'string'; -} - export type LiteralExpr = BooleanLiteral | NumberLiteral | StringLiteral; export const LiteralExpr = 'LiteralExpr'; @@ -80,12 +56,6 @@ export function isLiteralExpr(item: unknown): item is LiteralExpr { return reflection.isInstance(item, LiteralExpr); } -export type QualifiedName = string; - -export function isQualifiedName(item: unknown): item is QualifiedName { - return typeof item === 'string'; -} - export type ReferenceTarget = DataModelField | EnumField | FunctionParam; export const ReferenceTarget = 'ReferenceTarget'; @@ -137,7 +107,8 @@ export interface Attribute extends AstNode { readonly $container: Model; readonly $type: 'Attribute'; attributes: Array - name: AttributeName + comments: Array + name: string params: Array } @@ -163,6 +134,8 @@ export function isAttributeArg(item: unknown): item is AttributeArg { export interface AttributeParam extends AstNode { readonly $container: Attribute; readonly $type: 'AttributeParam'; + attributes: Array + comments: Array default: boolean name: RegularID type: AttributeParamType @@ -454,7 +427,7 @@ export function isGeneratorDecl(item: unknown): item is GeneratorDecl { } export interface InternalAttribute extends AstNode { - readonly $container: Attribute | FunctionDecl; + readonly $container: Attribute | AttributeParam | FunctionDecl; readonly $type: 'InternalAttribute'; args: Array decl: Reference @@ -519,7 +492,7 @@ export function isModelImport(item: unknown): item is ModelImport { export interface NullExpr extends AstNode { readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; readonly $type: 'NullExpr'; - value: string + value: 'null' } export const NullExpr = 'NullExpr'; @@ -619,7 +592,7 @@ export function isStringLiteral(item: unknown): item is StringLiteral { export interface ThisExpr extends AstNode { readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | UnaryExpr | UnsupportedFieldType; readonly $type: 'ThisExpr'; - value: string + value: 'this' } export const ThisExpr = 'ThisExpr'; @@ -801,6 +774,7 @@ export class ZModelAstReflection extends AbstractAstReflection { name: 'Attribute', mandatory: [ { name: 'attributes', type: 'array' }, + { name: 'comments', type: 'array' }, { name: 'params', type: 'array' } ] }; @@ -809,6 +783,8 @@ export class ZModelAstReflection extends AbstractAstReflection { return { name: 'AttributeParam', mandatory: [ + { name: 'attributes', type: 'array' }, + { name: 'comments', type: 'array' }, { name: 'default', type: 'boolean' } ] }; diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 9ae762b6a..5dbe02014 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -69,7 +69,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@63" }, "arguments": [] } @@ -139,7 +139,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@47" }, "arguments": [] } @@ -161,7 +161,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -177,7 +177,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -221,7 +221,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -237,7 +237,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -281,7 +281,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -293,7 +293,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -332,7 +332,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -348,7 +348,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -392,7 +392,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -404,7 +404,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -480,7 +480,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@68" + "$ref": "#/rules@64" }, "arguments": [] } @@ -502,7 +502,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@63" }, "arguments": [] } @@ -524,7 +524,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@57" }, "arguments": [] } @@ -648,7 +648,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@62" }, "arguments": [] } @@ -746,7 +746,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@62" }, "arguments": [] } @@ -907,11 +907,8 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "feature": "value", "operator": "=", "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@65" - }, - "arguments": [] + "$type": "Keyword", + "value": "this" } }, "definesHiddenTokens": false, @@ -929,11 +926,8 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "feature": "value", "operator": "=", "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@64" - }, - "arguments": [] + "$type": "Keyword", + "value": "null" } }, "definesHiddenTokens": false, @@ -961,7 +955,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] }, @@ -1177,14 +1171,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@63" }, "arguments": [] } @@ -1883,7 +1877,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -1929,7 +1923,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -1962,7 +1956,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2032,7 +2026,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2067,7 +2061,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2101,7 +2095,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -2114,7 +2108,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2138,7 +2132,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -2169,7 +2163,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@61" + "$ref": "#/rules@56" }, "arguments": [] } @@ -2198,7 +2192,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] }, @@ -2297,7 +2291,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -2314,7 +2308,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2345,7 +2339,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2379,7 +2373,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [] }, @@ -2392,7 +2386,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2404,7 +2398,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -2428,7 +2422,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -2444,7 +2438,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2541,7 +2535,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@52" }, "arguments": [] }, @@ -2565,7 +2559,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -2577,7 +2571,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2633,7 +2627,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@55" }, "arguments": [] } @@ -2650,7 +2644,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] }, @@ -2687,58 +2681,6 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, - { - "$type": "ParserRule", - "name": "QualifiedName", - "dataType": "string", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@66" - }, - "arguments": [] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "." - }, - { - "$type": "Alternatives", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@66" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@61" - }, - "arguments": [] - } - ] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, { "$type": "ParserRule", "name": "RegularID", @@ -2749,7 +2691,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@62" }, "arguments": [] }, @@ -2802,124 +2744,6 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, - { - "$type": "ParserRule", - "name": "InternalAttributeName", - "dataType": "string", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "@@@" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@46" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DataModelAttributeName", - "dataType": "string", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "@@" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@46" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DataModelFieldAttributeName", - "dataType": "string", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "@" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@46" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "AttributeName", - "dataType": "string", - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@49" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@50" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@48" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, { "$type": "ParserRule", "name": "Attribute", @@ -2927,11 +2751,16 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "Group", "elements": [ { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@69" + "$type": "Assignment", + "feature": "comments", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@65" + }, + "arguments": [] }, - "arguments": [], "cardinality": "*" }, { @@ -2943,11 +2772,30 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "feature": "name", "operator": "=", "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@51" - }, - "arguments": [] + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@59" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@60" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@61" + }, + "arguments": [] + } + ] } }, { @@ -2964,7 +2812,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@48" }, "arguments": [] } @@ -2983,7 +2831,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@48" }, "arguments": [] } @@ -3005,7 +2853,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@52" }, "arguments": [] }, @@ -3027,11 +2875,16 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "Group", "elements": [ { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@69" + "$type": "Assignment", + "feature": "comments", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@65" + }, + "arguments": [] }, - "arguments": [], "cardinality": "*" }, { @@ -3051,7 +2904,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -3067,10 +2920,23 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@49" }, "arguments": [] } + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@52" + }, + "arguments": [] + }, + "cardinality": "*" } ] }, @@ -3100,7 +2966,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@55" }, "arguments": [] }, @@ -3131,7 +2997,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] }, @@ -3191,12 +3057,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@52" + "$ref": "#/rules@47" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@61" }, "arguments": [] }, @@ -3213,7 +3079,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@53" }, "arguments": [], "cardinality": "?" @@ -3243,7 +3109,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@65" }, "arguments": [], "cardinality": "*" @@ -3255,12 +3121,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@52" + "$ref": "#/rules@47" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@60" }, "arguments": [] }, @@ -3277,7 +3143,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@53" }, "arguments": [], "cardinality": "?" @@ -3311,12 +3177,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@52" + "$ref": "#/rules@47" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -3333,7 +3199,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@53" }, "arguments": [], "cardinality": "?" @@ -3368,7 +3234,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@54" }, "arguments": [] } @@ -3387,7 +3253,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@54" }, "arguments": [] } @@ -3419,7 +3285,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@46" }, "arguments": [] } @@ -3592,26 +3458,30 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel }, { "$type": "TerminalRule", - "name": "NULL", + "name": "INTERNAL_ATTRIBUTE_NAME", "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "null" - } + "$type": "RegexToken", + "regex": "@@@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*" }, "fragment": false, "hidden": false }, { "$type": "TerminalRule", - "name": "THIS", + "name": "MODEL_ATTRIBUTE_NAME", "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "this" - } + "$type": "RegexToken", + "regex": "@@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "FIELD_ATTRIBUTE_NAME", + "definition": { + "$type": "RegexToken", + "regex": "@([_a-zA-Z][\\\\w_]*\\\\.)*[_a-zA-Z][\\\\w_]*" }, "fragment": false, "hidden": false diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 6760bb87d..da445c792 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -68,10 +68,10 @@ ConfigExpr: type ReferenceTarget = FunctionParam | DataModelField | EnumField; ThisExpr: - value=THIS; + value='this'; NullExpr: - value=NULL; + value='null'; ReferenceExpr: target=[ReferenceTarget:RegularID] ('(' ReferenceArgList ')')?; @@ -221,36 +221,17 @@ FunctionParam: FunctionParamType: (type=ExpressionType | reference=[TypeDeclaration:RegularID]) (array?='[' ']')?; -QualifiedName returns string: - // TODO: is this the right way to deal with token precedence? - ID ('.' (ID|BuiltinType))*; - // https://github.com/langium/langium/discussions/1012 RegularID returns string: // include keywords that we'd like to work as ID in most places ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'sort' | 'view' | 'import'; -// internal attribute -InternalAttributeName returns string: - '@@@' QualifiedName; - -// model-level attribute -DataModelAttributeName returns string: - '@@' QualifiedName; - -// field-level attribute -DataModelFieldAttributeName returns string: - '@' QualifiedName; - -AttributeName returns string: - DataModelAttributeName | DataModelFieldAttributeName | InternalAttributeName; - // attribute Attribute: - TRIPLE_SLASH_COMMENT* 'attribute' name=AttributeName '(' (params+=AttributeParam (',' params+=AttributeParam)*)? ')' (attributes+=InternalAttribute)*; + (comments+=TRIPLE_SLASH_COMMENT)* 'attribute' name=(INTERNAL_ATTRIBUTE_NAME|MODEL_ATTRIBUTE_NAME|FIELD_ATTRIBUTE_NAME) '(' (params+=AttributeParam (',' params+=AttributeParam)*)? ')' (attributes+=InternalAttribute)*; AttributeParam: - TRIPLE_SLASH_COMMENT* (default?='_')? name=RegularID ':' type=AttributeParamType; + (comments+=TRIPLE_SLASH_COMMENT)* (default?='_')? name=RegularID ':' type=AttributeParamType (attributes+=InternalAttribute)*; // FieldReference refers to fields declared in the current model // TransitiveFieldReference refers to fields declared in the model type of the current field @@ -259,13 +240,13 @@ AttributeParamType: type TypeDeclaration = DataModel | Enum; DataModelFieldAttribute: - decl=[Attribute:DataModelFieldAttributeName] ('(' AttributeArgList? ')')?; + decl=[Attribute:FIELD_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?; DataModelAttribute: - TRIPLE_SLASH_COMMENT* decl=[Attribute:DataModelAttributeName] ('(' AttributeArgList? ')')?; + TRIPLE_SLASH_COMMENT* decl=[Attribute:MODEL_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?; InternalAttribute: - decl=[Attribute:InternalAttributeName] ('(' AttributeArgList? ')')?; + decl=[Attribute:INTERNAL_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?; fragment AttributeArgList: args+=AttributeArg (',' args+=AttributeArg)*; @@ -283,8 +264,9 @@ Boolean returns boolean: 'true' | 'false'; hidden terminal WS: /\s+/; -terminal NULL: 'null'; -terminal THIS: 'this'; +terminal INTERNAL_ATTRIBUTE_NAME: /@@@([_a-zA-Z][\w_]*\.)*[_a-zA-Z][\w_]*/; +terminal MODEL_ATTRIBUTE_NAME: /@@([_a-zA-Z][\w_]*\.)*[_a-zA-Z][\w_]*/; +terminal FIELD_ATTRIBUTE_NAME: /@([_a-zA-Z][\w_]*\.)*[_a-zA-Z][\w_]*/; terminal ID: /[_a-zA-Z][\w_]*/; terminal STRING: /"(\\.|[^"\\])*"|'(\\.|[^'\\])*'/; terminal NUMBER: /[+-]?[0-9]+(\.[0-9]+)?/; diff --git a/packages/language/syntaxes/zmodel.tmLanguage b/packages/language/syntaxes/zmodel.tmLanguage index bacb471c3..cf70fb761 100644 --- a/packages/language/syntaxes/zmodel.tmLanguage +++ b/packages/language/syntaxes/zmodel.tmLanguage @@ -20,7 +20,7 @@ name keyword.control.zmodel match - \b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|plugin|sort|true|view)\b + \b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|sort|this|true|view)\b name diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json index d886d9b61..00c737c97 100644 --- a/packages/language/syntaxes/zmodel.tmLanguage.json +++ b/packages/language/syntaxes/zmodel.tmLanguage.json @@ -10,7 +10,7 @@ }, { "name": "keyword.control.zmodel", - "match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|plugin|sort|true|view)\\b" + "match": "\\b(Any|Asc|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|Desc|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|sort|this|true|view)\\b" }, { "name": "string.quoted.double.zmodel", diff --git a/packages/schema/language-configuration.json b/packages/schema/language-configuration.json index ea1cc2473..4000da2d9 100644 --- a/packages/schema/language-configuration.json +++ b/packages/schema/language-configuration.json @@ -17,7 +17,8 @@ ["[", "]"], ["(", ")"], ["\"", "\""], - ["'", "'"] + ["'", "'"], + { "open": "/**", "close": " */", "notIn": ["string"] } ], // symbols that can be used to surround a selection "surroundingPairs": [ @@ -26,5 +27,6 @@ ["(", ")"], ["\"", "\""], ["'", "'"] - ] + ], + "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\#\\%\\^\\&\\*\\-\\=\\+\\{\\}\\(\\)\\[\\]\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\s]+)" } diff --git a/packages/schema/src/language-server/validator/function-invocation-validator.ts b/packages/schema/src/language-server/validator/function-invocation-validator.ts index 5e0f1d639..3bc364bd2 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -50,7 +50,7 @@ export default class FunctionInvocationValidator implements AstValidator ExpressionContext.DefaultValue) - .with(P.union('@@allow', '@@deny'), () => ExpressionContext.AccessPolicy) + .with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy) .with('@@validate', () => ExpressionContext.ValidationRule) .otherwise(() => undefined); diff --git a/packages/schema/src/language-server/zmodel-completion-provider.ts b/packages/schema/src/language-server/zmodel-completion-provider.ts new file mode 100644 index 000000000..742f7087f --- /dev/null +++ b/packages/schema/src/language-server/zmodel-completion-provider.ts @@ -0,0 +1,377 @@ +import { + DataModelAttribute, + DataModelFieldAttribute, + ReferenceExpr, + StringLiteral, + isArrayExpr, + isAttribute, + isDataModel, + isDataModelAttribute, + isDataModelField, + isDataModelFieldAttribute, + isEnum, + isEnumField, + isFunctionDecl, + isInvocationExpr, + isMemberAccessExpr, +} from '@zenstackhq/language/ast'; +import { ZModelCodeGenerator, getAttribute, isEnumFieldReference, isFromStdlib } from '@zenstackhq/sdk'; +import { + AstNode, + AstNodeDescription, + CompletionAcceptor, + CompletionContext, + CompletionProviderOptions, + CompletionValueItem, + DefaultCompletionProvider, + LangiumDocument, + LangiumServices, + MaybePromise, + NextFeature, +} from 'langium'; +import { P, match } from 'ts-pattern'; +import { CompletionItemKind, CompletionList, CompletionParams, MarkupContent } from 'vscode-languageserver'; + +export class ZModelCompletionProvider extends DefaultCompletionProvider { + constructor(private readonly services: LangiumServices) { + super(services); + } + + readonly completionOptions?: CompletionProviderOptions = { + triggerCharacters: ['@', '(', ',', '.'], + }; + + override async getCompletion( + document: LangiumDocument, + params: CompletionParams + ): Promise { + try { + return await super.getCompletion(document, params); + } catch (e) { + console.error('Completion error:', (e as Error).message); + return undefined; + } + } + + override completionFor( + context: CompletionContext, + next: NextFeature, + acceptor: CompletionAcceptor + ): MaybePromise { + if (isDataModelAttribute(context.node) || isDataModelFieldAttribute(context.node)) { + const completions = this.getCompletionFromHint(context.node); + if (completions) { + completions.forEach(acceptor); + return; + } + } + return super.completionFor(context, next, acceptor); + } + + private getCompletionFromHint( + contextNode: DataModelAttribute | DataModelFieldAttribute + ): CompletionValueItem[] | undefined { + // get completion based on the hint on the next unfilled parameter + const unfilledParams = this.getUnfilledAttributeParams(contextNode); + const nextParam = unfilledParams[0]; + if (!nextParam) { + return undefined; + } + + const hintAttr = getAttribute(nextParam, '@@@completionHint'); + if (hintAttr) { + const hint = hintAttr.args[0]; + if (hint?.value) { + if (isArrayExpr(hint.value)) { + return hint.value.items.map((item) => { + return { + label: `${(item as StringLiteral).value}`, + kind: CompletionItemKind.Value, + detail: 'Parameter', + sortText: '0', + }; + }); + } + } + } + return undefined; + } + + // TODO: this doesn't work when the file contains parse errors + private getUnfilledAttributeParams(contextNode: DataModelAttribute | DataModelFieldAttribute) { + try { + const params = contextNode.decl.ref?.params; + if (params) { + const args = contextNode.args; + let unfilledParams = [...params]; + args.forEach((arg) => { + if (arg.name) { + unfilledParams = unfilledParams.filter((p) => p.name !== arg.name); + } else { + unfilledParams.shift(); + } + }); + + return unfilledParams; + } + } catch { + // noop + } + return []; + } + + override completionForCrossReference( + context: CompletionContext, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + crossRef: any, + acceptor: CompletionAcceptor + ): MaybePromise { + if (crossRef.property === 'member' && !isMemberAccessExpr(context.node)) { + // for guarding an error in the base implementation + return; + } + + const customAcceptor = (item: CompletionValueItem) => { + // attributes starting with @@@ are for internal use only + if (item.insertText?.startsWith('@@@') || item.label?.startsWith('@@@')) { + return; + } + + if ('nodeDescription' in item) { + const node = this.getAstNode(item.nodeDescription); + if (!node) { + return; + } + + // enums in stdlib are not supposed to be referenced directly + if ((isEnum(node) || isEnumField(node)) && isFromStdlib(node)) { + return; + } + + if ( + (isDataModelAttribute(context.node) || isDataModelFieldAttribute(context.node)) && + !this.filterAttributeApplicationCompletion(context.node, node) + ) { + // node not matching attribute context + return; + } + } + acceptor(item); + }; + + super.completionForCrossReference(context, crossRef, customAcceptor); + } + + override completionForKeyword( + context: CompletionContext, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + keyword: any, + acceptor: CompletionAcceptor + ): MaybePromise { + const customAcceptor = (item: CompletionValueItem) => { + if (!this.filterKeywordForContext(context, keyword.value)) { + return; + } + acceptor(item); + }; + super.completionForKeyword(context, keyword, customAcceptor); + } + + private filterKeywordForContext(context: CompletionContext, keyword: string) { + if (isInvocationExpr(context.node)) { + return ['true', 'false', 'null', 'this'].includes(keyword); + } else if (isDataModelAttribute(context.node) || isDataModelFieldAttribute(context.node)) { + const exprContext = this.getAttributeContextType(context.node); + if (exprContext === 'DefaultValue') { + return ['true', 'false', 'null'].includes(keyword); + } else { + return ['true', 'false', 'null', 'this'].includes(keyword); + } + } else { + return true; + } + } + + private filterAttributeApplicationCompletion( + contextNode: DataModelAttribute | DataModelFieldAttribute, + node: AstNode + ) { + const attrContextType = this.getAttributeContextType(contextNode); + + if (isFunctionDecl(node) && attrContextType) { + // functions are excluded if they are not allowed in the current context + const funcExprContextAttr = getAttribute(node, '@@@expressionContext'); + if (funcExprContextAttr && funcExprContextAttr.args[0]) { + const arg = funcExprContextAttr.args[0]; + if (isArrayExpr(arg.value)) { + return arg.value.items.some( + (item) => + isEnumFieldReference(item) && (item as ReferenceExpr).target.$refText === attrContextType + ); + } + } + return false; + } + + if (isDataModelField(node)) { + // model fields are not allowed in @default + return attrContextType !== 'DefaultValue'; + } + + return true; + } + + private getAttributeContextType(node: DataModelAttribute | DataModelFieldAttribute) { + return match(node.decl.$refText) + .with('@default', () => 'DefaultValue') + .with(P.union('@@allow', '@allow', '@@deny', '@deny'), () => 'AccessPolicy') + .with('@@validate', () => 'ValidationRule') + .otherwise(() => undefined); + } + + override createReferenceCompletionItem(nodeDescription: AstNodeDescription): CompletionValueItem { + const node = this.getAstNode(nodeDescription); + const documentation = this.getNodeDocumentation(node); + + return match(node) + .when(isDataModel, () => ({ + nodeDescription, + kind: CompletionItemKind.Class, + detail: 'Data model', + sortText: '1', + documentation, + })) + .when(isDataModelField, () => ({ + nodeDescription, + kind: CompletionItemKind.Field, + detail: 'Data model field', + sortText: '0', + documentation, + })) + .when(isEnum, () => ({ + nodeDescription, + kind: CompletionItemKind.Class, + detail: 'Enum', + sortText: '1', + documentation, + })) + .when(isEnumField, () => ({ + nodeDescription, + kind: CompletionItemKind.Enum, + detail: 'Enum value', + sortText: '1', + documentation, + })) + .when(isFunctionDecl, () => ({ + nodeDescription, + insertText: this.getFunctionInsertText(nodeDescription), + kind: CompletionItemKind.Function, + detail: 'Function', + sortText: '1', + documentation, + })) + .when(isAttribute, () => ({ + nodeDescription, + insertText: this.getAttributeInsertText(nodeDescription), + kind: CompletionItemKind.Property, + detail: 'Attribute', + sortText: '1', + documentation, + })) + .otherwise(() => ({ + nodeDescription, + kind: CompletionItemKind.Reference, + detail: nodeDescription.type, + sortText: '2', + documentation, + })); + } + + private getFunctionInsertText(nodeDescription: AstNodeDescription): string { + const node = this.getAstNode(nodeDescription); + if (isFunctionDecl(node)) { + if (node.params.some((p) => !p.optional)) { + return nodeDescription.name; + } + } + return `${nodeDescription.name}()`; + } + + private getAttributeInsertText(nodeDescription: AstNodeDescription): string { + const node = this.getAstNode(nodeDescription); + if (isAttribute(node)) { + if (node.name === '@relation') { + return `${nodeDescription.name}(fields: [], references: [])`; + } + } + return nodeDescription.name; + } + + private getAstNode(nodeDescription: AstNodeDescription) { + let node = nodeDescription.node; + if (!node) { + const doc = this.services.shared.workspace.LangiumDocuments.getOrCreateDocument( + nodeDescription.documentUri + ); + if (!doc) { + return undefined; + } + node = this.services.workspace.AstNodeLocator.getAstNode(doc.parseResult.value, nodeDescription.path); + if (!node) { + return undefined; + } + } + return node; + } + + private getNodeDocumentation(node?: AstNode): MarkupContent | undefined { + if (!node) { + return undefined; + } + const md = this.commentsToMarkdown(node); + return { + kind: 'markdown', + value: md, + }; + } + + private commentsToMarkdown(node: AstNode): string { + const md = this.services.documentation.DocumentationProvider.getDocumentation(node) ?? ''; + const zModelGenerator = new ZModelCodeGenerator(); + const docs: string[] = []; + + try { + match(node) + .when(isAttribute, (attr) => { + const zModelGenerator = new ZModelCodeGenerator(); + docs.push('```prisma', zModelGenerator.generate(attr), '```'); + }) + .when(isFunctionDecl, (func) => { + docs.push('```ts', zModelGenerator.generate(func), '```'); + }) + .when(isDataModel, (model) => { + docs.push('```prisma', `model ${model.name} { ... }`, '```'); + }) + .when(isEnum, (enumDecl) => { + docs.push('```prisma', zModelGenerator.generate(enumDecl), '```'); + }) + .when(isDataModelField, (field) => { + docs.push(`${field.name}: ${field.type.type ?? field.type.reference?.$refText}`); + }) + .otherwise((ast) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const name = (ast as any).name; + if (name) { + docs.push(name); + } + }); + } catch { + // noop + } + + if (md) { + docs.push('___', md); + } + return docs.join('\n'); + } +} diff --git a/packages/schema/src/language-server/zmodel-highlight.ts b/packages/schema/src/language-server/zmodel-highlight.ts new file mode 100644 index 000000000..dc8fcc62e --- /dev/null +++ b/packages/schema/src/language-server/zmodel-highlight.ts @@ -0,0 +1,16 @@ +import { DefaultDocumentHighlightProvider, LangiumDocument } from 'langium'; +import { DocumentHighlight, DocumentHighlightParams } from 'vscode-languageserver'; + +export class ZModelHighlightProvider extends DefaultDocumentHighlightProvider { + override async getDocumentHighlight( + document: LangiumDocument, + params: DocumentHighlightParams + ): Promise { + try { + return await super.getDocumentHighlight(document, params); + } catch (e) { + console.error('Highlight error:', (e as Error).message); + return undefined; + } + } +} diff --git a/packages/schema/src/language-server/zmodel-hover.ts b/packages/schema/src/language-server/zmodel-hover.ts new file mode 100644 index 000000000..06c40caaf --- /dev/null +++ b/packages/schema/src/language-server/zmodel-hover.ts @@ -0,0 +1,16 @@ +import { AstNode, LangiumDocument, MultilineCommentHoverProvider } from 'langium'; +import { Hover, HoverParams } from 'vscode-languageclient'; + +export class ZModelHoverProvider extends MultilineCommentHoverProvider { + override async getHoverContent( + document: LangiumDocument, + params: HoverParams + ): Promise { + try { + return await super.getHoverContent(document, params); + } catch (e) { + console.error('Hover error:', (e as Error).message); + return undefined; + } + } +} diff --git a/packages/schema/src/language-server/zmodel-module.ts b/packages/schema/src/language-server/zmodel-module.ts index ab0f32b45..07dc223e0 100644 --- a/packages/schema/src/language-server/zmodel-module.ts +++ b/packages/schema/src/language-server/zmodel-module.ts @@ -1,6 +1,5 @@ import { ZModelGeneratedModule, ZModelGeneratedSharedModule } from '@zenstackhq/language/module'; import { - createDefaultModule, DefaultConfigurationProvider, DefaultDocumentBuilder, DefaultIndexManager, @@ -9,24 +8,29 @@ import { DefaultLanguageServer, DefaultServiceRegistry, DefaultSharedModuleContext, - inject, LangiumDefaultSharedServices, LangiumServices, LangiumSharedServices, Module, MutexLock, PartialLangiumServices, + createGrammarConfig as createDefaultGrammarConfig, + createDefaultModule, + inject, } from 'langium'; import { TextDocuments } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { ZModelValidationRegistry, ZModelValidator } from './validator/zmodel-validator'; import { ZModelCodeActionProvider } from './zmodel-code-action'; +import { ZModelCompletionProvider } from './zmodel-completion-provider'; +import { ZModelDefinitionProvider } from './zmodel-definition'; import { ZModelFormatter } from './zmodel-formatter'; +import { ZModelHighlightProvider } from './zmodel-highlight'; +import { ZModelHoverProvider } from './zmodel-hover'; import { ZModelLinker } from './zmodel-linker'; import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope'; -import ZModelWorkspaceManager from './zmodel-workspace-manager'; -import { ZModelDefinitionProvider } from './zmodel-definition'; import { ZModelSemanticTokenProvider } from './zmodel-semantic'; +import ZModelWorkspaceManager from './zmodel-workspace-manager'; /** * Declaration of custom services - add your own service classes here. @@ -63,6 +67,12 @@ export const ZModelModule: Module new ZModelCodeActionProvider(services), DefinitionProvider: (services) => new ZModelDefinitionProvider(services), SemanticTokenProvider: (services) => new ZModelSemanticTokenProvider(services), + CompletionProvider: (services) => new ZModelCompletionProvider(services), + HoverProvider: (services) => new ZModelHoverProvider(services), + DocumentHighlightProvider: (services) => new ZModelHighlightProvider(services), + }, + parser: { + GrammarConfig: (services) => createGrammarConfig(services), }, }; @@ -115,3 +125,9 @@ export function createZModelServices(context: DefaultSharedModuleContext): { shared.ServiceRegistry.register(ZModel); return { shared, ZModel }; } + +function createGrammarConfig(services: LangiumServices) { + const config = createDefaultGrammarConfig(services); + config.nameRegexp = /^[@\w\p{L}]$/u; + return config; +} diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts index a227d9d1c..8eda869e8 100644 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ b/packages/schema/src/language-server/zmodel-scope.ts @@ -1,22 +1,34 @@ -import { isEnumField, isModel, Model, DataModel } from '@zenstackhq/language/ast'; +import { + DataModel, + MemberAccessExpr, + Model, + isDataModel, + isDataModelField, + isEnumField, + isInvocationExpr, + isMemberAccessExpr, + isModel, + isReferenceExpr, +} from '@zenstackhq/language/ast'; +import { getAuthModel, getDataModels } from '@zenstackhq/sdk'; import { AstNode, AstNodeDescription, DefaultScopeComputation, DefaultScopeProvider, EMPTY_SCOPE, - equalURI, - getContainerOfType, - interruptAndCheck, LangiumDocument, LangiumServices, Mutable, PrecomputedScopes, ReferenceInfo, Scope, + StreamScope, + equalURI, + getContainerOfType, + interruptAndCheck, stream, streamAllContents, - StreamScope, } from 'langium'; import { CancellationToken } from 'vscode-jsonrpc'; import { resolveImportUri } from '../utils/ast-utils'; @@ -125,4 +137,53 @@ export class ZModelScopeProvider extends DefaultScopeProvider { ); return new StreamScope(importedElements); } + + override getScope(context: ReferenceInfo): Scope { + if (isMemberAccessExpr(context.container) && context.container.operand && context.property === 'member') { + return this.getMemberAccessScope(context.container); + } + return super.getScope(context); + } + + private getMemberAccessScope(node: MemberAccessExpr) { + if (isReferenceExpr(node.operand)) { + // scope to target model's fields + const ref = node.operand.target.ref; + if (isDataModelField(ref)) { + const targetModel = ref.type.reference?.ref; + if (isDataModel(targetModel)) { + return this.createScopeForNodes(targetModel.fields); + } + } + } else if (isMemberAccessExpr(node.operand)) { + // scope to target model's fields + const ref = node.operand.member.ref; + if (isDataModelField(ref)) { + const targetModel = ref.type.reference?.ref; + if (isDataModel(targetModel)) { + return this.createScopeForNodes(targetModel.fields); + } + } + } else if (isInvocationExpr(node.operand)) { + // deal with member access from `auth()` and `future() + const funcName = node.operand.function.$refText; + if (funcName === 'auth') { + // resolve to `User` or `@@auth` model + const model = getContainerOfType(node, isModel); + if (model) { + const authModel = getAuthModel(getDataModels(model)); + if (authModel) { + return this.createScopeForNodes(authModel.fields); + } + } + } + if (funcName === 'future') { + const thisModel = getContainerOfType(node, isDataModel); + if (thisModel) { + return this.createScopeForNodes(thisModel.fields); + } + } + } + return EMPTY_SCOPE; + } } diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 6cd826e47..1a9446d7b 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -1,5 +1,6 @@ /** * Enum representing referential integrity related actions + * @see https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions */ enum ReferentialAction { /** @@ -16,7 +17,6 @@ enum ReferentialAction { /** * Similar to 'Restrict', the difference between the two is dependent on the database being used. - * See details: https://www.prisma.io/docs/concepts/components/prisma-schema/relations/referential-actions#noaction */ NoAction @@ -188,27 +188,55 @@ attribute @@@expressionContext(_ context: ExpressionContext[]) attribute @@@prisma() /** - * Defines an ID on the model. + * Provides hint for auto-completion. + */ +attribute @@@completionHint(_ values: String[]) + +/** + * Defines a single-field ID on the model. + * + * @param map: The name of the underlying primary key constraint in the database. + * @param length: Allows you to specify a maximum length for the subpart of the value to be indexed. + * @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc. + * @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true. */ attribute @id(map: String?, length: Int?, sort: String?, clustered: Boolean?) @@@prisma /** * Defines a default value for a field. + * @param value: An expression (e.g. 5, true, now()). */ attribute @default(_ value: ContextType, map: String?) @@@prisma /** * Defines a unique constraint for this field. + * + * @param length: Allows you to specify a maximum length for the subpart of the value to be indexed. + * @param sort: Allows you to specify in what order the entries of the constraint are stored in the database. The available options are Asc and Desc. + * @param clustered: Boolean Defines whether the constraint is clustered or non-clustered. Defaults to false. */ attribute @unique(map: String?, length: Int?, sort: String?, clustered: Boolean?) @@@prisma /** * Defines a multi-field ID (composite ID) on the model. + * + * @param fields: A list of field names - for example, [firstname, lastname] + * @param name: The name that Prisma Client will expose for the argument covering all fields, e.g. fullName in fullName: { firstName: "First", lastName: "Last"} + * @param map: The name of the underlying primary key constraint in the database. + * @param length: Allows you to specify a maximum length for the subpart of the value to be indexed. + * @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc. + * @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true. */ attribute @@id(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: String?, clustered: Boolean?) @@@prisma /** * Defines a compound unique constraint for the specified fields. + * + * @param fields: A list of field names - for example, [firstname, lastname]. Fields must be mandatory. + * @param name: The name of the unique combination of fields - defaults to fieldName1_fieldName2_fieldName3 + * @param length: Allows you to specify a maximum length for the subpart of the value to be indexed. + * @param sort: Allows you to specify in what order the entries of the constraint are stored in the database. The available options are Asc and Desc. + * @param clustered: Boolean Defines whether the constraint is clustered or non-clustered. Defaults to false. */ attribute @@unique(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: String?, clustered: Boolean?) @@@prisma @@ -226,21 +254,40 @@ enum IndexType { /** * Defines an index in the database. + * + * @params fields: A list of field names - for example, [firstname, lastname] + * @params name: The name that Prisma Client will expose for the argument covering all fields, e.g. fullName in fullName: { firstName: "First", lastName: "Last"} + * @params map: The name of the index in the underlying database (Prisma generates an index name that respects identifier length limits if you do not specify a name. Prisma uses the following naming convention: tablename.field1_field2_field3_unique) + * @params length: Allows you to specify a maximum length for the subpart of the value to be indexed. + * @params sort: Allows you to specify in what order the entries of the index or constraint are stored in the database. The available options are asc and desc. + * @params clustered: Defines whether the index is clustered or non-clustered. Defaults to false. + * @params type: Allows you to specify an index access method. Defaults to BTree. */ attribute @@index(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: String?, clustered: Boolean?, type: IndexType?) @@@prisma /** * Defines meta information about the relation. + * + * @param name: Sometimes (e.g. to disambiguate a relation) Defines the name of the relationship. In an m-n-relation, it also determines the name of the underlying relation table. + * @param fields: A list of fields of the current model + * @param references: A list of fields of the model on the other side of the relation + * @param map: Defines a custom name for the foreign key in the database. + * @param onUpdate: Defines the referential action to perform when a referenced entry in the referenced model is being updated. + * @param onDelete: Defines the referential action to perform when a referenced entry in the referenced model is being deleted. */ attribute @relation(_ name: String?, fields: FieldReference[]?, references: TransitiveFieldReference[]?, onDelete: ReferentialAction?, onUpdate: ReferentialAction?, map: String?) @@@prisma /** * Maps a field name or enum value from the schema to a column with a different name in the database. + * + * @param name: The database column name. */ attribute @map(_ name: String) @@@prisma /** * Maps the schema model name to a table with a different name, or an enum name to a different underlying enum in the database. + * + * @param name: The database column name. */ attribute @@map(_ name: String) @@@prisma @@ -359,29 +406,44 @@ attribute @db.Image() @@@targetField([BytesField]) @@@prisma /** * Specifies the schema to use in a multi-schema database. https://www.prisma.io/docs/guides/database/multi-schema. + * + * @param: The name of the database schema. */ attribute @@schema(_ name: String) @@@prisma /** * Defines an access policy that allows a set of operations when the given condition is true. + * + * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. + * @param condition: a boolean expression that controls if the operation should be allowed. */ -attribute @@allow(_ operation: String, _ condition: Boolean) +attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) /** * Defines an access policy that allows the annotated field to be read or updated. * You can pass a thrid argument as `true` to make it override the model-level policies. + * + * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. + * @param condition: a boolean expression that controls if the operation should be allowed. + * @param override: a boolean value that controls if the field-level policy should override the model-level policy. */ -attribute @allow(_ operation: String, _ condition: Boolean, _ override: Boolean?) +attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean, _ override: Boolean?) /** * Defines an access policy that denies a set of operations when the given condition is true. + * + * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. + * @param condition: a boolean expression that controls if the operation should be denied. */ -attribute @@deny(_ operation: String, _ condition: Boolean) +attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) /** * Defines an access policy that denies the annotated field to be read or updated. + * + * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. + * @param condition: a boolean expression that controls if the operation should be denied. */ -attribute @deny(_ operation: String, _ condition: Boolean) +attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) /** * Used to specify the model for resolving `auth()` function call in access policies. A Zmodel @@ -398,8 +460,8 @@ attribute @@auth() * * @see https://www.npmjs.com/package/bcryptjs for details * - * @saltLength: length of salt to use (cost factor for the hash function) - * @salt: salt to use (a pregenerated valid salt) + * @param saltLength: length of salt to use (cost factor for the hash function) + * @param salt: salt to use (a pregenerated valid salt) */ attribute @password(saltLength: Int?, salt: String?) @@@targetField([StringField]) diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index abec6092b..d32962f11 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -1,5 +1,7 @@ import { AstNode, + Attribute, + AttributeParam, ConfigExpr, DataModel, DataModelAttribute, @@ -110,11 +112,17 @@ export function indentString(string: string, count = 4): string { return string.replace(/^(?!\s*$)/gm, indent.repeat(count)); } -export function hasAttribute(decl: DataModel | DataModelField | Enum | EnumField, name: string) { +export function hasAttribute( + decl: DataModel | DataModelField | Enum | EnumField | FunctionDecl | Attribute | AttributeParam, + name: string +) { return !!getAttribute(decl, name); } -export function getAttribute(decl: DataModel | DataModelField | Enum | EnumField, name: string) { +export function getAttribute( + decl: DataModel | DataModelField | Enum | EnumField | FunctionDecl | Attribute | AttributeParam, + name: string +) { return (decl.attributes as (DataModelAttribute | DataModelFieldAttribute)[]).find( (attr) => attr.decl.$refText === name ); diff --git a/packages/sdk/src/zmodel-code-generator.ts b/packages/sdk/src/zmodel-code-generator.ts index 46cc2010f..1b1f001e1 100644 --- a/packages/sdk/src/zmodel-code-generator.ts +++ b/packages/sdk/src/zmodel-code-generator.ts @@ -2,7 +2,10 @@ import { Argument, ArrayExpr, AstNode, + Attribute, AttributeArg, + AttributeParam, + AttributeParamType, BinaryExpr, BinaryExprOperatorPriority, BooleanLiteral, @@ -18,6 +21,9 @@ import { Enum, EnumField, FieldInitializer, + FunctionDecl, + FunctionParam, + FunctionParamType, GeneratorDecl, InvocationExpr, LiteralExpr, @@ -282,7 +288,39 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ return 'this'; } - argument(ast: Argument) { + @gen(Attribute) + private _generateAttribute(ast: Attribute) { + return `attribute ${ast.name}(${ast.params.map((x) => this.generate(x)).join(', ')})`; + } + + @gen(AttributeParam) + private _generateAttributeParam(ast: AttributeParam) { + return `${ast.default ? '_ ' : ''}${ast.name}: ${this.generate(ast.type)}`; + } + + @gen(AttributeParamType) + private _generateAttributeParamType(ast: AttributeParamType) { + return `${ast.type ?? ast.reference?.$refText}${ast.array ? '[]' : ''}${ast.optional ? '?' : ''}`; + } + + @gen(FunctionDecl) + private _generateFunctionDecl(ast: FunctionDecl) { + return `function ${ast.name}(${ast.params.map((x) => this.generate(x)).join(', ')}) ${ + ast.returnType ? ': ' + this.generate(ast.returnType) : '' + } {}`; + } + + @gen(FunctionParam) + private _generateFunctionParam(ast: FunctionParam) { + return `${ast.name}: ${this.generate(ast.type)}`; + } + + @gen(FunctionParamType) + private _generateFunctionParamType(ast: FunctionParamType) { + return `${ast.type ?? ast.reference?.$refText}${ast.array ? '[]' : ''}`; + } + + private argument(ast: Argument) { return `${ast.name ? ast.name + ': ' : ''}${this.generate(ast.value)}`; }