Skip to content

Language services

VS Code programmatic language features often run through a Language Server. Lapis uses a custom LanguageServiceProvider contract consumed by app.languageServices. The API is LSP-shaped—positions, ranges, diagnostics, completions—but it does not use JSON-RPC or VS Code’s language server libraries.

Providers declare what they implement through metadata.capabilities:

CapabilityEditor support today
diagnosticsYes — lint gutter, inline markers, Lapis-owned lint tooltip
codeActionsYes — quick fixes from diagnostics
completionYes — TypeScript in code file views and notebook cells
hoverYes — TypeScript
definitionProvider API only — no go-to-definition editor UI yet

Manifest-only language services are the preferred path when Lapis can bind a known provider at boot. The bundled Markdown Lint plugin demonstrates the pattern:

{
"contributes": {
"configuration": [
{
"id": "markdown-lint",
"title": "Markdown Lint",
"properties": {
"disabledRules": {
"type": "array",
"title": "Disabled rules",
"items": { "type": "string" },
"default": []
}
}
}
],
"services": [
{
"id": "markdown-lint",
"service": "language-service",
"languages": ["markdown"],
"priority": 100,
"capabilities": {
"diagnostics": true,
"codeActions": true
}
}
]
}
}

System extensions connect the manifest declaration to a concrete provider implementation. Community plugins with custom analysis logic should register imperatively.

Hybrid plugins call Plugin.registerLapisServiceProvider() during onload():

this.registerLapisServiceProvider({
id: "example-plugin:analyzer",
service: "language-service",
languages: ["markdown"],
priority: 50,
capabilities: {
diagnostics: true,
},
provider: myProvider,
});

The plugin manager scopes the provider ID to your plugin and registers it with app.languageServices. Cleanup runs automatically on unload.

A LanguageServiceProvider includes metadata and optional methods:

interface LanguageServiceProvider {
metadata: {
id: string;
languages: string[];
runtime: "worker" | "native" | "in-process" | "lsp";
priority?: number;
capabilities: {
diagnostics?: boolean;
completion?: boolean;
hover?: boolean;
definition?: boolean;
codeActions?: boolean;
};
};
updateDocument?(update): void | Promise<void>;
provideDiagnostics?(context): Promise<Diagnostic[]>;
provideCompletions?(context, position): Promise<CompletionList | null>;
provideHover?(context, position): Promise<Hover | null>;
provideDefinition?(context, position): Promise<Location[]>;
provideCodeActions?(context, range): Promise<CodeAction[]>;
dispose?(): void | Promise<void>;
}

Request context includes the virtual document (uri, languageId, text, version) and optional global declarations for cross-file analysis.

RuntimeWhere it runsWhen to use
workerBrowser Web WorkerPWA-safe analysis (markdownlint, TypeScript)
nativeDesktop Electron sidecarHigher-priority desktop providers
in-processRenderer threadLightweight providers and tests
lspReservedNot implemented; do not rely on it

On desktop, native providers typically register with higher priority than worker providers for the same language. The manager merges diagnostics from all matching providers, preferring higher priority when combining results.

File-backed editors attach language-service UI through languageServiceExtensions() from @lapis-notes/api/editor/language-service:

import { languageServiceExtensions } from "@lapis-notes/api/editor/language-service";
this.registerEditorExtension(
markupEditor(
javascript({ typescript: true }),
languageServiceExtensions({ languageId: "typescript" }),
),
"typescript",
);

Options let you enable subsets of features per view:

languageServiceExtensions({
languageId: "markdown",
completion: false,
hover: false,
});

The markdown plugin enables diagnostics only. TypeScript views enable diagnostics, completions, and hover.

Shared adapters in the API package connect app.languageServices to CodeMirror:

  • Diagnostics — gutter markers, inline squiggles, and a Lapis-owned lint tooltip (not CodeMirror’s default lint tooltip)
  • Completions@codemirror/autocomplete override
  • HoverhoverTooltip() integration
  • Code actions — exposed through the lint UI

Notebook cell editors can supply a virtual document through a CodeMirror facet so TypeScript sees the full notebook while diagnostics map back to the focused cell.

Language services vs CodeMirror autocomplete

Section titled “Language services vs CodeMirror autocomplete”

Not every suggestion belongs in a language service.

Use language services whenUse CodeMirror extensions when
Analysis needs document or project contextSuggestions come from vault structure or local syntax
Results should merge with other providersBehavior is markdown- or view-specific
You want consistent diagnostics and quick fixesYou need custom trigger rules or widget UI

Examples:

  • TypeScript completions — language service (TypeScript compiler API in a worker)
  • Markdown wiki link autocomplete — CodeMirror completion in the markdown plugin
  • Search query suggestions — custom autocomplete in the search query editor
  • Markdownlint diagnostics — language service

Lapis ships these language-service providers today:

ProviderLanguagesCapabilities
Markdownlintmarkdowndiagnostics, code actions
TypeScripttypescript, javascriptdiagnostics, completions, hover, definition (API only)