Skip to content

Language intelligence should be plugin-contributed, not built into the editor #2438

Description

@andrewkchan

Human comments

This is a proposal to make LSPs into plugins. Also separate from LSPs, monaco has a concept of builtin language services that allows some limited intellisense functionality within files.


The workflow

The Monaco editor builtin (#2127) opens files but has no language intelligence: no go-to-definition, no find-references, no type checking. I want those, without BB deciding for everyone which language servers run, and without paying for them when they are switched off.

Three separate reasons this should be plugin-contributed rather than baked in:

  • Bundle size. Monaco's TypeScript service adds a 6.7 MB worker to the packaged app for one language.
  • CPU and memory. A semantic language server is not free while it runs. Anyone who wants an editor but not a background analyser should be able to have one.
  • Choice. People will want their own servers — rust-analyzer, gopls, pyright, a work-specific server — rather than whatever BB happens to bundle.

What happens today

Nothing supplies language providers. plugins/monaco-editor ships Monaco's editor and grammars but omits the CSS/HTML/JSON/TypeScript language services, so features that need a provider render nothing: the word-highlight contribution is in the bundle but had to be fed a textual DocumentHighlightProvider from plugin code to do anything, and go-to-definition has no provider at all.

Adding Monaco's TypeScript service back would restore single-file navigation, but it is unconditional (shipped and running for everyone, for one language) and it still cannot resolve an import, because that service sees only the open model.

Two capabilities are missing for the real thing:

  1. A plugin cannot run a process on a host. The host command surface a plugin can reach is entirely file and skill operations (host.read_file, host.list_paths, host.write_skill, …). terminals is the only way to start anything on a host, and a pty is the wrong transport for JSON-RPC. A language server has to run where the files are — including on an enrolled remote machine — so this needs a host-daemon command with a duplex stream and a lifecycle, which is a HOST_DAEMON_PROTOCOL_VERSION bump.
  2. A plugin cannot contribute providers into another plugin's editor. Plugin frontends are isolated bundles; there is no way to hand a DefinitionProvider to whatever editor is mounted.

What you would expect

A contribution point for language providers, with plugins as the only suppliers — so BB never ships a fixed set and every language is someone's plugin rather than a BB change.

Roughly two tiers, which are worth separating because they have different owners and costs:

Tier 1 — Monaco's own in-browser services. No new BB API: an editor-plugin setting plus a lazy import of the service bundle. Solves the CPU/memory objection (the worker never spawns when off) but not the size one, since bundled plugins ship whether enabled or not — note copy-builtin-plugins packages store-only "official" plugins too, so only a third-party git/npm plugin genuinely avoids the bytes. Still single-file scope, so its diagnostics stay wrong and stay suppressed.

Tier 2 — real language servers. Needs both missing capabilities above, plus a decision on transport latency: frontend → plugin RPC → server → daemon → process, on every keystroke for completions. Probably fine; worth measuring before the design is settled rather than after.

A host-mediated slot is the shape that fits BB: a plugin registers language-provider capabilities and the host routes them to the mounted editor, rather than plugins reaching into each other. #2270 is the precedent — it added a slot so plugins contribute rows into host chrome instead of manipulating it.

If tier 2's contract is designed first, tier 1 can be expressed through it and BB avoids two mechanisms for the same thing.

Context and alternatives

The alternative is BB bundling Monaco's language services and toggling them with a setting. That is much less work and would restore go-to-definition for TypeScript tomorrow, but it fixes one language, keeps the bytes in the package for everyone, and leaves "use my own server" unanswered — so it is worth doing only if the generic contract is far off.

Related: #2127 (the Monaco builtin, which is where this gap is visible), #2102 (a fileOpener cannot open a file or retitle its tab), #2093 (path listing hides dotfiles and node_modules).

AGENT GENERATED: by Claude Opus 5

Metadata

Metadata

Assignees

No one assigned

    Labels

    pluginsPlugin SDK, runtime, marketplace

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions