Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 0 additions & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,4 @@ jobs:
- name: Execute verification
shell: bash
run: |
bash ./verify/buildPeers.sh
bash ./verify/buildAll.sh
4 changes: 3 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"recommendations": [
"davidanson.vscode-markdownlint",
"oxc.oxc-vscode",
"vitest.explorer"
"vitest.explorer",
"langium.langium-vscode",
"hverlin.mise-vscode"
]
}
95 changes: 41 additions & 54 deletions docs/guides/langium/custom-notifications-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,35 +42,27 @@ 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 = {
uri: string;
content: string;
diagnostics: Diagnostic[];
};
const documentChangeNotification = new NotificationType<DocumentChangePayload>(
'browser/DocumentChange'
);
const documentChangeNotification = new NotificationType<DocumentChangePayload>('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.
Expand Down Expand Up @@ -113,41 +105,36 @@ type DocumentChangePayload = {
content: string;
diagnostics: Diagnostic[];
};
const documentChangeNotification = new NotificationType<DocumentChangePayload>(
'browser/DocumentChange'
);
const documentChangeNotification = new NotificationType<DocumentChangePayload>('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
Expand Down Expand Up @@ -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.

Expand Down
138 changes: 66 additions & 72 deletions docs/guides/langium/running-langium-ls-in-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
]
};
```

Expand Down Expand Up @@ -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';

Expand All @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -418,7 +411,8 @@ To do that, we can set up a regular HTML page that provides the container elemen
<meta charset="utf-8" />
<title>My Language Editor</title>
<style>
html, body {
html,
body {
margin: 0;
padding: 0;
width: 100%;
Expand Down
10 changes: 5 additions & 5 deletions docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Whenever you use `monaco-editor`/`@codingame/monaco-vscode-editor-api` `vscode`/
If you use pnpm or yarn, you have to add `vscode` / `@codingame/monaco-vscode-api` as direct dependency, otherwise the installation will fail:

```json
"vscode": "npm:@codingame/monaco-vscode-extension-api@^34.0.1"
"vscode": "npm:@codingame/monaco-vscode-extension-api@^34.1.3"
```

### Missing Overrides or Resolutions
Expand All @@ -23,7 +23,7 @@ To ensure all Monaco-related packages use a single, compatible version, you must
```json
{
"overrides": {
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^34.0.1"
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^34.1.3"
}
}
```
Expand All @@ -33,7 +33,7 @@ To ensure all Monaco-related packages use a single, compatible version, you must
```json
{
"resolutions": {
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^34.0.1"
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^34.1.3"
}
}
```
Expand All @@ -50,7 +50,7 @@ Additionally, if you see a message in the browser console starting with `Another

### @codingame/monaco-vscode-editor-api / monaco-editor usage

When you use the libraries from this project you are no longer required to proxy `monaco-editor` like `"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^34.0.1"` in you `package.json`. You can directly use it like so:
When you use the libraries from this project you are no longer required to proxy `monaco-editor` like `"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^34.1.3"` in you `package.json`. You can directly use it like so:

```js
import * as monaco from '@codingame/monaco-vscode-editor-api';
Expand All @@ -60,7 +60,7 @@ If your dependency stack already contains a reference `monaco-editor` you must e

```json
"overrides": {
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^34.0.1"
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@^34.1.3"
}
```

Expand Down
Loading
Loading