Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
6 changes: 6 additions & 0 deletions packages/schema/build/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ require('esbuild')
.then(() => {
fs.cpSync('./src/res', 'bundle/res', { force: true, recursive: true });
fs.cpSync('../language/syntaxes', 'bundle/syntaxes', { force: true, recursive: true });

// Copy release notes HTML file
if (fs.existsSync('src/release-notes.html')) {
fs.copyFileSync('src/release-notes.html', 'bundle/release-notes.html');
console.log('Copied release notes HTML file to bundle');
}
})
.then(() => console.log(success))
.catch((err) => {
Expand Down
11 changes: 11 additions & 0 deletions packages/schema/build/post-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,14 @@ console.log('Updating file: dist/cli/index.js');
fs.writeFileSync('dist/cli/index.js', cliContent, {
encoding: 'utf-8',
});

// Copy release notes HTML file to dist
const releaseNotesSource = 'src/release-notes.html';
const releaseNotesDest = 'dist/release-notes.html';

if (fs.existsSync(releaseNotesSource)) {
console.log('Copying release notes HTML file to dist');
fs.copyFileSync(releaseNotesSource, releaseNotesDest);
} else {
console.warn('Release notes HTML file not found at:', releaseNotesSource);
}
69 changes: 63 additions & 6 deletions packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@
"linkDirectory": true
},
"engines": {
"vscode": "^1.63.0"
"vscode": "^1.90.0"
},
"categories": [
"Programming Languages"
],
"contributes": {
"languageModelTools": [
{
"name": "zmodel_mermaid_generator",
"displayName": "ZModel Mermaid Generator",
"modelDescription": "Generate Mermaid charts from ZModel schema files. This tool analyzes the current ZModel file and creates comprehensive entity-relationship diagrams showing all models and their relationships.",
"canBeReferencedInPrompt": true,
"toolReferenceName": "zmodel_mermaid_generator"
}
],
"languages": [
{
"id": "zmodel",
Expand Down Expand Up @@ -64,13 +73,60 @@
"type": "boolean",
"default": true,
"description": "Use Prisma style indentation."
},
"zenstack.searchForExtensions": {
"type": "boolean",
"default": true,
"description": "Search for Mermaid extensions when viewing Mermaid source."
}
}
}
},
"menus": {
"editor/title": [
{
"command": "zenstack.preview-zmodel",
"when": "editorLangId == zmodel",
"group": "navigation"
}
],
"commandPalette": [
{
"command": "zenstack.preview-zmodel",
"when": "editorLangId == zmodel"
},
{
"command": "zenstack.clear-documentation-cache"
}
]
},
"commands": [
{
"command": "zenstack.preview-zmodel",
"title": "ZenStack: Preview ZModel",
"icon": "$(preview)"
},
{
"command": "zenstack.clear-documentation-cache",
"title": "ZenStack: Clear Documentation Cache",
"icon": "$(trash)"
}
],
"keybindings": [
{
"command": "zenstack.preview-zmodel",
"key": "ctrl+shift+v",
"mac": "cmd+shift+v",
"when": "editorLangId == zmodel"
}
]
},
"activationEvents": [],
"capabilities": {
"untrustedWorkspaces": {
"supported": true
},
"virtualWorkspaces": true
},
"activationEvents": [
"onLanguage:zmodel"
],
"bin": {
"zenstack": "bin/cli"
},
Expand Down Expand Up @@ -127,7 +183,8 @@
"@types/strip-color": "^0.1.0",
"@types/tmp": "^0.2.3",
"@types/uuid": "^8.3.4",
"@types/vscode": "^1.56.0",
"@types/vscode": "^1.102.0",
"@vscode/chat-extension-utils": "0.0.0-alpha.5",
"@vscode/vsce": "^3.5.0",
"@zenstackhq/runtime": "workspace:*",
"dotenv": "^16.0.3",
Expand Down
153 changes: 153 additions & 0 deletions packages/schema/src/documentation-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as vscode from 'vscode';
import crypto from 'crypto';

