Skip to content

LSP API

Use this API to register and manage language servers for Acode's CodeMirror LSP integration.

Import

js
const lsp = acode.require("lsp");

Current API Shape

The public API is intentionally small:

  • lsp.defineServer(...)
  • lsp.defineBundle(...)
  • lsp.register(entry, options?)
  • lsp.upsert(entry)
  • lsp.installers.*
  • lsp.servers.*
  • lsp.bundles.*

bundle is the public name for what the internal runtime still calls a provider:

  • a bundle can own one or more server definitions
  • a bundle can also provide install/check behavior hooks
  • most plugins only need a single server
  • use a bundle when you ship a family of related servers or custom install logic

Transport Reality

Acode's LSP client still speaks WebSocket to the transport layer.

  • transport.kind: "websocket" is the normal and recommended setup
  • local stdio servers should usually be launched through launcher.bridge
  • transport.kind: "stdio" still expects a WebSocket bridge URL
  • transport.kind: "external" is available for custom transport factories

For local servers, prefer transport.kind: "websocket" plus an AXS bridge.

WARNING

transport.kind: "stdio" is not a direct pipe from the editor to the server. It still resolves to the WebSocket transport layer and requires a bridge URL.

Use defineServer() and upsert() for idempotent registration.

js
const lsp = acode.require("lsp");

const typescriptServer = lsp.defineServer({
  id: "typescript-custom",
  label: "TypeScript (Custom)",
  languages: [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "tsx",
    "jsx",
  ],
  useWorkspaceFolders: true,
  transport: {
    kind: "websocket",
  },
  command: "typescript-language-server",
  args: ["--stdio"],
  checkCommand: "which typescript-language-server",
  installer: lsp.installers.npm({
    executable: "typescript-language-server",
    packages: ["typescript-language-server", "typescript"],
  }),
  initializationOptions: {
    provideFormatter: true,
  },
});

lsp.upsert(typescriptServer);

Bundle Setup

Use a bundle when one plugin contributes multiple servers or needs custom install behavior.

js
const lsp = acode.require("lsp");

const htmlServer = lsp.defineServer({
  id: "my-html",
  label: "My HTML Server",
  languages: ["html"],
  transport: {
    kind: "websocket",
  },
  command: "vscode-html-language-server",
  args: ["--stdio"],
  installer: lsp.installers.npm({
    executable: "vscode-html-language-server",
    packages: ["vscode-langservers-extracted"],
  }),
});

const cssServer = lsp.defineServer({
  id: "my-css",
  label: "My CSS Server",
  languages: ["css", "scss", "less"],
  transport: {
    kind: "websocket",
  },
  command: "vscode-css-language-server",
  args: ["--stdio"],
  installer: lsp.installers.npm({
    executable: "vscode-css-language-server",
    packages: ["vscode-langservers-extracted"],
  }),
});

const webBundle = lsp.defineBundle({
  id: "my-web-bundle",
  label: "My Web Bundle",
  servers: [htmlServer, cssServer],
});

lsp.upsert(webBundle);

Bundle Hooks

Bundles can own behavior, not just server lists.

Available hooks:

  • getExecutable(serverId, manifest)
  • checkInstallation(serverId, manifest)
  • installServer(serverId, manifest, mode, options?)

Example:

js
const bundle = lsp.defineBundle({
  id: "my-bundle",
  label: "My Bundle",
  servers: [myServer],
  hooks: {
    getExecutable(serverId, manifest) {
      return manifest.launcher?.install?.binaryPath || null;
    },
    async checkInstallation(serverId, manifest) {
      return {
        status: "present",
        version: null,
        canInstall: true,
        canUpdate: true,
      };
    },
    async installServer(serverId, manifest, mode) {
      console.log("install", serverId, mode);
      return true;
    },
  },
});

Structured Installers

Prefer structured installers over raw shell whenever possible.

Available installer builders:

  • lsp.installers.apk(...)
  • lsp.installers.npm(...)
  • lsp.installers.pip(...)
  • lsp.installers.cargo(...)
  • lsp.installers.githubRelease(...)
  • lsp.installers.manual(...)
  • lsp.installers.shell(...)

Example:

js
const server = lsp.defineServer({
  id: "python-custom",
  label: "Python (pylsp)",
  languages: ["python"],
  command: "pylsp",
  installer: lsp.installers.pip({
    executable: "pylsp",
    packages: ["python-lsp-server[all]"],
  }),
});

Installer Notes

  • managed installers should declare the executable they provide
  • githubRelease() is intended for arch-aware downloaded binaries
  • manual() is useful when the binary already exists at a known path
  • shell() should be treated as the advanced fallback, not the default path

Remote WebSocket Server

