diff --git a/contract.lock.json b/contract.lock.json index 0ceab3a..b0714c6 100644 --- a/contract.lock.json +++ b/contract.lock.json @@ -3,12 +3,12 @@ "source": { "repository": "https://github.com/1lck/Lithe-IDEA", "directory": "shared/contracts/plugins/v1", - "revision": "5d38352a1757a70fd5fa297d7aa3e43bb2e33607" + "revision": "5cc06d5f44cfe94956b174720d34445b027f51d7" }, "files": [ { "path": "README.md", - "sha256": "e5e938c1aa106dc1853e45b0dc20b493f645bbe16373336015cac1b627a336f1" + "sha256": "76f1ba3324ea001cd8ac2cee9bc0aac0126b647bc5fd5a8a5167cfbdf5b2c056" }, { "path": "fixtures/backend/backend-workspace.plugin.json", @@ -24,7 +24,15 @@ }, { "path": "fixtures/backend/function-protocol.json", - "sha256": "4341a2145d495431f4017ae2aed95214c512c14c5efb866c5a1178912c66006b" + "sha256": "08742d14152975c3e08c7b48f5d98a19f3a6ae6bf8bd22b6a5ae2edf2fa46b17" + }, + { + "path": "fixtures/backend/plugin-functions-caller.plugin.json", + "sha256": "cde349f0408a44971214fde930cd83144236684c3dc85b96a53a7389cd98da72" + }, + { + "path": "fixtures/backend/plugin-functions-exporter.plugin.json", + "sha256": "5383087ef74b27cd8225683a50091ebff4f0be197ffc1cc84b58f6d18237b860" }, { "path": "fixtures/backend/plugin-logging.json", @@ -154,6 +162,10 @@ "path": "fixtures/build-systems/build-toolchains.plugin.json", "sha256": "4cf0e02549ea24f30d245adb423dffddc0a51fba1c835fae600dc89468d23c3d" }, + { + "path": "fixtures/debug-adapters/debug-adapter-features.plugin.json", + "sha256": "10bbe0e5bd7e2e983d0019dcc81b3ff134621e7245a03c51bff6d8fec3b113ba" + }, { "path": "fixtures/debug-adapters/debug-adapter-language-server.plugin.json", "sha256": "c29d1e94a69840a63317e5d4b15a0a9cd8267a980fa2b21c37528bc08bb3ee96" @@ -170,10 +182,26 @@ "path": "fixtures/debug-adapters/debug-adapter.plugin.json", "sha256": "81038febbde31e0554451789106f8fc255e87038c8185602ce0c7e918f90ff78" }, + { + "path": "fixtures/debug-adapters/debug-start.json", + "sha256": "49073e0435fec52ca51e8c7d9799849bb31d1736908feb48dff2bea82b906ee1" + }, + { + "path": "fixtures/editor/editor-actions-v2.json", + "sha256": "9f0741a5c31a1c911e7c76c9ebba0eaf5c3029cfc15db45eea2f49660461fb7b" + }, { "path": "fixtures/editor/editor-actions.plugin.json", "sha256": "ca86471b63a15e6f12102777c59c300a6da7690ac13e02835e43abf41f26eb0b" }, + { + "path": "fixtures/editor/editor-providers.json", + "sha256": "1adad0fdf4939d8d6c9d1378bdf06b5639b64ceff2cb4e4e8dfebde2db5c7bfa" + }, + { + "path": "fixtures/editor/editor-providers.plugin.json", + "sha256": "1b562895a1523907a06fc10318df5e13b28c8f2df07a55ffc1d5e06e1ffc8a38" + }, { "path": "fixtures/keymap-import/keymap-import-provider.plugin.json", "sha256": "03d3843f30edd82fd02652a6f800eca9d7afcddc7b9d2a0bd1b3726c2dfb8eae" @@ -198,6 +226,14 @@ "path": "fixtures/languages/language-packaged-server.plugin.json", "sha256": "fd0e27a4cb3642c2094c70e4124f95c55711e0228fd77bfddc869d02fac3d7d6" }, + { + "path": "fixtures/languages/language-server-backend-launch.plugin.json", + "sha256": "a8abf56ce1f2f25cb966fdfa97f1d7fc2b53e443cfb3562382d7401c6867a07c" + }, + { + "path": "fixtures/languages/language-server-launch-resolution.json", + "sha256": "891e0cb18cf2f7f37a36d91c0b59f834b1342795971ab136393d448ff8b34292" + }, { "path": "fixtures/languages/language-system-command.plugin.json", "sha256": "4dbdd38bbe23003da61210296f986d94313b23a98be4d97b1a628570c4375501" @@ -216,7 +252,7 @@ }, { "path": "fixtures/manifest/invalid-manifests.json", - "sha256": "ea8d728d9e37a6ea6c3e3c0073692c87d1e55a2648d4bb8e74d5b7f583e5f979" + "sha256": "1998fdb5a583043644857702652b7f776ed1805f3ff016dd543f942a2c4d15bb" }, { "path": "fixtures/marketplace/index-cases.json", @@ -278,6 +314,18 @@ "path": "fixtures/project-generators/templates/tool-field/README.md", "sha256": "57d17750e2c3d0fed6158d0995b08cf782b745774e5a7e6bee3b7d3dbdb6a777" }, + { + "path": "fixtures/run/output-link-matchers.json", + "sha256": "3e28c25f15c54e791fd86e7f9e620ab822745e5234dfc6286e18bed75a67ae78" + }, + { + "path": "fixtures/run/run-start.json", + "sha256": "4bc9206445ea231863d0a2b44d9f7debdc6f2a63e8afc35cb1c5ccccda67455c" + }, + { + "path": "fixtures/run/tool-reference-launch.plugin.json", + "sha256": "9f12db5f3b4856db8d238089905ec175ba1a918f4f875780a371d06a94a7430c" + }, { "path": "fixtures/settings/settings-v2.plugin.json", "sha256": "83d4c7faf7bfbb9c474639bfc0ef42dae8cd7d370d388f8527589f2afed12981" @@ -320,11 +368,11 @@ }, { "path": "plugin-manifest.schema.json", - "sha256": "46739a6203dc7e250e28eb295e8693b57a668cda46be95fda5bd4c120da6eac3" + "sha256": "1043bc27aa0afd190e57bfda419eb068d19d8e653309f935e1483b348e022e6e" }, { "path": "toolchains/README.md", - "sha256": "3c5535140a3040753b6eab5e4e7032362820e397ac0a670a71e60e7b6a83ad82" + "sha256": "2ec9f9bbb93e0b8d92c2299525ecb56d64b889e79b15bad1a350f5f3c0654106" } ] } diff --git a/contract/v1/README.md b/contract/v1/README.md index 412388f..4581af5 100644 --- a/contract/v1/README.md +++ b/contract/v1/README.md @@ -14,19 +14,20 @@ This directory is the cross-platform source of truth: | `native-ui.schema.json` | JSON shape of native UI documents rendered by the host | | `plugin-index.schema.json` | JSON shape of a marketplace source index | | `fixtures/` | Valid example manifests and golden cases, one folder per contract area (below). Manifests are named after the capability they exercise rather than a published plugin. Case files name their base manifest by file name, so file names are unique across the folders | -| `toolchains/README.md` | Implemented Core foundation: plugin definitions/provider declarations, verified-origin registry, reply validation, directory pins, snapshot resolution, version matching and discovery plans; macOS uses it for its read-only settings pages and for Build and run tools, Windows is not connected | +| `toolchains/README.md` | Implemented Core foundation: plugin definitions/provider declarations, registry (a short tool ID defined by several enabled plugins belongs to none of them), reply validation, directory pins, snapshot resolution, version matching and discovery plans; macOS uses it for its read-only settings pages and for Build and run tools, Windows is not connected | | Fixture folder | Contents | | --- | --- | | `fixtures/manifest/` | `hello-lithe.plugin.json`, the smallest UI plugin, and `invalid-manifests.json`: edits to a valid manifest and the rejection code every host must report | -| `fixtures/languages/` | Language support: packaged, system-command, and host-integration language servers, `node-runtime` for a discovered runtime, and `language-grammar` with `grammars/`; `command-plans.json` holds golden run/test command expansions and language-server launch plans, `runtime-versions.json` the golden outcomes of the launch runtime version check | -| `fixtures/backend/` | Plugin backends (`backend`, `backend-workspace`, `command-backend`, `terminal-backend`); `function-protocol.json` holds golden plugin-function negotiation, registration, invocation, outcome, progress, and refusal cases, `plugin-logging.json` the golden `litheLogging` negotiation and notifications | -| `fixtures/editor/` | Editor actions served by a backend | +| `fixtures/languages/` | Language support: packaged, system-command, and host-integration language servers, `node-runtime` for a discovered runtime, and `language-grammar` with `grammars/`; `command-plans.json` holds golden run/test command expansions and language-server launch plans, `runtime-versions.json` the golden outcomes of the launch runtime version check; `language-server-backend-launch.plugin.json` is a language server whose backend resolves the launch and a session policy (`languageServers` 3, `serverProcess` 3), and `language-server-launch-resolution.json` holds its golden first-step plans and decoded answers | +| `fixtures/backend/` | Plugin backends (`backend`, `backend-workspace`, `command-backend`, `terminal-backend`); cross-plugin calls (`plugin-functions-exporter`, `plugin-functions-caller`); `function-protocol.json` holds golden plugin-function negotiation, registration, invocation, outcome, progress, and refusal cases, `plugin-logging.json` the golden `litheLogging` negotiation and notifications | +| `fixtures/editor/` | Editor actions served by a backend: `editor-actions.plugin.json`, version 2 icons, alternatives and click results in `editor-actions-v2.json`; editor providers in `editor-providers.plugin.json` with registration and result cases in `editor-providers.json` | | `fixtures/ui/` | Native controls, forms, and panels, and a web panel | | `fixtures/settings/` | Plugin settings declared with `settings: 2` | | `fixtures/build-systems/` | Level 3 build systems (`build-*`: static file, command output, host integration, instance options, model producer and consumer, tool path, toolchains); `build-plans.json` holds golden build-system plans and decoded outputs | | `fixtures/build-providers/` | Build providers (`build-provider*`) and the golden cases of their configuration, run options, run and debug preparation, execution, and test protocols | -| `fixtures/debug-adapters/` | Debug adapters (standalone, served by a language server, remote attach) and `debug-adapter-rejections.json` | +| `fixtures/debug-adapters/` | Debug adapters (standalone, served by a language server, remote attach, and `debug-adapter-features` with declared features and defaults), `debug-adapter-rejections.json`, and `debug-start.json`: what `debug.decodeBackendConfiguration` returns for each `debug/start` case | +| `fixtures/run/` | Run sessions and console links: `tool-reference-launch` names its runtimes by tool-definition ID (`serverProcess` 4) and declares output link matchers; `run-start.json` holds what `run.decodeStart` returns for each case, including the rejections, and `output-link-matchers.json` the golden matches and resolutions | | `fixtures/vcs/` | A version control provider | | `fixtures/keymap-import/` | Two independent shortcut import providers sharing one packaged backend | | `fixtures/project-generators/` | Project generators: template files (`templates/`), a scaffolding command, and an archive download; `project-generator-tool-field.plugin.json` with `templates/tool-field/` picks tool installations with `tool` fields (`projectGenerators` 2); `project-plans.json` holds golden `project.*` form evaluations, generation plans, and name checks, and a case's optional `manifest` names the fixture it runs against (the version 1 generator by default) | @@ -63,8 +64,10 @@ and `toolchainProviders: 1` declarations and provides the `toolchains.*` command listed there. `executableResolution: 2` applies to tool-definition candidates only; it does not admit new sources into existing Build candidate sets. Definitions are entirely -plugin-owned, with reserved short IDs checked against host-verified official -provenance when constructing the registry. +plugin-owned. Any plugin may define a short ID such as `compiler` or a qualified +`:`; the host supplies no provenance evidence. A short ID defined +by more than one enabled plugin belongs to none of them: the registry omits +those definitions and reports the contest as a conflict. macOS advertises these capabilities for its read-only toolchain settings pages. The native adapter scans declarations, probes checked executables, queries plugin @@ -175,6 +178,7 @@ symbolic links. A host treats an installed package as read-only. | `toolDefinitions` | Plugin-owned tool policy ([Core foundation](toolchains/README.md)); requires `toolDefinitions: 1` and `executableResolution: 2`; macOS hosts read-only discovery and resolves Build and run tools. | | `toolchainProviders` | Backend fact-provider declarations ([Core foundation](toolchains/README.md)); requires `toolchainProviders: 1`, backend/server-process capabilities and `toolchains.provide`. | | `keymapImportProviders` | Independent shortcut import providers; requires `keymapImportProviders: 1`, `backend: 2`, `serverProcess: 1` and `keymaps.provide`. macOS provides access to explicitly selected sources; Windows does not yet advertise this capability. | +| `pluginFunctions` | [Functions the plugin exports to, and calls from, other plugins](#cross-plugin-calls-protocol-version-4); requires `pluginFunctions: 1`, `backend: 2` and `serverProcess: 1`, and grants no permission. macOS relays the calls; Windows does not advertise this capability. | Unknown fields are refused (`plugin.manifest.invalid`) so an older host rejects a package instead of ignoring part of it. At levels 1 and 2 a new @@ -228,9 +232,9 @@ activation event, and a plugin may consist of grammars alone. explicit contract; an arbitrary new `kind` string cannot supply one. - This contribution does not register completion, hover, definition, symbol, formatting, rename, or Code Action providers backed by ordinary plugin - functions. Those independent provider bindings are not implemented; existing - LSP support and `editor/registerActions` are separate capabilities, not a - substitute for them. + functions. A backend binds the provider types a host routes with + [`editor/registerProviders`](#editor-language-feature-providers-protocol-version-3); + existing LSP support and `editor/registerActions` are separate capabilities. - `grammar.path` is a package-relative file that stays inside the package. Hosts read it read-only from the installed package, pass its text to `plugins.declarative.decodeGrammar`, and hand the returned object to the @@ -268,13 +272,14 @@ activation event, and a plugin may consist of grammars alone. | --- | --- | | `languageID` | A declared language. | | `languageIdentifier` | LSP `languageId` for documents; defaults to `languageID`. | -| `launch` | `{"kind": "packaged", "path": …}` runs an executable inside the package. `{"kind": "systemCommand", "commands": [...]}` lists bare command names the host resolves with its tool discovery (PATH, project toolchains, user overrides). API 2 adds `{"kind": "runtime", …}`; see Runtime launch. | +| `launch` | `{"kind": "packaged", "path": …}` runs an executable inside the package. `{"kind": "systemCommand", "commands": [...]}` lists bare command names the host resolves with its tool discovery (PATH, project toolchains, user overrides). API 2 adds `{"kind": "runtime", …}`; see Runtime launch. `serverProcess` 3 adds `{"kind": "backend", …}`, whose command line the plugin backend answers at each start; see [Backend-resolved launch](#backend-resolved-launch-serverprocess-3). | | `arguments` | Arguments passed to the server. Literal text at API 1; launch templates at API 2. | | `validationArguments` | Probe that must exit with status 0 before a candidate is used; empty means the host trusts the candidate and lets the LSP handshake decide. | | `runtimeDependencies` | Runtimes the host must discover and pass to the server. API 1 tools are `node` and `go`; API 2 adds `java`. The host exports each path as `LITHE__PATH` (`LITHE_NODE_PATH`, `LITHE_GO_PATH`; a user-set value takes precedence) and prepends its directory to the server's `PATH`, because servers such as gopls run the tool themselves and an app opened from the Dock does not inherit the shell `PATH`. A project-local copy is never chosen, since servers start automatically when a file opens. | | `initializationOptions` | API 2. A JSON object sent as the LSP `initializationOptions`. Every string value inside it is a launch template. | -| `hostIntegration` | API 2, official plugins only. A named integration implemented in the host, with the package resources it reads; see Host integrations. | +| `hostIntegration` | API 2. Any plugin may declare one. A named integration implemented in the host, with the package resources it reads; see Host integrations. | +| `requestMethods`, `commands`, `serializedCommands`, `forwardNotifications`, `state`, `remote` | `languageServers` 3. What the plugin backend may ask of the running server and how the host keeps its state; see [Plugin-owned sessions](#plugin-owned-sessions-languageservers-3). | The host owns the language-server process lifecycle, the LSP session, and stopping the process when the provider is disabled. An API 3 build provider is @@ -311,6 +316,39 @@ The reported version is the first dotted number in the probe output, ignoring lines that start with `Picked up ` (a JVM announcing `JAVA_TOOL_OPTIONS` before its version); see `fixtures/languages/runtime-versions.json`. +#### Tool references (`serverProcess` 4) + +`serverProcess` 4 stops naming runtimes from the host's closed lists (the +`tool` values that versions 1 to 3 accept) and names them by +[tool-definition ID](toolchains/README.md) instead, so a host never learns a tool from +a name. The plugin lists every tool it starts +a process with in the top-level `requiresTools`, and the definitions come from +`toolDefinitions` of the plugin itself or of another enabled plugin. + +| Field | Meaning | +| --- | --- | +| `requiresTools` | At most 32 tool-definition IDs, each short (`interpreter`) or plugin-qualified (`org.example.tools:interpreter`), without duplicates; stored sorted. Present exactly when `requires.serverProcess` is 4: the version without the list is `plugin.manifest.invalid` at `requires.serverProcess`, the list without the version is invalid at `requiresTools`. | +| `launch.tool` of a `runtime` launch, `runtimeDependencies[].tool`, `runSelectedRuntime.tool` of a debug adapter | A tool-definition ID that `requiresTools` lists. An ID it does not list is `plugin.runtime.undeclared` with the `field` that names it. Versions 1 to 3 keep the closed lists and report `plugin.runtime.unsupported`. | + +The definition, not the host, decides how its executable starts an entry: its +`launch` template says where `runtimeArguments`, the entry and `arguments` go +(`["${executable}", "${runtimeArguments}", "${entry}", "${arguments}"]` runs +` `, a template without `${entry}` +runs no entry). The launch plan of a version 4 process therefore returns the +parts, not a command line: + +| Field | Meaning | +| --- | --- | +| `launch` | `{kind: "tool", tool, entry, runtimeArguments, minimumVersion?, packagedRuntime?}`. `entry` is the absolute path of the package file, `runtimeArguments` are expanded, and `packagedRuntime` is the expanded path of a runtime the plugin ships (the host then uses that file as the tool's executable and never looks the tool up). | +| `arguments` | Only the plugin's own arguments; the host places them where the definition's `${arguments}` is. | +| `runtimeRequirements` | `{kind: "tool", tool, minimumVersion?}` for every tool the host must resolve. | + +The host resolves each tool through the four layers of +[toolchains/README.md](toolchains/README.md), checks `minimumVersion` with +`toolchains.checkVersion`, and expands the start with `toolchains.instantiateLaunch`. +A tool that resolves to no executable is `plugin.host.toolUnresolved` +(`details.reason` says why). See `fixtures/run/tool-reference-launch.plugin.json`. + #### Host integrations (API 2) Most of a language server is described by its launch. A few servers also need @@ -330,10 +368,16 @@ the files it needs are inside the plugin's own package: The plugin owns the files; the host owns the behavior. The host enables an integration only for the server that declares it and never matches on a language ID or looks for the files anywhere else, so removing the plugin -removes the integration's inputs with it. Because an integration runs trusted -host code, only plugins in the `dev.lithe` namespace may declare one; any -other package, or an ID the host does not implement, is refused with -`plugin.integration.unsupported`. +removes the integration's inputs with it. Any plugin may declare one, whatever +its namespace. The integration is host code, but it reads only resources inside +the declaring plugin's own package and hands them to the language server or +helper process that same plugin starts (extension bundles, agents, a test +runner, debugging); it grants the plugin no permission beyond running its own +server process, which an installed plugin may already do once the user confirms +the install review. An ID the host does not implement is refused with +`plugin.integration.unsupported`. A build system's `hostIntegration` is a +different declaration and stays limited to official plugins; see +[Host integrations](#host-integrations) under API level 3: build systems. Each resource is a package-relative path, or a list of them, that may vary only by `${arch}`. An integration defines which names it reads; a missing @@ -380,21 +424,24 @@ contains a machine path, and the host decides where state lives. host selects the process with exactly one of `languageID` (a language server), `buildSystemID` (a build provider), `vcsProviderID` (a VCS provider), and `backend: true` (the [plugin backend](#plugin-backend)), passes `{pluginDirectory, -stateDirectory, workspaceStateDirectory?, logDirectory?, architecture, settings?}`, and -receives. `workspaceStateDirectory` may be omitted only when the selected +stateDirectory, workspaceStateDirectory?, logDirectory?, temporaryDirectory?, architecture, +settings?}`, and receives. `workspaceStateDirectory` may be omitted only when the selected process does not use `${workspaceStateDir}`; otherwise the request is `plugin.launch.invalidRequest`. The same error applies when a template uses `${logDir}` but `logDirectory` is absent, empty, or not absolute. | Field | Meaning | | --- | --- | -| `launch` | `{kind: "packaged", path}`, `{kind: "systemCommand", commands}`, or `{kind: "runtime", tool, entry, packagedRuntime?, minimumVersion?, versionArguments?}`: what the host resolves to an executable. `packagedRuntime` is the expanded package-relative path of a shipped runtime. `minimumVersion` and `versionArguments` appear together when a discovered runtime must meet a minimum version. | +| `launch` | `{kind: "packaged", path}`, `{kind: "systemCommand", commands}`, or `{kind: "runtime", tool, entry, packagedRuntime?, minimumVersion?, versionArguments?}`: what the host resolves to an executable. `packagedRuntime` is the expanded package-relative path of a shipped runtime. `minimumVersion` and `versionArguments` appear together when a discovered runtime must meet a minimum version. A [backend-resolved launch](#backend-resolved-launch-serverprocess-3) returns `{kind: "backend", function, timeoutMilliseconds}` here, and `{kind: "tool", tool}` appears in the plan `plugins.languageServers.decodeLaunch` returns. | | `arguments` | Complete argument list after the executable, including `-jar` and the entry of a runtime launch. | | `validationArguments` | Expanded probe arguments. | | `runtimeRequirements` | `{tool, minimumVersion?}` for every runtime the host must discover, a discovered launch tool included, sorted by tool. A packaged runtime is not listed. Only the launch tool can carry a minimum version. | | `initializationOptions` | Expanded options; absent when the server declares none. | -| `requiredDirectories` | `state`, `workspaceState`, and/or `log`: the directories the host must create before starting the server. | +| `requiredDirectories` | `state`, `workspaceState`, `log`, and/or `temporary`: the directories the host must create before starting the server. | | `requiredPackagePaths` | Sorted package-relative paths the launch points at. The host verifies each is inside the package and not a symbolic link before starting the server. | | `settings` | Effective value of every declared setting that produced this plan; absent when the plugin declares none. | +| `resolveParams` | Present only for a backend-resolved launch: the params of the launch function, without the `context` and the optional `tools` the host adds. | +| `environment`, `writablePaths` | Present only in the plan `decodeLaunch` returns: the variables of the process, and the paths it writes. | +| `sessionPolicy` | Present for a language server of `languageServers` 3: the [session policy](#plugin-owned-sessions-languageservers-3) the host passes to `lsp.startServer` unchanged. | | `buildProvider` | Present only for a `buildSystemID` selection: `{buildSystemID, executesProjectCode, mayUseNetwork, languageServer?}`. `languageServer` names the language whose server is the provider; the rest of the plan is then that server's plan, and the host reuses its running session instead of starting a second process. | | `vcsProvider` | Present only for a `vcsProviderID` selection: the [provider descriptor](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/contracts/vcs-provider-v1.md#provider-descriptor) the host keeps for the session. | | `backend` | Present only for a `backend` selection: `{hostServices}`, the sorted [host services](#host-services-protocol-version-2) the plugin's permissions grant, plus `version: 2` for a [`backend` 2](#backend-2-workspace-contexts) process. The host passes `hostServices` to `lsp.startServer.hostServices` unchanged. | @@ -594,6 +641,182 @@ requests by section path from it, and sends value with `lsp.updateWorkspaceConfiguration` when an `immediate` setting changes; the running server receives it without a restart. +#### Backend-resolved launch (`serverProcess` 3) + +A server whose command line depends on what the plugin finds when it starts +(a version it resolves, a tool it bootstraps, options it derives from the +project) lets the plugin backend answer the command line each time instead of +declaring one: + +```json +"launch": {"kind": "backend", "function": "launch.resolve", "timeoutMilliseconds": 30000} +``` + +- Only a language server can use it. `requires.serverProcess` is `3`, the + lowest version covering it, or `4` when the manifest also lists + `requiresTools`; the plugin declares a `backend` of + version 2 or later, and `pluginAPIVersion` is at least 4. `function` is a + plugin function ID. `timeoutMilliseconds` is 1000 to 120000 and defaults to + 30000. +- The server declares none of `arguments`, `validationArguments`, + `runtimeDependencies`, `initializationOptions`, or `hostIntegration`: the + answer carries the first four, and a host integration is host code, which + this launch exists to avoid. `workspaceConfiguration` stays declarative + unless the answer carries its own. +- `plugins.declarative.languageServerLaunchPlan` for such a server returns the + first step, not a command line: `launch: {kind: "backend", function, + timeoutMilliseconds}`, `resolveParams`, `requiredDirectories` (always + `state`, `workspaceState`, `log`, and `temporary`), `settings`, and + `sessionPolicy`. Its request adds `temporaryDirectory`, an absolute scratch + directory of this start; the launch needs all four directories, so a request + without one is `plugin.launch.invalidRequest`. +- The host creates the directories, starts the plugin backend if it is not + running, and calls `function` through `lithe/function/invoke` with + `resolveParams` and the workspace `context`. A host may also send `tools`, + `{: {executable, home, version}}`, the resolution of each tool it + could find; Core does not validate it. The macOS host does not send `tools` + yet, so a function must not require it: it answers `{kind: "tool"}` and + lets the host resolve the tool. A failed or timed-out call, or an answer + for a context whose generation has changed, ends this start without a + process. +- The host passes `{manifest, languageID, request, result}` to + `plugins.languageServers.decodeLaunch`, which returns `{status: "ok", plan}` + or a rejection. The plan is an ordinary launch plan (`launch` is `packaged` + or `tool`) whose `sessionPolicy` also carries the answer's `gates`; the host + starts it like any other. A server that is not launched by the backend is + `plugin.launch.notBackend`, so a host never runs an answer it did not ask + for. + +`resolveParams`: + +| Field | Meaning | +| --- | --- | +| `serverID` | The server's `languageID`; also its ID in the `languageServer` host methods. | +| `platform` | `{os, arch}`: the host operating system and CPU architecture (`arm64` or `x86_64`). | +| `stateDirectory`, `workspaceStateDirectory`, `logDirectory`, `temporaryDirectory` | The writable directories of this start, absolute and outside the package and the application bundle. | +| `settings` | The effective value of every declared plugin setting. | + +The answer is an object with these fields; an unknown field is refused. + +| Field | Meaning | +| --- | --- | +| `executable` | `{kind: "packaged", path}` is a file inside the plugin package, read-only, listed in the plan's `requiredPackagePaths`. `{kind: "tool", tool}` names a tool definition of the plugin; the host resolves it like any tool launch. | +| `arguments` | At most 1024 strings of at most 32 KiB, without NUL. | +| `environment` | At most 256 variables; a name is non-empty without `=`, a value is at most 32 KiB. | +| `initializationOptions`, `workspaceConfiguration` | JSON objects of at most 1 MiB each. | +| `gates` | At most 8; see [Plugin-owned sessions](#plugin-owned-sessions-languageservers-3). | +| `writablePaths` | At most 32 paths the server writes. Each lies inside one of the four directories; `.` and `..` components are refused. The plan carries them sorted, and the host creates their parent directories before the start. | + +Rejections: `plugin.launch.invalidResult` for a malformed or oversized answer +or gate, `plugin.launch.outsidePackage` for a packaged path that leaves the +package, `plugin.launch.undeclaredTool` for a tool the plugin does not +define, `plugin.launch.unwritablePath` for a writable path outside the host +directories, `plugin.launch.invalidRequest` for a request without a usable +directory, `plugin.launch.unavailable` for a language without a server, and +`plugin.launch.notBackend`. See `fixtures/languages/language-server-backend-launch.plugin.json` +and `fixtures/languages/language-server-launch-resolution.json`. + +Host limits of the macOS host today: a `{kind: "tool"}` answer is resolved +with the user's tool choice and discovery, and a program that lies inside the +project is refused, because opening a project must not run it; a server whose +launch the backend resolves does not start in a remote workspace; and the +scratch `temporaryDirectory` is removed when the server process ends. + +#### Plugin-owned sessions (`languageServers` 3) + +A plugin backend drives its own server only through what the server +declares. With `requires.languageServers: 3` a server may declare: + +| Field | Meaning | +| --- | --- | +| `requestMethods` | LSP methods the backend may send with `languageServer/request` or `languageServer/notify`; at most 64. | +| `commands` | The `workspace/executeCommand` commands the backend may run; at most 128. Declared exactly when `requestMethods` lists `workspace/executeCommand`. | +| `serializedCommands` | A subset of `commands` that runs one at a time: a request for a command already running joins it and receives the same answer. | +| `forwardNotifications` | Server notifications the host forwards to the backend; at most 32. | +| `state` | `{scope: "workspace", retention: {maxEntries, maxAgeDays}}`. The host prunes the server's per-workspace state directories (see below). `maxEntries` is 1 to 64, `maxAgeDays` 1 to 365. | +| `remote` | `supported` (default) or `unsupported`. | + +- A name is non-empty text of at most 256 bytes without whitespace or control + characters, unique within its list. Names the host sends itself + (`initialize`, `initialized`, `shutdown`, `exit`, `textDocument/didOpen`, + `didChange`, `didClose`, `didSave`, `willSave`, `willSaveWaitUntil`, + `workspace/didChangeWorkspaceFolders`, and every `$/` method) cannot appear + in `requestMethods` or `forwardNotifications`. +- Below `languageServers` 3 each of these fields is `plugin.manifest.invalid`. + A server that lists `requestMethods` or `forwardNotifications` requires the + `languageServer.request` permission; declaring the permission without such a + server is `plugin.manifest.invalid`. +- The launch plan's `sessionPolicy` is these fields as sorted lists plus + `serverID` (the `languageID`) and `gates`. The host sends it unchanged as + `lsp.startServer.sessionPolicy`; Core's LSP engine enforces it, so a host + cannot widen it and a backend cannot step outside it. +- A server with `remote: "unsupported"` is not started for a remote + workspace: `lsp.startServer` answers `not_supported`. + +What the engine enforces on `lsp.pluginServerRequest` and +`lsp.pluginServerNotify`, the two commands behind the host methods: + +- A request must be in `requestMethods`, and a `workspace/executeCommand` + must carry a `params.command` in `commands`; anything else is + `plugin.host.methodNotDeclared`. A notification may be + `workspace/didChangeConfiguration` (the backend always knows the server's + settings; the value also becomes the settings the server reads through + `workspace/configuration`) or a `requestMethods` entry other than + `workspace/executeCommand`. The host's own methods stay refused. +- The session must belong to the calling plugin + (`plugin.host.foreignContribution` otherwise) and be running + (`plugin.host.languageServerUnavailable` otherwise, including every request + still pending when the process stops, crashes, restarts, or the plugin is + disabled). +- A request waits at most `timeoutMilliseconds` (default 60000, at most + 300000) and ends with `plugin.host.timeout`; cancelling it sends the server + `$/cancelRequest` and ends it with `plugin.host.cancelled`. A server's JSON-RPC + error is `plugin.host.languageServerError` with `details: {code, message}`; + the server's `data` is not forwarded. A result over the host's result bound + is `plugin.host.limitExceeded`. +- A **gate** is `{id, until: "notification", method, match}`, declared by the + launch answer. It opens when the server sends a notification with that + `method` whose params contain every member of `match` (nested objects match + the same way). Until every gate has opened, `request` calls wait in order, + at most 128; the 129th is `plugin.host.busy`. A waiting request still obeys + its timeout and cancellation. Notifications are never held. +- The first request for a `serializedCommands` command runs; a later request + for the same command, while the first is running or waiting, joins it and is + answered with the first one's outcome. Cancelling the first request ends the + joined requests with it. +- `forwardNotifications` reach the backend as + `lithe/host/languageServer/didReceiveNotification`. + `workspace/didChangeConfiguration` is the only notification Core consumes + itself. + +**State and retention.** The host keeps one state directory per workspace for +each server, under the plugin's storage (never under the package or application +bundle), and gives the server its own as `workspaceStateDirectory`. The +directories survive disabling and updating the plugin. The plugin chooses the +sub-directory names inside its `workspaceStateDirectory`; the host chooses the +names of the per-workspace directories themselves and never looks inside them. + +The host refreshes a workspace directory's modification time on every start of +the server and sweeps in the background after a start. It asks +`plugins.languageServers.stateRetention` with `{retention, nowUnixSeconds, +entries: [{name, lastModifiedUnixSeconds}], activeNames}`, where `entries` are +the per-workspace directories it found and `activeNames` are those of open +workspaces, and deletes the `expiredNames` it returns: a directory older than +`maxAgeDays` and, of the rest, all but the newest `maxEntries` that open +workspaces leave free. A workspace that is open is never pruned. Names that are +not a single path component are ignored, and the host deletes only names it +listed. A plugin that declares no `state.retention` keeps everything. + +The host can also discard the state of one workspace; the server rebuilds it +the next time it starts. In this version that is an engine path only, with no +user-facing action yet. + +**Preparation status.** `languageServer/setStatus` reports how ready the +server is. It exists because `lithe/function/progress` belongs to a call that +is running, while preparation (importing a build, indexing) outlives any call. + +Example package: `fixtures/languages/language-server-backend-launch.plugin.json`. + ### Execution providers `run` and `test` each name an `executable`: exactly one of a bare `command` @@ -662,8 +885,8 @@ hosts. Level 3 versions each section on its own. A level 3 manifest lists in | --- | --- | --- | | `languages` | `languages[]` | `1`, `2` (adds [`grammar` and `configuration`](#grammars-and-editor-configuration-languages-2)) | | `packageTrust` | `vendor.bundledSignatureRequirement`: origin-scoped trust for a host-sealed bundled copy of a publisher-signed plugin | `1` | -| `languageServers` | `languageServers[]` apart from their launch fields | `1`, `2` (adds [`workspaceConfiguration`](#workspace-configuration-languageservers-2)) | -| `serverProcess` | The launch fields of language servers, of build providers with a process of their own, of VCS providers, and of the plugin backend: `launch`, `arguments`, `validationArguments`, `runtimeDependencies`, `initializationOptions` | `1`, `2` (adds `runSelectedRuntime` for [debug adapters](#api-level-4-debug-adapters); level 4; v2 Core only, macOS and Windows advertise `1`) | +| `languageServers` | `languageServers[]` apart from their launch fields | `1`, `2` (adds [`workspaceConfiguration`](#workspace-configuration-languageservers-2)), `3` (adds the [session fields](#plugin-owned-sessions-languageservers-3)) | +| `serverProcess` | The launch fields of language servers, of build providers with a process of their own, of VCS providers, and of the plugin backend: `launch`, `arguments`, `validationArguments`, `runtimeDependencies`, `initializationOptions` | `1`, `2` (adds `runSelectedRuntime` for [debug adapters](#api-level-4-debug-adapters); level 4; v2 Core only), `3` (adds the [backend-resolved launch](#backend-resolved-launch-serverprocess-3) of a language server), `4` (names runtimes by [tool-definition ID](#tool-references-serverprocess-4)). Versions are cumulative: a manifest declares the lowest one covering every launch it uses. macOS advertises `1` and `3`, Windows `1` | | `executionProviders` | `executionProviders[]` | `1` | | `settings` | `settings[]` and `settingActions[]` | `1`, `2` (adds [placement, scope, effect, page buttons](#settings-v2-placement-scope-effect-and-page-buttons)) | | `ui` | `ui` | `1`, `2` (web content), `3` (native controls), `4` ([backend-only commands](#backend-only-commands-ui-4)) | @@ -671,11 +894,13 @@ hosts. Level 3 versions each section on its own. A level 3 manifest lists in | `buildProviders` | `buildSystems[].provider`, including own-process and shared language-server providers; v2 tasks, v3 tests, v4 run, v5 native debug preparation, v6 read-only model inputs, v7 native configuration, v8 inline test reports, v9 opaque semantic test selectors, v10 typed run options and service/task preparation, v11 registry connection debugging | `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `10`, `11` | | `buildModelInputs` | Optional public model sharing between enabled workspace plugins | `1` | | `vcsProviders` | `vcsProviders[]` | `1` | -| `debugAdapters` | [`debugAdapters[]`](#api-level-4-debug-adapters); v2 adds target arguments/environment | `1`, `2` | +| `debugAdapters` | [`debugAdapters[]`](#api-level-4-debug-adapters); v2 adds target arguments/environment; v3 adds [declared features and defaults](#adapter-features-and-defaults-debugadapters-3) | `1`, `2`, `3` | +| `outputLinkMatchers` | [`outputLinkMatchers[]`](#output-link-matchers-outputlinkmatchers-1): patterns that turn Run and Test console text into links | `1` | | `projectGenerators` | [`projectGenerators[]`](#project-generators): entries of the New Project window; v2 adds the [`tool` field](#tool-fields) | `1`, `2` | | `backend` | [`backend`](#plugin-backend): the plugin's own long-lived process | `1`, `2` (adds [start without a page and workspace contexts](#backend-2-workspace-contexts)) | | `toolDefinitions` | [Tool identities, layouts, discovery and four-layer candidates](toolchains/README.md) | `1` (Core; macOS settings pages, Build and run tools) | | `toolchainProviders` | [Provider declarations and permission-bound answer validation](toolchains/README.md) | `1` (Core; macOS settings pages, Build and run tools) | +| `pluginFunctions` | [`pluginFunctions`](#cross-plugin-calls-protocol-version-4): exported functions and declared calls between plugins | `1` (Core; macOS relays calls) | | `executableResolution` | [Shared executable candidates](#shared-executable-candidates-executableresolution-12): candidate ordering and strict selection; v2 adds tool-definition candidates and canonical project/subproject toolchain registry bindings without changing existing Build candidate sets | `1`, `2` (v2 Core; macOS settings pages, Build and run tools) | | `keymapImportProviders` | `keymapImportProviders[]`: independent shortcut import providers | `1` (macOS; Windows does not advertise it yet) | @@ -770,7 +995,12 @@ Rules for publishing across levels and capability versions: | `credentials.store` | the backend uses the [credentials host service](#host-services-protocol-version-2); refused without a `backend` | | `workspace.metadata` | the backend uses the [workspace host service](#workspace-contexts-and-capability-discovery-protocol-version-3); refused without `backend` 2 | | `editor.actions` | the backend registers independent editor actions; requires `backend` 2 and `documents.read`, grants only `editor/registerActions`, not document edits or domain execution | +| `editor.providers` | the backend supplies [editor language features](#editor-language-feature-providers-protocol-version-3) for matching files; requires `backend` 2 and `documents.read`, grants only `editor/registerProviders`, not document edits | | `terminal.create` | the backend creates and manages its own [native terminals](#plugin-owned-native-terminals-function-protocol-3), including bounded display history for reopen; requires `backend` 2 | +| `run.start` | the backend starts and stops host-owned [Run sessions](#run-and-debug-sessions-protocol-version-3); grants `lithe/host/run/start`, `lithe/host/run/stop` and the `lithe/host/run/didExit` notification; requires `backend` 2 | +| `debug.start` | the backend starts a [debug session](#run-and-debug-sessions-protocol-version-3) on one of its own adapters; grants only `lithe/host/debug/start`; requires `backend` 2 and a declared `debugAdapters` entry | +| `languageServer.request` | a `languageServers` 3 server lists `requestMethods` or `forwardNotifications`, and only then; requires `backend` 2. Grants the [`languageServer` host methods](#language-server-host-methods) for the plugin's own servers and their two notifications, nothing else | +| `documents.contentProvider` | the backend registers a [content provider](#content-providers); requires `backend` 2. Grants `documents/registerContentProvider` only, not reading or editing documents | | Activation event | Meaning | | --- | --- | @@ -1845,12 +2075,50 @@ A manifest below level 4 may not declare `debugAdapters`, and a level 3 manifest | `configurations.launch/attach` | Default DAP `arguments`, a packaged `parametersSchema`, and `targetBindings` (`program` ← `target.path`, `processId` ← `target.processID`, `cwd` ← `target.workingDirectory`, and for `attach` only `connection` ← `target.host`, `target.port`, `target.pathMappings`; version 2 also supports bounded `args` ← `target.arguments` and `env` ← `target.environment`; see [Remote attach](#remote-attach)). A configuration for an undeclared request is refused. | | `configurations.launch.executable` | For a target that is a source file the adapter cannot debug directly (C, C++, Swift): `setting` names a `file` setting of the plugin, `candidates[]` (up to 8) are the paths a host looks for when the user chose nothing. Each candidate starts with `${workspaceFolder}/` or `${fileDirectory}/`, may use `${fileBasenameNoExtension}`, and has no `..`. Declare it together with `targetBindings.program: "target.executable"` and neither without the other; a launch fills `program` with the path the host settled on. The user's setting always wins: a chosen path that is not a runnable file is an error, not a reason to try the candidates. The host shows the path it found in that setting, and the setting's `description` is where the plugin teaches how to produce the program. | | `capabilities.disabled` | Standard DAP `supports*` names or `exceptionBreakpointFilters` the host treats as absent. | +| `features` | `debugAdapters` 3. Adapter-specific DAP extensions the adapter implements; see [Adapter features and defaults](#adapter-features-and-defaults-debugadapters-3). | +| `defaults` | `debugAdapters` 3. Values the adapter starts with when the user configured none. | | `console.commandPrefix` | Text (1–8 characters, no spaces; for lldb-dap a backtick) the adapter's console reads as "what follows is a debugger command". The host puts it before the typed line only when it asks the adapter to complete, and moves the adapter's answer back, so the user never sees it. An adapter that treats a bare word as an expression (lldb-dap) otherwise mixes language keywords such as `long` into the first word's completions; the prefix limits them to commands. Declare at least this or `console.commands[]` when `console` is present. | | `console.commands[]` | Debugger commands (`name`, optional one-line `description`) the Debug Console offers as completions. An adapter that answers the DAP `completions` request completes live, so list commands only for an adapter that does not (or whose live answers lack some commands); the host merges both. Names are inserted as written, unique, and not padded with spaces; descriptions are shown as written and not translated. | -| `launch.process` | The shared `serverProcess` launch fields without `initializationOptions`, or `{"launch": {"kind": "runSelectedRuntime", "tool": "python" \| "java" \| "node"}}`, which needs `serverProcess` 2 and is the only use of it. Required for every transport except `tcpLanguageServer`, which must not have it. | +| `launch.process` | The shared `serverProcess` launch fields without `initializationOptions`, or `{"launch": {"kind": "runSelectedRuntime", "tool": "python" \| "java" \| "node"}}`, which needs `serverProcess` 2 or later and is the only use of 2; from `serverProcess` 4 `tool` is a [tool-definition ID](#tool-references-serverprocess-4). Required for every transport except `tcpLanguageServer`, which must not have it. | | `launch.transport` | `stdio`, `tcpAnnounced` (`endpoint`: `stream`, one-line `prefix`, `value` = `port` or `hostPort`), `tcpPort` (`endpoint.port`), or `tcpLanguageServer` (`endpoint`: `languageID`, `command`). TCP endpoints are loopback only. | | `launch.endpoint` for `tcpLanguageServer` | For an adapter that lives inside a language server and has no process of its own (the Java Debug Server runs in JDT LS). `languageID` names a `languageServers[]` entry of the same plugin; `command` (1–128 characters of letters, digits, `.`, `_`, `:`, `-`) is the `workspace/executeCommand` the host sends to that server for each session. The server answers with the loopback port to connect to, as an integer or a numeric string from 1 to 65535. The host starts the language server for the workspace first, as it does for editing. The command is part of what the user reviews at install and update, and changing it asks again. | +### Adapter features and defaults (`debugAdapters` 3) + +A host never recognizes an adapter by its ID. What is specific to an adapter +is declared, and the shared debug engine follows the declaration: + +```json +"features": { + "hotCodeReplace": { + "request": "script.reload", + "event": "script.reloaded", + "changedField": "modules", + "errorField": "error" + }, + "stepFilters": { "argument": "stepFilters" } +}, +"defaults": { + "stepFilters": { "classNameFilters": ["_vendor.*"], "skipSynthetics": true } +} +``` + +| Declaration | Meaning | +| --- | --- | +| `features.hotCodeReplace.request` | The DAP command the host sends to apply changed code to the running debuggee (the standard `redefineClasses` inspection). Present means the adapter supports hot code replace; absent means it does not. | +| `features.hotCodeReplace.event` | A DAP event whose `message` the Debug Console shows as progress. | +| `features.hotCodeReplace.changedField`, `errorField` | Fields of the response body: the list of replaced units, and a failure message the adapter reports in a successful response. A response without `changedField` replaces nothing visible. | +| `features.stepFilters.argument` | The launch argument through which the adapter receives stepping filters, shaped `{skipClasses, skipSynthetics, skipStaticInitializers, skipConstructors}`. An argument the user's configuration already sets is kept. | +| `defaults.stepFilters` | `classNameFilters` (up to 256 single-line patterns of at most 256 characters), `skipSynthetics`, `skipStaticInitializers`, `skipConstructors`, `hideFilteredStackFrames`. Used when neither the launch nor the user's settings name filters. | + +Names (`request`, `event`, fields, `argument`) are a letter followed by up to +63 letters, digits, `.`, `_`, or `-`. A declaration with a malformed name, or a +field this version does not define, is `plugin.manifest.invalid` with the +`field` that names it, and any `features` or `defaults` requires +`requires.debugAdapters` 3. The launch plan's `debugAdapter` descriptor carries +both so the host can pass them to `debug.createSession`. A plugin that declares +neither keeps the behavior of `debugAdapters` 1 and 2. + ### Remote attach An adapter that attaches to a debuggee on another machine (a JVM started with @@ -1957,6 +2225,57 @@ selection, a target missing a fact a binding reads; the request defaults to `fixtures/debug-adapters/debug-adapter-remote-attach.plugin.json` declares six adapters that differ in how they take a connection. +## Output link matchers (`outputLinkMatchers` 1) + +`outputLinkMatchers[]` lets a plugin make text in the Run and Test consoles +clickable, such as a traceback frame or a panic location. The host knows no +tool's output format: the plugin declares the pattern and Core finds the +matches. Requires `requires.outputLinkMatchers` 1. Matchers alone are a +complete plugin: one that declares nothing else is valid, and matchers without +`resolve` need no backend. + +```json +"outputLinkMatchers": [ + { "id": "traceback", "pattern": "File \"(?P[^\"]+)\", line (?P\\d+)", "scope": "run" } +] +``` + +| Field | Meaning | +| --- | --- | +| `id` | Plugin-local ID: a lowercase letter followed by up to 63 lowercase letters, digits, `.`, `_`, or `-`; unique in the plugin. At most 16 matchers per plugin. | +| `pattern` | A regular expression of 1 to 512 bytes matched against one console line. It uses the Rust `regex` syntax, so it has no backreferences and no look-around, and its compiled size is limited. It names at most 8 groups, each a camelCase name, and must not match empty text. | +| `scope` | `run` or `test`: the console the matcher applies to. Absent means both. | +| `resolve.function` | Optional ID of a function of the plugin's `backend` 2 process. Needs `backend` 2. | + +Without `resolve`, the pattern must capture a `path` group and may capture +`line` and `column` (1-based); no other group name is accepted. A relative +`path` becomes one candidate per workspace root, an absolute one counts only +inside a workspace root, and a path that leaves every root is not linked: a +console line can never open a file outside the workspace. With `resolve`, the +pattern may name any groups. The host calls the function when the user +clicks, with `{groups}` (the captured text by name) and the window's `context`, +and the backend answers `{uri, range?}` or `null`. + +Core owns the decision, so every host links the same text: + +- `outputLinks.match` takes `{scope, platform, caseSensitive?, workspaceRoots, + sources: [{pluginID, matchers}], lines}` and returns `{links}`. Each link is + `{line, start, length, pluginID, matcherID}` plus either `location` (`{paths, + line?, column?}`: the host opens the first existing path) or `resolve` + (`{function, groups}`). `start` and `length` count UTF-16 code units of the + line. Matches are found in source order and then declaration order, and a match + overlapping an earlier link is skipped. A request carries at most 256 lines, + 64 sources and 16 roots; a line is scanned up to 4096 bytes and links at most + 8 times. +- `outputLinks.decodeResolution` takes `{reply}` (what the function answered) + and returns the location to open, `{path, line?, column?}` with 1-based + positions, or `null`. A reply opens only a `file:` URI and only the members + `uri` and `range` are accepted; a 0-based `range.start` is converted. + +The host runs matching only for lines it displays, never for output a plugin +has not been enabled for, and ignores a link whose file does not exist. Fixtures: +`fixtures/run/output-link-matchers.json` and `fixtures/run/tool-reference-launch.plugin.json`. + ## Plugin backend A plugin with its own pages may need code that a page cannot run: database @@ -2003,8 +2322,11 @@ process starts and what it learns about workspaces. - It may start without a page. It needs at least one way to start: a web tool window (first page call, as in version 1), a `ui.commands[]` entry - (its `onCommand:` also starts the backend), or the `onWorkspaceOpen` - event. A backend 2 with none of them is `plugin.manifest.invalid`. + (its `onCommand:` also starts the backend), the `onWorkspaceOpen` + event, or a provider the host queries: a toolchain or keymap import + provider, or a language server with `launch.kind: "backend"`, whose start + asks the backend for its command line. A backend 2 with none of them is + `plugin.manifest.invalid`. - It is still one process per plugin, shared by every open workspace and window, on the machine that runs Lithe, also for remote workspaces. No workspace is process state: each call names its workspace explicitly @@ -2109,7 +2431,7 @@ tool settings, exactly as the tool's own settings page does. | Property | Meaning | | --- | --- | -| `tool` | Required. A tool definition ID: the short ID of a verified official definition, otherwise `:`. There is no closed list; a tool nobody declares has no installations. | +| `tool` | Required. A tool definition ID: a short ID such as `compiler` defined by exactly one enabled plugin, or `:`. There is no closed list; a tool nobody declares has no installations, and neither has a short ID several enabled plugins define, because it belongs to none of them. | | `minimumVersion` | Optional, one to three dotted numbers. An installation below it is not selectable. | | `pinAs` | Optional. `project` saves the selection as the created project's own tool choice (the same per-directory pin the tool's settings page edits), so the project builds and runs with the installation picked here. Without it the selection only feeds the steps. | | `optional` | Optional. Without a usable installation an optional field stays empty and leaves no pin; otherwise the form reports `required`. An installation below `minimumVersion` is not usable. | @@ -2220,11 +2542,15 @@ removes the staging directory and leaves nothing behind A plugin backend (a language server, build provider, or [plugin backend](#plugin-backend) process the plugin starts) can offer -**plugin functions**: plugin-private actions with -serializable params and results, which the host invokes on behalf of the same -plugin, for example from that plugin's own window. Functions complement the -typed domain protocols; they never replace them. Versions 1 to 3 of the protocol are -implemented in Rust Core (`rust/lithe-core/src/lsp/interface/plugin_function.rs`). +**plugin functions**: actions with +serializable params and results. A function is private to its plugin unless +the manifest exports it: the host invokes a private function on behalf of the +same plugin, for example from that plugin's own window, and another plugin +can call only an [exported function](#cross-plugin-calls-protocol-version-4). +Functions complement the +typed domain protocols; they never replace them. Versions 1 to 4 of the protocol are +implemented in Rust Core (`rust/lithe-core/src/lsp/interface/plugin_function.rs`, +`function_relay.rs`). macOS starts every build provider process as a session of the plugin that owns it and invokes its functions from that plugin's web pages (the `functions.*` bridge methods below). A provider served by a shared language @@ -2241,6 +2567,7 @@ Every capability has one owner, who defines its contract, and one direction: | Plugin provides a domain capability | Lithe: the typed domain contract | The plugin, declaratively or by its backend | A manifest `source`, Core plans and decodes, a typed `lithe/build/*` request | Build model, dependency tree; later task execution | | Plugin provides a plugin function | The plugin: its own params and results | The plugin's backend | `lithe/function/invoke` | Refresh a private snapshot, a step behind a button of the plugin's own UI | | Plugin calls a host capability | The host | The host | `lithe/host/*` requests, offered per protocol version | Run a process through the host's runner, write output, store state | +| Plugin calls another plugin's exported function | The exporting plugin: its own params and results, under an exported version | The exporting plugin's backend | `lithe/host/functions/invoke`, relayed by Core as `lithe/function/invoke` | A language plugin asks a build plugin for the options it contributes | Two rules keep these directions from forming a loop: @@ -2249,7 +2576,11 @@ Two rules keep these directions from forming a loop: then wait for itself. A backend request for `lithe/function/invoke` or any `lithe/build/*` method is refused with `plugin.function.recursiveCall`. A handler that needs, say, a Maven run asks for the host's process - capability, never for the task that its own plugin implements. + capability, never for the task that its own plugin implements. The + [relay methods](#cross-plugin-calls-protocol-version-4) are a different + request: they name another plugin's exported function, Core refuses a call + that would come back to a plugin already waiting on the call chain, and + the old refusals above stay in force. - Host capabilities are primitive host services, never another entry into a domain registry. Each is added to a protocol version together with its params, permission, and the operation it must belong to. Version 1 offers @@ -2265,10 +2596,12 @@ Two rules keep these directions from forming a loop: - A backend implements a domain operation through its typed method and Core's plan and decode commands, never through a plugin function, so IDE workflows, presentation, and other hosts keep a single contract. -- Plugin functions need no manifest field. What a running process can answer - is negotiated, as for [build providers](#build-providers); so declaring, - adding, or removing functions changes neither `pluginAPIVersion` nor any - `requires` version. +- A plugin-private function needs no manifest field. What a running process + can answer is negotiated, as for [build providers](#build-providers); so + declaring, adding, or removing private functions changes neither + `pluginAPIVersion` nor any `requires` version. Only a function that + another plugin may call, and a call to such a function, are declared, in + [`pluginFunctions`](#cross-plugin-calls-protocol-version-4). - A new kind of domain implementation, such as a provider for tasks, is a new version of the capability that owns it (`buildProviders`), so only plugins that use it are filtered from hosts that cannot run it. @@ -2276,7 +2609,7 @@ Two rules keep these directions from forming a loop: ### Negotiation For a session owned by a plugin (`lsp.startServer.pluginId`), the host offers -`capabilities.experimental.litheFunctions: { "versions": [1, 2, 3] }` in +`capabilities.experimental.litheFunctions: { "versions": [1, 2, 3, 4] }` in `initialize`, next to and independent of `litheBuild`. Host-owned sessions do not offer or accept this protocol, retain no plugin-function registry, and reject register/unregister with `-32601`. A plugin backend that offers @@ -2445,6 +2778,14 @@ notifications. `functionResultBytes`, `hostRequestBytes`, `hostResultBytes`, `errorMessageBytes` (the cut of a host failure's message), `errorDetailsBytes`, and `notificationBytes`. A backend must check `services` instead of assuming a method from the version. +- A service entry may also carry `options`: what the host declared for that + service. When `editor/registerActions` is granted, `services.editor.options` + has `actionIcons` (the icon tokens the host draws) and `limits` gains + `editorActionSourceBytes` (the largest document the host sends a + collector). When `editor/registerProviders` is granted, + `services.editor.options.providerTypes` lists the provider types the host + routes. A host that omits `actionIcons` accepts only the first editor-action + shape. - A context is one open workspace in one window: ```json @@ -2519,6 +2860,11 @@ notifications. | `plugin.host.preconditionFailed` | `-32019` | A state the request requires does not hold, such as a missing user confirmation | | `plugin.host.busy` | `-32020` | The resource is in use; retry later | | `plugin.host.rateLimited` | `-32021` | Too many requests in a period; retry after `details.retryAfterMilliseconds` when given | + | `plugin.host.languageServerUnavailable` | `-32022` | The plugin's language server is not running for the context, or stopped while the request was in flight | + | `plugin.host.languageServerError` | `-32023` | The language server answered with a JSON-RPC error; `details` is `{code, message}` | + | `plugin.host.methodNotDeclared` | `-32024` | The method, or the `workspace/executeCommand` command, is not in the server's declared session | + | `plugin.host.toolUnresolved` | `-32025` | A tool definition the request names resolves to no executable for this workspace; `details` is `{ tool, reason }`, `reason` being `notFound` (no candidate of the four layers meets the definition and its minimum version) or `unusable` (a chosen executable cannot be used) | + | `plugin.host.foreignContribution` | `-32026` | The named server or contribution belongs to another plugin | JSON-RPC reserves `-32768` to `-32000` for the protocol and lets implementations define `-32099` to `-32000` as server errors. `-32000` @@ -2552,6 +2898,9 @@ then never sees them. | `lithe/host/workspace/didChangeContexts` | `{ revision }` | `workspace.metadata` | | `lithe/host/subscriptions/notify` | `{ subscriptionID, sequence, event }` | every plugin-owned session | | `lithe/host/commands/didRun` | `{ commandID, context? }` | every plugin-owned session | +| `lithe/host/run/didExit` | `{ runID, exitCode?, signal? }` | `run.start` | +| `lithe/host/languageServer/didChangeState` | `{ context, serverID, state, generation }` | `languageServer.request` | +| `lithe/host/languageServer/didReceiveNotification` | `{ context, serverID, generation, method, params?, truncated? }` | `languageServer.request` | - `didChangeContexts` follows every change `revision` of `listContexts` counts; the backend calls `listContexts` for the contexts. A backend that @@ -2565,6 +2914,11 @@ then never sees them. running backend receives it in order with the other notifications; a stopped one is started for it only when it declares `onCommand:`, and receives it once ready. +- `run/didExit` reports the end of a Run session the backend started with + [`run/start`](#run-and-debug-sessions-protocol-version-3). `exitCode` is the + process's exit status and `signal` (1 to 127) the signal that ended it; both + are absent when the host lost the process. It is sent once per run, only + to a session that holds the `run.start` grant. - `revision` and `sequence` are positive integers, `event` is an object, and serialized params are at most `limits.notificationBytes`. Core refuses any other method, a service the session is not granted, malformed params, @@ -2635,6 +2989,7 @@ methods. Unknown options fail before side effects with `unsupportedOption`. | `documents/list` | `documents.read` | none | `{documents:[]}` | | `documents/getSnapshot` | `documents.read` | `documentID` | `{document:, text}` from the dirty live buffer | | `editor/registerActions` | `editor.actions` (+ `documents.read`) | `id,function,selector` | `{subscriptionID}`; independent editor actions, see below | +| `editor/registerProviders` | `editor.providers` (+ `documents.read`) | `id,selector,providers` | `{subscriptionID}`; editor language-feature providers, see below | | `editor/getActive` | `documents.read` | none | summary of the foreground text tab, or `null` | | `editor/getSelections` | `documents.read` | `documentID` | `{documentID,version,selections:[{anchor,active}],primaryIndex}` | | `editor/applyEdits` | `editor.edit` | `documentID,expectedVersion,edits:[{range,text}]` | `{documentID,version,applied}` | @@ -2691,6 +3046,184 @@ On `versionConflict`, reread and recompute; on `contextExpired`, resolve the original target again, never substitute the focused window. `busy`/`rateLimited` may be retried with bounds; `permissionDenied`/`readOnly` need user intervention. +### Run and debug sessions (protocol version 3) + +A backend can start a session that the host presents as its own: a Run session +in the Run tool window, or a debug session on one of the plugin's own adapters. +The backend decides what to start; the host owns the process, the console, the +Stop button and the end of the session. A Run session adds host presentation +(a Run window entry the user can stop, the console, the exit status) on top of +what `process.run` already allows; it grants no other capability. `run/start` +and `debug/start` take `context: {contextID, generation}` and the permission +shown. `run/stop` takes no `context`, so a run can still be stopped after the +context it started in has closed. + +| Method (prefix `lithe/host/`) | Permission | Params | Result | +| --- | --- | --- | --- | +| `run/start` | `run.start` | `context, label, process, temporaryFiles?, debug?, rerun?` | `{runID}` | +| `run/stop` | `run.start` | `{runID}` or `{sessionID}` | `null` | +| `debug/start` | `debug.start` | `context, label, adapter, request, configuration` | `{sessionID}` | + +`run/start`: + +- `label` is 1 to 256 characters without control characters. It titles the + Run window entry. +- `process` is `{executable, arguments, workingDirectory, environment}`, all + four required. `executable` is `{kind: "tool", tool}`, a tool that the + manifest lists in `requiresTools` (the host resolves it through the + [tool-definition layers](toolchains/README.md)), or `{kind: "packaged", path}`, a + normalized relative path of a file inside the plugin package. Arguments, the + working directory (relative to the workspace root and an existing directory + inside it) and the environment have the bounds of + [run preparation](#provider-run-preparation-buildproviders-4). An unknown + `process` field is refused. +- `temporaryFiles` is at most four `{name, contents}` files of up to 4 MiB of + text each. The host writes them into its own temporary directory before the + process starts, removes them when the run ends, and replaces + `${temporaryFile:}` in `process.arguments` and `process.environment` + with the file's path. A name is unique, 1 to 64 letters, digits, `.`, `_` or + `-`, and starts with a letter or digit. A plugin therefore never needs to + know where the host keeps temporary files. +- `debug` is `{configurationType, target}` and makes the run a debug session. + `configurationType` is a public debug adapter alias, as in + [registry connection debugging](#registry-connection-debugging-buildproviders-11). + `target` is `{kind: "connection", port}`: the process listens for a debugger on + `port` (1 to 65535; the backend puts it into the arguments), the host waits + for it and attaches the adapter. Or `{kind: "launch"}`: the adapter starts the + process itself, taking program, arguments, directory and environment from + `process` through its `targetBindings`. +- `rerun` is `{function, data}`: one of the plugin's registered functions and + its params, a JSON object of up to 64 KiB. When the user chooses Run again, + the host invokes the function with `data` as the params and the current + `context`, and the backend answers with a new `run/start`, so the backend + prepares the restart. Without `rerun` the entry cannot be rerun. +- The reply carries an opaque `runID`. The host sends + [`run/didExit`](#host-notifications-protocol-version-3) when the process ends. + +`run/stop` stops a run by `runID` or a debug session by `sessionID` and answers +`null`, also when it already ended. Cancelling the host request is the same as +stopping. A backend can stop only runs and sessions it started. + +`debug/start`: + +- `label` is as in `run/start`. `adapter` is one of the plugin's own adapters, + by its local ID or its qualified `/` ID. An adapter of + another plugin is refused with `plugin.host.notFound`, and one the plugin does + not declare is `plugin.host.notFound` too. +- `request` is `launch` or `attach`, and the adapter must serve it. +- `configuration` is a JSON object of at most 256 KiB that the host passes to + the adapter as the DAP launch or attach arguments, on top of the adapter's + declared defaults. `noDebug` and every key that starts with `__` belong to + the host and are refused. + +Both methods refuse malformed params with `plugin.host.invalidParams`; the +rejection from Core (`plugin.run.invalidStart` or `plugin.debug.invalidStart`, +with the `field`) is in `details.code` and `details.field`. A closed context is +`plugin.host.contextExpired`, and a `tool` that resolves to no executable is +`plugin.host.toolUnresolved`. + +The host keeps running processes when the plugin is disabled or its backend +exits; it only stops telling the backend. The run then ends the way a run +without a backend ends. A backend that starts again does not receive +`run/didExit` for a run it did not see begin. + +Fixtures: `fixtures/run/run-start.json` (what `run.decodeStart` returns for each +case, including the rejections) and `fixtures/debug-adapters/debug-start.json` (the same for +`debug.decodeBackendConfiguration`). Temporary files depend on the host's +temporary directory and are covered by Core's own tests. + +### Language server host methods + +The `languageServer` host service lets a plugin backend talk to the language +servers its own plugin declares. It is granted by the `languageServer.request` +permission, only for a `languageServers` 3 server that lists `requestMethods` +or `forwardNotifications`, and each method is granted by its full name. +`getCapabilities` lists the methods this host implements. Windows does not +advertise `backend` support. + +Every method takes `context: {contextID, generation}` and `serverID`, the +`languageID` of one of the calling plugin's servers. A server of another +plugin is `plugin.host.foreignContribution`. + +| Methods (prefix `lithe/host/languageServer/`) | Params in addition to `context`, `serverID` | Result | +| --- | --- | --- | +| `request` | `method`, `params?` (an object, default `{}`), `timeoutMilliseconds?` (1 to 300000, default 60000) | `{value}`: the server's JSON-RPC `result`, which may be `null`. | +| `notify` | `method`, `params?` | `{delivered}`. `false` only for `workspace/didChangeConfiguration` sent before the server is initialized; the value is then sent with `initialized`. | +| `setStatus` | `state` (`preparing`, `ready`, `degraded`, or `failed`), `message?`, `percentage?` (a whole number from 0 to 100) | `{}` | + +- `request` and `notify` are checked by the [session policy](#plugin-owned-sessions-languageservers-3): + `plugin.host.methodNotDeclared` for a method or command the server did not + declare, `plugin.host.languageServerUnavailable` when the server is not + running for this context, `plugin.host.languageServerError` when the server + answers with an error (`details: {code, message}`), `plugin.host.timeout`, + `plugin.host.cancelled`, `plugin.host.busy` for the 129th waiting request, + and `plugin.host.limitExceeded` for a result over the host's result bound. + Cancelling the host request (`$/cancelRequest`) cancels the server request. +- `setStatus` shows the plugin's own preparation state next to the server in + the host's status UI: `preparing` with an optional `message` and + `percentage` while the plugin imports a project or indexes, `ready` when the + server can answer, `degraded` when it works with reduced results, and + `failed` when it cannot. `message` is user-facing text of the plugin, shown + as given and cut to 1024 bytes on a character boundary. The latest status + replaces the earlier one of the same server; a new + server `generation` starts without one, and the host removes it when the + server stops. It is not `lithe/function/progress`, which belongs to the + function call that is running, while preparation outlives every call. +- The language server keeps no state of its own in the host: when the process + stops or crashes, every request in flight ends with + `plugin.host.languageServerUnavailable`, and the backend asks again after + `didChangeState` reports `running` with a new `generation`. + +Notifications to the backend, granted by the same permission: + +| Notification | Params | Meaning | +| --- | --- | --- | +| `lithe/host/languageServer/didChangeState` | `{context, serverID, state, generation}` | `state` is `starting`, `running`, `stopped`, or `crashed`. | +| `lithe/host/languageServer/didReceiveNotification` | `{context, serverID, generation, method, params?, truncated?}` | A notification the server sent and `forwardNotifications` lists. | + +`generation` is a positive integer the host raises each time it starts the +server for this context, so a backend can tell which process a state or a +notification belongs to; the host never delivers one of a replaced process +after the new one has started. A forwarded notification larger than +`limits.notificationBytes` is delivered once with `truncated: true` and +without `params`. Core enforces the request policy (whitelist, gates, +serialization, timeouts) when it relays a request, and +`fixtures/backend/function-protocol.json` holds the request, response, and +notification shapes every host must reproduce. + +### Content providers + +A plugin whose server hands out documents that have no file (for example the +decompiled source of a library class) serves them as a URI scheme: + +```json +{ + "context": {"contextID": "w1", "generation": 1}, + "scheme": "metals-library", + "function": "documents.read" +} +``` + +`lithe/host/documents/registerContentProvider`, granted by +`documents.contentProvider`, returns `{subscriptionID}`; dispose it with the +existing subscription API. Registration only records the binding and never +calls the backend; the function must already be registered on the session. + +- A scheme is 1 to 64 lowercase ASCII letters, digits, `+`, `-`, or `.`, and + starts with a letter. The schemes the host or the platform resolves are + refused: `about`, `blob`, `data`, `file`, `ftp`, `http`, `https`, + `javascript`, `lithe`, `mailto`, `untitled`, `ws`, and `wss`. A scheme has + one provider per workspace window; registering it again is + `plugin.host.preconditionFailed`. +- When a document whose URI has that scheme is opened, the host calls the + function through `lithe/function/invoke` with the workspace `context` and + `{uri}`. The answer is a string, or `{content: string}`, of at most 1 MiB. + The document opens read-only; any other answer, a failure, or a timeout + (the host waits 30 seconds) shows the user an error and opens nothing. +- The provider is the only source of the text. The host keeps no copy once + the document closes, and it asks again when the document is reopened. + Ending the session or closing the context withdraws the provider. + ### Backend-only commands (`ui` 4) `requires.ui:4` permits `ui.commands` without `toolWindowID` and allows an empty @@ -2746,8 +3279,10 @@ workspace context: - The host invokes the collection function via existing `lithe/function/invoke` with workspace context and `{document:{documentID,version,path},text}`. Version is the same public buffer version as `documents/getSnapshot`, not a WebView - revision. Source is the dirty live buffer, capped at 128 KiB. Remote workspaces - and read-only preview surfaces are not served by this first implementation. + revision. Source is the dirty live buffer, capped at the host's + `limits.editorActionSourceBytes` (1 MiB on macOS; 128 KiB when the host does + not report the limit). Remote workspaces and read-only preview surfaces are not + served by this first implementation. - Collection is read-only by contract: no network request or business execution. Edits invalidate the old actions immediately and cancel older collection work; the host debounces fresh collection and rejects stale or withdrawn results. @@ -2758,20 +3293,42 @@ workspace context: invoke:{functionID,params?}}]}`. Positions are one-based UTF-16, half-open and cannot split surrogate pairs. Each range stays in the exact input document. `id` is 1 to 128 bytes without controls, unique per answer; `title` is 1 to 200 - characters on one line. `icon` is `run` or `run-all`, a host-owned presentation - token. At most 1000 actions and 1 MiB serialized; malformed answers are refused - whole. Same-line actions share a menu rather than silently replacing each other. + characters on one line. `icon` is a host-owned presentation token from + `services.editor.options.actionIcons` (`run` or `run-all` when the host does not + report the list). At most 1000 actions and 1 MiB serialized; malformed answers + are refused whole. Same-line actions share a menu rather than silently replacing + each other. +- An action may carry `alternatives`: at most 4 `{id,title,icon,invoke}` entries + with the same rules and IDs unique within the action, offered in the action's + menu after the action itself (for example Debug or Test beside Run). Hosts that + omit `actionIcons` refuse an answer with alternatives. +- The macOS host draws `run`, `run-all`, `debug`, `test`, `test-passed`, + `test-failed`, `test-skipped`, `navigate-super` and `navigate-implementations`. + An icon names a picture, not an operation: a test icon does not make the + action a test, and the host never starts anything because of an icon. - Each invocation target is a currently registered function of the same plugin; `params` is a plugin-private JSON object. The host stores it behind an opaque per-batch action key, never accepts invocation payloads from the web editor, and validates document/context/session/registration again on click. It calls the function with `{documentID,expectedVersion,actionID,data:}` through the - ordinary invocation lifecycle. The plugin validates/re-reads the live document - before side effects. Click executions are not cancelled by unrelated re-parsing; + ordinary invocation lifecycle; a chosen alternative adds `alternativeID` and + passes that alternative's `params` as `data`. The plugin validates/re-reads the + live document before side effects. +- A click result may carry `{reveal:{locations:[{uri,range}]}}`: 1 to 100 + absolute URIs with zero-based LSP ranges. The host opens a single location and + lists several in its navigation results. Other result members stay the + plugin's own. Hosts that omit `actionIcons` ignore the result. +- A click that runs or debugs code starts it through the host's run and debug + methods (`lithe/host/run/start`, `lithe/host/debug/start`) once + `getCapabilities` lists them, so the run appears in the host's Run and Debug + windows. A private process started by the plugin is not a substitute. Click executions are not cancelled by unrelated re-parsing; withdrawing the binding, ending its session/context or tearing down the owning editor cancels them. The host never retries side effects automatically. - Registration/collection validation is shared Core command - `plugins.editorActions.decode`; public host failures retain protocol 3's existing + `plugins.editorActions.decode`; `version: 2` with the host's icons and source + bound admits alternatives and click results (`kind: invokeResult`), and version 1 + keeps the first shape. Cases: `fixtures/editor/editor-actions-v2.json`. Public host + failures retain protocol 3's existing distinct codes (`invalidParams`, `permissionDenied`, `versionConflict`, etc.). No tool-private error or result is interpreted as a build/test result. - Existing `commands/didRun {commandID,context?}` is unchanged. A command-palette @@ -2782,9 +3339,348 @@ workspace context: older host that does not recognize that permission, rather than silently granted. Grammar capability admission is separate. SDK/contract mirrors still need sync. +### Editor language-feature providers (protocol version 3) + +A provider adds a language feature the file's language server does not +supply: a cross-file fact only the plugin knows, such as a key in a template +that names an entry in a message bundle, or a framework declaration in a +configuration file that names a method in source. It never replaces the +language server. Where a language server answers, its answer comes first and +plugin results only add to it; a plugin that repeats what the server already +says shows the user the same thing twice. + +A backend declares `backend: 2`, `documents.read` and `editor.providers`, +finds `lithe/host/editor/registerProviders` and +`services.editor.options.providerTypes` with `getCapabilities`, then registers +per workspace context: + +```json +{ + "context": {"contextID": "w1", "generation": 1}, + "id": "message-keys", + "selector": {"languages": ["html"], "fileExtensions": ["properties"], "fileNames": []}, + "providers": { + "definition": {"function": "keys.definition"}, + "hover": {"function": "keys.hover"}, + "completion": {"function": "keys.complete", "triggerCharacters": ["."]}, + "semanticTokens": {"function": "keys.tokens", + "legend": {"tokenTypes": ["property", "string"], "tokenModifiers": ["declaration"]}} + } +} +``` + +- Registration returns `{subscriptionID}`; dispose it with the subscription + API. Ending the session, the context or the plugin's activation withdraws + every registration it made, and a plugin activated later registers again. + Each named function must already be registered on that session. +- A selector has 1 to 32 entries in total: language IDs, literal extensions + (optional leading dot) and literal file names, matched case-insensitively. + A document matches when any entry matches. At most 32 registrations per + plugin across contexts; duplicate IDs in a context are refused. +- `providers` maps a provider type to `{function}`. The macOS host routes + `definition`, `hover`, `completion`, `codeLens`, `foldingRange`, + `semanticTokens` and `documentLink`. Completion may add 1 to 16 + single-character `triggerCharacters`; semantic tokens must add their + `legend` (at most 64 types and 64 modifiers). A type outside the host's + `providerTypes` fails with `unsupportedOption` and + `details.option: "providers."` before anything is registered. + Diagnostics are not a provider type. +- The host calls the function through `lithe/function/invoke` with the + workspace context and `{documentID,version}`; definition, hover and + completion add `position: {line,character}` (zero-based, UTF-16), and + completion adds `triggerCharacter` when typing started it. The plugin reads + the text with `documents/getSnapshot` and answers for that version. +- Results use the LSP 3.17 shapes of the matching request: definition + `Location | Location[] | LocationLink[] | null`, `Hover | null`, + `CompletionItem[] | CompletionList | null` (no `data`, `command` or resolve + round trip), `CodeLens[] | null`, `FoldingRange[] | null`, + `SemanticTokens | null` indexed by the registered legend, and + `DocumentLink[] | null` with absolute targets. Every code lens carries its + `command`, whose `command` is a function of the same plugin and whose + `arguments` holds at most one params object; a click calls that function + with `{documentID,expectedVersion,data:}`. A malformed result is + dropped whole. +- Budgets are the host's: hover and completion 400 ms, definition 1500 ms, + the rest 2000 ms. A late answer, or one for an older document version, is + dropped without an error. A registration cannot set its own budget. +- The host merges: hover sections and completion items follow the language + server's; definitions add plugin locations to the server's (a location at + the requested position itself yields to the plugin's); code lenses, folding + ranges and links are added; semantic tokens apply only to files whose + language server has none. A plugin location equal to one already kept is + dropped. The merge is Core command `plugins.editorProviders.decode` with + `kind: "mergeDefinitions"`, so every host merges definitions alike. +- Validation is shared Core command `plugins.editorProviders.decode`. Cases: + `fixtures/editor/editor-providers.json`. Example package: + `fixtures/editor/editor-providers.plugin.json`. +- This is an additive host method within protocol 3, not a version bump. A + package requiring `editor.providers` is refused by an older host that does + not recognize the permission. + +### Cross-plugin calls (protocol version 4) + +A plugin function is private to its plugin unless the plugin exports it. An +exported function can be called by another enabled plugin whose manifest +declares that call. Core relays every call: the calling backend never +reaches the provider's process, the provider's backend never learns more than +the call it serves, and the host takes part only by starting a provider's +backend that is not running (see below). The relay is Rust Core logic, the same +on every platform that runs plugin backends. +`fixtures/backend/plugin-functions-exporter.plugin.json` and +`fixtures/backend/plugin-functions-caller.plugin.json` are the examples. + +#### Declaring exports and calls + +```json +"requires": { "pluginFunctions": 1, "backend": 2, "serverProcess": 1 }, +"pluginFunctions": { + "exports": [{ "id": "document.format", "version": 1, "context": "workspace" }], + "calls": [{ "id": "project.describe", "version": 2 }] +} +``` + +- `pluginFunctions` needs `pluginAPIVersion` 3 or later, a `backend`, and + `requires.pluginFunctions: 1`, `backend: 2` and `serverProcess: 1`. The + section holds at least one of `exports` and `calls`, each at most 32 + entries sorted by `id`, with no `id` twice. Anything else, including an + unknown field, is `plugin.manifest.invalid`. `requires` lists + `pluginFunctions` exactly when the section exists. +- An export is an object, never a bare function ID. `id` is a function ID + as in [References and IDs](#references-and-ids). `version` is the + exported contract version, an integer from 1 to 1000; an incompatible + change to the params or result takes a new version. `context` is a closed + set; version 1 knows only `"workspace"`: the function runs only for a + local workspace context, on a call Core relays. A missing or unknown + `context` refuses the whole manifest with `plugin.manifest.invalid`, so a + function whose context is unstated is never callable. +- A call is `{ id, version }`. It names a function ID and the version the + caller was written against, never the providing plugin's ID: the caller + discovers providers at run time. Core requires the versions to be equal; + otherwise the call fails with `plugin.function.versionMismatch`. +- Declaring an export does not register it. The provider's backend still + registers the function with `lithe/function/register`, and a function it + has not registered, or has withdrawn, is `plugin.function.notExported`. + A function the manifest does not export is private whatever the backend + registers, and a reference to it keeps answering + `plugin.function.wrongPlugin`. +- A plugin that exports needs no page, command or `onWorkspaceOpen` to start + its backend: a call starts it. A plugin that only calls needs one of + those, as every [`backend` 2](#backend-2-workspace-contexts) does. +- `pluginFunctions` grants no manifest permission. The caller's `calls` + and the provider's `exports` are the two sides' consent, and the host + grants a plugin that declares `calls` the `functions` host service, + through `backend.hostServices` of its launch plan, like every other + service. A host without `pluginFunctions` in `hostCapabilities` refuses + such a manifest with `plugin.api.unsupported`. + +#### What an exported function must and must not do + +- **Trust is not transitive.** The provider runs with its own permissions, + its own state directory and its own credentials. The caller gains none of + them, and the provider gains none of the caller's. Core never writes the + caller's plugin ID into the provider's session; it appears only in the + `caller` field of one request, below. +- **The provider owns its resources and processes.** An exported function + must not accept an executable path, command line or script from its caller + and start a process with it, nor read or write a path the caller chose + without checking it against what the provider itself may access. If a + caller needs the provider to run something, the function takes a request + the provider interprets (a task name, a document) and the provider decides + the executable. +- A function that decides by who asks reads the `caller` field below, never + a plugin ID in `params`, which any caller can write. + +#### Discovery and invocation + +A backend that negotiated protocol version 4, and whose plugin declares at +least one call, may send two requests. A backend on version 3 or earlier gets +`-32601`, `plugin.host.unavailable`; a plugin that declares no call gets +`-32010`, `plugin.host.permissionDenied`. `getCapabilities` lists the +methods the session may call under the `functions` service. + +| Request | Params | Result | +| --- | --- | --- | +| `lithe/host/functions/providers` | `{ functionID }` | `{ providers: [{ pluginID, version }] }` | +| `lithe/host/functions/invoke` | `{ pluginID, functionID, context: { contextID, generation }, params?, timeoutMilliseconds?, parentOperationID? }` | `{ value }` | + +- `providers` lists the enabled plugins other than the caller that export + `functionID`, sorted by plugin ID, each with its exported `version`. + Disabling, enabling, replacing and uninstalling a plugin take effect for + the list at once; a host that registers backend plugins only when it + launches (macOS today) lists a newly installed or updated backend plugin + only after a restart of Lithe (see [Hot activation](#hot-activation)). The + caller must have declared the function, otherwise + `plugin.function.notDeclared`. A listed provider's backend need not be + running. +- `invoke` calls one provider. `context` is required: a workspace context + the caller learned from the [workspace service](#workspace-contexts-and-capability-discovery-protocol-version-3), + checked like every call context. `params` is a JSON object and defaults + to `{}`. `timeoutMilliseconds` is a positive integer, defaults to 60 000 + and is capped at 600 000. A field the method does not define is `-32018`, + `plugin.host.unsupportedOption`. The reply is `{ value }`, where `value` + is the provider's result (`null` is a result). +- `parentOperationID` is optional: a string of 1 to 128 bytes, otherwise + `-32602`, `plugin.host.invalidParams`. A backend that makes a nested call + while it serves a relayed call sets it to the `operationID` of the + `lithe/function/invoke` it is serving (the field shown in + [The caller field](#the-caller-field)), so Core knows which call the new + one belongs to. Its effects are described in + [The call chain](#the-call-chain). A backend should always set it: without + it Core treats the new call as a child of every relayed call the backend is + serving. +- The caller cancels a call with `$/cancelRequest` for its `invoke` request + id; it fails with `plugin.host.cancelled`. + +Core checks, in this order, and refuses a call before anything is written to +the provider; the first failing check wins: + +| Check | Code | +| --- | --- | +| The caller declared the function | `plugin.function.notDeclared` | +| The provider is installed and enabled | `plugin.function.providerDisabled` | +| The provider exports the function | `plugin.function.notExported` | +| The declared and exported versions are equal | `plugin.function.versionMismatch` | +| The context is open, and has this generation | `plugin.host.contextExpired` | +| The context is a local workspace | `plugin.host.authorityUnsupported` | +| The params are at most 256 KiB | `plugin.function.payloadTooLarge` | +| The call named by `parentOperationID` is still running | `plugin.host.cancelled` | +| The provider is not on the call chain, the caller included | `plugin.function.callCycle` | +| The chain has at most 4 calls | `plugin.function.depthExceeded` | +| The caller has at most 8 relayed calls running | `plugin.function.tooManyCalls` | + +A provider's own failure keeps its meaning. The reply is a JSON-RPC error +with `data: { "code", "details" }`; when the provider's function answered with +an error, `code` is `plugin.function.failed` (`-32070`), `message` is the +provider's message cut to 1024 bytes, and `details.pluginErrorCode` is the +provider's own `error.data.code`. A reply with no `result`, or with one over +1 MiB serialized, is `plugin.function.invalidResult` (`-32071`); any other +result over 256 KiB is `plugin.function.payloadTooLarge`. Neither number is +`-32603`, which stays `plugin.host.failed`, a failure inside Core. The relay's deadline passing is +`plugin.host.timeout`. A call that is running normally ends through the +provider session's own timeout; if the provider has still not answered 2 +seconds after the deadline, the relay ends the call with +`plugin.host.timeout` itself and cancels the provider's operation. A +provider whose backend exits, or that withdraws +the function, while the call runs ends it with +`plugin.function.providerUnavailable`; a provider that is disabled or +uninstalled, or whose update stops exporting the function or changes its +version, ends it with `plugin.function.providerDisabled`. A caller whose +update stops declaring the call ends it with `plugin.function.notDeclared`. +A workspace context that the host closes, or replaces with a newer +generation, ends the relayed calls that carry it with +`plugin.host.contextExpired`: both the calls still waiting for a provider and +the calls already running, whose provider also receives `$/cancelRequest`. +Core never restarts a backend or retries a call to finish one. + +`fixtures/backend/function-protocol.json` holds the cases: the host requests +(`hostRequests`), the numbers of every failure (`relayErrors`) and the +`caller` field (`invocationCallers`). + +#### The call chain + +Core records the chain a call belongs to. A provider that makes a call while it +serves a relayed call names that call in `parentOperationID`. The new call is +then a child of that call only: + +- Its chain is the chain of that call plus the caller. +- Cancelling that call, its timing out, or its ending cancels the new call. + +A call to a plugin already on the chain, including a plugin calling itself, is +`plugin.function.callCycle`: the plugin on the chain is waiting for the call +that would reach it. A chain has at most 4 calls. So a provider that is asked +to apply values must not call back the plugin that asked inside that call; it +records the values, or calls after the call ends. + +- If `parentOperationID` names a call the host started directly, not a relayed + one, the new call starts a new chain. +- If it names a call this backend is not serving (one that has finished, or + one that never existed), the call is refused with `plugin.host.cancelled` + and the message "The call this request belongs to is not running." +- If it is omitted, the call counts as a child of every relayed call the + backend is serving at that moment. This is kept for compatibility with + backends written before the field; SDKs should always fill it in. + +Cancelling a call, its timing out or ending, or the end of its caller's +session, cancels the calls made as its children, down the chain, and each +provider receives `$/cancelRequest`. The relay never retries a call. + +#### Isolation between backends + +Core writes to each backend session through a queue of its own. A backend +that stops reading its standard +input only delays the calls and replies sent to it; the relayed calls of other +plugins continue. If a write to a backend stays blocked past the request +timeout, Core ends that backend's session as a transport failure +(`transportFailed`, stage `relayWrite`), and the calls of that session end like +those of any session failure. + +#### The caller field + +For every relayed call Core forwards `lithe/function/invoke` to the provider +as for any call (see [Invocation](#invocation)) and adds one top-level field: + +```json +{ + "functionID": "project.describe", + "operationID": "7", + "context": { "contextID": "w1", "generation": 1 }, + "params": { "project": "app" }, + "caller": { "pluginID": "dev.lithe.fixture.python" } +} +``` + +- `caller.pluginID` is written by Core from the identity of the calling + session. It is a sibling of `functionID`, `operationID`, `params` and + `context`, never inside `params`, and the caller cannot supply it: a + `lithe/host/functions/invoke` request carrying a top-level `caller` is + refused with `-32018`, `plugin.host.unsupportedOption`. A `caller` key + inside the caller's own `params` is ordinary data and is passed unchanged. +- A call the host itself started has no `caller` field. Absent means the + host. +- A provider that negotiated version 3 or earlier never receives the field. + A provider that relies on it must negotiate version 4; a provider that + keeps per-caller state (for example a slot per calling plugin) keys it by + this ID, which cannot be forged. + +#### Providers that are not running + +If the provider's backend is not running, Core waits for it and emits the +runtime event `functionProviderStartRequired` with +`result: { "pluginID" }` on the calling session, once per waiting provider. +The host starts that plugin's backend as an ordinary plugin start, and Core +dispatches the call when the provider has negotiated the protocol and registered +the function. If that takes longer than 30 seconds the call fails with +`plugin.function.providerUnavailable`; the relay's deadline still applies. +A host that cannot start the backend ignores the event, and the call ends the +same way. + +#### Remote workspaces + +The relay is a local-workspace feature. A call whose context is a remote +workspace is refused with `plugin.host.authorityUnsupported`, and the +exported `context` value `"workspace"` means a local workspace only. Backends +still run on the machine that runs Lithe. + +#### Hot activation + +Disabling, enabling, replacing and uninstalling a plugin take effect for the +relay at once and need no restart of Lithe or of any other plugin. +The host sends the plugin and workspace-context snapshot with the Core +command `lsp.configureFunctionRelay` +([Rust Core API](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/contracts/rust-core-api.md)), +and calls the new snapshot no longer allows end at once with the errors above. + +Installing or updating a plugin that has a backend is different on a host that +registers backend plugins only when it launches (macOS today): that host lists +a newly installed or updated backend plugin in `providers` only after a +restart of Lithe. + ### Compatibility -- Packages of every API level are unchanged: functions add no manifest field. +- Packages of every API level are unchanged: private functions add no + manifest field. `pluginFunctions` is a new capability + (`requires.pluginFunctions: 1`), so only a plugin that exports or calls + functions is refused by a host that cannot relay them. - A backend written before this protocol does not answer `litheFunctions`. It keeps serving its language and its `litheBuild` requests, receives no function calls, and its function requests are answered with `-32601`. @@ -2796,13 +3692,17 @@ workspace context: - A backend that answers `version: 1` keeps working unchanged: version 2 only adds host services. A backend that answers `version: 2` keeps working unchanged: version 3 only adds methods, notifications, the call - context, and failure codes and details, all of which it never sees. + context, and failure codes and details, all of which it never sees. A + backend that answers `version: 3` keeps working unchanged: version 4 only + adds the relay methods, which it must not call, and the `caller` field + of a relayed call, which it never receives. - `requires.backend: 1` packages keep their meaning; a new behavior of the backend capability is `backend` 2, never a silent change of version 1. - `fixtures/backend/function-protocol.json` holds the golden cases, including the - host requests (`hostRequests`), call contexts (`invocationContexts`), host - failure codes (`hostFailures`), and host notifications - (`hostNotifications`). Rust Core tests run them, and a scripted backend covers routing, result, failure, cancellation, + host requests (`hostRequests`), call contexts (`invocationContexts`), the + caller of a relayed call (`invocationCallers`), host + failure codes (`hostFailures`), relay failure codes (`relayErrors`), and + host notifications (`hostNotifications`). Rust Core tests run them, and a scripted backend covers routing, result, failure, cancellation, deadline, late reply, revocation, and compatibility with a build-only backend. @@ -2865,17 +3765,29 @@ command payload is a Core `invalid_request` failure instead. | `plugin.permission.undeclared` | Core | A provider or build system is declared without its permission | | `plugin.activation.invalid` | Core | Missing or orphan activation event | | `plugin.runtime.unsupported` | Core | Unknown runtime dependency, or a launch runtime that cannot start an entry file | +| `plugin.runtime.undeclared` | Core | From `serverProcess` 4, a runtime tool that `requiresTools` does not list; `field` names where it is used | +| `plugin.run.invalidStart` | Core | The params of `lithe/host/run/start` are malformed, name a tool the manifest does not require, or leave the workspace or package; `field` names the part | +| `plugin.debug.invalidStart` | Core | The params of `lithe/host/debug/start` are malformed, or `configuration` holds a key the host reserves; `field` names the part | +| `plugin.debug.unknownAdapter` | Core | `lithe/host/debug/start` names an adapter the plugin does not declare | +| `plugin.debug.foreignAdapter` | Core | `lithe/host/debug/start` names an adapter of another plugin | +| `plugin.debug.limitExceeded` | Core | A `debug/start` configuration is larger than 256 KiB | +| `plugin.toolchain.launchMismatch` | Core | A tool definition's `launch` template cannot carry the entry, runtime arguments or arguments a launch supplies, or needs a part the launch left out | | `plugin.template.invalid` | Core | Unknown, misplaced, or unguarded template variable | | `plugin.path.invalid` | Core | A request path leaves the workspace, or a declared path leaves its base directory | | `plugin.test.invalidIdentifier` | Core | Empty or multi-line test identifier | | `plugin.test.unsupportedProject` | Core | No project marker in the workspace | | `plugin.execution.unavailable` | Core | The plugin has no run/test provider for the language | -| `plugin.integration.unsupported` | Core | A host integration the host does not implement, or one requested by a plugin outside the official namespace | +| `plugin.integration.unsupported` | Core | A host integration the host does not implement, or a build system's host integration requested by a plugin outside the official namespace | | `plugin.launch.unavailable` | Core | The plugin has no language server for the language, no provider process for the build system, or no such VCS provider | | `plugin.build.unavailable` | Core | The plugin declares no such build system, or the build system does not declare the requested operation | | `plugin.build.invalidSelection` | Core | A build request selects a scope, target, filter, or argument the declaration does not allow, or misses a required path | | `plugin.build.invalidOutput` | Core | Build-tool output or a report does not fit its decoder | | `plugin.launch.invalidRequest` | Core | A launch plan request has an unusable directory or an unknown architecture | +| `plugin.launch.notBackend` | Core | `decodeLaunch` named a language server that the plugin backend does not launch | +| `plugin.launch.invalidResult` | Core | A [backend-resolved launch](#backend-resolved-launch-serverprocess-3) answer is malformed, unknown in a field, or over a bound | +| `plugin.launch.outsidePackage` | Core | The answer's packaged executable leaves the plugin package | +| `plugin.launch.undeclaredTool` | Core | The answer's tool is not one of the plugin's tool definitions | +| `plugin.launch.unwritablePath` | Core | The answer writes outside the state, workspace state, log, and temporary directories | | `plugin.grammar.invalid` | Core | A language's grammar file is not a Monarch grammar Core accepts; `field` names the failing part | | `plugin.grammar.unavailable` | Core | The plugin declares no grammar for the language | | `plugin.vcs.reserved` | Core | A VCS provider claims the ID of the built-in Git support | @@ -2900,6 +3812,9 @@ command payload is a Core `invalid_request` failure instead. | `plugin.function.invalidReference`, `plugin.function.invalidParams`, `plugin.function.unavailable`, `plugin.function.wrongPlugin`, `plugin.function.unknown` | Core | A [plugin-function](#plugin-functions) call is refused before it reaches the backend | | `plugin.function.failed`, `plugin.function.invalidResult`, `plugin.function.revoked` | Core | A plugin-function call ended without a usable result | | `plugin.function.invalidRegistration` | Core | A backend's register or unregister request was refused as a whole | +| `plugin.function.notDeclared`, `plugin.function.notExported`, `plugin.function.providerDisabled`, `plugin.function.versionMismatch` | Core | A [relayed call](#cross-plugin-calls-protocol-version-4) is refused: the caller did not declare it, the provider does not export (or has not registered) the function, the provider is not enabled, or the declared and exported versions differ | +| `plugin.function.callCycle`, `plugin.function.depthExceeded`, `plugin.function.tooManyCalls`, `plugin.function.payloadTooLarge` | Core | A relayed call is refused by a bound: the provider is already on the call chain, the chain is too long, the caller has too many relayed calls running, or the params or result are too large | +| `plugin.function.providerUnavailable` | Core | The provider's backend did not start in time, exited, or withdrew the function while a relayed call waited for it | | `plugin.project.unavailable` | Core | The plugin declares no such project generator | | `plugin.project.invalidInput` | Core | The project name or a field value is not accepted; `field` is `projectName` or `values.`, or a step whose expanded path leaves the project | | `plugin.project.templateInvalid` | Core | A template directory is missing, too large, contains a symbolic link or non-UTF-8 file, or uses an unknown variable | @@ -2919,10 +3834,15 @@ command payload is a Core `invalid_request` failure instead. | `plugin.host.cancelled` | Core, Host | The backend cancelled the host request, or the host stopped it | | `plugin.host.timeout` | Host | The host's own deadline for the operation passed | | `plugin.host.notFound` | Host | The named resource does not exist | +| `plugin.host.toolUnresolved` | Host | A tool definition the request names resolves to no executable; `details` is `{ tool, reason }` | | `plugin.host.readOnly` | Host | The resource or workspace cannot be written | | `plugin.host.preconditionFailed` | Host | A state the request requires does not hold | | `plugin.host.busy` | Host | The resource is in use; retry later | | `plugin.host.rateLimited` | Host | Too many requests in a period | +| `plugin.host.languageServerUnavailable` | Core | The plugin's language server is not running for the context, or stopped while a request was in flight | +| `plugin.host.languageServerError` | Core | The language server answered a forwarded request with an error; `details` is `{code, message}` | +| `plugin.host.methodNotDeclared` | Core | A forwarded method or command is not in the server's declared session | +| `plugin.host.foreignContribution` | Core | The named language server belongs to another plugin | | `plugin.backend.exited` | Host | A page's call, or a host operation for the backend, ended because the backend process exited | Host-facing plugin states are `notInstalled`, `installed` (enabled or @@ -3314,7 +4234,7 @@ including when a prerequisite runtime fails first; they cannot silently fall back to Provider discovery. Host-managed `java`, `node` or `go` dependencies may select one such candidate, while retaining Core's version probe. Maven declares its namespace-owned tool -and the official Java definition for its Java prerequisite; no Maven-specific +and uses the short `java` definition for its Java prerequisite; no Maven-specific JDK registry entry is added. Its backend bootstrap remains a separate consumer. Passive import performs no version or runtime probes; a definition that requires verification is not reported as an already verified import executable. diff --git a/contract/v1/fixtures/backend/function-protocol.json b/contract/v1/fixtures/backend/function-protocol.json index 89cf0f6..65dd318 100644 --- a/contract/v1/fixtures/backend/function-protocol.json +++ b/contract/v1/fixtures/backend/function-protocol.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "protocol": "litheFunctions", - "offer": { "versions": [1, 2, 3] }, + "offer": { "versions": [1, 2, 3, 4] }, "negotiation": [ { "name": "no answer leaves the protocol off", @@ -46,9 +46,14 @@ "capabilities": { "experimental": { "litheFunctions": { "version": 3, "functions": [{ "id": "query.run" }] } } }, "expected": { "functions": ["query.run"], "version": 3 } }, + { + "name": "version 4 negotiates the same registry and adds the cross-plugin relay", + "capabilities": { "experimental": { "litheFunctions": { "version": 4, "functions": [{ "id": "query.run" }] } } }, + "expected": { "functions": ["query.run"], "version": 4 } + }, { "name": "a version the host never offered is not negotiated", - "capabilities": { "experimental": { "litheFunctions": { "version": 4 } } }, + "capabilities": { "experimental": { "litheFunctions": { "version": 5 } } }, "expected": { "functions": null } }, { @@ -543,7 +548,118 @@ {"name": "terminal aggregate grant cannot authorize terminal creation", "version": 3, "hostServices": ["terminal"], "method": "lithe/host/terminal/create", "params": {}, "expected": {"code": "plugin.host.permissionDenied", "rpcCode": -32010}}, {"name": "an older protocol cannot create a native terminal", "version": 2, "hostServices": ["lithe/host/terminal/create"], "method": "lithe/host/terminal/create", "params": {}, "expected": {"code": "plugin.host.unavailable", "rpcCode": -32601}}, {"name": "terminal reopen is granted on protocol 3", "version": 3, "hostServices": ["lithe/host/terminal/reopen"], "method": "lithe/host/terminal/reopen", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "terminalID": "owned"}, "expected": {"forward": "terminal"}}, - {"name": "terminal dispose is granted on protocol 3", "version": 3, "hostServices": ["lithe/host/terminal/dispose"], "method": "lithe/host/terminal/dispose", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "terminalID": "owned"}, "expected": {"forward": "terminal"}} + {"name": "terminal dispose is granted on protocol 3", "version": 3, "hostServices": ["lithe/host/terminal/dispose"], "method": "lithe/host/terminal/dispose", "params": {"context": {"contextID": "w-terminal", "generation": 1}, "terminalID": "owned"}, "expected": {"forward": "terminal"}}, + { "name": "a run start is granted on protocol 3", "version": 3, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/start", "params": { "context": { "contextID": "w-run", "generation": 1 }, "label": "Run app" }, "expected": { "forward": "run" } }, + { "name": "a run stop is granted on protocol 3", "version": 3, "hostServices": ["lithe/host/run/stop"], "method": "lithe/host/run/stop", "params": { "context": { "contextID": "w-run", "generation": 1 }, "runID": "r1" }, "expected": { "forward": "run" } }, + { "name": "a run start needs its own grant", "version": 3, "hostServices": ["lithe/host/run/stop"], "method": "lithe/host/run/start", "params": { "context": { "contextID": "w-run", "generation": 1 } }, "expected": { "code": "plugin.host.permissionDenied", "rpcCode": -32010 } }, + { "name": "a run start takes an object", "version": 3, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/start", "params": [], "expected": { "code": "plugin.host.invalidParams", "rpcCode": -32602 } }, + { "name": "an older protocol cannot start a run", "version": 2, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/start", "params": {}, "expected": { "code": "plugin.host.unavailable", "rpcCode": -32601 } }, + { "name": "a debug start is granted on protocol 3", "version": 3, "hostServices": ["lithe/host/debug/start"], "method": "lithe/host/debug/start", "params": { "context": { "contextID": "w-run", "generation": 1 }, "label": "Debug app" }, "expected": { "forward": "debug" } }, + { "name": "a debug start is not covered by the run grant", "version": 3, "hostServices": ["lithe/host/run/start", "lithe/host/run/stop"], "method": "lithe/host/debug/start", "params": { "context": { "contextID": "w-run", "generation": 1 } }, "expected": { "code": "plugin.host.permissionDenied", "rpcCode": -32010 } }, + {"name": "a language server request is forwarded when languageServer.request is granted", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun", "params": {"verbose": true}}, "expected": {"forward": "languageServer"}}, + {"name": "a language server request may bound its own timeout", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "rust-analyzer", "method": "rust-analyzer/expandMacro", "params": {}, "timeoutMilliseconds": 300000}, "expected": {"forward": "languageServer"}}, + {"name": "a language server request may run a declared command", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "workspace/executeCommand", "params": {"command": "metals.build-import"}}, "expected": {"forward": "languageServer"}}, + {"name": "a language server notification is forwarded", "version": 3, "hostServices": ["lithe/host/languageServer/notify"], "method": "lithe/host/languageServer/notify", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "workspace/didChangeConfiguration", "params": {"settings": {}}}, "expected": {"forward": "languageServer"}}, + {"name": "a language server status is forwarded", "version": 3, "hostServices": ["lithe/host/languageServer/setStatus"], "method": "lithe/host/languageServer/setStatus", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "preparing", "message": "Importing build", "percentage": 40}, "expected": {"forward": "languageServer"}}, + {"name": "a language server status may omit the message and the percentage", "version": 3, "hostServices": ["lithe/host/languageServer/setStatus"], "method": "lithe/host/languageServer/setStatus", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "rust-analyzer", "state": "ready"}, "expected": {"forward": "languageServer"}}, + {"name": "a language server request needs the languageServer.request grant", "version": 3, "hostServices": [], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun"}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "another service's grant never authorizes a language server request", "version": 3, "hostServices": ["workspace"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun"}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "the aggregate service name never authorizes a language server request", "version": 3, "hostServices": ["languageServer"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun"}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "a notify grant never authorizes a language server request", "version": 3, "hostServices": ["lithe/host/languageServer/notify"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun"}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "a language server request is unavailable before protocol 3", "version": 2, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun"}, "expected": {"rpcCode": -32601, "code": "plugin.host.unavailable"}}, + {"name": "a language server request needs a context", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"serverID": "metals", "method": "metals/doctorRun"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server request names a server", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "method": "metals/doctorRun"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server request names a method", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server request refuses an empty method", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": ""}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server request timeout is positive", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun", "timeoutMilliseconds": 0}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server request timeout has an upper bound", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun", "timeoutMilliseconds": 300001}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server request names the option it does not support", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/request", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/doctorRun", "workspaceFolder": "/tmp"}, "expected": {"rpcCode": -32018, "code": "plugin.host.unsupportedOption", "details": {"option": "workspaceFolder"}}}, + {"name": "a language server notification takes no timeout", "version": 3, "hostServices": ["lithe/host/languageServer/notify"], "method": "lithe/host/languageServer/notify", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "workspace/didChangeConfiguration", "timeoutMilliseconds": 5}, "expected": {"rpcCode": -32018, "code": "plugin.host.unsupportedOption", "details": {"option": "timeoutMilliseconds"}}}, + {"name": "a language server status state is a closed set", "version": 3, "hostServices": ["lithe/host/languageServer/setStatus"], "method": "lithe/host/languageServer/setStatus", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "importing"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server status percentage stops at one hundred", "version": 3, "hostServices": ["lithe/host/languageServer/setStatus"], "method": "lithe/host/languageServer/setStatus", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "preparing", "percentage": 101}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server status percentage is a whole number", "version": 3, "hostServices": ["lithe/host/languageServer/setStatus"], "method": "lithe/host/languageServer/setStatus", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "preparing", "percentage": 12.5}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server status message is text", "version": 3, "hostServices": ["lithe/host/languageServer/setStatus"], "method": "lithe/host/languageServer/setStatus", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "failed", "message": {"text": "x"}}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a language server status has no method field", "version": 3, "hostServices": ["lithe/host/languageServer/setStatus"], "method": "lithe/host/languageServer/setStatus", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "ready", "method": "x"}, "expected": {"rpcCode": -32018, "code": "plugin.host.unsupportedOption", "details": {"option": "method"}}}, + {"name": "a content provider is registered with the documents.contentProvider grant", "version": 3, "hostServices": ["lithe/host/documents/registerContentProvider"], "method": "lithe/host/documents/registerContentProvider", "params": {"context": {"contextID": "w1", "generation": 1}, "scheme": "metals-library", "function": "documents.read"}, "expected": {"forward": "documents"}}, + {"name": "the aggregate documents name never authorizes a content provider", "version": 3, "hostServices": ["documents"], "method": "lithe/host/documents/registerContentProvider", "params": {"context": {"contextID": "w1", "generation": 1}, "scheme": "metals-library", "function": "documents.read"}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "a content provider needs its own grant", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/documents/registerContentProvider", "params": {"context": {"contextID": "w1", "generation": 1}, "scheme": "metals-library", "function": "documents.read"}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "a content provider cannot serve a scheme the host owns", "version": 3, "hostServices": ["lithe/host/documents/registerContentProvider"], "method": "lithe/host/documents/registerContentProvider", "params": {"context": {"contextID": "w1", "generation": 1}, "scheme": "file", "function": "documents.read"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a content provider cannot serve the lithe scheme", "version": 3, "hostServices": ["lithe/host/documents/registerContentProvider"], "method": "lithe/host/documents/registerContentProvider", "params": {"context": {"contextID": "w1", "generation": 1}, "scheme": "lithe", "function": "documents.read"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a content scheme is lowercase", "version": 3, "hostServices": ["lithe/host/documents/registerContentProvider"], "method": "lithe/host/documents/registerContentProvider", "params": {"context": {"contextID": "w1", "generation": 1}, "scheme": "Metals", "function": "documents.read"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a content scheme starts with a letter", "version": 3, "hostServices": ["lithe/host/documents/registerContentProvider"], "method": "lithe/host/documents/registerContentProvider", "params": {"context": {"contextID": "w1", "generation": 1}, "scheme": "1metals", "function": "documents.read"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a content provider names a plugin function", "version": 3, "hostServices": ["lithe/host/documents/registerContentProvider"], "method": "lithe/host/documents/registerContentProvider", "params": {"context": {"contextID": "w1", "generation": 1}, "scheme": "metals-library", "function": "not a function"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a content provider needs a context", "version": 3, "hostServices": ["lithe/host/documents/registerContentProvider"], "method": "lithe/host/documents/registerContentProvider", "params": {"scheme": "metals-library", "function": "documents.read"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a declared caller lists the providers of a function", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/providers", "params": {"functionID": "languageImport.settings"}, "expected": {"relay": true}}, + {"name": "a declared caller invokes another plugin's function through the relay", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "languageImport.settings", "context": {"contextID": "w1", "generation": 1}, "params": {"project": "app"}, "timeoutMilliseconds": 5000}, "expected": {"relay": true}}, + {"name": "a nested invocation names the relayed call it belongs to", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "languageImport.settings", "context": {"contextID": "w1", "generation": 1}, "params": {"project": "app"}, "timeoutMilliseconds": 5000, "parentOperationID": "17"}, "expected": {"relay": true}}, + {"name": "an invocation's parent operation ID is a string", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "languageImport.settings", "context": {"contextID": "w1", "generation": 1}, "params": {"project": "app"}, "timeoutMilliseconds": 5000, "parentOperationID": 17}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "the relay needs protocol version 4", "version": 3, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "languageImport.settings", "context": {"contextID": "w1", "generation": 1}}, "expected": {"rpcCode": -32601, "code": "plugin.host.unavailable"}}, + {"name": "the relay needs the manifest's pluginFunctions calls", "version": 4, "hostServices": [], "method": "lithe/host/functions/providers", "params": {"functionID": "languageImport.settings"}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "an invocation names the workspace context it runs in", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "languageImport.settings"}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "an invocation's function ID is well formed", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "Not-Valid", "context": {"contextID": "w1", "generation": 1}}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "an invocation's params are an object", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "languageImport.settings", "context": {"contextID": "w1", "generation": 1}, "params": [1]}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "an invocation's timeout is positive", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "languageImport.settings", "context": {"contextID": "w1", "generation": 1}, "timeoutMilliseconds": 0}, "expected": {"rpcCode": -32602, "code": "plugin.host.invalidParams"}}, + {"name": "a caller cannot choose who the callee thinks called", "version": 4, "hostServices": ["functions"], "method": "lithe/host/functions/invoke", "params": {"pluginID": "dev.lithe.fixture.go", "functionID": "languageImport.settings", "context": {"contextID": "w1", "generation": 1}, "caller": {"pluginID": "dev.lithe.fixture.other"}}, "expected": {"rpcCode": -32018, "code": "plugin.host.unsupportedOption", "details": {"option": "caller"}}} + ], + "invocationCallers": [ + { + "name": "a version 4 callee receives the plugin that called it", + "version": 4, + "params": { "functionID": "languageImport.settings", "operationID": "7", "params": {} }, + "caller": "dev.lithe.fixture.python", + "expected": { + "params": { + "functionID": "languageImport.settings", + "operationID": "7", + "params": {}, + "caller": { "pluginID": "dev.lithe.fixture.python" } + } + } + }, + { + "name": "an older callee never receives a caller", + "version": 3, + "params": { "functionID": "languageImport.settings", "operationID": "7", "params": {} }, + "caller": "dev.lithe.fixture.python", + "expected": { "params": { "functionID": "languageImport.settings", "operationID": "7", "params": {} } } + }, + { + "name": "a call the host started has no caller, which means the host", + "version": 4, + "params": { "functionID": "languageImport.settings", "operationID": "7", "params": {} }, + "expected": { "params": { "functionID": "languageImport.settings", "operationID": "7", "params": {} } } + }, + { + "name": "Core's caller replaces any field the params already carried", + "version": 4, + "params": { "functionID": "languageImport.settings", "operationID": "7", "params": {}, "caller": { "pluginID": "dev.lithe.fixture.other" } }, + "caller": "dev.lithe.fixture.python", + "expected": { + "params": { + "functionID": "languageImport.settings", + "operationID": "7", + "params": {}, + "caller": { "pluginID": "dev.lithe.fixture.python" } + } + } + } + ], + "relayErrors": [ + { "name": "the caller did not declare the call", "code": "plugin.function.notDeclared", "rpcCode": -32027 }, + { "name": "the provider does not export the function", "code": "plugin.function.notExported", "rpcCode": -32028 }, + { "name": "the provider is not enabled", "code": "plugin.function.providerDisabled", "rpcCode": -32029 }, + { "name": "the declared and exported contract versions differ", "code": "plugin.function.versionMismatch", "rpcCode": -32030 }, + { "name": "the provider is already on the call chain", "code": "plugin.function.callCycle", "rpcCode": -32065 }, + { "name": "the call chain is too long", "code": "plugin.function.depthExceeded", "rpcCode": -32066 }, + { "name": "the caller has too many relayed calls running", "code": "plugin.function.tooManyCalls", "rpcCode": -32067 }, + { "name": "the params or result exceed the relay's bound", "code": "plugin.function.payloadTooLarge", "rpcCode": -32068 }, + { "name": "the provider's backend could not serve the call", "code": "plugin.function.providerUnavailable", "rpcCode": -32069 }, + { "name": "the provider's function answered with an error", "code": "plugin.function.failed", "rpcCode": -32070 }, + { "name": "the provider answered without a result Core can pass on", "code": "plugin.function.invalidResult", "rpcCode": -32071 }, + { "name": "the relay's own deadline passed", "code": "plugin.host.timeout", "rpcCode": -32015 }, + { "name": "the caller or an upstream caller cancelled", "code": "plugin.host.cancelled", "rpcCode": -32800 }, + { "name": "the workspace context is unknown or replaced", "code": "plugin.host.contextExpired", "rpcCode": -32011 }, + { "name": "the workspace is remote", "code": "plugin.host.authorityUnsupported", "rpcCode": -32012 } ], "invocationContexts": [ { @@ -594,7 +710,12 @@ { "name": "the host is busy and the request may be retried", "code": "plugin.host.busy", "expected": { "code": "plugin.host.busy", "rpcCode": -32020 } }, { "name": "the plugin sent requests too often", "code": "plugin.host.rateLimited", "expected": { "code": "plugin.host.rateLimited", "rpcCode": -32021 } }, { "name": "a host cannot borrow a function code", "code": "plugin.function.unknown", "expected": { "code": "plugin.host.failed", "rpcCode": -32603 } }, - { "name": "a host cannot report a backend exit to the backend", "code": "plugin.backend.exited", "expected": { "code": "plugin.host.failed", "rpcCode": -32603 } } + { "name": "a host cannot report a backend exit to the backend", "code": "plugin.backend.exited", "expected": { "code": "plugin.host.failed", "rpcCode": -32603 } }, + { "name": "a tool the host cannot resolve", "code": "plugin.host.toolUnresolved", "expected": { "code": "plugin.host.toolUnresolved", "rpcCode": -32025 } }, + {"name": "the plugin's language server is not running", "code": "plugin.host.languageServerUnavailable", "expected": {"code": "plugin.host.languageServerUnavailable", "rpcCode": -32022}}, + {"name": "the language server answered with an error", "code": "plugin.host.languageServerError", "expected": {"code": "plugin.host.languageServerError", "rpcCode": -32023}}, + {"name": "the method is not in the language server's declared session", "code": "plugin.host.methodNotDeclared", "expected": {"code": "plugin.host.methodNotDeclared", "rpcCode": -32024}}, + {"name": "the server belongs to another plugin", "code": "plugin.host.foreignContribution", "expected": {"code": "plugin.host.foreignContribution", "rpcCode": -32026}} ], "hostNotifications": [ { "name": "changed contexts reach a workspace session", "version": 3, "hostServices": ["workspace"], "method": "lithe/host/workspace/didChangeContexts", "params": { "revision": 4 }, "expected": { "delivered": true } }, @@ -606,6 +727,25 @@ { "name": "a command run in a window", "version": 3, "hostServices": [], "method": "lithe/host/commands/didRun", "params": { "commandID": "db.refresh", "context": { "contextID": "w1", "generation": 2 } }, "expected": { "delivered": true } }, { "name": "a command without a window", "version": 3, "hostServices": [], "method": "lithe/host/commands/didRun", "params": { "commandID": "db.refresh" }, "expected": { "delivered": true } }, { "name": "extra command params are refused", "version": 3, "hostServices": [], "method": "lithe/host/commands/didRun", "params": { "commandID": "db.refresh", "path": "/etc" }, "expected": { "code": "plugin.host.invalidParams" } }, - { "name": "the host cannot call the backend's own protocol", "version": 3, "hostServices": [], "method": "lithe/functions/invoke", "params": {}, "expected": { "code": "plugin.host.unavailable" } } + { "name": "the host cannot call the backend's own protocol", "version": 3, "hostServices": [], "method": "lithe/functions/invoke", "params": {}, "expected": { "code": "plugin.host.unavailable" } }, + { "name": "a run exit with a status", "version": 3, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/didExit", "params": { "runID": "r1", "exitCode": 0 }, "expected": { "delivered": true } }, + { "name": "a run exit by signal", "version": 3, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/didExit", "params": { "runID": "r1", "signal": 9 }, "expected": { "delivered": true } }, + { "name": "a run exit the host lost track of", "version": 3, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/didExit", "params": { "runID": "r1" }, "expected": { "delivered": true } }, + { "name": "a run exit needs the run grant", "version": 3, "hostServices": ["lithe/host/debug/start"], "method": "lithe/host/run/didExit", "params": { "runID": "r1", "exitCode": 0 }, "expected": { "code": "plugin.host.permissionDenied" } }, + { "name": "a run exit status is a 32-bit integer", "version": 3, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/didExit", "params": { "runID": "r1", "exitCode": 4294967296 }, "expected": { "code": "plugin.host.invalidParams" } }, + { "name": "a run exit signal is a signal number", "version": 3, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/didExit", "params": { "runID": "r1", "signal": 0 }, "expected": { "code": "plugin.host.invalidParams" } }, + { "name": "a run exit names its run", "version": 3, "hostServices": ["lithe/host/run/start"], "method": "lithe/host/run/didExit", "params": { "exitCode": 0 }, "expected": { "code": "plugin.host.invalidParams" } }, + {"name": "a language server changed state", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didChangeState", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "running", "generation": 3}, "expected": {"delivered": true}}, + {"name": "a language server state is a closed set", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didChangeState", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "ready", "generation": 3}, "expected": {"code": "plugin.host.invalidParams"}}, + {"name": "a language server state names its generation", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didChangeState", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "stopped"}, "expected": {"code": "plugin.host.invalidParams"}}, + {"name": "a language server state generation is positive", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didChangeState", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "crashed", "generation": 0}, "expected": {"code": "plugin.host.invalidParams"}}, + {"name": "a language server state needs the languageServer.request grant", "version": 3, "hostServices": [], "method": "lithe/host/languageServer/didChangeState", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "running", "generation": 1}, "expected": {"code": "plugin.host.permissionDenied"}}, + {"name": "a version 2 backend skips language server state", "version": 2, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didChangeState", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "state": "running", "generation": 1}, "expected": {"delivered": false}}, + {"name": "a forwarded server notification carries its params", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didReceiveNotification", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "generation": 2, "method": "metals/status", "params": {"text": "Importing"}}, "expected": {"delivered": true}}, + {"name": "an oversized server notification is marked truncated without params", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didReceiveNotification", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "generation": 2, "method": "metals/status", "truncated": true}, "expected": {"delivered": true}}, + {"name": "a truncated server notification drops its params", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didReceiveNotification", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "generation": 2, "method": "metals/status", "params": {}, "truncated": true}, "expected": {"code": "plugin.host.invalidParams"}}, + {"name": "a server notification names its method", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didReceiveNotification", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "generation": 2}, "expected": {"code": "plugin.host.invalidParams"}}, + {"name": "a server notification names its generation", "version": 3, "hostServices": ["lithe/host/languageServer/request"], "method": "lithe/host/languageServer/didReceiveNotification", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "method": "metals/status", "params": {}}, "expected": {"code": "plugin.host.invalidParams"}}, + {"name": "a server notification needs the languageServer.request grant", "version": 3, "hostServices": [], "method": "lithe/host/languageServer/didReceiveNotification", "params": {"context": {"contextID": "w1", "generation": 1}, "serverID": "metals", "generation": 2, "method": "metals/status", "params": {}}, "expected": {"code": "plugin.host.permissionDenied"}} ] } diff --git a/contract/v1/fixtures/backend/plugin-functions-caller.plugin.json b/contract/v1/fixtures/backend/plugin-functions-caller.plugin.json new file mode 100644 index 0000000..301571b --- /dev/null +++ b/contract/v1/fixtures/backend/plugin-functions-caller.plugin.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.functioncaller", + "displayName": "Plugin function caller fixture", + "version": "1.0.0", + "pluginAPIVersion": 3, + "requires": {"pluginFunctions": 1, "backend": 2, "serverProcess": 1}, + "vendor": {"id": "org.example", "displayName": "Fixture publisher", "signatureRequirement": "publisherPackage"}, + "permissions": ["backend.launch"], + "activationEvents": ["onWorkspaceOpen"], + "pluginFunctions": { + "calls": [ + {"id": "document.format", "version": 1} + ] + }, + "backend": {"launch": {"kind": "packaged", "path": "bin/backend"}, "arguments": ["--stdio"]} +} diff --git a/contract/v1/fixtures/backend/plugin-functions-exporter.plugin.json b/contract/v1/fixtures/backend/plugin-functions-exporter.plugin.json new file mode 100644 index 0000000..dedf1e4 --- /dev/null +++ b/contract/v1/fixtures/backend/plugin-functions-exporter.plugin.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.functionexporter", + "displayName": "Plugin function exporter fixture", + "version": "1.0.0", + "pluginAPIVersion": 3, + "requires": {"pluginFunctions": 1, "backend": 2, "serverProcess": 1}, + "vendor": {"id": "org.example", "displayName": "Fixture publisher", "signatureRequirement": "publisherPackage"}, + "permissions": ["backend.launch"], + "pluginFunctions": { + "exports": [ + {"id": "document.format", "version": 1, "context": "workspace"}, + {"id": "symbols.list", "version": 2, "context": "workspace"} + ] + }, + "backend": {"launch": {"kind": "packaged", "path": "bin/backend"}, "arguments": ["--stdio"]} +} diff --git a/contract/v1/fixtures/debug-adapters/debug-adapter-features.plugin.json b/contract/v1/fixtures/debug-adapters/debug-adapter-features.plugin.json new file mode 100644 index 0000000..0c8b044 --- /dev/null +++ b/contract/v1/fixtures/debug-adapters/debug-adapter-features.plugin.json @@ -0,0 +1,68 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.debug-adapter-features", + "displayName": "Debug Adapter Features Fixture", + "version": "0.1.0", + "pluginAPIVersion": 4, + "requires": { "backend": 2, "debugAdapters": 3, "serverProcess": 1 }, + "vendor": { + "id": "org.example", + "displayName": "Example", + "signatureRequirement": "publisherPackage" + }, + "permissions": [ + "backend.launch", + "debug.launch", + "debug.start" + ], + "activationEvents": [ + "onDebug:script", + "onWorkspaceOpen" + ], + "languages": [], + "backend": { + "launch": { "kind": "packaged", "path": "bin/backend" }, + "arguments": ["--state", "${stateDir}"] + }, + "debugAdapters": [ + { + "id": "script", + "displayName": "Script Debugger", + "adapterID": "script-dap", + "requests": ["launch"], + "selectors": [{ "targetKinds": ["file"], "languageIDs": ["script"] }], + "configurationTypes": ["script"], + "configurations": { + "launch": { + "arguments": { "stopOnEntry": false }, + "targetBindings": { "program": "target.path", "cwd": "target.workingDirectory" } + } + }, + "features": { + "hotCodeReplace": { + "request": "script.reload", + "event": "script.reloaded", + "changedField": "modules", + "errorField": "error" + }, + "stepFilters": { "argument": "stepFilters" } + }, + "defaults": { + "stepFilters": { + "classNameFilters": ["_vendor.*"], + "skipSynthetics": true, + "skipStaticInitializers": false, + "skipConstructors": false, + "hideFilteredStackFrames": false + } + }, + "launch": { + "process": { + "launch": { "kind": "packaged", "path": "bin/script-dap" }, + "arguments": ["--stdio"] + }, + "transport": "stdio" + } + } + ] +} diff --git a/contract/v1/fixtures/debug-adapters/debug-start.json b/contract/v1/fixtures/debug-adapters/debug-start.json new file mode 100644 index 0000000..d2cd2ad --- /dev/null +++ b/contract/v1/fixtures/debug-adapters/debug-start.json @@ -0,0 +1,117 @@ +{ + "description": "Parameters of lithe/host/debug/start and what debug.decodeBackendConfiguration returns. The host passes the plugin's manifest (`manifest`, a file in this directory) and the request's `params`. `start` is the normalized start; `error` is the rejection every host reports. The 256 KiB size limit is covered by Core's own tests.", + "manifest": "debug-adapter-features.plugin.json", + "cases": [ + { + "name": "the plugin's own adapter by its local ID", + "params": { + "context": { "contextID": "w-run", "generation": 1 }, + "label": "Debug main.script", + "adapter": "script", + "request": "launch", + "configuration": { "program": "main.script", "args": ["--fast"] } + }, + "start": { + "adapterID": "dev.lithe.fixture.debug-adapter-features/script", + "request": "launch", + "label": "Debug main.script", + "configuration": { "program": "main.script", "args": ["--fast"] } + } + }, + { + "name": "the plugin's own adapter by its qualified ID", + "params": { + "label": "Debug main.script", + "adapter": "dev.lithe.fixture.debug-adapter-features/script", + "request": "launch", + "configuration": {} + }, + "start": { + "adapterID": "dev.lithe.fixture.debug-adapter-features/script", + "request": "launch", + "label": "Debug main.script", + "configuration": {} + } + }, + { + "name": "an adapter of another plugin", + "params": { + "label": "Debug", + "adapter": "org.example.other/script", + "request": "launch", + "configuration": {} + }, + "error": { "code": "plugin.debug.foreignAdapter", "field": "adapter" } + }, + { + "name": "an adapter the plugin does not declare", + "params": { + "label": "Debug", + "adapter": "missing", + "request": "launch", + "configuration": {} + }, + "error": { "code": "plugin.debug.unknownAdapter", "field": "adapter" } + }, + { + "name": "a malformed adapter ID", + "params": { + "label": "Debug", + "adapter": "Not An ID", + "request": "launch", + "configuration": {} + }, + "error": { "code": "plugin.debug.invalidStart", "field": "adapter" } + }, + { + "name": "a request the adapter does not serve", + "params": { + "label": "Attach", + "adapter": "script", + "request": "attach", + "configuration": {} + }, + "error": { "code": "plugin.debug.invalidStart", "field": "request" } + }, + { + "name": "the session mode belongs to the host", + "params": { + "label": "Debug", + "adapter": "script", + "request": "launch", + "configuration": { "noDebug": true } + }, + "error": { "code": "plugin.debug.invalidStart", "field": "configuration.noDebug" } + }, + { + "name": "host bookkeeping keys belong to the host", + "params": { + "label": "Debug", + "adapter": "script", + "request": "launch", + "configuration": { "__sessionId": "x" } + }, + "error": { "code": "plugin.debug.invalidStart", "field": "configuration.__sessionId" } + }, + { + "name": "an empty label", + "params": { + "label": " ", + "adapter": "script", + "request": "launch", + "configuration": {} + }, + "error": { "code": "plugin.debug.invalidStart", "field": "label" } + }, + { + "name": "a configuration that is not an object", + "params": { + "label": "Debug", + "adapter": "script", + "request": "launch", + "configuration": ["main.script"] + }, + "error": { "code": "plugin.debug.invalidStart", "field": "params" } + } + ] +} diff --git a/contract/v1/fixtures/editor/editor-actions-v2.json b/contract/v1/fixtures/editor/editor-actions-v2.json new file mode 100644 index 0000000..f008386 --- /dev/null +++ b/contract/v1/fixtures/editor/editor-actions-v2.json @@ -0,0 +1,118 @@ +{ + "schemaVersion": 1, + "description": "Version 2 editor action validation. `host` is what the host declared in getCapabilities (`services.editor.options.actionIcons` and `limits.editorActionSourceBytes`); `kind` is `actions` for a collector result (one-based UTF-16 positions in `text`) or `invokeResult` for the result of a clicked action or alternative. Accepted cases list the normalized value; rejected cases reject the whole result.", + "host": { + "actionIcons": ["run", "run-all", "debug", "test", "test-passed", "test-failed", "test-skipped", "navigate-super", "navigate-implementations"], + "actionSourceBytes": 1048576 + }, + "cases": [ + { + "name": "a Go main marker offers debug and test alternatives", + "kind": "actions", + "text": "package main\n\nfunc main() {}\n", + "value": { + "actions": [ + { + "id": "main", + "range": { "start": { "line": 3, "column": 1 }, "end": { "line": 3, "column": 10 } }, + "title": "Run main", + "icon": "run", + "invoke": { "functionID": "go.markers.invoke", "params": { "entry": "main" } }, + "alternatives": [ + { "id": "debug", "title": "Debug main", "icon": "debug", "invoke": { "functionID": "go.markers.invoke", "params": { "entry": "main", "debug": true } } }, + { "id": "test", "title": "Test package", "icon": "test", "invoke": { "functionID": "go.markers.invoke" } } + ] + } + ] + }, + "expected": { + "status": "accepted", + "value": [ + { + "id": "main", + "range": { "start": { "line": 3, "column": 1 }, "end": { "line": 3, "column": 10 } }, + "title": "Run main", + "icon": "run", + "invoke": { "functionID": "go.markers.invoke", "params": { "entry": "main" } }, + "alternatives": [ + { "id": "debug", "title": "Debug main", "icon": "debug", "invoke": { "functionID": "go.markers.invoke", "params": { "entry": "main", "debug": true } } }, + { "id": "test", "title": "Test package", "icon": "test", "invoke": { "functionID": "go.markers.invoke", "params": {} } } + ] + } + ] + } + }, + { + "name": "a Go test function shows its last outcome", + "kind": "actions", + "text": "func TestParse(t *testing.T) {}\n", + "value": { + "actions": [ + { "id": "TestParse", "range": { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 15 } }, "title": "Run TestParse", "icon": "test-failed", "invoke": { "functionID": "go.markers.invoke", "params": { "test": "TestParse" } } } + ] + }, + "expected": { + "status": "accepted", + "value": [ + { "id": "TestParse", "range": { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 15 } }, "title": "Run TestParse", "icon": "test-failed", "invoke": { "functionID": "go.markers.invoke", "params": { "test": "TestParse" } } } + ] + } + }, + { + "name": "an icon the host did not declare", + "kind": "actions", + "text": "x", + "value": { "actions": [ { "id": "a", "range": { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 1 } }, "title": "Run", "icon": "rocket", "invoke": { "functionID": "go.markers.invoke" } } ] }, + "expected": { "status": "rejected" } + }, + { + "name": "at most four alternatives", + "kind": "actions", + "text": "x", + "value": { "actions": [ { "id": "a", "range": { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 1 } }, "title": "Run", "icon": "run", "invoke": { "functionID": "go.markers.invoke" }, + "alternatives": [ + { "id": "1", "title": "One", "icon": "debug", "invoke": { "functionID": "go.markers.invoke" } }, + { "id": "2", "title": "Two", "icon": "debug", "invoke": { "functionID": "go.markers.invoke" } }, + { "id": "3", "title": "Three", "icon": "debug", "invoke": { "functionID": "go.markers.invoke" } }, + { "id": "4", "title": "Four", "icon": "debug", "invoke": { "functionID": "go.markers.invoke" } }, + { "id": "5", "title": "Five", "icon": "debug", "invoke": { "functionID": "go.markers.invoke" } } + ] } ] }, + "expected": { "status": "rejected" } + }, + { + "name": "alternative IDs are unique within their action", + "kind": "actions", + "text": "x", + "value": { "actions": [ { "id": "a", "range": { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 1 } }, "title": "Run", "icon": "run", "invoke": { "functionID": "go.markers.invoke" }, + "alternatives": [ + { "id": "same", "title": "Debug", "icon": "debug", "invoke": { "functionID": "go.markers.invoke" } }, + { "id": "same", "title": "Test", "icon": "test", "invoke": { "functionID": "go.markers.invoke" } } + ] } ] }, + "expected": { "status": "rejected" } + }, + { + "name": "a click reveals resource-key definitions", + "kind": "invokeResult", + "value": { "reveal": { "locations": [ + { "uri": "file:///workspace/messages.properties", "range": { "start": { "line": 4, "character": 0 }, "end": { "line": 4, "character": 9 } } }, + { "uri": "file:///workspace/messages_zh.properties", "range": { "start": { "line": 2, "character": 0 }, "end": { "line": 2, "character": 9 } } } + ] }, "started": true }, + "expected": { "status": "accepted", "value": { "reveal": { "locations": [ + { "uri": "file:///workspace/messages.properties", "range": { "start": { "line": 4, "character": 0 }, "end": { "line": 4, "character": 9 } } }, + { "uri": "file:///workspace/messages_zh.properties", "range": { "start": { "line": 2, "character": 0 }, "end": { "line": 2, "character": 9 } } } + ] } } } + }, + { + "name": "other result members stay the plugin's own", + "kind": "invokeResult", + "value": { "runID": "r1" }, + "expected": { "status": "accepted", "value": {} } + }, + { + "name": "a reveal location needs an absolute URI", + "kind": "invokeResult", + "value": { "reveal": { "locations": [ { "uri": "messages.properties", "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } } } ] } }, + "expected": { "status": "rejected" } + } + ] +} diff --git a/contract/v1/fixtures/editor/editor-providers.json b/contract/v1/fixtures/editor/editor-providers.json new file mode 100644 index 0000000..671aaea --- /dev/null +++ b/contract/v1/fixtures/editor/editor-providers.json @@ -0,0 +1,132 @@ +{ + "schemaVersion": 1, + "description": "lithe/host/editor/registerProviders validation. `host.providerTypes` is what the host declared in getCapabilities (`services.editor.options.providerTypes`). Registration cases carry the method params without `context`; result cases carry one provider function's LSP 3.17 shaped result and the normalized value the host merges after the language server's own answer. Positions are zero-based LSP positions. A rejected registration names the host failure code; a rejected result is dropped whole.", + "host": { + "providerTypes": ["definition", "hover", "completion", "codeLens", "foldingRange", "semanticTokens", "documentLink"] + }, + "registrations": [ + { + "name": "resource keys navigate from templates to message bundles", + "params": { + "id": "message-keys", + "selector": { "languages": ["html"], "fileExtensions": [".Properties"], "fileNames": ["Messages"] }, + "providers": { + "definition": { "function": "keys.definition" }, + "hover": { "function": "keys.hover" }, + "completion": { "function": "keys.complete", "triggerCharacters": ["."] }, + "codeLens": { "function": "keys.usages" }, + "semanticTokens": { "function": "keys.tokens", "legend": { "tokenTypes": ["property", "string"], "tokenModifiers": ["declaration"] } } + } + }, + "expected": { + "status": "accepted", + "registration": { + "id": "message-keys", + "selector": { "languages": ["html"], "fileExtensions": ["properties"], "fileNames": ["messages"] }, + "providers": { + "codeLens": { "function": "keys.usages", "budgetMs": 2000 }, + "completion": { "function": "keys.complete", "triggerCharacters": ["."], "budgetMs": 400 }, + "definition": { "function": "keys.definition", "budgetMs": 1500 }, + "hover": { "function": "keys.hover", "budgetMs": 400 }, + "semanticTokens": { "function": "keys.tokens", "legend": { "tokenTypes": ["property", "string"], "tokenModifiers": ["declaration"] }, "budgetMs": 2000 } + } + } + } + }, + { + "name": "a provider type the host does not route", + "params": { "id": "message-keys", "selector": { "fileExtensions": ["properties"] }, "providers": { "signatureHelp": { "function": "keys.signature" } } }, + "expected": { "status": "rejected", "code": "plugin.host.unsupportedOption", "details": { "option": "providers.signatureHelp" } } + }, + { + "name": "diagnostics are not a provider", + "params": { "id": "message-keys", "selector": { "fileExtensions": ["properties"] }, "providers": { "diagnostics": { "function": "keys.check" } } }, + "expected": { "status": "rejected", "code": "plugin.host.unsupportedOption", "details": { "option": "providers.diagnostics" } } + }, + { + "name": "a selector names at least one language, extension or file", + "params": { "id": "message-keys", "selector": {}, "providers": { "hover": { "function": "keys.hover" } } }, + "expected": { "status": "rejected", "code": "plugin.host.invalidParams" } + }, + { + "name": "semantic tokens declare their legend", + "params": { "id": "message-keys", "selector": { "fileExtensions": ["properties"] }, "providers": { "semanticTokens": { "function": "keys.tokens" } } }, + "expected": { "status": "rejected", "code": "plugin.host.invalidParams" } + }, + { + "name": "budgets are the host's", + "params": { "id": "message-keys", "selector": { "fileExtensions": ["properties"] }, "providers": { "hover": { "function": "keys.hover", "budgetMs": 5000 } } }, + "expected": { "status": "rejected", "code": "plugin.host.invalidParams" } + } + ], + "results": [ + { + "name": "a definition link becomes its target selection", + "type": "definition", + "value": [ { "targetUri": "file:///workspace/messages.properties", "targetRange": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 30 } }, "targetSelectionRange": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 9 } } } ], + "expected": { "status": "accepted", "value": { "locations": [ { "uri": "file:///workspace/messages.properties", "range": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 9 } } } ] } } + }, + { + "name": "no definition", + "type": "definition", + "value": null, + "expected": { "status": "accepted", "value": { "locations": [] } } + }, + { + "name": "a hover with a code block", + "type": "hover", + "value": { "contents": [ { "language": "properties", "value": "app.title=Lithe" }, "Defined in messages.properties" ], "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 16 } } }, + "expected": { "status": "accepted", "value": { "contents": [ { "kind": "markdown", "value": "```properties\napp.title=Lithe\n```" }, { "kind": "markdown", "value": "Defined in messages.properties" } ], "range": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 16 } } } } + }, + { + "name": "completion keeps presentation and edits only", + "type": "completion", + "value": { "isIncomplete": false, "items": [ { "label": "app.title", "kind": 10, "detail": "Lithe", "data": { "cursor": 1 }, "command": { "title": "Resolve", "command": "keys.resolve" } } ] }, + "expected": { "status": "accepted", "value": { "isIncomplete": false, "items": [ { "label": "app.title", "kind": 10, "detail": "Lithe" } ] } } + }, + { + "name": "a code lens calls a function of its plugin", + "type": "codeLens", + "value": [ { "range": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 9 } }, "command": { "title": "2 usages", "command": "keys.usages.reveal", "arguments": [ { "key": "app.title" } ] } } ], + "expected": { "status": "accepted", "value": { "lenses": [ { "range": { "start": { "line": 3, "character": 0 }, "end": { "line": 3, "character": 9 } }, "command": { "title": "2 usages", "command": "keys.usages.reveal", "arguments": [ { "key": "app.title" } ] } } ] } } + }, + { + "name": "a code lens without a command", + "type": "codeLens", + "value": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } } } ], + "expected": { "status": "rejected" } + }, + { + "name": "folding ranges keep their kind", + "type": "foldingRange", + "value": [ { "startLine": 0, "endLine": 4, "kind": "region", "collapsedText": "..." } ], + "expected": { "status": "accepted", "value": { "ranges": [ { "startLine": 0, "endLine": 4, "kind": "region" } ] } } + }, + { + "name": "semantic tokens index the registered legend", + "type": "semanticTokens", + "legend": { "tokenTypes": ["property", "string"], "tokenModifiers": ["declaration"] }, + "value": { "resultId": "1", "data": [0, 0, 9, 0, 1, 0, 10, 5, 1, 0] }, + "expected": { "status": "accepted", "value": { "data": [0, 0, 9, 0, 1, 0, 10, 5, 1, 0] } } + }, + { + "name": "a semantic token type outside the legend", + "type": "semanticTokens", + "legend": { "tokenTypes": ["property"], "tokenModifiers": [] }, + "value": { "data": [0, 0, 9, 1, 0] }, + "expected": { "status": "rejected" } + }, + { + "name": "a document link needs an absolute target", + "type": "documentLink", + "value": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 4 } }, "target": "messages.properties" } ], + "expected": { "status": "rejected" } + }, + { + "name": "a document link to a bundle", + "type": "documentLink", + "value": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 4 } }, "target": "file:///workspace/messages.properties", "tooltip": "Open bundle" } ], + "expected": { "status": "accepted", "value": { "links": [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 4 } }, "target": "file:///workspace/messages.properties", "tooltip": "Open bundle" } ] } } + } + ] +} diff --git a/contract/v1/fixtures/editor/editor-providers.plugin.json b/contract/v1/fixtures/editor/editor-providers.plugin.json new file mode 100644 index 0000000..0a3dd90 --- /dev/null +++ b/contract/v1/fixtures/editor/editor-providers.plugin.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.editorproviders", + "displayName": "Editor Providers", + "version": "1.0.0", + "pluginAPIVersion": 3, + "requires": { + "backend": 2, + "serverProcess": 1 + }, + "vendor": { + "id": "org.example", + "displayName": "Example", + "signatureRequirement": "publisherPackage" + }, + "permissions": [ + "backend.launch", + "documents.read", + "editor.providers", + "workspace.metadata" + ], + "activationEvents": [ + "onWorkspaceOpen" + ], + "languages": [], + "backend": { + "launch": { + "kind": "packaged", + "path": "bin/backend" + }, + "arguments": [ + "--state", + "${stateDir}" + ] + } +} diff --git a/contract/v1/fixtures/languages/language-server-backend-launch.plugin.json b/contract/v1/fixtures/languages/language-server-backend-launch.plugin.json new file mode 100644 index 0000000..2bc42b5 --- /dev/null +++ b/contract/v1/fixtures/languages/language-server-backend-launch.plugin.json @@ -0,0 +1,116 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.language-server-backend-launch", + "displayName": "Backend Launched Language Server Fixture", + "version": "0.1.0", + "pluginAPIVersion": 4, + "requires": { + "backend": 2, + "executableResolution": 2, + "languageServers": 3, + "languages": 1, + "serverProcess": 3, + "toolDefinitions": 1 + }, + "vendor": { + "id": "org.example", + "displayName": "Example", + "signatureRequirement": "publisherPackage" + }, + "permissions": [ + "backend.launch", + "languageServer.launch", + "languageServer.request" + ], + "activationEvents": [ + "onLanguage:scala" + ], + "languages": [ + { + "id": "scala", + "displayName": "Scala", + "fileExtensions": [ + "scala", + "sc" + ], + "projectFileNames": [ + "build.sbt" + ] + } + ], + "toolDefinitions": [ + { + "id": "coursier", + "displayName": "Coursier", + "executableLayouts": [ + { + "path": "bin/cs" + } + ], + "executables": [ + { + "id": "shell", + "kind": "command", + "command": "cs" + } + ], + "version": { + "arguments": [ + "--version" + ], + "pattern": "(\\d+(?:\\.\\d+)*)" + }, + "commands": [ + "cs" + ], + "launch": [ + "${executable}", + "${arguments}" + ] + } + ], + "languageServers": [ + { + "languageID": "scala", + "launch": { + "kind": "backend", + "function": "metals.resolveLaunch", + "timeoutMilliseconds": 30000 + }, + "requestMethods": [ + "textDocument/definition", + "workspace/executeCommand" + ], + "commands": [ + "build-connect", + "build-import", + "doctor-run" + ], + "serializedCommands": [ + "build-import" + ], + "forwardNotifications": [ + "metals/executeClientCommand", + "metals/status" + ], + "state": { + "scope": "workspace", + "retention": { + "maxEntries": 8, + "maxAgeDays": 30 + } + }, + "remote": "unsupported" + } + ], + "backend": { + "launch": { + "kind": "packaged", + "path": "bin/backend" + }, + "arguments": [ + "--state", + "${stateDir}" + ] + } +} diff --git a/contract/v1/fixtures/languages/language-server-launch-resolution.json b/contract/v1/fixtures/languages/language-server-launch-resolution.json new file mode 100644 index 0000000..97637f2 --- /dev/null +++ b/contract/v1/fixtures/languages/language-server-launch-resolution.json @@ -0,0 +1,490 @@ +{ + "schemaVersion": 1, + "description": "Golden launch of a language server whose backend resolves the command line (serverProcess 3). `languageServerLaunchPlan` is the first step: Core names the function to call and its params. `decodeLaunch` is the second step: Core bounds the function's `result` and returns the plan the host starts. A case runs against `plugin` and `languageID` unless it names its own, with `request` changed by `requestEdits` (JSON Pointer `set` or `remove`). `expected` is the command's data object, or `{status: \"rejected\", code}` for the stable rejection code. `resolveParams.platform.os` is the host's operating system and is not compared.", + "plugin": "language-server-backend-launch.plugin.json", + "languageID": "scala", + "request": { + "pluginDirectory": "/plugins/metals", + "stateDirectory": "/state/metals", + "workspaceStateDirectory": "/state/metals/workspaces/abc", + "logDirectory": "/logs/metals", + "temporaryDirectory": "/tmp/metals", + "architecture": "arm64" + }, + "cases": [ + { + "name": "the first step asks the backend to resolve the launch", + "operation": "languageServerLaunchPlan", + "expected": { + "status": "ok", + "plan": { + "launch": { + "kind": "backend", + "function": "metals.resolveLaunch", + "timeoutMilliseconds": 30000 + }, + "resolveParams": { + "serverID": "scala", + "platform": { + "arch": "arm64" + }, + "stateDirectory": "/state/metals", + "workspaceStateDirectory": "/state/metals/workspaces/abc", + "logDirectory": "/logs/metals", + "temporaryDirectory": "/tmp/metals", + "settings": {} + }, + "arguments": [], + "validationArguments": [], + "runtimeRequirements": [], + "requiredDirectories": [ + "state", + "workspaceState", + "log", + "temporary" + ], + "requiredPackagePaths": [], + "sessionPolicy": { + "serverID": "scala", + "requestMethods": [ + "textDocument/definition", + "workspace/executeCommand" + ], + "commands": [ + "build-connect", + "build-import", + "doctor-run" + ], + "serializedCommands": [ + "build-import" + ], + "forwardNotifications": [ + "metals/executeClientCommand", + "metals/status" + ], + "remote": "unsupported", + "gates": [], + "state": { + "scope": "workspace", + "retention": { + "maxEntries": 8, + "maxAgeDays": 30 + } + } + } + } + } + }, + { + "name": "the first step needs the temporary directory", + "operation": "languageServerLaunchPlan", + "requestEdits": [ + { + "op": "remove", + "path": "/temporaryDirectory" + } + ], + "expected": { + "status": "rejected", + "code": "plugin.launch.invalidRequest" + } + }, + { + "name": "the first step refuses an unknown architecture", + "operation": "languageServerLaunchPlan", + "requestEdits": [ + { + "op": "set", + "path": "/architecture", + "value": "sparc" + } + ], + "expected": { + "status": "rejected", + "code": "plugin.launch.invalidRequest" + } + }, + { + "name": "a packaged answer becomes a start plan with its gates", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "arguments": [ + "-Dmetals.client=lithe" + ], + "environment": { + "COURSIER_CACHE": "/tmp/metals/coursier" + }, + "initializationOptions": { + "isHttpEnabled": false + }, + "workspaceConfiguration": { + "metals": { + "showInferredType": true + } + }, + "gates": [ + { + "id": "build-imported", + "until": "notification", + "method": "metals/status", + "match": { + "text": "Indexing complete" + } + } + ], + "writablePaths": [ + "/tmp/metals/coursier", + "/state/metals/workspaces/abc/.bloop" + ] + }, + "expected": { + "status": "ok", + "plan": { + "launch": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "arguments": [ + "-Dmetals.client=lithe" + ], + "validationArguments": [], + "runtimeRequirements": [], + "environment": { + "COURSIER_CACHE": "/tmp/metals/coursier" + }, + "writablePaths": [ + "/state/metals/workspaces/abc/.bloop", + "/tmp/metals/coursier" + ], + "requiredDirectories": [], + "requiredPackagePaths": [ + "server/bin/metals" + ], + "initializationOptions": { + "isHttpEnabled": false + }, + "workspaceConfiguration": { + "metals": { + "showInferredType": true + } + }, + "sessionPolicy": { + "serverID": "scala", + "requestMethods": [ + "textDocument/definition", + "workspace/executeCommand" + ], + "commands": [ + "build-connect", + "build-import", + "doctor-run" + ], + "serializedCommands": [ + "build-import" + ], + "forwardNotifications": [ + "metals/executeClientCommand", + "metals/status" + ], + "remote": "unsupported", + "gates": [ + { + "id": "build-imported", + "until": "notification", + "method": "metals/status", + "match": { + "text": "Indexing complete" + } + } + ], + "state": { + "scope": "workspace", + "retention": { + "maxEntries": 8, + "maxAgeDays": 30 + } + } + } + } + } + }, + { + "name": "a tool answer names a declared tool definition", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "tool", + "tool": "coursier" + }, + "arguments": [ + "launch", + "metals" + ] + }, + "expected": { + "status": "ok", + "plan": { + "launch": { + "kind": "tool", + "tool": "coursier" + }, + "arguments": [ + "launch", + "metals" + ], + "validationArguments": [], + "runtimeRequirements": [], + "environment": {}, + "writablePaths": [], + "requiredDirectories": [], + "requiredPackagePaths": [], + "sessionPolicy": { + "serverID": "scala", + "requestMethods": [ + "textDocument/definition", + "workspace/executeCommand" + ], + "commands": [ + "build-connect", + "build-import", + "doctor-run" + ], + "serializedCommands": [ + "build-import" + ], + "forwardNotifications": [ + "metals/executeClientCommand", + "metals/status" + ], + "remote": "unsupported", + "gates": [], + "state": { + "scope": "workspace", + "retention": { + "maxEntries": 8, + "maxAgeDays": 30 + } + } + } + } + } + }, + { + "name": "an executable outside the package is refused", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "../bin/metals" + } + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.outsidePackage" + } + }, + { + "name": "a tool the plugin does not define is refused", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "tool", + "tool": "sbt" + } + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.undeclaredTool" + } + }, + { + "name": "a writable path outside the host directories is refused", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "writablePaths": [ + "/home/user/.metals" + ] + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.unwritablePath" + } + }, + { + "name": "a writable path cannot climb out of a host directory", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "writablePaths": [ + "/state/metals/../other" + ] + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.unwritablePath" + } + }, + { + "name": "an unknown member of the answer is refused", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "directories": {} + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.invalidResult" + } + }, + { + "name": "initialization options are an object", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "initializationOptions": [ + "not", + "an", + "object" + ] + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.invalidResult" + } + }, + { + "name": "an argument cannot contain NUL", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "arguments": [ + "a\u0000b" + ] + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.invalidResult" + } + }, + { + "name": "an environment name cannot contain an equals sign", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "environment": { + "A=B": "x" + } + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.invalidResult" + } + }, + { + "name": "gate IDs are unique", + "operation": "decodeLaunch", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + }, + "gates": [ + { + "id": "build-imported", + "until": "notification", + "method": "metals/status", + "match": { + "text": "Indexing complete" + } + }, + { + "id": "build-imported", + "until": "notification", + "method": "metals/status", + "match": { + "text": "Indexing complete" + } + } + ] + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.invalidResult" + } + }, + { + "name": "a server the backend does not launch has no answer to decode", + "operation": "decodeLaunch", + "plugin": "language-packaged-server.plugin.json", + "languageID": "php", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + } + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.notBackend" + } + }, + { + "name": "a language without a server has no answer to decode", + "operation": "decodeLaunch", + "languageID": "hack", + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + } + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.unavailable" + } + }, + { + "name": "decoding needs the same directories the first step did", + "operation": "decodeLaunch", + "requestEdits": [ + { + "op": "remove", + "path": "/logDirectory" + } + ], + "result": { + "executable": { + "kind": "packaged", + "path": "server/bin/metals" + } + }, + "expected": { + "status": "rejected", + "code": "plugin.launch.invalidRequest" + } + } + ] +} diff --git a/contract/v1/fixtures/manifest/invalid-manifests.json b/contract/v1/fixtures/manifest/invalid-manifests.json index 65ce236..d29eb3c 100644 --- a/contract/v1/fixtures/manifest/invalid-manifests.json +++ b/contract/v1/fixtures/manifest/invalid-manifests.json @@ -230,12 +230,6 @@ "edits": [{ "op": "set", "path": "/languageServers/0/hostIntegration/id", "value": "roslyn" }], "expectedCode": "plugin.integration.unsupported" }, - { - "name": "host integration requested outside the official namespace", - "base": "language-host-integration.plugin.json", - "edits": [{ "op": "set", "path": "/id", "value": "io.example.java" }, { "op": "set", "path": "/vendor/signatureRequirement", "value": "none" }], - "expectedCode": "plugin.integration.unsupported" - }, { "name": "jdt integration without a java runtime launch", "base": "language-host-integration.plugin.json", @@ -1055,6 +1049,49 @@ {"name": "a grammar names a known engine", "base": "language-grammar.plugin.json", "edits": [{"op": "set", "path": "/languages/0/grammar/kind", "value": "textmate"}], "expectedCode": "plugin.manifest.invalid"}, {"name": "a grammar file stays in the package", "base": "language-grammar.plugin.json", "edits": [{"op": "set", "path": "/languages/0/grammar/path", "value": "../http.monarch.json"}], "expectedCode": "plugin.manifest.invalid"}, {"name": "a comment delimiter is one short line", "base": "language-grammar.plugin.json", "edits": [{"op": "set", "path": "/languages/0/configuration/lineComment", "value": "#\n"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a runtime tool must be listed in requiresTools", "base": "tool-reference-launch.plugin.json", "edits": [{"op": "set", "path": "/requiresTools", "value": ["other-tool"]}], "expectedCode": "plugin.runtime.undeclared"}, + {"name": "requiresTools needs serverProcess 4", "base": "tool-reference-launch.plugin.json", "edits": [{"op": "set", "path": "/requires/serverProcess", "value": 1}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "serverProcess 4 lists its tools", "base": "tool-reference-launch.plugin.json", "edits": [{"op": "remove", "path": "/requiresTools"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a required tool is a tool-definition ID", "base": "tool-reference-launch.plugin.json", "edits": [{"op": "set", "path": "/requiresTools", "value": ["Interpreter"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "adapter features need debugAdapters 3", "base": "debug-adapter-features.plugin.json", "edits": [{"op": "set", "path": "/requires/debugAdapters", "value": 2}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a step filter argument is a DAP name", "base": "debug-adapter-features.plugin.json", "edits": [{"op": "set", "path": "/debugAdapters/0/features/stepFilters/argument", "value": "step filters"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a hot code replace request is a DAP name", "base": "debug-adapter-features.plugin.json", "edits": [{"op": "set", "path": "/debugAdapters/0/features/hotCodeReplace/request", "value": "1reload"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "an output link pattern must compile", "base": "tool-reference-launch.plugin.json", "edits": [{"op": "set", "path": "/outputLinkMatchers/0/pattern", "value": "File ("}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "an output link without a resolver captures a path", "base": "tool-reference-launch.plugin.json", "edits": [{"op": "set", "path": "/outputLinkMatchers/0/pattern", "value": "File (?P.+)"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "run.start needs backend 2", "base": "backend.plugin.json", "edits": [{"op": "set", "path": "/permissions", "value": ["backend.launch", "credentials.store", "run.start", "ui.contribute", "ui.web"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "debug.start needs a declared adapter", "base": "backend-workspace.plugin.json", "edits": [{"op": "set", "path": "/permissions", "value": ["backend.launch", "debug.start"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "session fields need languageServers 3", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/requires/languageServers", "value": 2}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a backend launch needs serverProcess 3", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/requires/serverProcess", "value": 2}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "serverProcess 3 needs pluginAPIVersion 4", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/pluginAPIVersion", "value": 3}], "expectedCode": "plugin.api.unsupported"}, + {"name": "a backend launch needs a backend", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "remove", "path": "/backend"}, {"op": "remove", "path": "/requires/backend"}, {"op": "set", "path": "/permissions", "value": ["languageServer.launch", "languageServer.request"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a backend launch needs backend 2", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/requires/backend", "value": 1}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a backend launch takes its arguments from the answer", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/arguments", "value": ["--stdio"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a backend launch function is a function ID", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/launch/function", "value": "Resolve Launch"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a backend launch timeout stays in range", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/launch/timeoutMilliseconds", "value": 100}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "request methods need the request permission", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/permissions", "value": ["backend.launch", "languageServer.launch"]}], "expectedCode": "plugin.permission.undeclared"}, + {"name": "the request permission needs a server to reach", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "remove", "path": "/languageServers/0/requestMethods"}, {"op": "remove", "path": "/languageServers/0/commands"}, {"op": "remove", "path": "/languageServers/0/serializedCommands"}, {"op": "remove", "path": "/languageServers/0/forwardNotifications"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "executeCommand needs a command list", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "remove", "path": "/languageServers/0/commands"}, {"op": "remove", "path": "/languageServers/0/serializedCommands"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a serialized command is a listed command", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/serializedCommands", "value": ["build-reset"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a request method is listed once", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/requestMethods", "value": ["workspace/executeCommand", "workspace/executeCommand"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a backend cannot send what the host sends", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/requestMethods", "value": ["textDocument/didOpen", "workspace/executeCommand"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a backend cannot forward protocol plumbing", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/forwardNotifications", "value": ["$/progress"]}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "state retention keeps a workspace for a day at least", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/state/retention/maxAgeDays", "value": 0}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "state belongs to a known scope", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/state/scope", "value": "user"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "remote support is supported or unsupported", "base": "language-server-backend-launch.plugin.json", "edits": [{"op": "set", "path": "/languageServers/0/remote", "value": "partial"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a content provider needs backend 2", "base": "backend.plugin.json", "edits": [{"op": "set", "path": "/permissions/1", "value": "documents.contentProvider"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "an export states its calling context", "base": "plugin-functions-exporter.plugin.json", "edits": [{"op": "remove", "path": "/pluginFunctions/exports/0/context"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "an export names a context Lithe knows", "base": "plugin-functions-exporter.plugin.json", "edits": [{"op": "set", "path": "/pluginFunctions/exports/0/context", "value": "remote"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "an export is an object, not a bare function ID", "base": "plugin-functions-exporter.plugin.json", "edits": [{"op": "set", "path": "/pluginFunctions/exports/0", "value": "document.format"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "an export version is a positive integer", "base": "plugin-functions-exporter.plugin.json", "edits": [{"op": "set", "path": "/pluginFunctions/exports/0/version", "value": 0}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a function is exported once", "base": "plugin-functions-exporter.plugin.json", "edits": [{"op": "set", "path": "/pluginFunctions/exports/1/id", "value": "document.format"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "an exported function ID is lowerCamelCase segments", "base": "plugin-functions-exporter.plugin.json", "edits": [{"op": "set", "path": "/pluginFunctions/exports/0/id", "value": "Document.Format"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a call states the contract version it was written for", "base": "plugin-functions-caller.plugin.json", "edits": [{"op": "remove", "path": "/pluginFunctions/calls/0/version"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a call never names the provider plugin", "base": "plugin-functions-caller.plugin.json", "edits": [{"op": "set", "path": "/pluginFunctions/calls/0/pluginID", "value": "dev.lithe.fixture.functionexporter"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "exported and called functions need backend 2", "base": "plugin-functions-exporter.plugin.json", "edits": [{"op": "set", "path": "/requires/backend", "value": 1}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "pluginFunctions lists an export or a call", "base": "plugin-functions-exporter.plugin.json", "edits": [{"op": "set", "path": "/pluginFunctions", "value": {}}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "pluginFunctions is listed in requires", "base": "plugin-functions-caller.plugin.json", "edits": [{"op": "remove", "path": "/requires/pluginFunctions"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "requires lists pluginFunctions only when the manifest uses it", "base": "plugin-functions-caller.plugin.json", "edits": [{"op": "remove", "path": "/pluginFunctions"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "editor providers read the documents they answer for", "base": "editor-providers.plugin.json", "edits": [{"op": "set", "path": "/permissions", "value": ["backend.launch", "editor.providers", "workspace.metadata"]}], "expectedCode": "plugin.manifest.invalid"}, {"name": "a tool field needs projectGenerators 2", "base": "project-generator.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/fields/1", "value": {"id": "sdk", "type": "tool", "title": "SDK", "tool": "python"}}], "expectedCode": "plugin.manifest.invalid"}, {"name": "projectGenerators 2 has no runtime field", "base": "project-generator.plugin.json", "edits": [{"op": "set", "path": "/requires/projectGenerators", "value": 2}], "expectedCode": "plugin.manifest.invalid"}, {"name": "a tool field names a tool definition ID", "base": "project-generator-tool-field.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/fields/0/tool", "value": "Python Interpreter"}], "expectedCode": "plugin.manifest.invalid"}, diff --git a/contract/v1/fixtures/run/output-link-matchers.json b/contract/v1/fixtures/run/output-link-matchers.json new file mode 100644 index 0000000..1370b78 --- /dev/null +++ b/contract/v1/fixtures/run/output-link-matchers.json @@ -0,0 +1,192 @@ +{ + "description": "Requests of outputLinks.match and the links Core returns, and replies of a matcher's resolve function with what outputLinks.decodeResolution returns. `start` and `length` count UTF-16 code units of the console line. A relative path yields one candidate per workspace root, and a path outside every root yields no link.", + "match": [ + { + "name": "a frame in the workspace links to its line", + "request": { + "scope": "run", + "platform": "macos", + "workspaceRoots": ["/work/app"], + "sources": [ + { + "pluginID": "example.script", + "matchers": [ + { "id": "frame", "pattern": "File \"(?P[^\"]+)\", line (?P\\d+)" } + ] + } + ], + "lines": [" File \"/work/app/main.script\", line 12, in "] + }, + "links": [ + { + "line": 0, + "start": 2, + "length": 37, + "pluginID": "example.script", + "matcherID": "frame", + "location": { "paths": ["/work/app/main.script"], "line": 12 } + } + ] + }, + { + "name": "a relative path becomes one candidate per workspace root", + "request": { + "scope": "run", + "platform": "linux", + "workspaceRoots": ["/work/app", "/work/lib"], + "sources": [ + { + "pluginID": "example.script", + "matchers": [ + { "id": "frame", "pattern": "File \"(?P[^\"]+)\", line (?P\\d+)" } + ] + } + ], + "lines": [" File \"./pkg/util.script\", line 3"] + }, + "links": [ + { + "line": 0, + "start": 2, + "length": 32, + "pluginID": "example.script", + "matcherID": "frame", + "location": { + "paths": ["/work/app/pkg/util.script", "/work/lib/pkg/util.script"], + "line": 3 + } + } + ] + }, + { + "name": "positions count UTF-16 code units", + "request": { + "scope": "test", + "platform": "linux", + "workspaceRoots": ["/work/app"], + "sources": [ + { + "pluginID": "example.script", + "matchers": [ + { "id": "frame", "pattern": "File \"(?P[^\"]+)\", line (?P\\d+)" } + ] + } + ], + "lines": ["😀 File \"/work/app/a.script\", line 2"] + }, + "links": [ + { + "line": 0, + "start": 3, + "length": 33, + "pluginID": "example.script", + "matcherID": "frame", + "location": { "paths": ["/work/app/a.script"], "line": 2 } + } + ] + }, + { + "name": "paths outside the workspace are not linked", + "request": { + "scope": "run", + "platform": "linux", + "workspaceRoots": ["/work/app"], + "sources": [ + { + "pluginID": "example.script", + "matchers": [ + { "id": "frame", "pattern": "File \"(?P[^\"]+)\", line (?P\\d+)" } + ] + } + ], + "lines": [ + " File \"/usr/lib/script/os.script\", line 1", + " File \"../outside.script\", line 1" + ] + }, + "links": [] + }, + { + "name": "a matcher of the other console does not apply", + "request": { + "scope": "run", + "platform": "linux", + "workspaceRoots": ["/work/app"], + "sources": [ + { + "pluginID": "example.script", + "matchers": [ + { "id": "test-frame", "pattern": "(?P\\S+\\.script)", "scope": "test" } + ] + } + ], + "lines": ["/work/app/main.script"] + }, + "links": [] + }, + { + "name": "a matcher with a resolve function reports its groups", + "request": { + "scope": "test", + "platform": "linux", + "workspaceRoots": ["/work"], + "sources": [ + { + "pluginID": "example.script", + "matchers": [ + { + "id": "symbol", + "pattern": "(?P[\\w/]+)\\.(?P\\w+)\\(", + "resolve": { "function": "links.resolve" } + } + ] + } + ], + "lines": ["main.handler(0x1)"] + }, + "links": [ + { + "line": 0, + "start": 0, + "length": 13, + "pluginID": "example.script", + "matcherID": "symbol", + "resolve": { + "function": "links.resolve", + "groups": { "package": "main", "symbol": "handler" } + } + } + ] + } + ], + "resolution": [ + { + "name": "a file URI with a 0-based start position", + "reply": { + "uri": "file:///work/main%20app.script", + "range": { "start": { "line": 9, "character": 0 } } + }, + "location": { "path": "/work/main app.script", "line": 10, "column": 1 } + }, + { + "name": "a file URI without a range", + "reply": { "uri": "file:///work/main.script" }, + "location": { "path": "/work/main.script" } + }, + { + "name": "nothing found", + "reply": null, + "location": null + }, + { + "name": "a URI that is not a local file", + "reply": { "uri": "https://example.com/a.script" }, + "invalid": true + }, + { + "name": "a reply member Core does not know", + "reply": { "uri": "file:///work/main.script", "extra": 1 }, + "invalid": true + } + ] +} diff --git a/contract/v1/fixtures/run/run-start.json b/contract/v1/fixtures/run/run-start.json new file mode 100644 index 0000000..50228af --- /dev/null +++ b/contract/v1/fixtures/run/run-start.json @@ -0,0 +1,284 @@ +{ + "description": "Parameters of lithe/host/run/start and what run.decodeStart returns. The host passes the plugin's manifest (`manifest`, a file in this directory), a workspace root that holds the directories in `workspaceDirectories`, and the request's `params`. `start` is the normalized start; `error` is the rejection every host reports. Cases with temporary files depend on the host's temporary directory and are covered by Core's own tests.", + "manifest": "tool-reference-launch.plugin.json", + "workspaceDirectories": ["app", "app/tools"], + "cases": [ + { + "name": "a required tool runs in a workspace directory", + "params": { + "context": { "contextID": "w-run", "generation": 1 }, + "label": "Run main.script", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": ["main.script", "a b"], + "workingDirectory": "app", + "environment": { "MODE": "test" } + } + }, + "start": { + "label": "Run main.script", + "executable": { "kind": "toolchain", "value": "interpreter" }, + "arguments": ["main.script", "a b"], + "workingDirectory": "app", + "environment": { "MODE": "test" }, + "temporaryFiles": [] + } + }, + { + "name": "a file packaged with the plugin is the executable", + "params": { + "label": "Run helper", + "process": { + "executable": { "kind": "packaged", "path": "bin/helper" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + } + }, + "start": { + "label": "Run helper", + "executable": { "kind": "packaged", "value": "bin/helper" }, + "arguments": [], + "workingDirectory": ".", + "environment": {}, + "temporaryFiles": [] + } + }, + { + "name": "a debug connection and a rerun function are kept", + "params": { + "label": "Debug main.script", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": ["--listen", "5678", "main.script"], + "workingDirectory": "app/tools", + "environment": {} + }, + "debug": { + "configurationType": "script", + "target": { "kind": "connection", "port": 5678 } + }, + "rerun": { "function": "script.rerun", "data": { "file": "main.script" } } + }, + "start": { + "label": "Debug main.script", + "executable": { "kind": "toolchain", "value": "interpreter" }, + "arguments": ["--listen", "5678", "main.script"], + "workingDirectory": "app/tools", + "environment": {}, + "temporaryFiles": [], + "debug": { + "configurationType": "script", + "target": { "kind": "connection", "port": 5678 } + }, + "rerun": { "function": "script.rerun", "data": { "file": "main.script" } } + } + }, + { + "name": "a debug launch target needs no port", + "params": { + "label": "Debug main.script", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": "app", + "environment": {} + }, + "debug": { "configurationType": "script", "target": { "kind": "launch" } } + }, + "start": { + "label": "Debug main.script", + "executable": { "kind": "toolchain", "value": "interpreter" }, + "arguments": [], + "workingDirectory": "app", + "environment": {}, + "temporaryFiles": [], + "debug": { "configurationType": "script", "target": { "kind": "launch" } } + } + }, + { + "name": "a tool the manifest does not require", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "other-tool" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "process.executable.tool" } + }, + { + "name": "an executable of an unknown kind", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "command", "command": "sh" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "process.executable" } + }, + { + "name": "a packaged executable leaves the package", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "packaged", "path": "../bin/helper" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "process.executable.path" } + }, + { + "name": "a working directory outside the workspace", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": "../elsewhere", + "environment": {} + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "process" } + }, + { + "name": "a working directory that does not exist", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": "missing", + "environment": {} + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "process" } + }, + { + "name": "an environment name that is not a variable name", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": { "1BAD": "x" } + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "process" } + }, + { + "name": "a process field the host does not know", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {}, + "shell": true + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "process.shell" } + }, + { + "name": "a label with a line break", + "params": { + "label": "Run\nmain", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "label" } + }, + { + "name": "a debug connection port out of range", + "params": { + "label": "Debug", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + }, + "debug": { + "configurationType": "script", + "target": { "kind": "connection", "port": 0 } + } + }, + "error": { "code": "plugin.run.invalidStart", "field": "debug.target.port" } + }, + { + "name": "a rerun function that is not a function ID", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + }, + "rerun": { "function": "Not A Function", "data": null } + }, + "error": { "code": "plugin.run.invalidStart", "field": "rerun.function" } + }, + { + "name": "rerun data that is not an object", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + }, + "rerun": { "function": "script.rerun", "data": "main.script" } + }, + "error": { "code": "plugin.run.invalidStart", "field": "rerun.data" } + }, + { + "name": "more than four temporary files", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + }, + "temporaryFiles": [ + { "name": "a", "contents": "" }, + { "name": "b", "contents": "" }, + { "name": "c", "contents": "" }, + { "name": "d", "contents": "" }, + { "name": "e", "contents": "" } + ] + }, + "error": { "code": "plugin.run.invalidStart", "field": "temporaryFiles" } + }, + { + "name": "an unknown start parameter", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + }, + "background": true + }, + "error": { "code": "plugin.run.invalidStart", "field": "params" } + } + ] +} diff --git a/contract/v1/fixtures/run/tool-reference-launch.plugin.json b/contract/v1/fixtures/run/tool-reference-launch.plugin.json new file mode 100644 index 0000000..a713a79 --- /dev/null +++ b/contract/v1/fixtures/run/tool-reference-launch.plugin.json @@ -0,0 +1,105 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.tool-reference-launch", + "displayName": "Tool Reference Launch Fixture", + "version": "0.1.0", + "pluginAPIVersion": 4, + "requires": { + "backend": 2, + "debugAdapters": 1, + "executableResolution": 2, + "languageServers": 1, + "languages": 1, + "outputLinkMatchers": 1, + "serverProcess": 4, + "toolDefinitions": 1 + }, + "requiresTools": ["interpreter"], + "vendor": { + "id": "org.example", + "displayName": "Example", + "signatureRequirement": "publisherPackage" + }, + "permissions": [ + "backend.launch", + "debug.launch", + "debug.start", + "languageServer.launch", + "run.start" + ], + "activationEvents": [ + "onDebug:script", + "onLanguage:script", + "onWorkspaceOpen" + ], + "languages": [ + { + "id": "script", + "displayName": "Script", + "fileExtensions": ["script"] + } + ], + "toolDefinitions": [ + { + "id": "interpreter", + "displayName": "Script interpreter", + "icon": { "host": "plugin" }, + "executableLayouts": [ + { "path": "bin/interpreter", "platforms": ["macos", "linux"] }, + { "path": "bin/interpreter.exe", "platforms": ["windows"] } + ], + "executables": [ + { "id": "default", "kind": "setting", "settingID": "executable", "layer": "global" }, + { "id": "shell", "kind": "command", "command": "interpreter" } + ], + "version": { + "arguments": ["--version"], + "pattern": "Interpreter (\\d+(?:\\.\\d+)*)" + }, + "homeLayouts": [{ "match": "*", "home": "." }], + "commands": ["interpreter"], + "launch": ["${executable}", "${runtimeArguments}", "${entry}", "${arguments}"] + } + ], + "languageServers": [ + { + "languageID": "script", + "launch": { + "kind": "runtime", + "tool": "interpreter", + "minimumVersion": "3.9", + "runtimeArguments": ["-X", "utf8"], + "entry": "language-server/server.script" + }, + "arguments": ["--stdio"] + } + ], + "backend": { + "launch": { "kind": "packaged", "path": "bin/backend" }, + "arguments": ["--state", "${stateDir}"] + }, + "debugAdapters": [ + { + "id": "script", + "displayName": "Script Debugger", + "adapterID": "script-dap", + "requests": ["launch"], + "selectors": [{ "targetKinds": ["file"], "languageIDs": ["script"] }], + "configurationTypes": ["script"], + "launch": { + "process": { + "launch": { "kind": "packaged", "path": "bin/script-dap" }, + "arguments": ["--stdio"] + }, + "transport": "stdio" + } + } + ], + "outputLinkMatchers": [ + { + "id": "traceback", + "pattern": "File \"(?P[^\"]+)\", line (?P\\d+)", + "scope": "run" + } + ] +} diff --git a/contract/v1/plugin-manifest.schema.json b/contract/v1/plugin-manifest.schema.json index 3122ad9..131feab 100644 --- a/contract/v1/plugin-manifest.schema.json +++ b/contract/v1/plugin-manifest.schema.json @@ -18,11 +18,17 @@ "if": { "required": ["pluginAPIVersion"], "properties": { "pluginAPIVersion": { "maximum": 1 } } }, "then": { "properties": { "settings": { "maxItems": 0 } } } }, + {"if": {"required": ["requiresTools"]}, "then": {"properties": {"pluginAPIVersion": {"minimum": 4}, "requires": {"required": ["serverProcess"], "properties": {"serverProcess": {"const": 4}}}}}}, + {"if": {"required": ["outputLinkMatchers"]}, "then": {"properties": {"pluginAPIVersion": {"minimum": 4}, "requires": {"required": ["outputLinkMatchers"]}}}}, + {"if": {"required": ["requires"], "properties": {"requires": {"required": ["debugAdapters"], "properties": {"debugAdapters": {"const": 3}}}}}, "else": {"properties": {"debugAdapters": {"items": {"properties": {"features": false, "defaults": false}}}}}}, {"if": {"required": ["toolDefinitions"]}, "then": {"properties": {"pluginAPIVersion": {"minimum": 3}, "requires": {"required": ["toolDefinitions", "executableResolution"], "properties": {"executableResolution": {"const": 2}}}}}}, {"if": {"required": ["toolchainProviders"]}, "then": {"required": ["backend"], "properties": {"pluginAPIVersion": {"minimum": 3}, "requires": {"required": ["toolchainProviders", "backend", "serverProcess"], "properties": {"backend": {"const": 2}}}, "permissions": {"allOf": [{"contains": {"const": "toolchains.provide"}}, {"contains": {"const": "backend.launch"}}]}}}}, + {"if": {"required": ["pluginFunctions"]}, "then": {"required": ["backend"], "properties": {"pluginAPIVersion": {"minimum": 3}, "requires": {"required": ["pluginFunctions", "backend", "serverProcess"], "properties": {"backend": {"const": 2}}}}}}, {"if": {"required": ["keymapImportProviders"]}, "then": {"required": ["backend", "permissions"], "properties": {"pluginAPIVersion": {"minimum": 3}, "requires": {"required": ["keymapImportProviders", "backend", "serverProcess"], "properties": {"backend": {"const": 2}}}, "permissions": {"allOf": [{"contains": {"const": "keymaps.provide"}}, {"contains": {"const": "keymaps.readSelectedSources"}}, {"contains": {"const": "backend.launch"}}]}}}}, {"if": {"required": ["requires"], "properties": {"requires": {"required": ["settings"], "properties": {"settings": {"const": 2}}}}}, "else": {"properties": {"settings": {"items": {"$ref": "#/$defs/settingVersion1"}}, "settingActions": {"maxItems": 0}}}}, - {"if": {"required": ["requires"], "properties": {"requires": {"required": ["languageServers"], "properties": {"languageServers": {"const": 2}}}}}, "else": {"properties": {"languageServers": {"items": {"properties": {"workspaceConfiguration": false}}}}}} + {"if": {"required": ["requires"], "properties": {"requires": {"required": ["languageServers"], "properties": {"languageServers": {"minimum": 2}}}}}, "else": {"properties": {"languageServers": {"items": {"properties": {"workspaceConfiguration": false}}}}}}, + {"if": {"required": ["requires"], "properties": {"requires": {"required": ["languageServers"], "properties": {"languageServers": {"const": 3}}}}}, "else": {"properties": {"languageServers": {"items": {"properties": {"requestMethods": false, "commands": false, "serializedCommands": false, "forwardNotifications": false, "state": false, "remote": false}}}}}}, + {"if": {"required": ["languageServers"], "properties": {"languageServers": {"contains": {"required": ["launch"], "properties": {"launch": {"required": ["kind"], "properties": {"kind": {"const": "backend"}}}}}}}}, "then": {"required": ["backend"], "properties": {"pluginAPIVersion": {"minimum": 4}, "requires": {"required": ["backend", "serverProcess"], "properties": {"backend": {"const": 2}, "serverProcess": {"const": 3}}}}}} ], "properties": { "schemaVersion": { "const": 2 }, @@ -40,18 +46,20 @@ "buildModelInputs": {"description": "Optional public model sharing between already enabled workspace plugins; no installation, private function, or process authority. Required exactly when modelSharing or modelInputs is used. Consumers require buildProviders 6. Version 1 producers cannot also consume inputs.", "enum": [1]}, "buildProviders": { "description": "Required exactly when any buildSystems[].provider is present, including own-process and shared language-server providers. Declares host support for Provider orchestration, independent of declarative buildSystems and serverProcess launch support; version 2 adds task execution, version 3 test execution/discovery, version 4 application run preparation, and version 5 native debug preparation; version 6 optional read-only model inputs; version 7 non-sensitive native configuration sources; version 8 inline provider test reports; version 9 opaque semantic test selectors; version 10 typed run options and service/task preparation; version 11 registry-selected adapter and host-owned connection target for service debugging.", "enum": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] }, "buildSystems": { "description": "Declarative build detection, models, and command plans; v2 adds the executable setting adapter, v3 instance-scoped options, v4 declarative preparation roles; does not imply buildProviders support.", "enum": [1, 2, 3, 4] }, - "debugAdapters": { "description": "Required exactly when debugAdapters is non-empty. Version 2 adds bounded target argument and environment bindings. The DAP session itself is negotiated by the adapter in initialize.", "enum": [1, 2] }, + "debugAdapters": { "description": "Required exactly when debugAdapters is non-empty. Version 2 adds bounded target argument and environment bindings. Version 3 adds debugAdapters[].features and defaults, and is required exactly when one is used. The DAP session itself is negotiated by the adapter in initialize.", "enum": [1, 2, 3] }, "executableResolution": { "enum": [1, 2], "description": "Shared executable candidate ordering and strict selection." }, "executionProviders": { "enum": [1] }, - "languageServers": { "description": "Required exactly when languageServers is non-empty. Version 2 adds languageServers[].workspaceConfiguration.", "enum": [1, 2] }, + "languageServers": { "description": "Required exactly when languageServers is non-empty. Version 2 adds languageServers[].workspaceConfiguration. Version 3 adds the session fields requestMethods, commands, serializedCommands, forwardNotifications, state, and remote.", "enum": [1, 2, 3] }, "languages": { "description": "Version 2 adds languages[].grammar and languages[].configuration and is required exactly when one of them is used.", "enum": [1, 2] }, + "outputLinkMatchers": { "description": "Required exactly when outputLinkMatchers is non-empty.", "const": 1 }, "packageTrust": { "description": "Required exactly when vendor.bundledSignatureRequirement is present.", "enum": [1] }, "projectGenerators": { "description": "Entries of the New Project window: fields, requirements, and steps. Version 2 adds the tool field and retires the runtime field.", "enum": [1, 2] }, - "serverProcess": { "description": "The launch fields of language servers, of build providers that start their own process, of VCS providers, of debug adapters, and of the plugin backend. Version 2 adds runSelectedRuntime and is required exactly when a debug adapter uses it (pluginAPIVersion 4).", "enum": [1, 2] }, + "serverProcess": { "description": "The launch fields of language servers, of build providers that start their own process, of VCS providers, of debug adapters, and of the plugin backend. Versions are cumulative: declare the lowest version that covers every launch the manifest uses. Version 2 adds runSelectedRuntime for a debug adapter (pluginAPIVersion 4). Version 3 adds the backend-resolved launch of a language server (pluginAPIVersion 4, backend 2). Version 4 names runtimes by tool-definition ID and is declared exactly when requiresTools is present.", "enum": [1, 2, 3, 4] }, "settings": { "description": "Required exactly when settings or settingActions is non-empty. Version 1 is the API level 2 section: integer, enum, and boolean options. Version 2 adds scope, effect, page, section, the multiSelect, text, file, and directory types, and settingActions.", "enum": [1, 2] }, "toolDefinitions": { "const": 1 }, "toolchainProviders": { "const": 1 }, "keymapImportProviders": { "const": 1 }, + "pluginFunctions": { "description": "Required exactly when pluginFunctions is present: functions this plugin's backend exports to other plugins, and exported functions it calls. Needs backend 2 (litheFunctions protocol 4).", "const": 1 }, "ui": { "description": "Version 2 adds web content; version 3 adds native controls; version 4 adds backend-only commands.", "enum": [1, 2, 3, 4] }, "vcsProviders": { "description": "Required exactly when vcsProviders is non-empty. The provider protocol itself is negotiated in initialize (shared/contracts/vcs-provider-v1.md).", "enum": [1] } } @@ -79,6 +87,7 @@ "documents.read", "editor.edit", "editor.actions", + "editor.providers", "editor.open", "preview.open", "toolchains.provide", @@ -86,7 +95,11 @@ "keymaps.readSelectedSources", "browser.open", "window.messages", - "terminal.create"] } + "terminal.create", + "run.start", + "debug.start", + "languageServer.request", + "documents.contentProvider"] } }, "activationEvents": { "type": "array", @@ -97,8 +110,24 @@ } }, "toolDefinitions": { "type": "array", "minItems": 1, "maxItems": 32, "items": { "$ref": "#/$defs/toolchainToolDefinition" } }, + "requiresTools": { + "description": "requires.serverProcess 4: every tool definition the plugin starts or names, by ID. A runtime tool of a launch, a runtime dependency, a runSelectedRuntime tool, or the tool of a lithe/host/run/start must be listed here. An ID is short or :; a short ID defined by more than one enabled plugin belongs to none of them.", + "type": "array", + "minItems": 1, + "maxItems": 32, + "uniqueItems": true, + "items": { "$ref": "#/$defs/toolchainToolID" } + }, + "outputLinkMatchers": { + "description": "requires.outputLinkMatchers 1: patterns that turn Run and Test console text into links.", + "type": "array", + "minItems": 1, + "maxItems": 16, + "items": { "$ref": "#/$defs/outputLinkMatcher" } + }, "toolchainProviders": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "$ref": "#/$defs/toolchainProvider" } }, "keymapImportProviders": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "$ref": "#/$defs/keymapImportProvider" } }, + "pluginFunctions": { "$ref": "#/$defs/pluginFunctions" }, "languages": { "type": "array", "minItems": 0, @@ -268,6 +297,41 @@ } }, "$defs": { + "pluginFunctionID": { + "description": "Dot-separated lowerCamelCase segments, for example document.format.", + "type": "string", + "maxLength": 128, + "pattern": "^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$" + }, + "pluginFunctions": { + "description": "Functions this plugin exports to other plugins and exported functions it calls. A function is private to its plugin unless it is exported here and registered at runtime with lithe/function/register; calling it takes no permission, because the receiver's export and the caller's declaration are the consent. The called function runs with the receiver's own permissions.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "exports": { "type": "array", "minItems": 1, "maxItems": 32, "items": { "$ref": "#/$defs/pluginFunctionExport" } }, + "calls": { "type": "array", "minItems": 1, "maxItems": 32, "items": { "$ref": "#/$defs/pluginFunctionCall" } } + } + }, + "pluginFunctionExport": { + "type": "object", + "additionalProperties": false, + "required": ["id", "version", "context"], + "properties": { + "id": { "$ref": "#/$defs/pluginFunctionID" }, + "version": { "description": "Version of the function's input and result contract; an incompatible change uses a new version.", "type": "integer", "minimum": 1, "maximum": 1000 }, + "context": { "description": "The one calling context the function accepts. workspace: a local trusted workspace, scheduled by the host. An export without a context Lithe knows is refused.", "enum": ["workspace"] } + } + }, + "pluginFunctionCall": { + "type": "object", + "additionalProperties": false, + "required": ["id", "version"], + "properties": { + "id": { "$ref": "#/$defs/pluginFunctionID" }, + "version": { "description": "The export contract version this plugin was written for; Core refuses a provider that exports another version.", "type": "integer", "minimum": 1, "maximum": 1000 } + } + }, "keymapImportProvider": { "type": "object", "additionalProperties": false, @@ -434,11 +498,8 @@ "const": "runSelectedRuntime" }, "tool": { - "enum": [ - "python", - "java", - "node" - ] + "description": "serverProcess 2 accepts python, java and node; serverProcess 4 accepts the ID of a tool definition listed in requiresTools.", + "$ref": "#/$defs/toolchainToolID" } } }, @@ -816,6 +877,93 @@ }, "launch": { "$ref": "#/$defs/debugLaunch" + }, + "features": { + "description": "debugAdapters 3: adapter-specific DAP extensions the adapter implements.", + "$ref": "#/$defs/debugAdapterFeatures" + }, + "defaults": { + "description": "debugAdapters 3: values the adapter starts with when the user configured none.", + "$ref": "#/$defs/debugAdapterDefaults" + } + } + }, + "debugDeclaredName": { + "description": "A DAP command, event, field or argument name: a letter followed by up to 63 letters, digits, dots, underscores or hyphens.", + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,63}$" + }, + "debugAdapterFeatures": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "hotCodeReplace": { + "description": "A custom request that applies changed code to the running debuggee.", + "type": "object", + "additionalProperties": false, + "required": ["request"], + "properties": { + "request": { "description": "DAP command sent for a redefineClasses inspection.", "$ref": "#/$defs/debugDeclaredName" }, + "event": { "description": "DAP event whose message the Debug Console shows as progress.", "$ref": "#/$defs/debugDeclaredName" }, + "changedField": { "description": "Response body field listing the replaced units.", "$ref": "#/$defs/debugDeclaredName" }, + "errorField": { "description": "Response body field with a failure message the adapter reports in a successful response.", "$ref": "#/$defs/debugDeclaredName" } + } + }, + "stepFilters": { + "description": "The launch argument through which the adapter receives {skipClasses, skipSynthetics, skipStaticInitializers, skipConstructors}.", + "type": "object", + "additionalProperties": false, + "required": ["argument"], + "properties": { + "argument": { "$ref": "#/$defs/debugDeclaredName" } + } + } + } + }, + "debugAdapterDefaults": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "stepFilters": { + "type": "object", + "additionalProperties": false, + "properties": { + "classNameFilters": { + "type": "array", + "maxItems": 256, + "items": { "type": "string", "minLength": 1, "maxLength": 256 } + }, + "skipSynthetics": { "type": "boolean" }, + "skipStaticInitializers": { "type": "boolean" }, + "skipConstructors": { "type": "boolean" }, + "hideFilteredStackFrames": { "type": "boolean" } + } + } + } + }, + "outputLinkMatcher": { + "type": "object", + "additionalProperties": false, + "required": ["id", "pattern"], + "properties": { + "id": { "description": "Plugin-local ID, unique in the plugin.", "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,63}$" }, + "pattern": { + "description": "A Rust regex matched against one console line. Without resolve it captures path and may capture line and column; it must not match empty text.", + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "scope": { "description": "The console the matcher applies to; both when absent.", "enum": ["run", "test"] }, + "resolve": { + "description": "A function of the plugin's backend 2 process that turns the named groups into a location.", + "type": "object", + "additionalProperties": false, + "required": ["function"], + "properties": { + "function": { "type": "string", "minLength": 1 } + } } } }, @@ -873,7 +1021,7 @@ "optional": { "type": "boolean" }, "runtime": { "enum": ["java"] }, "tool": { - "description": "tool: a tool definition ID, the short ID of a verified official definition or :.", + "description": "tool: a tool definition ID, a short ID or :; a short ID that several enabled plugins define belongs to none of them.", "type": "string", "maxLength": 192, "pattern": "^([a-z0-9]+([.-][a-z0-9]+)*:)?[a-z][a-z0-9-]{0,63}$" @@ -1262,11 +1410,24 @@ "maxItems": 2, "items": { "$ref": "#/$defs/editorDelimiter" } }, + "protocolName": { + "description": "An LSP method or command name: non-empty text of at most 256 bytes without whitespace or control characters.", + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, "languageServer": { "type": "object", "$ref": "#/$defs/serverProcess", "unevaluatedProperties": false, "required": ["languageID"], + "allOf": [ + { + "if": { "required": ["launch"], "properties": { "launch": { "required": ["kind"], "properties": { "kind": { "const": "backend" } } } } }, + "then": { "properties": { "arguments": false, "validationArguments": false, "runtimeDependencies": false, "initializationOptions": false }, "not": { "required": ["hostIntegration"] } } + } + ], "properties": { "languageID": { "type": "string" }, "languageIdentifier": { "type": "string", "minLength": 1 }, @@ -1274,8 +1435,58 @@ "description": "languageServers 2: what the server reads through workspace/configuration and receives again in workspace/didChangeConfiguration. String leaves may be ${setting:} templates, including settings whose effect is immediate, and no other variable.", "type": "object" }, + "requestMethods": { + "description": "languageServers 3: LSP methods the plugin backend may send with languageServer/request or languageServer/notify. The methods the host sends itself are refused. Requires the languageServer.request permission.", + "type": "array", + "maxItems": 64, + "uniqueItems": true, + "items": { "$ref": "#/$defs/protocolName" } + }, + "commands": { + "description": "languageServers 3: workspace/executeCommand commands the backend may run. Declared exactly when requestMethods lists workspace/executeCommand.", + "type": "array", + "maxItems": 128, + "uniqueItems": true, + "items": { "$ref": "#/$defs/protocolName" } + }, + "serializedCommands": { + "description": "languageServers 3: commands that run one at a time; a later request for a running command joins it. Each is also listed in commands.", + "type": "array", + "maxItems": 128, + "uniqueItems": true, + "items": { "$ref": "#/$defs/protocolName" } + }, + "forwardNotifications": { + "description": "languageServers 3: server notifications the host forwards to the backend. Requires the languageServer.request permission.", + "type": "array", + "maxItems": 32, + "uniqueItems": true, + "items": { "$ref": "#/$defs/protocolName" } + }, + "state": { + "description": "languageServers 3: how the host keeps the server's per-workspace state directories.", + "type": "object", + "additionalProperties": false, + "required": ["scope", "retention"], + "properties": { + "scope": { "const": "workspace" }, + "retention": { + "type": "object", + "additionalProperties": false, + "required": ["maxEntries", "maxAgeDays"], + "properties": { + "maxEntries": { "type": "integer", "minimum": 1, "maximum": 64 }, + "maxAgeDays": { "type": "integer", "minimum": 1, "maximum": 365 } + } + } + } + }, + "remote": { + "description": "languageServers 3: whether the server works on a remote workspace. Supported when absent.", + "enum": ["supported", "unsupported"] + }, "hostIntegration": { - "description": "pluginAPIVersion 2, official plugins only: a named host integration and the package resources it reads.", + "description": "pluginAPIVersion 2: a named host integration and the package resources it reads.", "type": "object", "additionalProperties": false, "required": ["id"], @@ -1336,12 +1547,23 @@ "required": ["kind", "tool", "entry"], "properties": { "kind": { "const": "runtime" }, - "tool": { "enum": ["java", "node"] }, + "tool": { "description": "serverProcess 1 and 2 accept java and node; serverProcess 4 accepts the ID of a tool definition listed in requiresTools.", "$ref": "#/$defs/toolchainToolID" }, "minimumVersion": { "$ref": "#/$defs/minimumVersion" }, "packagedRuntime": { "type": "string", "minLength": 1 }, "runtimeArguments": { "type": "array", "items": { "type": "string" } }, "entry": { "type": "string", "minLength": 1 } } + }, + { + "description": "serverProcess 3, language servers only: the plugin backend answers the command line each time the server starts. Core refuses it in any other process.", + "type": "object", + "additionalProperties": false, + "required": ["kind", "function"], + "properties": { + "kind": { "const": "backend" }, + "function": { "description": "Plugin function ID: dot-separated lowerCamelCase segments.", "type": "string", "maxLength": 128, "pattern": "^[a-z][A-Za-z0-9]*(\\.[a-z][A-Za-z0-9]*)*(?![\\s\\S])" }, + "timeoutMilliseconds": { "type": "integer", "minimum": 1000, "maximum": 120000 } + } } ] }, @@ -1353,7 +1575,7 @@ "type": "object", "additionalProperties": false, "required": ["tool"], - "properties": { "tool": { "enum": ["go", "java", "node"] } } + "properties": { "tool": { "description": "serverProcess 1 and 2 accept go, java and node; serverProcess 4 accepts the ID of a tool definition listed in requiresTools.", "$ref": "#/$defs/toolchainToolID" } } } }, "initializationOptions": { diff --git a/contract/v1/toolchains/README.md b/contract/v1/toolchains/README.md index a11e308..80654a2 100644 --- a/contract/v1/toolchains/README.md +++ b/contract/v1/toolchains/README.md @@ -14,8 +14,6 @@ settings detections are never used as launch permits. Existing Java and language-server consumers still use their previous adapters. Windows has no toolchain adapter and does not advertise `toolDefinitions` or `toolchainProviders`, so Core refuses packages that require them there. -Publisher signatures provide independent origin evidence when present; legacy -unsigned records never do. Consumer migration, the native query lifecycle and cache, pin saving and version-manager plugins are not specified yet. The manifest shape is [plugin-manifest.schema.json](../plugin-manifest.schema.json). @@ -47,13 +45,18 @@ replies may not contain statements. Providers share the plugin's backend; admission permits a host-query trigger without a page or startup event, and the macOS host starts the backend on the first query. Bootstrap must be `packaged`, or `runtime` with `packagedRuntime`, without discovered runtime dependencies. Existing -runtime launch tool restrictions remain; generic consumer launches are not added. +runtime launch tool restrictions remain for `serverProcess` 1 and 2, which name +a closed set of runtimes. From `serverProcess` 4 a plugin names any tool definition +by ID instead; see [`instantiateLaunch`](#launching-a-tool-definition). `toolchains.provide` may influence which program a defined tool launches. It does not grant arbitrary command replacement, installation, network or workspace-code -execution. Short definition IDs require independently verified official origin; -community definitions use `:`. Community providers may -answer short IDs. No host-owned tool table, name alias or manager parser is added. +execution. Any plugin may define a short ID such as `compiler` or a qualified +`:`; the prefix of a qualified ID must be the defining +plugin's own ID, so qualified IDs of different plugins never collide. A short ID +defined by more than one enabled plugin belongs to none of them (see the registry +rules below). Providers may answer short IDs. No host-owned tool table, name +alias or manager parser is added. A native Build consumer that directly executes a tool currently accepts only `["${executable}", "${arguments}"]`: its tool context carries a program and @@ -103,22 +106,35 @@ are not a substitute for that adapter. These commands perform no native I/O. | Command | Input | Success key | | --- | --- | --- | -| `toolchains.registry` | `{plugins:[{manifest,verifiedOfficial}]}` | `registry` | +| `toolchains.registry` | `{plugins:[{manifest}]}` | `registry` | | `toolchains.decodeReply` | Query/response and native evidence described below | `snapshot` | | `toolchains.failureReply` | `{params,code}` described below | `reply` | | `toolchains.resolvePins` | `{document,directory,tool}` | `resolution` | | `toolchains.resolve` | Captured resolver input described below | `resolution` | | `toolchains.checkVersion` | `{registry,tool,output,minimumVersion?}` | `version` | +| `toolchains.instantiateLaunch` | `{registry,tool,executable,home?,entry?,runtimeArguments,arguments}` | `launch` | | `toolchains.discoveryPlan` | `{registry,platform}` | `plan` | | `toolchains.candidates` | Captured discovery input described below | `candidates` | -Every `registry` input is the raw `{plugins:[...]}` request, not a previously -normalized result. Core revalidates complete production manifests and the -host-supplied `verifiedOfficial` boolean; a plugin cannot assert that evidence -inside its own manifest. Output is `{definitions:[{pluginID,definition}], -providers:[{pluginID,provider}]}` in stable order. Duplicate plugin/tool ownership -rejects the batch without replacing earlier owners. Limits are 128 plugins, -512 aggregate definitions and 256 aggregate providers. +Every `registry` input is the raw `{plugins:[{manifest}]}` request of the enabled +plugins, not a previously normalized result. Core revalidates complete production +manifests; the host supplies no proof of a plugin's origin, and a plugin's own +claim of one is not read. Output is `{definitions:[{pluginID,definition}], +providers:[{pluginID,provider}],conflicts:[{tool,pluginIDs}]}` in stable order. + +A short ID defined by more than one enabled plugin belongs to none of them. Core +registers none of those definitions and registers every other definition as +usual. `conflicts` lists each contested tool once, sorted by `tool`, with the IDs +of all plugins that defined it, sorted. Ownership is never decided by arrival +order or by plugin ID order, because a hostile plugin could take a tool by being +loaded first or by choosing a smaller ID. A contested tool is absent from the +registry: `discoveryPlan` lists no scans or providers for it, and a consumer that +resolves it treats it as declared by nobody. A host should tell the user which +plugins contest the tool; the macOS settings page shows one diagnostic per +conflict and advises keeping only one of those plugins enabled. Qualified IDs +cannot be contested, since their prefix is the defining plugin's ID. A repeated +plugin ID still rejects the whole batch without replacing earlier owners. Limits +are 128 plugins, 512 aggregate definitions and 256 aggregate providers. ## Provider backend negotiation @@ -333,6 +349,34 @@ login-shell home variables (`{variable,path}` from its platform-applicable `environment` candidates). It neither scans, queries nor executes them; limits are 256 roots, 4096 entries and 64 ancestors for the host adapter. +## Launching a tool definition + +A plugin that starts a tool by definition ID (a `launch.kind: "tool"` plan, or a +`run/start` with `{kind: "tool"}`) never writes the tool's argv. The definition's +`launch` template says how the tool takes an entry file, runtime arguments and +program arguments, so no host and no plugin branches on a tool name. +`toolchains.instantiateLaunch` expands it for the executable the host selected: + +- `registry` and `tool` are as in `checkVersion`; `executable` is the selected + executable (required). `home` is the resolved tool home, `entry` an absolute + file path, `runtimeArguments` and `arguments` string lists. A text is at most + 4096 bytes, a list at most 1024 items of 64 KiB each, and none holds a NUL. +- `${executable}`, `${home}`, `${entry}`, `${runtimeArguments}` and + `${arguments}` are replaced by the matching value, a list expanding in place. + Any other template token is passed as written. +- The result is `{executable, arguments, versionArguments}`: the program, the + rest of the argv, and the definition's `version.arguments` for the probe. +- A value the caller supplies that the template cannot carry (an `entry` for a + tool that launches none, or arguments for one that takes none), or a `${home}` + or `${entry}` the caller left out, is + `plugin.toolchain.launchMismatch`. An unknown or disabled `tool` is + `plugin.toolchain.invalidParams` with `field: "tool"`. + +The command performs no native I/O. The host selects the executable with the +four resolution layers and the launch's `minimumVersion`, probes +`versionArguments`, checks the output with `checkVersion`, and then starts the +expanded argv. + ## Settings candidates `candidates` reports what a settings page shows for one tool's global and diff --git a/guides/build-tool-integration.md b/guides/build-tool-integration.md index 9ba0ad1..ee04698 100644 --- a/guides/build-tool-integration.md +++ b/guides/build-tool-integration.md @@ -23,7 +23,7 @@ Windows 不支持。各能力的版本和宿主对照见[构建系统](../refere | 补全、跳转、诊断 | API 1/2 `languageServers` | 按 LSP 提供能力,不自动声明编译和测试 | | 运行文件或固定项目命令 | `executionProviders[].run` | 命令与 argv 模板,工作目录为工作区根;不提供通用目标选择或构建模型 | | 运行整个工程、文件所在范围或指定测试 | `executionProviders[].test` | 测试项目标记、文件发现和命令规划;不是完整语义测试发现与结构化结果协议 | -| 官方 Java 项目的运行/测试/调试 | 官方 Java 插件的 API 2 `hostIntegration: jdt` | 宿主已有 JDT 集成,不能复制为社区通用扩展点 | +| Java 项目的运行/测试/调试 | 任何插件都可以声明 API 2 `languageServers[].hostIntegration: jdt` | 使用宿主已有的 JDT 集成;集成只读插件自己包里的资源,交给同一插件启动的语言服务器,不额外授予权限;宿主未实现的集成 ID 返回 `plugin.integration.unsupported`,见[契约](../contract/v1/README.md#host-integrations-api-2) | | 通用 Build 工具窗口、工程模型、任务、依赖与同步 | API 3 `buildSystems`;模型需要程序计算时再加 `buildProviders` | Core 和 macOS 已实现;官方 Gradle、Maven、Cargo 插件已按 API 3 发布;Windows 在安装前拒绝构建插件 | 当需求只是 `lua <当前文件>`、`go test ./...` 这类固定命令,可以使用现有执行提供者。 @@ -178,9 +178,17 @@ Core 的计划接口本身不启动进程、不监视文件、不持久化选择 不要把内部 dispatcher 命令写成社区插件可以调用的宿主脚本 API。 `languageServers[].hostIntegration` 的 JDT 集成(API 2)与构建系统 `model.source: "hostIntegration"` -是不同接口,都只限官方插件:语言服务器侧只有 `jdt`,构建侧目前只有 `maven`。 -它们都不是社区作者任意命名即可扩展的机制;不要复制官方 ID 或修改 `vendor` 来绕过限制。 -社区构建插件用 `static`、`command` 或 `provider` 模型。 +是不同接口,开放范围也不同: + +- 语言服务器侧对所有插件开放。任何插件都可以声明宿主已实现的集成 ID(目前语言服务器侧有 `jdt`); + 集成只读取声明插件自己包里的资源,交给同一插件启动的服务器或辅助进程,不额外授予权限。 + 宿主未实现的 ID 返回 `plugin.integration.unsupported`,见 + [契约](../contract/v1/README.md#host-integrations-api-2)。 +- 构建系统侧仍只限官方插件,目前只有 `maven`;官方命名空间以外的插件请求它同样得到 + `plugin.integration.unsupported`。不要复制官方 ID 或修改 `vendor` 来绕过这个限制。 + 社区构建插件用 `static`、`command` 或 `provider` 模型。 + +两侧都不是作者自己命名就能新增的机制:集成 ID 必须是宿主已经实现的。 ### 6. 验证完整工作流 @@ -231,7 +239,7 @@ Before publishing, still confirm that the target host version supports every cap | Completion, navigation, diagnostics | API 1/2 `languageServers` | Features come from LSP; compile and test are not declared automatically | | Run a file or a fixed project command | `executionProviders[].run` | A command and argv template; the working directory is the workspace root; no general target selection or build model | | Run the whole project, a file's scope, or a named test | `executionProviders[].test` | Test-project markers, file discovery, and command planning; not a full semantic test-discovery or structured-results protocol | -| Run, test, and debug official Java projects | The official Java plugin's API 2 `hostIntegration: jdt` | The host already has the JDT integration; it cannot be copied as a general community extension point | +| Run, test, and debug Java projects | Any plugin may declare API 2 `languageServers[].hostIntegration: jdt` | Uses the host's existing JDT integration; the integration reads only resources inside the plugin's own package and hands them to the language server that same plugin starts, and grants no extra permission; an integration ID the host does not implement returns `plugin.integration.unsupported`; see the [contract](../contract/v1/README.md#host-integrations-api-2) | | A general Build tool window, project model, tasks, dependencies, and sync | API 3 `buildSystems`, plus `buildProviders` when a program must compute the model | Implemented in Core and macOS; the official Gradle, Maven, and Cargo plugins are published at API 3; Windows rejects build plugins before install | When all you need is a fixed command such as `lua ` or `go test ./...`, an @@ -398,10 +406,19 @@ you still have to accept it in a real host. Do not present internal dispatcher commands as a host scripting API that community plugins can call. The JDT integration in `languageServers[].hostIntegration` (API 2) and the build-system -`model.source: "hostIntegration"` are different interfaces, and both are limited to official plugins: -the language-server side has only `jdt`, and the build side currently has only `maven`. -Neither is a mechanism community authors can extend by picking a name; do not copy an official ID -or change `vendor` to get around the limit. Community build plugins use the `static`, `command`, or `provider` models. +`model.source: "hostIntegration"` are different interfaces, and they are open to different plugins: + +- The language-server side is open to every plugin. Any plugin may declare an integration ID the host + already implements (the language-server side currently has `jdt`); the integration reads only + resources inside the declaring plugin's own package and hands them to the server or helper process + that same plugin starts, and grants no extra permission. An ID the host does not implement returns + `plugin.integration.unsupported`; see the [contract](../contract/v1/README.md#host-integrations-api-2). +- The build-system side is still limited to official plugins, and currently has only `maven`; a plugin + outside the official namespace that asks for it also gets `plugin.integration.unsupported`. + Do not copy an official ID or change `vendor` to get around this limit. + Community build plugins use the `static`, `command`, or `provider` models. + +Neither side lets an author add an integration by picking a name: the ID must be one the host already implements. ### 6. Verify the complete workflow diff --git a/guides/debug-adapter-plugin.md b/guides/debug-adapter-plugin.md index c5a65ef..f0544d5 100644 --- a/guides/debug-adapter-plugin.md +++ b/guides/debug-adapter-plugin.md @@ -66,7 +66,7 @@ 要点: -- `pluginAPIVersion` 是 `4`,`requires` 必须逐项列出实际用到的能力(`debugAdapters` 为 `1`)。 +- `pluginAPIVersion` 是 `4`,`requires` 必须逐项列出实际用到的能力(`debugAdapters` 为 `1`;声明 `features` 或 `defaults` 时为 `3`,由后端发起调试时还要列出 `backend` 2,见第 6 节)。 - 每个适配器都要有一条 `onDebug:` 激活事件,并声明 `debug.launch` 权限。适配器列出 `attach` 时才需要(也只能声明)`debug.attach`。 - `selectors` 决定 Lithe 在哪些目标上自动选这个适配器:`targetKinds` 是目标种类,`languageIDs` 是语言。不写 `selectors` 表示只能被用户显式选择。 - `languages[]` 声明适配器服务的语言。如果这个语言已经由别的插件提供(例如 Rust),你的声明只会给它增加“可以调试”的能力,并并入你声明的文件扩展名;语言名、语言标识和语言服务器不变,装上调试器也不会重启正在运行的语言服务器。 @@ -83,6 +83,8 @@ | `{"launch": {"kind": "systemCommand", "commands": ["lldb-dap"]}}` | 适配器已经装在用户机器上(例如随 Xcode 命令行工具提供);Lithe 用自己的工具发现(PATH、项目工具链、用户覆盖)解析命令名,macOS 上也覆盖 Xcode 工具链 | | `{"launch": {"kind": "runSelectedRuntime", "tool": "python"}}` | 适配器是 Python / Java / Node.js 程序,用用户选定的运行时启动;这是 `serverProcess` 2 唯一的用途 | +从 `serverProcess` 4 起,`runSelectedRuntime.tool` 不再限于上面的三个名字,而是清单顶层 `requiresTools` 列出的工具定义 ID,见契约的 [Tool references](../contract/v1/README.md#tool-references-serverprocess-4)。 + `launch.transport` 是 `stdio`,或者 TCP(`tcpAnnounced`:适配器在输出里打印端口;`tcpPort`:固定端口)。TCP 只允许回环地址。 插件包和 Lithe 的安装目录运行时是只读的,适配器不能往里面写日志、索引或状态,否则会破坏代码签名和增量更新。macOS 上宿主把适配器的状态目录放在 Application Support 的 `debug-adapters/<适配器 id>` 下。 @@ -125,18 +127,53 @@ Lithe 的调试控制台有输入行和补全弹窗。补全来自适配器的 D 两者至少声明一个。适配器能实时补全时,不要把整个命令集再抄一遍。 -### 6. 本地验证 +### 6. 适配器行为声明与由后端发起调试(`debugAdapters` 3) + +宿主从不按 ID 认识某个适配器:适配器特有的行为写在清单里,共享的调试引擎照着执行。需要 `requires.debugAdapters` 3。 + +- `features.hotCodeReplace`:`request` 是宿主发给适配器、让它把改动的代码应用到运行中的被调试程序的 DAP 命令,声明了就表示支持热代码替换; + `event` 是其 `message` 在调试控制台里显示为进度的 DAP 事件;`changedField`、`errorField` 是响应体里已替换单元的列表和适配器在成功响应里报告的失败信息。 +- `features.stepFilters.argument`:适配器接收步进过滤器所用的启动参数名;用户配置里已经设置的同名参数保持不变。 +- `defaults.stepFilters`:用户没有配置过滤器时的默认值(`classNameFilters`、`skipSynthetics`、`skipStaticInitializers`、`skipConstructors`、`hideFilteredStackFrames`)。 +- 名字写错或使用本版本未定义的字段是 `plugin.manifest.invalid`;两者都不声明时行为与 `debugAdapters` 1、2 相同。完整字段和限制以契约的 + [Adapter features and defaults](../contract/v1/README.md#adapter-features-and-defaults-debugadapters-3)为准。 + +插件有后端(`backend` 2)时,可以由后端决定启动什么调试会话,会话仍由宿主拥有(进程、控制台、停止按钮、会话结束)。方法名前缀是 `lithe/host/`: + +| 方法 | API | 权限 | 用途 | +| --- | --- | --- | --- | +| `debug/start` | API-30 | `debug.start` | 在本插件自己声明的适配器上启动 `launch` 或 `attach` 会话,返回 `sessionID`;需要已声明的 `debugAdapters` 条目 | +| `run/start` 的 `debug` 字段 | API-23 | `run.start` | 把一次宿主拥有的运行变成调试会话 | +| `run/stop` | API-23 | `run.start` | 按 `sessionID`(或 `runID`)停止本插件启动的调试会话或运行 | + +- `debug/start` 的参数是 `context, label, adapter, request, configuration`。`adapter` 只能是本插件的适配器(本地 ID 或 `<插件 ID>/<本地 ID>`), + 别的插件的适配器是 `plugin.host.notFound`;`request` 必须是该适配器声明过的;`configuration` 是不超过 256 KiB 的 JSON 对象, + 作为 DAP 的 launch 或 attach 参数叠在适配器声明的默认值之上,`noDebug` 和以 `__` 开头的键属于宿主,会被拒绝。适配器本身照常声明 `debug.launch`(`attach` 还要 `debug.attach`)。 +- `run/start` 的 `debug` 是 `{configurationType, target}`。`target` 为 `{kind: "connection", port}` 时,进程在 `port` 上等调试器,宿主等它出现后挂接适配器; + 为 `{kind: "launch"}` 时由适配器自己启动进程,程序、参数、目录和环境变量经 `targetBindings` 取自 `process`。 + `process.executable` 只能是清单 `requiresTools` 列出的工具定义 ID,或插件包内的文件。运行结束时宿主发 `run/didExit` 通知。 + +仅 macOS。官方 Node、Java、Rust SDK 还没有 `debug/start`、`run/start`、`run/stop` 的封装,可以先用 SDK 的通用宿主请求,以完整方法名调用, +例如 Node SDK 的 `backend.host.request(method, params?)`,见 [Host services](../sdk/node/README.md#host-services)。 + +细节见[调试适配器](../reference/domains/debug-adapters.md)和契约的 +[Run and debug sessions](../contract/v1/README.md#run-and-debug-sessions-protocol-version-3)。 +示例:[`debug-adapters/debug-adapter-features.plugin.json`](../contract/v1/fixtures/debug-adapters/debug-adapter-features.plugin.json)、 +[`debug-adapters/debug-start.json`](../contract/v1/fixtures/debug-adapters/debug-start.json)、 +[`run/run-start.json`](../contract/v1/fixtures/run/run-start.json)。 + +### 7. 本地验证 1. 对照契约示例 [`contract/v1/fixtures/debug-adapters/debug-adapter.plugin.json`](../contract/v1/fixtures/debug-adapters/debug-adapter.plugin.json),并用[编辑器 Schema](../README.md#在编辑器里校验-pluginjson) 检查字段。 2. [`contract/v1/fixtures/debug-adapters/debug-adapter-rejections.json`](../contract/v1/fixtures/debug-adapters/debug-adapter-rejections.json) 列出每种拒绝的稳定错误码、出错字段和报告它的层(`schema`、`coreManifest`、`corePlan`、`coreInstall`),写测试时可以直接对照。 3. 在支持 API 4 的 Lithe 里安装插件,打开一个目标文件,设置断点并启动调试。适配器被拒绝请求时,Lithe 会显示适配器自己的解释,所以适配器返回的错误文字要写得让用户能看懂。 4. 在设置页确认 `executable` 找到的路径被回显,再把它改成一个不存在的文件,确认得到的是错误而不是悄悄换文件。 -### 7. 安装后是否需要重启 +### 8. 安装后是否需要重启 只贡献调试适配器(以及它声明的语言和设置)的插件,在支持热激活的 Lithe 里安装、更新、回退、卸载、启用、停用后**不需要重启**。是否免重启由宿主按它实际能在运行期登记和撤回的贡献类型决定,不是 `plugin.json` 里的字段,所以插件作者不需要也不能声明。带有其他贡献(语言服务器、构建系统、界面等)的插件仍然在重启后生效。 -### 8. 发布 +### 9. 发布 调试适配器插件需要 API 4,只有支持 API 4 的 Lithe 才能安装。旧版 Lithe 在插件市场里会提示升级,而不是安装。 @@ -144,10 +181,10 @@ Lithe 的调试控制台有输入行和补全弹窗。补全来自适配器的 D - 稳定版支持插件需要的全部能力和协议后,把 Release 转为正式版,追踪器会自动提交后续版本的索引更新。 - 发布后不要替换或删除资产;索引固定了它的 SHA-256。 -### 9. 当前边界 +### 10. 当前边界 - 只有 macOS 宿主实现了调试适配器。 -- 宿主的调试入口对插件适配器只发起 `launch` 请求。`attach` 可以在清单里声明,目前只在构建提供者服务运行的连接调试(`buildProviders` 11)中由宿主选用;用户手动填写主机和端口的远程 attach 还没有宿主入口,不要依赖。 +- 宿主自己的调试入口对插件适配器只发起 `launch` 请求。`attach` 可以在清单里声明:宿主在构建提供者服务运行的连接调试(`buildProviders` 11)中选用它,插件后端也可以用 `debug/start` 或 `run/start` 的 `debug` 字段发起(见第 6 节);用户手动填写主机和端口的远程 attach 还没有宿主入口,不要依赖。 - 适配器声明是纯数据,没有自定义界面、没有宿主集成点;需要调试之外的能力(构建、测试、运行配置)请另用对应的贡献类型。 --- @@ -215,7 +252,7 @@ Plugins are not loaded into the Lithe process. The manifest is pure data: no res Key points: -- `pluginAPIVersion` is `4`, and `requires` must list exactly the capabilities you use (`debugAdapters` is `1`). +- `pluginAPIVersion` is `4`, and `requires` must list exactly the capabilities you use (`debugAdapters` is `1`, or `3` when you declare `features` or `defaults`; when the backend starts debug sessions, also list `backend` 2; see section 6). - Every adapter needs an `onDebug:` activation event and the `debug.launch` permission. `debug.attach` is needed (and allowed) only when the adapter lists `attach`. - `selectors` decide on which targets Lithe picks this adapter automatically: `targetKinds` are target kinds and `languageIDs` are languages. Without `selectors`, the adapter can only be chosen explicitly by the user. - `languages[]` declares the languages the adapter serves. If another plugin already provides the language (for example Rust), your declaration only adds the ability to debug it and merges in the file extensions you declare; the language name, language ID, and language server stay the same, and installing the debugger does not restart a running language server. @@ -232,6 +269,8 @@ Key points: | `{"launch": {"kind": "systemCommand", "commands": ["lldb-dap"]}}` | The adapter is already installed on the user's machine (for example with the Xcode command line tools); Lithe resolves the command name with its own tool discovery (PATH, project toolchains, user overrides), which on macOS also covers the Xcode toolchain | | `{"launch": {"kind": "runSelectedRuntime", "tool": "python"}}` | The adapter is a Python, Java, or Node.js program started with the runtime the user selected; this is the only use of `serverProcess` 2 | +From `serverProcess` 4, `runSelectedRuntime.tool` is no longer limited to the three names above; it is a tool-definition ID listed in the top-level `requiresTools`, see [Tool references](../contract/v1/README.md#tool-references-serverprocess-4) in the contract. + `launch.transport` is `stdio` or TCP (`tcpAnnounced`: the adapter prints its port in its output; `tcpPort`: a fixed port). TCP allows only loopback addresses. The plugin package and the Lithe install directory are read-only at run time. An adapter must not write logs, indexes, or state there, or it breaks code signing and delta updates. On macOS the host puts the adapter's state directory under `debug-adapters/` in Application Support. @@ -274,18 +313,53 @@ Lithe's debug console has an input line and a completion popup. Completions come Declare at least one of the two. When the adapter completes live, do not copy its whole command set as well. -### 6. Local verification +### 6. Adapter behavior declarations and backend-started debugging (`debugAdapters` 3) + +A host never recognizes an adapter by its ID: what is specific to an adapter is written in the manifest, and the shared debug engine follows it. This needs `requires.debugAdapters` 3. + +- `features.hotCodeReplace`: `request` is the DAP command the host sends to have the adapter apply changed code to the running debuggee, and declaring it means the adapter supports hot code replace; + `event` is a DAP event whose `message` the Debug Console shows as progress; `changedField` and `errorField` are the response-body fields for the list of replaced units and for a failure the adapter reports in a successful response. +- `features.stepFilters.argument`: the launch argument through which the adapter receives stepping filters; an argument the user's configuration already sets is kept. +- `defaults.stepFilters`: the filters used when the user configured none (`classNameFilters`, `skipSynthetics`, `skipStaticInitializers`, `skipConstructors`, `hideFilteredStackFrames`). +- A malformed name or a field this version does not define is `plugin.manifest.invalid`; declaring neither keeps the behavior of `debugAdapters` 1 and 2. For the full fields and limits, go by + [Adapter features and defaults](../contract/v1/README.md#adapter-features-and-defaults-debugadapters-3) in the contract. + +A plugin with a backend (`backend` 2) can decide which debug session to start, while the host still owns the session (the process, the console, the Stop button, and the end of the session). The method prefix is `lithe/host/`: + +| Method | API | Permission | Purpose | +| --- | --- | --- | --- | +| `debug/start` | API-30 | `debug.start` | Start a `launch` or `attach` session on an adapter the plugin declares itself and return a `sessionID`; needs a declared `debugAdapters` entry | +| `debug` field of `run/start` | API-23 | `run.start` | Turn a host-owned run into a debug session | +| `run/stop` | API-23 | `run.start` | Stop a debug session (by `sessionID`) or a run (by `runID`) the plugin started | + +- `debug/start` takes `context, label, adapter, request, configuration`. `adapter` must be one of the plugin's own adapters (its local ID or `/`); + an adapter of another plugin is `plugin.host.notFound`. `request` must be one the adapter declares. `configuration` is a JSON object of at most 256 KiB that is passed to the adapter as the DAP launch or attach arguments + on top of its declared defaults; `noDebug` and keys starting with `__` belong to the host and are refused. The adapter itself still declares `debug.launch` (plus `debug.attach` for `attach`). +- The `debug` field of `run/start` is `{configurationType, target}`. With `target` as `{kind: "connection", port}`, the process listens for a debugger on `port` and the host waits for it and attaches the adapter; + with `{kind: "launch"}`, the adapter starts the process itself and takes the program, arguments, directory, and environment from `process` through its `targetBindings`. + `process.executable` can only be a tool-definition ID listed in the manifest's `requiresTools`, or a file inside the plugin package. The host sends the `run/didExit` notification when the run ends. + +macOS only. The official Node, Java, and Rust SDKs do not wrap `debug/start`, `run/start`, or `run/stop` yet; until they do, call them with the SDK's generic host request using the full method name, +for example `backend.host.request(method, params?)` in the Node SDK, see [Host services](../sdk/node/README.md#host-services). + +For details, see [Debug adapters](../reference/domains/debug-adapters.md) and +[Run and debug sessions](../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) in the contract. +Examples: [`debug-adapters/debug-adapter-features.plugin.json`](../contract/v1/fixtures/debug-adapters/debug-adapter-features.plugin.json), +[`debug-adapters/debug-start.json`](../contract/v1/fixtures/debug-adapters/debug-start.json), and +[`run/run-start.json`](../contract/v1/fixtures/run/run-start.json). + +### 7. Local verification 1. Compare with the contract example [`contract/v1/fixtures/debug-adapters/debug-adapter.plugin.json`](../contract/v1/fixtures/debug-adapters/debug-adapter.plugin.json) and check fields with the [editor schema](../README.md#validate-pluginjson-in-your-editor). 2. [`contract/v1/fixtures/debug-adapters/debug-adapter-rejections.json`](../contract/v1/fixtures/debug-adapters/debug-adapter-rejections.json) lists, for each rejection, the stable error code, the offending field, and the layer that reports it (`schema`, `coreManifest`, `corePlan`, `coreInstall`); compare against it directly when writing tests. 3. Install the plugin in a Lithe that supports API 4, open a target file, set a breakpoint, and start debugging. When the adapter rejects a request, Lithe shows the adapter's own explanation, so write the adapter's error text for users to understand. 4. On the settings page, confirm that the path `executable` found is shown, then change it to a file that does not exist and confirm you get an error rather than a quietly swapped file. -### 7. Whether installing needs a restart +### 8. Whether installing needs a restart A plugin that contributes only debug adapters (and the languages and settings it declares) needs **no restart** after install, update, rollback, uninstall, enable, or disable in a Lithe that supports hot activation. The host decides this from the contribution types it can actually register and withdraw at run time; it is not a `plugin.json` field, so plugin authors neither need to nor can declare it. Plugins with other contributions (language servers, build systems, UI, and so on) still take effect after a restart. -### 8. Publishing +### 9. Publishing A debug adapter plugin needs API 4 and installs only on a Lithe that supports API 4. Older Lithe versions show an update prompt in the marketplace instead of installing. @@ -293,8 +367,8 @@ A debug adapter plugin needs API 4 and installs only on a Lithe that supports AP - Once a stable release supports every capability and protocol the plugin needs, convert the Release to a full release, and the tracker submits index updates for later versions automatically. - Do not replace or delete assets after publishing; the index pins their SHA-256. -### 9. Current limits +### 10. Current limits - Only the macOS host implements debug adapters. -- The host's debug entry issues only `launch` requests to plugin adapters. `attach` can be declared in the manifest; today the host chooses it only for connection debugging of build-provider service runs (`buildProviders` 11). Remote attach with a user-entered host and port has no host entry yet, so do not rely on it. +- The host's own debug entry issues only `launch` requests to plugin adapters. `attach` can be declared in the manifest: the host chooses it for connection debugging of build-provider service runs (`buildProviders` 11), and a plugin backend can start it with `debug/start` or the `debug` field of `run/start` (see section 6). Remote attach with a user-entered host and port has no host entry yet, so do not rely on it. - An adapter declaration is pure data, with no custom UI and no host integration points; for capabilities beyond debugging (build, test, run configurations), use the matching contribution types. diff --git a/guides/lsp-plugin-lifecycle.md b/guides/lsp-plugin-lifecycle.md index 0c76552..400e30d 100644 --- a/guides/lsp-plugin-lifecycle.md +++ b/guides/lsp-plugin-lifecycle.md @@ -19,7 +19,8 @@ Lua 模板目前只提供 macOS arm64、x64 两个包。各能力版本见[版 LSP(Language Server Protocol,语言服务器协议)让编辑器通过独立进程获得补全、诊断、 跳转等能力。Lithe 的声明式插件用 JSON 描述要启动的程序;宿主读取清单、发现运行时并管理 -进程及 LSP 会话。你不需要编写宿主内的 Swift/JavaScript 插件入口。 +进程及 LSP 会话。你不需要编写宿主内的 Swift/JavaScript 插件入口。命令行要到启动时才能确定的服务器, +可以改由插件后端在每次启动时解析,见第 3 节的“由插件后端驱动的语言服务器”。 ```text 开发与构建:固定上游版本 → 下载并校验 → 组装各平台插件包 → 发布 zip → 索引收录 @@ -153,6 +154,7 @@ API 1 的服务器参数是字面量,宿主原样传给进程、不展开模 | API 1 `packaged` | 随包提供原生服务器或启动器 | 声明相对路径,打包全部资源并保留可执行权限 | | API 1 `systemCommand` | 明确要求用户自行安装的服务器 | 声明裸命令候选和正确探测参数,记录用户安装要求 | | API 2 `runtime` | 包内 JS/JAR 由 Node.js/Java 启动 | 声明包内入口及发现的运行时最低版本,或声明随包运行时 | +| `serverProcess` 3 `backend` | 命令行取决于启动时才能确定的内容(解析版本、引导工具、按项目派生选项) | 声明解析命令行的后端函数;见下文“由插件后端驱动的语言服务器” | 例如,下面是 `languageServers[]` 中一个服务器声明的替代方案,要求用户预先安装 `gopls`, 完整清单还必须声明 `go` 语言及相应权限、事件: @@ -174,6 +176,53 @@ API 1 的服务器参数是字面量,宿主原样传给进程、不展开模 启动时下载服务器的变通方案。运行时依赖与主入口是两回事:例如随包服务器调用 Go 时, `runtimeDependencies` 让宿主发现 Go 并传递环境;API 1 支持 `node`、`go`,API 2 增加 `java`。 +#### 由插件后端驱动的语言服务器(`serverProcess` 3 与 `languageServers` 3) + +上面几种方式都由清单直接写出命令行。命令行要到启动时才能确定(解析某个版本、引导一个工具、按项目派生选项), +或者插件需要在服务器运行期间向它发请求、接收它的通知时,可以让插件后端参与。进程、LSP 会话和停止仍归宿主; +后端只回答命令行,并通过宿主方法与**自己的**服务器交谈。 + +- **后端决定启动方式。** `launch` 写 `{"kind": "backend", "function": "launch.resolve", "timeoutMilliseconds": 30000}`。 + 每次启动时,宿主调用这个插件函数,后端返回可执行文件(包内文件,或插件自己定义的工具)、参数、环境变量等,宿主按答案启动。 + 这类服务器不再声明 `arguments`、`validationArguments`、`runtimeDependencies`、`initializationOptions` 和 `hostIntegration`。 + 它要求 `pluginAPIVersion` 4、`backend` 2、`serverProcess` 3(同时列出 `requiresTools` 时为 4),并声明 `backend.launch` 权限。 +- **会话字段(`languageServers` 3)。** 后端只能做服务器条目声明过的事: + `requestMethods`(允许用 `languageServer/request`、`languageServer/notify` 发的 LSP 方法)、 + `commands`(允许执行的 `workspace/executeCommand` 命令,与 `requestMethods` 是否列出该方法一一对应)、 + `serializedCommands`(`commands` 的子集:同一命令同时只跑一个,后来的请求并入并得到同一个结果)、 + `forwardNotifications`(宿主转给后端的服务器通知)、 + `state`(按工作区的状态目录及 `retention` 的 `maxEntries`、`maxAgeDays`,宿主据此在后台回收旧目录)、 + `remote`(`supported` 或 `unsupported`,后者不在远程工作区启动)。 + 这些声明由 Core 的会话策略强制执行:宿主不能放宽,后端也越不过去。 +- **权限 `languageServer.request`。** 只有服务器列出 `requestMethods` 或 `forwardNotifications` 时才能声明,反过来也一样;需要 `backend` 2。 + 它只授予本插件自己服务器的 `languageServer/*` 宿主方法和下面两个通知。 + +宿主方法和通知(前缀 `lithe/host/`;方法都要带 `context` 和 `serverID`,`serverID` 是本插件某个服务器的 `languageID`, +别的插件的服务器返回 `plugin.host.foreignContribution`): + +| 方法或通知 | API | 权限 | 用途 | +| --- | --- | --- | --- | +| `languageServer/request` | API-25 | `languageServer.request` | 向自己的服务器发 JSON-RPC 请求,返回服务器的 `result`;超时默认 60 秒、最长 300 秒 | +| `languageServer/notify` | API-25 | `languageServer.request` | 向自己的服务器发通知 | +| `languageServer/setStatus` | API-22、API-25 | `languageServer.request` | 在宿主状态界面显示插件自己的准备状态(`preparing`、`ready`、`degraded`、`failed`,可带消息和百分比),用于导入工程、建索引 | +| `languageServer/didChangeState`(通知) | API-25 | `languageServer.request` | 服务器状态变化(`starting`、`running`、`stopped`、`crashed`),带 `generation` | +| `languageServer/didReceiveNotification`(通知) | API-25 | `languageServer.request` | 转发服务器发出、`forwardNotifications` 列出的通知;过大时只送 `truncated: true` | +| `documents/registerContentProvider` | API-12 | `documents.contentProvider` | 把一个 URI scheme 绑定到后端函数,见下 | + +服务器交出的文档没有对应文件(例如反编译的库类)时,用 `documents/registerContentProvider` 提供:用户打开该 scheme 的文档时, +宿主调用绑定的函数取只读内容(最多 1 MiB,宿主等 30 秒)。每个工作区窗口每个 scheme 只能有一个 provider,用 `subscriptions/dispose` 撤销。 + +边界:仅 macOS。后端决定启动方式的服务器不会在远程工作区启动(远程后端启动未支持);Windows 宿主不提供 `backend`,也就没有这种启动方式。 +官方 Node、Java、Rust SDK 还没有这些方法的封装,可以先用 SDK 的通用宿主请求,以完整方法名调用, +例如 Node SDK 的 `backend.host.request(method, params?)`,见 [Host services](../sdk/node/README.md#host-services)。 + +细节见[语言、语言服务器与运行/测试](../reference/domains/languages.md)、[编辑器、文档与预览](../reference/domains/editor.md), +以及契约的 [Backend-resolved launch](../contract/v1/README.md#backend-resolved-launch-serverprocess-3)、 +[Plugin-owned sessions](../contract/v1/README.md#plugin-owned-sessions-languageservers-3)、 +[Language server host methods](../contract/v1/README.md#language-server-host-methods)、 +[Content providers](../contract/v1/README.md#content-providers)。 +示例包:[`languages/language-server-backend-launch.plugin.json`](../contract/v1/fixtures/languages/language-server-backend-launch.plugin.json)。 + ### 4. 在构建阶段准备完整语言服务器 沿用模板的处理顺序: @@ -427,7 +476,8 @@ The Lua template ships only macOS arm64 and x64 packages. For capability version LSP (Language Server Protocol) lets an editor get completion, diagnostics, navigation, and more from a separate process. A Lithe declarative plugin describes the program to start in JSON; the host reads the manifest, discovers runtimes, and manages -the process and the LSP session. You do not write a Swift or JavaScript entry point inside the host. +the process and the LSP session. You do not write a Swift or JavaScript entry point inside the host. A server whose command line is only known at launch +can have the plugin backend resolve it at each start; see "Language servers driven by the plugin backend" in section 3. ```text Develop and build: pin upstream version → download and verify → assemble per-platform packages → publish zips → index entry @@ -561,6 +611,7 @@ A file extension is not the LSP document identifier; set `languageIdentifier` se | API 1 `packaged` | A native server or launcher shipped in the package | Declare the relative path, package every resource, and keep the executable bit | | API 1 `systemCommand` | A server the user is explicitly expected to install | Declare bare command candidates and a correct probe, and document the install requirement | | API 2 `runtime` | A packaged JS file or JAR started by Node.js or Java | Declare the packaged entry and the discovered runtime's minimum version, or declare a packaged runtime | +| `serverProcess` 3 `backend` | A command line that is only known at launch (resolving a version, bootstrapping a tool, deriving options from the project) | Declare the backend function that resolves the command line; see "Language servers driven by the plugin backend" below | For example, this is an alternative server declaration in `languageServers[]` that expects the user to have installed `gopls`; the full manifest must also declare the `go` language and the matching permission and event: @@ -582,6 +633,54 @@ When shipping a server, prefer a pinned, reproducible one. `systemCommand` does downloading a server at launch. Runtime dependencies are separate from the main entry: for example, when a packaged server calls Go, `runtimeDependencies` makes the host discover Go and pass the environment. API 1 supports `node` and `go`; API 2 adds `java`. +#### Language servers driven by the plugin backend (`serverProcess` 3 and `languageServers` 3) + +The kinds above all write the command line into the manifest. When the command line is only known at launch (resolving a version, bootstrapping a tool, +deriving options from the project), or when the plugin needs to send requests to the running server and receive its notifications, the plugin backend can take part. +The process, the LSP session, and stopping still belong to the host; the backend only answers the command line and talks to **its own** server +through host methods. + +- **The backend decides how to launch.** Write `launch` as `{"kind": "backend", "function": "launch.resolve", "timeoutMilliseconds": 30000}`. + At each start the host calls that plugin function, the backend returns the executable (a file in the package, or a tool the plugin defines), the arguments, the environment, and so on, + and the host starts the server from the answer. Such a server no longer declares `arguments`, `validationArguments`, `runtimeDependencies`, `initializationOptions`, or `hostIntegration`. + It requires `pluginAPIVersion` 4, `backend` 2, and `serverProcess` 3 (4 when the manifest also lists `requiresTools`), and declares the `backend.launch` permission. +- **Session fields (`languageServers` 3).** The backend may do only what the server entry declares: + `requestMethods` (the LSP methods `languageServer/request` and `languageServer/notify` may send), + `commands` (the `workspace/executeCommand` commands it may run; declared exactly when `requestMethods` lists that method), + `serializedCommands` (a subset of `commands`: one runs at a time, and a later request joins it and gets the same result), + `forwardNotifications` (server notifications the host forwards to the backend), + `state` (per-workspace state directories with a `retention` of `maxEntries` and `maxAgeDays`, which the host uses to prune old directories in the background), + and `remote` (`supported` or `unsupported`; the latter is not started for a remote workspace). + Core's session policy enforces these declarations: the host cannot widen them and the backend cannot step outside them. +- **The `languageServer.request` permission.** It can be declared only when a server lists `requestMethods` or `forwardNotifications`, and the other way round; it needs `backend` 2. + It grants only the `languageServer/*` host methods and the two notifications below, for the plugin's own servers. + +Host methods and notifications (prefix `lithe/host/`; every method takes `context` and `serverID`, the `languageID` of one of the plugin's servers; +a server of another plugin returns `plugin.host.foreignContribution`): + +| Method or notification | API | Permission | Purpose | +| --- | --- | --- | --- | +| `languageServer/request` | API-25 | `languageServer.request` | Send a JSON-RPC request to your own server and get the server's `result`; the timeout defaults to 60 s and is at most 300 s | +| `languageServer/notify` | API-25 | `languageServer.request` | Send a notification to your own server | +| `languageServer/setStatus` | API-22, API-25 | `languageServer.request` | Show the plugin's own preparation state in the host's status UI (`preparing`, `ready`, `degraded`, `failed`, with an optional message and percentage), for importing a project or indexing | +| `languageServer/didChangeState` (notification) | API-25 | `languageServer.request` | The server's state changed (`starting`, `running`, `stopped`, `crashed`), with a `generation` | +| `languageServer/didReceiveNotification` (notification) | API-25 | `languageServer.request` | A notification the server sent and `forwardNotifications` lists; only `truncated: true` is delivered when it is too large | +| `documents/registerContentProvider` | API-12 | `documents.contentProvider` | Bind a URI scheme to a backend function; see below | + +When the server hands out documents that have no file (for example the decompiled source of a library class), serve them with `documents/registerContentProvider`: when the user opens a document of that scheme, +the host calls the bound function for its read-only content (at most 1 MiB; the host waits 30 seconds). Each workspace window can have one provider per scheme; revoke it with `subscriptions/dispose`. + +Limits: macOS only. A server whose launch the backend resolves does not start in a remote workspace (remote backend launch is not supported); the Windows host does not offer `backend`, so it has no such launch kind. +The official Node, Java, and Rust SDKs do not wrap these methods yet; until they do, call them with the SDK's generic host request using the full method name, +for example `backend.host.request(method, params?)` in the Node SDK, see [Host services](../sdk/node/README.md#host-services). + +For details, see [Languages, language servers, and run and test](../reference/domains/languages.md) and [Editor, documents, and previews](../reference/domains/editor.md), +and these contract sections: [Backend-resolved launch](../contract/v1/README.md#backend-resolved-launch-serverprocess-3), +[Plugin-owned sessions](../contract/v1/README.md#plugin-owned-sessions-languageservers-3), +[Language server host methods](../contract/v1/README.md#language-server-host-methods), and +[Content providers](../contract/v1/README.md#content-providers). +Example package: [`languages/language-server-backend-launch.plugin.json`](../contract/v1/fixtures/languages/language-server-backend-launch.plugin.json). + ### 4. Prepare the complete language server at build time Follow the template's order: diff --git a/guides/plugin-backend-workspace.md b/guides/plugin-backend-workspace.md index fa93324..272213e 100644 --- a/guides/plugin-backend-workspace.md +++ b/guides/plugin-backend-workspace.md @@ -13,7 +13,8 @@ > 只提供 `backend: [1]` 的旧宿主会以 `plugin.api.unsupported` 拒绝 `backend` 2 的插件,所以老版本 Lithe > 不会启动一个期待工作区上下文的进程。三个官方 SDK 都实现了协议 3:Node.js SDK 0.2.0、Java SDK 0.2.0 和 Rust SDK 0.1.0 起 > (见各自 README 的 Host services:[Node.js](../sdk/node/README.md#host-services)、[Java](../sdk/java/README.md#host-services)、 -> [Rust](../sdk/rust/README.md#host-services))。用其他语言或更早的 SDK 需要自己处理下文的请求和通知。 +> [Rust](../sdk/rust/README.md#host-services))。宿主还支持函数协议 4(跨插件调用,见第 10 节),三个官方 SDK 目前只实现到协议 3。 +> 用其他语言或更早的 SDK 需要自己处理下文的请求和通知。 > 各宿主支持到哪一版,见按最新 preview 核对的[版本对照](../reference/capabilities-and-versions.md)。 ### 1. 三个版本号各管什么 @@ -21,7 +22,7 @@ | 写在哪里 | 值 | 决定什么 | | --- | --- | --- | | `requires.backend` | `1` 或 `2` | 后端进程**什么时候启动**、能不能声明 `workspace.metadata` 和 `onWorkspaceOpen` | -| `initialize` 应答的 `litheFunctions.version` | `1`、`2` 或 `3` | 进程启动后**线路协议**的规则:`2` 起有宿主服务(凭据),`3` 起有能力发现、工作区上下文、宿主通知和带类型的失败码 | +| `initialize` 应答的 `litheFunctions.version` | `1`、`2`、`3` 或 `4` | 进程启动后**线路协议**的规则:`2` 起有宿主服务(凭据),`3` 起有能力发现、工作区上下文、宿主通知和带类型的失败码,`4` 起有跨插件调用(第 10 节) | | `getCapabilities.services` | 运行时查询结果 | 本次会话**实际能调用**哪些宿主方法、会收到哪些通知 | 清单只做声明,宿主读清单决定装不装、什么时候启动;进程起来以后能做什么,靠协商和 `getCapabilities` 判断。 @@ -65,7 +66,7 @@ ### 4. 协商协议 3 -宿主在 `initialize` 里提供 `capabilities.experimental.litheFunctions: { "versions": [1, 2, 3] }`。后端从中选自己实现的最高版本作答: +宿主在 `initialize` 里提供 `capabilities.experimental.litheFunctions: { "versions": [1, 2, 3, 4] }`。后端从中选自己实现的最高版本作答(本篇讲协议 3,协议 4 见第 10 节): ```json "experimental": { "litheFunctions": { "version": 3, "functions": [{ "id": "orders.refresh" }] } } @@ -203,7 +204,13 @@ - 后台没有挂载编辑器的文档可以列出和读取,但编辑与选区可能返回 `busy`。 - 消息有频率和排队上限(每插件 10 秒 5 条、最多 3 条待答),用户回答前请求一直挂起。 -协议 3 还包括插件自有的编辑器动作(`editor.actions`)和原生终端(`terminal.create`),分别见[编辑器、文档与预览](../reference/domains/editor.md)和[从插件打开 Lithe 原生终端](native-terminals.md)。 +协议 3 还包括插件自有的编辑器动作(`editor.actions`)、编辑器语言功能 Provider(`editor.providers`)和原生终端(`terminal.create`),分别见[编辑器、文档与预览](../reference/domains/editor.md)和[从插件打开 Lithe 原生终端](native-terminals.md)。 + +协议 3 里还有三组方法,同样只有 macOS 宿主实现,调用前同样先查 `getCapabilities.services`。它们的权限都要求 `backend` 2,各自的清单条件见对应的契约章节: + +- 运行与调试会话(权限 `run.start`、`debug.start`):`run/start`、`run/stop` 启动并停止由宿主拥有的 Run 会话(Run 窗口条目、控制台、停止按钮、退出状态),`debug/start` 在本插件自己声明的调试适配器上启动调试会话,`run/didExit` 通知运行结束;见[插件后端与插件函数](../reference/domains/backend.md)、[调试适配器](../reference/domains/debug-adapters.md)和契约的[运行与调试会话](../contract/v1/README.md#run-and-debug-sessions-protocol-version-3)。 +- 自有语言服务器(权限 `languageServer.request`):`languageServer/request`、`languageServer/notify` 向本插件自己声明的语言服务器发送请求和通知,`languageServer/setStatus` 在宿主状态界面显示它的准备状态,服务器状态变化和转发的通知经 `languageServer/didChangeState`、`languageServer/didReceiveNotification` 送达;见[语言、语言服务器与运行/测试](../reference/domains/languages.md)和契约的[语言服务器宿主方法](../contract/v1/README.md#language-server-host-methods)。 +- 内容 Provider(权限 `documents.contentProvider`):`documents/registerContentProvider` 把一个 URI scheme 绑定到后端函数,用户打开该 scheme 的文档时宿主调用这个函数取只读内容;见[编辑器、文档与预览](../reference/domains/editor.md)和契约的[内容 Provider](../contract/v1/README.md#content-providers)。 ### 9. 宿主失败与错误码 @@ -234,7 +241,7 @@ 从协议 3 起,宿主再开放新能力(新服务及其方法和通知、可选参数、结果字段、错误码)都在协议 3 内新增, 不再升协议版本;只有会让已有后端读错的改动(删除或改名、改字段含义或类型、把可选参数改成必填、改错误码号码) -才会出协议 4。这样插件只要实现一次协议 3,就能在今后所有协议 3 宿主上工作,前提是遵守下面四条: +才会出新的协议版本,与旧版本并行提供。这样插件只要实现一次协议 3,就能在今后所有协议 3 宿主上工作,前提是遵守下面四条: 1. **只调用 `getCapabilities.services` 列出的方法。** 其他方法会得到 `-32601` `plugin.host.unavailable`。 2. **忽略不认识的字段和通知。** 结果、通知、`details`、`limits` 里多出来的字段,以及不认识的 `lithe/host/*` 通知,都直接忽略。 @@ -246,6 +253,24 @@ 新服务总是配一个新的清单权限。旧宿主不认识这个权限时,会在安装时以 `plugin.permission.unknown` 拒绝整个插件包, 权限不会被悄悄授予或忽略。 +#### 协议 4:跨插件调用 + +协议 4 只为[跨插件调用](../contract/v1/README.md#cross-plugin-calls-protocol-version-4)增加两样东西,协议 3 的内容全部保留,上面四条规则照旧: + +- 调用方用的两个中转方法:先用 `functions/providers` 找到导出某个函数的插件,再用 `functions/invoke` 调用其中一个。 + 调用由 Core 中转,调用方接触不到被调方的进程。只有协商到协议 4、且清单至少声明了一个调用(`pluginFunctions.calls`)的后端能发这两个请求: + 协商到 3 或更早得到 `-32601` `plugin.host.unavailable`,没有声明调用得到 `-32010` `plugin.host.permissionDenied`。 +- 被中转调用里的 `caller`:被调方收到的 `lithe/function/invoke` 顶层多一个 `caller`,是 Core 按调用方会话写入的插件 ID; + 没有这个字段就表示宿主发起的调用。要按调用方判断的被调方只读这个顶层字段,不读 `params` 里自称的插件 ID。 + 被调方在处理中转调用时再发起嵌套调用,要把这次调用顶层的 `operationID` 填进 `parentOperationID`,Core 才知道嵌套调用属于哪条调用链。 + +使用时要记住: + +- 只协商到 3 的后端照常工作。它不能调用 `functions/*`;它导出的函数仍可被中转调用,但收不到 `caller`,要按调用方判断就必须协商 4。 +- 导出或调用跨插件函数,清单要声明 `pluginFunctions`:`pluginAPIVersion` 不低于 3,`requires` 里写 `pluginFunctions: 1`、`backend: 2`、`serverProcess: 1`。 + 它不授予任何清单权限;清单声明了导出,后端仍要用 `lithe/function/register` 注册该函数。 +- 目前只有 macOS 宿主实现;三个官方 SDK 还只协商到协议 3,没有 `functions/*` 的封装,也读不到 `caller`。 + ### 11. 验证 - 用契约里的 [`fixtures/backend/function-protocol.json`](../contract/v1/fixtures/backend/function-protocol.json) 核对你的实现: @@ -268,7 +293,8 @@ The [contract](../contract/v1/README.md#backend-2-workspace-contexts) is normati > An older host that offers only `backend: [1]` refuses a `backend` 2 plugin with `plugin.api.unsupported`, so an older Lithe > never starts a process that expects workspace contexts. All three official SDKs implement protocol 3 from Node.js SDK 0.2.0, Java SDK 0.2.0, and Rust SDK 0.1.0 > (see Host services in each README: [Node.js](../sdk/node/README.md#host-services), [Java](../sdk/java/README.md#host-services), -> [Rust](../sdk/rust/README.md#host-services)). With another language or an earlier SDK, handle the requests and notifications below yourself. +> [Rust](../sdk/rust/README.md#host-services)). The host also supports function protocol 4 (cross-plugin calls, see section 10); the three official SDKs implement only up to protocol 3 so far. +> With another language or an earlier SDK, handle the requests and notifications below yourself. > For how far each host supports each version, see the [version tables](../reference/capabilities-and-versions.md), which track the latest preview. ### 1. What each of the three version numbers controls @@ -276,7 +302,7 @@ The [contract](../contract/v1/README.md#backend-2-workspace-contexts) is normati | Where | Value | Controls | | --- | --- | --- | | `requires.backend` | `1` or `2` | **When** the backend process starts, and whether it may declare `workspace.metadata` and `onWorkspaceOpen` | -| `litheFunctions.version` in the `initialize` answer | `1`, `2`, or `3` | The **wire protocol** rules once the process runs: `2` adds host services (credentials); `3` adds capability discovery, workspace contexts, host notifications, and typed failure codes | +| `litheFunctions.version` in the `initialize` answer | `1`, `2`, `3`, or `4` | The **wire protocol** rules once the process runs: `2` adds host services (credentials); `3` adds capability discovery, workspace contexts, host notifications, and typed failure codes; `4` adds cross-plugin calls (section 10) | | `getCapabilities.services` | Runtime query result | Which host methods this session **can actually call**, and which notifications it receives | The manifest only declares. The host reads it to decide whether to install and when to start; what the running process can do is decided by negotiation and `getCapabilities`. @@ -320,7 +346,7 @@ The manifest only declares. The host reads it to decide whether to install and w ### 4. Negotiating protocol 3 -The host offers `capabilities.experimental.litheFunctions: { "versions": [1, 2, 3] }` in `initialize`. The backend answers with the highest version it implements: +The host offers `capabilities.experimental.litheFunctions: { "versions": [1, 2, 3, 4] }` in `initialize`. The backend answers with the highest version it implements (this guide covers protocol 3; for protocol 4 see section 10): ```json "experimental": { "litheFunctions": { "version": 3, "functions": [{ "id": "orders.refresh" }] } } @@ -458,7 +484,13 @@ The [contract](../contract/v1/README.md#public-document-file-and-window-methods- - Documents open in the background without a mounted editor can be listed and read, but edits and selections may return `busy`. - Messages have rate and queue limits (5 per 10 seconds per plugin, at most 3 awaiting an answer), and the request stays pending until the user answers. -Protocol 3 also includes the plugin's own editor actions (`editor.actions`) and native terminals (`terminal.create`); see [Editor, documents, and previews](../reference/domains/editor.md) and [Open a native Lithe terminal from a plugin](native-terminals.md). +Protocol 3 also includes the plugin's own editor actions (`editor.actions`), editor language-feature providers (`editor.providers`), and native terminals (`terminal.create`); see [Editor, documents, and previews](../reference/domains/editor.md) and [Open a native Lithe terminal from a plugin](native-terminals.md). + +Protocol 3 has three more groups of methods. Only the macOS host implements them, and you should likewise check `getCapabilities.services` before calling. Every one of their permissions requires `backend` 2; the manifest conditions of each are in its contract section: + +- Run and debug sessions (permissions `run.start`, `debug.start`): `run/start` and `run/stop` start and stop a host-owned Run session (Run window entry, console, Stop button, exit status), `debug/start` starts a debug session on a debug adapter the plugin itself declares, and the `run/didExit` notification reports that a run ended; see [Plugin backend and plugin functions](../reference/domains/backend.md), [Debug adapters](../reference/domains/debug-adapters.md), and the contract's [Run and debug sessions](../contract/v1/README.md#run-and-debug-sessions-protocol-version-3). +- Own language servers (permission `languageServer.request`): `languageServer/request` and `languageServer/notify` send requests and notifications to a language server the plugin itself declares, `languageServer/setStatus` shows its preparation state in the host's status UI, and server state changes and forwarded notifications arrive through `languageServer/didChangeState` and `languageServer/didReceiveNotification`; see [Languages, language servers, run and test](../reference/domains/languages.md) and the contract's [Language server host methods](../contract/v1/README.md#language-server-host-methods). +- Content providers (permission `documents.contentProvider`): `documents/registerContentProvider` binds a URI scheme to a backend function, and the host calls that function for read-only content when the user opens a document of that scheme; see [Editor, documents, and previews](../reference/domains/editor.md) and the contract's [Content providers](../contract/v1/README.md#content-providers). ### 9. Host failures and error codes @@ -489,7 +521,7 @@ Protocol 3 host failures are typed: each JSON-RPC number maps one-to-one to an ` From protocol 3 on, new host capabilities (new services with their methods and notifications, optional params, result fields, error codes) are added within protocol 3 without raising the protocol version; only changes that would make an existing backend misread something (removing or renaming, changing a field's meaning or type, making an optional param required, changing an error number) -lead to protocol 4. So a plugin that implements protocol 3 once works on every future protocol 3 host, provided it follows these four rules: +lead to a new protocol version, offered next to the old one. So a plugin that implements protocol 3 once works on every future protocol 3 host, provided it follows these four rules: 1. **Call only methods listed in `getCapabilities.services`.** Any other method gets `-32601` `plugin.host.unavailable`. 2. **Ignore unknown fields and notifications.** Ignore extra fields in results, notifications, `details`, and `limits`, and any unknown `lithe/host/*` notification. @@ -501,6 +533,24 @@ lead to protocol 4. So a plugin that implements protocol 3 once works on every f A new service always comes with a new manifest permission. An older host that does not know the permission refuses the whole plugin package at installation with `plugin.permission.unknown`, so the permission is never silently granted or ignored. +#### Protocol 4: cross-plugin calls + +Protocol 4 adds only two things, both for [cross-plugin calls](../contract/v1/README.md#cross-plugin-calls-protocol-version-4); it keeps everything in protocol 3, and the four rules above still apply: + +- Two relay methods for the caller: it finds the plugins that export a function with `functions/providers`, then calls one of them with `functions/invoke`. + Core relays the call, and the caller never reaches the provider's process. Only a backend that negotiated protocol 4 and whose manifest declares at least one call (`pluginFunctions.calls`) may send these requests: + one on version 3 or earlier gets `-32601` `plugin.host.unavailable`, and one that declares no call gets `-32010` `plugin.host.permissionDenied`. +- `caller` in a relayed call: the `lithe/function/invoke` a provider receives has one more top-level field, `caller`, the calling plugin's ID that Core writes from the calling session; + its absence means the host started the call. A provider that decides by who is asking reads only this top-level field, never a plugin ID that `params` claims. + When a provider makes a nested call while serving a relayed call, it puts that call's top-level `operationID` in `parentOperationID`, so Core knows which call chain the nested call belongs to. + +What to remember: + +- A backend that negotiated only 3 keeps working as before. It cannot call `functions/*`; the functions it exports can still be called through the relay, but it never receives `caller`, so a provider that decides by who is asking must negotiate 4. +- To export or call cross-plugin functions, the manifest declares `pluginFunctions`: `pluginAPIVersion` 3 or later, with `pluginFunctions: 1`, `backend: 2`, and `serverProcess: 1` in `requires`. + It grants no manifest permission, and declaring an export does not register it: the backend still registers the function with `lithe/function/register`. +- Only the macOS host implements it so far; the three official SDKs still negotiate only up to protocol 3, have no wrapper for `functions/*`, and do not expose `caller`. + ### 11. Verification - Check your implementation against the contract's [`fixtures/backend/function-protocol.json`](../contract/v1/fixtures/backend/function-protocol.json): diff --git a/reference/README.md b/reference/README.md index 1b9ecd4..58a1223 100644 --- a/reference/README.md +++ b/reference/README.md @@ -21,9 +21,9 @@ | 功能页 | 内容 | API 编号 | 能力(`requires`) | | --- | --- | --- | --- | | [清单与打包](domains/manifest.md) | `plugin.json` 字段、插件 API 级别、权限与激活事件、包目录 | 不在编号内 | — | -| [语言、语言服务器与运行/测试](domains/languages.md) | 语言、语法、语言服务器、进程启动、运行和测试命令 | API-25 | `languages`、`languageServers`、`serverProcess`、`executionProviders` | -| [插件后端与插件函数](domains/backend.md) | 后端进程、函数协议、宿主方法、工作区上下文、文件、凭据、终端 | API-01、04–08、20–22、24、37、40、41 | `backend`、`serverProcess` 1 | -| [编辑器、文档与预览](domains/editor.md) | 文档和选区、版本化编辑、打开与定位、HTML 预览、编辑器动作 | API-09、10、15、16、27 | `backend` 2 | +| [语言、语言服务器与运行/测试](domains/languages.md) | 语言、语法、语言服务器、进程启动、运行和测试命令、控制台输出链接 | API-25 | `languages`、`languageServers`、`serverProcess`、`executionProviders`、`outputLinkMatchers` | +| [插件后端与插件函数](domains/backend.md) | 后端进程、函数协议(1–4)、宿主方法、工作区上下文、文件、凭据、终端、跨插件调用 | API-01、04–08、20–22、24、35、37、40、41 | `backend`、`serverProcess` 1、`pluginFunctions` | +| [编辑器、文档与预览](domains/editor.md) | 文档和选区、版本化编辑、打开与定位、HTML 预览、编辑器动作、编辑器语言功能 Provider、虚拟文档内容提供者 | API-09、10、12、15、16、25、27 | `backend` 2 | | [界面](domains/ui.md) | 工具窗口、命令、包内网页、原生控件、后端命令、消息框 | API-02、03、13、14、17 | `ui` | | [插件设置](domains/settings.md) | 声明式设置、放置位置、作用域、生效方式、设置页按钮 | API-19 | `settings` | | [声明式构建系统](domains/build-systems.md) | 构建检测、模型、命令规划、项目准备角色 | API-29 | `buildSystems` | @@ -36,7 +36,7 @@ | [包信任](domains/package-trust.md) | 签名要求、签名文件格式、随应用发布的插件 | 不在编号内 | `packageTrust` | | [插件市场](domains/marketplace.md) | 索引来源和格式、可用性判断、安装审查和确认 | 不在编号内 | — | -计划中、还没有方法的 API(API-11、12、18、26、28、32、34–36、38、39、42–44)只在[总表](api-catalog.md#计划中的-api)列出,没有功能页。 +计划中、还没有方法的 API(API-11、18、26、28、32、34、36、38、39、42–44)只在[总表](api-catalog.md#计划中的-api)列出,没有功能页。 ### 每个功能页的结构 @@ -68,9 +68,9 @@ This directory organizes the plugin contract by feature: what each numbered API | Page | Covers | API numbers | Capabilities (`requires`) | | --- | --- | --- | --- | | [Manifest and packaging](domains/manifest.md) | `plugin.json` fields, plugin API levels, permissions and activation events, package layout | Not numbered | — | -| [Languages, language servers, run and test](domains/languages.md) | Languages, grammars, language servers, process launch, run and test commands | API-25 | `languages`, `languageServers`, `serverProcess`, `executionProviders` | -| [Plugin backend and plugin functions](domains/backend.md) | Backend process, function protocol, host methods, workspace contexts, files, credentials, terminals | API-01, 04–08, 20–22, 24, 37, 40, 41 | `backend`, `serverProcess` 1 | -| [Editor, documents, and previews](domains/editor.md) | Documents and selections, versioned edits, open and reveal, HTML preview, editor actions | API-09, 10, 15, 16, 27 | `backend` 2 | +| [Languages, language servers, run and test](domains/languages.md) | Languages, grammars, language servers, process launch, run and test commands, console output links | API-25 | `languages`, `languageServers`, `serverProcess`, `executionProviders`, `outputLinkMatchers` | +| [Plugin backend and plugin functions](domains/backend.md) | Backend process, function protocol (1–4), host methods, workspace contexts, files, credentials, terminals, cross-plugin calls | API-01, 04–08, 20–22, 24, 35, 37, 40, 41 | `backend`, `serverProcess` 1, `pluginFunctions` | +| [Editor, documents, and previews](domains/editor.md) | Documents and selections, versioned edits, open and reveal, HTML preview, editor actions, editor language-feature providers, virtual-document content providers | API-09, 10, 12, 15, 16, 25, 27 | `backend` 2 | | [UI](domains/ui.md) | Tool windows, commands, package web content, native controls, backend commands, message boxes | API-02, 03, 13, 14, 17 | `ui` | | [Plugin settings](domains/settings.md) | Declarative settings, placement, scope, apply behavior, settings page actions | API-19 | `settings` | | [Declarative build systems](domains/build-systems.md) | Build detection, models, command planning, project preparation roles | API-29 | `buildSystems` | @@ -83,7 +83,7 @@ This directory organizes the plugin contract by feature: what each numbered API | [Package trust](domains/package-trust.md) | Signature requirements, the signature file format, plugins bundled with the application | Not numbered | `packageTrust` | | [Marketplace](domains/marketplace.md) | Index sources and format, availability, install review and confirmation | Not numbered | — | -Planned APIs that have no methods yet (API-11, 12, 18, 26, 28, 32, 34–36, 38, 39, 42–44) are listed only in the [catalog](api-catalog.md#planned-apis) and have no feature page. +Planned APIs that have no methods yet (API-11, 18, 26, 28, 32, 34, 36, 38, 39, 42–44) are listed only in the [catalog](api-catalog.md#planned-apis) and have no feature page. ### Layout of each feature page diff --git a/reference/api-catalog.md b/reference/api-catalog.md index 53bf7e4..fcfc14e 100644 --- a/reference/api-catalog.md +++ b/reference/api-catalog.md @@ -36,23 +36,25 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I | API-08 | 搜索 | 部分完成 | `workspace/findFiles`、`findText`,分页、可取消 | 只搜磁盘内容,不含未保存的修改;匹配的精确范围目前是 `null` | [插件后端](domains/backend.md) | | API-09 | 文档与选区读取 | 已公开 | `documents/list`、`documents/getSnapshot`、`editor/getActive`、`editor/getSelections`,包含未保存状态、版本、编码、换行符 | — | [编辑器](domains/editor.md) | | API-10 | 版本化编辑 | 已公开 | `editor/applyEdits`、`replaceSelections`、`insertText`;校验版本,拒绝输入法组字中和只读文档,一批编辑是一次撤销 | — | [编辑器](domains/editor.md) | +| API-12 | 多文件编辑、虚拟文档、diff | 部分完成 | [内容提供者 `documents/registerContentProvider`](../contract/v1/README.md#content-providers):把一个 URI scheme 绑定到后端函数,打开该 scheme 的文档时宿主取只读内容(≤1 MiB,每个工作区窗口每个 scheme 一个;仅 macOS) | 跨文件编辑、对比视图 | [编辑器](domains/editor.md) | | API-13 | 原生视图数据源 | 部分完成 | [`ui` 3 原生 Tree、List、Form](../contract/v1/README.md#host-rendered-native-content-ui-3-native-schema-1) | 独立的后端数据源、懒加载 | [界面](domains/ui.md) | | API-14 | 网页消息 | 部分完成 | [包内网页](../contract/v1/README.md#web-content-ui-2)的桥接、状态和生命周期 | 通用的页面双向消息 | [界面](domains/ui.md) | | API-15 | 打开与定位 | 部分完成 | `editor/open`、`editor/reveal`,返回 `documentID` 和版本 | 传入范围时只把光标放到起点;不能替换预览标签 | [编辑器](domains/editor.md) | | API-16 | HTML 预览与外部浏览器 | 已公开 | `preview/openHTML`(`editorAndPreview` 或 `previewOnly`)、`browser/openExternal`(HTTP(S) 地址或已保存的 HTML) | — | [编辑器](domains/editor.md) | | API-17 | 消息、选择、输入 | 部分完成 | `window/showInformation`、`showWarning`、`showError`,带按钮和取消 | 通用的快速选择、输入框、经授权的文件句柄 | [界面](domains/ui.md) | | API-19 | 设置 | 部分完成 | [声明式设置 `settings` 1、2](../contract/v1/README.md#plugin-settings-api-2) | 运行期读取、查看来源、修改设置和设置变化事件 | [设置](domains/settings.md) | -| API-20 | 存储 | 部分完成 | 插件级 `${stateDir}`、后端和工作区的可写目录(`stateDirectory`)、网页状态 | 统一的键值存储、配额 | [插件后端](domains/backend.md) | +| API-20 | 存储 | 部分完成 | 插件级 `${stateDir}`、后端和工作区的可写目录(`stateDirectory`)、网页状态;语言服务器按工作区的状态目录,按声明的 [`state.retention`](../contract/v1/README.md#plugin-owned-sessions-languageservers-3) 由宿主在后台回收(Core 决定,仅 macOS) | 统一的键值存储、配额;用户可见的状态重置入口 | [插件后端](domains/backend.md) | | API-21 | 凭据 | 既有能力 | [协议 2 的 `credentials/get`、`set`、`delete`](../contract/v1/README.md#host-services-protocol-version-2),按插件隔离 | — | [插件后端](domains/backend.md) | -| API-22 | 输出、日志、进度 | 部分完成 | [插件函数的进度和取消](../contract/v1/README.md#progress);面向开发者的[结构化诊断日志 `litheLogging` 1](../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1) | 独立的输出面板、面向用户的日志和进度 API | [插件后端](domains/backend.md) | -| API-23 | 工具链与受管进程 | 部分完成 | 进程声明(`serverProcess`)、运行时解析、[共享可执行文件候选](../contract/v1/README.md#shared-executable-candidates-executableresolution-12)、工具定义、工具链提供者(macOS) | 后端可调用的通用工具链和进程服务;保存固定(pin)、`toolchainConsumers`、Windows 支持 | [工具链](domains/toolchains.md) | +| API-22 | 输出、日志、进度 | 部分完成 | [插件函数的进度和取消](../contract/v1/README.md#progress);面向开发者的[结构化诊断日志 `litheLogging` 1](../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1);[`languageServer/setStatus`](../contract/v1/README.md#language-server-host-methods) 在宿主状态界面显示插件自己的语言服务器准备状态(只有消息和百分比,没有 actions,仅 macOS);[`outputLinkMatchers` 1](../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1):插件声明的匹配器让 Run 与测试控制台的输出出现链接(macOS) | 独立的输出面板、面向用户的日志和进度 API | [插件后端](domains/backend.md) | +| API-23 | 工具链与受管进程 | 部分完成 | 进程声明(`serverProcess`)、运行时解析、[共享可执行文件候选](../contract/v1/README.md#shared-executable-candidates-executableresolution-12)、工具定义、工具链提供者(macOS);[`serverProcess` 4](../contract/v1/README.md#tool-references-serverprocess-4) 按工具定义 ID 引用运行时(清单 `requiresTools`);后端发起的[`run/start`、`run/stop` 和 `run/didExit` 通知](../contract/v1/README.md#run-and-debug-sessions-protocol-version-3),由宿主拥有 Run 会话(权限 `run.start`,macOS;工具解析不到可执行文件时返回 `plugin.host.toolUnresolved`) | 后端可调用的通用工具链和进程服务;保存固定(pin)、`toolchainConsumers`、Windows 支持 | [工具链](domains/toolchains.md) | | API-24 | 终端 | 部分完成 | [插件自有原生终端](../contract/v1/README.md#plugin-owned-native-terminals-function-protocol-3) `terminal/create`、`show`、`close`、`reopen`、`dispose`、`onExit`(macOS、本机工作区) | 向终端发送文本、读取终端输出;Windows | [插件后端](domains/backend.md) | -| API-25 | 语言、语法、LSP | 部分完成 | [语言](../contract/v1/README.md#languages)与[语言服务器](../contract/v1/README.md#language-servers);[`languages` 2 的包内 Monarch 语法和编辑器配置](../contract/v1/README.md#grammars-and-editor-configuration-languages-2) | 运行期注册或替换高亮提供者、独立的语言语义提供者 | [语言](domains/languages.md) | -| API-27 | 编辑器动作、主题、装饰 | 部分完成 | [独立编辑器动作 `editor/registerActions`](../contract/v1/README.md#independent-editor-actions-protocol-version-3),绑定普通函数和文件范围,不依赖 LSP、构建或运行 | 主题、代码片段、其他装饰 | [编辑器](domains/editor.md) | +| API-25 | 语言、语法、LSP | 部分完成 | [语言](../contract/v1/README.md#languages)与[语言服务器](../contract/v1/README.md#language-servers);[`languages` 2 的包内 Monarch 语法和编辑器配置](../contract/v1/README.md#grammars-and-editor-configuration-languages-2);[编辑器语言功能 Provider `editor/registerProviders`](../contract/v1/README.md#editor-language-feature-providers-protocol-version-3),把 `definition`、`hover`、`completion`、`codeLens`、`foldingRange`、`semanticTokens`、`documentLink` 这 7 种类型绑定到普通插件函数,语言服务器优先、插件结果只补充(macOS);插件自有的语言服务器会话:[`languageServers` 3](../contract/v1/README.md#plugin-owned-sessions-languageservers-3) 声明后端可用的请求、命令、转发通知,[`languageServer/request`、`notify`、`setStatus`](../contract/v1/README.md#language-server-host-methods) 与 `languageServer/didChangeState`、`languageServer/didReceiveNotification` 两个通知,会话策略由 Core 执行;[`serverProcess` 3](../contract/v1/README.md#backend-resolved-launch-serverprocess-3) 的 `launch.kind: "backend"` 由插件后端决定启动方式(仅 macOS) | 符号、格式化、重命名、Code Action、签名帮助、内联提示、运行期传入 JSON 动态注册高亮;远程工作区中由后端决定启动方式的服务器 | [语言](domains/languages.md)、[编辑器](domains/editor.md) | +| API-27 | 编辑器动作、主题、装饰 | 部分完成 | [独立编辑器动作 `editor/registerActions`](../contract/v1/README.md#independent-editor-actions-protocol-version-3),绑定普通函数和文件范围,不依赖 LSP、构建或运行;编辑器动作第 2 版增加行首多图标、至多 4 个 `alternatives`、点击结果的 `reveal` | 主题、代码片段、其他装饰 | [编辑器](domains/editor.md) | | API-29 | 构建、任务、测试、运行准备 | 部分完成 | [`buildSystems` 1–4](../contract/v1/README.md#api-level-3-build-systems)、[`buildProviders` 1–11](../contract/v1/README.md#build-providers)、`buildModelInputs` 1 | 开发指南还不完整 | [声明式构建](domains/build-systems.md)、[构建提供者](domains/build-providers.md) | -| API-30 | 调试适配器 | 既有能力 | [`debugAdapters` 1、2](../contract/v1/README.md#api-level-4-debug-adapters) 与宿主的 DAP 会话 | — | [调试适配器](domains/debug-adapters.md) | +| API-30 | 调试适配器 | 部分完成 | [`debugAdapters` 1、2](../contract/v1/README.md#api-level-4-debug-adapters) 与宿主的 DAP 会话;[`debugAdapters` 3](../contract/v1/README.md#adapter-features-and-defaults-debugadapters-3) 的 `features`、`defaults`(热代码替换、步进过滤器);后端发起的 [`debug/start`](../contract/v1/README.md#run-and-debug-sessions-protocol-version-3)(权限 `debug.start`,macOS) | 后端只能在本插件自己声明的适配器上启动会话,不能使用其他插件的适配器 | [调试适配器](domains/debug-adapters.md) | | API-31 | VCS 提供者 | 既有能力 | [`vcsProviders` 1](../contract/v1/README.md#api-level-3-vcs-providers) | 不含内置 Git 的公共读取(见 API-42) | [VCS](domains/vcs.md) | | API-33 | 项目生成器 | 既有能力 | [`projectGenerators` 1、2](../contract/v1/README.md#project-generators) | — | [项目生成器](domains/project-generators.md) | +| API-35 | 插件间调用与依赖 | 部分完成 | 第一阶段:[`pluginFunctions` 1](../contract/v1/README.md#cross-plugin-calls-protocol-version-4)(清单 `exports`、`calls` 声明,不授予清单权限)与函数协议 4 的 [`functions/providers`、`functions/invoke`](../contract/v1/README.md#discovery-and-invocation):经 Core 中转,调用另一个已启用插件导出的函数;信任不传递,不是安装依赖(macOS,本机工作区) | 依赖与导出的安装闭包、版本求解、远程 Provider;macOS 上新安装或更新的后端插件要重启 Lithe 后才会出现在 `providers` 里;官方 SDK 尚无 `functions/*` 封装 | [插件后端](domains/backend.md) | | API-37 | 环境信息与语言包 | 部分完成 | `getCapabilities` 返回协议版本、可用方法、限制和后端运行位置(`backendAuthority`) | 语言包、环境和主题变化事件 | [插件后端](domains/backend.md) | | API-40 | 工作区 authority | 部分完成 | 工作区上下文带 `local` 或 `remote`;文件和编辑器方法对远程上下文明确返回 `plugin.host.authorityUnsupported` | 远程执行 | [插件后端](domains/backend.md) | | API-41 | 激活事件与启动预算 | 部分完成 | [`onWorkspaceOpen`](../contract/v1/README.md#permissions-and-activation)、`onCommand`、后台两路并发启动、`initialize` 超时、有界通知队列 | 按工作区内容匹配激活、全局冷启动预算 | [插件后端](domains/backend.md) | @@ -62,13 +64,11 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I | ID | 名称 | 状态 | 计划提供 | | --- | --- | --- | --- | | API-11 | 保存与文件变更 | 未做 | 保存、另存为、关闭,以及工作区文件的创建、改名、删除 | -| API-12 | 多文件编辑、虚拟文档、diff | 未做 | 跨文件编辑、按 URI scheme 提供的只读虚拟文档、对比视图 | | API-18 | 剪贴板、Finder | 未做 | 剪贴板读写、在 Finder 中显示 | | API-26 | 诊断集合 | 未做 | 后端推送的诊断集合,带文档版本;现有的 LSP 诊断不属于这个 API | | API-28 | 自定义编辑器 | 未做 | 插件自有文档的编辑器,含保存、撤销、恢复 | | API-32 | 服务 | 未做 | 通用服务提供者(仍是提案) | | API-34 | IDE/AI/MCP | 未做 | 插件可用的 IDE、AI、MCP 能力与授权 | -| API-35 | 插件间调用与依赖 | 进行中 | 第一阶段:经 Core 中转调用另一个插件的函数(不是安装依赖);之后是依赖与导出、版本求解 | | API-36 | WASM | 未做 | WASM/WASI 运行时 | | API-38 | 受管网络 | 未做 | 宿主代为执行的网络请求和下载 | | API-39 | 身份验证 | 未做 | 身份验证会话和提供者 | @@ -83,8 +83,8 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I 后端向宿主发送的请求,前缀都是 `lithe/host/`,下表省略前缀。一个 API 编号下可以有多个方法,每个方法单独一行,按 API 编号排列。 - **“API”列是规划编号**,来自 [API 实施计划](https://github.com/1lck/Lithe-IDEA/blob/preview/docs/development/macos-plugin-api-plan.md),和“协议”列的 `litheFunctions` 版本、清单里的能力版本分开记录。同一个 API 的方法可以分几个 PR 陆续加入;协议 3 里向后兼容地新增方法,也不升协议版本。每个方法哪天、由哪个 PR 首次加入,见[插件协议更新记录](protocol-changelog.md#宿主方法按-api-编号)。 -- **“协议”列**是最低的 `litheFunctions` 版本。除 `getCapabilities`、`subscriptions/dispose`、`workspace/listContexts` 外,协议 3 的方法都要带 `context: {contextID, generation}`。 -- **“权限”列**:除 `credentials.store` 外,都要求清单声明 `backend` 2,否则安装时拒绝。 +- **“协议”列**是最低的 `litheFunctions` 版本。除 `getCapabilities`、`subscriptions/dispose`、`workspace/listContexts`、`run/stop`、`functions/providers` 外,协议 3 及以上的方法都要带 `context: {contextID, generation}`。协议 4 的方法(`functions/*`)只对协商到协议 4 的后端开放;官方 SDK 目前只协商 1–3。 +- **“权限”列**:除 `credentials.store` 外,都要求清单声明 `backend` 2,否则安装时拒绝。`functions/*` 没有清单权限,由清单 `pluginFunctions` 段的 `calls` 是否声明了该函数决定。另有两条前置条件:`debug.start` 还要求已声明的 `debugAdapters` 条目;`languageServer.request` 只在 `languageServers` 3 的某个服务器列出 `requestMethods` 或 `forwardNotifications` 时才能声明。 | API | 方法 | 用途 | 权限 | 协议 | | --- | --- | --- | --- | --- | @@ -106,6 +106,7 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I | API-10 | `editor/applyEdits` | 按范围批量编辑,先校验 `expectedVersion` | `editor.edit` | 3 | | API-10 | `editor/replaceSelections` | 替换选区的文本:一个替换文本用于全部选区,或每个选区一个 | `editor.edit` | 3 | | API-10 | `editor/insertText` | 在指定位置插入文本 | `editor.edit` | 3 | +| API-12 | `documents/registerContentProvider` | 把一个 URI scheme 绑定到后端函数,打开该 scheme 的文档时宿主调用函数取只读内容(≤1 MiB),返回 `subscriptionID`;用 `subscriptions/dispose` 撤销 | `documents.contentProvider` | 3 | | API-15 | `editor/open` | 在编辑器中打开文件,没有缓冲区时从磁盘读取;可以前台或后台打开,并定位光标 | `editor.open` | 3 | | API-15 | `editor/reveal` | 参数和 `editor/open` 相同,但只处理已打开的文件;文件没打开时返回 `plugin.host.notFound`,不会从磁盘打开 | `editor.open` | 3 | | API-16 | `preview/openHTML` | 打开 HTML 文件的预览(`editorAndPreview` 或 `previewOnly`),预览使用实时缓冲区 | `preview.open` | 3 | @@ -116,13 +117,22 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I | API-21 | `credentials/get` | 读取本插件保存的密钥,没有时为 `null` | `credentials.store` | 2 | | API-21 | `credentials/set` | 保存密钥,最多 16 KiB | `credentials.store` | 2 | | API-21 | `credentials/delete` | 删除密钥,没有保存过也返回 `null` | `credentials.store` | 2 | +| API-22、API-25 | `languageServer/setStatus` | 在宿主状态界面显示插件自己的语言服务器准备状态(`preparing`、`ready`、`degraded`、`failed`,可带消息和百分比) | `languageServer.request` | 3 | +| API-23 | `run/start` | 启动宿主拥有的 Run 会话(Run 窗口条目、控制台、停止按钮);可执行文件只能是工具定义 ID 或插件包内文件,可带临时文件、调试和再次运行,返回 `runID` | `run.start` | 3 | +| API-23 | `run/stop` | 按 `runID` 或 `sessionID` 停止本插件启动的运行或调试会话;不带 `context` | `run.start` | 3 | | API-24 | `terminal/create` | 经执行确认创建插件自有原生终端,返回 `terminalID` 和 `generation` | `terminal.create` | 3 | | API-24 | `terminal/show` | 显示并选中本插件的终端 | `terminal.create` | 3 | | API-24 | `terminal/close` | 结束进程树并关闭标签,按字节预算保留显示历史和启动配置 | `terminal.create` | 3 | | API-24 | `terminal/reopen` | 经重新确认,用原启动配置创建新进程,`generation` 加 1,并恢复有界的显示历史 | `terminal.create` | 3 | | API-24 | `terminal/dispose` | 关闭终端,彻底释放句柄、启动配置和历史 | `terminal.create` | 3 | | API-24 | `terminal/onExit` | 订阅终端每个 `generation` 的退出结果 | `terminal.create` | 3 | +| API-25 | `editor/registerProviders` | 注册编辑器语言功能 Provider:把 `definition`、`hover`、`completion`、`codeLens`、`foldingRange`、`semanticTokens`、`documentLink` 绑定到后端的普通函数和文件范围(`selector`),返回 `subscriptionID`;语言服务器优先,插件结果只补充 | `editor.providers`(同时需要 `documents.read`) | 3 | +| API-25 | `languageServer/request` | 向本插件自己的语言服务器发 JSON-RPC 请求,受会话策略检查,返回 `{value}` | `languageServer.request` | 3 | +| API-25 | `languageServer/notify` | 向本插件自己的语言服务器发通知,返回 `{delivered}` | `languageServer.request` | 3 | | API-27 | `editor/registerActions` | 注册编辑器动作:把后端的普通函数绑定到文件范围(`selector`),返回 `subscriptionID` | `editor.actions`(同时需要 `documents.read`) | 3 | +| API-30 | `debug/start` | 在本插件自己声明的调试适配器上启动 launch 或 attach 调试会话,返回 `sessionID` | `debug.start` | 3 | +| API-35 | `functions/providers` | 列出导出某函数、已启用、且不是调用方自己的插件及其导出版本 | 无清单权限,需在 `pluginFunctions.calls` 声明该函数 | 4 | +| API-35 | `functions/invoke` | 经 Core 调用另一个插件导出的函数,可带 `parentOperationID` 标明所属调用链,返回 `{value}` | 无清单权限,需在 `pluginFunctions.calls` 声明该函数 | 4 | 快捷键导入提供者另有 `keymapImport/listInputs`、`keymapImport/readChunk` 两个方法,属于 `litheKeymapImport` 协议,不在 API-01–44 内,见[快捷键导入](domains/keymap-import.md)。 @@ -133,12 +143,16 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I | API-02 | `commands/didRun` | 用户运行了本插件的命令;只是事件,不表示命令的业务操作已经成功 | 每个插件会话都有 | | API-04、API-05 | `workspace/didChangeContexts` | 工作区上下文有变化,带新的 `revision`;后端再调用 `listContexts` 读取 | `workspace.metadata` | | API-07、API-24 | `subscriptions/notify` | 送达一个订阅的事件,例如文件变化(`workspace/watch`)、终端退出(`terminal/onExit`) | 每个插件会话都有 | +| API-23 | `run/didExit` | 后端用 `run/start` 启动的运行结束,带 `exitCode` 或 `signal`(宿主丢失进程时两者都没有);每次运行只发一次 | `run.start` | +| API-25 | `languageServer/didChangeState` | 本插件某个语言服务器的状态变化(`starting`、`running`、`stopped`、`crashed`),带 `generation` | `languageServer.request` | +| API-25 | `languageServer/didReceiveNotification` | 转发服务器发出、`forwardNotifications` 列出的通知;超过 `limits.notificationBytes` 时只送一次 `truncated: true`,不带 `params` | `languageServer.request` | ### 限制 | 项目 | 上限 | | --- | --- | | 单次文件读取、文档快照 | 128 KiB 源文本或字节 | +| 发给编辑器动作收集函数的文档 | 宿主的 `limits.editorActionSourceBytes`(macOS 1 MiB;宿主未报告时 128 KiB) | | 一批编辑或选区 | 1,000 项 | | 目录分页 | 默认 200,最多 1,000 | | 搜索分页 | 默认 100,最多 200(搜索总结果 200) | @@ -147,6 +161,14 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I | 未回答的宿主请求 | 每个会话 16 个 | | 通知队列 | 每个会话 128 条 | | 插件自有终端 | 每个插件最多保留 8 个句柄 | +| 编辑器 Provider 注册 | 每个选择器 1–32 项,每个插件跨上下文最多 32 个注册 | +| 编辑器 Provider 单次调用时限 | hover、completion 400 ms,definition 1500 ms,其余类型 2000 ms;迟到的答复直接丢弃,注册不能自定义时限 | +| `languageServer/request` | `timeoutMilliseconds` 1–300000 ms,默认 60000;每个会话最多 128 个等待 gate 打开的请求,第 129 个返回 `plugin.host.busy` | +| `languageServer/setStatus` | `message` 按字符边界截到 1024 字节 | +| `run/start` | `label` 1–256 字符;`temporaryFiles` 最多 4 个,每个文本 ≤4 MiB;`rerun.data` ≤64 KiB | +| `debug/start` | `configuration` ≤256 KiB | +| 内容提供者 | 提供的内容 ≤1 MiB,宿主等待 30 秒 | +| `functions/invoke` | `timeoutMilliseconds` 默认 60000 ms,上限 600000 ms;`parentOperationID` 1–128 字节 | 失败时的错误码和处理方式见契约的 [Outcomes and error codes](../contract/v1/README.md#outcomes-and-error-codes) 与 [Protocol 3 宿主失败码表](../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3)。取消或超时**不保证回滚**:编辑可能已经生效,不要自动重放。 @@ -186,23 +208,25 @@ Status meanings: | API-08 | Search | Partial | `workspace/findFiles` and `findText`, paged and cancellable | Searches disk content only, not unsaved changes; the exact match range is `null` for now | [Plugin backend](domains/backend.md) | | API-09 | Documents and selections | Public | `documents/list`, `documents/getSnapshot`, `editor/getActive`, `editor/getSelections`, with dirty state, version, encoding, and line ending | — | [Editor](domains/editor.md) | | API-10 | Versioned edits | Public | `editor/applyEdits`, `replaceSelections`, `insertText`; version checks, refusal during IME composition and in read-only documents, one undo step per batch | — | [Editor](domains/editor.md) | +| API-12 | Multi-file edits, virtual documents, diff | Partial | [Content providers `documents/registerContentProvider`](../contract/v1/README.md#content-providers): bind a URI scheme to a backend function, and the host fetches read-only content from it when a document of that scheme opens (≤1 MiB, one provider per scheme per workspace window; macOS only) | Cross-file edits, a diff view | [Editor](domains/editor.md) | | API-13 | Native view data sources | Partial | [`ui` 3 native Tree, List, Form](../contract/v1/README.md#host-rendered-native-content-ui-3-native-schema-1) | An independent backend data source, lazy loading | [UI](domains/ui.md) | | API-14 | Webview messaging | Partial | The [package web page](../contract/v1/README.md#web-content-ui-2) bridge, state, and lifecycle | General two-way page messaging | [UI](domains/ui.md) | | API-15 | Open and reveal | Partial | `editor/open` and `editor/reveal`, returning `documentID` and version | A range only places the caret at its start; no preview-tab replacement | [Editor](domains/editor.md) | | API-16 | HTML preview and external browser | Public | `preview/openHTML` (`editorAndPreview` or `previewOnly`), `browser/openExternal` (HTTP(S) URLs or saved HTML) | — | [Editor](domains/editor.md) | | API-17 | Messages, quick pick, input | Partial | `window/showInformation`, `showWarning`, `showError` with buttons and dismissal | General quick pick, input box, authorized file handles | [UI](domains/ui.md) | | API-19 | Settings | Partial | [Declarative settings `settings` 1 and 2](../contract/v1/README.md#plugin-settings-api-2) | Reading, inspecting the source of, and updating settings at runtime, and settings change events | [Settings](domains/settings.md) | -| API-20 | Storage | Partial | The plugin-wide `${stateDir}`, writable backend and workspace directories (`stateDirectory`), web page state | A unified key-value store, quotas | [Plugin backend](domains/backend.md) | +| API-20 | Storage | Partial | The plugin-wide `${stateDir}`, writable backend and workspace directories (`stateDirectory`), web page state; a language server's per-workspace state directories, which the host prunes in the background by the declared [`state.retention`](../contract/v1/README.md#plugin-owned-sessions-languageservers-3) (Core decides, macOS only) | A unified key-value store, quotas; a user-facing way to reset state | [Plugin backend](domains/backend.md) | | API-21 | Credentials | Existing | [Protocol 2 `credentials/get`, `set`, `delete`](../contract/v1/README.md#host-services-protocol-version-2), isolated per plugin | — | [Plugin backend](domains/backend.md) | -| API-22 | Output, log, progress | Partial | [Plugin function progress and cancellation](../contract/v1/README.md#progress); developer-facing [structured diagnostics `litheLogging` 1](../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1) | A dedicated output panel, user-facing log and progress APIs | [Plugin backend](domains/backend.md) | -| API-23 | Toolchains and managed processes | Partial | Process declarations (`serverProcess`), runtime resolution, [shared executable candidates](../contract/v1/README.md#shared-executable-candidates-executableresolution-12), tool definitions, toolchain providers (macOS) | A general toolchain and process service backends can call; saving pins, `toolchainConsumers`, Windows support | [Toolchains](domains/toolchains.md) | +| API-22 | Output, log, progress | Partial | [Plugin function progress and cancellation](../contract/v1/README.md#progress); developer-facing [structured diagnostics `litheLogging` 1](../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1); [`languageServer/setStatus`](../contract/v1/README.md#language-server-host-methods) shows the plugin's own language server preparation state in the host's status UI (a message and a percentage only, no actions; macOS only); [`outputLinkMatchers` 1](../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1): matchers a plugin declares make output in the Run and Test consoles clickable (macOS) | A dedicated output panel, user-facing log and progress APIs | [Plugin backend](domains/backend.md) | +| API-23 | Toolchains and managed processes | Partial | Process declarations (`serverProcess`), runtime resolution, [shared executable candidates](../contract/v1/README.md#shared-executable-candidates-executableresolution-12), tool definitions, toolchain providers (macOS); [`serverProcess` 4](../contract/v1/README.md#tool-references-serverprocess-4) references runtimes by tool-definition ID (manifest `requiresTools`); backend-started [`run/start`, `run/stop`, and the `run/didExit` notification](../contract/v1/README.md#run-and-debug-sessions-protocol-version-3), where the host owns the Run session (permission `run.start`, macOS; a tool that resolves to no executable returns `plugin.host.toolUnresolved`) | A general toolchain and process service backends can call; saving pins, `toolchainConsumers`, Windows support | [Toolchains](domains/toolchains.md) | | API-24 | Terminals | Partial | [Plugin-owned native terminals](../contract/v1/README.md#plugin-owned-native-terminals-function-protocol-3) `terminal/create`, `show`, `close`, `reopen`, `dispose`, `onExit` (macOS, local workspaces) | Sending text to a terminal, reading its output; Windows | [Plugin backend](domains/backend.md) | -| API-25 | Languages, grammars, LSP | Partial | [Languages](../contract/v1/README.md#languages) and [language servers](../contract/v1/README.md#language-servers); [packaged Monarch grammars and editor configuration in `languages` 2](../contract/v1/README.md#grammars-and-editor-configuration-languages-2) | Registering or replacing highlighting providers at runtime, independent language semantic providers | [Languages](domains/languages.md) | -| API-27 | Editor actions, themes, decorations | Partial | [Independent editor actions `editor/registerActions`](../contract/v1/README.md#independent-editor-actions-protocol-version-3), binding ordinary functions to file ranges without LSP, build, or run | Themes, snippets, other decorations | [Editor](domains/editor.md) | +| API-25 | Languages, grammars, LSP | Partial | [Languages](../contract/v1/README.md#languages) and [language servers](../contract/v1/README.md#language-servers); [packaged Monarch grammars and editor configuration in `languages` 2](../contract/v1/README.md#grammars-and-editor-configuration-languages-2); [editor language-feature providers `editor/registerProviders`](../contract/v1/README.md#editor-language-feature-providers-protocol-version-3), binding 7 types (`definition`, `hover`, `completion`, `codeLens`, `foldingRange`, `semanticTokens`, `documentLink`) to ordinary plugin functions, where the language server answers first and plugin results only add (macOS); plugin-owned language server sessions: [`languageServers` 3](../contract/v1/README.md#plugin-owned-sessions-languageservers-3) declares the requests, commands, and forwarded notifications a backend may use, with [`languageServer/request`, `notify`, `setStatus`](../contract/v1/README.md#language-server-host-methods) and the `languageServer/didChangeState` and `languageServer/didReceiveNotification` notifications, and Core enforces the session policy; `launch.kind: "backend"` of [`serverProcess` 3](../contract/v1/README.md#backend-resolved-launch-serverprocess-3) lets the plugin backend decide how the server starts (macOS only) | Symbols, formatting, rename, code actions, signature help, inlay hints, dynamic registration of highlighting from JSON passed at runtime; servers whose launch the backend decides in remote workspaces | [Languages](domains/languages.md), [Editor](domains/editor.md) | +| API-27 | Editor actions, themes, decorations | Partial | [Independent editor actions `editor/registerActions`](../contract/v1/README.md#independent-editor-actions-protocol-version-3), binding ordinary functions to file ranges without LSP, build, or run; editor actions version 2 adds several icons at a line start, up to 4 `alternatives`, and a `reveal` in the click result | Themes, snippets, other decorations | [Editor](domains/editor.md) | | API-29 | Build, tasks, tests, run preparation | Partial | [`buildSystems` 1–4](../contract/v1/README.md#api-level-3-build-systems), [`buildProviders` 1–11](../contract/v1/README.md#build-providers), `buildModelInputs` 1 | The developer guides are incomplete | [Build systems](domains/build-systems.md), [Build providers](domains/build-providers.md) | -| API-30 | Debug adapters | Existing | [`debugAdapters` 1 and 2](../contract/v1/README.md#api-level-4-debug-adapters) and the host's DAP sessions | — | [Debug adapters](domains/debug-adapters.md) | +| API-30 | Debug adapters | Partial | [`debugAdapters` 1 and 2](../contract/v1/README.md#api-level-4-debug-adapters) and the host's DAP sessions; `features` and `defaults` of [`debugAdapters` 3](../contract/v1/README.md#adapter-features-and-defaults-debugadapters-3) (hot code replace, step filters); backend-started [`debug/start`](../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) (permission `debug.start`, macOS) | A backend can start sessions only on adapters its own plugin declares, not on another plugin's adapters | [Debug adapters](domains/debug-adapters.md) | | API-31 | VCS providers | Existing | [`vcsProviders` 1](../contract/v1/README.md#api-level-3-vcs-providers) | Public reads of built-in Git are not included (see API-42) | [VCS](domains/vcs.md) | | API-33 | Project generators | Existing | [`projectGenerators` 1, 2](../contract/v1/README.md#project-generators) | — | [Project generators](domains/project-generators.md) | +| API-35 | Cross-plugin calls and dependencies | Partial | Phase one: [`pluginFunctions` 1](../contract/v1/README.md#cross-plugin-calls-protocol-version-4) (`exports` and `calls` in the manifest, granting no manifest permission) and, in function protocol 4, [`functions/providers` and `functions/invoke`](../contract/v1/README.md#discovery-and-invocation): Core relays a call to a function another enabled plugin exports; trust is not transitive and this is not an install dependency (macOS, local workspaces) | The install closure of dependencies and exports, version solving, remote providers; on macOS a newly installed or updated backend plugin appears in `providers` only after Lithe restarts; the official SDKs have no `functions/*` wrapper yet | [Plugin backend](domains/backend.md) | | API-37 | Environment info and l10n | Partial | `getCapabilities` returns the protocol version, usable methods, limits, and where the backend runs (`backendAuthority`) | Language packs, environment and theme change events | [Plugin backend](domains/backend.md) | | API-40 | Workspace authority | Partial | Workspace contexts carry `local` or `remote`; file and editor methods refuse a remote context with `plugin.host.authorityUnsupported` | Remote execution | [Plugin backend](domains/backend.md) | | API-41 | Activation events and startup budget | Partial | [`onWorkspaceOpen`](../contract/v1/README.md#permissions-and-activation), `onCommand`, two concurrent background starts, `initialize` timeout, bounded notification queues | Activation by workspace content, a global cold-start budget | [Plugin backend](domains/backend.md) | @@ -212,13 +236,11 @@ Status meanings: | ID | Name | Status | Planned | | --- | --- | --- | --- | | API-11 | Save and file operations | Not started | Save, save as, close, and creating, renaming, and deleting workspace files | -| API-12 | Multi-file edits, content providers, diff | Not started | Cross-file edits, read-only virtual documents provided per URI scheme, a diff view | | API-18 | Clipboard and Finder | Not started | Reading and writing the clipboard, revealing in Finder | | API-26 | Diagnostics collections | Not started | Diagnostics collections pushed by a backend, with document versions; existing LSP diagnostics are not this API | | API-28 | Custom editors | Not started | Editors for plugin-owned documents, with save, undo, and restore | | API-32 | Services | Not started | General service providers (still a proposal) | | API-34 | IDE, AI, MCP | Not started | IDE, AI, and MCP capabilities for plugins, with authorization | -| API-35 | Cross-plugin calls and dependencies | In progress | Phase one: calling another plugin's function through Core (not an install dependency); then dependencies and exports, version solving | | API-36 | WASM | Not started | A WASM/WASI runtime | | API-38 | Managed network | Not started | Network requests and downloads run by the host | | API-39 | Authentication | Not started | Authentication sessions and providers | @@ -233,8 +255,8 @@ The manifest, marketplace, package trust, and shortcut import are installation a Requests a backend sends to the host, all prefixed `lithe/host/`; the tables omit the prefix. One API ID can cover several methods; each method has its own row, ordered by API ID. - **The “API” column holds planning IDs** from the [API implementation plan](https://github.com/1lck/Lithe-IDEA/blob/preview/docs/development/macos-plugin-api-plan.md). They are recorded separately from the `litheFunctions` version in the “Protocol” column and from manifest capability versions. The methods of one API can arrive over several PRs, and a backward-compatible method added within protocol 3 does not bump the protocol version. The date and PR that first added each method are in the [protocol changelog](protocol-changelog.md#宿主方法按-api-编号) (Chinese only). -- **The “Protocol” column** is the lowest `litheFunctions` version. Apart from `getCapabilities`, `subscriptions/dispose`, and `workspace/listContexts`, every protocol 3 method takes `context: {contextID, generation}`. -- **The “Permission” column**: apart from `credentials.store`, every permission requires the manifest to declare `backend` 2, or installation refuses it. +- **The “Protocol” column** is the lowest `litheFunctions` version. Apart from `getCapabilities`, `subscriptions/dispose`, `workspace/listContexts`, `run/stop`, and `functions/providers`, every method of protocol 3 and later takes `context: {contextID, generation}`. Protocol 4 methods (`functions/*`) are open only to a backend that negotiated protocol 4; the official SDKs currently negotiate only 1–3. +- **The “Permission” column**: apart from `credentials.store`, every permission requires the manifest to declare `backend` 2, or installation refuses it. `functions/*` has no manifest permission; whether the manifest's `pluginFunctions` section lists the function under `calls` decides. Two more preconditions: `debug.start` also requires a declared `debugAdapters` entry, and `languageServer.request` can be declared only when a `languageServers` 3 server lists `requestMethods` or `forwardNotifications`. | API | Method | Purpose | Permission | Protocol | | --- | --- | --- | --- | --- | @@ -256,6 +278,7 @@ Requests a backend sends to the host, all prefixed `lithe/host/`; the tables omi | API-10 | `editor/applyEdits` | Applies a batch of range edits after checking `expectedVersion` | `editor.edit` | 3 | | API-10 | `editor/replaceSelections` | Replaces the text of selections: one replacement for all of them, or one per selection | `editor.edit` | 3 | | API-10 | `editor/insertText` | Inserts text at a position | `editor.edit` | 3 | +| API-12 | `documents/registerContentProvider` | Binds a URI scheme to a backend function; when a document of that scheme opens, the host calls the function to get read-only content (≤1 MiB), and the call returns a `subscriptionID`, which `subscriptions/dispose` withdraws | `documents.contentProvider` | 3 | | API-15 | `editor/open` | Opens a file in the editor, reading it from disk when no buffer exists; opens it in the foreground or background and can place the caret | `editor.open` | 3 | | API-15 | `editor/reveal` | Takes the same params as `editor/open` but only handles files already open; a file that is not open returns `plugin.host.notFound` and is not opened from disk | `editor.open` | 3 | | API-16 | `preview/openHTML` | Opens a preview of an HTML file (`editorAndPreview` or `previewOnly`) that uses the live buffer | `preview.open` | 3 | @@ -266,13 +289,22 @@ Requests a backend sends to the host, all prefixed `lithe/host/`; the tables omi | API-21 | `credentials/get` | Reads a secret this plugin stored, or `null` | `credentials.store` | 2 | | API-21 | `credentials/set` | Stores a secret of at most 16 KiB | `credentials.store` | 2 | | API-21 | `credentials/delete` | Deletes a secret; also returns `null` when nothing was stored | `credentials.store` | 2 | +| API-22, API-25 | `languageServer/setStatus` | Shows the plugin's own language server preparation state (`preparing`, `ready`, `degraded`, `failed`, optionally with a message and a percentage) in the host's status UI | `languageServer.request` | 3 | +| API-23 | `run/start` | Starts a host-owned Run session (Run window entry, console, Stop button); the executable can only be a tool-definition ID or a file inside the plugin package, and the call can carry temporary files, debugging, and rerun, and returns a `runID` | `run.start` | 3 | +| API-23 | `run/stop` | Stops a run or debug session this plugin started, by `runID` or `sessionID`; takes no `context` | `run.start` | 3 | | API-24 | `terminal/create` | Creates a plugin-owned native terminal after execution consent and returns `terminalID` and `generation` | `terminal.create` | 3 | | API-24 | `terminal/show` | Shows and selects one of this plugin's terminals | `terminal.create` | 3 | | API-24 | `terminal/close` | Ends the process tree and closes the tab, keeping display history within a byte budget and the launch settings | `terminal.create` | 3 | | API-24 | `terminal/reopen` | After renewed consent, starts a new process with the original launch settings, increments `generation`, and restores bounded display history | `terminal.create` | 3 | | API-24 | `terminal/dispose` | Closes the terminal and releases its handle, launch settings, and history for good | `terminal.create` | 3 | | API-24 | `terminal/onExit` | Subscribes to the exit outcome of each `generation` of a terminal | `terminal.create` | 3 | +| API-25 | `editor/registerProviders` | Registers editor language-feature providers: binds `definition`, `hover`, `completion`, `codeLens`, `foldingRange`, `semanticTokens`, or `documentLink` to an ordinary backend function and file ranges (`selector`), and returns a `subscriptionID`; the language server answers first and plugin results only add | `editor.providers` (also needs `documents.read`) | 3 | +| API-25 | `languageServer/request` | Sends a JSON-RPC request to the plugin's own language server, checked against the session policy, and returns `{value}` | `languageServer.request` | 3 | +| API-25 | `languageServer/notify` | Sends a notification to the plugin's own language server and returns `{delivered}` | `languageServer.request` | 3 | | API-27 | `editor/registerActions` | Registers editor actions that bind an ordinary backend function to file ranges (`selector`) and returns a `subscriptionID` | `editor.actions` (also needs `documents.read`) | 3 | +| API-30 | `debug/start` | Starts a launch or attach debug session on a debug adapter the plugin itself declares, and returns a `sessionID` | `debug.start` | 3 | +| API-35 | `functions/providers` | Lists the enabled plugins, other than the caller, that export a function, with their exported versions | No manifest permission; the function must be declared in `pluginFunctions.calls` | 4 | +| API-35 | `functions/invoke` | Calls a function another plugin exports, relayed by Core; can carry `parentOperationID` to name the call chain it belongs to, and returns `{value}` | No manifest permission; the function must be declared in `pluginFunctions.calls` | 4 | Shortcut import providers also have two methods, `keymapImport/listInputs` and `keymapImport/readChunk`. They belong to the `litheKeymapImport` protocol, outside API-01–44; see [Shortcut import](domains/keymap-import.md). @@ -283,12 +315,16 @@ Notifications the host sends to a backend (the backend answers none), also prefi | API-02 | `commands/didRun` | The user ran one of this plugin's commands; it is an event only and does not mean the command's business action succeeded | Every plugin-owned session | | API-04, API-05 | `workspace/didChangeContexts` | The workspace contexts changed, with the new `revision`; the backend then calls `listContexts` to read them | `workspace.metadata` | | API-07, API-24 | `subscriptions/notify` | Delivers an event of one subscription, such as a file change (`workspace/watch`) or a terminal exit (`terminal/onExit`) | Every plugin-owned session | +| API-23 | `run/didExit` | A run the backend started with `run/start` ended, with `exitCode` or `signal` (neither when the host lost the process); sent once per run | `run.start` | +| API-25 | `languageServer/didChangeState` | The state of one of this plugin's language servers changed (`starting`, `running`, `stopped`, `crashed`), with `generation` | `languageServer.request` | +| API-25 | `languageServer/didReceiveNotification` | Forwards a notification the server sent and `forwardNotifications` lists; past `limits.notificationBytes` it is delivered once with `truncated: true` and without `params` | `languageServer.request` | ### Limits | Item | Limit | | --- | --- | | One file read or document snapshot | 128 KiB of source text or bytes | +| The document sent to an editor action collector | The host's `limits.editorActionSourceBytes` (1 MiB on macOS; 128 KiB when the host does not report it) | | One batch of edits or selections | 1,000 entries | | Directory pages | 200 by default, at most 1,000 | | Search pages | 100 by default, at most 200 (200 search results in total) | @@ -297,5 +333,13 @@ Notifications the host sends to a backend (the backend answers none), also prefi | Unanswered host requests | 16 per session | | Notification queue | 128 entries per session | | Plugin-owned terminals | At most 8 retained handles per plugin | +| Editor provider registrations | 1–32 selector entries per registration, at most 32 registrations per plugin across contexts | +| Editor provider call budget | 400 ms for hover and completion, 1500 ms for definition, 2000 ms for the other types; a late answer is dropped, and a registration cannot set its own budget | +| `languageServer/request` | `timeoutMilliseconds` 1–300000 ms, default 60000; at most 128 requests per session waiting for a gate to open, and the 129th returns `plugin.host.busy` | +| `languageServer/setStatus` | `message` is cut to 1024 bytes on a character boundary | +| `run/start` | `label` 1–256 characters; `temporaryFiles` at most 4, each up to 4 MiB of text; `rerun.data` up to 64 KiB | +| `debug/start` | `configuration` up to 256 KiB | +| Content providers | Provided content up to 1 MiB; the host waits 30 seconds | +| `functions/invoke` | `timeoutMilliseconds` defaults to 60000 ms, capped at 600000 ms; `parentOperationID` is 1–128 bytes | For failure codes and how to handle them, see the contract's [Outcomes and error codes](../contract/v1/README.md#outcomes-and-error-codes) and the [protocol 3 host failure table](../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3). Cancellation or timeout **does not promise rollback**: an edit may already have taken effect, so do not replay it automatically. diff --git a/reference/capabilities-and-versions.md b/reference/capabilities-and-versions.md index 63fc562..373295d 100644 --- a/reference/capabilities-and-versions.md +++ b/reference/capabilities-and-versions.md @@ -4,7 +4,7 @@ ## 中文 -本页把插件会碰到的所有版本号放在一起:每个能力、每个进程协议、每个文档格式的**全部版本**,以及 Rust Core、macOS、Windows、三个官方 SDK 各自支持到哪一版。规范以 [`contract/v1/README.md`](../contract/v1/README.md) 为准;本页与契约不一致时以契约为准,并请提 Issue。本页以主仓库最新 `preview` 为基准:Core、macOS、Windows 列按 `preview` 上的代码核对(2026-11-05,`644afdaac`),契约链接对应 [`contract.lock.json`](../contract.lock.json) 锁定的修订。Lithe 稳定版可能还没有其中的部分版本,发布插件前以目标版本的发行说明为准。 +本页把插件会碰到的所有版本号放在一起:每个能力、每个进程协议、每个文档格式的**全部版本**,以及 Rust Core、macOS、Windows、三个官方 SDK 各自支持到哪一版。规范以 [`contract/v1/README.md`](../contract/v1/README.md) 为准;本页与契约不一致时以契约为准,并请提 Issue。本页以主仓库最新 `preview` 为基准:Core、macOS、Windows 列按 `preview` 上的代码核对(2026-11-05,`5cc06d5f4`),契约链接对应 [`contract.lock.json`](../contract.lock.json) 锁定的修订。Lithe 稳定版可能还没有其中的部分版本,发布插件前以目标版本的发行说明为准。 ### 四类版本号 @@ -47,8 +47,11 @@ | `languages` | 2 | [`grammar` 与 `configuration`](../contract/v1/README.md#grammars-and-editor-configuration-languages-2):包内 Monarch 语法和编辑器配置 | ✅ | ✅ | — | [语言](domains/languages.md) | | `languageServers` | 1 | `languageServers[]`(启动字段之外的部分) | ✅ | ✅ | ✅ | [语言](domains/languages.md) | | `languageServers` | 2 | [`workspaceConfiguration`](../contract/v1/README.md#workspace-configuration-languageservers-2):向语言服务器推送工作区配置 | ✅ | ✅ | ✅ | [语言](domains/languages.md) | +| `languageServers` | 3 | [插件自有服务器的会话字段](../contract/v1/README.md#plugin-owned-sessions-languageservers-3):`requestMethods`、`commands`、`serializedCommands`、`forwardNotifications`、`state`、`remote`,供后端经[语言服务器宿主方法](../contract/v1/README.md#language-server-host-methods)与自己的服务器交谈 | ✅ | ✅ | — | [语言](domains/languages.md) | | `serverProcess` | 1 | 进程启动字段:`launch`、`arguments`、`validationArguments`、`runtimeDependencies`、`initializationOptions`;语言服务器、自有进程的构建提供者、VCS 提供者、插件后端共用 | ✅ | ✅ | ✅ | [语言](domains/languages.md) | | `serverProcess` | 2 | `runSelectedRuntime`,供[调试适配器](../contract/v1/README.md#api-level-4-debug-adapters)使用;需要级别 4 | ✅ | — | — | [调试适配器](domains/debug-adapters.md) | +| `serverProcess` | 3 | [后端决定启动方式](../contract/v1/README.md#backend-resolved-launch-serverprocess-3):语言服务器的 `launch.kind: "backend"`,命令行由插件后端在每次启动时回答;只有语言服务器能用,需要 `backend` 2 和插件 API 级别 4 | ✅ | ✅ | — | [语言](domains/languages.md) | +| `serverProcess` | 4 | [按工具定义 ID 引用运行时](../contract/v1/README.md#tool-references-serverprocess-4):清单 `requiresTools` 列出工具定义 ID,`launch.tool`、`runtimeDependencies[].tool`、`runSelectedRuntime.tool` 按它们引用,不再用宿主的封闭运行时列表 | ✅ | — | — | [语言](domains/languages.md) | | `executionProviders` | 1 | `executionProviders[]`:运行和测试提供者 | ✅ | ✅ | — | [语言](domains/languages.md) | | `settings` | 1 | `settings[]` 与 `settingActions[]` | ✅ | ✅ | ✅ | [设置](domains/settings.md) | | `settings` | 2 | [放置位置、作用域、生效方式、页面按钮](../contract/v1/README.md#settings-v2-placement-scope-effect-and-page-buttons) | ✅ | ✅ | ✅ | [设置](domains/settings.md) | @@ -75,6 +78,8 @@ | `vcsProviders` | 1 | [`vcsProviders[]`](../contract/v1/README.md#api-level-3-vcs-providers) | ✅ | ✅ | — | [VCS](domains/vcs.md) | | `debugAdapters` | 1 | [`debugAdapters[]`](../contract/v1/README.md#api-level-4-debug-adapters);需要级别 4 | ✅ | ✅ | — | [调试适配器](domains/debug-adapters.md) | | `debugAdapters` | 2 | 调试目标的参数和环境变量 | ✅ | ✅ | — | [调试适配器](domains/debug-adapters.md) | +| `debugAdapters` | 3 | [适配器声明的 `features` 与 `defaults`](../contract/v1/README.md#adapter-features-and-defaults-debugadapters-3):热代码替换、步进过滤器及其默认值 | ✅ | ✅ | — | [调试适配器](domains/debug-adapters.md) | +| `outputLinkMatchers` | 1 | [`outputLinkMatchers[]`](../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1):插件声明的匹配规则让 Run 和测试控制台的输出出现链接;Core 负责匹配,`resolve` 函数在点击时才调用 | ✅ | ✅ | — | [语言](domains/languages.md) | | `projectGenerators` | 1 | [`projectGenerators[]`](../contract/v1/README.md#project-generators):新建项目窗口里的条目 | ✅ | ✅ | — | [项目生成器](domains/project-generators.md) | | `projectGenerators` | 2 | [`tool` 字段](../contract/v1/README.md#tool-fields):从插件声明的工具的已有安装里选一个,可存为新项目的工具固定;不能再用 `runtime` 字段 | ✅ | ✅ | — | [项目生成器](domains/project-generators.md) | | `backend` | 1 | [插件后端](../contract/v1/README.md#plugin-backend):插件自己的长驻进程,从插件页面启动 | ✅ | ✅ | — | [插件后端](domains/backend.md) | @@ -82,12 +87,15 @@ | `packageTrust` | 1 | `vendor.bundledSignatureRequirement`:随宿主发布的发布者签名插件副本按来源信任 | ✅ | ✅ | — | [包信任](domains/package-trust.md) | | `toolDefinitions` | 1 | [工具身份、布局、发现和四层候选](../contract/v1/toolchains/README.md) | ✅ | ✅(只读设置、构建与运行工具) | — | [工具链](domains/toolchains.md) | | `toolchainProviders` | 1 | [工具链提供者声明和按权限校验应答](../contract/v1/toolchains/README.md) | ✅ | ✅(只读设置、构建与运行工具) | — | [工具链](domains/toolchains.md) | +| `pluginFunctions` | 1 | [跨插件调用](../contract/v1/README.md#cross-plugin-calls-protocol-version-4):清单顶层 `pluginFunctions` 段声明导出的函数(`exports`)和要调用的其他插件函数(`calls`);需要插件 API 级别 3 及以上、`backend` 2、`serverProcess` 1,不授予任何清单权限;调用经 Core 中转,调用方后端需协商函数协议 4 | ✅ | ✅(中转调用) | — | [插件后端](domains/backend.md) | | `executableResolution` | 1 | [共享可执行文件候选](../contract/v1/README.md#shared-executable-candidates-executableresolution-12):候选排序和严格选择 | ✅ | ✅ | — | [工具链](domains/toolchains.md) | | `executableResolution` | 2 | 工具定义候选、项目/子项目工具链注册表绑定;不改变已有构建候选集 | ✅ | ✅(只读设置、构建与运行工具) | — | [工具链](domains/toolchains.md) | | `keymapImportProviders` | 1 | [快捷键导入提供者](../contract/v1/README.md#shortcut-import-providers-core-foundation) | ✅ | ✅ | — | [快捷键导入](domains/keymap-import.md) | `requires` 必须**恰好**列出用到的能力:漏一个或多列一个都是 `plugin.manifest.invalid`。每个语言服务器、每个自有进程的构建提供者、每个 VCS 提供者和 `backend` 都要带上 `serverProcess`。 +macOS 目前只声明 `serverProcess` 1 和 3:版本 4 已由 Core 校验,但宿主尚未声明,需要它的插件版本暂时装不上。Windows 不声明本页新增的 `languageServers` 3、`serverProcess` 3 和 4、`debugAdapters` 3、`outputLinkMatchers` 1、`pluginFunctions` 1。 + ### 进程协议版本 这些协议在进程启动时协商,不写进清单。宿主在 `initialize` 的 `capabilities.experimental.<协议>` 里给出 `{ "versions": [...] }`,后端回答 `{ "version": N }`。回答了宿主没给出的版本,或格式不对,该协议整个不启用。 @@ -96,7 +104,8 @@ | --- | --- | --- | --- | --- | | `litheFunctions` | 1 | 插件函数:注册、撤回、调用、结果、进度、取消;不提供宿主服务,所有 `lithe/host/*` 请求返回 `plugin.host.unavailable` | 插件拥有的会话 | [Plugin functions](../contract/v1/README.md#plugin-functions) | | `litheFunctions` | 2 | 宿主服务:凭据 `credentials/get`、`set`、`delete` | 同上 | [Host services](../contract/v1/README.md#host-services-protocol-version-2) | -| `litheFunctions` | 3 | 工作区上下文与能力发现、宿主通知、公共文档/文件/窗口方法、独立编辑器动作、插件自有原生终端 | 同上 | [Protocol 3](../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | +| `litheFunctions` | 3 | 工作区上下文与能力发现、宿主通知、公共文档/文件/窗口方法、独立编辑器动作、编辑器语言功能 Provider、插件自有原生终端 | 同上 | [Protocol 3](../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | +| `litheFunctions` | 4 | 跨插件调用:后端经 `functions/providers` 列出导出函数的插件、经 `functions/invoke` 调用它们;被调函数收到 Core 写入的 `caller`,嵌套调用用 `parentOperationID` 指明所属调用链(对应被服务调用的 `operationID`) | 同上 | [Cross-plugin calls](../contract/v1/README.md#cross-plugin-calls-protocol-version-4) | | `litheBuild` | 1 | `lithe/build/model` 与 `lithe/build/dependencies` | 构建提供者 | [Build providers](../contract/v1/README.md#build-providers) | | `litheBuild` | 2 | 类型化任务执行(`lithe/build/execute`) | 同上 | [Task execution](../contract/v1/README.md#provider-task-execution-buildproviders-2) | | `litheBuild` | 3 | 类型化测试执行和发现(`lithe/build/test`、`lithe/build/discoverTests`) | 同上 | [Test execution](../contract/v1/README.md#provider-test-execution-buildproviders-3) | @@ -115,7 +124,8 @@ - `litheBuild` 各版本新增的内容与同号的 `buildProviders` 能力版本一致。 - `litheFunctions` 3 内部还有两个子版本号:宿主服务版本 `HOST_SERVICES_VERSION = 2`、上下文版本 `CONTEXT_VERSION = 3`,SDK 用它们判断能调用哪些宿主方法;运行时以 `lithe/host/getCapabilities` 的返回为准。 -- 宿主侧:上表的协议都由 Rust Core 实现;macOS 运行全部会话类型。Windows 没有插件后端宿主,也不运行构建提供者和 VCS 提供者。 +- `litheFunctions` 4 保留版本 3 的全部内容,只新增跨插件中转;协商到版本 3 或更低的后端不能调用 `functions/*`;它导出的函数仍可被中转调用,但收不到 `caller`。 +- 宿主侧:上表的协议都由 Rust Core 实现,Core 支持 `litheFunctions` 1–4;macOS 运行全部会话类型并中转跨插件调用。Windows 没有插件后端宿主(不提供 `backend`),也不运行构建提供者和 VCS 提供者。 ### 宿主与官方 SDK 对照 @@ -123,7 +133,7 @@ | 协议 | Core 提供 | Node SDK 0.3.0 | Java SDK 0.3.0 | Rust SDK 0.2.0 | | --- | --- | --- | --- | --- | -| `litheFunctions` | 1、2、3 | 3 | 3 | 3、2、1 | +| `litheFunctions` | 1、2、3、4 | 3 | 3 | 3、2、1 | | `litheBuild` | 1–11 | 1–6 | 1–6 | — | | `litheVcs` | 1 | 1 | — | — | | `litheKeymapImport` | 1 | 1 | 1 | 1 | @@ -131,6 +141,7 @@ | `litheLogging` | 1 | 1(必需) | 1(必需) | 1(必需) | - Node 和 Java SDK 的构建方法按协商结果启用:`model`、`dependencies` 需要 1,`execute` 需要 2,`test`、`discoverTests` 需要 3,`prepareRun` 需要 4,`prepareDebug` 需要 5。版本 7–11 的内容目前没有 SDK 封装。 +- 宿主支持函数协议 1–4,三个官方 SDK(Node、Java、Rust)目前只协商 1–3,所以用它们构建的后端不会协商到协议 4。协议 4 的 `functions/*` 和 `caller`,以及协议 3 宿主方法中的 `run/*`、`debug/start`、`languageServer/*`、`documents/registerContentProvider`,目前都没有 SDK 封装;插件可以先用 SDK 的通用宿主请求调用这些协议 3 的方法。 - 当前 SDK 要求宿主提供 `litheLogging` 1,否则拒绝初始化;用这些 SDK 构建的后端只能在提供该协议的 Lithe 版本中启动。 ### SDK 发版历史 @@ -164,7 +175,7 @@ ## English -This page puts every version number a plugin meets in one place: **every version** of each capability, each process protocol, and each document format, and how far Rust Core, macOS, Windows, and the three official SDKs support each. [`contract/v1/README.md`](../contract/v1/README.md) is normative; where this page disagrees with it, the contract wins, and please open an issue. This page tracks the latest `preview` of the Lithe repository: the Core, macOS, and Windows columns are checked against the code on `preview` (2026-11-05, `644afdaac`), and contract links follow the revision pinned in [`contract.lock.json`](../contract.lock.json). A stable Lithe release may lack some of these versions, so check your target version's release notes before publishing. +This page puts every version number a plugin meets in one place: **every version** of each capability, each process protocol, and each document format, and how far Rust Core, macOS, Windows, and the three official SDKs support each. [`contract/v1/README.md`](../contract/v1/README.md) is normative; where this page disagrees with it, the contract wins, and please open an issue. This page tracks the latest `preview` of the Lithe repository: the Core, macOS, and Windows columns are checked against the code on `preview` (2026-11-05, `5cc06d5f4`), and contract links follow the revision pinned in [`contract.lock.json`](../contract.lock.json). A stable Lithe release may lack some of these versions, so check your target version's release notes before publishing. ### Four kinds of version @@ -207,8 +218,11 @@ Each row is one version of one capability. The Core column is what Core can vali | `languages` | 2 | [`grammar` and `configuration`](../contract/v1/README.md#grammars-and-editor-configuration-languages-2): packaged Monarch grammars and editor configuration | ✅ | ✅ | — | [Languages](domains/languages.md) | | `languageServers` | 1 | `languageServers[]` apart from their launch fields | ✅ | ✅ | ✅ | [Languages](domains/languages.md) | | `languageServers` | 2 | [`workspaceConfiguration`](../contract/v1/README.md#workspace-configuration-languageservers-2): workspace configuration pushed to the server | ✅ | ✅ | ✅ | [Languages](domains/languages.md) | +| `languageServers` | 3 | [Session fields for a plugin's own server](../contract/v1/README.md#plugin-owned-sessions-languageservers-3): `requestMethods`, `commands`, `serializedCommands`, `forwardNotifications`, `state`, `remote`, so a backend can talk to its own server through the [language server host methods](../contract/v1/README.md#language-server-host-methods) | ✅ | ✅ | — | [Languages](domains/languages.md) | | `serverProcess` | 1 | Launch fields `launch`, `arguments`, `validationArguments`, `runtimeDependencies`, `initializationOptions`, shared by language servers, own-process build providers, VCS providers, and the plugin backend | ✅ | ✅ | ✅ | [Languages](domains/languages.md) | | `serverProcess` | 2 | `runSelectedRuntime` for [debug adapters](../contract/v1/README.md#api-level-4-debug-adapters); needs level 4 | ✅ | — | — | [Debug adapters](domains/debug-adapters.md) | +| `serverProcess` | 3 | [Backend-resolved launch](../contract/v1/README.md#backend-resolved-launch-serverprocess-3): a language server's `launch.kind: "backend"`, whose command line the plugin backend answers at each start; only language servers can use it, and it needs `backend` 2 and plugin API level 4 | ✅ | ✅ | — | [Languages](domains/languages.md) | +| `serverProcess` | 4 | [Runtimes named by tool-definition ID](../contract/v1/README.md#tool-references-serverprocess-4): the manifest's `requiresTools` lists tool-definition IDs, which `launch.tool`, `runtimeDependencies[].tool`, and `runSelectedRuntime.tool` refer to instead of the host's closed runtime list | ✅ | — | — | [Languages](domains/languages.md) | | `executionProviders` | 1 | `executionProviders[]`: run and test providers | ✅ | ✅ | — | [Languages](domains/languages.md) | | `settings` | 1 | `settings[]` and `settingActions[]` | ✅ | ✅ | ✅ | [Settings](domains/settings.md) | | `settings` | 2 | [Placement, scope, effect, page buttons](../contract/v1/README.md#settings-v2-placement-scope-effect-and-page-buttons) | ✅ | ✅ | ✅ | [Settings](domains/settings.md) | @@ -235,6 +249,8 @@ Each row is one version of one capability. The Core column is what Core can vali | `vcsProviders` | 1 | [`vcsProviders[]`](../contract/v1/README.md#api-level-3-vcs-providers) | ✅ | ✅ | — | [VCS](domains/vcs.md) | | `debugAdapters` | 1 | [`debugAdapters[]`](../contract/v1/README.md#api-level-4-debug-adapters); needs level 4 | ✅ | ✅ | — | [Debug adapters](domains/debug-adapters.md) | | `debugAdapters` | 2 | Debug target arguments and environment | ✅ | ✅ | — | [Debug adapters](domains/debug-adapters.md) | +| `debugAdapters` | 3 | [Adapter `features` and `defaults`](../contract/v1/README.md#adapter-features-and-defaults-debugadapters-3): hot code replace, step filters, and their defaults | ✅ | ✅ | — | [Debug adapters](domains/debug-adapters.md) | +| `outputLinkMatchers` | 1 | [`outputLinkMatchers[]`](../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1): plugin-declared patterns that turn Run and Test console output into links; Core does the matching, and the `resolve` function is called only on click | ✅ | ✅ | — | [Languages](domains/languages.md) | | `projectGenerators` | 1 | [`projectGenerators[]`](../contract/v1/README.md#project-generators): entries of the New Project window | ✅ | ✅ | — | [Project generators](domains/project-generators.md) | | `projectGenerators` | 2 | [`tool` fields](../contract/v1/README.md#tool-fields): pick an existing installation of a plugin-declared tool, optionally saved as the new project's tool pin; `runtime` fields are no longer allowed | ✅ | ✅ | — | [Project generators](domains/project-generators.md) | | `backend` | 1 | [Plugin backend](../contract/v1/README.md#plugin-backend): the plugin's own long-lived process, started from its pages | ✅ | ✅ | — | [Plugin backend](domains/backend.md) | @@ -242,12 +258,15 @@ Each row is one version of one capability. The Core column is what Core can vali | `packageTrust` | 1 | `vendor.bundledSignatureRequirement`: origin-scoped trust for a host-sealed bundled copy of a publisher-signed plugin | ✅ | ✅ | — | [Package trust](domains/package-trust.md) | | `toolDefinitions` | 1 | [Tool identities, layouts, discovery, and four-layer candidates](../contract/v1/toolchains/README.md) | ✅ | ✅ (read-only settings, Build and run tools) | — | [Toolchains](domains/toolchains.md) | | `toolchainProviders` | 1 | [Provider declarations and permission-bound answer validation](../contract/v1/toolchains/README.md) | ✅ | ✅ (read-only settings, Build and run tools) | — | [Toolchains](domains/toolchains.md) | +| `pluginFunctions` | 1 | [Cross-plugin calls](../contract/v1/README.md#cross-plugin-calls-protocol-version-4): the manifest's top-level `pluginFunctions` section declares the functions the plugin exports (`exports`) and the functions of other plugins it calls (`calls`); needs plugin API level 3 or later, `backend` 2, and `serverProcess` 1, grants no manifest permission; Core relays the calls, and a calling backend must negotiate function protocol 4 | ✅ | ✅ (relays calls) | — | [Plugin backend](domains/backend.md) | | `executableResolution` | 1 | [Shared executable candidates](../contract/v1/README.md#shared-executable-candidates-executableresolution-12): candidate ordering and strict selection | ✅ | ✅ | — | [Toolchains](domains/toolchains.md) | | `executableResolution` | 2 | Tool-definition candidates and project/subproject toolchain registry bindings, without changing existing build candidate sets | ✅ | ✅ (read-only settings, Build and run tools) | — | [Toolchains](domains/toolchains.md) | | `keymapImportProviders` | 1 | [Shortcut import providers](../contract/v1/README.md#shortcut-import-providers-core-foundation) | ✅ | ✅ | — | [Shortcut import](domains/keymap-import.md) | `requires` lists **exactly** the capabilities a manifest uses: leaving one out or listing an unused one is `plugin.manifest.invalid`. Every language server, own-process build provider, VCS provider, and `backend` also needs `serverProcess`. +macOS currently advertises only `serverProcess` 1 and 3: version 4 is validated by Core, but the host does not advertise it yet, so a plugin version that needs it cannot be installed for now. Windows advertises none of the versions this page adds: `languageServers` 3, `serverProcess` 3 and 4, `debugAdapters` 3, `outputLinkMatchers` 1, and `pluginFunctions` 1. + ### Process protocol versions These protocols are negotiated when a process starts and never appear in the manifest. The host offers `{ "versions": [...] }` under `capabilities.experimental.` in `initialize`; the backend answers `{ "version": N }`. An answer with a version the host did not offer, or a malformed answer, leaves the whole protocol disabled. @@ -256,7 +275,8 @@ These protocols are negotiated when a process starts and never appear in the man | --- | --- | --- | --- | --- | | `litheFunctions` | 1 | Plugin functions: register, withdraw, invoke, results, progress, cancellation; no host services, every `lithe/host/*` request is `plugin.host.unavailable` | Plugin-owned sessions | [Plugin functions](../contract/v1/README.md#plugin-functions) | | `litheFunctions` | 2 | Host services: credentials `credentials/get`, `set`, `delete` | Same | [Host services](../contract/v1/README.md#host-services-protocol-version-2) | -| `litheFunctions` | 3 | Workspace contexts and capability discovery, host notifications, public document/file/window methods, independent editor actions, plugin-owned native terminals | Same | [Protocol 3](../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | +| `litheFunctions` | 3 | Workspace contexts and capability discovery, host notifications, public document/file/window methods, independent editor actions, editor language-feature providers, plugin-owned native terminals | Same | [Protocol 3](../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | +| `litheFunctions` | 4 | Cross-plugin calls: a backend lists the plugins that export a function with `functions/providers` and calls them with `functions/invoke`; the called function receives a Core-written `caller`, and a nested call names its call chain with `parentOperationID` (the `operationID` of the call being served) | Same | [Cross-plugin calls](../contract/v1/README.md#cross-plugin-calls-protocol-version-4) | | `litheBuild` | 1 | `lithe/build/model` and `lithe/build/dependencies` | Build providers | [Build providers](../contract/v1/README.md#build-providers) | | `litheBuild` | 2 | Typed task execution (`lithe/build/execute`) | Same | [Task execution](../contract/v1/README.md#provider-task-execution-buildproviders-2) | | `litheBuild` | 3 | Typed test execution and discovery (`lithe/build/test`, `lithe/build/discoverTests`) | Same | [Test execution](../contract/v1/README.md#provider-test-execution-buildproviders-3) | @@ -275,7 +295,8 @@ These protocols are negotiated when a process starts and never appear in the man - Each `litheBuild` version adds what the `buildProviders` capability version of the same number adds. - `litheFunctions` 3 carries two sub-versions: host services `HOST_SERVICES_VERSION = 2` and contexts `CONTEXT_VERSION = 3`, which SDKs use to decide which host methods they may call; at runtime, `lithe/host/getCapabilities` is authoritative. -- Hosts: Rust Core implements every protocol above, and macOS runs every session kind. Windows has no plugin backend host and runs no build or VCS providers. +- `litheFunctions` 4 keeps everything in version 3 and only adds the cross-plugin relay; a backend that negotiated version 3 or lower cannot call `functions/*`; the functions it exports can still be called through the relay, but it never receives `caller`. +- Hosts: Rust Core implements every protocol above and supports `litheFunctions` 1–4, and macOS runs every session kind and relays cross-plugin calls. Windows has no plugin backend host (it does not offer `backend`) and runs no build or VCS providers. ### Host and official SDKs @@ -283,7 +304,7 @@ These protocols are negotiated when a process starts and never appear in the man | Protocol | Core offers | Node SDK 0.3.0 | Java SDK 0.3.0 | Rust SDK 0.2.0 | | --- | --- | --- | --- | --- | -| `litheFunctions` | 1, 2, 3 | 3 | 3 | 3, 2, 1 | +| `litheFunctions` | 1, 2, 3, 4 | 3 | 3 | 3, 2, 1 | | `litheBuild` | 1–11 | 1–6 | 1–6 | — | | `litheVcs` | 1 | 1 | — | — | | `litheKeymapImport` | 1 | 1 | 1 | 1 | @@ -291,6 +312,7 @@ These protocols are negotiated when a process starts and never appear in the man | `litheLogging` | 1 | 1 (required) | 1 (required) | 1 (required) | - The Node and Java SDKs enable each build method from the negotiated version: `model` and `dependencies` need 1, `execute` 2, `test` and `discoverTests` 3, `prepareRun` 4, `prepareDebug` 5. Versions 7–11 have no SDK wrappers yet. +- The host supports function protocols 1–4, while the three official SDKs (Node, Java, Rust) negotiate only 1–3 today, so a backend built on them never negotiates protocol 4. The protocol 4 `functions/*` methods and `caller` field, and the protocol 3 host methods `run/*`, `debug/start`, `languageServer/*`, and `documents/registerContentProvider`, have no SDK wrappers yet; a plugin can call those protocol 3 methods through the SDK's generic host request meanwhile. - The current SDKs require the host to offer `litheLogging` 1 and refuse to initialize otherwise, so a backend built on them starts only in a Lithe release that offers it. ### SDK release history diff --git a/reference/domains/backend.md b/reference/domains/backend.md index 4ac2693..97c47f9 100644 --- a/reference/domains/backend.md +++ b/reference/domains/backend.md @@ -4,18 +4,18 @@ ## 中文 -插件后端是插件自己的长驻进程,用来运行网页做不到的事:数据库驱动、网络客户端、文件格式解析、调用宿主服务。它只服务插件自己,不属于任何构建系统、语言或工作区,也不回答 `lithe/build/*`、VCS 等领域请求。后端与宿主之间说插件函数协议(`litheFunctions`),从版本 2 起可以按权限请求宿主服务。 +插件后端是插件自己的长驻进程,用来运行网页做不到的事:数据库驱动、网络客户端、文件格式解析、调用宿主服务。它只服务插件自己,不属于任何构建系统、语言或工作区,也不回答 `lithe/build/*`、VCS 等领域请求。后端与宿主之间说插件函数协议(`litheFunctions`),从版本 2 起可以按权限请求宿主服务;版本 3 起可以启动由宿主拥有的 Run 会话和调试会话;版本 4 起可以经 Core 调用其他插件导出的函数。 | 项目 | 内容 | | --- | --- | -| 清单字段 | `backend`(与 `serverProcess` 的启动字段相同) | -| 能力版本 | `backend` 1–2、`serverProcess` 1;`ui` 4 的后端命令需要 `backend` 2,见[版本对照](../capabilities-and-versions.md#能力版本requires) | -| 协议 | `litheFunctions` 1–3、`litheLogging` 1,见[进程协议版本](../capabilities-and-versions.md#进程协议版本) | -| API 编号 | API-01、04、05、06、07、08、20、21、22、24、37、40、41,见 [API 总表](../api-catalog.md#现在能用的-api) | -| 权限 | `backend.launch`;宿主服务按方法另加权限(`credentials.store`、`workspace.metadata`、`workspace.files.read`、`workspace.files.watch`、`workspace.search`、`terminal.create` 等),见[已公开的宿主方法](../api-catalog.md#已公开的宿主方法) | -| 激活事件 | `backend` 1:插件网页第一次调用 `functions.*` 时启动。`backend` 2 另可用 `onCommand:` 或可选的 `onWorkspaceOpen` | +| 清单字段 | `backend`(与 `serverProcess` 的启动字段相同);跨插件调用另有 `pluginFunctions` 段(`exports`、`calls`) | +| 能力版本 | `backend` 1–2、`serverProcess` 1;`ui` 4 的后端命令需要 `backend` 2;跨插件调用需要 `pluginFunctions` 1(同时要求 `backend` 2、`serverProcess` 1,`pluginAPIVersion` ≥ 3);见[版本对照](../capabilities-and-versions.md#能力版本requires) | +| 协议 | `litheFunctions` 1–4、`litheLogging` 1,见[进程协议版本](../capabilities-and-versions.md#进程协议版本) | +| API 编号 | API-01、04、05、06、07、08、20、21、22、23、24、35、37、40、41,见 [API 总表](../api-catalog.md#现在能用的-api) | +| 权限 | `backend.launch`;宿主服务按方法另加权限(`credentials.store`、`workspace.metadata`、`workspace.files.read`、`workspace.files.watch`、`workspace.search`、`terminal.create`、`run.start` 等),见[已公开的宿主方法](../api-catalog.md#已公开的宿主方法)。跨插件调用没有清单权限:调用方的 `calls` 和被调方的 `exports` 就是双方的同意 | +| 激活事件 | `backend` 1:插件网页第一次调用 `functions.*` 时启动。`backend` 2 另可用 `onCommand:` 或可选的 `onWorkspaceOpen`。只导出函数的插件不需要网页、命令或 `onWorkspaceOpen`,被调用时启动后端;只调用的插件仍要有其中一种 | | Windows | 没有插件后端宿主,不声明 `backend` | -| 官方 SDK | [Node](../../sdk/node/README.md)、[Java](../../sdk/java/README.md)、[Rust](../../sdk/rust/README.md);各 SDK 支持的协议版本见[宿主与官方 SDK 对照](../capabilities-and-versions.md#宿主与官方-sdk-对照) | +| 官方 SDK | [Node](../../sdk/node/README.md)、[Java](../../sdk/java/README.md)、[Rust](../../sdk/rust/README.md);各 SDK 支持的协议版本见[宿主与官方 SDK 对照](../capabilities-and-versions.md#宿主与官方-sdk-对照)。三个 SDK 目前只协商函数协议 1–3,没有 `functions/*`、`run/*` 的封装,也收不到 `caller` 字段;协议 3 的方法可以先用 SDK 的通用宿主请求调用,协议 4 要等 SDK 支持后才能协商 | ### 版本对照 @@ -25,9 +25,102 @@ | `backend` 2 | 可以不依赖网页启动(网页、`ui.commands` 或 `onWorkspaceOpen` 至少一种);所有工作区共享一个进程,每次调用显式带工作区上下文 | [Backend 2: workspace contexts](../../contract/v1/README.md#backend-2-workspace-contexts) | | `litheFunctions` 1 | 注册、撤回、调用、结果、进度、取消;不提供宿主服务 | [Plugin functions](../../contract/v1/README.md#plugin-functions) | | `litheFunctions` 2 | 宿主服务:凭据 `credentials/get`、`set`、`delete` | [Host services (protocol version 2)](../../contract/v1/README.md#host-services-protocol-version-2) | -| `litheFunctions` 3 | 工作区上下文与能力发现、宿主通知、公共文件/文档/窗口方法、独立编辑器动作、插件自有原生终端 | [Protocol 3](../../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | +| `litheFunctions` 3 | 工作区上下文与能力发现、宿主通知、公共文件/文档/窗口方法、独立编辑器动作、插件自有原生终端、宿主拥有的 Run 与调试会话(`run/start`、`run/stop`、`debug/start`、通知 `run/didExit`) | [Protocol 3](../../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3)、[Run and debug sessions (protocol version 3)](../../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) | +| `litheFunctions` 4 | 保留版本 3 的全部内容,新增跨插件调用:`functions/providers`、`functions/invoke`,被调方收到顶层 `caller` | [Cross-plugin calls (protocol version 4)](../../contract/v1/README.md#cross-plugin-calls-protocol-version-4) | +| `pluginFunctions` 1 | 清单 `pluginFunctions` 段:导出(`exports`)和调用声明(`calls`);不授予权限 | [Declaring exports and calls](../../contract/v1/README.md#declaring-exports-and-calls) | | `litheLogging` 1 | 结构化诊断通知 `lithe/log`;当前官方 SDK 要求宿主提供 | [Structured plugin diagnostics](../../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1) | +### 跨插件调用(协议 4) + +插件函数默认只属于插件自己。插件导出函数后,另一个已启用、且在清单里声明了这次调用的插件,可以经 Rust Core 调用它:调用方的后端碰不到被调方的进程,被调方的后端也只知道自己正在服务的那次调用;宿主只在被调方的后端没有运行时负责启动它。中转是 Core 的逻辑,在所有运行插件后端的平台上一样;目前只有 macOS 运行插件后端,Windows 不声明 `pluginFunctions`。 + +#### 清单声明 + +| 项目 | 规则 | +| --- | --- | +| 前提 | `pluginAPIVersion` ≥ 3,有 `backend`,`requires` 写 `pluginFunctions: 1`、`backend: 2`、`serverProcess: 1`;`requires` 列出 `pluginFunctions` 当且仅当清单有 `pluginFunctions` 段 | +| 段内容 | `exports` 与 `calls` 至少有一个,各最多 32 项、按 `id` 排序、`id` 不重复;多余的字段是 `plugin.manifest.invalid` | +| 导出 | `{id, version, context}`。`id` 是函数 ID;`version` 是导出的契约版本(1–1000 的整数),参数或结果有不兼容变更就换新版本;`context` 目前只有 `"workspace"`,表示只在本地工作区上下文里运行,缺失或未知则整个清单被拒绝 | +| 调用 | `{id, version}`。只写函数 ID 和调用方对照的版本,不写被调方的插件 ID,运行时再发现提供者;两个版本必须相等,否则 `plugin.function.versionMismatch` | +| 注册 | 声明导出不等于注册:被调方的后端仍要 `lithe/function/register`,未注册或已撤回的函数是 `plugin.function.notExported`。清单没有导出的函数保持私有,引用它仍是 `plugin.function.wrongPlugin` | +| 宿主支持 | 宿主向声明了 `calls` 的插件开放 `functions` 宿主服务;不支持 `pluginFunctions` 的宿主拒绝这类清单(`plugin.api.unsupported`) | + +#### 宿主方法 + +只对协商到协议 4、且声明了至少一个 `calls` 的后端开放。协议 3 及以下返回 `-32601`、`plugin.host.unavailable`;没有声明任何 call 返回 `-32010`、`plugin.host.permissionDenied`。`getCapabilities` 在 `functions` 服务下列出这两个方法。 + +| 方法(前缀 `lithe/host/`) | 参数 | 结果 | +| --- | --- | --- | +| `functions/providers` | `{functionID}` | `{providers: [{pluginID, version}]}` | +| `functions/invoke` | `{pluginID, functionID, context: {contextID, generation}, params?, timeoutMilliseconds?, parentOperationID?}` | `{value}` | + +- `providers` 列出导出该函数、已启用、且不是调用方自己的插件,按插件 ID 排序,每项带导出的 `version`。调用方必须声明过该函数,否则 `plugin.function.notDeclared`。被调方的后端不必正在运行。 +- `invoke` 调用一个提供者。`context` 必填,是调用方从工作区服务得到的本地工作区上下文;`params` 是 JSON 对象,默认 `{}`;`timeoutMilliseconds` 是正整数,默认 60000,上限 600000;未定义的字段返回 `-32018`、`plugin.host.unsupportedOption`。`value` 是被调方的结果,`null` 也是结果。 +- 调用方用 `$/cancelRequest` 取消,得到 `plugin.host.cancelled`。 +- Core 按固定顺序检查(调用方声明、被调方启用、被调方导出、版本相等、上下文、本地工作区、参数大小、所属调用、调用链、深度、并发数),在向被调方写任何内容之前拒绝,第一个失败的检查决定错误码。 + +#### 被调方收到的请求 + +Core 把中转的调用以 `lithe/function/invoke` 转给被调方。和任何函数调用一样,请求顶层有 `functionID`、`operationID`、`context`、`params`;中转的调用另有顶层字段 `caller: {pluginID}`。 + +- `caller.pluginID` 由 Core 按调用会话的身份写入,在 `params` 之外,调用方无法提供:`functions/invoke` 请求带顶层 `caller` 会被 `-32018` 拒绝。要按调用方区分行为或保存状态的函数,读这个字段,不要读 `params` 里的插件 ID。调用方 `params` 里叫 `caller` 的键只是普通数据,原样传递。 +- 宿主自己发起的调用没有 `caller`,缺失表示宿主。协商到协议 3 及以下的被调方收不到这个字段;依赖它的被调方必须协商协议 4。 +- `operationID` 标识正在服务的这次调用。被调方在服务一次中转的调用时再发起 `functions/invoke`(嵌套调用),要把这个 `operationID` 作为 `parentOperationID` 传入,Core 才知道新调用属于哪一次。`parentOperationID` 是 1–128 字节的字符串,否则 `-32602`、`plugin.host.invalidParams`;不传时,Core 把新调用当作后端正在服务的每一次中转调用的子调用,所以后端应当总是传。 +- 调用链:新调用的链是父调用的链加上调用方。调用一个已在链上的插件(包括调用自己)是 `plugin.function.callCycle`,链最长 4 次调用。取消、超时或结束父调用,会取消它的子调用,各被调方收到 `$/cancelRequest`。`parentOperationID` 指向宿主直接发起的调用时,新调用开始一条新链;指向后端没有在服务的调用(已结束或不存在)时被拒绝,`plugin.host.cancelled`。 +- 被调方的后端没有运行时,Core 等待,并在调用方会话上发出运行时事件 `functionProviderStartRequired`,宿主按普通的插件启动流程启动它;30 秒内没有协商并注册好函数,调用以 `plugin.function.providerUnavailable` 结束。 + +#### 信任不传递 + +- 被调方用自己的权限、状态目录和凭据运行;调用方不获得被调方的任何权限,被调方也不获得调用方的。调用方的插件 ID 只出现在 `caller` 字段里,不写进被调方的会话。 +- 导出的函数不得接受调用方给的可执行路径、命令行或脚本去启动进程,也不得读写调用方指定的路径而不核对自己的访问范围。调用方需要被调方运行什么,函数应当接收一个由被调方自己解释的请求(任务名、文档),由被调方决定可执行文件。 +- 这不是安装依赖:安装调用方不会连带安装被调方,停用被调方只会让调用找不到提供者;不参与安装闭包,不做版本求解。远程工作区的上下文被拒绝,`plugin.host.authorityUnsupported`;导出的 `context: "workspace"` 只指本地工作区,后端仍在运行 Lithe 的机器上。 +- 停用、启用、替换和卸载插件对中转立即生效,不需要重启。macOS 只在启动时注册后端插件,所以新安装或更新的后端插件要重启 Lithe 后才会出现在 `providers` 里。 + +#### 错误码 + +| 错误码 | 何时 | +| --- | --- | +| `plugin.function.notDeclared` | 调用方没有声明该函数;调用进行中调用方更新后不再声明,也是它 | +| `plugin.function.providerDisabled` | 被调方没有安装或未启用;调用进行中被调方被停用、卸载,或更新后不再导出该函数或改了版本,也是它 | +| `plugin.function.notExported` | 被调方的后端没有注册该函数,或已撤回 | +| `plugin.function.versionMismatch` | 调用方声明的版本与被调方导出的版本不相等 | +| `plugin.function.callCycle` | 被调方已在调用链上,包括调用自己 | +| `plugin.function.depthExceeded` | 调用链超过 4 次调用 | +| `plugin.function.tooManyCalls` | 调用方已有 8 个中转调用在运行 | +| `plugin.function.payloadTooLarge` | 参数超过 256 KiB,或被调方的结果超过 256 KiB(超过 1 MiB 是 `invalidResult`) | +| `plugin.function.providerUnavailable` | 被调方的后端 30 秒内没有就绪;或调用期间后端退出、撤回了该函数 | + +被调方自己的失败保留原意:函数回答了错误时是 `plugin.function.failed`(`-32070`),`details.pluginErrorCode` 是被调方的 `error.data.code`;没有 `result`,或结果序列化后超过 1 MiB,是 `plugin.function.invalidResult`(`-32071`)。工作区上下文被关闭或换代是 `plugin.host.contextExpired`,截止时间到了是 `plugin.host.timeout`。Core 不重启后端,也不重试调用。 + +### Run 与调试会话(协议 3) + +后端可以启动宿主当作自己的会话呈现的运行或调试:后端决定启动什么,宿主拥有进程、控制台、停止按钮和会话的结束。Run 会话只增加宿主呈现(Run 窗口里的条目、控制台、退出状态),不授予其他能力。`run/start` 与 `debug/start` 带 `context: {contextID, generation}` 和对应权限;`run/stop` 不带 `context`,所以启动它的上下文关闭后仍能停止。 + +| 方法(前缀 `lithe/host/`) | 权限 | 参数 | 结果 | +| --- | --- | --- | --- | +| `run/start` | `run.start` | `context, label, process, temporaryFiles?, debug?, rerun?` | `{runID}` | +| `run/stop` | `run.start` | `{runID}` 或 `{sessionID}` | `null` | +| `debug/start` | `debug.start` | `context, label, adapter, request, configuration` | `{sessionID}`,见[调试适配器](debug-adapters.md#由插件后端发起调试会话) | + +通知(后端不回复):`lithe/host/run/didExit`,参数 `{runID, exitCode?, signal?}`,授予条件是 `run.start`。`run.start` 需要 `backend` 2,授予 `run/start`、`run/stop` 和 `run/didExit`。 + +`run/start` 的参数: + +- `label`:1–256 个字符,不含控制字符,作为 Run 窗口条目的标题。 +- `process`:`{executable, arguments, workingDirectory, environment}`,四个字段都必填,未知字段被拒绝。`executable` 是 `{kind: "tool", tool}`(`tool` 是清单 `requiresTools` 列出的工具定义 ID,宿主按[工具链](toolchains.md)的分层解析)或 `{kind: "packaged", path}`(插件包内文件的规范化相对路径)。参数、工作目录(相对工作区根、且是其中已存在的目录)和环境的上限同[运行准备](../../contract/v1/README.md#provider-run-preparation-buildproviders-4)。 +- `temporaryFiles`:最多 4 个 `{name, contents}`,每个文本最多 4 MiB。宿主在自己的临时目录写入,运行结束时删除,并把 `process.arguments` 和 `process.environment` 里的 `${temporaryFile:}` 替换成文件路径,插件不需要知道临时目录在哪。`name` 唯一,1–64 个字母、数字、`.`、`_`、`-`,以字母或数字开头。 +- `debug`:`{configurationType, target}`,让这次运行成为调试会话,见[调试适配器](debug-adapters.md#由插件后端发起调试会话)。 +- `rerun`:`{function, data}`,插件已注册的函数和它的参数(最多 64 KiB 的 JSON 对象)。用户选“再次运行”时,宿主以 `data` 为参数、当前 `context` 调用该函数,后端再回答一次新的 `run/start`,所以重启由后端准备;没有 `rerun` 的条目不能再次运行。 + +其他规则: + +- `run/stop` 按 `runID` 停止运行,或按 `sessionID` 停止调试会话,已经结束时同样返回 `null`;取消宿主请求等同停止。后端只能停止自己启动的运行和会话。 +- 进程结束时宿主发 `run/didExit`,每次运行只发一次,只发给持有 `run.start` 的会话;`exitCode` 是退出状态,`signal`(1–127)是结束它的信号,宿主丢失进程时两个字段都没有。 +- 参数格式错误是 `plugin.host.invalidParams`,Core 的拒绝(`plugin.run.invalidStart`、`plugin.debug.invalidStart`)在 `details.code` 和 `details.field`;上下文已关闭是 `plugin.host.contextExpired`;`tool` 解析不到可执行文件是 `plugin.host.toolUnresolved`。 +- 插件被禁用或后端退出时,宿主保留运行中的进程,只是不再通知后端;重新启动的后端不会收到它没见过开始的运行的 `run/didExit`。 +- 想让 Run 控制台里的输出出现链接,用 `outputLinkMatchers` 1 声明匹配器:Core 负责匹配,`resolve` 函数在用户点击时才调用,见 [Output link matchers](../../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1)。 +- Windows 没有插件后端宿主,没有这些方法。 + ### 契约章节 | 章节 | 内容 | @@ -43,10 +136,13 @@ | [Invocation](../../contract/v1/README.md#invocation)、[Progress](../../contract/v1/README.md#progress) | 调用、结果、进度、取消 | | [Host services (protocol version 2)](../../contract/v1/README.md#host-services-protocol-version-2) | 凭据服务 | | [Workspace contexts and capability discovery (protocol version 3)](../../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | `getCapabilities`、上下文 `contextID`/`generation`、宿主失败码 | -| [Host notifications (protocol version 3)](../../contract/v1/README.md#host-notifications-protocol-version-3) | `workspace/didChangeContexts`、`subscriptions/notify`、`commands/didRun` | +| [Host notifications (protocol version 3)](../../contract/v1/README.md#host-notifications-protocol-version-3) | `workspace/didChangeContexts`、`subscriptions/notify`、`commands/didRun`、`run/didExit` | | [Extending version 3](../../contract/v1/README.md#extending-version-3) | 协议 3 如何不升版本地增加方法 | | [Public document, file and window methods (protocol version 3)](../../contract/v1/README.md#public-document-file-and-window-methods-protocol-version-3) | 文件读取、监听、搜索与窗口消息(文档和编辑器方法见[编辑器](editor.md)) | | [Plugin-owned native terminals (function protocol 3)](../../contract/v1/README.md#plugin-owned-native-terminals-function-protocol-3)、[Close, retained context and reopen](../../contract/v1/README.md#close-retained-context-and-reopen) | 插件自有原生终端 | +| [Run and debug sessions (protocol version 3)](../../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) | `run/start`、`run/stop`、`debug/start`、`run/didExit`:参数、限制、失败码 | +| [Cross-plugin calls (protocol version 4)](../../contract/v1/README.md#cross-plugin-calls-protocol-version-4) | `pluginFunctions` 清单声明、`functions/providers`、`functions/invoke`、检查顺序、调用链、`caller` 字段、未运行的提供者、热生效 | +| [Output link matchers (`outputLinkMatchers` 1)](../../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1) | Run 与测试控制台里的输出链接,`resolve` 函数在点击时调用 | | [Structured plugin diagnostics (logging protocol 1)](../../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1) | `lithe/log` 的字段和限制 | | [Compatibility](../../contract/v1/README.md#compatibility)、[Official backend SDK](../../contract/v1/README.md#official-backend-sdk) | 版本兼容规则与官方 SDK | | [Outcomes and error codes](../../contract/v1/README.md#outcomes-and-error-codes) | 结果与错误码 | @@ -59,8 +155,13 @@ | [`backend/backend-workspace.plugin.json`](../../contract/v1/fixtures/backend/backend-workspace.plugin.json) | `backend` 2:`onWorkspaceOpen` 启动,`workspace.metadata` | | [`backend/command-backend.plugin.json`](../../contract/v1/fixtures/backend/command-backend.plugin.json) | `backend` 2 加 `ui` 4:不打开工具窗口的命令,读文档并编辑 | | [`backend/terminal-backend.plugin.json`](../../contract/v1/fixtures/backend/terminal-backend.plugin.json) | `backend` 2:插件自有原生终端的最小清单 | -| [`backend/function-protocol.json`](../../contract/v1/fixtures/backend/function-protocol.json) | 插件函数协议的标准报文:协商、注册、调用、结果、进度、42 个宿主请求、18 个宿主失败、10 个宿主通知 | +| [`backend/function-protocol.json`](../../contract/v1/fixtures/backend/function-protocol.json) | 插件函数协议的标准报文:协商(含协议 4)、注册、调用、结果、进度、93 个宿主请求、23 个宿主失败、29 个宿主通知,以及被中转调用的 `caller`(`invocationCallers`)和中转失败码(`relayErrors`) | +| [`backend/plugin-functions-exporter.plugin.json`](../../contract/v1/fixtures/backend/plugin-functions-exporter.plugin.json) | `pluginFunctions` 1:只导出两个函数(`document.format` 版本 1、`symbols.list` 版本 2,`context` 为 `workspace`),没有网页、命令或 `onWorkspaceOpen` | +| [`backend/plugin-functions-caller.plugin.json`](../../contract/v1/fixtures/backend/plugin-functions-caller.plugin.json) | `pluginFunctions` 1:声明调用 `document.format` 版本 1,`onWorkspaceOpen` 启动 | | [`backend/plugin-logging.json`](../../contract/v1/fixtures/backend/plugin-logging.json) | `litheLogging` 1:提供与回答、通知、拒绝用例、级别 | +| [`run/run-start.json`](../../contract/v1/fixtures/run/run-start.json) | `run/start` 的参数和 `run.decodeStart` 对每个用例的返回,共 17 个用例(工具与包内可执行文件、`debug` 与 `rerun`、各种拒绝);临时文件依赖宿主的临时目录,由 Core 自己的测试覆盖 | +| [`run/tool-reference-launch.plugin.json`](../../contract/v1/fixtures/run/tool-reference-launch.plugin.json) | `run-start.json` 使用的清单:`requiresTools` 列出工具 `interpreter`,声明 `run.start`、`debug.start`,并带 `outputLinkMatchers` 1 | +| [`run/output-link-matchers.json`](../../contract/v1/fixtures/run/output-link-matchers.json) | `outputLinks.match` 的请求与返回的链接,以及匹配器 `resolve` 函数的回复经 `outputLinks.decodeResolution` 得到的结果 | 后端相关的拒绝用例在 [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json)。 @@ -74,18 +175,18 @@ ## English -A plugin backend is the plugin's own long-lived process. It runs what a web page cannot: database drivers, network clients, file-format parsers, calls to host services. It serves only its plugin, belongs to no build system, language, or workspace, and never answers domain requests such as `lithe/build/*` or VCS. Backend and host speak the plugin-function protocol (`litheFunctions`); from version 2 on, the backend may request host services its permissions grant. +A plugin backend is the plugin's own long-lived process. It runs what a web page cannot: database drivers, network clients, file-format parsers, calls to host services. It serves only its plugin, belongs to no build system, language, or workspace, and never answers domain requests such as `lithe/build/*` or VCS. Backend and host speak the plugin-function protocol (`litheFunctions`); from version 2 on, the backend may request host services its permissions grant; from version 3 on, it may start Run and debug sessions that the host owns; and from version 4 on, it may call functions that other plugins export, through Core. | Item | Value | | --- | --- | -| Manifest field | `backend` (the same launch fields as `serverProcess`) | -| Capability versions | `backend` 1–2, `serverProcess` 1; `ui` 4 backend commands need `backend` 2; see [versions](../capabilities-and-versions.md#capability-versions-requires) | -| Protocols | `litheFunctions` 1–3, `litheLogging` 1; see [process protocol versions](../capabilities-and-versions.md#process-protocol-versions) | -| API IDs | API-01, 04, 05, 06, 07, 08, 20, 21, 22, 24, 37, 40, 41; see the [API catalog](../api-catalog.md#apis-usable-now) | -| Permissions | `backend.launch`; host services need their own permission per method (`credentials.store`, `workspace.metadata`, `workspace.files.read`, `workspace.files.watch`, `workspace.search`, `terminal.create`, …); see [public host methods](../api-catalog.md#public-host-methods) | -| Activation | `backend` 1 starts on the first `functions.*` call from the plugin's page. `backend` 2 may also start on `onCommand:` or the optional `onWorkspaceOpen` | +| Manifest field | `backend` (the same launch fields as `serverProcess`); cross-plugin calls add a `pluginFunctions` section (`exports`, `calls`) | +| Capability versions | `backend` 1–2, `serverProcess` 1; `ui` 4 backend commands need `backend` 2; cross-plugin calls need `pluginFunctions` 1 (which also requires `backend` 2 and `serverProcess` 1, and `pluginAPIVersion` ≥ 3); see [versions](../capabilities-and-versions.md#capability-versions-requires) | +| Protocols | `litheFunctions` 1–4, `litheLogging` 1; see [process protocol versions](../capabilities-and-versions.md#process-protocol-versions) | +| API IDs | API-01, 04, 05, 06, 07, 08, 20, 21, 22, 23, 24, 35, 37, 40, 41; see the [API catalog](../api-catalog.md#apis-usable-now) | +| Permissions | `backend.launch`; host services need their own permission per method (`credentials.store`, `workspace.metadata`, `workspace.files.read`, `workspace.files.watch`, `workspace.search`, `terminal.create`, `run.start`, …); see [public host methods](../api-catalog.md#public-host-methods). Cross-plugin calls have no manifest permission: the caller's `calls` and the provider's `exports` are the consent of the two sides | +| Activation | `backend` 1 starts on the first `functions.*` call from the plugin's page. `backend` 2 may also start on `onCommand:` or the optional `onWorkspaceOpen`. A plugin that only exports functions needs no page, command, or `onWorkspaceOpen`: a call starts its backend; a plugin that only calls still needs one of them | | Windows | No plugin backend host; `backend` is not advertised | -| Official SDKs | [Node](../../sdk/node/README.md), [Java](../../sdk/java/README.md), [Rust](../../sdk/rust/README.md); protocol versions per SDK are in [Host and official SDKs](../capabilities-and-versions.md#host-and-official-sdks) | +| Official SDKs | [Node](../../sdk/node/README.md), [Java](../../sdk/java/README.md), [Rust](../../sdk/rust/README.md); protocol versions per SDK are in [Host and official SDKs](../capabilities-and-versions.md#host-and-official-sdks). The three SDKs currently negotiate function protocol 1–3 only: they have no wrappers for `functions/*` or `run/*` and never receive the `caller` field. Protocol 3 methods can already be called through the SDK's generic host request; protocol 4 can be negotiated once an SDK supports it | ### Versions compared @@ -95,9 +196,102 @@ A plugin backend is the plugin's own long-lived process. It runs what a web page | `backend` 2 | Starts without a page (a web window, `ui.commands`, or `onWorkspaceOpen`, at least one); one process shared by every workspace, each call names its workspace context | [Backend 2: workspace contexts](../../contract/v1/README.md#backend-2-workspace-contexts) | | `litheFunctions` 1 | Registration, withdrawal, invocation, results, progress, cancellation; no host services | [Plugin functions](../../contract/v1/README.md#plugin-functions) | | `litheFunctions` 2 | Host services: credentials `credentials/get`, `set`, `delete` | [Host services (protocol version 2)](../../contract/v1/README.md#host-services-protocol-version-2) | -| `litheFunctions` 3 | Workspace contexts and capability discovery, host notifications, public file/document/window methods, independent editor actions, plugin-owned native terminals | [Protocol 3](../../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | +| `litheFunctions` 3 | Workspace contexts and capability discovery, host notifications, public file/document/window methods, independent editor actions, plugin-owned native terminals, host-owned Run and debug sessions (`run/start`, `run/stop`, `debug/start`, and the `run/didExit` notification) | [Protocol 3](../../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3), [Run and debug sessions (protocol version 3)](../../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) | +| `litheFunctions` 4 | Everything in version 3, plus cross-plugin calls: `functions/providers` and `functions/invoke`; a provider receives a top-level `caller` | [Cross-plugin calls (protocol version 4)](../../contract/v1/README.md#cross-plugin-calls-protocol-version-4) | +| `pluginFunctions` 1 | The manifest `pluginFunctions` section: exports (`exports`) and call declarations (`calls`); grants no permission | [Declaring exports and calls](../../contract/v1/README.md#declaring-exports-and-calls) | | `litheLogging` 1 | Structured diagnostics notification `lithe/log`; current official SDKs require the host to offer it | [Structured plugin diagnostics](../../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1) | +### Cross-plugin calls (protocol 4) + +A plugin function is private to its plugin unless the plugin exports it. An exported function can be called, through Rust Core, by another enabled plugin whose manifest declares that call: the calling backend never reaches the provider's process, the provider's backend never learns more than the call it serves, and the host takes part only by starting a provider's backend that is not running. The relay is Core logic, the same on every platform that runs plugin backends; today only macOS does, and Windows does not advertise `pluginFunctions`. + +#### Manifest declarations + +| Item | Rule | +| --- | --- | +| Prerequisites | `pluginAPIVersion` ≥ 3, a `backend`, and `requires` with `pluginFunctions: 1`, `backend: 2`, `serverProcess: 1`; `requires` lists `pluginFunctions` exactly when the manifest has a `pluginFunctions` section | +| Section | At least one of `exports` and `calls`, each at most 32 entries, sorted by `id`, no `id` twice; any other field is `plugin.manifest.invalid` | +| Export | `{id, version, context}`. `id` is a function ID; `version` is the exported contract version (an integer from 1 to 1000), and an incompatible change to the params or result takes a new version; `context` has one value for now, `"workspace"`, meaning the function runs only for a local workspace context; a missing or unknown `context` refuses the whole manifest | +| Call | `{id, version}`. It names a function ID and the version the caller was written against, never the providing plugin's ID: the caller discovers providers at run time; the two versions must be equal, otherwise `plugin.function.versionMismatch` | +| Registration | Declaring an export does not register it: the provider's backend still calls `lithe/function/register`, and a function it has not registered, or has withdrawn, is `plugin.function.notExported`. A function the manifest does not export stays private, and a reference to it is still `plugin.function.wrongPlugin` | +| Host support | The host grants a plugin that declares `calls` the `functions` host service; a host without `pluginFunctions` refuses such a manifest (`plugin.api.unsupported`) | + +#### Host methods + +Only a backend that negotiated protocol version 4 and whose plugin declares at least one call may use them. A backend on version 3 or earlier gets `-32601`, `plugin.host.unavailable`; a plugin that declares no call gets `-32010`, `plugin.host.permissionDenied`. `getCapabilities` lists both methods under the `functions` service. + +| Method (prefix `lithe/host/`) | Params | Result | +| --- | --- | --- | +| `functions/providers` | `{functionID}` | `{providers: [{pluginID, version}]}` | +| `functions/invoke` | `{pluginID, functionID, context: {contextID, generation}, params?, timeoutMilliseconds?, parentOperationID?}` | `{value}` | + +- `providers` lists the enabled plugins other than the caller that export the function, sorted by plugin ID, each with its exported `version`. The caller must have declared the function, otherwise `plugin.function.notDeclared`. A provider's backend need not be running. +- `invoke` calls one provider. `context` is required: a local workspace context the caller learned from the workspace service. `params` is a JSON object and defaults to `{}`. `timeoutMilliseconds` is a positive integer, defaults to 60000, and is capped at 600000. A field the method does not define is `-32018`, `plugin.host.unsupportedOption`. `value` is the provider's result, and `null` is a result. +- The caller cancels a call with `$/cancelRequest`; it fails with `plugin.host.cancelled`. +- Core checks in a fixed order (the caller declared the function, the provider is enabled, the provider exports it, the versions are equal, the context, a local workspace, the params size, the call it belongs to, the call chain, the depth, the concurrency) and refuses before anything is written to the provider; the first failing check decides the code. + +#### What the provider receives + +Core forwards a relayed call to the provider as `lithe/function/invoke`. As for any function call, the request has top-level `functionID`, `operationID`, `context`, and `params`; a relayed call adds one more top-level field, `caller: {pluginID}`. + +- Core writes `caller.pluginID` from the identity of the calling session. It sits outside `params` and the caller cannot supply it: a `functions/invoke` request carrying a top-level `caller` is refused with `-32018`. A function that decides by who asks, or keeps per-caller state, reads this field, never a plugin ID in `params`. A key named `caller` inside the caller's own `params` is ordinary data and is passed unchanged. +- A call the host itself started has no `caller`; absent means the host. A provider that negotiated version 3 or earlier never receives the field; a provider that relies on it must negotiate version 4. +- `operationID` identifies the call being served. A provider that makes a nested `functions/invoke` while serving a relayed call sets `parentOperationID` to this `operationID`, so Core knows which call the new one belongs to. `parentOperationID` is a string of 1 to 128 bytes, otherwise `-32602`, `plugin.host.invalidParams`. Without it, Core treats the new call as a child of every relayed call the backend is serving, so a backend should always set it. +- The call chain: the chain of a new call is the chain of its parent plus the caller. A call to a plugin already on the chain, including a plugin calling itself, is `plugin.function.callCycle`, and a chain has at most 4 calls. Cancelling, timing out, or ending the parent call cancels its children, and each provider receives `$/cancelRequest`. If `parentOperationID` names a call the host started directly, the new call starts a new chain; if it names a call this backend is not serving (finished, or never existed), the call is refused with `plugin.host.cancelled`. +- If the provider's backend is not running, Core waits for it and emits the runtime event `functionProviderStartRequired` on the calling session; the host starts the backend as an ordinary plugin start. If the provider has not negotiated the protocol and registered the function within 30 seconds, the call ends with `plugin.function.providerUnavailable`. + +#### Trust is not transitive + +- The provider runs with its own permissions, state directory, and credentials. The caller gains none of the provider's, and the provider gains none of the caller's. The caller's plugin ID appears only in the `caller` field, never in the provider's session. +- An exported function must not accept an executable path, command line, or script from its caller and start a process with it, nor read or write a path the caller chose without checking it against what the provider itself may access. If a caller needs the provider to run something, the function takes a request the provider interprets (a task name, a document) and the provider decides the executable. +- This is not an install dependency: installing the caller does not install a provider, and disabling a provider only leaves calls without one; it takes no part in the install closure and does no version solving. A call whose context is a remote workspace is refused with `plugin.host.authorityUnsupported`; the exported `context: "workspace"` means a local workspace only, and backends still run on the machine that runs Lithe. +- Disabling, enabling, replacing, and uninstalling a plugin take effect for the relay at once, with no restart. macOS registers backend plugins only when it launches, so a newly installed or updated backend plugin appears in `providers` only after Lithe restarts. + +#### Error codes + +| Code | When | +| --- | --- | +| `plugin.function.notDeclared` | The caller did not declare the function; also when a caller update stops declaring it while the call runs | +| `plugin.function.providerDisabled` | The provider is not installed or not enabled; also when, while the call runs, the provider is disabled or uninstalled, or its update stops exporting the function or changes its version | +| `plugin.function.notExported` | The provider's backend has not registered the function, or has withdrawn it | +| `plugin.function.versionMismatch` | The version the caller declared differs from the version the provider exports | +| `plugin.function.callCycle` | The provider is already on the call chain, including the caller itself | +| `plugin.function.depthExceeded` | The chain would have more than 4 calls | +| `plugin.function.tooManyCalls` | The caller already has 8 relayed calls running | +| `plugin.function.payloadTooLarge` | The params are over 256 KiB, or the provider's result is over 256 KiB (over 1 MiB is `invalidResult`) | +| `plugin.function.providerUnavailable` | The provider's backend was not ready within 30 seconds, or it exited or withdrew the function while the call ran | + +A provider's own failure keeps its meaning: when its function answered with an error, the code is `plugin.function.failed` (`-32070`) and `details.pluginErrorCode` is the provider's own `error.data.code`; a reply with no `result`, or a result over 1 MiB serialized, is `plugin.function.invalidResult` (`-32071`). A workspace context that is closed or replaced by a newer generation is `plugin.host.contextExpired`, and the deadline passing is `plugin.host.timeout`. Core never restarts a backend or retries a call. + +### Run and debug sessions (protocol 3) + +A backend can start a run or a debug session that the host presents as its own: the backend decides what to start, and the host owns the process, the console, the Stop button, and the end of the session. A Run session adds host presentation (a Run window entry, the console, the exit status) and grants no other capability. `run/start` and `debug/start` take `context: {contextID, generation}` and the permission shown; `run/stop` takes no `context`, so a run can still be stopped after the context it started in has closed. + +| Method (prefix `lithe/host/`) | Permission | Params | Result | +| --- | --- | --- | --- | +| `run/start` | `run.start` | `context, label, process, temporaryFiles?, debug?, rerun?` | `{runID}` | +| `run/stop` | `run.start` | `{runID}` or `{sessionID}` | `null` | +| `debug/start` | `debug.start` | `context, label, adapter, request, configuration` | `{sessionID}`; see [Debug adapters](debug-adapters.md#starting-a-debug-session-from-a-plugin-backend) | + +Notification (the backend does not reply): `lithe/host/run/didExit` with `{runID, exitCode?, signal?}`, granted by `run.start`. `run.start` requires `backend` 2 and grants `run/start`, `run/stop`, and `run/didExit`. + +`run/start` parameters: + +- `label`: 1 to 256 characters without control characters; it titles the Run window entry. +- `process`: `{executable, arguments, workingDirectory, environment}`, all four required; an unknown field is refused. `executable` is `{kind: "tool", tool}` (`tool` is a tool-definition ID that the manifest lists in `requiresTools`, which the host resolves through the layers described in [Toolchains](toolchains.md)) or `{kind: "packaged", path}` (a normalized relative path of a file inside the plugin package). Arguments, the working directory (relative to the workspace root and an existing directory inside it), and the environment have the bounds of [run preparation](../../contract/v1/README.md#provider-run-preparation-buildproviders-4). +- `temporaryFiles`: at most four `{name, contents}` files of up to 4 MiB of text each. The host writes them into its own temporary directory, removes them when the run ends, and replaces `${temporaryFile:}` in `process.arguments` and `process.environment` with the file's path, so the plugin never needs to know where the host keeps temporary files. A `name` is unique, 1 to 64 letters, digits, `.`, `_`, or `-`, and starts with a letter or digit. +- `debug`: `{configurationType, target}`, which makes the run a debug session; see [Debug adapters](debug-adapters.md#starting-a-debug-session-from-a-plugin-backend). +- `rerun`: `{function, data}`, one of the plugin's registered functions and its params (a JSON object of up to 64 KiB). When the user chooses Run again, the host invokes the function with `data` as the params and the current `context`, and the backend answers with a new `run/start`, so the backend prepares the restart; without `rerun` the entry cannot be rerun. + +Other rules: + +- `run/stop` stops a run by `runID` or a debug session by `sessionID` and answers `null`, also when it already ended; cancelling the host request is the same as stopping. A backend can stop only runs and sessions it started. +- The host sends `run/didExit` when the process ends, once per run and only to a session that holds the `run.start` grant. `exitCode` is the exit status and `signal` (1 to 127) the signal that ended it; both are absent when the host lost the process. +- Malformed params are `plugin.host.invalidParams`, and Core's rejection (`plugin.run.invalidStart` or `plugin.debug.invalidStart`) is in `details.code` and `details.field`; a closed context is `plugin.host.contextExpired`; a `tool` that resolves to no executable is `plugin.host.toolUnresolved`. +- The host keeps running processes when the plugin is disabled or its backend exits; it only stops telling the backend. A backend that starts again does not receive `run/didExit` for a run it did not see begin. +- To make output in the Run console clickable, declare matchers with `outputLinkMatchers` 1: Core does the matching and the `resolve` function is called only when the user clicks; see [Output link matchers](../../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1). +- Windows has no plugin backend host, so it has none of these methods. + ### Contract sections | Section | Content | @@ -113,10 +307,13 @@ A plugin backend is the plugin's own long-lived process. It runs what a web page | [Invocation](../../contract/v1/README.md#invocation), [Progress](../../contract/v1/README.md#progress) | Calls, results, progress, cancellation | | [Host services (protocol version 2)](../../contract/v1/README.md#host-services-protocol-version-2) | Credentials service | | [Workspace contexts and capability discovery (protocol version 3)](../../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | `getCapabilities`, `contextID`/`generation` contexts, host failure codes | -| [Host notifications (protocol version 3)](../../contract/v1/README.md#host-notifications-protocol-version-3) | `workspace/didChangeContexts`, `subscriptions/notify`, `commands/didRun` | +| [Host notifications (protocol version 3)](../../contract/v1/README.md#host-notifications-protocol-version-3) | `workspace/didChangeContexts`, `subscriptions/notify`, `commands/didRun`, `run/didExit` | | [Extending version 3](../../contract/v1/README.md#extending-version-3) | How protocol 3 gains methods without a new version | | [Public document, file and window methods (protocol version 3)](../../contract/v1/README.md#public-document-file-and-window-methods-protocol-version-3) | File reads, watches, search, and window messages (document and editor methods are on [Editor](editor.md)) | | [Plugin-owned native terminals (function protocol 3)](../../contract/v1/README.md#plugin-owned-native-terminals-function-protocol-3), [Close, retained context and reopen](../../contract/v1/README.md#close-retained-context-and-reopen) | Plugin-owned native terminals | +| [Run and debug sessions (protocol version 3)](../../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) | `run/start`, `run/stop`, `debug/start`, `run/didExit`: params, limits, failure codes | +| [Cross-plugin calls (protocol version 4)](../../contract/v1/README.md#cross-plugin-calls-protocol-version-4) | The `pluginFunctions` manifest declarations, `functions/providers`, `functions/invoke`, the check order, the call chain, the `caller` field, providers that are not running, hot activation | +| [Output link matchers (`outputLinkMatchers` 1)](../../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1) | Links in Run and Test console output; the `resolve` function is called on click | | [Structured plugin diagnostics (logging protocol 1)](../../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1) | `lithe/log` fields and limits | | [Compatibility](../../contract/v1/README.md#compatibility), [Official backend SDK](../../contract/v1/README.md#official-backend-sdk) | Compatibility rules and the official SDKs | | [Outcomes and error codes](../../contract/v1/README.md#outcomes-and-error-codes) | Outcomes and error codes | @@ -129,8 +326,13 @@ A plugin backend is the plugin's own long-lived process. It runs what a web page | [`backend/backend-workspace.plugin.json`](../../contract/v1/fixtures/backend/backend-workspace.plugin.json) | `backend` 2: starts on `onWorkspaceOpen`, uses `workspace.metadata` | | [`backend/command-backend.plugin.json`](../../contract/v1/fixtures/backend/command-backend.plugin.json) | `backend` 2 with `ui` 4: a command without a tool window that reads and edits documents | | [`backend/terminal-backend.plugin.json`](../../contract/v1/fixtures/backend/terminal-backend.plugin.json) | `backend` 2: the smallest manifest for plugin-owned native terminals | -| [`backend/function-protocol.json`](../../contract/v1/fixtures/backend/function-protocol.json) | Canonical plugin-function messages: negotiation, registration, invocation, results, progress, 42 host requests, 18 host failures, 10 host notifications | +| [`backend/function-protocol.json`](../../contract/v1/fixtures/backend/function-protocol.json) | Canonical plugin-function messages: negotiation (including protocol 4), registration, invocation, results, progress, 93 host requests, 23 host failures, 29 host notifications, plus the `caller` of a relayed call (`invocationCallers`) and the relay failure codes (`relayErrors`) | +| [`backend/plugin-functions-exporter.plugin.json`](../../contract/v1/fixtures/backend/plugin-functions-exporter.plugin.json) | `pluginFunctions` 1: only exports two functions (`document.format` version 1, `symbols.list` version 2, `context` `workspace`), with no page, command, or `onWorkspaceOpen` | +| [`backend/plugin-functions-caller.plugin.json`](../../contract/v1/fixtures/backend/plugin-functions-caller.plugin.json) | `pluginFunctions` 1: declares a call to `document.format` version 1, starts on `onWorkspaceOpen` | | [`backend/plugin-logging.json`](../../contract/v1/fixtures/backend/plugin-logging.json) | `litheLogging` 1: offer and answer, notifications, rejections, levels | +| [`run/run-start.json`](../../contract/v1/fixtures/run/run-start.json) | The params of `run/start` and what `run.decodeStart` returns for each of its 17 cases (tool and packaged executables, `debug` and `rerun`, the refusals); temporary files depend on the host's temporary directory and are covered by Core's own tests | +| [`run/tool-reference-launch.plugin.json`](../../contract/v1/fixtures/run/tool-reference-launch.plugin.json) | The manifest `run-start.json` uses: `requiresTools` lists the tool `interpreter`, and it declares `run.start`, `debug.start`, and `outputLinkMatchers` 1 | +| [`run/output-link-matchers.json`](../../contract/v1/fixtures/run/output-link-matchers.json) | The requests of `outputLinks.match` and the links it returns, plus what `outputLinks.decodeResolution` makes of a matcher's `resolve` reply | Backend rejections are in [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json). diff --git a/reference/domains/debug-adapters.md b/reference/domains/debug-adapters.md index 829d4ca..7d54a48 100644 --- a/reference/domains/debug-adapters.md +++ b/reference/domains/debug-adapters.md @@ -4,15 +4,15 @@ ## 中文 -插件在 `debugAdapters[]` 里声明自己实现的调试适配器。每个适配器是一个 DAP 进程,宿主启动它,并通过 stdio 或本机回环 TCP 端口和它通信;也可以是插件语言服务器里内置的调试服务(例如运行在 JDT LS 里的 Java Debug Server)。声明只是数据:没有解析器、工作流、宿主集成或 `initializationOptions`,出现这些字段会被拒绝。调试界面、断点、变量、控制台都归宿主。 +插件在 `debugAdapters[]` 里声明自己实现的调试适配器。每个适配器是一个 DAP 进程,宿主启动它,并通过 stdio 或本机回环 TCP 端口和它通信;也可以是插件语言服务器里内置的调试服务(例如运行在 JDT LS 里的 Java Debug Server)。声明只是数据:没有解析器、工作流、宿主集成或 `initializationOptions`,出现这些字段会被拒绝。调试界面、断点、变量、控制台都归宿主。插件后端还可以用宿主方法 `debug/start` 在自己声明的适配器上发起调试会话。 | 项目 | 内容 | | --- | --- | | 清单字段 | `debugAdapters[]` | -| 能力版本 | `debugAdapters` 1–2,需要级别 4;`launch.process` 用到的 `serverProcess` 1,`runSelectedRuntime` 需要 `serverProcess` 2。见[版本对照](../capabilities-and-versions.md#能力版本requires) | -| 协议 | DAP(Debug Adapter Protocol),没有 Lithe 专用协议 | -| API 编号 | [API-30](../api-catalog.md#现在能用的-api):调试适配器 | -| 权限 | `debug.launch`;任何适配器支持 `attach` 时加 `debug.attach`(远程 attach 不另设权限);`tcpLanguageServer` 依赖同一插件的语言服务器,所以还要有 `languageServers` 和 `languageServer.launch` | +| 能力版本 | `debugAdapters` 1–3,需要级别 4;`launch.process` 用到的 `serverProcess` 1,`runSelectedRuntime` 需要 `serverProcess` 2。见[版本对照](../capabilities-and-versions.md#能力版本requires) | +| 协议 | DAP(Debug Adapter Protocol),没有 Lithe 专用协议;后端发起会话用 `litheFunctions` 3 的宿主方法 `debug/start`,见[由插件后端发起调试会话](#由插件后端发起调试会话) | +| API 编号 | [API-30](../api-catalog.md#现在能用的-api):调试适配器,包括 `debug/start` | +| 权限 | `debug.launch`;任何适配器支持 `attach` 时加 `debug.attach`(远程 attach 不另设权限);后端调用 `debug/start` 时加 `debug.start`(需要 `backend` 2 和已声明的 `debugAdapters` 条目,只授予 `debug/start`);`tcpLanguageServer` 依赖同一插件的语言服务器,所以还要有 `languageServers` 和 `languageServer.launch` | | 激活事件 | 每个适配器一个 `onDebug:` | | Windows | 不支持;Windows 宿主不声明 `debugAdapters` | @@ -22,7 +22,8 @@ | --- | --- | --- | --- | --- | | `debugAdapters` 1 | 4 | `debugAdapters[]` 的全部字段:`launch`/`attach` 请求、自动匹配 `selectors`、`configurationTypes`、默认参数与 `targetBindings`(`program`、`processId`、`cwd`、`connection`)、`executable` 候选、`capabilities.disabled`、`console`、四种传输 | 支持 | 不支持 | | `debugAdapters` 2 | 4 | `targetBindings` 新增有界的 `args` ← `target.arguments`、`env` ← `target.environment` | 支持 | 不支持 | -| `serverProcess` 2 | 4 | `launch.process` 可以写成 `{"launch": {"kind": "runSelectedRuntime", "tool": "python" \| "java" \| "node"}}`,由宿主用用户选中的运行时启动适配器;这是 `serverProcess` 2 唯一的用途 | 只有 Core 支持,macOS 宿主只声明 `serverProcess` 1 | 不支持 | +| `debugAdapters` 3 | 4 | 适配器可声明 `features`(`hotCodeReplace`、`stepFilters`)和 `defaults.stepFilters`;任何 `features` 或 `defaults` 都要求 `requires.debugAdapters` 3,见[适配器行为](#适配器行为debugadapters-3) | 支持 | 不支持 | +| `serverProcess` 2 | 4 | `launch.process` 可以写成 `{"launch": {"kind": "runSelectedRuntime", "tool": "python" \| "java" \| "node"}}`,由宿主用用户选中的运行时启动适配器;这是 `serverProcess` 2 唯一的用途。从 `serverProcess` 4 起 `tool` 是 `requiresTools` 列出的[工具定义 ID](../../contract/v1/README.md#tool-references-serverprocess-4) | 只有 Core 支持,macOS 宿主声明 `serverProcess` 1 和 3 | 不支持 | | `buildProviders` 5 | 4 | 构建提供者为原生调试准备目标,运行声明 `debug: {"adapter": "native"}`;同时需要 `debugAdapters` 2,见[构建提供者](build-providers.md) | 支持 | 不支持 | | `buildProviders` 11 | 3 | 服务运行声明 `debug: {"configurationType": "…"}`,宿主选一个已启用、支持 `attach` 和该类型的适配器连接 | 支持 | 不支持 | @@ -39,6 +40,20 @@ 所有 TCP 端点只能是本机回环地址。`tcpLanguageServer` 的命令是安装和更新审查的一部分,命令改变时会重新询问用户。 +### 适配器行为(`debugAdapters` 3) + +宿主从不按适配器 ID 识别适配器。适配器特有的行为由声明给出,共享的调试引擎按声明执行: + +| 声明 | 含义 | +| --- | --- | +| `features.hotCodeReplace.request` | 宿主向运行中的被调试程序应用变更代码时发送的 DAP 命令(标准的 `redefineClasses` 检查)。写了表示适配器支持热代码替换,没写表示不支持 | +| `features.hotCodeReplace.event` | 一个 DAP 事件,调试控制台把它的 `message` 显示为进度 | +| `features.hotCodeReplace.changedField`、`errorField` | 响应体里的字段:已替换单元的列表,以及适配器在成功响应里报告的失败消息。没有 `changedField` 的响应不会显示任何已替换内容 | +| `features.stepFilters.argument` | 适配器接收步进过滤器的启动参数,形状为 `{skipClasses, skipSynthetics, skipStaticInitializers, skipConstructors}`。用户配置里已设置的参数保持不变 | +| `defaults.stepFilters` | `classNameFilters`(最多 256 个单行模式,每个最多 256 个字符)、`skipSynthetics`、`skipStaticInitializers`、`skipConstructors`、`hideFilteredStackFrames`。启动和用户设置都没有给出过滤器时使用 | + +名称(`request`、`event`、各字段、`argument`)是一个字母加最多 63 个字母、数字、`.`、`_` 或 `-`。名称格式错误,或写了这一版没有定义的字段,会被拒绝为 `plugin.manifest.invalid`,`field` 指明位置;任何 `features` 或 `defaults` 都要求 `requires.debugAdapters` 3。启动计划的 `debugAdapter` 描述符同时携带两者,宿主据此传给 `debug.createSession`。两者都不声明的插件,行为和 `debugAdapters` 1、2 相同。 + ### 远程 attach 连接另一台机器上的被调试程序(带 `-agentlib:jdwp` 的 JVM、Node inspector、gdbserver)时,适配器仍在本机运行,只有被调试程序在远端。`attach` 配置的 `targetBindings.connection` 把用户确认的主机、端口、路径映射写进适配器的 `arguments`: @@ -54,11 +69,37 @@ Core 只把 `localhost`、`127.0.0.0/8`、`::1` 判为本机;其他主机都是 `remote: true`,宿主每次都请用户确认,默认不记住。`connection` 不声明外部 DAP 端点或远程执行适配器(例如 `ssh host gdb -i=dap`),也不提供代理、隧道、端口转发。 +### 由插件后端发起调试会话 + +后端可以让宿主启动一个调试会话:后端决定启动什么,调试界面、进程、控制台、停止按钮和会话结束都归宿主。有两条路径,都按 `litheFunctions` 3 的宿主方法调用,都带 `context: {contextID, generation}`: + +| 方法(前缀 `lithe/host/`) | 权限 | 参数 | 结果 | +| --- | --- | --- | --- | +| `debug/start` | `debug.start` | `context, label, adapter, request, configuration` | `{sessionID}` | +| `run/start`,带 `debug` | `run.start` | `context, label, process, debug: {configurationType, target}`,另有 `temporaryFiles?`、`rerun?` | `{runID}` | +| `run/stop` | `run.start` | `{runID}` 或 `{sessionID}` | `null` | + +`debug/start`: + +- `label`:1 到 256 个字符,不含控制字符。 +- `adapter`:本插件自己的适配器,写本地 ID 或限定 ID `/`。别的插件的适配器,以及本插件没有声明的适配器,都返回 `plugin.host.notFound`。 +- `request`:`launch` 或 `attach`,适配器必须支持它。 +- `configuration`:最多 256 KiB 的 JSON 对象,宿主在适配器声明的默认值之上,把它作为 DAP 的 launch 或 attach 参数传给适配器。`noDebug` 和所有以 `__` 开头的键归宿主,会被拒绝。 + +`run/start` 的 `debug` 字段让一次宿主拥有的运行成为调试会话: + +- `configurationType` 是公开的调试适配器别名,和[按配置类型选择 attach 适配器](../../contract/v1/README.md#registry-connection-debugging-buildproviders-11)相同。 +- `target` 为 `{kind: "connection", port}` 时,进程在 `port`(1 到 65535,由后端写进参数)上监听调试器,宿主等它就绪后连接适配器;为 `{kind: "launch"}` 时,适配器自己启动进程,通过 `targetBindings` 从 `process` 取程序、参数、工作目录和环境。 + +`run/start`、`run/stop` 和 `run/didExit` 的其他规则见[插件后端](backend.md#run-与调试会话协议-3)。参数格式错误返回 `plugin.host.invalidParams`,Core 的拒绝(`plugin.debug.invalidStart` 或 `plugin.run.invalidStart`,带 `field`)在 `details.code` 和 `details.field` 里;已关闭的上下文是 `plugin.host.contextExpired`。macOS 支持;Windows 没有插件后端宿主,也不声明 `debugAdapters`,没有这些方法。 + ### 契约章节 | 章节 | 内容 | | --- | --- | | [API level 4: debug adapters](../../contract/v1/README.md#api-level-4-debug-adapters) | 全部字段、绑定、`executable` 候选、控制台、传输 | +| [Adapter features and defaults (`debugAdapters` 3)](../../contract/v1/README.md#adapter-features-and-defaults-debugadapters-3) | `features.hotCodeReplace`、`features.stepFilters` 和 `defaults.stepFilters` | +| [Run and debug sessions (protocol version 3)](../../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) | `debug/start`、`run/start` 的 `debug` 字段、`run/stop`:参数、限制、失败码 | | [Remote attach](../../contract/v1/README.md#remote-attach) | `connection` 绑定、四种路径映射、目标格式、本机判定、拒绝层次 | | [Native debug preparation (version 5)](../../contract/v1/README.md#native-debug-preparation-version-5) | 构建提供者准备原生调试目标 | | [Registry connection debugging (buildProviders 11)](../../contract/v1/README.md#registry-connection-debugging-buildproviders-11) | 服务运行按配置类型选择 attach 适配器 | @@ -70,6 +111,9 @@ Core 只把 `localhost`、`127.0.0.0/8`、`::1` 判为本机;其他主机都 | [`debug-adapters/debug-adapter.plugin.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter.plugin.json) | 合法示例:两个适配器(`example`、`native`),`settings` 2 的 `file` 设置选择要调试的程序 | | [`debug-adapters/debug-adapter-language-server.plugin.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter-language-server.plugin.json) | `tcpLanguageServer`:调试服务在插件的语言服务器里 | | [`debug-adapters/debug-adapter-remote-attach.plugin.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter-remote-attach.plugin.json) | 六个适配器,分别演示 `connection` 的不同写法:`flat`(平铺的 `host`、`port`)、`nested`(嵌套路径加 `objects`)、`scalar`(`first`)、`joined`(`address`)、`keyed`(`dictionary`)、`paired`(`pairs`) | +| [`debug-adapters/debug-adapter-features.plugin.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter-features.plugin.json) | `debugAdapters` 3 加 `backend` 2、`serverProcess` 1,权限 `debug.launch` 和 `debug.start`:适配器 `script` 声明 `features`(`hotCodeReplace`、`stepFilters`)和 `defaults.stepFilters` | +| [`debug-adapters/debug-start.json`](../../contract/v1/fixtures/debug-adapters/debug-start.json) | `debug/start` 的参数,以及 `debug.decodeBackendConfiguration` 对 10 个用例返回的规范化启动或拒绝:本插件适配器按本地 ID 或限定 ID、别的插件的适配器、未声明的适配器、`noDebug` 和 `__` 键等;256 KiB 上限由 Core 自己的测试覆盖 | +| [`run/run-start.json`](../../contract/v1/fixtures/run/run-start.json) | `run/start` 的参数,以及 `run.decodeStart` 的结果;其中包含 `debug` 的 `connection` 和 `launch` 目标、端口越界等用例 | | [`debug-adapters/debug-adapter-rejections.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter-rejections.json) | 76 个拒绝用例,每个写明稳定错误码、字段和报告层次(`schema`、`coreManifest`、`corePlan`、`coreInstall`) | | [`build-providers/build-provider-debug-preparation.plugin.json`](../../contract/v1/fixtures/build-providers/build-provider-debug-preparation.plugin.json) | `buildProviders` 5 加 `debugAdapters` 2 | | [`build-providers/build-debug-preparation.json`](../../contract/v1/fixtures/build-providers/build-debug-preparation.json) | `lithe/build/prepareDebug` 的结果和拒绝用例 | @@ -85,15 +129,15 @@ Core 只把 `localhost`、`127.0.0.0/8`、`::1` 判为本机;其他主机都 ## English -A plugin declares the debug adapters it implements in `debugAdapters[]`. Each adapter is a DAP process the host starts and talks to over stdio or a loopback TCP port, or a debug server built into the plugin's language server (such as the Java Debug Server inside JDT LS). The declaration is data only: there is no resolver, workflow, host integration, or `initializationOptions`, and any such field is refused. The debug UI, breakpoints, variables, and console belong to the host. +A plugin declares the debug adapters it implements in `debugAdapters[]`. Each adapter is a DAP process the host starts and talks to over stdio or a loopback TCP port, or a debug server built into the plugin's language server (such as the Java Debug Server inside JDT LS). The declaration is data only: there is no resolver, workflow, host integration, or `initializationOptions`, and any such field is refused. The debug UI, breakpoints, variables, and console belong to the host. A plugin backend can also start a debug session on one of its own declared adapters with the host method `debug/start`. | Item | Value | | --- | --- | | Manifest field | `debugAdapters[]` | -| Capability versions | `debugAdapters` 1–2, at level 4; `serverProcess` 1 for `launch.process`, and `serverProcess` 2 for `runSelectedRuntime`. See [versions](../capabilities-and-versions.md#capability-versions-requires) | -| Protocols | DAP (Debug Adapter Protocol); no Lithe-specific protocol | -| API IDs | [API-30](../api-catalog.md#apis-usable-now): debug adapters | -| Permissions | `debug.launch`; `debug.attach` when any adapter serves `attach` (remote attach has no separate permission); `tcpLanguageServer` relies on a language server of the same plugin, so it also needs `languageServers` and `languageServer.launch` | +| Capability versions | `debugAdapters` 1–3, at level 4; `serverProcess` 1 for `launch.process`, and `serverProcess` 2 for `runSelectedRuntime`. See [versions](../capabilities-and-versions.md#capability-versions-requires) | +| Protocols | DAP (Debug Adapter Protocol); no Lithe-specific protocol. A backend starts a session with the `litheFunctions` 3 host method `debug/start`; see [Starting a debug session from a plugin backend](#starting-a-debug-session-from-a-plugin-backend) | +| API IDs | [API-30](../api-catalog.md#apis-usable-now): debug adapters, including `debug/start` | +| Permissions | `debug.launch`; `debug.attach` when any adapter serves `attach` (remote attach has no separate permission); `debug.start` when a backend calls `debug/start` (needs `backend` 2 and a declared `debugAdapters` entry, and grants only `debug/start`); `tcpLanguageServer` relies on a language server of the same plugin, so it also needs `languageServers` and `languageServer.launch` | | Activation events | One `onDebug:` per adapter | | Windows | Not supported; the Windows host does not advertise `debugAdapters` | @@ -103,7 +147,8 @@ A plugin declares the debug adapters it implements in `debugAdapters[]`. Each ad | --- | --- | --- | --- | --- | | `debugAdapters` 1 | 4 | Every `debugAdapters[]` field: `launch`/`attach` requests, automatic `selectors`, `configurationTypes`, default arguments and `targetBindings` (`program`, `processId`, `cwd`, `connection`), `executable` candidates, `capabilities.disabled`, `console`, the four transports | Yes | No | | `debugAdapters` 2 | 4 | Bounded `args` ← `target.arguments` and `env` ← `target.environment` in `targetBindings` | Yes | No | -| `serverProcess` 2 | 4 | `launch.process` may be `{"launch": {"kind": "runSelectedRuntime", "tool": "python" \| "java" \| "node"}}`, so the host starts the adapter with the runtime the user selected; this is the only use of `serverProcess` 2 | Core only; the macOS host advertises `serverProcess` 1 | No | +| `debugAdapters` 3 | 4 | An adapter may declare `features` (`hotCodeReplace`, `stepFilters`) and `defaults.stepFilters`; any `features` or `defaults` requires `requires.debugAdapters` 3; see [Adapter features](#adapter-features-debugadapters-3) | Yes | No | +| `serverProcess` 2 | 4 | `launch.process` may be `{"launch": {"kind": "runSelectedRuntime", "tool": "python" \| "java" \| "node"}}`, so the host starts the adapter with the runtime the user selected; this is the only use of `serverProcess` 2. From `serverProcess` 4, `tool` is a [tool-definition ID](../../contract/v1/README.md#tool-references-serverprocess-4) that `requiresTools` lists | Core only; the macOS host advertises `serverProcess` 1 and 3 | No | | `buildProviders` 5 | 4 | A build provider prepares a native debug target, and a run declares `debug: {"adapter": "native"}`; also needs `debugAdapters` 2; see [build providers](build-providers.md) | Yes | No | | `buildProviders` 11 | 3 | A service run declares `debug: {"configurationType": "…"}` and the host connects an enabled adapter that serves `attach` and that type | Yes | No | @@ -120,6 +165,20 @@ A manifest below level 4 may not declare `debugAdapters`, and a level 3 manifest Every TCP endpoint is loopback only. The `tcpLanguageServer` command is part of the install and update review, and changing it asks the user again. +### Adapter features (`debugAdapters` 3) + +A host never recognizes an adapter by its ID. What is specific to an adapter is declared, and the shared debug engine follows the declaration: + +| Declaration | Meaning | +| --- | --- | +| `features.hotCodeReplace.request` | The DAP command the host sends to apply changed code to the running debuggee (the standard `redefineClasses` inspection). Present means the adapter supports hot code replace; absent means it does not | +| `features.hotCodeReplace.event` | A DAP event whose `message` the Debug Console shows as progress | +| `features.hotCodeReplace.changedField`, `errorField` | Fields of the response body: the list of replaced units, and a failure message the adapter reports in a successful response. A response without `changedField` replaces nothing visible | +| `features.stepFilters.argument` | The launch argument through which the adapter receives stepping filters, shaped `{skipClasses, skipSynthetics, skipStaticInitializers, skipConstructors}`. An argument the user's configuration already sets is kept | +| `defaults.stepFilters` | `classNameFilters` (up to 256 single-line patterns of at most 256 characters), `skipSynthetics`, `skipStaticInitializers`, `skipConstructors`, `hideFilteredStackFrames`. Used when neither the launch nor the user's settings name filters | + +Names (`request`, `event`, fields, `argument`) are a letter followed by up to 63 letters, digits, `.`, `_`, or `-`. A declaration with a malformed name, or a field this version does not define, is `plugin.manifest.invalid` with the `field` that names it, and any `features` or `defaults` requires `requires.debugAdapters` 3. The launch plan's `debugAdapter` descriptor carries both so the host can pass them to `debug.createSession`. A plugin that declares neither keeps the behavior of `debugAdapters` 1 and 2. + ### Remote attach When the debuggee runs on another machine (a JVM with `-agentlib:jdwp`, a Node inspector, gdbserver), the adapter still runs locally and only the debuggee is remote. The `targetBindings.connection` of an `attach` configuration writes the host, port, and path mappings the user confirmed into the adapter's `arguments`: @@ -135,11 +194,37 @@ When the debuggee runs on another machine (a JVM with `-agentlib:jdwp`, a Node i Core treats only `localhost`, `127.0.0.0/8`, and `::1` as local; every other host is `remote: true`, and the host asks the user to confirm it each time without remembering the answer by default. `connection` declares no external DAP endpoint or remote adapter execution (such as `ssh host gdb -i=dap`), and no proxy, tunnel, or port forwarding. +### Starting a debug session from a plugin backend + +A backend can ask the host to start a debug session: the backend decides what to start, and the debug UI, the process, the console, the Stop button, and the end of the session belong to the host. There are two paths. Both are `litheFunctions` 3 host methods and both take `context: {contextID, generation}`: + +| Method (prefix `lithe/host/`) | Permission | Params | Result | +| --- | --- | --- | --- | +| `debug/start` | `debug.start` | `context, label, adapter, request, configuration` | `{sessionID}` | +| `run/start` with `debug` | `run.start` | `context, label, process, debug: {configurationType, target}`, plus `temporaryFiles?` and `rerun?` | `{runID}` | +| `run/stop` | `run.start` | `{runID}` or `{sessionID}` | `null` | + +`debug/start`: + +- `label`: 1 to 256 characters without control characters. +- `adapter`: one of the plugin's own adapters, by its local ID or its qualified ID `/`. An adapter of another plugin, and one the plugin does not declare, both return `plugin.host.notFound`. +- `request`: `launch` or `attach`, and the adapter must serve it. +- `configuration`: a JSON object of at most 256 KiB that the host passes to the adapter as the DAP launch or attach arguments, on top of the adapter's declared defaults. `noDebug` and every key that starts with `__` belong to the host and are refused. + +The `debug` field of `run/start` makes a host-owned run a debug session: + +- `configurationType` is a public debug adapter alias, as in [registry connection debugging](../../contract/v1/README.md#registry-connection-debugging-buildproviders-11). +- With `target` `{kind: "connection", port}`, the process listens for a debugger on `port` (1 to 65535; the backend puts it into the arguments), and the host waits for it and attaches the adapter. With `{kind: "launch"}`, the adapter starts the process itself, taking program, arguments, directory, and environment from `process` through its `targetBindings`. + +The other rules of `run/start`, `run/stop`, and `run/didExit` are in [Plugin backend](backend.md#run-and-debug-sessions-protocol-3). Malformed params are `plugin.host.invalidParams`, and Core's rejection (`plugin.debug.invalidStart` or `plugin.run.invalidStart`, with the `field`) is in `details.code` and `details.field`; a closed context is `plugin.host.contextExpired`. macOS supports them; Windows has no plugin backend host and does not advertise `debugAdapters`, so it has none of these methods. + ### Contract sections | Section | Content | | --- | --- | | [API level 4: debug adapters](../../contract/v1/README.md#api-level-4-debug-adapters) | Every field, bindings, `executable` candidates, console, transports | +| [Adapter features and defaults (`debugAdapters` 3)](../../contract/v1/README.md#adapter-features-and-defaults-debugadapters-3) | `features.hotCodeReplace`, `features.stepFilters`, and `defaults.stepFilters` | +| [Run and debug sessions (protocol version 3)](../../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) | `debug/start`, the `debug` field of `run/start`, and `run/stop`: params, limits, failure codes | | [Remote attach](../../contract/v1/README.md#remote-attach) | The `connection` binding, the four mapping shapes, the target format, local detection, refusal layers | | [Native debug preparation (version 5)](../../contract/v1/README.md#native-debug-preparation-version-5) | Build providers preparing native debug targets | | [Registry connection debugging (buildProviders 11)](../../contract/v1/README.md#registry-connection-debugging-buildproviders-11) | Service runs choosing an attach adapter by configuration type | @@ -151,6 +236,9 @@ Core treats only `localhost`, `127.0.0.0/8`, and `::1` as local; every other hos | [`debug-adapters/debug-adapter.plugin.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter.plugin.json) | Valid example: two adapters (`example`, `native`), and a `settings` 2 `file` setting picks the program to debug | | [`debug-adapters/debug-adapter-language-server.plugin.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter-language-server.plugin.json) | `tcpLanguageServer`: the debug server lives in the plugin's language server | | [`debug-adapters/debug-adapter-remote-attach.plugin.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter-remote-attach.plugin.json) | Six adapters, one per way of taking a `connection`: `flat` (top-level `host` and `port`), `nested` (nested paths with `objects`), `scalar` (`first`), `joined` (`address`), `keyed` (`dictionary`), `paired` (`pairs`) | +| [`debug-adapters/debug-adapter-features.plugin.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter-features.plugin.json) | `debugAdapters` 3 with `backend` 2 and `serverProcess` 1, and the permissions `debug.launch` and `debug.start`: the adapter `script` declares `features` (`hotCodeReplace`, `stepFilters`) and `defaults.stepFilters` | +| [`debug-adapters/debug-start.json`](../../contract/v1/fixtures/debug-adapters/debug-start.json) | The params of `debug/start` and the normalized start or refusal `debug.decodeBackendConfiguration` returns for each of its 10 cases: the plugin's own adapter by local or qualified ID, an adapter of another plugin, an undeclared adapter, `noDebug`, `__` keys, and more; the 256 KiB limit is covered by Core's own tests | +| [`run/run-start.json`](../../contract/v1/fixtures/run/run-start.json) | The params of `run/start` and the `run.decodeStart` results; its cases include a `debug` `connection` target, a `launch` target, and a port out of range | | [`debug-adapters/debug-adapter-rejections.json`](../../contract/v1/fixtures/debug-adapters/debug-adapter-rejections.json) | 76 refusal cases, each with its stable code, field, and reporting layer (`schema`, `coreManifest`, `corePlan`, `coreInstall`) | | [`build-providers/build-provider-debug-preparation.plugin.json`](../../contract/v1/fixtures/build-providers/build-provider-debug-preparation.plugin.json) | `buildProviders` 5 with `debugAdapters` 2 | | [`build-providers/build-debug-preparation.json`](../../contract/v1/fixtures/build-providers/build-debug-preparation.json) | `lithe/build/prepareDebug` results and refusal cases | diff --git a/reference/domains/editor.md b/reference/domains/editor.md index b8a508a..deacaa8 100644 --- a/reference/domains/editor.md +++ b/reference/domains/editor.md @@ -4,15 +4,15 @@ ## 中文 -插件后端通过插件函数协议 3 读取打开的文档和选区、做版本化编辑、打开和定位文件、打开 HTML 预览或外部浏览器,并在编辑器里注册自己的动作。这些都是宿主方法,由 Rust Core 按权限校验、macOS 宿主执行;插件不直接操作编辑器。 +插件后端通过插件函数协议 3 读取打开的文档和选区、做版本化编辑、打开和定位文件、打开 HTML 预览或外部浏览器,并在编辑器里注册自己的动作和语言功能 Provider(定义、悬浮、补全等),还能把没有对应文件的文档(例如反编译出的库类源码)按 URI scheme 交给宿主打开。这些都是宿主方法,由 Rust Core 按权限校验、macOS 宿主执行;插件不直接操作编辑器。 | 项目 | 内容 | | --- | --- | | 清单字段 | 没有专用字段;用 `backend` 加下面的权限 | | 能力版本 | `backend` 2,见[版本对照](../capabilities-and-versions.md#能力版本requires) | | 协议 | `litheFunctions` 3,见[进程协议版本](../capabilities-and-versions.md#进程协议版本) | -| API 编号 | API-09、10、15、16、27,见 [API 总表](../api-catalog.md#现在能用的-api) | -| 权限 | `documents.read`、`editor.edit`、`editor.open`、`preview.open`、`browser.open`、`editor.actions`(同时需要 `documents.read`),见[已公开的宿主方法](../api-catalog.md#已公开的宿主方法) | +| API 编号 | API-09、10、12、15、16、25、27,见 [API 总表](../api-catalog.md#现在能用的-api) | +| 权限 | `documents.read`、`editor.edit`、`editor.open`、`preview.open`、`browser.open`、`editor.actions`(同时需要 `documents.read`)、`editor.providers`(同时需要 `documents.read`)、`documents.contentProvider`(只授予 `documents/registerContentProvider`,不授予读写文档),见[已公开的宿主方法](../api-catalog.md#已公开的宿主方法) | | 激活事件 | 与后端相同:`onWorkspaceOpen`、`onCommand:` 或网页调用 | | Windows | 不支持;Windows 没有插件后端宿主 | @@ -25,16 +25,34 @@ | `editor/open`、`editor/reveal` | `editor.open` | API-15 | 返回 `documentID` 和版本;传入范围时只把光标放到起点 | | `preview/openHTML` | `preview.open` | API-16 | `editorAndPreview` 或 `previewOnly` | | `browser/openExternal` | `browser.open` | API-16 | HTTP(S) 地址或已保存的 HTML | -| `editor/registerActions` | `editor.actions` + `documents.read` | API-27 | 先用 `getCapabilities` 发现,再按工作区上下文注册;动作绑定普通函数和文件范围,不依赖 LSP、构建或运行 | +| `editor/registerActions` | `editor.actions` + `documents.read` | API-27 | 先用 `getCapabilities` 发现,再按工作区上下文注册;动作绑定普通函数和文件范围,不依赖 LSP、构建或运行。第 2 版支持多个图标、最多 4 个 `alternatives` 和点击结果 `reveal`,可用图标由宿主在 `services.editor.options.actionIcons` 中声明 | +| `editor/registerProviders` | `editor.providers` + `documents.read` | API-25 | 先用 `getCapabilities` 发现方法和 `services.editor.options.providerTypes`,再按工作区上下文注册;把 `definition`、`hover`、`completion`、`codeLens`、`foldingRange`、`semanticTokens`、`documentLink` 这 7 种类型绑定到普通函数,不在 `providerTypes` 中的类型返回 `unsupportedOption`;语言服务器的结果优先,插件结果只补充;预算由宿主决定 | -所有方法都要带 `context: {contextID, generation}`;远程工作区上下文返回 `plugin.host.authorityUnsupported`。 +所有方法都要带 `context: {contextID, generation}`;除 `documents/registerContentProvider` 外,远程工作区上下文返回 `plugin.host.authorityUnsupported`。`documents/registerContentProvider` 见下一节。 + +### 内容 Provider(API-12) + +语言服务器可能给出没有文件的文档(例如库类的反编译源码)。插件把这些文档当作一个 URI scheme 提供,用 `documents/registerContentProvider`,权限 `documents.contentProvider`(需要 `backend` 2)。 + +| 方法 | 权限 | API | 参数 | 结果 | +| --- | --- | --- | --- | --- | +| `documents/registerContentProvider` | `documents.contentProvider` | API-12 | `{context, scheme, function}` | `{subscriptionID}` | + +- 注册只记录绑定,不会调用后端;`function` 必须已经在当前会话里注册。用已有的 `subscriptions/dispose` 撤销。 +- `scheme` 是 1 到 64 个小写 ASCII 字母、数字、`+`、`-` 或 `.`,以字母开头。宿主或平台自己解析的 scheme 会被拒绝:`about`、`blob`、`data`、`file`、`ftp`、`http`、`https`、`javascript`、`lithe`、`mailto`、`untitled`、`ws`、`wss`。scheme 不合法、保留或 `function` 不是函数 ID,返回 `plugin.host.invalidParams`。 +- 每个工作区窗口里每个 scheme 只能有一个 Provider;再注册同一个 scheme 返回 `plugin.host.preconditionFailed`。 +- 打开 URI 带这个 scheme 的文档时,宿主用工作区 `context` 和 `{uri}` 通过 `lithe/function/invoke` 调用该函数。函数返回字符串或 `{content: string}`,最多 1 MiB,文档以只读方式打开;其他返回、失败或超时(宿主等 30 秒)都会向用户显示错误,不打开任何文档。 +- Provider 是文本的唯一来源:文档关闭后宿主不保留副本,重新打开时再问一次。会话结束或上下文关闭会撤销 Provider。 +- 只有 `documents.contentProvider` 授予这个方法;聚合服务名 `documents` 和其他授权都不会放行,返回 `plugin.host.permissionDenied`(`-32010`)。 ### 契约章节 | 章节 | 内容 | | --- | --- | | [Public document, file and window methods (protocol version 3)](../../contract/v1/README.md#public-document-file-and-window-methods-protocol-version-3) | 文档、编辑、打开、预览、浏览器方法的参数、限制和失败码 | -| [Independent editor actions (protocol version 3)](../../contract/v1/README.md#independent-editor-actions-protocol-version-3) | `editor/registerActions` 的注册、范围、调用和撤回 | +| [Independent editor actions (protocol version 3)](../../contract/v1/README.md#independent-editor-actions-protocol-version-3) | `editor/registerActions` 的注册、范围、调用和撤回,以及第 2 版的图标、`alternatives` 和点击结果 | +| [Editor language-feature providers (protocol version 3)](../../contract/v1/README.md#editor-language-feature-providers-protocol-version-3) | `editor/registerProviders` 的注册、选择器、结果形状、预算和合并规则 | +| [Content providers](../../contract/v1/README.md#content-providers) | `documents/registerContentProvider` 的 scheme 规则、调用、只读文档和撤回 | | [Workspace contexts and capability discovery (protocol version 3)](../../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | `contextID`/`generation` 与 `getCapabilities` | | [Outcomes and error codes](../../contract/v1/README.md#outcomes-and-error-codes) | 结果与错误码 | @@ -43,8 +61,11 @@ | 文件 | 内容 | | --- | --- | | [`editor/editor-actions.plugin.json`](../../contract/v1/fixtures/editor/editor-actions.plugin.json) | `backend` 2 加 `editor.actions`、`documents.read`:`onWorkspaceOpen` 启动后注册编辑器动作 | +| [`editor/editor-actions-v2.json`](../../contract/v1/fixtures/editor/editor-actions-v2.json) | 第 2 版动作的 Core 用例:宿主声明的图标和源码上限、`alternatives`、点击结果 `reveal`,以及整个结果被拒绝的情形 | +| [`editor/editor-providers.plugin.json`](../../contract/v1/fixtures/editor/editor-providers.plugin.json) | `backend` 2 加 `editor.providers`、`documents.read`:`onWorkspaceOpen` 启动后注册编辑器语言功能 Provider | +| [`editor/editor-providers.json`](../../contract/v1/fixtures/editor/editor-providers.json) | 注册与结果的 Core 用例:选择器和类型校验(`unsupportedOption`)、7 种类型的结果形状与被丢弃的结果 | | [`backend/command-backend.plugin.json`](../../contract/v1/fixtures/backend/command-backend.plugin.json) | `ui` 4 命令读取文档并编辑 | -| [`backend/function-protocol.json`](../../contract/v1/fixtures/backend/function-protocol.json) | `editor/applyEdits`、`editor/registerActions` 的标准请求与回复,以及 `versionConflict`、`readOnly` 等宿主失败码 | +| [`backend/function-protocol.json`](../../contract/v1/fixtures/backend/function-protocol.json) | `editor/applyEdits`、`editor/registerActions` 的标准请求与回复,以及 `versionConflict`、`readOnly` 等宿主失败码;还有 `documents/registerContentProvider` 的用例:需要 `documents.contentProvider` 授权、聚合名 `documents` 不够、保留 scheme、大写或数字开头的 scheme、非法函数名、缺少 `context` | ### 相关指南 @@ -54,15 +75,15 @@ ## English -Through plugin-function protocol 3, a plugin backend reads open documents and selections, applies versioned edits, opens and reveals files, opens an HTML preview or an external browser, and registers its own editor actions. These are host methods: Rust Core checks permissions and the macOS host executes them. The plugin never drives the editor directly. +Through plugin-function protocol 3, a plugin backend reads open documents and selections, applies versioned edits, opens and reveals files, opens an HTML preview or an external browser, and registers its own editor actions and language-feature providers (definition, hover, completion, and so on). It can also serve documents that have no file (such as the decompiled source of a library class) to the host as a URI scheme. These are host methods: Rust Core checks permissions and the macOS host executes them. The plugin never drives the editor directly. | Item | Value | | --- | --- | | Manifest field | None of its own; use `backend` with the permissions below | | Capability versions | `backend` 2; see [versions](../capabilities-and-versions.md#capability-versions-requires) | | Protocols | `litheFunctions` 3; see [process protocol versions](../capabilities-and-versions.md#process-protocol-versions) | -| API IDs | API-09, 10, 15, 16, 27; see the [API catalog](../api-catalog.md#apis-usable-now) | -| Permissions | `documents.read`, `editor.edit`, `editor.open`, `preview.open`, `browser.open`, `editor.actions` (also needs `documents.read`); see [public host methods](../api-catalog.md#public-host-methods) | +| API IDs | API-09, 10, 12, 15, 16, 25, 27; see the [API catalog](../api-catalog.md#apis-usable-now) | +| Permissions | `documents.read`, `editor.edit`, `editor.open`, `preview.open`, `browser.open`, `editor.actions` (also needs `documents.read`), `editor.providers` (also needs `documents.read`), `documents.contentProvider` (grants only `documents/registerContentProvider`, not reading or editing documents); see [public host methods](../api-catalog.md#public-host-methods) | | Activation | As for the backend: `onWorkspaceOpen`, `onCommand:`, or a page call | | Windows | Not supported; Windows has no plugin backend host | @@ -75,16 +96,34 @@ Through plugin-function protocol 3, a plugin backend reads open documents and se | `editor/open`, `editor/reveal` | `editor.open` | API-15 | Returns `documentID` and version; a range only places the caret at its start | | `preview/openHTML` | `preview.open` | API-16 | `editorAndPreview` or `previewOnly` | | `browser/openExternal` | `browser.open` | API-16 | An HTTP(S) URL or saved HTML | -| `editor/registerActions` | `editor.actions` + `documents.read` | API-27 | Discover it with `getCapabilities`, then register per workspace context; actions bind ordinary functions and file scopes, independent of LSP, build, or run | +| `editor/registerActions` | `editor.actions` + `documents.read` | API-27 | Discover it with `getCapabilities`, then register per workspace context; actions bind ordinary functions and file scopes, independent of LSP, build, or run. Version 2 adds multiple icons, up to 4 `alternatives`, and a click result `reveal`; the host declares the usable icons in `services.editor.options.actionIcons` | +| `editor/registerProviders` | `editor.providers` + `documents.read` | API-25 | Discover the method and `services.editor.options.providerTypes` with `getCapabilities`, then register per workspace context; binds the 7 types `definition`, `hover`, `completion`, `codeLens`, `foldingRange`, `semanticTokens`, and `documentLink` to ordinary functions, and a type outside `providerTypes` returns `unsupportedOption`; language-server results come first and plugin results only add to them; the host sets the budgets | + +Every method takes `context: {contextID, generation}`; except for `documents/registerContentProvider`, a remote workspace context returns `plugin.host.authorityUnsupported`. `documents/registerContentProvider` is described in the next section. + +### Content providers (API-12) + +A language server may hand out documents that have no file (such as the decompiled source of a library class). The plugin serves them as a URI scheme with `documents/registerContentProvider`, granted by `documents.contentProvider` (needs `backend` 2). + +| Method | Permission | API | Params | Result | +| --- | --- | --- | --- | --- | +| `documents/registerContentProvider` | `documents.contentProvider` | API-12 | `{context, scheme, function}` | `{subscriptionID}` | -Every method takes `context: {contextID, generation}`; a remote workspace context returns `plugin.host.authorityUnsupported`. +- Registration only records the binding and never calls the backend; `function` must already be registered on the session. Withdraw it with the existing `subscriptions/dispose`. +- `scheme` is 1 to 64 lowercase ASCII letters, digits, `+`, `-`, or `.`, and starts with a letter. The schemes the host or the platform resolves are refused: `about`, `blob`, `data`, `file`, `ftp`, `http`, `https`, `javascript`, `lithe`, `mailto`, `untitled`, `ws`, and `wss`. A malformed or reserved scheme, or a `function` that is not a function ID, returns `plugin.host.invalidParams`. +- A scheme has one provider per workspace window; registering it again returns `plugin.host.preconditionFailed`. +- When a document whose URI has that scheme is opened, the host calls the function through `lithe/function/invoke` with the workspace `context` and `{uri}`. The function answers a string, or `{content: string}`, of at most 1 MiB, and the document opens read-only. Any other answer, a failure, or a timeout (the host waits 30 seconds) shows the user an error and opens nothing. +- The provider is the only source of the text: the host keeps no copy once the document closes and asks again when it is reopened. Ending the session or closing the context withdraws the provider. +- Only `documents.contentProvider` grants this method; the aggregate service name `documents` and other grants do not, and return `plugin.host.permissionDenied` (`-32010`). ### Contract sections | Section | Content | | --- | --- | | [Public document, file and window methods (protocol version 3)](../../contract/v1/README.md#public-document-file-and-window-methods-protocol-version-3) | Parameters, limits, and failure codes for document, edit, open, preview, and browser methods | -| [Independent editor actions (protocol version 3)](../../contract/v1/README.md#independent-editor-actions-protocol-version-3) | Registering, scoping, invoking, and withdrawing `editor/registerActions` | +| [Independent editor actions (protocol version 3)](../../contract/v1/README.md#independent-editor-actions-protocol-version-3) | Registering, scoping, invoking, and withdrawing `editor/registerActions`, plus version 2 icons, `alternatives`, and click results | +| [Editor language-feature providers (protocol version 3)](../../contract/v1/README.md#editor-language-feature-providers-protocol-version-3) | Registering `editor/registerProviders`, selectors, result shapes, budgets, and merge rules | +| [Content providers](../../contract/v1/README.md#content-providers) | Scheme rules, invoking, read-only documents, and withdrawing for `documents/registerContentProvider` | | [Workspace contexts and capability discovery (protocol version 3)](../../contract/v1/README.md#workspace-contexts-and-capability-discovery-protocol-version-3) | `contextID`/`generation` and `getCapabilities` | | [Outcomes and error codes](../../contract/v1/README.md#outcomes-and-error-codes) | Outcomes and error codes | @@ -93,8 +132,11 @@ Every method takes `context: {contextID, generation}`; a remote workspace contex | File | Content | | --- | --- | | [`editor/editor-actions.plugin.json`](../../contract/v1/fixtures/editor/editor-actions.plugin.json) | `backend` 2 with `editor.actions` and `documents.read`: registers editor actions after `onWorkspaceOpen` | +| [`editor/editor-actions-v2.json`](../../contract/v1/fixtures/editor/editor-actions-v2.json) | Core cases for version 2 actions: the host-declared icons and source limit, `alternatives`, click-result `reveal`, and results refused whole | +| [`editor/editor-providers.plugin.json`](../../contract/v1/fixtures/editor/editor-providers.plugin.json) | `backend` 2 with `editor.providers` and `documents.read`: registers editor language-feature providers after `onWorkspaceOpen` | +| [`editor/editor-providers.json`](../../contract/v1/fixtures/editor/editor-providers.json) | Core cases for registrations and results: selector and type validation (`unsupportedOption`), result shapes for the 7 types, and dropped results | | [`backend/command-backend.plugin.json`](../../contract/v1/fixtures/backend/command-backend.plugin.json) | A `ui` 4 command that reads and edits documents | -| [`backend/function-protocol.json`](../../contract/v1/fixtures/backend/function-protocol.json) | Canonical requests and replies for `editor/applyEdits` and `editor/registerActions`, plus host failure codes such as `versionConflict` and `readOnly` | +| [`backend/function-protocol.json`](../../contract/v1/fixtures/backend/function-protocol.json) | Canonical requests and replies for `editor/applyEdits` and `editor/registerActions`, plus host failure codes such as `versionConflict` and `readOnly`; also the `documents/registerContentProvider` cases: it needs the `documents.contentProvider` grant, the aggregate `documents` name is not enough, reserved schemes, an uppercase or digit-first scheme, a malformed function name, and a missing `context` | ### Related guides diff --git a/reference/domains/languages.md b/reference/domains/languages.md index 7c052f4..a5f6805 100644 --- a/reference/domains/languages.md +++ b/reference/domains/languages.md @@ -4,27 +4,133 @@ ## 中文 -让 Lithe 认识一种语言:识别文件,用包内 Monarch 语法高亮,启动 LSP 语言服务器,提供“运行当前文件”和测试。全部是数据声明,宿主负责进程生命周期、LSP 会话和停止进程,插件不需要写宿主代码。 +让 Lithe 认识一种语言:识别文件,用包内 Monarch 语法高亮,启动 LSP 语言服务器,提供“运行当前文件”和测试。全部是数据声明,宿主负责进程生命周期、LSP 会话和停止进程,插件不需要写宿主代码。命令行要到启动时才能确定、或后端需要驱动自己的服务器时,可以让插件后端回答启动命令,并在自己声明的范围内向服务器发请求(见下文“后端决定启动方式”和“插件自有会话”)。 | 项目 | 内容 | | --- | --- | -| 清单字段 | `languages`、`languageServers`、`executionProviders` | -| 能力版本 | `languages` 1–2、`languageServers` 1–2、`serverProcess` 1–2、`executionProviders` 1,见[版本对照](../capabilities-and-versions.md#能力版本requires) | -| 协议 | LSP;所有插件进程都可以协商 [`litheLogging` 1](../capabilities-and-versions.md#进程协议版本) | -| API 编号 | [API-25](../api-catalog.md#现在能用的-api):语言、语法、LSP | -| 权限 | `languageServer.launch`;声明 `run`、`test` 时加 `process.run`、`process.test` | +| 清单字段 | `languages`、`languageServers`、`executionProviders`、`outputLinkMatchers` | +| 能力版本 | `languages` 1–2、`languageServers` 1–3、`serverProcess` 1–4、`executionProviders` 1、`outputLinkMatchers` 1,见[版本对照](../capabilities-and-versions.md#能力版本requires) | +| 协议 | LSP;所有插件进程都可以协商 [`litheLogging` 1](../capabilities-and-versions.md#进程协议版本)。后端驱动服务器用的 `languageServer/*` 宿主方法在函数协议 3 里,见[插件后端](backend.md) | +| API 编号 | [API-25](../api-catalog.md#现在能用的-api):语言、语法、LSP。[API-12](../api-catalog.md#现在能用的-api):按 URI scheme 提供的只读文档(`documents/registerContentProvider`)。[API-20](../api-catalog.md#现在能用的-api):语言服务器按工作区的状态目录及其回收。[API-22](../api-catalog.md#现在能用的-api):语言服务器的准备状态(`languageServer/setStatus`)和 Run、测试控制台输出里的链接(`outputLinkMatchers`)。插件函数提供的编辑器语言功能见[编辑器](editor.md)的 `editor/registerProviders` | +| 权限 | `languageServer.launch`;声明 `run`、`test` 时加 `process.run`、`process.test`;`languageServer.request`(`languageServers` 3 的服务器列出 `requestMethods` 或 `forwardNotifications` 时才可声明,需要 `backend` 2);`documents.contentProvider`(后端注册只读文档内容提供者,需要 `backend` 2) | | 激活事件 | `onLanguage:`、`onRun:`、`onTest:` | -| Windows | 级别 3 清单按宿主能力表准入:`languages` 1、`languageServers` 1–2、`serverProcess` 1。不支持 `languages` 2 的语法,也不声明 `executionProviders` | +| macOS | `languageServers` 3 的会话、`serverProcess` 3 的后端决定启动方式和 `outputLinkMatchers` 只在 macOS 实现;后端决定启动方式的服务器不在远程工作区启动(远程后端启动未支持)。宿主目前只声明 `serverProcess` 1 和 3:版本 4 已由 Core 校验,宿主尚未声明,需要它的插件版本暂时装不上 | +| Windows | 级别 3 清单按宿主能力表准入:`languages` 1、`languageServers` 1–2、`serverProcess` 1。不支持 `languages` 2 的语法,也不声明 `executionProviders`、`backend`、`outputLinkMatchers`,因此没有 `languageServers` 3 的会话、`serverProcess` 3–4 和输出链接 | ### 各级别的变化 | 版本 | 新增 | | --- | --- | | 级别 1 | 语言、`packaged` 和 `systemCommand` 语言服务器、`node` 和 `go` 运行时依赖、运行和测试提供者 | -| 级别 2 | `runtime` 启动(包内运行时或宿主发现的运行时)、`java` 运行时、启动模板、`initializationOptions`、宿主集成(只限官方插件) | +| 级别 2 | `runtime` 启动(包内运行时或宿主发现的运行时)、`java` 运行时、启动模板、`initializationOptions`、宿主集成(所有插件都可以声明) | | `languages` 2 | 包内 Monarch 语法 `grammar` 与编辑器配置 `configuration`;只有语法的插件不需要任何进程 | | `languageServers` 2 | `workspaceConfiguration`:Core 按设置展开后回答 `workspace/configuration`,`immediate` 设置变化时不重启即可推送 | +| `languageServers` 3 | 插件自有会话字段 `requestMethods`、`commands`、`serializedCommands`、`forwardNotifications`、`state`、`remote`:后端只能通过服务器声明的内容驱动它;会话策略由 Core 执行 | | `serverProcess` 2 | `runSelectedRuntime`,只给调试适配器用,见[调试适配器](debug-adapters.md) | +| `serverProcess` 3 | 语言服务器的 `launch.kind: "backend"`:命令行由插件后端在每次启动时回答;需要 `backend` 2 和不低于 4 的 `pluginAPIVersion` | +| `serverProcess` 4 | 运行时按工具定义 ID 引用(顶层 `requiresTools`),不再用宿主的封闭运行时列表,见[工具链](toolchains.md) | +| `outputLinkMatchers` 1 | 清单顶层 `outputLinkMatchers[]`:插件声明匹配规则,让 Run 和测试控制台的输出出现链接,见下文“输出链接” | + +### 宿主集成(API 2) + +语言服务器的 `hostIntegration` 对所有插件开放,不限命名空间。集成是宿主代码,但它只读取声明插件自己包里的资源,交给同一个插件启动的语言服务器或辅助进程,除了运行插件自己的服务器进程(安装确认时用户已同意)之外不授予任何权限。契约目前只列出 `jdt`;宿主没有实现的 ID 返回 `plugin.integration.unsupported`。构建系统的 `hostIntegration` 是另一个声明,仍只限官方插件,见[构建系统](build-systems.md)。 + +### 后端决定启动方式(`serverProcess` 3) + +服务器的命令行取决于启动时才知道的东西(解析出的版本、引导出来的工具、从工程推导的选项)时,不写死 `launch`,让插件后端每次回答: + +```json +"launch": {"kind": "backend", "function": "launch.resolve", "timeoutMilliseconds": 30000} +``` + +| 项目 | 规则 | +| --- | --- | +| 适用 | 只有语言服务器能用。`function` 是插件函数 ID;`timeoutMilliseconds` 为 1000–120000,默认 30000 | +| 其他字段 | 服务器不能再声明 `arguments`、`validationArguments`、`runtimeDependencies`、`initializationOptions` 和 `hostIntegration`:前四项由后端的应答携带,宿主集成是宿主代码,正是这种启动方式要避免的。`workspaceConfiguration` 仍可声明式给出,除非应答自带 | +| 启动计划 | `plugins.declarative.languageServerLaunchPlan` 返回第一步而不是命令行:`launch: {kind: "backend", function, timeoutMilliseconds}`、`resolveParams`、`requiredDirectories`(固定为 `state`、`workspaceState`、`log`、`temporary`)、`settings`、`sessionPolicy`。请求要带绝对路径的 `temporaryDirectory`,否则是 `plugin.launch.invalidRequest` | +| 调用 | 宿主建好四个目录,必要时启动插件后端,用 `lithe/function/invoke` 调用 `function`,参数是 `resolveParams`(`serverID`、`platform`、四个目录、`settings`)和工作区 `context`。调用失败、超时,或工作区代次已变,这次启动就不产生进程 | +| 应答 | `executable`(`{kind: "packaged", path}` 是包内只读文件;`{kind: "tool", tool}` 是插件自己定义的工具,由宿主按工具解析)、`arguments`、`environment`、`initializationOptions`、`workspaceConfiguration`、`gates`(最多 8 个)、`writablePaths`(最多 32 个,都在四个目录之内);不认识的字段会被拒绝 | +| 解码 | 宿主把 `{manifest, languageID, request, result}` 交给 `plugins.languageServers.decodeLaunch`,得到普通的启动计划(`launch` 为 `packaged` 或 `tool`),`sessionPolicy` 另带应答的 `gates` | +| 拒绝码 | `plugin.launch.invalidResult`、`plugin.launch.outsidePackage`、`plugin.launch.undeclaredTool`、`plugin.launch.unwritablePath`、`plugin.launch.invalidRequest`、`plugin.launch.unavailable`、`plugin.launch.notBackend` | +| macOS 现状 | `{kind: "tool"}` 应答按用户的工具选择和发现解析,位于项目内的程序会被拒绝(打开项目不能运行它);不在远程工作区启动;临时目录在服务器进程结束时删除 | + +### 插件自有会话(`languageServers` 3) + +后端只能通过服务器声明的内容驱动它自己的服务器。`languageServers` 3 的服务器可以声明: + +| 字段 | 含义 | +| --- | --- | +| `requestMethods` | 后端可以用 `languageServer/request`、`languageServer/notify` 发送的 LSP 方法,最多 64 个 | +| `commands` | 后端可以运行的 `workspace/executeCommand` 命令,最多 128 个;恰好在 `requestMethods` 列出 `workspace/executeCommand` 时声明 | +| `serializedCommands` | `commands` 的子集,一次只运行一个:同一命令已在运行时,新请求并入它,得到同一个答案 | +| `forwardNotifications` | 宿主转发给后端的服务器通知,最多 32 个 | +| `state` | `{scope: "workspace", retention: {maxEntries, maxAgeDays}}`,`maxEntries` 为 1–64,`maxAgeDays` 为 1–365 | +| `remote` | `supported`(默认)或 `unsupported`;`unsupported` 的服务器不为远程工作区启动 | + +- 名称是不含空白和控制字符、最多 256 字节的非空文本,在各自列表里唯一。宿主自己发送的方法(`initialize`、`initialized`、`shutdown`、`exit`、`textDocument/didOpen` 等文档同步方法、`workspace/didChangeWorkspaceFolders` 和所有 `$/` 方法)不能出现在 `requestMethods` 或 `forwardNotifications` 里。 +- 低于 `languageServers` 3 声明这些字段是 `plugin.manifest.invalid`。服务器列出 `requestMethods` 或 `forwardNotifications` 时必须声明 `languageServer.request`;没有这样的服务器却声明该权限,也是 `plugin.manifest.invalid`。 +- 启动计划的 `sessionPolicy` 就是这些字段(排序后的列表)加 `serverID`(即 `languageID`)和 `gates`。宿主原样交给 `lsp.startServer.sessionPolicy`,由 Core 的 LSP 引擎执行:宿主不能放宽,后端不能越界。 +- 请求必须在 `requestMethods` 里,`workspace/executeCommand` 的 `params.command` 必须在 `commands` 里,否则是 `plugin.host.methodNotDeclared`。通知可以是 `workspace/didChangeConfiguration`,或 `requestMethods` 里除 `workspace/executeCommand` 以外的方法;宿主自己的方法仍然被拒绝。 +- 会话必须属于调用方插件,并且正在运行;请求等待最多 `timeoutMilliseconds`(默认 60000,最大 300000),取消时向服务器发 `$/cancelRequest`。服务器的 JSON-RPC 错误不转发 `data`。 +- **gate** 是 `{id, until: "notification", method, match}`,由启动应答声明:服务器发出该 `method` 的通知、且参数包含 `match` 的所有成员时才打开。所有 gate 打开之前,`request` 按顺序等待,最多 128 个,第 129 个是 `plugin.host.busy`;通知从不被挡住。 +- 服务器停止、崩溃、重启或插件被停用时,所有未完成的请求以 `plugin.host.languageServerUnavailable` 结束;后端在 `didChangeState` 报告新的 `generation` 的 `running` 之后再请求。 + +**状态目录与回收(API-20)。** 宿主在插件的存储目录下(不在插件包或应用包里)为每个服务器、每个工作区保留一个状态目录,并把它作为 `workspaceStateDirectory` 交给服务器;停用和更新插件都不会删除。目录内的子目录名由插件决定,每个工作区目录本身的名字由宿主决定,宿主从不查看里面的内容。宿主每次启动服务器时刷新该目录的修改时间,并在启动后于后台回收:先用 Core 的 `plugins.languageServers.stateRetention` 决定要删除的目录,即超过 `maxAgeDays` 的,以及其余目录里扣除打开的工作区占用的名额后只保留最新 `maxEntries` 个以外的。打开的工作区从不被回收,没有声明 `state.retention` 的插件保留全部。这一回收仅 macOS。 + +### 宿主方法与通知 + +后端用这些方法和通知与自己的语言服务器交谈;方法名省略前缀 `lithe/host/`。`languageServer/*` 的每个方法都带 `context: {contextID, generation}` 和 `serverID`(本插件某个服务器的 `languageID`),别的插件的服务器返回 `plugin.host.foreignContribution`。 + +| API | 方法 | 用途 | 权限 | +| --- | --- | --- | --- | +| API-25 | `languageServer/request` | 向本插件自己的语言服务器发 JSON-RPC 请求,参数加 `method`、`params?`、`timeoutMilliseconds?`(1–300000,默认 60000),返回 `{value}`(服务器的 `result`,可以是 `null`);受会话策略检查 | `languageServer.request` | +| API-25 | `languageServer/notify` | 向本插件自己的语言服务器发通知,参数加 `method`、`params?`,返回 `{delivered}`;只有服务器初始化前发出的 `workspace/didChangeConfiguration` 是 `false`,这时值会随 `initialized` 送出 | `languageServer.request` | +| API-22、API-25 | `languageServer/setStatus` | 在宿主状态界面显示插件自己的准备状态(导入工程、建索引):`state` 为 `preparing`、`ready`、`degraded` 或 `failed`,可带 `message?`(原样显示,截到 1024 字节)和 `percentage?`(0–100 的整数),返回 `{}`;服务器停止时宿主移除它。它不同于只属于正在运行的调用的 `lithe/function/progress` | `languageServer.request` | +| API-12 | `documents/registerContentProvider` | 把一个 URI scheme 绑定到后端函数:打开该 scheme 的文档时,宿主调用函数取只读内容(不超过 1 MiB,宿主等 30 秒);每个工作区窗口每个 scheme 只能有一个提供者,返回 `{subscriptionID}`。不带 `serverID`,用 `subscriptions/dispose` 撤销 | `documents.contentProvider` | + +| API | 通知 | 含义 | 权限 | +| --- | --- | --- | --- | +| API-25 | `languageServer/didChangeState` | `{context, serverID, state, generation}`;`state` 为 `starting`、`running`、`stopped` 或 `crashed` | `languageServer.request` | +| API-25 | `languageServer/didReceiveNotification` | `{context, serverID, generation, method, params?, truncated?}`:服务器发出且 `forwardNotifications` 列出的通知;超过 `limits.notificationBytes` 时只送一次 `truncated: true`,不带 `params` | `languageServer.request` | + +`generation` 是宿主每次为这个上下文启动服务器时递增的正整数,后端据此分辨状态或通知属于哪个进程;新进程启动后,宿主不再送旧进程的通知。 + +这些方法的错误码: + +| 错误码 | JSON-RPC | 含义 | +| --- | --- | --- | +| `plugin.host.methodNotDeclared` | `-32024` | 方法或 `workspace/executeCommand` 命令不在服务器声明的会话里 | +| `plugin.host.languageServerUnavailable` | `-32022` | 服务器没有为这个上下文运行,或请求进行中服务器已停止 | +| `plugin.host.languageServerError` | `-32023` | 服务器以 JSON-RPC 错误应答;`details` 是 `{code, message}` | +| `plugin.host.foreignContribution` | `-32026` | 指名的服务器属于别的插件 | +| `plugin.host.timeout` | `-32015` | 超过宿主的截止时间 | +| `plugin.host.cancelled` | `-32800` | 请求被取消 | +| `plugin.host.busy` | `-32020` | 第 129 个等待中的请求(见 gate) | +| `plugin.host.limitExceeded` | `-32014` | 结果超过宿主的上限 | +| `plugin.host.preconditionFailed` | `-32019` | `documents/registerContentProvider`:这个 scheme 已有提供者 | + +### 输出链接(`outputLinkMatchers` 1) + +`outputLinkMatchers[]` 让插件把 Run 和测试控制台里的文字变成可点击的链接,例如回溯里的一帧或 panic 的位置。宿主不认识任何工具的输出格式:插件声明匹配规则,Core 找出匹配。需要 `requires.outputLinkMatchers` 1。只声明匹配器本身就是完整的插件;不带 `resolve` 的匹配器不需要后端。 + +```json +"outputLinkMatchers": [ + { "id": "traceback", "pattern": "File \"(?P[^\"]+)\", line (?P\\d+)", "scope": "run" } +] +``` + +| 字段 | 含义 | +| --- | --- | +| `id` | 插件内的 ID:小写字母开头,后面最多 63 个小写字母、数字、`.`、`_` 或 `-`,在插件内唯一。每个插件最多 16 个匹配器 | +| `pattern` | 对控制台的一行做匹配的正则,1–512 字节,使用 Rust `regex` 语法,所以没有反向引用和环视,编译后的大小也有上限。最多命名 8 个分组,每个是 camelCase 名称,且不能匹配空文本 | +| `scope` | `run` 或 `test`:匹配器作用的控制台。缺省表示两者 | +| `resolve.function` | 可选,插件 `backend` 2 进程里的函数 ID;需要 `backend` 2 | + +- 不带 `resolve` 时,规则必须捕获 `path` 分组,可以捕获 `line` 和 `column`(从 1 开始),不接受其他分组名。相对 `path` 对每个工作区根生成一个候选,绝对路径只在某个工作区根之内有效,离开所有根的路径不生成链接:控制台里的一行不可能打开工作区之外的文件。 +- 带 `resolve` 时,规则可以命名任意分组。用户点击时宿主调用该函数,参数是 `{groups}`(按名称的捕获文本)和窗口的 `context`,后端返回 `{uri, range?}` 或 `null`。 +- 匹配由 Core 决定,所有宿主链接同样的文字:`outputLinks.match` 接收 `{scope, platform, caseSensitive?, workspaceRoots, sources: [{pluginID, matchers}], lines}`,返回 `{links}`。每个链接是 `{line, start, length, pluginID, matcherID}` 加 `location`(`{paths, line?, column?}`,宿主打开第一个存在的路径)或 `resolve`(`{function, groups}`)。`start` 和 `length` 按该行的 UTF-16 代码单元计数。按来源顺序、再按声明顺序找匹配,与前面链接重叠的匹配被跳过。一次请求最多 256 行、64 个来源、16 个根;每行最多扫描 4096 字节,最多 8 个链接。 +- `outputLinks.decodeResolution` 接收 `{reply}`(函数的应答),返回要打开的位置 `{path, line?, column?}`(从 1 开始)或 `null`。应答只会打开 `file:` URI,只接受 `uri` 和 `range` 两个成员;从 0 开始的 `range.start` 会被转换。 +- 宿主只对显示出来的行做匹配,不对没有启用该插件的输出做匹配,并忽略文件不存在的链接。只在 macOS 实现(API-22)。 ### 契约章节 @@ -32,11 +138,17 @@ | --- | --- | | [Languages](../../contract/v1/README.md#languages) | 语言 ID、扩展名、文件名、项目标记、依赖路径 | | [Grammars and editor configuration (`languages` 2)](../../contract/v1/README.md#grammars-and-editor-configuration-languages-2) | Monarch 语法的上限和拒绝规则、注释与括号配置、只在可终止的 worker 中运行 | -| [Language servers](../../contract/v1/README.md#language-servers) | 启动方式、参数、探测参数、运行时依赖 | +| [Language servers](../../contract/v1/README.md#language-servers) | 启动方式(含 `backend`)、参数、探测参数、运行时依赖、会话字段 | | [Runtime launch (API 2)](../../contract/v1/README.md#runtime-launch-api-2) | 用 `java` 或 `node` 启动包内入口文件,可以自带运行时 | -| [Host integrations (API 2)](../../contract/v1/README.md#host-integrations-api-2) | 数据表达不了的行为由宿主代码实现,只限官方插件按名称声明 | +| [Tool references (`serverProcess` 4)](../../contract/v1/README.md#tool-references-serverprocess-4) | 按工具定义 ID 引用运行时、启动计划里的 `tool` 启动方式 | +| [Host integrations (API 2)](../../contract/v1/README.md#host-integrations-api-2) | 数据表达不了的行为由宿主代码实现,所有插件都可以按名称声明;宿主没有实现的 ID 返回 `plugin.integration.unsupported`。构建系统的宿主集成仍只限官方插件 | | [Launch templates (API 2)](../../contract/v1/README.md#launch-templates-api-2) | `${pluginDir}`、`${stateDir}`、`${workspaceStateDir}` 等模板变量 | | [Workspace configuration (`languageServers` 2)](../../contract/v1/README.md#workspace-configuration-languageservers-2) | `workspaceConfiguration` 的展开与推送 | +| [Backend-resolved launch (`serverProcess` 3)](../../contract/v1/README.md#backend-resolved-launch-serverprocess-3) | 后端回答命令行:`resolveParams`、应答字段、拒绝码、macOS 现状 | +| [Plugin-owned sessions (`languageServers` 3)](../../contract/v1/README.md#plugin-owned-sessions-languageservers-3) | 会话字段、会话策略、gate、状态目录与回收、准备状态 | +| [Language server host methods](../../contract/v1/README.md#language-server-host-methods) | `languageServer/request`、`notify`、`setStatus` 和两个通知 | +| [Content providers](../../contract/v1/README.md#content-providers) | `documents/registerContentProvider`:按 URI scheme 提供只读文档 | +| [Output link matchers (`outputLinkMatchers` 1)](../../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1) | Run 与测试控制台输出里的链接、`outputLinks.match`、`outputLinks.decodeResolution` | | [Execution providers](../../contract/v1/README.md#execution-providers) | `run`、`test` 的可执行文件、参数模板、测试发现 | ### 示例与用例 @@ -48,9 +160,13 @@ | [`languages/node-runtime.plugin.json`](../../contract/v1/fixtures/languages/node-runtime.plugin.json) | 级别 2:用 `node` 运行时启动,带插件设置 | | [`languages/language-host-integration.plugin.json`](../../contract/v1/fixtures/languages/language-host-integration.plugin.json) | 级别 2:声明宿主集成的语言服务器(Java) | | [`languages/language-grammar.plugin.json`](../../contract/v1/fixtures/languages/language-grammar.plugin.json) | 级别 3、`languages` 2:只有语法、不启动进程的插件 | +| [`languages/language-server-backend-launch.plugin.json`](../../contract/v1/fixtures/languages/language-server-backend-launch.plugin.json) | 级别 3、`serverProcess` 3 与 `languageServers` 3:命令行由后端回答、声明会话字段和 `languageServer.request` 的语言服务器 | | [`languages/grammars/http.monarch.json`](../../contract/v1/fixtures/languages/grammars/http.monarch.json) | 完整的 Monarch 语法示例 | | [`languages/command-plans.json`](../../contract/v1/fixtures/languages/command-plans.json) | 35 个标准答案:运行/测试模板和语言服务器启动计划的展开结果 | | [`languages/runtime-versions.json`](../../contract/v1/fixtures/languages/runtime-versions.json) | 12 个标准答案:`plugins.declarative.checkRuntimeVersion` 的运行时版本判断 | +| [`languages/language-server-launch-resolution.json`](../../contract/v1/fixtures/languages/language-server-launch-resolution.json) | 17 个标准答案:后端解析启动的第一步(`languageServerLaunchPlan`)和解码后端应答(`plugins.languageServers.decodeLaunch`)得到的启动计划或拒绝码 | +| [`run/output-link-matchers.json`](../../contract/v1/fixtures/run/output-link-matchers.json) | `outputLinks.match` 的请求与返回的链接,以及匹配器 `resolve` 函数的回复经 `outputLinks.decodeResolution` 得到的结果 | +| [`run/tool-reference-launch.plugin.json`](../../contract/v1/fixtures/run/tool-reference-launch.plugin.json) | 语言服务器的 `runtime` 启动按工具 ID `interpreter` 引用运行时(`serverProcess` 4),并带一个 `outputLinkMatchers` 条目 | 语言相关的拒绝用例在 [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json)。 @@ -63,27 +179,133 @@ ## English -Teach Lithe a language: recognize its files, highlight them with a packaged Monarch grammar, start an LSP language server, and offer "run current file" and tests. Everything is declared as data. The host owns the process lifecycle, the LSP session, and stopping the process; the plugin writes no host code. +Teach Lithe a language: recognize its files, highlight them with a packaged Monarch grammar, start an LSP language server, and offer "run current file" and tests. Everything is declared as data. The host owns the process lifecycle, the LSP session, and stopping the process; the plugin writes no host code. When the command line is only known at launch time, or the backend needs to drive its own server, the plugin backend can answer the launch command and send requests to the server within what the plugin declares (see "Backend-resolved launch" and "Plugin-owned sessions" below). | Item | Value | | --- | --- | -| Manifest fields | `languages`, `languageServers`, `executionProviders` | -| Capability versions | `languages` 1–2, `languageServers` 1–2, `serverProcess` 1–2, `executionProviders` 1; see [versions](../capabilities-and-versions.md#capability-versions-requires) | -| Protocols | LSP; every plugin process may negotiate [`litheLogging` 1](../capabilities-and-versions.md#process-protocol-versions) | -| API IDs | [API-25](../api-catalog.md#apis-usable-now): languages, grammars, LSP | -| Permissions | `languageServer.launch`; `process.run` and `process.test` when `run` or `test` is declared | +| Manifest fields | `languages`, `languageServers`, `executionProviders`, `outputLinkMatchers` | +| Capability versions | `languages` 1–2, `languageServers` 1–3, `serverProcess` 1–4, `executionProviders` 1, `outputLinkMatchers` 1; see [versions](../capabilities-and-versions.md#capability-versions-requires) | +| Protocols | LSP; every plugin process may negotiate [`litheLogging` 1](../capabilities-and-versions.md#process-protocol-versions). The `languageServer/*` host methods a backend uses to drive a server are in function protocol 3; see [Plugin backend](backend.md) | +| API IDs | [API-25](../api-catalog.md#apis-usable-now): languages, grammars, LSP. [API-12](../api-catalog.md#apis-usable-now): read-only documents served by URI scheme (`documents/registerContentProvider`). [API-20](../api-catalog.md#apis-usable-now): per-workspace language-server state directories and their pruning. [API-22](../api-catalog.md#apis-usable-now): language-server preparation status (`languageServer/setStatus`) and links in Run and Test console output (`outputLinkMatchers`). Editor language features provided by plugin functions are covered by `editor/registerProviders` in [Editor](editor.md) | +| Permissions | `languageServer.launch`; `process.run` and `process.test` when `run` or `test` is declared; `languageServer.request` (declarable only when a `languageServers` 3 server lists `requestMethods` or `forwardNotifications`; needs `backend` 2); `documents.contentProvider` (the backend registers a read-only document content provider; needs `backend` 2) | | Activation events | `onLanguage:`, `onRun:`, `onTest:` | -| Windows | Level 3 manifests are admitted by the host table: `languages` 1, `languageServers` 1–2, `serverProcess` 1. No `languages` 2 grammars, and `executionProviders` is not advertised | +| macOS | The `languageServers` 3 sessions, the `serverProcess` 3 backend-resolved launch, and `outputLinkMatchers` are implemented on macOS only; a server whose launch the backend resolves does not start in a remote workspace (remote backend launch is unsupported). The host currently advertises only `serverProcess` 1 and 3: version 4 is validated by Core but the host does not advertise it yet, so a plugin version that needs it cannot be installed for now | +| Windows | Level 3 manifests are admitted by the host table: `languages` 1, `languageServers` 1–2, `serverProcess` 1. No `languages` 2 grammars, and none of `executionProviders`, `backend`, or `outputLinkMatchers` is advertised, so there are no `languageServers` 3 sessions, no `serverProcess` 3–4, and no output links | ### What each version adds | Version | Adds | | --- | --- | | Level 1 | Languages, `packaged` and `systemCommand` language servers, `node` and `go` runtime dependencies, run and test providers | -| Level 2 | `runtime` launches (packaged or host-discovered runtime), the `java` runtime, launch templates, `initializationOptions`, host integrations (official plugins only) | +| Level 2 | `runtime` launches (packaged or host-discovered runtime), the `java` runtime, launch templates, `initializationOptions`, host integrations (any plugin may declare one) | | `languages` 2 | Packaged Monarch `grammar` and editor `configuration`; a grammar-only plugin needs no process | | `languageServers` 2 | `workspaceConfiguration`: Core expands it from settings, answers `workspace/configuration`, and pushes `immediate` setting changes without a restart | +| `languageServers` 3 | Plugin-owned session fields `requestMethods`, `commands`, `serializedCommands`, `forwardNotifications`, `state`, `remote`: the backend drives its server only through what the server declares; Core enforces the session policy | | `serverProcess` 2 | `runSelectedRuntime`, used only by debug adapters; see [Debug adapters](debug-adapters.md) | +| `serverProcess` 3 | A language server's `launch.kind: "backend"`: the plugin backend answers the command line at each start; needs `backend` 2 and a `pluginAPIVersion` of at least 4 | +| `serverProcess` 4 | Runtimes are referenced by tool-definition ID (top-level `requiresTools`) instead of the host's closed runtime lists; see [Toolchains](toolchains.md) | +| `outputLinkMatchers` 1 | Top-level `outputLinkMatchers[]`: a plugin declares patterns that turn Run and Test console output into links; see "Output links" below | + +### Host integrations (API 2) + +A language server's `hostIntegration` is open to every plugin, whatever its namespace. An integration is host code, but it reads only resources inside the declaring plugin's own package and hands them to the language server or helper process that same plugin starts; it grants no permission beyond running the plugin's own server process, which the user already agreed to at the install review. The contract lists only `jdt` today; an ID the host does not implement returns `plugin.integration.unsupported`. A build system's `hostIntegration` is a different declaration and stays limited to official plugins; see [Build systems](build-systems.md). + +### Backend-resolved launch (`serverProcess` 3) + +When a server's command line depends on something only known at launch time (a version the plugin resolves, a tool it bootstraps, options derived from the project), do not hard-code `launch`. Let the plugin backend answer each time: + +```json +"launch": {"kind": "backend", "function": "launch.resolve", "timeoutMilliseconds": 30000} +``` + +| Item | Rule | +| --- | --- | +| Applies to | Language servers only. `function` is a plugin function ID; `timeoutMilliseconds` is 1000–120000, default 30000 | +| Other fields | The server cannot also declare `arguments`, `validationArguments`, `runtimeDependencies`, `initializationOptions`, or `hostIntegration`: the answer carries the first four, and a host integration is host code, which this launch exists to avoid. `workspaceConfiguration` can stay declarative unless the answer carries its own | +| Launch plan | `plugins.declarative.languageServerLaunchPlan` returns a first step, not a command line: `launch: {kind: "backend", function, timeoutMilliseconds}`, `resolveParams`, `requiredDirectories` (always `state`, `workspaceState`, `log`, and `temporary`), `settings`, and `sessionPolicy`. The request must carry an absolute `temporaryDirectory`, otherwise the result is `plugin.launch.invalidRequest` | +| Call | The host creates the four directories, starts the plugin backend if needed, and calls `function` through `lithe/function/invoke` with `resolveParams` (`serverID`, `platform`, the four directories, `settings`) and the workspace `context`. A failed or timed-out call, or a context whose generation has changed, ends this start without a process | +| Answer | `executable` (`{kind: "packaged", path}` is a read-only file in the package; `{kind: "tool", tool}` names a tool definition of the plugin itself, which the host resolves like any tool launch), `arguments`, `environment`, `initializationOptions`, `workspaceConfiguration`, `gates` (at most 8), `writablePaths` (at most 32, all inside the four directories); an unknown field is refused | +| Decoding | The host passes `{manifest, languageID, request, result}` to `plugins.languageServers.decodeLaunch`, which returns an ordinary launch plan (`launch` is `packaged` or `tool`); the plan's `sessionPolicy` also carries the answer's `gates` | +| Rejection codes | `plugin.launch.invalidResult`, `plugin.launch.outsidePackage`, `plugin.launch.undeclaredTool`, `plugin.launch.unwritablePath`, `plugin.launch.invalidRequest`, `plugin.launch.unavailable`, `plugin.launch.notBackend` | +| macOS today | A `{kind: "tool"}` answer is resolved with the user's tool choice and discovery, and a program inside the project is refused (opening a project must not run it); it does not start in a remote workspace; the temporary directory is removed when the server process ends | + +### Plugin-owned sessions (`languageServers` 3) + +A plugin backend drives its own server only through what the server declares. A `languageServers` 3 server can declare: + +| Field | Meaning | +| --- | --- | +| `requestMethods` | LSP methods the backend may send with `languageServer/request` or `languageServer/notify`; at most 64 | +| `commands` | `workspace/executeCommand` commands the backend may run; at most 128; declared exactly when `requestMethods` lists `workspace/executeCommand` | +| `serializedCommands` | A subset of `commands` that runs one at a time: a request for a command that is already running joins it and receives the same answer | +| `forwardNotifications` | Server notifications the host forwards to the backend; at most 32 | +| `state` | `{scope: "workspace", retention: {maxEntries, maxAgeDays}}`; `maxEntries` is 1–64, `maxAgeDays` is 1–365 | +| `remote` | `supported` (default) or `unsupported`; an `unsupported` server is not started for a remote workspace | + +- A name is non-empty text of at most 256 bytes without whitespace or control characters, unique within its list. Methods the host sends itself (`initialize`, `initialized`, `shutdown`, `exit`, the document-sync methods such as `textDocument/didOpen`, `workspace/didChangeWorkspaceFolders`, and every `$/` method) cannot appear in `requestMethods` or `forwardNotifications`. +- Declaring these fields below `languageServers` 3 is `plugin.manifest.invalid`. A server that lists `requestMethods` or `forwardNotifications` must declare `languageServer.request`; declaring the permission without such a server is also `plugin.manifest.invalid`. +- The launch plan's `sessionPolicy` is these fields (as sorted lists) plus `serverID` (the `languageID`) and `gates`. The host passes it unchanged as `lsp.startServer.sessionPolicy` and Core's LSP engine enforces it: the host cannot widen it and the backend cannot step outside it. +- A request must be in `requestMethods`, and a `workspace/executeCommand` must carry a `params.command` in `commands`; otherwise the result is `plugin.host.methodNotDeclared`. A notification may be `workspace/didChangeConfiguration` or a `requestMethods` entry other than `workspace/executeCommand`; the host's own methods are still refused. +- The session must belong to the calling plugin and be running. A request waits at most `timeoutMilliseconds` (default 60000, at most 300000); cancelling it sends `$/cancelRequest` to the server. A server's JSON-RPC error does not forward its `data`. +- A **gate** is `{id, until: "notification", method, match}`, declared by the launch answer. It opens when the server sends a notification with that `method` whose params contain every member of `match`. Until every gate has opened, `request` calls wait in order, at most 128; the 129th is `plugin.host.busy`. Notifications are never held. +- When the server stops, crashes, restarts, or the plugin is disabled, every pending request ends with `plugin.host.languageServerUnavailable`; the backend asks again after `didChangeState` reports `running` with a new `generation`. + +**State directories and pruning (API-20).** For each server and each workspace the host keeps one state directory under the plugin's storage (never under the package or the application bundle) and gives the server its own as `workspaceStateDirectory`. Disabling and updating the plugin do not delete it. The plugin chooses the sub-directory names inside it; the host chooses the name of each per-workspace directory and never looks inside. The host refreshes the directory's modification time at every server start and prunes in the background after a start: it asks Core's `plugins.languageServers.stateRetention` which directories to delete, namely those older than `maxAgeDays` and, of the rest, all but the newest `maxEntries` that open workspaces leave free. A workspace that is open is never pruned, and a plugin that declares no `state.retention` keeps everything. Pruning runs on macOS only. + +### Host methods and notifications + +A backend talks to its own language server with these methods and notifications; the `lithe/host/` prefix is omitted. Every `languageServer/*` method takes `context: {contextID, generation}` and `serverID` (the `languageID` of one of this plugin's servers); a server of another plugin is `plugin.host.foreignContribution`. + +| API | Method | Purpose | Permission | +| --- | --- | --- | --- | +| API-25 | `languageServer/request` | Send a JSON-RPC request to the plugin's own language server; extra params `method`, `params?`, `timeoutMilliseconds?` (1–300000, default 60000); returns `{value}` (the server's `result`, which may be `null`); checked against the session policy | `languageServer.request` | +| API-25 | `languageServer/notify` | Send a notification to the plugin's own language server; extra params `method`, `params?`; returns `{delivered}`, which is `false` only for a `workspace/didChangeConfiguration` sent before the server is initialized (the value is then sent with `initialized`) | `languageServer.request` | +| API-22, API-25 | `languageServer/setStatus` | Show the plugin's own preparation state (importing a project, indexing) in the host status UI: `state` is `preparing`, `ready`, `degraded`, or `failed`, with optional `message?` (shown as given, cut to 1024 bytes) and `percentage?` (a whole number 0–100); returns `{}`; the host removes it when the server stops. It is not `lithe/function/progress`, which belongs to a running call | `languageServer.request` | +| API-12 | `documents/registerContentProvider` | Bind a URI scheme to a backend function: when a document of that scheme is opened, the host calls the function for read-only content (at most 1 MiB, the host waits 30 seconds); one provider per scheme per workspace window; returns `{subscriptionID}`. Takes no `serverID`; revoke it with `subscriptions/dispose` | `documents.contentProvider` | + +| API | Notification | Meaning | Permission | +| --- | --- | --- | --- | +| API-25 | `languageServer/didChangeState` | `{context, serverID, state, generation}`; `state` is `starting`, `running`, `stopped`, or `crashed` | `languageServer.request` | +| API-25 | `languageServer/didReceiveNotification` | `{context, serverID, generation, method, params?, truncated?}`: a notification the server sent and `forwardNotifications` lists; above `limits.notificationBytes` it is delivered once with `truncated: true` and without `params` | `languageServer.request` | + +`generation` is a positive integer the host raises each time it starts the server for this context, so the backend can tell which process a state or notification belongs to; once a new process has started, the host no longer delivers notifications of the old one. + +Errors of these methods: + +| Error code | JSON-RPC | Meaning | +| --- | --- | --- | +| `plugin.host.methodNotDeclared` | `-32024` | The method or `workspace/executeCommand` command is not in the server's declared session | +| `plugin.host.languageServerUnavailable` | `-32022` | The server is not running for this context, or it stopped while the request was in flight | +| `plugin.host.languageServerError` | `-32023` | The server answered with a JSON-RPC error; `details` is `{code, message}` | +| `plugin.host.foreignContribution` | `-32026` | The named server belongs to another plugin | +| `plugin.host.timeout` | `-32015` | The host's deadline passed | +| `plugin.host.cancelled` | `-32800` | The request was cancelled | +| `plugin.host.busy` | `-32020` | The 129th waiting request (see gate) | +| `plugin.host.limitExceeded` | `-32014` | The result exceeds the host's bound | +| `plugin.host.preconditionFailed` | `-32019` | `documents/registerContentProvider`: the scheme already has a provider | + +### Output links (`outputLinkMatchers` 1) + +`outputLinkMatchers[]` lets a plugin make text in the Run and Test consoles clickable, such as a traceback frame or a panic location. The host knows no tool's output format: the plugin declares the pattern and Core finds the matches. Requires `requires.outputLinkMatchers` 1. Matchers alone are a complete plugin; a matcher without `resolve` needs no backend. + +```json +"outputLinkMatchers": [ + { "id": "traceback", "pattern": "File \"(?P[^\"]+)\", line (?P\\d+)", "scope": "run" } +] +``` + +| Field | Meaning | +| --- | --- | +| `id` | Plugin-local ID: a lowercase letter followed by up to 63 lowercase letters, digits, `.`, `_`, or `-`, unique in the plugin. At most 16 matchers per plugin | +| `pattern` | A regular expression of 1–512 bytes matched against one console line. It uses the Rust `regex` syntax, so it has no backreferences and no look-around, and its compiled size is limited. It names at most 8 groups, each a camelCase name, and must not match empty text | +| `scope` | `run` or `test`: the console the matcher applies to. Absent means both | +| `resolve.function` | Optional ID of a function of the plugin's `backend` 2 process; needs `backend` 2 | + +- Without `resolve`, the pattern must capture a `path` group and may capture `line` and `column` (1-based); no other group name is accepted. A relative `path` becomes one candidate per workspace root, an absolute one counts only inside a workspace root, and a path that leaves every root is not linked: a console line can never open a file outside the workspace. +- With `resolve`, the pattern may name any groups. When the user clicks, the host calls the function with `{groups}` (the captured text by name) and the window's `context`, and the backend answers `{uri, range?}` or `null`. +- Core owns the decision, so every host links the same text: `outputLinks.match` takes `{scope, platform, caseSensitive?, workspaceRoots, sources: [{pluginID, matchers}], lines}` and returns `{links}`. Each link is `{line, start, length, pluginID, matcherID}` plus either `location` (`{paths, line?, column?}`: the host opens the first existing path) or `resolve` (`{function, groups}`). `start` and `length` count UTF-16 code units of the line. Matches are found in source order and then declaration order, and a match overlapping an earlier link is skipped. A request carries at most 256 lines, 64 sources, and 16 roots; a line is scanned up to 4096 bytes and links at most 8 times. +- `outputLinks.decodeResolution` takes `{reply}` (what the function answered) and returns the location to open, `{path, line?, column?}` with 1-based positions, or `null`. A reply opens only a `file:` URI, only the members `uri` and `range` are accepted, and a 0-based `range.start` is converted. +- The host runs matching only for lines it displays, never for output of a plugin that is not enabled, and ignores a link whose file does not exist. Implemented on macOS only (API-22). ### Contract sections @@ -91,11 +313,17 @@ Teach Lithe a language: recognize its files, highlight them with a packaged Mona | --- | --- | | [Languages](../../contract/v1/README.md#languages) | Language IDs, extensions, file names, project markers, dependency paths | | [Grammars and editor configuration (`languages` 2)](../../contract/v1/README.md#grammars-and-editor-configuration-languages-2) | Monarch limits and rejections, comment and bracket configuration, worker-only execution | -| [Language servers](../../contract/v1/README.md#language-servers) | Launch kinds, arguments, probe arguments, runtime dependencies | +| [Language servers](../../contract/v1/README.md#language-servers) | Launch kinds (including `backend`), arguments, probe arguments, runtime dependencies, session fields | | [Runtime launch (API 2)](../../contract/v1/README.md#runtime-launch-api-2) | Start a packaged entry with `java` or `node`, optionally with a packaged runtime | -| [Host integrations (API 2)](../../contract/v1/README.md#host-integrations-api-2) | Behavior data cannot express lives in host code; only official plugins opt in by name | +| [Tool references (`serverProcess` 4)](../../contract/v1/README.md#tool-references-serverprocess-4) | Reference runtimes by tool-definition ID; the `tool` launch kind in a launch plan | +| [Host integrations (API 2)](../../contract/v1/README.md#host-integrations-api-2) | Behavior data cannot express lives in host code; any plugin can opt in by name; an ID the host does not implement returns `plugin.integration.unsupported`. A build system's host integration is still official plugins only | | [Launch templates (API 2)](../../contract/v1/README.md#launch-templates-api-2) | Template variables such as `${pluginDir}`, `${stateDir}`, `${workspaceStateDir}` | | [Workspace configuration (`languageServers` 2)](../../contract/v1/README.md#workspace-configuration-languageservers-2) | Expanding and pushing `workspaceConfiguration` | +| [Backend-resolved launch (`serverProcess` 3)](../../contract/v1/README.md#backend-resolved-launch-serverprocess-3) | The backend answers the command line: `resolveParams`, answer fields, rejection codes, macOS limits | +| [Plugin-owned sessions (`languageServers` 3)](../../contract/v1/README.md#plugin-owned-sessions-languageservers-3) | Session fields, session policy, gates, state directories and pruning, preparation status | +| [Language server host methods](../../contract/v1/README.md#language-server-host-methods) | `languageServer/request`, `notify`, `setStatus` and the two notifications | +| [Content providers](../../contract/v1/README.md#content-providers) | `documents/registerContentProvider`: read-only documents served by URI scheme | +| [Output link matchers (`outputLinkMatchers` 1)](../../contract/v1/README.md#output-link-matchers-outputlinkmatchers-1) | Links in Run and Test console output, `outputLinks.match`, `outputLinks.decodeResolution` | | [Execution providers](../../contract/v1/README.md#execution-providers) | `run` and `test` executables, argument templates, test discovery | ### Examples and cases @@ -107,9 +335,13 @@ Teach Lithe a language: recognize its files, highlight them with a packaged Mona | [`languages/node-runtime.plugin.json`](../../contract/v1/fixtures/languages/node-runtime.plugin.json) | Level 2: launched with the `node` runtime, with plugin settings | | [`languages/language-host-integration.plugin.json`](../../contract/v1/fixtures/languages/language-host-integration.plugin.json) | Level 2: a language server that declares a host integration (Java) | | [`languages/language-grammar.plugin.json`](../../contract/v1/fixtures/languages/language-grammar.plugin.json) | Level 3, `languages` 2: a grammar-only plugin that starts no process | +| [`languages/language-server-backend-launch.plugin.json`](../../contract/v1/fixtures/languages/language-server-backend-launch.plugin.json) | Level 3, `serverProcess` 3 and `languageServers` 3: a language server whose command line the backend answers, with session fields and `languageServer.request` | | [`languages/grammars/http.monarch.json`](../../contract/v1/fixtures/languages/grammars/http.monarch.json) | A complete Monarch grammar | | [`languages/command-plans.json`](../../contract/v1/fixtures/languages/command-plans.json) | 35 golden expansions of run/test templates and language-server launch plans | | [`languages/runtime-versions.json`](../../contract/v1/fixtures/languages/runtime-versions.json) | 12 golden outcomes of `plugins.declarative.checkRuntimeVersion` | +| [`languages/language-server-launch-resolution.json`](../../contract/v1/fixtures/languages/language-server-launch-resolution.json) | 17 golden outcomes: the first step of a backend-resolved launch (`languageServerLaunchPlan`) and the launch plan or rejection code from decoding the backend's answer (`plugins.languageServers.decodeLaunch`) | +| [`run/output-link-matchers.json`](../../contract/v1/fixtures/run/output-link-matchers.json) | The requests of `outputLinks.match` and the links it returns, plus what `outputLinks.decodeResolution` makes of a matcher's `resolve` reply | +| [`run/tool-reference-launch.plugin.json`](../../contract/v1/fixtures/run/tool-reference-launch.plugin.json) | A language server's `runtime` launch references the runtime by tool ID `interpreter` (`serverProcess` 4), and the manifest also declares an `outputLinkMatchers` entry | Language rejections are in [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json). diff --git a/reference/domains/manifest.md b/reference/domains/manifest.md index eae861b..bbc8891 100644 --- a/reference/domains/manifest.md +++ b/reference/domains/manifest.md @@ -19,21 +19,28 @@ | 清单字段 | 最低级别 | `requires` 里的能力 | 功能页 | | --- | --- | --- | --- | | `languages` | 1 | `languages` | [语言](languages.md) | -| `languageServers` | 1 | `languageServers`、`serverProcess` | [语言](languages.md) | +| `languageServers` | 1 | `languageServers`(1–3)、`serverProcess`(1–4) | [语言](languages.md) | +| `languageServers[].hostIntegration` | 2 | 同 `languageServers`;所有插件都可以声明 | [语言](languages.md) | +| `requiresTools` | 3 | `serverProcess` 4;当且仅当 `requires.serverProcess` 为 4 时存在 | [工具链](toolchains.md) | | `executionProviders` | 1 | `executionProviders` | [语言](languages.md) | +| `outputLinkMatchers` | 3 | `outputLinkMatchers`;匹配器带 `resolve` 时加 `backend` 2 | [语言](languages.md) | | `settings`、`settingActions` | 2 | `settings` | [设置](settings.md) | | `ui` | 2 | `ui` | [界面](ui.md) | | `buildSystems` | 3 | `buildSystems`,可选 `executableResolution`、`buildModelInputs` | [声明式构建](build-systems.md) | +| `buildSystems` 里的 `hostIntegration`(模型、测试结果、依赖来源) | 3 | 同 `buildSystems`;只限官方插件,目前只有 `maven` | [声明式构建](build-systems.md) | | `buildSystems[].provider` | 3 | `buildProviders`,自有进程时加 `serverProcess` | [构建提供者](build-providers.md) | | `vcsProviders` | 3 | `vcsProviders`、`serverProcess` | [VCS](vcs.md) | | `backend` | 3 | `backend`、`serverProcess` | [插件后端](backend.md) | +| `pluginFunctions` | 3 | `pluginFunctions`、`backend` 2、`serverProcess` 1;不授予权限 | [插件后端](backend.md) | | `projectGenerators` | 3 | `projectGenerators` | [项目生成器](project-generators.md) | | `toolDefinitions`、`toolchainProviders` | 3 | `toolDefinitions`、`toolchainProviders`、`executableResolution` | [工具链](toolchains.md) | | `keymapImportProviders` | 3 | `keymapImportProviders`、`backend`、`serverProcess` | [快捷键导入](keymap-import.md) | | `vendor.bundledSignatureRequirement` | 3 | `packageTrust` | [包信任](package-trust.md) | -| `debugAdapters` | 4 | `debugAdapters`,`runSelectedRuntime` 需要 `serverProcess` 2 | [调试适配器](debug-adapters.md) | +| `debugAdapters` | 4 | `debugAdapters`(1–3),`runSelectedRuntime` 需要 `serverProcess` 2 | [调试适配器](debug-adapters.md) | -级别 1、2 的清单不写 `requires`;级别 3 起必须写,而且要**恰好**列出用到的能力。 +级别 1、2 的清单不写 `requires`;级别 3 起必须写,而且要**恰好**列出用到的能力。括号里的范围是契约现有的版本,各版本新增什么见[版本对照](../capabilities-and-versions.md#能力版本requires)。 + +`hostIntegration` 在两处是不同的声明:语言服务器的 `hostIntegration` 对所有插件开放,只读取声明插件自己包里的资源,不授予额外权限(见[语言](languages.md));构建系统的 `hostIntegration` 仍只限官方插件,目前只有 `maven`(见[声明式构建](build-systems.md))。宿主没有实现的 ID 一律是 `plugin.integration.unsupported`。 ### 权限与激活事件 @@ -49,6 +56,10 @@ | `backend.launch` | 声明了 `backend` | [插件后端](backend.md) | | `keymaps.provide`、`keymaps.readSelectedSources` | `keymapImportProviders` 不为空 | [快捷键导入](keymap-import.md) | | `toolchains.provide` | 声明了 `toolchainProviders` | [工具链](toolchains.md) | +| `run.start` | 后端启动和停止由宿主持有的 Run 会话(`run/start`、`run/stop`,通知 `run/didExit`);需要 `backend` 2 | [插件后端](backend.md) | +| `debug.start` | 后端在自己的调试适配器上启动调试会话(`debug/start`);需要 `backend` 2 和已声明的 `debugAdapters` | [插件后端](backend.md)、[调试适配器](debug-adapters.md) | +| `languageServer.request` | `languageServers` 3 的服务器列出了 `requestMethods` 或 `forwardNotifications`,而且只有这时才可声明;需要 `backend` 2。只授予对本插件自己服务器的 `languageServer/*` 宿主方法和两个通知 | [语言](languages.md) | +| `documents.contentProvider` | 后端注册只读文档内容提供者;需要 `backend` 2。只授予 `documents/registerContentProvider`,不授予读取或编辑文档 | [语言](languages.md) | | `credentials.store`、`workspace.*`、`documents.read`、`editor.*`、`preview.open`、`browser.open`、`window.messages`、`terminal.create` | 后端调用对应的宿主方法 | [API 总表](../api-catalog.md#已公开的宿主方法) | | 激活事件 | 含义 | @@ -81,7 +92,7 @@ | 文件 | 内容 | | --- | --- | | [`manifest/hello-lithe.plugin.json`](../../contract/v1/fixtures/manifest/hello-lithe.plugin.json) | 最小的级别 2 界面插件:一个工具窗口、一个命令。ID 在保留的测试命名空间里,不能直接发布 | -| [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json) | 174 个拒绝用例,覆盖所有功能。每个用例给某个合法 fixture 打补丁,并写明稳定的拒绝码和字段 | +| [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json) | 225 个拒绝用例,覆盖所有功能。每个用例给某个合法 fixture 打补丁,并写明稳定的拒绝码和字段 | ### 相关指南 @@ -107,21 +118,28 @@ Every plugin package has a `plugin.json` at its root. It says who the plugin is, | Manifest field | Lowest level | Capabilities in `requires` | Domain page | | --- | --- | --- | --- | | `languages` | 1 | `languages` | [Languages](languages.md) | -| `languageServers` | 1 | `languageServers`, `serverProcess` | [Languages](languages.md) | +| `languageServers` | 1 | `languageServers` (1–3), `serverProcess` (1–4) | [Languages](languages.md) | +| `languageServers[].hostIntegration` | 2 | The same as `languageServers`; any plugin may declare one | [Languages](languages.md) | +| `requiresTools` | 3 | `serverProcess` 4; present exactly when `requires.serverProcess` is 4 | [Toolchains](toolchains.md) | | `executionProviders` | 1 | `executionProviders` | [Languages](languages.md) | +| `outputLinkMatchers` | 3 | `outputLinkMatchers`, plus `backend` 2 when a matcher has `resolve` | [Languages](languages.md) | | `settings`, `settingActions` | 2 | `settings` | [Settings](settings.md) | | `ui` | 2 | `ui` | [UI](ui.md) | | `buildSystems` | 3 | `buildSystems`, optionally `executableResolution` and `buildModelInputs` | [Declarative build systems](build-systems.md) | +| `hostIntegration` inside `buildSystems` (model, test-result, or dependency source) | 3 | The same as `buildSystems`; official plugins only, and only `maven` exists | [Declarative build systems](build-systems.md) | | `buildSystems[].provider` | 3 | `buildProviders`, plus `serverProcess` for an own process | [Build providers](build-providers.md) | | `vcsProviders` | 3 | `vcsProviders`, `serverProcess` | [VCS](vcs.md) | | `backend` | 3 | `backend`, `serverProcess` | [Plugin backend](backend.md) | +| `pluginFunctions` | 3 | `pluginFunctions`, `backend` 2, `serverProcess` 1; grants no permission | [Plugin backend](backend.md) | | `projectGenerators` | 3 | `projectGenerators` | [Project generators](project-generators.md) | | `toolDefinitions`, `toolchainProviders` | 3 | `toolDefinitions`, `toolchainProviders`, `executableResolution` | [Toolchains](toolchains.md) | | `keymapImportProviders` | 3 | `keymapImportProviders`, `backend`, `serverProcess` | [Shortcut import](keymap-import.md) | | `vendor.bundledSignatureRequirement` | 3 | `packageTrust` | [Package trust](package-trust.md) | -| `debugAdapters` | 4 | `debugAdapters`; `runSelectedRuntime` needs `serverProcess` 2 | [Debug adapters](debug-adapters.md) | +| `debugAdapters` | 4 | `debugAdapters` (1–3); `runSelectedRuntime` needs `serverProcess` 2 | [Debug adapters](debug-adapters.md) | + +Level 1 and 2 manifests have no `requires`. From level 3 on it is required and must list **exactly** the capabilities the manifest uses. The ranges in parentheses are the versions the contract has today; what each version adds is in the [versions](../capabilities-and-versions.md#capability-versions-requires) page. -Level 1 and 2 manifests have no `requires`. From level 3 on it is required and must list **exactly** the capabilities the manifest uses. +`hostIntegration` is two different declarations. A language server's `hostIntegration` is open to every plugin; it reads only resources inside the declaring plugin's own package and grants no extra permission (see [Languages](languages.md)). A build system's `hostIntegration` is still limited to official plugins, and only `maven` exists (see [Declarative build systems](build-systems.md)). An ID the host does not implement is always `plugin.integration.unsupported`. ### Permissions and activation events @@ -137,6 +155,10 @@ Level 1 and 2 manifests have no `requires`. From level 3 on it is required and m | `backend.launch` | `backend` is declared | [Plugin backend](backend.md) | | `keymaps.provide`, `keymaps.readSelectedSources` | `keymapImportProviders` is not empty | [Shortcut import](keymap-import.md) | | `toolchains.provide` | `toolchainProviders` is declared | [Toolchains](toolchains.md) | +| `run.start` | the backend starts and stops host-owned Run sessions (`run/start`, `run/stop`, notification `run/didExit`); needs `backend` 2 | [Plugin backend](backend.md) | +| `debug.start` | the backend starts a debug session on one of its own adapters (`debug/start`); needs `backend` 2 and a declared `debugAdapters` entry | [Plugin backend](backend.md), [Debug adapters](debug-adapters.md) | +| `languageServer.request` | a `languageServers` 3 server lists `requestMethods` or `forwardNotifications`, and only then; needs `backend` 2. Grants the `languageServer/*` host methods for the plugin's own servers and their two notifications, nothing else | [Languages](languages.md) | +| `documents.contentProvider` | the backend registers a read-only document content provider; needs `backend` 2. Grants `documents/registerContentProvider` only, not reading or editing documents | [Languages](languages.md) | | `credentials.store`, `workspace.*`, `documents.read`, `editor.*`, `preview.open`, `browser.open`, `window.messages`, `terminal.create` | the backend calls the matching host methods | [API catalog](../api-catalog.md#public-host-methods) | | Activation event | Meaning | @@ -169,7 +191,7 @@ Each provider needs its event and each event needs a provider. There is no event | File | Content | | --- | --- | | [`manifest/hello-lithe.plugin.json`](../../contract/v1/fixtures/manifest/hello-lithe.plugin.json) | The smallest level 2 UI plugin: one tool window and one command. Its ID is in the reserved test namespace and cannot be published as is | -| [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json) | 174 rejection cases across every domain. Each patches a valid fixture and names the stable rejection code and field | +| [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json) | 225 rejection cases across every domain. Each patches a valid fixture and names the stable rejection code and field | ### Related guides diff --git a/reference/domains/toolchains.md b/reference/domains/toolchains.md index 1477b3b..278407d 100644 --- a/reference/domains/toolchains.md +++ b/reference/domains/toolchains.md @@ -9,7 +9,7 @@ | 项目 | 内容 | | --- | --- | | 清单字段 | `toolDefinitions[]`(工具定义)、`toolchainProviders[]`(工具链提供者);`executables` 候选集里的 `toolchain` 候选 | -| 能力版本 | 工具定义:`toolDefinitions` 1 + `executableResolution` 2;提供者:`toolchainProviders` 1 + `backend` 2 + `serverProcess` 1;都是级别 3。见[版本对照](../capabilities-and-versions.md#能力版本requires) | +| 能力版本 | 工具定义:`toolDefinitions` 1 + `executableResolution` 2;提供者:`toolchainProviders` 1 + `backend` 2 + `serverProcess` 1;用工具定义 ID 启动进程:`serverProcess` 4;都是级别 3。见[版本对照](../capabilities-and-versions.md#能力版本requires) | | 协议 | `litheToolchains` 1:Core 只向声明了提供者的插件会话提供 `experimental.litheToolchains: {versions: [1], providers}`,后端回答 `{version: 1, providers}`;请求方法是 `toolchains/query`。见[进程协议版本](../capabilities-and-versions.md#进程协议版本) | | API 编号 | [API-23](../api-catalog.md#现在能用的-api):工具链与受管进程(部分完成) | | 权限 | 工具定义不需要权限;提供者需要 `backend.launch`、`toolchains.provide` | @@ -26,6 +26,7 @@ | `executableResolution` 2 | 工具定义里的候选,构建候选集里的 `toolchain` 候选可以指向插件定义的工具;不改变已有构建候选集 | 支持 | 支持(只读设置、构建与运行工具) | 不支持 | 不涉及 | | `toolDefinitions` 1 | 工具身份、可执行文件布局、版本探针、声明式发现、主目录变量、导出的环境变量、`PATH` 前缀、启动模板 | 支持 | 支持(只读设置、构建与运行工具) | 不支持 | 不涉及 | | `toolchainProviders` 1 | 提供者声明 `{id, displayName, tools, watchFiles?}`;按权限校验应答 | 支持 | 支持(只读设置、构建与运行工具) | 不支持 | 不涉及 | +| `serverProcess` 4 | 进程启动按工具定义 ID 引用工具:清单顶层 `requiresTools`,启动计划是 `launch.kind: "tool"`,`toolchains.instantiateLaunch` 按定义的 `launch` 模板展开;见下文“按工具定义 ID 启动进程” | 支持 | 宿主目前只声明 `serverProcess` 1 和 3:版本 4 已由 Core 校验,宿主尚未声明,需要它的插件版本暂时装不上 | 只声明 `serverProcess` 1 | 不涉及 | | `litheToolchains` 1 | `toolchains/query` 请求和应答;后端从声明的提供者中选出非空子集 | 支持 | 支持 | 不支持 | 都未实现 | | `.lithe/toolchains/local.json` `version` 1 | 机器本地文档:旧的 `toolchains` 条目和按目录的 `pins` | 支持读取和解析 | 只读,不保存 | 不支持 | 不涉及 | @@ -51,18 +52,26 @@ 没有安装的项目要求会带着原因保留为"未满足",即使更低的层选中了别的安装。冲突必须由用户明确选择,不按插件 ID 或路径先后决定。一次操作还可以带 `fixed: {home}` 或 `fixed: {executable}`,它排在目录固定之前,无效时直接失败。 +### 工具 ID 与冲突 + +- 任何插件都可以定义短 ID(例如 `compiler`)或限定 ID `<自己的插件 ID>:<本地 ID>`。限定 ID 的前缀必须是定义它的插件自己的 ID,所以不同插件的限定 ID 永远不会冲突。宿主不提供来源证明,插件自称的来源也不会被读取。提供者可以回答短 ID。 +- 同一个短 ID 被多个已启用插件定义时,它不归其中任何一方:注册表不登记这些定义,其余定义照常登记;`discoveryPlan` 不给它列扫描和提供者;解析它的消费者把它当作没有人声明。归属从不按到达顺序或插件 ID 顺序决定,否则恶意插件可以靠先加载或取更小的 ID 抢走工具。 +- 注册表的输出带 `conflicts: [{tool, pluginIDs}]`:每个被争用的工具一条,按 `tool` 排序,`pluginIDs` 是所有定义它的插件 ID(也排序)。宿主应该告诉用户是哪些插件在争用;macOS 设置页对每个冲突显示一条诊断,建议只保留其中一个插件启用。 +- 同一个插件 ID 在一批输入里重复出现,仍然拒绝整批,不会替换先前登记的归属。 + ### Core 命令 所有命令都只处理 UTF-8 JSON,输入上限 4 MiB,不做任何本机 I/O。成功返回 `{status: "ok", <键>: 值}`,拒绝返回 `{status: "rejected", error: {code, message, field?}}`。 | 命令 | 输入 | 成功时的键 | 用途 | | --- | --- | --- | --- | -| `toolchains.registry` | `{plugins: [{manifest, verifiedOfficial}]}` | `registry` | 重新校验完整清单,汇总定义和提供者;最多 128 个插件、512 个定义、256 个提供者 | +| `toolchains.registry` | `{plugins: [{manifest}]}` | `registry` | 重新校验完整清单,汇总定义和提供者,返回 `{definitions, providers, conflicts}`;宿主不提供来源证明;被多个插件争用的短 ID 不登记,列在 `conflicts: [{tool, pluginIDs}]`;最多 128 个插件、512 个定义、256 个提供者 | | `toolchains.decodeReply` | 查询请求、提供者应答、宿主观察到的文件系统事实 | `snapshot` | 校验一次 `toolchains/query` 应答,得到 `{installations, statements, watch, diagnostics}` | | `toolchains.failureReply` | `{params, code}` | `reply` | 查询没有应答时生成对应的 JSON-RPC 错误;`code` 只能是 `plugin.toolchain.timeout` 或 `plugin.toolchain.providerUnavailable` | | `toolchains.resolvePins` | `{document, directory, tool}` | `resolution` | 在 `local.json` 里找最近的固定,返回 `{pin, sourceDirectory}` | | `toolchains.resolve` | 某个目录的完整解析输入 | `resolution` | 走四层选择,结果是 `resolved`、`skip` 或 `terminalFailure`,附带 `trace`、`unmet`、`conflicts` | | `toolchains.checkVersion` | `{registry, tool, output, minimumVersion?}` | `version` | 用定义的版本正则取版本并比较;最多 8 段数字、64 字节,不解释别名 | +| `toolchains.instantiateLaunch` | `{registry, tool, executable, home?, entry?, runtimeArguments, arguments}` | `launch` | 按工具定义的 `launch` 模板,为宿主选中的可执行文件展开启动参数,返回 `{executable, arguments, versionArguments}`;见“按工具定义 ID 启动进程” | | `toolchains.discoveryPlan` | `{registry, platform}` | `plan` | 每个工具的扫描计划和声明它的提供者;最多 256 个根目录、4096 个条目、64 层祖先 | | `toolchains.candidates` | 设置页的发现输入 | `candidates` | 设置页显示的全局层和自动层:`{tool, shell, installations, skipped, selected, conflicts}`;第一次调用把待探测的可执行文件列为 `probeFailed`,宿主运行探针后再调用一次 | @@ -94,6 +103,21 @@ Core 另有 `toolchains.resolveConsumerPreferences` 和 `toolchains.editConsumer | `-32076` | `plugin.toolchain.unsafePath` | 绝对路径、包含关系或可执行文件证据不安全 | | `-32077` | `plugin.toolchain.operationExpired` | 发出查询时上下文或代次已经失效 | +### 按工具定义 ID 启动进程(`serverProcess` 4) + +插件用工具定义 ID 启动进程时(`launch.kind: "tool"` 的启动计划,或后端的 `run/start` 带 `{kind: "tool"}`),从不自己写这个工具的 argv。定义的 `launch` 模板说明工具怎样接收入口文件、运行时参数和程序参数,宿主和插件都不按工具名分支。 + +| 项目 | 规则 | +| --- | --- | +| 声明 | `requires.serverProcess` 为 4 时,清单顶层 `requiresTools` 列出插件启动进程用到的所有工具定义 ID:最多 32 个,短 ID 或限定 ID,不重复,保存时排序。只有版本没有列表,或只有列表没有版本,都是 `plugin.manifest.invalid`。定义来自插件自己或别的已启用插件的 `toolDefinitions` | +| 引用位置 | `runtime` 启动的 `launch.tool`、`runtimeDependencies[].tool`、调试适配器的 `runSelectedRuntime.tool`。引用 `requiresTools` 没列出的 ID 是 `plugin.runtime.undeclared`,带 `field`;版本 1 到 3 仍用宿主的封闭运行时列表,报 `plugin.runtime.unsupported` | +| 启动计划 | 返回各部分而不是命令行:`launch: {kind: "tool", tool, entry, runtimeArguments, minimumVersion?, packagedRuntime?}`;`arguments` 只含插件自己的参数;`runtimeRequirements` 列出宿主要解析的每个工具。`packagedRuntime` 是插件自带运行时的路径,宿主直接把这个文件当作工具的可执行文件,不再查找 | +| `run/start` | `process.executable` 可以是 `{kind: "tool", tool}`(`tool` 是清单 `requiresTools` 列出的工具定义 ID,宿主按四层选择解析),或 `{kind: "packaged", path}`(插件包内文件);见[插件后端](backend.md) | +| 宿主做什么 | 按四层选择解析每个工具,用 `toolchains.checkVersion` 检查 `minimumVersion`,再用 `toolchains.instantiateLaunch` 展开,然后启动 | +| 解析不到 | 工具解析不到可执行文件是 `plugin.host.toolUnresolved`(JSON-RPC `-32025`);`details` 是 `{tool, reason}`,`reason` 为 `notFound`(四层里没有候选满足定义和最低版本)或 `unusable`(选中的可执行文件不能用) | + +`toolchains.instantiateLaunch` 的 `executable` 必填;`home` 是解析出的工具主目录,`entry` 是绝对文件路径,`runtimeArguments` 和 `arguments` 是字符串列表。文本最多 4096 字节,列表最多 1024 项、每项最多 64 KiB,都不含 NUL。模板里的 `${executable}`、`${home}`、`${entry}`、`${runtimeArguments}`、`${arguments}` 被对应的值替换,列表原位展开;其他占位符原样保留。调用方给了模板带不动的值(给不启动入口的工具一个 `entry`,或给不接受参数的工具参数),或漏了模板需要的 `${home}`、`${entry}`,是 `plugin.toolchain.launchMismatch`;未知或已停用的 `tool` 是 `plugin.toolchain.invalidParams`,`field` 为 `tool`。这个命令不做任何本机 I/O:选可执行文件、探测版本、启动进程都由宿主完成。 + ### 宿主现状 | 用途 | macOS | Windows | @@ -101,9 +125,11 @@ Core 另有 `toolchains.resolveConsumerPreferences` 和 `toolchains.editConsumer | 设置页显示发现结果 | 两条来源都跑,只读;监视的文件变化时重新加载整个快照;不保存任何东西 | 不支持 | | 构建工具、插件运行方案的可执行文件 | 候选或运行方案指向插件定义的工具(不在宿主旧的工具链表里)时,单独重新解析一次;设置页的发现结果不会被当作启动许可 | 不支持 | | Java、语言服务器 | 仍然走原来的适配器 | 不支持 | +| 后端决定启动方式的 `{kind: "tool"}` 应答 | 按用户的工具选择和发现解析;位于项目内的程序被拒绝,因为打开项目不能运行它 | 不支持 | +| 命名冲突 | 设置页对每个被争用的短 ID 显示一条诊断,建议只保留其中一个插件 | 不支持 | | 保存固定、`toolchainConsumers`、通用运行时启动、提供者热激活 | 尚未提供 | 尚未提供 | -`toolchains.provide` 可以影响某个已定义工具启动哪个程序,但不授权替换任意命令、安装软件、访问网络或执行工作区代码。短定义 ID(例如 `java`)要求经过独立验证的官方来源;社区插件的定义 ID 写成 `<自己的插件 ID>:<本地 ID>`,社区提供者可以回答短 ID。 +`toolchains.provide` 可以影响某个已定义工具启动哪个程序,但不授权替换任意命令、安装软件、访问网络或执行工作区代码。任何插件都可以定义短 ID 或 `<自己的插件 ID>:<本地 ID>`,提供者也可以回答短 ID,宿主不提供来源证明;短 ID 被多个插件定义时的处理见上文“工具 ID 与冲突”。 ### 契约章节 @@ -111,9 +137,12 @@ Core 另有 `toolchains.resolveConsumerPreferences` 和 `toolchains.editConsumer | --- | --- | | [Plugin-owned toolchains (Core foundation)](../../contract/v1/README.md#plugin-owned-toolchains-core-foundation) | 契约总入口里的工具链说明和当前宿主状态 | | [Plugin-owned toolchains: Core foundation](../../contract/v1/toolchains/README.md) | 已实现部分的完整说明 | -| [Manifest admission and registry](../../contract/v1/toolchains/README.md#manifest-admission-and-registry) | 能力和权限要求、定义字段、提供者字段、ID 规则 | +| [Manifest admission and registry](../../contract/v1/toolchains/README.md#manifest-admission-and-registry) | 能力和权限要求、定义字段、提供者字段、ID 规则(短 ID 与限定 ID) | | [Two installation routes](../../contract/v1/toolchains/README.md#two-installation-routes) | 声明式发现和提供者查询 | -| [Commands and envelopes](../../contract/v1/toolchains/README.md#commands-and-envelopes) | 八个 Core 命令、返回格式、注册表限制 | +| [Commands and envelopes](../../contract/v1/toolchains/README.md#commands-and-envelopes) | 九个 Core 命令、返回格式、注册表限制、`conflicts` | +| [Launching a tool definition](../../contract/v1/toolchains/README.md#launching-a-tool-definition) | `toolchains.instantiateLaunch`:按定义的 `launch` 模板展开启动参数、`launchMismatch` | +| [Tool references (`serverProcess` 4)](../../contract/v1/README.md#tool-references-serverprocess-4) | 清单 `requiresTools`、引用位置、启动计划的各部分、`plugin.host.toolUnresolved` | +| [Run and debug sessions (protocol version 3)](../../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) | `run/start` 的 `process.executable`:`{kind: "tool"}` 或 `{kind: "packaged"}` | | [Provider reply validation](../../contract/v1/toolchains/README.md#provider-reply-validation) | `decodeReply`、应答格式与限制、`failureReply` | | [Pins and snapshot resolution](../../contract/v1/toolchains/README.md#pins-and-snapshot-resolution) | `local.json`、`resolve`、四层选择、`checkVersion`、`discoveryPlan` | | [Settings candidates](../../contract/v1/toolchains/README.md#settings-candidates) | `candidates` 和两次调用的探测流程 | @@ -127,6 +156,7 @@ Core 另有 `toolchains.resolveConsumerPreferences` 和 `toolchains.editConsumer | --- | --- | | [`toolchains/tool-definition.plugin.json`](../../contract/v1/fixtures/toolchains/tool-definition.plugin.json) | 生产形态的工具定义:`java`,8 个候选(项目/全局设置、项目/全局工具链请求、macOS/Linux 与 Windows 的 `JAVA_HOME`、`java` 命令、已安装的工具链),在 macOS 的 `JavaVirtualMachines` 目录里发现 `*.jdk` | | [`toolchains/toolchain-provider.plugin.json`](../../contract/v1/fixtures/toolchains/toolchain-provider.plugin.json) | 生产形态的提供者:`mise`,覆盖 `java`、`node`、`maven`、`pnpm`、`go`,监视 6 个文件(`.tool-versions` 用兼容读取器),后端是包内可执行文件 | +| [`run/tool-reference-launch.plugin.json`](../../contract/v1/fixtures/run/tool-reference-launch.plugin.json) | 级别 3、`serverProcess` 4:清单顶层 `requiresTools` 列出插件自己定义的 `interpreter`,语言服务器的 `runtime` 启动按这个工具定义 ID 引用运行时 | Core 的验收测试在主仓库的 [`toolchain_definitions.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_definitions.rs) 和同目录的 `toolchain_pins.rs`、`toolchain_queries.rs`、`toolchain_resolution.rs`。这三个测试共用的固定值文档、查询往返和每个稳定失败码的错误回复内联在 [`toolchain_vectors.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_vectors.rs) 里。 @@ -144,7 +174,7 @@ A plugin can tell Lithe what a tool (a JDK, Maven, Node, Go and so on) looks lik | Item | Value | | --- | --- | | Manifest fields | `toolDefinitions[]` (tool definitions), `toolchainProviders[]` (toolchain providers); `toolchain` candidates in `executables` candidate sets | -| Capability versions | Definitions: `toolDefinitions` 1 + `executableResolution` 2; providers: `toolchainProviders` 1 + `backend` 2 + `serverProcess` 1; all at level 3. See [versions](../capabilities-and-versions.md#capability-versions-requires) | +| Capability versions | Definitions: `toolDefinitions` 1 + `executableResolution` 2; providers: `toolchainProviders` 1 + `backend` 2 + `serverProcess` 1; starting processes by tool-definition ID: `serverProcess` 4; all at level 3. See [versions](../capabilities-and-versions.md#capability-versions-requires) | | Protocols | `litheToolchains` 1: Core offers `experimental.litheToolchains: {versions: [1], providers}` only to a plugin session that declares providers, and the backend answers `{version: 1, providers}`; the request method is `toolchains/query`. See [process protocol versions](../capabilities-and-versions.md#process-protocol-versions) | | API IDs | [API-23](../api-catalog.md#apis-usable-now): toolchains and managed processes (partial) | | Permissions | Definitions need none; providers need `backend.launch` and `toolchains.provide` | @@ -161,6 +191,7 @@ A plugin can tell Lithe what a tool (a JDK, Maven, Node, Go and so on) looks lik | `executableResolution` 2 | Candidates inside tool definitions; a `toolchain` candidate in a build candidate set may name a plugin-defined tool; existing build candidate sets are unchanged | Yes | Yes (read-only settings, Build and run tools) | No | Not applicable | | `toolDefinitions` 1 | Tool identity, executable layouts, version probe, declarative discovery, home variable, exported environment, `PATH` prefixes, launch template | Yes | Yes (read-only settings, Build and run tools) | No | Not applicable | | `toolchainProviders` 1 | Provider declarations `{id, displayName, tools, watchFiles?}`; permission-bound reply validation | Yes | Yes (read-only settings, Build and run tools) | No | Not applicable | +| `serverProcess` 4 | A process launch references a tool by tool-definition ID: top-level `requiresTools`, a `launch.kind: "tool"` launch plan, and `toolchains.instantiateLaunch` expanding the definition's `launch` template; see "Starting a process by tool-definition ID" below | Yes | The host currently advertises only `serverProcess` 1 and 3: version 4 is validated by Core but the host does not advertise it yet, so a plugin version that needs it cannot be installed for now | Advertises only `serverProcess` 1 | Not applicable | | `litheToolchains` 1 | The `toolchains/query` request and reply; the backend selects a nonempty subset of its declared providers | Yes | Yes | No | Not implemented | | `.lithe/toolchains/local.json` `version` 1 | Machine-local document: legacy `toolchains` entries and per-directory `pins` | Reads and resolves | Read-only, never saved | No | Not applicable | @@ -186,18 +217,26 @@ The declarative primitives are frozen: roots stay `home`, `environment` and `abs An uninstalled project requirement stays visible as unmet, with its reason, even when a lower layer selects another installation. Conflicts need an explicit user choice, never plugin-ID or path precedence. One operation may also carry `fixed: {home}` or `fixed: {executable}`, which precedes directory pins and fails terminally when invalid. +### Tool IDs and conflicts + +- Any plugin may define a short ID (such as `compiler`) or a qualified ID `:`. The prefix of a qualified ID must be the defining plugin's own ID, so qualified IDs of different plugins never collide. The host supplies no proof of origin, and a plugin's own claim of one is not read. Providers may answer short IDs. +- A short ID defined by more than one enabled plugin belongs to none of them: the registry registers none of those definitions and registers every other definition as usual; `discoveryPlan` lists no scans or providers for it; and a consumer that resolves it treats it as declared by nobody. Ownership is never decided by arrival order or plugin ID order, because a hostile plugin could take a tool by being loaded first or by choosing a smaller ID. +- The registry output carries `conflicts: [{tool, pluginIDs}]`: one entry per contested tool, sorted by `tool`, with `pluginIDs` being the IDs of all plugins that defined it (also sorted). A host should tell the user which plugins contest the tool; the macOS settings page shows one diagnostic per conflict and advises keeping only one of those plugins enabled. +- A repeated plugin ID in one batch still rejects the whole batch, without replacing earlier owners. + ### Core commands Every command takes UTF-8 JSON with a 4 MiB input budget and performs no native I/O. Success is `{status: "ok", : value}`; refusal is `{status: "rejected", error: {code, message, field?}}`. | Command | Input | Success key | Purpose | | --- | --- | --- | --- | -| `toolchains.registry` | `{plugins: [{manifest, verifiedOfficial}]}` | `registry` | Revalidates complete manifests and collects definitions and providers; at most 128 plugins, 512 definitions and 256 providers | +| `toolchains.registry` | `{plugins: [{manifest}]}` | `registry` | Revalidates complete manifests and collects definitions and providers, returning `{definitions, providers, conflicts}`; the host supplies no proof of origin; a short ID contested by several plugins is not registered and is listed in `conflicts: [{tool, pluginIDs}]`; at most 128 plugins, 512 definitions and 256 providers | | `toolchains.decodeReply` | The query request, the provider reply and host-observed filesystem facts | `snapshot` | Validates one `toolchains/query` reply into `{installations, statements, watch, diagnostics}` | | `toolchains.failureReply` | `{params, code}` | `reply` | Produces the JSON-RPC error for a query that got no reply; `code` is only `plugin.toolchain.timeout` or `plugin.toolchain.providerUnavailable` | | `toolchains.resolvePins` | `{document, directory, tool}` | `resolution` | Finds the nearest pin in `local.json` and returns `{pin, sourceDirectory}` | | `toolchains.resolve` | The complete resolver input for one directory | `resolution` | Runs the four layers; the outcome is `resolved`, `skip` or `terminalFailure`, with `trace`, `unmet` and `conflicts` | | `toolchains.checkVersion` | `{registry, tool, output, minimumVersion?}` | `version` | Extracts the version with the definition's regex and compares it; at most 8 numeric components and 64 bytes, no aliases interpreted | +| `toolchains.instantiateLaunch` | `{registry, tool, executable, home?, entry?, runtimeArguments, arguments}` | `launch` | Expands the launch argv for the executable the host selected, using the tool definition's `launch` template, and returns `{executable, arguments, versionArguments}`; see "Starting a process by tool-definition ID" | | `toolchains.discoveryPlan` | `{registry, platform}` | `plan` | Each tool's scan plan and the providers that declare it; at most 256 roots, 4096 entries and 64 ancestors | | `toolchains.candidates` | A settings page's discovery input | `candidates` | The global and automatic layers a settings page shows: `{tool, shell, installations, skipped, selected, conflicts}`; the first call lists executables to probe as `probeFailed`, and the host calls again after running the probes | @@ -229,6 +268,21 @@ A provider may use these codes in JSON-RPC errors, and `decodeReply` checks them | `-32076` | `plugin.toolchain.unsafePath` | Unsafe absolute path, containment or executable evidence | | `-32077` | `plugin.toolchain.operationExpired` | The context or generation was already invalid when the query was sent | +### Starting a process by tool-definition ID (`serverProcess` 4) + +A plugin that starts a process by tool-definition ID (a `launch.kind: "tool"` launch plan, or a backend `run/start` with `{kind: "tool"}`) never writes the tool's argv. The definition's `launch` template says how the tool takes an entry file, runtime arguments and program arguments, so neither the host nor the plugin branches on a tool name. + +| Item | Rule | +| --- | --- | +| Declaration | With `requires.serverProcess` at 4, the top-level `requiresTools` lists every tool-definition ID the plugin starts a process with: at most 32, short or qualified, without duplicates, stored sorted. The version without the list, or the list without the version, is `plugin.manifest.invalid`. Definitions come from the plugin's own `toolDefinitions` or from another enabled plugin's | +| Where an ID is used | `launch.tool` of a `runtime` launch, `runtimeDependencies[].tool`, and `runSelectedRuntime.tool` of a debug adapter. An ID that `requiresTools` does not list is `plugin.runtime.undeclared` with the `field`; versions 1 to 3 keep the host's closed runtime lists and report `plugin.runtime.unsupported` | +| Launch plan | Returns parts, not a command line: `launch: {kind: "tool", tool, entry, runtimeArguments, minimumVersion?, packagedRuntime?}`; `arguments` holds only the plugin's own arguments; `runtimeRequirements` lists every tool the host must resolve. `packagedRuntime` is the path of a runtime the plugin ships; the host then uses that file as the tool's executable and never looks the tool up | +| `run/start` | `process.executable` can be `{kind: "tool", tool}` (`tool` is a tool-definition ID that the manifest's `requiresTools` lists; the host resolves it through the four layers) or `{kind: "packaged", path}` (a file in the plugin package); see [Plugin backend](backend.md) | +| What the host does | Resolves each tool through the four layers, checks `minimumVersion` with `toolchains.checkVersion`, expands the start with `toolchains.instantiateLaunch`, then starts it | +| Unresolvable tool | A tool that resolves to no executable is `plugin.host.toolUnresolved` (JSON-RPC `-32025`); `details` is `{tool, reason}`, with `reason` being `notFound` (no candidate of the four layers meets the definition and its minimum version) or `unusable` (a chosen executable cannot be used) | + +`toolchains.instantiateLaunch` requires `executable`; `home` is the resolved tool home, `entry` an absolute file path, and `runtimeArguments` and `arguments` string lists. A text is at most 4096 bytes and a list at most 1024 items of 64 KiB each, none holding a NUL. In the template, `${executable}`, `${home}`, `${entry}`, `${runtimeArguments}` and `${arguments}` are replaced by the matching value, with a list expanding in place; any other token is passed as written. A value the caller supplies that the template cannot carry (an `entry` for a tool that launches none, or arguments for one that takes none), or a `${home}` or `${entry}` the caller left out, is `plugin.toolchain.launchMismatch`; an unknown or disabled `tool` is `plugin.toolchain.invalidParams` with `field: "tool"`. The command performs no native I/O: selecting the executable, probing the version and starting the process are the host's work. + ### Host status | Use | macOS | Windows | @@ -236,9 +290,11 @@ A provider may use these codes in JSON-RPC errors, and `decodeReply` checks them | Settings pages showing detections | Both routes run read-only; a change to a watched file reloads the whole snapshot; nothing is saved | No | | Executables for Build tools and plugin run plans | A candidate or run plan that names a plugin-defined tool (not one in the host's legacy toolchain table) is resolved afresh; settings detections are never launch permits | No | | Java and language servers | Still use their previous adapters | No | +| A `{kind: "tool"}` answer to a backend-resolved launch | Resolved with the user's tool choice and discovery; a program inside the project is refused, because opening a project must not run it | No | +| Name conflicts | The settings page shows one diagnostic per contested short ID and advises keeping only one of those plugins | No | | Saving pins, `toolchainConsumers`, generic runtime launches, live provider activation | Not yet | Not yet | -`toolchains.provide` may influence which program a defined tool launches, but it does not authorize replacing arbitrary commands, installing software, network access or running workspace code. Short definition IDs (such as `java`) need independently verified official origin; a community plugin writes definition IDs as `:`, and community providers may answer short IDs. +`toolchains.provide` may influence which program a defined tool launches, but it does not authorize replacing arbitrary commands, installing software, network access or running workspace code. Any plugin may define a short ID or `:`, providers may answer short IDs, and the host supplies no proof of origin; what happens when several plugins define the same short ID is described in "Tool IDs and conflicts" above. ### Contract sections @@ -246,9 +302,12 @@ A provider may use these codes in JSON-RPC errors, and `decodeReply` checks them | --- | --- | | [Plugin-owned toolchains (Core foundation)](../../contract/v1/README.md#plugin-owned-toolchains-core-foundation) | The toolchain summary and current host status in the contract entry point | | [Plugin-owned toolchains: Core foundation](../../contract/v1/toolchains/README.md) | The complete description of the implemented part | -| [Manifest admission and registry](../../contract/v1/toolchains/README.md#manifest-admission-and-registry) | Capability and permission requirements, definition fields, provider fields, ID rules | +| [Manifest admission and registry](../../contract/v1/toolchains/README.md#manifest-admission-and-registry) | Capability and permission requirements, definition fields, provider fields, ID rules (short and qualified IDs) | | [Two installation routes](../../contract/v1/toolchains/README.md#two-installation-routes) | Declarative discovery and provider queries | -| [Commands and envelopes](../../contract/v1/toolchains/README.md#commands-and-envelopes) | The eight Core commands, result envelopes, registry limits | +| [Commands and envelopes](../../contract/v1/toolchains/README.md#commands-and-envelopes) | The nine Core commands, result envelopes, registry limits, `conflicts` | +| [Launching a tool definition](../../contract/v1/toolchains/README.md#launching-a-tool-definition) | `toolchains.instantiateLaunch`: expanding launch argv from the definition's `launch` template, `launchMismatch` | +| [Tool references (`serverProcess` 4)](../../contract/v1/README.md#tool-references-serverprocess-4) | Manifest `requiresTools`, where an ID is used, the parts of a launch plan, `plugin.host.toolUnresolved` | +| [Run and debug sessions (protocol version 3)](../../contract/v1/README.md#run-and-debug-sessions-protocol-version-3) | `process.executable` of `run/start`: `{kind: "tool"}` or `{kind: "packaged"}` | | [Provider reply validation](../../contract/v1/toolchains/README.md#provider-reply-validation) | `decodeReply`, reply shape and limits, `failureReply` | | [Pins and snapshot resolution](../../contract/v1/toolchains/README.md#pins-and-snapshot-resolution) | `local.json`, `resolve`, the four layers, `checkVersion`, `discoveryPlan` | | [Settings candidates](../../contract/v1/toolchains/README.md#settings-candidates) | `candidates` and the two-call probe flow | @@ -262,6 +321,7 @@ A provider may use these codes in JSON-RPC errors, and `decodeReply` checks them | --- | --- | | [`toolchains/tool-definition.plugin.json`](../../contract/v1/fixtures/toolchains/tool-definition.plugin.json) | A production-shape tool definition: `java` with 8 candidates (project and global settings, project and global toolchain requests, `JAVA_HOME` on macOS/Linux and on Windows, the `java` command, installed toolchains), discovering `*.jdk` in the macOS `JavaVirtualMachines` directories | | [`toolchains/toolchain-provider.plugin.json`](../../contract/v1/fixtures/toolchains/toolchain-provider.plugin.json) | A production-shape provider: `mise` for `java`, `node`, `maven`, `pnpm` and `go`, watching 6 files (`.tool-versions` with the compatible reader), with a packaged executable backend | +| [`run/tool-reference-launch.plugin.json`](../../contract/v1/fixtures/run/tool-reference-launch.plugin.json) | Level 3, `serverProcess` 4: top-level `requiresTools` lists the plugin's own `interpreter`, and the language server's `runtime` launch references the runtime by that tool-definition ID | Core's acceptance tests are [`toolchain_definitions.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_definitions.rs) and its siblings `toolchain_pins.rs`, `toolchain_queries.rs` and `toolchain_resolution.rs` in the main repository. The pin document, query exchange and one error reply per stable failure code that those three share are inline in [`toolchain_vectors.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_vectors.rs). diff --git a/reference/protocol-changelog.md b/reference/protocol-changelog.md index 061b2de..8445e28 100644 --- a/reference/protocol-changelog.md +++ b/reference/protocol-changelog.md @@ -5,7 +5,7 @@ - **收录范围**:Lithe 主仓库 `preview` 分支上的 PR,只要它修改了下面三处中的任意一处:插件契约 `shared/contracts/plugins/v1/`、Core 能力表 [`capabilities.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/plugins/capabilities.rs)、插件函数协议 [`plugin_function.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/lsp/interface/plugin_function.rs)。三处都没改、只改宿主内部实现的 PR 不收录。 - **日期**:PR 合入 `preview` 的日期。 - **修改人**:原 PR 的作者(GitHub 账号)。登记这些 PR 的同步 PR 会在描述里 @ 他们,请他们核对自己那一行。 -- **记录到**:`preview` 的 [`5d38352a1`](https://github.com/1lck/Lithe-IDEA/commit/5d38352a1757a70fd5fa297d7aa3e43bb2e33607)([#1752](https://github.com/1lck/Lithe-IDEA/pull/1752)),即本仓库 `contract/v1/` 当前副本的来源。 +- **记录到**:`preview` 的 [`5cc06d5f4`](https://github.com/1lck/Lithe-IDEA/commit/5cc06d5f44cfe94956b174720d34445b027f51d7)([#1764](https://github.com/1lck/Lithe-IDEA/pull/1764)),即本仓库 `contract/v1/` 当前副本的来源。 - **每一版的内容和宿主现状**:见[能力与协议版本对照](capabilities-and-versions.md)。字段和行为以 [`contract/v1/README.md`](../contract/v1/README.md) 为准,官方 SDK 的发版见 [SDK 发版历史](capabilities-and-versions.md#sdk-发版历史)。 时间线的两列这样读: @@ -41,8 +41,11 @@ | `languages` | 2 | 2026-10-31 | [#1617](https://github.com/1lck/Lithe-IDEA/pull/1617) | | `languageServers` | 1 | 2026-10-18 | [#1318](https://github.com/1lck/Lithe-IDEA/pull/1318) | | `languageServers` | 2 | 2026-10-21 | [#1395](https://github.com/1lck/Lithe-IDEA/pull/1395) | +| `languageServers` | 3 | 2026-11-05 | [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | | `serverProcess` | 1 | 2026-10-18 | [#1318](https://github.com/1lck/Lithe-IDEA/pull/1318) | | `serverProcess` | 2 | 2026-10-26 | [#1510](https://github.com/1lck/Lithe-IDEA/pull/1510) | +| `serverProcess` | 3 | 2026-11-05 | [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | +| `serverProcess` | 4 | 2026-11-05 | [#1746](https://github.com/1lck/Lithe-IDEA/pull/1746) | | `executionProviders` | 1 | 2026-10-18 | [#1318](https://github.com/1lck/Lithe-IDEA/pull/1318) | | `settings` | 1 | 2026-10-18 | [#1318](https://github.com/1lck/Lithe-IDEA/pull/1318) | | `settings` | 2 | 2026-10-21 | [#1395](https://github.com/1lck/Lithe-IDEA/pull/1395) | @@ -66,10 +69,13 @@ | `backend` | 2 | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | | `debugAdapters` | 1 | 2026-10-26 | [#1510](https://github.com/1lck/Lithe-IDEA/pull/1510) | | `debugAdapters` | 2 | 2026-10-28 | [#1517](https://github.com/1lck/Lithe-IDEA/pull/1517) | +| `debugAdapters` | 3 | 2026-11-05 | [#1746](https://github.com/1lck/Lithe-IDEA/pull/1746) | | `packageTrust` | 1 | 2026-10-28 | [#1483](https://github.com/1lck/Lithe-IDEA/pull/1483) | | `toolDefinitions` | 1 | 2026-11-01 | [#1581](https://github.com/1lck/Lithe-IDEA/pull/1581) | | `toolchainProviders` | 1 | 2026-11-01 | [#1581](https://github.com/1lck/Lithe-IDEA/pull/1581) | | `keymapImportProviders` | 1 | 2026-11-03 | [#1695](https://github.com/1lck/Lithe-IDEA/pull/1695) | +| `outputLinkMatchers` | 1 | 2026-11-05 | [#1746](https://github.com/1lck/Lithe-IDEA/pull/1746) | +| `pluginFunctions` | 1 | 2026-11-05 | [#1742](https://github.com/1lck/Lithe-IDEA/pull/1742) | ### 进程协议版本 @@ -78,6 +84,7 @@ | `litheFunctions` | 1 | 2026-10-20 | [#1358](https://github.com/1lck/Lithe-IDEA/pull/1358) | | `litheFunctions` | 2(宿主服务) | 2026-10-24 | [#1470](https://github.com/1lck/Lithe-IDEA/pull/1470) | | `litheFunctions` | 3(工作区上下文) | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | +| `litheFunctions` | 4(跨插件调用) | 2026-11-05 | [#1742](https://github.com/1lck/Lithe-IDEA/pull/1742) | | `litheBuild` | 1 | 2026-10-18 | [#1318](https://github.com/1lck/Lithe-IDEA/pull/1318) 定义;macOS 宿主由 [#1356](https://github.com/1lck/Lithe-IDEA/pull/1356) 接入 | | `litheBuild` | 2–6 | 2026-10-28 | [#1517](https://github.com/1lck/Lithe-IDEA/pull/1517) | | `litheBuild` | 7–11 | 2026-11-04 | [#1558](https://github.com/1lck/Lithe-IDEA/pull/1558) | @@ -88,7 +95,7 @@ ### 宿主方法(按 API 编号) -函数协议 3 里向后兼容地新增方法时不升协议版本,上面的版本表看不出某个方法是什么时候加的,所以宿主方法和通知在这里逐个登记。一个 API 编号下可以有多个方法,每个方法一行,记首次加入的日期和 PR。方法名省略前缀 `lithe/host/`;用途和权限见 [API 总表](api-catalog.md#已公开的宿主方法)。`keymapImport/*` 属于 `litheKeymapImport` 协议,不在 API-01–44 内。 +函数协议 3 里向后兼容地新增方法时不升协议版本,上面的版本表看不出某个方法是什么时候加的,所以宿主方法和通知在这里逐个登记。一个 API 编号下可以有多个方法,每个方法一行,记首次加入的日期和 PR。方法名省略前缀 `lithe/host/`;用途和权限见 [API 总表](api-catalog.md#已公开的宿主方法)。`keymapImport/*` 属于 `litheKeymapImport` 协议,不在 API-01–44 内。`functions/providers`、`functions/invoke` 属于函数协议 4,只对协商到 4 的后端开放。 | API | 方法 | 协议 | 日期 | PR | | --- | --- | --- | --- | --- | @@ -113,6 +120,7 @@ | API-10 | `editor/applyEdits` | 3 | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | | API-10 | `editor/replaceSelections` | 3 | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | | API-10 | `editor/insertText` | 3 | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | +| API-12 | `documents/registerContentProvider` | 3 | 2026-11-05 | [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | | API-15 | `editor/open` | 3 | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | | API-15 | `editor/reveal` | 3 | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | | API-16 | `preview/openHTML` | 3 | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | @@ -123,13 +131,25 @@ | API-21 | `credentials/get` | 2 | 2026-10-24 | [#1470](https://github.com/1lck/Lithe-IDEA/pull/1470) | | API-21 | `credentials/set` | 2 | 2026-10-24 | [#1470](https://github.com/1lck/Lithe-IDEA/pull/1470) | | API-21 | `credentials/delete` | 2 | 2026-10-24 | [#1470](https://github.com/1lck/Lithe-IDEA/pull/1470) | +| API-22、API-25 | `languageServer/setStatus` | 3 | 2026-11-05 | [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | +| API-23 | `run/start` | 3 | 2026-11-05 | [#1746](https://github.com/1lck/Lithe-IDEA/pull/1746) | +| API-23 | `run/stop` | 3 | 2026-11-05 | [#1746](https://github.com/1lck/Lithe-IDEA/pull/1746) | +| API-23 | `run/didExit`(通知) | 3 | 2026-11-05 | [#1746](https://github.com/1lck/Lithe-IDEA/pull/1746) | | API-24 | `terminal/create` | 3 | 2026-11-02 | [#1653](https://github.com/1lck/Lithe-IDEA/pull/1653) | | API-24 | `terminal/show` | 3 | 2026-11-02 | [#1653](https://github.com/1lck/Lithe-IDEA/pull/1653) | | API-24 | `terminal/close` | 3 | 2026-11-02 | [#1653](https://github.com/1lck/Lithe-IDEA/pull/1653) | | API-24 | `terminal/reopen` | 3 | 2026-11-02 | [#1653](https://github.com/1lck/Lithe-IDEA/pull/1653) | | API-24 | `terminal/dispose` | 3 | 2026-11-02 | [#1653](https://github.com/1lck/Lithe-IDEA/pull/1653) | | API-24 | `terminal/onExit` | 3 | 2026-11-02 | [#1653](https://github.com/1lck/Lithe-IDEA/pull/1653) | +| API-25 | `editor/registerProviders` | 3 | 2026-11-05 | [#1730](https://github.com/1lck/Lithe-IDEA/pull/1730) | +| API-25 | `languageServer/request` | 3 | 2026-11-05 | [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | +| API-25 | `languageServer/notify` | 3 | 2026-11-05 | [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | +| API-25 | `languageServer/didChangeState`(通知) | 3 | 2026-11-05 | [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | +| API-25 | `languageServer/didReceiveNotification`(通知) | 3 | 2026-11-05 | [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | | API-27 | `editor/registerActions` | 3 | 2026-10-31 | [#1617](https://github.com/1lck/Lithe-IDEA/pull/1617) | +| API-30 | `debug/start` | 3 | 2026-11-05 | [#1746](https://github.com/1lck/Lithe-IDEA/pull/1746) | +| API-35 | `functions/providers` | 4 | 2026-11-05 | [#1742](https://github.com/1lck/Lithe-IDEA/pull/1742) | +| API-35 | `functions/invoke` | 4 | 2026-11-05 | [#1742](https://github.com/1lck/Lithe-IDEA/pull/1742) | | — | `keymapImport/listInputs` | 3 | 2026-11-03 | [#1695](https://github.com/1lck/Lithe-IDEA/pull/1695) | | — | `keymapImport/readChunk` | 3 | 2026-11-03 | [#1695](https://github.com/1lck/Lithe-IDEA/pull/1695) | @@ -139,6 +159,12 @@ | PR | 修改人 | 协议 / API | 改了什么 | | --- | --- | --- | --- | +| [#1764](https://github.com/1lck/Lithe-IDEA/pull/1764) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | 清单 schema:`requiresTools` 的描述(API-23) | `plugin-manifest.schema.json` 里 `requiresTools` 的 `description` 改为 #1763 的规则:ID 是短 ID 或 `:`,同一个短 ID 被多个已启用插件定义时不归任何一方。原描述“短 ID 需要经过验证的官方来源”作废。只改描述,校验规则、能力版本和 fixture 都不变。 | +| [#1763](https://github.com/1lck/Lithe-IDEA/pull/1763) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | 清单与工具定义:`hostIntegration`、短工具 ID(API-23、API-25) | 语言服务器的 `hostIntegration`(插件 API 级别 2)对所有插件开放,不再限 `dev.lithe` 命名空间:集成只读取声明插件自己包里的资源,交给同一插件启动的服务器或辅助进程,不额外授予权限;宿主未实现的 ID 仍返回 `plugin.integration.unsupported`。构建系统的 `hostIntegration` 仍只限官方插件。工具定义:任何插件都可以定义短 ID(如 `compiler`)或限定 ID `:`,宿主不再提供来源证明;同一个短 ID 被多个已启用插件定义时不归任何一方,注册表不登记这些定义,并报告为冲突;限定 ID 不会冲突;所有权不按到达顺序或插件 ID 顺序决定。`toolchains.registry` 的输入从 `{plugins:[{manifest,verifiedOfficial}]}` 改为 `{plugins:[{manifest}]}`,输出新增 `conflicts:[{tool,pluginIDs}]`(按 `tool` 排序,`pluginIDs` 也排序)。macOS 设置页对每个冲突显示一条诊断,建议只保留其中一个插件。`plugin.integration.unsupported` 的含义改为:宿主未实现的集成,或官方命名空间以外的插件请求构建系统的宿主集成。`fixtures/manifest/invalid-manifests.json` 删除 1 条清单拒绝用例(“host integration requested outside the official namespace”)。不新增版本号,不新增宿主方法。 | +| [#1746](https://github.com/1lck/Lithe-IDEA/pull/1746) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | **`debugAdapters` 3**、**`serverProcess` 4**、**`outputLinkMatchers` 1**、函数协议 3:后端发起的运行与调试(API-22、API-23、API-30) | `debugAdapters` 3:适配器可声明 `features` 和 `defaults`(热代码替换、步进过滤器),新增 fixture `debug-adapter-features.plugin.json`。`serverProcess` 4:运行时按工具定义 ID 引用(清单 `requiresTools`),不再使用宿主的封闭运行时列表。`outputLinkMatchers` 1(API-22,新能力):插件声明匹配器,让 Run 和测试控制台的输出出现链接;Core 负责匹配,宿主只上样式并转发点击,`resolve` 函数在点击时才调用。新增权限 `run.start`(需要 `backend` 2),授予 `lithe/host/run/start`、`lithe/host/run/stop` 和通知 `lithe/host/run/didExit`;新增权限 `debug.start`(需要 `backend` 2 和已声明的 `debugAdapters` 条目),只授予 `lithe/host/debug/start`。`run/start`(API-23):参数 `context, label, process, temporaryFiles?, debug?, rerun?`,结果 `{runID}`,启动宿主拥有的 Run 会话(Run 窗口条目、控制台、停止按钮、退出状态);`process.executable` 只能是 `{kind:"tool", tool}`(清单 `requiresTools` 列出的工具定义 ID)或 `{kind:"packaged", path}`(插件包内文件);`temporaryFiles` 最多 4 个,每个不超过 4 MiB;`debug` 让它成为调试会话,`rerun` 让用户可以再次运行。`run/stop`(API-23):参数 `{runID}` 或 `{sessionID}`,结果 `null`,停止本插件启动的运行或调试会话,不带 `context`。`debug/start`(API-30):参数 `context, label, adapter, request, configuration`,结果 `{sessionID}`,在本插件自己的适配器上启动调试会话;`configuration` 不超过 256 KiB,`noDebug` 和 `__` 开头的键属于宿主,会被拒绝。通知 `lithe/host/run/didExit`:`{runID, exitCode?, signal?}`,用 `run/start` 启动的运行结束时发一次,宿主丢失进程时两个字段都没有。新增错误码 `plugin.host.toolUnresolved`(-32025):工具解析不到可执行文件。新增 fixtures `fixtures/run/`(含 `run-start.json`)和 `fixtures/debug-adapters/debug-start.json`。方法和通知是函数协议 3 内的增量,不升协议版本。`outputLinkMatchers` 和后端发起的运行与调试目前只在 macOS 实现。 | +| [#1745](https://github.com/1lck/Lithe-IDEA/pull/1745) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | **`languageServers` 3**、**`serverProcess` 3**、函数协议 3:后端决定启动方式、语言服务器会话、内容 Provider(API-12、API-20、API-22、API-25) | `serverProcess` 3 新增后端解析的启动方式 `launch.kind: "backend"`:语言服务器的命令行由插件后端函数每次给出,只有语言服务器能用,需要 `backend` 2 和 `pluginAPIVersion` 不低于 4;新增 fixtures `fixtures/languages/language-server-backend-launch.plugin.json`、`fixtures/languages/language-server-launch-resolution.json`。`languageServers` 3 新增插件后端与自己的服务器交谈所用的会话字段:`requestMethods`、`commands`、`serializedCommands`、`forwardNotifications`、`state`、`remote`。新增权限 `languageServer.request`(需要 `backend` 2;只在 `languageServers` 3 的服务器列出 `requestMethods` 或 `forwardNotifications` 时才可声明),只授予本插件自己服务器的 `lithe/host/languageServer/*` 方法和两个通知;新增权限 `documents.contentProvider`(需要 `backend` 2),只授予 `lithe/host/documents/registerContentProvider`,不授予读写文档。新宿主方法均带 `context` 和 `serverID`,`serverID` 是本插件某个服务器的 `languageID`,别的插件的服务器返回 `plugin.host.foreignContribution`:`lithe/host/languageServer/request`(API-25)参数加 `method`、`params?`、`timeoutMilliseconds?`(1–300000,默认 60000),结果 `{value}`,向本插件自己的服务器发请求,受会话策略检查,第 129 个等待中的请求返回 `plugin.host.busy`;`lithe/host/languageServer/notify`(API-25)参数加 `method`、`params?`,结果 `{delivered}`;`lithe/host/languageServer/setStatus`(API-22、API-25)参数加 `state`(`preparing`、`ready`、`degraded`、`failed`)、`message?`、`percentage?`(0–100 整数),结果 `{}`,在宿主状态界面显示插件自己的准备状态(导入工程、建索引);`lithe/host/documents/registerContentProvider`(API-12)参数 `{context, scheme, function}`,结果 `{subscriptionID}`,打开该 scheme 的文档时宿主调用函数取只读内容(不超过 1 MiB,宿主等 30 秒),每个工作区窗口每个 scheme 只能有一个 Provider,用 `subscriptions/dispose` 撤销。新宿主通知(权限 `languageServer.request`,API-25):`lithe/host/languageServer/didChangeState`(`{context, serverID, state, generation}`,`state` 为 `starting`、`running`、`stopped`、`crashed`)和 `lithe/host/languageServer/didReceiveNotification`(`{context, serverID, generation, method, params?, truncated?}`,转发服务器发出且 `forwardNotifications` 列出的通知,超过 `limits.notificationBytes` 时只送一次 `truncated: true`,不带 `params`)。新增错误码:`plugin.host.languageServerUnavailable`(-32022,服务器未运行)、`plugin.host.languageServerError`(-32023,服务器返回错误)、`plugin.host.methodNotDeclared`(-32024,方法未在会话里声明)、`plugin.host.foreignContribution`(-32026)。语言服务器按工作区的状态目录按 `state.retention` 后台回收(API-20,由 Core 决定)。方法和通知是函数协议 3 内的增量,不升协议版本。仅 macOS;远程后端启动未支持;Windows 不声明 `backend`。 | +| [#1742](https://github.com/1lck/Lithe-IDEA/pull/1742) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | **`pluginFunctions` 1**、**函数协议 4**(API-35) | 插件函数跨插件中转的第一阶段:由 Core 中转调用,不是安装依赖。清单顶层新增 `pluginFunctions` 段,含导出 `exports` 和调用声明 `calls`;需要 `pluginAPIVersion` 不低于 3,以及 `requires.pluginFunctions: 1`、`backend: 2`、`serverProcess: 1`;不授予任何清单权限。函数协议 4 保留版本 3 的全部内容,新增两个宿主方法:`lithe/host/functions/providers`,参数 `{functionID}`,结果 `{providers:[{pluginID,version}]}`,列出导出该函数、已启用且不是调用方自己的插件,按插件 ID 排序,调用方必须声明过该函数,否则 `plugin.function.notDeclared`;`lithe/host/functions/invoke`,参数 `{pluginID, functionID, context:{contextID,generation}, params?, timeoutMilliseconds?, parentOperationID?}`,结果 `{value}`,Core 以 `lithe/function/invoke` 转给被调方,带顶层 `caller` 和 `operationID`。`timeoutMilliseconds` 默认 60000、上限 600000;未定义的字段返回 `-32018 plugin.host.unsupportedOption`;`parentOperationID` 为 1–128 字节字符串,否则 `-32602 plugin.host.invalidParams`。两个方法只对协商到协议 4 且声明了至少一个 call 的后端开放:协议 3 及以下返回 `-32601 plugin.host.unavailable`,未声明 call 返回 `-32010 plugin.host.permissionDenied`;`getCapabilities` 在 `functions` 服务下列出。新增错误码 `plugin.function.notDeclared`、`notExported`、`providerDisabled`、`versionMismatch`、`callCycle`、`depthExceeded`、`tooManyCalls`、`payloadTooLarge`、`providerUnavailable`。信任不传递:被调方用自己的权限执行,调用方不获得任何权限;不参与安装闭包,不做版本求解;远程工作区拒绝。新增 fixtures `fixtures/backend/plugin-functions-caller.plugin.json`、`fixtures/backend/plugin-functions-exporter.plugin.json`,`fixtures/backend/function-protocol.json` 增补用例。macOS 中转调用,Windows 不声明 `pluginFunctions`。官方 Node、Java、Rust SDK 目前只协商函数协议 1–3,还不能调用 `functions/*`。 | +| [#1730](https://github.com/1lck/Lithe-IDEA/pull/1730) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | 函数协议 3:编辑器 Provider、编辑器动作第 2 版(API-25、API-27) | 新增权限 `editor.providers`(需要 `backend` 2 和 `documents.read`),只授予宿主方法 `lithe/host/editor/registerProviders`,不授予文档编辑。macOS 路由 7 种类型:`definition`、`hover`、`completion`、`codeLens`、`foldingRange`、`semanticTokens`、`documentLink`;语言服务器优先,插件结果只补充。宿主在 `services.editor.options.providerTypes` 里列出可用类型,类型不在其中时返回 `unsupportedOption`。编辑器动作第 2 版:`services.editor.options.actionIcons` 列出宿主绘制的图标(macOS 绘制 9 个),动作可带最多 4 个 `alternatives`,点击结果可带 `reveal`;宿主未报告 `actionIcons` 时只接受第一版形状。`limits.editorActionSourceBytes` 给出发给收集函数的文档上限(macOS 1 MiB,宿主未报告时 128 KiB,此前固定 128 KiB)。新增 3 个 fixtures(`editor-providers.plugin.json`、`editor-providers.json`、`editor-actions-v2.json`)和 1 条清单拒绝用例(声明 `editor.providers` 但缺 `documents.read`)。这是协议 3 内的增量方法,不升版本号;旧宿主不认识 `editor.providers`,会拒绝该包。 | | [#1752](https://github.com/1lck/Lithe-IDEA/pull/1752) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | `litheToolchains` 1:写入正式契约 | 删除工具链设计草案 `toolchains/proposal/` 和 `fixtures/toolchains/proposal/`。草案里已实现的部分写进正式契约 `toolchains/README.md`:新增“Provider backend negotiation”一节(`initialize` 里的 `experimental.litheToolchains` 协商、`lsp.toolchainRequest`、超时)和“Error replies”小节(错误回复格式与 11 个稳定失败码的数字码对照)。Core 测试改用内联数据 `toolchain_vectors.rs`。协议行为没有变化。 | | [#1740](https://github.com/1lck/Lithe-IDEA/pull/1740) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | **`projectGenerators` 2**(API-33) | 生成器新增 `tool` 字段:从插件声明的工具定义的已有安装里选一个(`tool`、`minimumVersion`、`pinAs`、`optional`),模板可用 `${field..executable}`;`pinAs: "project"` 时计划返回 `toolPins`,宿主把它存为新项目的工具固定。表单输入加 `tools`,结果字段带 `installations`,新增问题码 `toolTooOld`;目录条目带 `tools`(声明该工具的插件)。版本 2 不能用 `runtime` 字段,版本 1 不能用 `tool` 字段。macOS 声明 1、2,Windows 不声明。新增示例 `project-generator-tool-field.plugin.json`、12 个计划用例和 9 个清单拒绝用例。 | | [#1747](https://github.com/1lck/Lithe-IDEA/pull/1747) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | 清单 schema:`requires.buildSystems` | schema 的枚举补齐到 1–4(Core 从 #1558 起已经支持 3、4,schema 当时只写到 3)。fixtures 按功能分目录,工具链提案移到 `toolchains/proposal/`,契约 README 的能力版本表随之修正。 |