Skip to content

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:

ts
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.

ts
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
// @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.

ts
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:

ts
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

exportwhat 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, defaultCompletionQueriessensible defaults you can spread/override.

BackendOptions lets you override pieces without rebuilding the config by hand:

ts
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:

fieldrequiredmeaning
nameyesbackend identifier / label
urlyesSPARQL endpoint URL
defaultwhether it is the default backend
prefixMap{ prefix: namespace } used for prefix completion
queriescompletion-query templates, keyed by qlue-ls CompletionTemplate (subjectCompletion, predicateCompletionContextSensitive, objectCompletionContextSensitive, …). Needed for term completion. An empty object still gives prefix/keyword completion.
engine, requestMethod, healthCheckUrloptional

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:

ts
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:

  1. Replace qlue-ls.worker.ts / qlue-ls.ts with that server's worker and connection.
  2. Add it as another languageServers entry (its own worker/client), alongside or instead of qlue-ls.
  3. In that entry's onReady / onEndpointChange, send whatever that server needs to target an endpoint (its own custom requests) on the client you receive.

No changes to the @rdfjs/* packages are required.