Language server
Smart features, autocompletion, diagnostics, hover, formatting and semantic highlighting, come from a SPARQL language server (LSP) running in a Web Worker. Yasqe and SparqlStudio are language-server agnostic: you pass them a ready LSP Worker and they wire a monaco-languageclient to it.
The recommended server is qlue-ls, a fast WASM SPARQL language server. Yasqe ships the qlue-ls plumbing (settings, backend/endpoint registration, prefix discovery, completion-query templates and types) under the qlueLs namespace, so the only thing you write yourself is the WASM worker:
import { qlueLs } from "@rdfjs/sparql-editor-monaco"; // the Monaco editor; not re-exported from "@rdfjs/sparql-studio"The worker
qlue-ls is distributed as a WASM module; you wrap it in a Web Worker and resolve a factory once it signals it is ready. This is the only qlue-ls specific code you maintain (it depends on the qlue-ls package); everything else comes from the qlueLs helpers.
import QlueLsWorker from "./qlue-ls.worker?worker";
/** Create a qlue-ls worker and resolve once its WASM is ready. */
export function createQlueLsWorker(): Promise<Worker> {
return new Promise((resolve) => {
const worker = new QlueLsWorker({ name: "qlue-ls" });
worker.onmessage = (e) => {
if (e.data?.type === "ready") resolve(worker);
};
});
}// @ts-ignore qlue-ls is loaded as a WASM module via vite-plugin-wasm
import init, { init_language_server, listen } from "qlue-ls?init";
init().then(() => {
// Connection Worker <-> Language Server (WASM)
const wasmInputStream = new TransformStream();
const wasmOutputStream = new TransformStream();
const wasmReader = wasmOutputStream.readable.getReader();
const wasmWriter = wasmInputStream.writable.getWriter();
// Initialize and start the language server
const server = init_language_server(wasmOutputStream.writable.getWriter());
listen(server, wasmInputStream.readable.getReader());
// Language Client -> Language Server
self.onmessage = (message) => wasmWriter.write(JSON.stringify(message.data));
// Language Server -> Language Client
(async () => {
while (true) {
const { value, done } = await wasmReader.read();
if (done) break;
self.postMessage(JSON.parse(value));
}
})();
// Signal to the host that the WASM server is initialized and ready
self.postMessage({ type: "ready" });
});
export {};Hooking it up
Configure one or more servers through the languageServers array. Each entry has a label, the worker (instance or factory) and two optional per-server hooks — only the active server's hooks fire:
onReady(client, yasqe)— runs when that server becomes active (on load or when switched to). Use it to push settings and register the active endpoint as the default backend.onEndpointChange(client, endpoint, yasqe)— runs when the endpoint changes while that server is active. Use it to re-register the backend for the new endpoint.
The first entry is activated on load; with two or more configured, a switcher appears (right-click the editor in Monaco, a dropdown in CodeMirror) and the user's choice is remembered per endpoint.
import SparqlStudio from "@rdfjs/sparql-studio";
import Yasqe, { qlueLs } from "@rdfjs/sparql-editor-monaco";
import { createQlueLsWorker } from "./qlue-ls";
new SparqlStudio(el, {
// SparqlStudio is editor-independent: pass an editor factory and list the servers in the editor.
yasqe: (parent, conf) =>
new Yasqe(parent, {
...conf,
languageServers: [
{
label: "Qlue-ls",
description: "SPARQL language server with endpoint-powered completions",
worker: createQlueLsWorker,
onReady: (client) => {
qlueLs.configureSettings(client);
qlueLs.configureBackend(client, yasgui?.getTab()?.getEndpoint());
},
onEndpointChange: (client, endpoint) => qlueLs.configureBackend(client, endpoint),
},
],
}),
});Standalone Yasqe is the same — the per-server onReady (and onEndpointChange, which you can trigger yourself via yasqe.notifyEndpointChange(endpoint)) carry the setup:
import Yasqe, { qlueLs } from "@rdfjs/sparql-editor-monaco";
import { createQlueLsWorker } from "./qlue-ls";
new Yasqe(el, {
languageServers: [
{
label: "Qlue-ls",
worker: createQlueLsWorker,
onReady: (lc) => {
qlueLs.configureSettings(lc);
qlueLs.configureBackend(lc, "https://sparql.dblp.org/sparql");
},
},
],
});Per-server vs SparqlStudio-level
The per-server onEndpointChange only fires for the active server, so each server handles endpoints its own way. SparqlStudio still has a top-level onEndpointChange(yasgui, endpoint) for app-wide, server-independent work (analytics, UI). Both fire.
qlueLs.configureBackend is safe to call repeatedly (it skips re-registering the same endpoint on the same client). yasqe.getLanguageClient() returns the active monaco-languageclient, so you can also send any other LSP request or custom notification yourself.
Offering several servers
List more than one entry to let users switch at runtime (e.g. qlue-ls for QLever endpoints, another server for large Virtuoso ones). Each entry's worker is resolved lazily the first time it is activated, so unused servers are never started. The reserved configSchema / configCallback fields are placeholders for a future generic config UI and are not yet implemented.
The qlueLs helpers
| export | what it does |
|---|---|
configureBackend(client, endpoint, options?) | register endpoint as the default backend so completions resolve against it. Fetches the endpoint's prefixes when none are passed, and uses defaultCompletionQueries for term completion. |
configureSettings(client, settings?) | push server settings (formatting, completion, prefix handling). Defaults to defaultSettings. |
createBackendConf(endpoint, options?) | build a BackendConfiguration (fetching prefixes when not provided) without sending it. |
fetchPrefixMap(endpoint) | query the endpoint for sh:prefix / sh:namespace declarations, falling back to fallbackPrefixMap. |
defaultSettings, fallbackPrefixMap, defaultCompletionQueries | sensible defaults you can spread/override. |
BackendOptions lets you override pieces without rebuilding the config by hand:
qlueLs.configureBackend(lc, endpoint, {
prefixMap: { rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", ...qlueLs.fallbackPrefixMap },
queries: qlueLs.defaultCompletionQueries, // or your own CompletionTemplate map
engine: "QLever",
});The backend object
The qlue-ls BackendConfiguration (what createBackendConf builds) is flat and camelCase:
| field | required | meaning |
|---|---|---|
name | yes | backend identifier / label |
url | yes | SPARQL endpoint URL |
default | — | whether it is the default backend |
prefixMap | — | { prefix: namespace } used for prefix completion |
queries | — | completion-query templates, keyed by qlue-ls CompletionTemplate (subjectCompletion, predicateCompletionContextSensitive, objectCompletionContextSensitive, …). Needed for term completion. An empty object still gives prefix/keyword completion. |
engine, requestMethod, healthCheckUrl | — | optional |
Auto-discovering prefixes
configureBackend / createBackendConf call fetchPrefixMap for you when you don't pass a prefixMap: many endpoints expose their prefixes via sh:namespace / sh:prefix, and qlueLs falls back to fallbackPrefixMap (a broad set of common vocab prefixes) when none are returned.
CodeMirror editor (@rdfjs/sparql-editor-codemirror)
The Monaco editor (@rdfjs/sparql-editor-monaco) is the default, but SparqlStudio is editor-independent: you can build the editor factory around the CodeMirror 6 editor instead. Each languageServers entry takes a ready LSP client (rather than a worker) as client (instance or factory). You own the qlue-ls wiring (transport, pull diagnostics, semantic-token highlighting) and pass the resulting @codemirror/lsp-client LSPClient in:
import SparqlStudio from "@rdfjs/sparql-studio";
import Yasqe from "@rdfjs/sparql-editor-codemirror";
import { createQlueLsClient, setQlueLsBackend } from "./qlueLsClient";
new SparqlStudio(el, {
requestConfig: { endpoint },
yasqe: (parent, conf) =>
new Yasqe(parent, {
...conf,
languageServers: [
{
label: "Qlue-ls",
client: () => createQlueLsClient(), // resolved lazily on first activation
onReady: (client) => setQlueLsBackend(client, yasgui?.getTab()?.getEndpoint()),
onEndpointChange: (client, endpoint) => setQlueLsBackend(client, endpoint),
},
],
}),
});With two or more entries the editor shows a labelled switcher dropdown in its toolbar (left of the format/share/run buttons). The completion-query templates (defaultCompletionQueries) used by the client live in @rdfjs/sparql-utils. See dev/codemirror.html and dev/qlueLsClient.ts in the repo for the full qlue-ls wiring (the qlueLs helpers above are Monaco-specific and not used here).
Using a different language server
Yasqe and SparqlStudio only need a ready LSP Worker (Monaco) or LSPClient (CodeMirror). The qlueLs helpers are a convenience for qlue-ls; they are not required. To use, for example, swls instead:
- Replace
qlue-ls.worker.ts/qlue-ls.tswith that server's worker and connection. - Add it as another
languageServersentry (its ownworker/client), alongside or instead of qlue-ls. - In that entry's
onReady/onEndpointChange, send whatever that server needs to target an endpoint (its own custom requests) on theclientyou receive.
No changes to the @rdfjs/* packages are required.