diff --git a/.vscode/launch.json b/.vscode/launch.json index 125b94cd2..ea6451fed 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -45,6 +45,28 @@ // Ignore all dependencies (optional) "${workspaceFolder}/node_modules/**" ] + }, + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/packages/ide/vscode" + ], + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/packages/ide/vscode/dist/**/*.js"] + }, + { + "name": "Attach to Language Server", + "type": "node", + "port": 6009, + "request": "attach", + "skipFiles": ["/**"], + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/packages/ide/vscode/dist/**/*.js", + "${workspaceFolder}/packages/ide/vscode/node_modules/langium" + ] } ] } diff --git a/package.json b/package.json index f1fa25cea..d9447d17d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "keywords": [], "author": "", - "license": "ISC", + "license": "MIT", "devDependencies": { "@swc/core": "^1.10.15", "@typescript-eslint/eslint-plugin": "~7.3.1", diff --git a/packages/ide/vscode/.gitignore b/packages/ide/vscode/.gitignore new file mode 100644 index 000000000..9a14bc7d6 --- /dev/null +++ b/packages/ide/vscode/.gitignore @@ -0,0 +1 @@ +syntaxes/ diff --git a/packages/ide/vscode/LICENSE b/packages/ide/vscode/LICENSE new file mode 120000 index 000000000..5853aaea5 --- /dev/null +++ b/packages/ide/vscode/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/packages/ide/vscode/asset/logo-256-bg.png b/packages/ide/vscode/asset/logo-256-bg.png new file mode 100644 index 000000000..8ef1fcf09 Binary files /dev/null and b/packages/ide/vscode/asset/logo-256-bg.png differ diff --git a/packages/ide/vscode/asset/logo-dark-256.png b/packages/ide/vscode/asset/logo-dark-256.png new file mode 100644 index 000000000..599faab72 Binary files /dev/null and b/packages/ide/vscode/asset/logo-dark-256.png differ diff --git a/packages/ide/vscode/asset/logo-light-256.png b/packages/ide/vscode/asset/logo-light-256.png new file mode 100644 index 000000000..7358d53b6 Binary files /dev/null and b/packages/ide/vscode/asset/logo-light-256.png differ diff --git a/packages/ide/vscode/language-configuration.json b/packages/ide/vscode/language-configuration.json new file mode 100644 index 000000000..4000da2d9 --- /dev/null +++ b/packages/ide/vscode/language-configuration.json @@ -0,0 +1,32 @@ +{ + "comments": { + // symbol used for single line comment. Remove this entry if your language does not support line comments + "lineComment": "//", + // symbols used for start and end a block comment. Remove this entry if your language does not support block comments + "blockComment": ["/*", "*/"] + }, + // symbols used as brackets + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + // symbols that are auto closed when typing + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"], + { "open": "/**", "close": " */", "notIn": ["string"] } + ], + // symbols that can be used to surround a selection + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\#\\%\\^\\&\\*\\-\\=\\+\\{\\}\\(\\)\\[\\]\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\s]+)" +} diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json new file mode 100644 index 000000000..b6c172003 --- /dev/null +++ b/packages/ide/vscode/package.json @@ -0,0 +1,88 @@ +{ + "name": "zenstack", + "publisher": "zenstack", + "version": "3.0.0", + "displayName": "ZenStack Language Tools", + "description": "VSCode extension for ZenStack ZModel language", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/zenstackhq/zenstack-v3" + }, + "scripts": { + "build": "tsc --noEmit && tsup", + "watch": "run-p watch:*", + "watch:tsc": "tsc --watch --noEmit", + "watch:tsup": "tsup --watch", + "lint": "eslint src --ext ts", + "vscode:publish": "pnpm build && vsce publish --no-dependencies --pre-release --follow-symlinks", + "vscode:package": "pnpm build && vsce package --no-dependencies" + }, + "homepage": "https://zenstack.dev", + "icon": "asset/logo-256-bg.png", + "keywords": [ + "fullstack", + "react", + "typescript", + "data modeling", + "prisma" + ], + "author": { + "name": "ZenStack Team" + }, + "license": "MIT", + "packageManager": "pnpm@10.12.1", + "dependencies": { + "langium": "~3.3.0", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1", + "@zenstackhq/language": "workspace:*" + }, + "devDependencies": { + "@types/vscode": "^1.63.0" + }, + "files": [ + "dist", + "res", + "syntaxes", + "asset", + "language-configuration.json" + ], + "engines": { + "vscode": "^1.63.0", + "node": ">=18.0.0" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "languages": [ + { + "id": "zmodel", + "aliases": [ + "ZenStack Model", + "zmodel" + ], + "extensions": [ + ".zmodel" + ], + "configuration": "./language-configuration.json", + "icon": { + "light": "./asset/logo-light-256.png", + "dark": "./asset/logo-dark-256.png" + } + } + ], + "grammars": [ + { + "language": "zmodel", + "scopeName": "source.zmodel", + "path": "./syntaxes/zmodel.tmLanguage.json" + } + ] + }, + "activationEvents": [ + "onLanguage:zmodel" + ], + "main": "./dist/extension.js" +} diff --git a/packages/ide/vscode/res b/packages/ide/vscode/res new file mode 120000 index 000000000..6ae424e99 --- /dev/null +++ b/packages/ide/vscode/res @@ -0,0 +1 @@ +../../language/res \ No newline at end of file diff --git a/packages/ide/vscode/src/extension/main.ts b/packages/ide/vscode/src/extension/main.ts new file mode 100644 index 000000000..189eecdb2 --- /dev/null +++ b/packages/ide/vscode/src/extension/main.ts @@ -0,0 +1,67 @@ +import * as path from 'node:path'; +import type * as vscode from 'vscode'; +import type { + LanguageClientOptions, + ServerOptions, +} from 'vscode-languageclient/node.js'; +import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; + +let client: LanguageClient; + +// This function is called when the extension is activated. +export function activate(context: vscode.ExtensionContext): void { + client = startLanguageClient(context); +} + +// This function is called when the extension is deactivated. +export function deactivate(): Thenable | undefined { + if (client) { + return client.stop(); + } + return undefined; +} + +function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { + const serverModule = context.asAbsolutePath( + path.join('dist', 'language-server.js') + ); + // The debug options for the server + // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging. + // By setting `process.env.DEBUG_BREAK` to a truthy value, the language server will wait until a debugger is attached. + const debugOptions = { + execArgv: [ + '--nolazy', + `--inspect${process.env['DEBUG_BREAK'] ? '-brk' : ''}=${ + process.env['DEBUG_SOCKET'] || '6009' + }`, + ], + }; + + // If the extension is launched in debug mode then the debug server options are used + // Otherwise the run options are used + const serverOptions: ServerOptions = { + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + options: debugOptions, + }, + }; + + // Options to control the language client + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: '*', language: 'zmodel' }], + }; + + // Create the language client and start the client. + const client = new LanguageClient( + 'zmodel', + 'ZModel', + serverOptions, + clientOptions + ); + + // Start the client. This will also launch the server + client.start(); + return client; +} diff --git a/packages/ide/vscode/src/language-server/main.ts b/packages/ide/vscode/src/language-server/main.ts new file mode 100644 index 000000000..6bd10e9e6 --- /dev/null +++ b/packages/ide/vscode/src/language-server/main.ts @@ -0,0 +1,19 @@ +import { createZModelLanguageServices } from '@zenstackhq/language'; +import { startLanguageServer } from 'langium/lsp'; +import { NodeFileSystem } from 'langium/node'; +import { + createConnection, + ProposedFeatures, +} from 'vscode-languageserver/node.js'; + +// Create a connection to the client +const connection = createConnection(ProposedFeatures.all); + +// Inject the shared services and language-specific services +const { shared } = createZModelLanguageServices({ + connection, + ...NodeFileSystem, +}); + +// Start the language server with the shared services +startLanguageServer(shared); diff --git a/packages/ide/vscode/syntaxes b/packages/ide/vscode/syntaxes new file mode 120000 index 000000000..68dd8d1f2 --- /dev/null +++ b/packages/ide/vscode/syntaxes @@ -0,0 +1 @@ +../../language/syntaxes \ No newline at end of file diff --git a/packages/ide/vscode/tsconfig.json b/packages/ide/vscode/tsconfig.json new file mode 100644 index 000000000..9402a34e6 --- /dev/null +++ b/packages/ide/vscode/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/ide/vscode/tsup.config.ts b/packages/ide/vscode/tsup.config.ts new file mode 100644 index 000000000..78380d7ec --- /dev/null +++ b/packages/ide/vscode/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + extension: 'src/extension/main.ts', + 'language-server': 'src/language-server/main.ts', + }, + outDir: 'dist', + splitting: false, + clean: true, + format: ['cjs'], + noExternal: [/^(?!vscode$)/], + external: ['vscode'], +}); diff --git a/packages/language/package.json b/packages/language/package.json index ff070f780..a143208cb 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -5,7 +5,8 @@ "license": "MIT", "author": "ZenStack Team", "files": [ - "dist" + "dist", + "res" ], "type": "module", "scripts": { @@ -20,12 +21,24 @@ }, "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } }, "./ast": { - "import": "./dist/ast.js", - "types": "./dist/ast.d.ts" + "import": { + "types": "./dist/ast.d.ts", + "default": "./dist/ast.js" + }, + "require": { + "types": "./dist/ast.d.cts", + "default": "./dist/ast.cjs" + } }, "./package.json": { "import": "./package.json", diff --git a/packages/language/src/index.ts b/packages/language/src/index.ts index f2dc07a39..102a04f93 100644 --- a/packages/language/src/index.ts +++ b/packages/language/src/index.ts @@ -43,16 +43,16 @@ export async function loadDocument( } // load standard library + + // isomorphic __dirname + const _dirname = + typeof __dirname !== 'undefined' + ? __dirname + : path.dirname(fileURLToPath(import.meta.url)); const stdLib = await services.shared.workspace.LangiumDocuments.getOrCreateDocument( URI.file( - path.resolve( - path.join( - path.dirname(fileURLToPath(import.meta.url)), - './res', - STD_LIB_MODULE_NAME - ) - ) + path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME)) ) ); diff --git a/packages/language/src/module.ts b/packages/language/src/module.ts index acfa3eb8e..0721285b4 100644 --- a/packages/language/src/module.ts +++ b/packages/language/src/module.ts @@ -1,4 +1,4 @@ -import { inject, type Module } from 'langium'; +import { inject, type DeepPartial, type Module } from 'langium'; import { createDefaultModule, createDefaultSharedModule, @@ -13,8 +13,9 @@ import { ZModelLanguageMetaData, } from './generated/module'; import { ZModelValidator, registerValidationChecks } from './validator'; -import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope'; import { ZModelLinker } from './zmodel-linker'; +import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope'; +import { ZModelWorkspaceManager } from './zmodel-workspace-manager'; export { ZModelLanguageMetaData }; /** @@ -51,6 +52,17 @@ export const ZModelLanguageModule: Module< }, }; +export type ZModelSharedServices = LangiumSharedServices; + +export const ZModelSharedModule: Module< + ZModelSharedServices, + DeepPartial +> = { + workspace: { + WorkspaceManager: (services) => new ZModelWorkspaceManager(services), + }, +}; + /** * Create the full set of services required by Langium. * @@ -74,7 +86,8 @@ export function createZModelLanguageServices( } { const shared = inject( createDefaultSharedModule(context), - ZModelGeneratedSharedModule + ZModelGeneratedSharedModule, + ZModelSharedModule ); const ZModelLanguage = inject( createDefaultModule({ shared }), diff --git a/packages/language/src/zmodel-workspace-manager.ts b/packages/language/src/zmodel-workspace-manager.ts new file mode 100644 index 000000000..dda053a0d --- /dev/null +++ b/packages/language/src/zmodel-workspace-manager.ts @@ -0,0 +1,83 @@ +import { + DefaultWorkspaceManager, + URI, + type AstNode, + type LangiumDocument, + type LangiumDocumentFactory, + type WorkspaceFolder, +} from 'langium'; +import type { LangiumSharedServices } from 'langium/lsp'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { STD_LIB_MODULE_NAME } from './constants'; + +export class ZModelWorkspaceManager extends DefaultWorkspaceManager { + private documentFactory: LangiumDocumentFactory; + + constructor(services: LangiumSharedServices) { + super(services); + this.documentFactory = services.workspace.LangiumDocumentFactory; + } + + protected override async loadAdditionalDocuments( + folders: WorkspaceFolder[], + collector: (document: LangiumDocument) => void + ): Promise { + await super.loadAdditionalDocuments(folders, collector); + + // load stdlib.zmodel + let stdLibPath: string; + + // First, try to find the stdlib from an installed zenstack package + // in the project's node_modules + let installedStdlibPath: string | undefined; + for (const folder of folders) { + const folderPath = this.getRootFolder(folder).fsPath; + try { + // Try to resolve zenstack from the workspace folder + const languagePackagePath = require.resolve( + '@zenstackhq/language/package.json', + { + paths: [folderPath], + } + ); + const languagePackageDir = path.dirname(languagePackagePath); + const candidateStdlibPath = path.join( + languagePackageDir, + 'res', + STD_LIB_MODULE_NAME + ); + + // Check if the stdlib file exists in the installed package + if (fs.existsSync(candidateStdlibPath)) { + installedStdlibPath = candidateStdlibPath; + console.log( + `Found installed zenstack package stdlib at: ${installedStdlibPath}` + ); + break; + } + } catch (error) { + // Package not found or other error, continue to next folder + continue; + } + } + + if (installedStdlibPath) { + stdLibPath = installedStdlibPath; + } else { + // Fallback to bundled stdlib + // isomorphic __dirname + const _dirname = + typeof __dirname !== 'undefined' + ? __dirname + : path.dirname(fileURLToPath(import.meta.url)); + + stdLibPath = path.join(_dirname, '../res', STD_LIB_MODULE_NAME); + console.log(`Using bundled stdlib in extension:`, stdLibPath); + } + + const stdlib = await this.documentFactory.fromUri(URI.file(stdLibPath)); + collector(stdlib); + } +} diff --git a/packages/language/tsup.config.ts b/packages/language/tsup.config.ts index a28df8752..a6af92cea 100644 --- a/packages/language/tsup.config.ts +++ b/packages/language/tsup.config.ts @@ -1,5 +1,4 @@ import { defineConfig } from 'tsup'; -import fs from 'node:fs'; export default defineConfig({ entry: { @@ -11,8 +10,5 @@ export default defineConfig({ sourcemap: true, clean: true, dts: true, - format: ['esm'], - async onSuccess() { - fs.cpSync('./res', './dist/res', { recursive: true }); - }, + format: ['esm', 'cjs'], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4432b4afe..2d544c488 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,6 +125,25 @@ importers: specifier: ^20.0.0 version: 20.17.24 + packages/ide/vscode: + dependencies: + '@zenstackhq/language': + specifier: workspace:* + version: link:../../language + langium: + specifier: ~3.3.0 + version: 3.3.0 + vscode-languageclient: + specifier: ^9.0.1 + version: 9.0.1 + vscode-languageserver: + specifier: ^9.0.1 + version: 9.0.1 + devDependencies: + '@types/vscode': + specifier: ^1.63.0 + version: 1.101.0 + packages/language: dependencies: langium: @@ -1078,6 +1097,9 @@ packages: '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} + '@types/vscode@1.101.0': + resolution: {integrity: sha512-ZWf0IWa+NGegdW3iU42AcDTFHWW7fApLdkdnBqwYEtHVIBGbTu0ZNQKP/kX3Ds/uMJXIMQNAojHR4vexCEEz5Q==} + '@typescript-eslint/eslint-plugin@7.3.1': resolution: {integrity: sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==} engines: {node: ^18.18.0 || >=20.0.0} @@ -2035,6 +2057,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -2893,6 +2919,10 @@ packages: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} + vscode-languageclient@9.0.1: + resolution: {integrity: sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==} + engines: {vscode: ^1.82.0} + vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} @@ -3487,6 +3517,8 @@ snapshots: '@types/tmp@0.2.6': {} + '@types/vscode@1.101.0': {} + '@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -4633,6 +4665,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.3: dependencies: brace-expansion: 2.0.1 @@ -5522,6 +5558,12 @@ snapshots: vscode-jsonrpc@8.2.0: {} + vscode-languageclient@9.0.1: + dependencies: + minimatch: 5.1.6 + semver: 7.6.3 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-protocol@3.17.5: dependencies: vscode-jsonrpc: 8.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bf16bb42c..1afd55804 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,5 +4,6 @@ packages: - packages/runtime - packages/cli - packages/create-zenstack + - packages/ide/** - packages/testtools - samples/**