// Cache entry interface
interface CacheEntry {
data: string;
timestamp: number;
extensionVersion: string;
}

/**
* DocumentationCache class handles persistent caching of ZModel documentation
* using VS Code's globalState for cross-session persistence
*/
export class DocumentationCache implements vscode.Disposable {
private static readonly CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes cache duration
private static readonly CACHE_PREFIX = 'doc-cache.';

private extensionContext: vscode.ExtensionContext;
private extensionVersion: string;

constructor(context: vscode.ExtensionContext) {
this.extensionContext = context;
this.extensionVersion = context.extension.packageJSON.version as string;
// clear expired cache entries on initialization
this.clearExpiredCache();
}

/**
* Dispose of the cache resources (implements vscode.Disposable)
*/
dispose(): void {}

/**
* Get the cache prefix used for keys
*/
getCachePrefix(): string {
return DocumentationCache.CACHE_PREFIX;
}

/**
* Enable cache synchronization across machines via VS Code Settings Sync
*/
private enableCacheSync(): void {
const cacheKeys = this.extensionContext.globalState
.keys()
.filter((key) => key.startsWith(DocumentationCache.CACHE_PREFIX));
if (cacheKeys.length > 0) {
this.extensionContext.globalState.setKeysForSync(cacheKeys);
}
}

/**
* Generate a cache key from request body with normalized content
*/
private generateCacheKey(requestBody: { models: string[] }): string {
// Remove ALL whitespace characters from each model string for cache key generation
// This ensures identical content with different formatting uses the same cache
const normalizedModels = requestBody.models.map((model) => model.replace(/\s/g, ''));
const hash = crypto
.createHash('sha512')
.update(JSON.stringify({ models: normalizedModels }))
.digest('hex');
return `${DocumentationCache.CACHE_PREFIX}${hash}`;
}

/**
* Check if cache entry is still valid (not expired)
*/
private isCacheValid(entry: CacheEntry): boolean {
return Date.now() - entry.timestamp < DocumentationCache.CACHE_DURATION_MS;
}

/**
* Get cached response if available and valid
*/
async getCachedResponse(requestBody: { models: string[] }): Promise<string | null> {
const cacheKey = this.generateCacheKey(requestBody);
const entry = this.extensionContext.globalState.get<CacheEntry>(cacheKey);

if (entry && this.isCacheValid(entry)) {
console.log('Using cached documentation response from persistent storage');
return entry.data;
}

// Clean up expired entry if it exists
if (entry) {
await this.extensionContext.globalState.update(cacheKey, undefined);
}

return null;
}

/**
* Cache a response for future use
*/
async setCachedResponse(requestBody: { models: string[] }, data: string): Promise<void> {
const cacheKey = this.generateCacheKey(requestBody);
const cacheEntry: CacheEntry = {
data,
timestamp: Date.now(),
extensionVersion: this.extensionVersion,
};

await this.extensionContext.globalState.update(cacheKey, cacheEntry);

// Update sync keys to include new cache entry
this.enableCacheSync();
}

/**
* Clear expired cache entries from persistent storage
*/
async clearExpiredCache(): Promise<void> {
const now = Date.now();
let clearedCount = 0;
const allKeys = this.extensionContext.globalState.keys();

for (const key of allKeys) {
if (key.startsWith(DocumentationCache.CACHE_PREFIX)) {
const entry = this.extensionContext.globalState.get<CacheEntry>(key);
if (
entry?.extensionVersion !== this.extensionVersion ||
now - entry.timestamp >= DocumentationCache.CACHE_DURATION_MS
) {
await this.extensionContext.globalState.update(key, undefined);
clearedCount++;
}
}
}

if (clearedCount > 0) {
console.log(`Cleared ${clearedCount} expired cache entries from persistent storage`);
}
}

/**
* Clear all cache entries from persistent storage
*/
async clearAllCache(): Promise<void> {
const allKeys = this.extensionContext.globalState.keys();
let clearedCount = 0;

for (const key of allKeys) {
if (key.startsWith(DocumentationCache.CACHE_PREFIX)) {
await this.extensionContext.globalState.update(key, undefined);
clearedCount++;
}
}

console.log(`Cleared all cache entries from persistent storage (${clearedCount} items)`);
}
}
38 changes: 38 additions & 0 deletions packages/schema/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
import * as vscode from 'vscode';
import * as path from 'path';

