Use in the browser¶
The @postgres-language-server/wasm package compiles the language server to WebAssembly, so it runs entirely in the browser, without a server process. It provides two APIs:
| API | Use case | Import path |
|---|---|---|
| Workspace | Call parse, lint, format, complete and hover directly | @postgres-language-server/wasm/workspace |
| LanguageServer | Handle LSP JSON-RPC messages, for editors with an LSP client | @postgres-language-server/wasm/lsp |
Each API manages its own workspace. Use one or the other, not both. This guide uses the LanguageServer API to connect an editor. The package README documents the Workspace API.
npm install @postgres-language-server/wasm
The browser build cannot connect to a database. Schema-aware features, such as completing table and column names, use a schema that you export once and pass to the language server (see Providing the database schema).
The LanguageServer API supports these LSP methods:
initialize,initialized,shutdowntextDocument/didOpen,textDocument/didChange(full document sync),textDocument/didClosetextDocument/completiontextDocument/hovertextDocument/publishDiagnostics, sent after a document is opened or changed, and for all open documents after the schema is setpgls/setSchemaandpgls/clearSchema(custom notifications)
For formatting, use the format function of the package (see Formatting).
Run the language server in a web worker¶
Run the language server in a web worker, so that parsing and linting do not block the editor. handleMessage takes one JSON-RPC message, as an object or a string, and returns the outgoing messages: the response, if any, and notifications such as textDocument/publishDiagnostics. Post each of them back separately:
// pgls-worker.ts
import { createLanguageServer } from "@postgres-language-server/wasm/lsp";
const languageServer = createLanguageServer();
self.onmessage = async (event: MessageEvent) => {
const lsp = await languageServer;
for (const message of lsp.handleMessage(event.data)) {
self.postMessage(message);
}
};
// Signal the main thread that the WASM module is loaded
languageServer.then(() => self.postMessage({ type: "ready" }));
In the browser, the package loads pgls.wasm from new URL("./pgls.wasm", import.meta.url), next to its own modules. Make sure your bundler or server serves the file there.
Monaco¶
monaco-languageclient can talk to the worker through BrowserMessageReader and BrowserMessageWriter, which exchange one message per postMessage:
import { MonacoLanguageClient } from "monaco-languageclient";
import {
BrowserMessageReader,
BrowserMessageWriter,
} from "vscode-languageserver-protocol/browser";
const worker = new Worker(new URL("./pgls-worker.ts", import.meta.url), { type: "module" });
// Wait until the WASM module is loaded
await new Promise<void>((resolve) => {
worker.onmessage = (event) => {
if (event.data.type === "ready") resolve();
};
});
const reader = new BrowserMessageReader(worker);
const writer = new BrowserMessageWriter(worker);
const languageClient = new MonacoLanguageClient({
name: "Postgres Language Server",
clientOptions: {
documentSelector: [{ language: "sql" }],
},
messageTransports: { reader, writer },
});
languageClient.start();
How Monaco and its services are initialised depends on the version of monaco-languageclient; follow its documentation for that part.
CodeMirror¶
Any LSP client for CodeMirror works with the same worker. @codemirror/lsp-client talks to the server through a Transport that sends and receives messages as strings:
import { LSPClient, languageServerExtensions, type Transport } from "@codemirror/lsp-client";
import { basicSetup, EditorView } from "codemirror";
import { sql } from "@codemirror/lang-sql";
function workerTransport(worker: Worker): Transport {
let handlers: ((message: string) => void)[] = [];
worker.addEventListener("message", (event) => {
// Skip the ready signal, forward the JSON-RPC messages
if (event.data.jsonrpc !== "2.0") return;
const message = JSON.stringify(event.data);
for (const handler of handlers) handler(message);
});
return {
send(message) {
worker.postMessage(message);
},
subscribe(handler) {
handlers.push(handler);
},
unsubscribe(handler) {
handlers = handlers.filter((h) => h !== handler);
},
};
}
const worker = new Worker(new URL("./pgls-worker.ts", import.meta.url), { type: "module" });
const client = new LSPClient({ extensions: languageServerExtensions() }).connect(
workerTransport(worker),
);
new EditorView({
extensions: [basicSetup, sql(), client.plugin("file:///query.sql")],
parent: document.body,
});
Providing the database schema¶
Export the schema of your database to JSON with the CLI:
postgres-language-server schema-export \
--connection-string postgres://postgres:postgres@localhost:5432/postgres \
--output schema.json
Then send its content to the language server with the pgls/setSchema notification. The schema parameter is the JSON as a string:
const schema = await fetch("/schema.json").then((response) => response.text());
// monaco-languageclient
await languageClient.sendNotification("pgls/setSchema", { schema });
// @codemirror/lsp-client
client.notification("pgls/setSchema", { schema });
The pgls/clearSchema notification removes the schema again. The TypeScript type of the schema is exported as SchemaCache from @postgres-language-server/wasm.