diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5d1ff2003..7186fadfe 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -31,5 +31,4 @@ jobs: - name: Execute verification shell: bash run: | - bash ./verify/buildPeers.sh bash ./verify/buildAll.sh diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0ad98d780..57d2b28af 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,8 @@ "recommendations": [ "davidanson.vscode-markdownlint", "oxc.oxc-vscode", - "vitest.explorer" + "vitest.explorer", + "langium.langium-vscode", + "hverlin.mise-vscode" ] } diff --git a/docs/guides/langium/custom-notifications-requests.md b/docs/guides/langium/custom-notifications-requests.md index 401b01f90..f5bad9488 100644 --- a/docs/guides/langium/custom-notifications-requests.md +++ b/docs/guides/langium/custom-notifications-requests.md @@ -42,10 +42,7 @@ In our Langium language server's browser entry point (`main-browser.ts`), we hoo ```ts import { DocumentState, type LangiumDocument } from 'langium'; -import { - Diagnostic, - NotificationType -} from 'vscode-languageserver/browser'; +import { Diagnostic, NotificationType } from 'vscode-languageserver/browser'; // define the notification type and its payload shape type DocumentChangePayload = { @@ -53,24 +50,19 @@ type DocumentChangePayload = { content: string; diagnostics: Diagnostic[]; }; -const documentChangeNotification = new NotificationType( - 'browser/DocumentChange' -); +const documentChangeNotification = new NotificationType('browser/DocumentChange'); // listen for documents that have completed validation -shared.workspace.DocumentBuilder.onBuildPhase( - DocumentState.Validated, - (documents: LangiumDocument[]) => { - for (const document of documents) { - // build whatever payload your language needs - // can be any json serializable object - const payload = buildPayload(document); - - // send the notification to the client - connection.sendNotification(documentChangeNotification, payload); - } +shared.workspace.DocumentBuilder.onBuildPhase(DocumentState.Validated, (documents: LangiumDocument[]) => { + for (const document of documents) { + // build whatever payload your language needs + // can be any json serializable object + const payload = buildPayload(document); + + // send the notification to the client + connection.sendNotification(documentChangeNotification, payload); } -); +}); ``` The notification type string (`'browser/DocumentChange'`) is arbitrary, it just needs to match between server and client. The payload can be any JSON-serializable object. Additionally, notifications can be sent in response to anything else we want to respond to, not just a build phase callback. @@ -113,41 +105,36 @@ type DocumentChangePayload = { content: string; diagnostics: Diagnostic[]; }; -const documentChangeNotification = new NotificationType( - 'browser/DocumentChange' -); +const documentChangeNotification = new NotificationType('browser/DocumentChange'); // MiniLogo is the Langium-generated services object from createMiniLogoServices() const jsonSerializer = MiniLogo.serializer.JsonSerializer; -shared.workspace.DocumentBuilder.onBuildPhase( - DocumentState.Validated, - (documents: LangiumDocument[]) => { - for (const document of documents) { - const model = document.parseResult.value as Model; - let commands: Command[] = []; - - // only generate commands when there are no errors - const hasErrors = document.diagnostics?.some((d) => d.severity === 1) ?? false; - if (!hasErrors) { - commands = generateStatements(model.stmts); - } - - // attach the generated commands to the model for serialization - (model as unknown as { $commands: Command[] }).$commands = commands; - - // send a notification with a model + commands attached - connection.sendNotification(documentChangeNotification, { - uri: document.uri.toString(), - content: jsonSerializer.serialize(model, { - sourceText: true, - textRegions: true - }), - diagnostics: document.diagnostics ?? [] - }); +shared.workspace.DocumentBuilder.onBuildPhase(DocumentState.Validated, (documents: LangiumDocument[]) => { + for (const document of documents) { + const model = document.parseResult.value as Model; + let commands: Command[] = []; + + // only generate commands when there are no errors + const hasErrors = document.diagnostics?.some((d) => d.severity === 1) ?? false; + if (!hasErrors) { + commands = generateStatements(model.stmts); } + + // attach the generated commands to the model for serialization + (model as unknown as { $commands: Command[] }).$commands = commands; + + // send a notification with a model + commands attached + connection.sendNotification(documentChangeNotification, { + uri: document.uri.toString(), + content: jsonSerializer.serialize(model, { + sourceText: true, + textRegions: true + }), + diagnostics: document.diagnostics ?? [] + }); } -); +}); ``` #### Client Side @@ -312,12 +299,12 @@ This assumes `client` was obtained from `lcWrapper.getLanguageClient()` during s The following is a quick table to summarize when it makes sense to use notifications or requests, in terms of their tradeoffs. -| | Notifications | Requests | -|---|---|---| -| **Direction** | One-way | Round trip | -| **Response** | None (fire-and-forget) | Awaited response | -| **Use case** | Continuous, semi-frequent updates | On-demand, as needed | -| **Example** | Push logs or generated output | Generate output from a program on click | +| | Notifications | Requests | +| ------------- | --------------------------------- | --------------------------------------- | +| **Direction** | One-way | Round trip | +| **Response** | None (fire-and-forget) | Awaited response | +| **Use case** | Continuous, semi-frequent updates | On-demand, as needed | +| **Example** | Push logs or generated output | Generate output from a program on click | Both patterns can go in either direction, i.e clients can send notifications, and servers can send requests. LSP itself uses both directions heavily (ex. `textDocument/didOpen` is a client-to-server notification, `window/showMessageRequest` is a server-to-client request). Depending on what your're trying to set up, you may want to flip things around. diff --git a/docs/guides/langium/running-langium-ls-in-browser.md b/docs/guides/langium/running-langium-ls-in-browser.md index 413722a40..0077c3c05 100644 --- a/docs/guides/langium/running-langium-ls-in-browser.md +++ b/docs/guides/langium/running-langium-ls-in-browser.md @@ -31,11 +31,7 @@ Create a file `src/language-server/main-browser.ts` in your Langium project: ```ts import { EmptyFileSystem } from 'langium'; import { startLanguageServer } from 'langium/lsp'; -import { - BrowserMessageReader, - BrowserMessageWriter, - createConnection -} from 'vscode-languageserver/browser'; +import { BrowserMessageReader, BrowserMessageWriter, createConnection } from 'vscode-languageserver/browser'; // your services import will differ based on your language import { createMyLanguageServices } from './my-language-module.js'; @@ -111,10 +107,7 @@ As noted before, we're using `--format=esm` to produce an ES module worker. This If our client application uses Vite, we can also consume the LS without needing to pre-bundle it. Vite can bundle the worker inline when we reference it with `import.meta.url`: ```ts -const worker = new Worker( - new URL('./path/to/main-browser.ts', import.meta.url), - { type: 'module', name: 'MyLanguageServer' } -); +const worker = new Worker(new URL('./path/to/main-browser.ts', import.meta.url), { type: 'module', name: 'MyLanguageServer' }); ``` Vite will automatically bundle the worker entry point and its dependencies at build time. This approach is actually used by most of the examples in this repository as well, so there's plenty of reference material here. @@ -124,10 +117,7 @@ Vite will automatically bundle the worker entry point and its dependencies at bu The MiniLogo example in this repository takes a slightly different approach. We consume a **pre-built** language server worker from the [`langium-minilogo`](https://github.com/TypeFox/langium-minilogo) npm package rather than building from source. The package provides a `ls-web` export endpoint that gives us a pre-bundled ESM language server ready to load as a Web Worker: ```ts -const worker = new Worker( - new URL('langium-minilogo/ls-web', import.meta.url), - { type: 'module', name: 'MiniLogo Language Server' } -); +const worker = new Worker(new URL('langium-minilogo/ls-web', import.meta.url), { type: 'module', name: 'MiniLogo Language Server' }); ``` This allows us to depend on the LS as a standalone artifact without needing to bundle it ourselves. However, rest assured the process outlined above is how that bundle is produced. You can check out the [langium-minilogo](https://github.com/TypeFox/langium-minilogo) project to see exactly how it's done, and you can see the working [MiniLogo example](../../../packages/examples/src/langium/langium-dsl/minilogo/) in this repository for the complete client-side integration. @@ -147,10 +137,7 @@ The client setup involves three parts: First, we can create the Web Worker by pointing at our bundled language server, wherever that may be: ```ts -const worker = new Worker( - new URL('./worker/my-language-server-bundle.js', import.meta.url), - { type: 'module', name: 'MyLanguageServer' } -); +const worker = new Worker(new URL('./worker/my-language-server-bundle.js', import.meta.url), { type: 'module', name: 'MyLanguageServer' }); ``` In a Vite project, we can reference the source file directly as well, as noted in the prior section. @@ -184,30 +171,36 @@ extensionFilesOrContents.set('/my-language-grammar.json', textmateGrammar); const vscodeApiConfig: MonacoVscodeApiConfig = { $type: 'extended', // ... other config (see full example below) - extensions: [{ - config: { - name: 'my-language-example', - publisher: 'my-org', - version: '1.0.0', - engines: { vscode: '*' }, - contributes: { - languages: [{ - id: 'my-language', - extensions: ['.mylang'], - aliases: ['MyLanguage'], - // should match the path above - configuration: '/my-language-configuration.json' - }], - grammars: [{ - language: 'my-language', - scopeName: 'source.my-language', - // should match the path above - path: '/my-language-grammar.json' - }] - } - }, - filesOrContents: extensionFilesOrContents - }] + extensions: [ + { + config: { + name: 'my-language-example', + publisher: 'my-org', + version: '1.0.0', + engines: { vscode: '*' }, + contributes: { + languages: [ + { + id: 'my-language', + extensions: ['.mylang'], + aliases: ['MyLanguage'], + // should match the path above + configuration: '/my-language-configuration.json' + } + ], + grammars: [ + { + language: 'my-language', + scopeName: 'source.my-language', + // should match the path above + path: '/my-language-grammar.json' + } + ] + } + }, + filesOrContents: extensionFilesOrContents + } + ] }; ``` @@ -256,10 +249,7 @@ import { LogLevel } from '@codingame/monaco-vscode-api'; import getKeybindingsServiceOverride from '@codingame/monaco-vscode-keybindings-service-override'; import { EditorApp, type EditorAppConfig } from 'monaco-languageclient/editorApp'; import { LanguageClientWrapper, type LanguageClientConfig } from 'monaco-languageclient/lcwrapper'; -import { - MonacoVscodeApiWrapper, - type MonacoVscodeApiConfig -} from 'monaco-languageclient/vscodeApiWrapper'; +import { MonacoVscodeApiWrapper, type MonacoVscodeApiConfig } from 'monaco-languageclient/vscodeApiWrapper'; import { configureDefaultWorkerFactory } from 'monaco-languageclient/workerFactory'; import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageclient/browser'; @@ -269,10 +259,7 @@ import textmateGrammar from './syntaxes/my-language.tmLanguage.json?raw'; async function startEditor() { // 1. create the language server worker - const worker = new Worker( - new URL('./worker/my-language-server.js', import.meta.url), - { type: 'module', name: 'MyLanguageServer' } - ); + const worker = new Worker(new URL('./worker/my-language-server.js', import.meta.url), { type: 'module', name: 'MyLanguageServer' }); const reader = new BrowserMessageReader(worker); const writer = new BrowserMessageWriter(worker); @@ -303,28 +290,34 @@ async function startEditor() { 'editor.experimental.asyncTokenization': true }) }, - extensions: [{ - config: { - name: 'my-language-example', - publisher: 'my-org', - version: '1.0.0', - engines: { vscode: '*' }, - contributes: { - languages: [{ - id: languageId, - extensions: ['.mylang'], - aliases: ['MyLanguage'], - configuration: '/my-language-configuration.json' - }], - grammars: [{ - language: languageId, - scopeName: 'source.my-language', - path: '/my-language-grammar.json' - }] - } - }, - filesOrContents: extensionFilesOrContents - }] + extensions: [ + { + config: { + name: 'my-language-example', + publisher: 'my-org', + version: '1.0.0', + engines: { vscode: '*' }, + contributes: { + languages: [ + { + id: languageId, + extensions: ['.mylang'], + aliases: ['MyLanguage'], + configuration: '/my-language-configuration.json' + } + ], + grammars: [ + { + language: languageId, + scopeName: 'source.my-language', + path: '/my-language-grammar.json' + } + ] + } + }, + filesOrContents: extensionFilesOrContents + } + ] }; // 4. configure the language client @@ -418,7 +411,8 @@ To do that, we can set up a regular HTML page that provides the container elemen My Language Editor