js
lsp.upsert({
  id: "remote-json-lsp",
  label: "Remote JSON LSP",
  languages: ["json"],
  transport: {
    kind: "websocket",
    url: "ws://127.0.0.1:2087/",
    options: {
      binary: true,
      timeout: 5000,
    },
  },
  enabled: true,
});

Custom URI Translation

Use rootUri and documentUri when the server does not see the same filesystem layout as Acode.

Typical cases:

  • the server runs in Termux
  • the server runs behind a remote WebSocket bridge
  • the editor opens files as content://... but the server expects file://...
  • the default cache-file fallback does not point at the real project path

rootUri controls the workspace root sent during initialize and workspace folder handling.

documentUri controls the URI used for opened documents, changes, formatting, and similar file-scoped LSP requests.

Both hooks may be synchronous or async.

documentUri(uri, context) receives:

  • uri: the original file URI known to Acode
  • context.normalizedUri: Acode's default normalized URI, including content:// -> file:// conversion or cache fallback when available
  • the same context fields available to rootUri, such as file, view, languageId, and rootUri

Example:

js
const lsp = acode.require("lsp");

const termuxWorkspaceUri =
  "file:///data/data/com.termux/files/home/projects/my-project";

function toTermuxDocumentUri(uri, fallbackUri) {
  if (typeof uri !== "string") return fallbackUri || null;

  if (uri.startsWith("file:///storage/emulated/0/")) {
    return uri.replace(
      "file:///storage/emulated/0/",
      "file:///data/data/com.termux/files/home/storage/shared/",
    );
  }

  return fallbackUri || uri;
}

const termuxServer = lsp.defineServer({
  id: "termux-typescript",
  label: "TypeScript (Termux)",
  languages: [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "tsx",
    "jsx",
  ],
  useWorkspaceFolders: true,
  transport: {
    kind: "websocket",
    url: "ws://127.0.0.1:2087/",
  },
  rootUri() {
    return termuxWorkspaceUri;
  },
  documentUri(uri, context) {
    return toTermuxDocumentUri(uri, context.normalizedUri);
  },
});

lsp.upsert(termuxServer);

Definition API

lsp.defineServer(options)

Builds a normalized server manifest for later registration.

Common fields:

  • id: required, normalized to lowercase by the registry
  • label: optional display label
  • languages: required non-empty array
  • enabled: defaults to true
  • transport
  • command and args: used to create an AXS launcher bridge
  • installer: structured installer config
  • checkCommand
  • versionCommand
  • updateCommand
  • initializationOptions
  • clientConfig
  • startupTimeout
  • capabilityOverrides
  • rootUri: optional workspace-root resolver; if provided it takes precedence over Acode's default root detection
  • documentUri: optional document URI resolver for translating file paths before they are sent to the server
  • resolveLanguageId
  • useWorkspaceFolders

lsp.defineBundle(options)

Creates a bundle record.

Fields:

  • id: required bundle id
  • label: optional
  • servers: array returned by lsp.defineServer(...)
  • hooks?: optional behavioral hooks

Registration API

lsp.register(entry, options?)

Registers either a server or bundle if the id is free.

  • options.replace?: boolean defaults to false

lsp.upsert(entry)

Registers or replaces either a server or bundle. This is the preferred method for plugin startup code.

Server Inspection API

  • lsp.servers.get(id)
  • lsp.servers.list()
  • lsp.servers.listForLanguage(languageId, options?)
  • lsp.servers.update(id, updater)
  • lsp.servers.unregister(id)
  • lsp.servers.onChange(listener)

Example:

js
const jsServers = lsp.servers.listForLanguage("javascript");

lsp.servers.update("typescript-custom", (current) => ({
  ...current,
  enabled: false,
}));

listForLanguage() options:

  • includeDisabled?: boolean default false

Bundle Inspection API

  • lsp.bundles.list()
  • lsp.bundles.getForServer(serverId)
  • lsp.bundles.unregister(id)

Client Manager

  • lsp.clientManager.setOptions(options)
  • lsp.clientManager.getActiveClients()
js
lsp.clientManager.setOptions({
  diagnosticsUiExtension: [],
});

const activeClients = lsp.clientManager.getActiveClients();
console.log(activeClients);

Best Practices

  • Prefer lsp.upsert(...) during plugin init.
  • Prefer defineServer() and defineBundle() instead of hand-assembling objects everywhere.
  • Prefer structured installers over raw shell commands.
  • Use a bundle when your plugin owns a family of related servers or custom install logic.
  • Use useWorkspaceFolders: true for heavy workspace-aware servers like TypeScript or Rust.
  • If your server runs outside Acode's local filesystem view, define both rootUri and documentUri so the server receives paths it can resolve.

Released under the MIT License.