import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node';
import { AUTH_PROVIDER_ID, ZenStackAuthenticationProvider } from './zenstack-auth-provider';
import { DocumentationCache } from './documentation-cache';
import { ZModelPreview } from './zmodel-preview';
import { ReleaseNotesManager } from './release-notes-manager';

// Global variables
let client: LanguageClient;

// Utility to require authentication when needed
export async function requireAuth(): Promise<vscode.AuthenticationSession | undefined> {
let session: vscode.AuthenticationSession | undefined;

session = await vscode.authentication.getSession(AUTH_PROVIDER_ID, [], { createIfNone: false });

if (!session) {
const signIn = 'Sign in';
const selection = await vscode.window.showWarningMessage('Please sign in to use this feature', signIn);
if (selection === signIn) {
try {
session = await vscode.authentication.getSession(AUTH_PROVIDER_ID, [], { createIfNone: true });
if (session) {
vscode.window.showInformationMessage('ZenStack sign in successful!');
}
} catch (e) {
vscode.window.showErrorMessage('ZenStack sign in failed: ' + String(e));
}
}
}
return session;
}

// This function is called when the extension is activated.
export function activate(context: vscode.ExtensionContext): void {
// Initialize and register the ZenStack authentication provider
context.subscriptions.push(new ZenStackAuthenticationProvider(context));

// Start language client
client = startLanguageClient(context);

const documentationCache = new DocumentationCache(context);
context.subscriptions.push(documentationCache);
context.subscriptions.push(new ZModelPreview(context, client, documentationCache));
context.subscriptions.push(new ReleaseNotesManager(context));
}

// This function is called when the extension is deactivated.
Expand Down
48 changes: 48 additions & 0 deletions packages/schema/src/language-server/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,61 @@
import { startLanguageServer } from 'langium';
import { NodeFileSystem } from 'langium/node';
import { createConnection, ProposedFeatures } from 'vscode-languageserver/node';
import { URI } from 'vscode-uri';
import { createZModelServices } from './zmodel-module';
import { eagerLoadAllImports } from '../cli/cli-util';

// Create a connection to the client
const connection = createConnection(ProposedFeatures.all);

// Inject the shared services and language-specific services
const { shared } = createZModelServices({ connection, ...NodeFileSystem });

// Add custom LSP request handlers
connection.onRequest('zenstack/getAllImportedZModelURIs', async (params: { textDocument: { uri: string } }) => {
try {
const uri = URI.parse(params.textDocument.uri);
const document = shared.workspace.LangiumDocuments.getOrCreateDocument(uri);

// Ensure the document is parsed and built
if (!document.parseResult) {
await shared.workspace.DocumentBuilder.build([document]);
}

// #region merge imported documents
const langiumDocuments = shared.workspace.LangiumDocuments;

// load all imports
const importedURIs = eagerLoadAllImports(document, langiumDocuments);

const importedDocuments = importedURIs.map((uri) => langiumDocuments.getOrCreateDocument(uri));

// build the document together with standard library, plugin modules, and imported documents
await shared.workspace.DocumentBuilder.build([document, ...importedDocuments], {
validationChecks: 'all',
});

const hasSyntaxErrors = [uri, ...importedURIs].some((uri) => {
const doc = langiumDocuments.getOrCreateDocument(uri);
return (
doc.parseResult.lexerErrors.length > 0 ||
doc.parseResult.parserErrors.length > 0 ||
doc.diagnostics?.some((e) => e.severity === 1)
);
});

return {
hasSyntaxErrors,
importedURIs,
};
} catch (error) {
console.error('Error getting imported ZModel file:', error);
return {
hasSyntaxErrors: true,
importedURIs: [],
};
}
});

// Start the language server with the shared services
startLanguageServer(shared);
Loading
Loading