Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/ide/vscode/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zenstack-v3",
"publisher": "zenstack",
"version": "3.0.11",
"version": "3.0.12",
"displayName": "ZenStack V3 Language Tools",
"description": "VSCode extension for ZenStack (v3) ZModel language",
"private": true,
Expand Down
3 changes: 2 additions & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"dependencies": {
"langium": "catalog:",
"pluralize": "^8.0.0",
"ts-pattern": "catalog:"
"ts-pattern": "catalog:",
"vscode-languageserver": "^9.0.1"
},
"devDependencies": {
"@types/pluralize": "^0.0.33",
Expand Down
1 change: 1 addition & 0 deletions packages/language/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { loadDocument } from './document';
export * from './module';
export { ZModelCodeGenerator } from './zmodel-code-generator';
37 changes: 29 additions & 8 deletions packages/language/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ import type { Model } from './ast';
import { ZModelGeneratedModule, ZModelGeneratedSharedModule, ZModelLanguageMetaData } from './generated/module';
import { getPluginDocuments } from './utils';
import { registerValidationChecks, ZModelValidator } from './validator';
import { ZModelCompletionProvider } from './zmodel-completion-provider';
import { ZModelDefinitionProvider } from './zmodel-definition';
import { ZModelDocumentBuilder } from './zmodel-document-builder';
import { ZModelDocumentationProvider } from './zmodel-documentation-provider';
import { ZModelFormatter } from './zmodel-formatter';
import { ZModelLinker } from './zmodel-linker';
import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope';
import { ZModelSemanticTokenProvider } from './zmodel-semantic';
import { ZModelWorkspaceManager } from './zmodel-workspace-manager';
import { ZModelCommentProvider } from './zmodle-comment-provider';
export { ZModelLanguageMetaData };

/**
Expand Down Expand Up @@ -49,6 +55,16 @@ export const ZModelLanguageModule: Module<ZModelServices, PartialLangiumServices
validation: {
ZModelValidator: (services) => new ZModelValidator(services),
},
lsp: {
Formatter: (services) => new ZModelFormatter(services),
DefinitionProvider: (services) => new ZModelDefinitionProvider(services),
CompletionProvider: (services) => new ZModelCompletionProvider(services),
SemanticTokenProvider: (services) => new ZModelSemanticTokenProvider(services),
},
documentation: {
CommentProvider: (services) => new ZModelCommentProvider(services),
DocumentationProvider: (services) => new ZModelDocumentationProvider(services),
},
};

export type ZModelSharedServices = LangiumSharedServices;
Expand Down Expand Up @@ -109,15 +125,20 @@ export function createZModelLanguageServices(

const schemaPath = fileURLToPath(doc.uri.toString());
const pluginSchemas = getPluginDocuments(doc.parseResult.value as Model, schemaPath);

// ensure plugin docs are loaded
for (const plugin of pluginSchemas) {
// load the plugin model document
const pluginDoc = await shared.workspace.LangiumDocuments.getOrCreateDocument(
URI.file(path.resolve(plugin)),
);
// add to indexer so the plugin model's definitions are globally visible
shared.workspace.IndexManager.updateContent(pluginDoc);
if (logToConsole) {
console.log(`Loaded plugin model: ${plugin}`);
const pluginDocUri = URI.file(path.resolve(plugin));
let pluginDoc = shared.workspace.LangiumDocuments.getDocument(pluginDocUri);
if (!pluginDoc) {
pluginDoc = await shared.workspace.LangiumDocuments.getOrCreateDocument(pluginDocUri);
if (pluginDoc) {
// add to indexer so the plugin model's definitions are globally visible
shared.workspace.IndexManager.updateContent(pluginDoc);
if (logToConsole) {
console.log(`Loaded plugin model: ${plugin}`);
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
// TODO: design a way to let plugin register validation
@check('@@allow')
@check('@@deny')
// @ts-expect-error
private _checkModelLevelPolicy(attr: AttributeApplication, accept: ValidationAcceptor) {
const kind = getStringLiteral(attr.args[0]?.value);
if (!kind) {
Expand Down Expand Up @@ -247,7 +246,6 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
// TODO: design a way to let plugin register validation
@check('@allow')
@check('@deny')
// @ts-expect-error
private _checkFieldLevelPolicy(attr: AttributeApplication, accept: ValidationAcceptor) {
const kind = getStringLiteral(attr.args[0]?.value);
if (!kind) {
Expand Down Expand Up @@ -277,7 +275,6 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
}

@check('@@validate')
// @ts-expect-error
private _checkValidate(attr: AttributeApplication, accept: ValidationAcceptor) {
const condition = attr.args[0]?.value;
if (
Expand All @@ -293,7 +290,6 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
@check('@@id')
@check('@@index')
@check('@@unique')
// @ts-expect-error
private _checkConstraint(attr: AttributeApplication, accept: ValidationAcceptor) {
const fields = attr.args[0]?.value;
const attrName = attr.decl.ref?.name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ export default class FunctionInvocationValidator implements AstValidator<Express
}

@func('length')
// @ts-expect-error
private _checkLength(expr: InvocationExpr, accept: ValidationAcceptor) {
const msg = 'argument must be a string or list field';
const fieldArg = expr.args[0]!.value;
Expand All @@ -206,7 +205,6 @@ export default class FunctionInvocationValidator implements AstValidator<Express
}

@func('regex')
// @ts-expect-error
private _checkRegex(expr: InvocationExpr, accept: ValidationAcceptor) {
const regex = expr.args[1]?.value;
if (!isStringLiteral(regex)) {
Expand All @@ -228,7 +226,6 @@ export default class FunctionInvocationValidator implements AstValidator<Express

// TODO: move this to policy plugin
@func('check')
// @ts-expect-error
private _checkCheck(expr: InvocationExpr, accept: ValidationAcceptor) {
let valid = true;

Expand Down
169 changes: 169 additions & 0 deletions packages/language/src/zmodel-code-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {
type AstReflection,
type IndexManager,
type LangiumDocument,
type LangiumDocuments,
type MaybePromise,
} from 'langium';
import { DataField, DataModel, Model, isDataModel } from './ast';

import type { CodeActionProvider, LangiumServices } from 'langium/lsp';
import { CodeAction, CodeActionKind, type CodeActionParams, Command, Diagnostic } from 'vscode-languageserver';
import { IssueCodes } from './constants';
import { getAllFields, getDocument } from './utils';
import type { MissingOppositeRelationData } from './validators/datamodel-validator';
import { ZModelFormatter } from './zmodel-formatter';

export class ZModelCodeActionProvider implements CodeActionProvider {
protected readonly reflection: AstReflection;
protected readonly indexManager: IndexManager;
protected readonly formatter: ZModelFormatter;
protected readonly documents: LangiumDocuments;

constructor(services: LangiumServices) {
this.reflection = services.shared.AstReflection;
this.indexManager = services.shared.workspace.IndexManager;
this.formatter = services.lsp.Formatter as ZModelFormatter;
this.documents = services.shared.workspace.LangiumDocuments;
}

getCodeActions(
document: LangiumDocument,
params: CodeActionParams,
): MaybePromise<Array<Command | CodeAction> | undefined> {
const result: CodeAction[] = [];
const acceptor = (ca: CodeAction | undefined) => ca && result.push(ca);
for (const diagnostic of params.context.diagnostics) {
this.createCodeActions(diagnostic, document, acceptor);
}
return result;
}

private createCodeActions(
diagnostic: Diagnostic,
document: LangiumDocument,
accept: (ca: CodeAction | undefined) => void,
) {
switch (diagnostic.code) {
case IssueCodes.MissingOppositeRelation:
accept(this.fixMissingOppositeRelation(diagnostic, document));
}

return undefined;
}

private fixMissingOppositeRelation(diagnostic: Diagnostic, document: LangiumDocument): CodeAction | undefined {
const data = diagnostic.data as MissingOppositeRelationData;

const rootCst =
data.relationFieldDocUri == document.textDocument.uri
? document.parseResult.value
: this.documents.all.find((doc) => doc.textDocument.uri === data.relationFieldDocUri)?.parseResult
.value;

if (rootCst) {
const fieldModel = rootCst as Model;
const fieldAstNode = (
fieldModel.declarations.find(
(x) => isDataModel(x) && x.name === data.relationDataModelName,
) as DataModel
)?.fields.find((x) => x.name === data.relationFieldName) as DataField;

if (!fieldAstNode) return undefined;

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const oppositeModel = fieldAstNode.type.reference!.ref! as DataModel;

const currentModel = document.parseResult.value as Model;

const container = currentModel.declarations.find(
(decl) => decl.name === data.dataModelName && isDataModel(decl),
) as DataModel;

if (container && container.$cstNode) {
// indent
let indent = '\t';
const formatOptions = this.formatter.getFormatOptions();
if (formatOptions?.insertSpaces) {
indent = ' '.repeat(formatOptions.tabSize);
}
indent = indent.repeat(this.formatter.getIndent());

let newText = '';
if (fieldAstNode.type.array) {
// post Post[]
const idField = getAllFields(container).find((f) =>
f.attributes.find((attr) => attr.decl.ref?.name === '@id'),
);

// if no id field, we can't generate reference
if (!idField) {
return undefined;
}

const typeName = container.name;
const fieldName = this.lowerCaseFirstLetter(typeName);

// might already exist
let referenceField = '';

const idFieldName = idField.name;
const referenceIdFieldName = fieldName + this.upperCaseFirstLetter(idFieldName);

if (!getAllFields(oppositeModel).find((f) => f.name === referenceIdFieldName)) {
referenceField = '\n' + indent + `${referenceIdFieldName} ${idField.type.type}`;
}

newText =
'\n' +
indent +
`${fieldName} ${typeName} @relation(fields: [${referenceIdFieldName}], references: [${idFieldName}])` +
referenceField +
'\n';
} else {
// user User @relation(fields: [userAbc], references: [id])
const typeName = container.name;
const fieldName = this.lowerCaseFirstLetter(typeName);
newText = '\n' + indent + `${fieldName} ${typeName}[]` + '\n';
}

// the opposite model might be in the imported file
const targetDocument = getDocument(oppositeModel);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const endOffset = oppositeModel.$cstNode!.end - 1;
const position = targetDocument.textDocument.positionAt(endOffset);

return {
title: `Add opposite relation fields on ${oppositeModel.name}`,
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
isPreferred: false,
edit: {
changes: {
[targetDocument.textDocument.uri]: [
{
range: {
start: position,
end: position,
},
newText,
},
],
},
},
};
}
}

return undefined;
}

private lowerCaseFirstLetter(str: string) {
return str.charAt(0).toLowerCase() + str.slice(1);
}

private upperCaseFirstLetter(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import {
TypeDef,
UnaryExpr,
type AstNode,
} from '@zenstackhq/language/ast';
import { resolved } from './model-utils';
} from './ast';
import { resolved } from './utils';

/**
* Options for the generator.
Expand Down
Loading
Loading