diff --git a/README.md b/README.md index 514fef1..704f40e 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,14 @@ Lithe 读取并校验 `plugin.json`,然后自己启动其中声明的外部进 | --- | --- | --- | | `1` | 语言、语言服务器(随包附带或使用用户安装的命令)、运行和测试 | Lua、Go、PHP | | `2` | 增加:用 Node.js / Java 运行时启动、启动模板、插件设置、工具窗口和命令 | — | -| `3` | 增加:按能力声明版本(`requires`)、声明式构建系统、构建后端 | Gradle、Maven、Cargo | +| `3` | 增加:按能力声明版本(`requires`)、声明式构建系统、构建后端、VCS 提供者 | Gradle、Maven、Cargo | +| `4` | 增加:调试适配器(`debugAdapters`,DAP 进程)、用选定的运行时启动适配器 | LLDB(预发布) | 三个版本号不要混淆:`schemaVersion` 永远是 `2`,表示清单格式;`pluginAPIVersion` 是上表的等级;`version` 是你自己插件的版本。 -API 3 必须在 `requires` 里逐项列出实际用到的能力。例如只用声明式构建系统时写 `"requires": { "buildSystems": 1 }`,不要多写。 +API 3 及以上必须在 `requires` 里逐项列出实际用到的能力。例如只用声明式构建系统时写 `"requires": { "buildSystems": 1 }`,不要多写。 + +API 4 需要 Lithe 明确支持 API 4:旧版 Lithe 在插件市场里会提示升级,而不是安装。目前只有 macOS 宿主实现了调试适配器。 ## 在编辑器里校验 plugin.json @@ -118,6 +121,7 @@ SDK 的源码都在本仓库的 `sdk/` 下,发布在[本仓库的 Releases](ht | [Gradle](https://github.com/Lithe-IDEA/lithe-plugin-gradle) | 3 | 纯声明式构建系统,不需要后端 | | [Maven](https://github.com/Lithe-IDEA/lithe-plugin-maven) | 3 | 用 Java SDK 写的构建后端 | | [Cargo](https://github.com/Lithe-IDEA/lithe-plugin-cargo) | 3 | 用 Node.js SDK 写的构建后端和插件设置 | +| [LLDB](https://github.com/Lithe-IDEA/lithe-plugin-lldb) | 4 | 调试适配器的参考实现;用 Xcode 自带的 `lldb-dap` 调试 C、C++、Swift 和 Rust,含 `executable` 查找和调试控制台补全 | ## 教程 @@ -125,6 +129,7 @@ SDK 的源码都在本仓库的 `sdk/` 下,发布在[本仓库的 Releases](ht | --- | --- | | [LSP 插件完整生命周期](guides/lsp-plugin-lifecycle.md) | 从 Lua 模板开始:清单、服务器打包、激活、状态目录、停止、升级与卸载 | | [构建、运行与测试接入](guides/build-tool-integration.md) | 可用的执行提供者示例、工程后端边界,以及开发中的 API 3 状态 | +| [编写调试适配器插件](guides/debug-adapter-plugin.md) | API 4:从最小清单到启动方式、先编译再调试的 `executable`、调试控制台补全,以及预发布与转正 | 收录和上架的规则由 Lithe Store 维护:[发布社区插件](https://github.com/Lithe-IDEA/Lithe-store/blob/main/docs/publishing.md)讲怎么打包、提交收录和发布新版本,[版本追踪与更新](https://github.com/Lithe-IDEA/Lithe-store/blob/main/docs/versioning-and-updates.md)讲 Lithe 怎么选择可安装的版本。 diff --git a/contract.lock.json b/contract.lock.json index b3fc27b..5e9e96c 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": "d046fd1768d00a52b1fc99d45067d06cf535bc23" + "revision": "32021a86f0c6c4ad2964dbbe3c2bb7854d84ad67" }, "files": [ { "path": "README.md", - "sha256": "87b6738fdd0d764b93f33556992d0586dfb97e68abb58e0bc3a5ac690ec97aab" + "sha256": "75cf6add5c473eb333caca793dc9add6394476c7f4c5196b2bf5f1aea7873d59" }, { "path": "fixtures/backend.plugin.json", @@ -50,6 +50,14 @@ "path": "fixtures/command-plans.json", "sha256": "a188fd65f6cdb47f9e9ad4b0de3a7ddcd17e4e577b34f6e25b96d2c1f2bdca7c" }, + { + "path": "fixtures/debug-adapter-rejections.json", + "sha256": "0aaa586c19884025eb9bee36dc213fbebacc4d0503c3f5ed10a196c922cfaeaf" + }, + { + "path": "fixtures/debug-adapter.plugin.json", + "sha256": "81038febbde31e0554451789106f8fc255e87038c8185602ce0c7e918f90ff78" + }, { "path": "fixtures/function-protocol.json", "sha256": "980e8d39db2af9d7f922cdc01b0ec06e2cb8c33da2142ec8040befa975b7eae5" @@ -160,7 +168,7 @@ }, { "path": "plugin-manifest.schema.json", - "sha256": "a70921c16f1d45e342777fc6f8fe9fc91efa82427d377461d356b8f00054008f" + "sha256": "f35d510bf76feeff2978289bebbeba19453cd60b1e101b7677c6106934a27f95" } ] } diff --git a/contract/v1/README.md b/contract/v1/README.md index 3cf6a4b..35d2fd5 100644 --- a/contract/v1/README.md +++ b/contract/v1/README.md @@ -377,6 +377,7 @@ files sorted by path, or an empty list when no project marker is present. | `1` | Languages, `packaged` and `systemCommand` language servers, `node` and `go` runtime dependencies, run and test providers. | | `2` | `runtime` launches with a packaged or discovered runtime, host integrations, the `java` runtime, launch templates, `initializationOptions`, and plugin settings. | | `3` | Capability versions (`requires`), `buildSystems`: declarative build tools with detection, options, a project model, tasks, run entries, tests, diagnostics, dependencies, and sync, and [`vcsProviders`](#api-level-3-vcs-providers). | +| `4` | [`debugAdapters`](#api-level-4-debug-adapters) and `serverProcess` 2 (`runSelectedRuntime`). | A level 2 feature in a level 1 manifest is refused with `plugin.manifest.invalid` (`plugin.runtime.unsupported` for the `java` tool), @@ -407,6 +408,7 @@ hosts. Level 3 versions each section on its own. A level 3 manifest lists in | `buildSystems` | Declarative build detection, models, and command plans | `1` | | `buildProviders` | `buildSystems[].provider`, including own-process and shared language-server providers | `1` | | `vcsProviders` | `vcsProviders[]` | `1` | +| `debugAdapters` | [`debugAdapters[]`](#api-level-4-debug-adapters) | `1` | | `projectGenerators` | [`projectGenerators[]`](#project-generators): entries of the New Project window | `1` | | `backend` | [`backend`](#plugin-backend): the plugin's own long-lived process for its pages | `1` | @@ -494,6 +496,8 @@ Rules for publishing across levels and capability versions: | `process.test` | any provider declares `test` | | `process.build` | `buildSystems` is not empty | | `process.vcs` | `vcsProviders` is not empty | +| `debug.launch` | `debugAdapters` is not empty | +| `debug.attach` | an adapter lists `attach` in `requests` (and only then) | | `project.generate` | `projectGenerators` is not empty | | `ui.contribute` | `ui` is declared | | `ui.web` | a tool window shows [web content](#web-content-ui-2) | @@ -507,6 +511,7 @@ Rules for publishing across levels and capability versions: | `onTest:` | Activate the test provider when the user lists or runs tests. | | `onBuildSystem:` | Activate the build system before its first plan, model, or decode. | | `onVcsRoot:` | Start the VCS provider once a root of its VCS is detected in the workspace. | +| `onDebug:` | Start the debug adapter when a debug session needs it (API level 4). | Each provider needs its event and each event needs a provider. Enabling a plugin only permits activation; nothing starts until its event occurs. @@ -1007,6 +1012,38 @@ its VCS client (`vcsTool`) and `mayUseNetwork`. Requests go through `vcs.decodeReply`. The fields, operations, reply shapes, and error codes are defined in [the VCS provider contract](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/contracts/vcs-provider-v1.md). +## API level 4: debug adapters + +`debugAdapters[]` declares debug adapters the plugin implements. Each one is a +DAP process the host starts and talks to over stdio or a loopback TCP port. +The declaration is data only: there is no resolver, workflow, host integration, +or `initializationOptions`, and any such field is refused. Requires +`requires.debugAdapters` 1, `debug.launch` (plus `debug.attach` when an +adapter serves `attach`), and one `onDebug:` activation event per adapter. +A manifest below level 4 may not declare `debugAdapters`, and a level 3 manifest may not require `debugAdapters` or `serverProcess` 2. + +| Field | Meaning | +| --- | --- | +| `id`, `displayName` | Plugin-local ID (unique in the plugin) and its title. | +| `adapterID` | DAP `initialize.adapterID`; defaults to `id`. | +| `requests` | `launch` (default) and/or `attach`. | +| `selectors[]` | Optional automatic matching by `targetKinds` and `languageIDs`; absent means explicit selection only. | +| `configurationTypes` | Imported `launch.json` `type` values the adapter answers to. | +| `configurations.launch/attach` | Default DAP `arguments`, a packaged `parametersSchema`, and `targetBindings` (`program` ← `target.path`, `processId` ← `target.processID`, `cwd` ← `target.workingDirectory`). 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. | +| `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. | +| `launch.transport` | `stdio`, `tcpAnnounced` (`endpoint`: `stream`, one-line `prefix`, `value` = `port` or `hostPort`), or `tcpPort` (`endpoint.port`). TCP endpoints are loopback only. | + +The cases in `fixtures/debug-adapter-rejections.json` state the stable code and +field for each refusal and the layer that reports it: `schema`, +`coreManifest`, `corePlan` (one start: stale registration or runtime +selection, a target missing a fact a binding reads), and `coreInstall` +(a `parametersSchema` that is missing or links outside the package). +`fixtures/debug-adapter.plugin.json` is a valid example. + ## Plugin backend A plugin with its own pages may need code that a page cannot run: database diff --git a/contract/v1/fixtures/debug-adapter-rejections.json b/contract/v1/fixtures/debug-adapter-rejections.json new file mode 100644 index 0000000..89ee530 --- /dev/null +++ b/contract/v1/fixtures/debug-adapter-rejections.json @@ -0,0 +1,349 @@ +{ + "description": "Rejection cases for plugin.json debugAdapters, run by the Core tests; each case names the validation layer that must reject it. Synthetic rejection specifications; edits use the same JSON Pointer fixture convention as invalid-manifests.json. Paths only build test data and are not plugin syntax.", + "baseline": { + "block": { + "debugAdapters": [ + { + "id": "synthetic", + "displayName": "Synthetic contract input", + "launch": { + "process": { + "launch": { + "kind": "packaged", + "path": "bin/synthetic" + } + }, + "transport": "stdio" + } + } + ] + }, + "context": { + "manifestMetadata": { + "pluginAPIVersion": 4, + "requires": { + "debugAdapters": 1, + "serverProcess": 1 + }, + "permissions": [ + "debug.launch" + ] + }, + "hostCapabilities": { + "debugAdapters": [ + 1 + ], + "serverProcess": [ + 1, + 2 + ], + "settings": [ + 1, + 2 + ] + }, + "registration": { + "generation": 1, + "currentGeneration": 1 + }, + "runtimeSelection": { + "generation": 1, + "currentGeneration": 1 + }, + "target": { + "kind": "file" + }, + "packageResources": [{"path": "bin/synthetic", "kind": "file"}] + } + }, + "cases": [ + { + "id": "removed-platform-fields", + "layer": "schema", + "edits": [{"op": "set", "path": "/block/pluginDependencies", "value": {"missing": {"pluginID": "dev.lithe.fixture.absent"}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "pluginDependencies"} + }, + { + "id": "removed-resolver", + "layer": "schema", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/resolver", "value": {"backendRef": "absent", "functionID": "debug.resolve"}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].resolver"} + }, + { + "id": "removed-host-integration", + "layer": "schema", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/hostIntegration", "value": {"integrationID": "jdt"}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].hostIntegration"} + }, + { + "id": "removed-workflow", + "layer": "schema", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/launch/steps", "value": [{"id": "same"}, {"id": "same"}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.steps"} + }, + { + "id": "program-from-process-id", + "layer": "schema", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.processID"}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.targetBindings.program"} + }, + { + "id": "function-for-step-back", + "layer": "schema", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/capabilities", "value": {"stepBack": {"source": "function", "functionID": "synthetic.stepBack"}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].capabilities.stepBack"} + }, + { + "id": "old-api-debug", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/context/manifestMetadata/pluginAPIVersion", "value": 3}], + "expected": {"code": "plugin.api.unsupported", "field": "requires.debugAdapters"} + }, + { + "id": "missing-host-capability", + "layer": "coreManifest", + "edits": [{"op": "remove", "path": "/context/hostCapabilities/debugAdapters"}], + "expected": {"code": "plugin.api.unsupported", "field": "requires.debugAdapters"} + }, + { + "id": "missing-debug-requirement", + "layer": "coreManifest", + "edits": [{"op": "remove", "path": "/context/manifestMetadata/requires/debugAdapters"}], + "expected": {"code": "plugin.manifest.invalid", "field": "requires"} + }, + { + "id": "missing-process-requirement", + "layer": "coreManifest", + "edits": [{"op": "remove", "path": "/context/manifestMetadata/requires/serverProcess"}], + "expected": {"code": "plugin.manifest.invalid", "field": "requires"} + }, + { + "id": "unused-process-v2", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/context/manifestMetadata/requires/serverProcess", "value": 2}], + "expected": {"code": "plugin.manifest.invalid", "field": "requires.serverProcess"} + }, + { + "id": "duplicate-adapter-id", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters", "value": [{"id": "synthetic", "displayName": "Synthetic contract input", "launch": {"process": {"launch": {"kind": "packaged", "path": "bin/synthetic"}}, "transport": "stdio"}}, {"id": "synthetic", "displayName": "Different title, same identity", "launch": {"process": {"launch": {"kind": "packaged", "path": "bin/synthetic"}}, "transport": "stdio"}}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[1].id"} + }, + { + "id": "missing-launch-permission", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/context/manifestMetadata/permissions", "value": []}], + "expected": {"code": "plugin.permission.undeclared", "field": "permissions"} + }, + { + "id": "attach-without-permission", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/requests", "value": ["launch", "attach"]}], + "expected": {"code": "plugin.permission.undeclared", "field": "permissions"} + }, + { + "id": "unsupported-request-config", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"attach": {"arguments": {"port": 30000}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.attach"} + }, + { + "id": "selected-runtime-v1", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/launch/process", "value": {"launch": {"kind": "runSelectedRuntime", "tool": "python"}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "requires.serverProcess"} + }, + { + "id": "stale-selected-runtime", + "layer": "corePlan", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/launch/process", "value": {"launch": {"kind": "runSelectedRuntime", "tool": "python"}}}, {"op": "set", "path": "/context/manifestMetadata/requires/serverProcess", "value": 2}, {"op": "set", "path": "/context/runtimeSelection/currentGeneration", "value": 2}], + "expected": {"code": "debug.runtime.stale", "field": "runtimeSelection"} + }, + { + "id": "unknown-dap-capability", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/capabilities", "value": {"disabled": ["supportsSyntheticUnknownRequest"]}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].capabilities.disabled[0]"} + }, + { + "id": "empty-console", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/console", "value": {}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].console.commands"} + }, + { + "id": "empty-console-commands", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/console", "value": {"commands": []}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].console.commands"} + }, + { + "id": "padded-console-command-name", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/console", "value": {"commands": [{"name": " frame"}]}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].console.commands[0].name"} + }, + { + "id": "multiline-console-command-description", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/console", "value": {"commands": [{"name": "frame", "description": "one\ntwo"}]}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].console.commands[0].description"} + }, + { + "id": "duplicate-console-command", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/console", "value": {"commands": [{"name": "frame"}, {"name": "frame", "description": "again"}]}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].console.commands"} + }, + { + "id": "spaced-console-command-prefix", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/console", "value": {"commandPrefix": "` "}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].console.commandPrefix"} + }, + { + "id": "unknown-console-field", + "layer": "schema", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/console", "value": {"commands": [{"name": "frame"}], "script": "complete.js"}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].console.script"} + }, + { + "id": "missing-target-path", + "layer": "corePlan", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.path"}}}}], + "expected": {"code": "debug.target.invalid", "field": "target.path"} + }, + { + "id": "executable-without-binding", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"executable": {"setting": "program", "candidates": ["${workspaceFolder}/build/app"]}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable"} + }, + { + "id": "executable-binding-without-lookup", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable"} + }, + { + "id": "executable-undeclared-setting", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "missing", "candidates": []}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable.setting"} + }, + { + "id": "executable-setting-not-file", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "boolean", "title": "Program", "default": false}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 1}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "program", "candidates": ["${workspaceFolder}/build/app"]}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable.setting"} + }, + { + "id": "executable-candidate-outside-roots", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "program", "candidates": ["/usr/local/bin/app"]}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable.candidates[0]"} + }, + { + "id": "executable-candidate-parent-directory", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "program", "candidates": ["${workspaceFolder}/../app"]}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable.candidates[0]"} + }, + { + "id": "executable-candidate-unknown-variable", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "program", "candidates": ["${workspaceFolder}/${env:HOME}"]}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable.candidates[0]"} + }, + { + "id": "executable-duplicate-candidates", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "program", "candidates": ["${workspaceFolder}/app", "${workspaceFolder}/app"]}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable.candidates"} + }, + { + "id": "executable-too-many-candidates", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "program", "candidates": ["${workspaceFolder}/c0", "${workspaceFolder}/c1", "${workspaceFolder}/c2", "${workspaceFolder}/c3", "${workspaceFolder}/c4", "${workspaceFolder}/c5", "${workspaceFolder}/c6", "${workspaceFolder}/c7", "${workspaceFolder}/c8"]}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.executable.candidates"} + }, + { + "id": "executable-on-attach", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/requests", "value": ["launch", "attach"]}, {"op": "set", "path": "/context/manifestMetadata/permissions", "value": ["debug.attach", "debug.launch"]}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"attach": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "program", "candidates": ["${workspaceFolder}/build/app"]}}}}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.attach.executable"} + }, + { + "id": "missing-target-executable", + "layer": "corePlan", + "edits": [{"op": "set", "path": "/block/settings", "value": [{"id": "program", "type": "file", "title": "Program", "default": "", "scope": "project", "effect": "nextRun"}]}, {"op": "set", "path": "/context/manifestMetadata/requires/settings", "value": 2}, {"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"targetBindings": {"program": "target.executable"}, "executable": {"setting": "program", "candidates": ["${workspaceFolder}/build/app"]}}}}], + "expected": {"code": "debug.target.invalid", "field": "target.executable"} + }, + { + "id": "escaping-parameters-schema", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/configurations", "value": {"launch": {"parametersSchema": "schemas/parameters.json"}}}, {"op": "set", "path": "/context/packageResources", "value": [{"path": "bin/synthetic", "kind": "file"}, {"path": "schemas/parameters.json", "kind": "symlink", "target": "../../../outside/parameters.json"}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].configurations.launch.parametersSchema"} + }, + { + "id": "api-level-below-4", + "layer": "coreManifest", + "edits": [{"op": "set", "path": "/context/manifestMetadata/pluginAPIVersion", "value": 2}, {"op": "remove", "path": "/context/manifestMetadata/requires"}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters"} + }, + { + "id": "missing-adapter-executable", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/context/packageResources", "value": []}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.process.launch.path"} + }, + { + "id": "adapter-executable-is-directory", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/context/packageResources", "value": [{"path": "bin/synthetic", "kind": "directory"}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.process.launch.path"} + }, + { + "id": "escaping-adapter-executable", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/context/packageResources", "value": [{"path": "bin/synthetic", "kind": "symlink", "target": "../../outside/adapter"}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.process.launch.path"} + }, + { + "id": "adapter-executable-link-is-dangling", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/context/packageResources", "value": [{"path": "bin/synthetic", "kind": "symlink", "target": "real/missing"}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.process.launch.path"} + }, + { + "id": "adapter-executable-link-targets-directory", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/context/packageResources", "value": [{"path": "bin/synthetic", "kind": "symlink", "target": "real"}, {"path": "bin/real", "kind": "directory"}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.process.launch.path"} + }, + { + "id": "adapter-executable-link-loop", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/context/packageResources", "value": [{"path": "bin/synthetic", "kind": "symlink", "target": "again"}, {"path": "bin/again", "kind": "symlink", "target": "synthetic"}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.process.launch.path"} + }, + { + "id": "runtime-adapter-entry-missing", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/launch/process/launch", "value": {"kind": "runtime", "tool": "java", "entry": "lib/adapter.jar"}}, {"op": "set", "path": "/context/packageResources", "value": []}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.process.launch.entry"} + }, + { + "id": "non-canonical-path-is-still-found", + "layer": "coreInstall", + "edits": [{"op": "set", "path": "/block/debugAdapters/0/launch/process/launch/path", "value": "./bin//synthetic"}, {"op": "set", "path": "/context/packageResources", "value": [{"path": "bin/synthetic", "kind": "directory"}]}], + "expected": {"code": "plugin.manifest.invalid", "field": "debugAdapters[0].launch.process.launch.path"} + }, + { + "id": "stale-registration", + "layer": "corePlan", + "edits": [{"op": "set", "path": "/context/registration/currentGeneration", "value": 2}], + "expected": {"code": "debug.provider.stale", "field": "registration"} + } + ] +} diff --git a/contract/v1/fixtures/debug-adapter.plugin.json b/contract/v1/fixtures/debug-adapter.plugin.json new file mode 100644 index 0000000..568cfa8 --- /dev/null +++ b/contract/v1/fixtures/debug-adapter.plugin.json @@ -0,0 +1,96 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.debug-adapter", + "displayName": "Debug Adapter Fixture", + "version": "0.1.0", + "pluginAPIVersion": 4, + "requires": { "debugAdapters": 1, "serverProcess": 1, "settings": 2 }, + "vendor": { + "id": "org.example", + "displayName": "Example", + "signatureRequirement": "publisherPackage" + }, + "permissions": [ + "debug.attach", + "debug.launch" + ], + "activationEvents": [ + "onDebug:example", + "onDebug:native" + ], + "languages": [], + "settings": [ + { + "id": "program", + "type": "file", + "title": "Program", + "description": "The program to debug. Build it with debug information first, for example cc -g main.c -o main.", + "default": "", + "placeholder": "Found next to the open file", + "scope": "project", + "effect": "nextRun" + } + ], + "debugAdapters": [ + { + "id": "example", + "displayName": "Example Debugger", + "adapterID": "example-dap", + "requests": ["launch", "attach"], + "selectors": [{ "targetKinds": ["file"], "languageIDs": ["example"] }], + "configurationTypes": ["example"], + "configurations": { + "launch": { + "arguments": { "stopOnEntry": false }, + "parametersSchema": "schemas/launch.json", + "targetBindings": { "program": "target.path", "cwd": "target.workingDirectory" } + }, + "attach": { + "targetBindings": { "processId": "target.processID" } + } + }, + "capabilities": { "disabled": ["supportsStepBack"] }, + "console": { + "commandPrefix": "`", + "commands": [ + { "name": "breakpoint set", "description": "Set a breakpoint" }, + { "name": "frame variable" } + ] + }, + "launch": { + "process": { + "launch": { "kind": "packaged", "path": "bin/example-dap" }, + "arguments": ["--stdio"] + }, + "transport": "stdio" + } + }, + { + "id": "native", + "displayName": "Native Debugger", + "adapterID": "native-dap", + "requests": ["launch"], + "selectors": [{ "targetKinds": ["file"], "languageIDs": ["example"] }], + "configurationTypes": ["native"], + "configurations": { + "launch": { + "arguments": { "stopOnEntry": false }, + "targetBindings": { "program": "target.executable", "cwd": "target.workingDirectory" }, + "executable": { + "setting": "program", + "candidates": [ + "${fileDirectory}/${fileBasenameNoExtension}", + "${workspaceFolder}/build/${fileBasenameNoExtension}" + ] + } + } + }, + "launch": { + "process": { + "launch": { "kind": "systemCommand", "commands": ["native-dap"] } + }, + "transport": "stdio" + } + } + ] +} diff --git a/contract/v1/plugin-manifest.schema.json b/contract/v1/plugin-manifest.schema.json index e210b47..26b7512 100644 --- a/contract/v1/plugin-manifest.schema.json +++ b/contract/v1/plugin-manifest.schema.json @@ -9,6 +9,12 @@ "if": { "required": ["pluginAPIVersion"], "properties": { "pluginAPIVersion": { "minimum": 3 } } }, "then": { "required": ["requires"] }, "else": { "not": { "required": ["requires"] } }, + "allOf": [ + { + "if": { "required": ["pluginAPIVersion"], "properties": { "pluginAPIVersion": { "maximum": 3 } } }, + "then": { "properties": { "debugAdapters": { "maxItems": 0 } } } + } + ], "properties": { "schemaVersion": { "const": 2 }, "id": { "type": "string", "pattern": "^[a-z0-9.-]+$" }, @@ -24,12 +30,13 @@ "backend": { "description": "Required exactly when backend is present: the plugin's own process serving its web pages.", "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.", "enum": [1] }, "buildSystems": { "description": "Declarative build detection, models, and command plans; does not imply buildProviders support.", "enum": [1, 2] }, + "debugAdapters": { "description": "Required exactly when debugAdapters is non-empty. The DAP session itself is negotiated by the adapter in initialize.", "enum": [1] }, "executableResolution": { "enum": [1], "description": "Shared executable candidate ordering and strict selection." }, "executionProviders": { "enum": [1] }, "languageServers": { "enum": [1] }, "languages": { "enum": [1] }, "projectGenerators": { "description": "Entries of the New Project window: fields, requirements, and steps.", "enum": [1] }, - "serverProcess": { "description": "The launch fields of language servers, of build providers that start their own process, of VCS providers, and of the plugin backend.", "enum": [1] }, + "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] }, "settings": { "enum": [1] }, "ui": { "description": "Version 2 adds web content; version 3 adds native controls.", "enum": [1, 2, 3] }, "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] } @@ -47,11 +54,11 @@ }, "permissions": { "type": "array", - "items": { "enum": ["backend.launch", "credentials.store", "languageServer.launch", "process.build", "process.run", "process.test", "process.vcs", "project.generate", "ui.contribute", "ui.web"] } + "items": { "enum": ["backend.launch", "credentials.store", "debug.attach", "debug.launch", "languageServer.launch", "process.build", "process.run", "process.test", "process.vcs", "project.generate", "ui.contribute", "ui.web"] } }, "activationEvents": { "type": "array", - "items": { "type": "string", "pattern": "^(onLanguage|onRun|onTest|onView|onCommand|onBuildSystem|onVcsRoot):[a-z0-9.+_-]+$" } + "items": { "type": "string", "pattern": "^(onLanguage|onRun|onTest|onView|onCommand|onBuildSystem|onVcsRoot|onDebug):[a-z0-9.+_-]+$" } }, "languages": { "type": "array", @@ -82,6 +89,12 @@ "maxItems": 8, "items": { "$ref": "#/$defs/vcsProvider" } }, + "debugAdapters": { + "description": "pluginAPIVersion 4, requires.debugAdapters 1: debug adapters the plugin implements, each started as its own DAP process. Requires debug.launch (and debug.attach when an adapter serves attach) and one onDebug: activation event per adapter.", + "type": "array", + "maxItems": 16, + "items": { "$ref": "#/$defs/debugAdapter" } + }, "projectGenerators": { "description": "pluginAPIVersion 3 with requires.projectGenerators: entries of the New Project window. Requires the project.generate permission.", "type": "array", @@ -212,6 +225,386 @@ } }, "$defs": { + "debugLocalID": { + "type": "string", + "pattern": "^[a-z][a-z0-9._-]{0,63}$" + }, + "debugPackagePath": { + "description": "Read-only package-relative path. Core must additionally reject escaping symlinks and platform-specific path escapes.", + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(?:^|/)[.]{1,2}(?:/|$))[^\\\\:\\u0000]+$" + }, + "debugServerProcessV1": { + "$ref": "#/$defs/serverProcess", + "description": "Reuse existing launch fields and validation. LSP initializationOptions have no meaning for a DAP transport and are excluded.", + "propertyNames": { + "enum": [ + "launch", + "arguments", + "validationArguments", + "runtimeDependencies" + ] + }, + "unevaluatedProperties": false + }, + "debugRunSelectedRuntimeProcess": { + "description": "Proposed serverProcess 2 branch. Resolve through Run's selected runtime for this target, not a plugin dependency or a separate Debug runtime preference.", + "type": "object", + "additionalProperties": false, + "required": [ + "launch" + ], + "properties": { + "launch": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "tool" + ], + "properties": { + "kind": { + "const": "runSelectedRuntime" + }, + "tool": { + "enum": [ + "python", + "java", + "node" + ] + } + } + }, + "arguments": { + "$ref": "#/$defs/serverProcess/properties/arguments" + }, + "validationArguments": { + "$ref": "#/$defs/serverProcess/properties/validationArguments" + } + } + }, + "debugProcess": { + "oneOf": [ + { + "$ref": "#/$defs/debugServerProcessV1" + }, + { + "$ref": "#/$defs/debugRunSelectedRuntimeProcess" + } + ] + }, + "debugLaunch": { + "type": "object", + "required": [ + "process", + "transport" + ], + "properties": { + "process": { + "$ref": "#/$defs/debugProcess" + } + }, + "oneOf": [ + { + "properties": { + "transport": { + "const": "stdio" + } + } + }, + { + "required": [ + "endpoint" + ], + "properties": { + "transport": { + "const": "tcpAnnounced" + }, + "endpoint": { + "description": "A bounded line decoder, not an executable-name-specific format or a regex engine. Only loopback endpoints are accepted.", + "type": "object", + "additionalProperties": false, + "required": [ + "stream", + "prefix", + "value" + ], + "properties": { + "stream": { + "enum": [ + "stdout", + "stderr" + ] + }, + "prefix": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[^\\r\\n]+$" + }, + "value": { + "enum": [ + "port", + "hostPort" + ] + } + } + } + } + }, + { + "required": [ + "endpoint" + ], + "properties": { + "transport": { + "const": "tcpPort" + }, + "endpoint": { + "description": "A fixed loopback port for this adapter process, not the debuggee's attach port. Conflicts fail explicitly; no automatic port substitution.", + "type": "object", + "additionalProperties": false, + "required": [ + "port" + ], + "properties": { + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + } + } + } + } + ], + "unevaluatedProperties": false + }, + "debugSelector": { + "description": "Optional automatic matching: OR between selectors, AND between fields. Omission means explicit selection only, not a wildcard registration.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "targetKinds": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "file", + "executable", + "runTarget", + "process", + "connection" + ] + } + }, + "languageIDs": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[a-z0-9+_-]+$" + } + } + } + }, + "debugConfiguration": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "arguments": { + "description": "Adapter-owned default DAP arguments. Ordinary JSON data; no embedded references, expressions or resource handles are interpreted.", + "type": "object" + }, + "parametersSchema": { + "description": "Optional packaged schema for final merged DAP arguments, with local read-only refs only. Not a new Debug configuration file.", + "$ref": "#/$defs/debugPackagePath" + }, + "targetBindings": { + "description": "Typed projections into known DAP fields. They only fill missing arguments; explicit values retain their existing precedence.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "program": { + "description": "`target.path` is the target's own path. `target.executable` is the program the host found through `executable`, which then must be declared.", + "enum": ["target.path", "target.executable"] + }, + "processId": { + "const": "target.processID" + }, + "cwd": { + "const": "target.workingDirectory" + } + } + }, + "executable": { + "description": "Launch only. Where a host finds the program a source file builds into: the user's `file` setting first, then the candidates. Required exactly when `targetBindings.program` is `target.executable`.", + "type": "object", + "additionalProperties": false, + "required": ["setting"], + "properties": { + "setting": { + "description": "ID of a `file` setting of the same plugin.", + "type": "string", + "minLength": 1 + }, + "candidates": { + "description": "Paths tried in order when the setting is empty. Each starts with `${workspaceFolder}/` or `${fileDirectory}/` and may use `${fileBasenameNoExtension}`; no `..`.", + "type": "array", + "maxItems": 8, + "uniqueItems": true, + "items": { + "type": "string", + "maxLength": 1024, + "pattern": "^\\$\\{(workspaceFolder|fileDirectory)\\}/" + } + } + } + } + } + }, + "debugCapabilities": { + "description": "Optional suppression only. Disabled entries are standard DAP initialize capability names plus exceptionBreakpointFilters. Omission follows DAP; true values and function implementations are not accepted.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "disabled": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^(supports[A-Z][A-Za-z0-9]*|exceptionBreakpointFilters)$" + } + } + } + }, + "debugConsole": { + "description": "Debugger commands the adapter's console understands, offered as completions while typing in the Debug Console. Adapters that answer the DAP completions request complete live; this list is for the ones that do not. Names are inserted as written; Core refuses padded or duplicate names.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "commandPrefix": { + "description": "Text the adapter's console reads as \"what follows is a debugger command\". The host puts it before the typed line when it asks the adapter to complete, so the adapter proposes commands instead of expression keywords. No spaces.", + "type": "string", + "minLength": 1, + "maxLength": 8, + "pattern": "^\\S+$" + }, + "commands": { + "type": "array", + "minItems": 1, + "maxItems": 1024, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "description": { + "description": "One line shown beside the name, as written. Not translated by the host.", + "type": "string", + "minLength": 1, + "maxLength": 240 + } + } + } + } + } + }, + "debugAdapter": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "displayName", + "launch" + ], + "properties": { + "id": { + "$ref": "#/$defs/debugLocalID" + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "adapterID": { + "description": "DAP initialize adapterID. Defaults to the local adapter id; never substitutes for the qualified Registry identity.", + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "requests": { + "description": "Defaults to launch only. Attach must be explicitly declared and separately authorized.", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "launch", + "attach" + ] + }, + "default": [ + "launch" + ] + }, + "selectors": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/debugSelector" + } + }, + "configurationTypes": { + "description": "Optional imported launch.json type aliases. Ambiguity requires selection; aliases do not override another provider.", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "configurations": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "launch": { + "$ref": "#/$defs/debugConfiguration" + }, + "attach": { + "$ref": "#/$defs/debugConfiguration" + } + } + }, + "capabilities": { + "$ref": "#/$defs/debugCapabilities" + }, + "console": { + "$ref": "#/$defs/debugConsole" + }, + "launch": { + "$ref": "#/$defs/debugLaunch" + } + } + }, "generatorCondition": { "type": "object", "additionalProperties": false, diff --git a/guides/debug-adapter-plugin.md b/guides/debug-adapter-plugin.md new file mode 100644 index 0000000..824f5f6 --- /dev/null +++ b/guides/debug-adapter-plugin.md @@ -0,0 +1,147 @@ +# 编写调试适配器插件(API 4) + +这篇指南面向想让 Lithe 调试某种语言或运行时的插件作者。调试适配器插件只需要一份 `plugin.json`:声明一个遵循 +[DAP(Debug Adapter Protocol)](https://microsoft.github.io/debug-adapter-protocol/)的进程怎么启动,Lithe 负责断点、 +单步、变量、调用栈、调试控制台这些界面和会话管理。我们以官方的 +[LLDB 插件](https://github.com/Lithe-IDEA/lithe-plugin-lldb)为例,它用 Xcode 自带的 `lldb-dap` 调试 C、C++、Objective-C、Rust 和 Swift。 + +字段的权威定义在[契约](../contract/v1/README.md#api-level-4-debug-adapters),这里只讲怎么用、为什么这样设计。发布步骤见 +[发布社区插件](https://github.com/Lithe-IDEA/Lithe-store/blob/main/docs/publishing.md)。 + +> **宿主支持。** 调试适配器是 API 4,需要所用 Lithe 明确支持 API 4。目前只有 macOS 宿主实现了调试适配器; +> Windows 宿主还没有。不要只凭应用版本号推断支持情况,以[契约](../contract/v1/README.md)和发行说明为准。 + +## 1. 插件和宿主各自负责什么 + +| 对象 | 插件作者负责 | Lithe 负责 | +| --- | --- | --- | +| 调试适配器 | 选型、许可、固定版本、启动入口 | 启动进程、DAP 通信、会话停止 | +| 要调试的程序 | 说明怎么产生它(构建命令、编译参数) | 在设置里显示实际找到的路径,把它交给适配器 | +| 调试界面 | 不需要写 | 断点、变量、调用栈、调试控制台与补全 | +| 权限 | 声明 `debug.launch` | 安装时让用户确认 | +| 激活 | 声明 `onDebug:<适配器 id>` | 调试会话需要时才启动适配器 | + +插件不加载进 Lithe 进程。清单是纯数据:没有解析器、没有工作流、没有宿主集成,也不能传 `initializationOptions`,写了会被拒绝。 + +## 2. 最小清单 + +```json +{ + "schemaVersion": 2, + "id": "com.example.demo-debug", + "displayName": "Demo Debugger", + "version": "0.1.0", + "pluginAPIVersion": 4, + "requires": { "debugAdapters": 1, "languages": 1, "serverProcess": 1 }, + "vendor": { "id": "com.example", "displayName": "Example", "signatureRequirement": "none" }, + "permissions": ["debug.launch"], + "activationEvents": ["onDebug:demo"], + "languages": [ + { "id": "demo", "displayName": "Demo", "fileExtensions": ["demo"] } + ], + "debugAdapters": [ + { + "id": "demo", + "displayName": "Demo", + "requests": ["launch"], + "selectors": [{ "targetKinds": ["file"], "languageIDs": ["demo"] }], + "configurations": { + "launch": { + "arguments": { "stopOnEntry": false }, + "targetBindings": { "program": "target.path", "cwd": "target.workingDirectory" } + } + }, + "launch": { + "process": { "launch": { "kind": "systemCommand", "commands": ["demo-dap"] } }, + "transport": "stdio" + } + } + ] +} +``` + +要点: + +- `pluginAPIVersion` 是 `4`,`requires` 必须逐项列出实际用到的能力(`debugAdapters` 为 `1`)。 +- 每个适配器都要有一条 `onDebug:` 激活事件,并声明 `debug.launch` 权限。适配器列出 `attach` 时才需要(也只能声明)`debug.attach`。 +- `selectors` 决定 Lithe 在哪些目标上自动选这个适配器:`targetKinds` 是目标种类,`languageIDs` 是语言。不写 `selectors` 表示只能被用户显式选择。 +- `languages[]` 声明适配器服务的语言。如果这个语言已经由别的插件提供(例如 Rust),你的声明只会给它增加“可以调试”的能力,并并入你声明的文件扩展名;语言名、语言标识和语言服务器不变,装上调试器也不会重启正在运行的语言服务器。 +- `targetBindings` 把调试目标的事实填进适配器的启动参数:`program` ← 目标路径,`cwd` ← 工作目录,`processId` ← 进程 ID。只填 `arguments` 里没写的字段,不会覆盖你显式写出的默认值。目标缺少绑定要读的事实时,Lithe 会拒绝这次启动并说明缺什么。 +- `configurationTypes` 声明适配器对应的 `launch.json` `type`,用于导入用户已有的 VS Code 风格配置。 + +## 3. 怎么启动适配器 + +`launch.process` 有三种常见写法: + +| 写法 | 适用场景 | +| --- | --- | +| `{"launch": {"kind": "packaged", "path": …}}` | 适配器许可允许分发、你能为每个平台提供构建;可执行文件随插件包安装 | +| `{"launch": {"kind": "systemCommand", "commands": ["lldb-dap"]}}` | 适配器已经装在用户机器上(例如随 Xcode 命令行工具提供);Lithe 用自己的工具发现(PATH、项目工具链、用户覆盖)解析命令名,macOS 上也覆盖 Xcode 工具链 | +| `{"launch": {"kind": "runSelectedRuntime", "tool": "python"}}` | 适配器是 Python / Java / Node.js 程序,用用户选定的运行时启动;这是 `serverProcess` 2 唯一的用途 | + +`launch.transport` 是 `stdio`,或者 TCP(`tcpAnnounced`:适配器在输出里打印端口;`tcpPort`:固定端口)。TCP 只允许回环地址。 + +插件包和 Lithe 的安装目录运行时是只读的,适配器不能往里面写日志、索引或状态,否则会破坏代码签名和增量更新。macOS 上宿主把适配器的状态目录放在 Application Support 的 `debug-adapters/<适配器 id>` 下。 + +## 4. 调试要先编译的语言:`executable` + +DAP 适配器大多直接接受“要运行的程序”,但 C、C++、Swift 这类语言的用户打开的是**源文件**,真正要调试的是编译产物。 +`configurations.launch.executable` 就是为这种情况设计的: + +```json +"configurations": { + "launch": { + "targetBindings": { "program": "target.executable", "cwd": "target.workingDirectory" }, + "executable": { + "setting": "program", + "candidates": [ + "${fileDirectory}/${fileBasenameNoExtension}", + "${workspaceFolder}/${fileBasenameNoExtension}", + "${workspaceFolder}/build/${fileBasenameNoExtension}" + ] + } + } +} +``` + +- `setting` 是你在 `settings[]` 里声明的一个 `file` 类型设置的 id。用户在设置页里选了路径,就用它。 +- 用户没选时,Lithe 依次查找 `candidates`(最多 8 个,每个必须以 `${workspaceFolder}/` 或 `${fileDirectory}/` 开头,可以用 `${fileBasenameNoExtension}`,不能含 `..`),用找到的第一个可运行文件。 +- 找到的路径会**回显在这个设置里**,用户能看到实际会调试哪个文件。 +- 用户选的路径如果不是可运行文件,是错误,**不会**悄悄回落到候选路径。 +- `executable` 和 `targetBindings.program: "target.executable"` 必须一起声明,缺一不可。 + +设置的 `description` 是教用户怎么产生这个程序的地方,写清楚构建命令(例如 `clang -g main.c -o main`)和适配器本身的安装方式。LLDB 插件的 `program` 设置就是这样写的。设置的写法见契约的 `settings` 一节,`scope: "project"`、`effect: "nextRun"` 适合这类“下次运行生效”的路径。 + +## 5. 调试控制台:提示符、补全和 `console` + +Lithe 的调试控制台有输入行和补全弹窗。补全来自适配器的 DAP `completions` 请求;`console` 补充两件事: + +- `commandPrefix`:1 到 8 个字符、不含空格。有些适配器把“光秃秃的单词”当表达式(lldb-dap 就是这样),补全第一个词时会混进 `long` 之类的语言关键字。声明前缀(lldb-dap 用反引号 `` ` ``)后,Lithe 只在向适配器请求补全时把它加在输入前面,并把适配器的答案移回去,用户看不到它。 +- `commands[]`:适配器不支持 `completions`,或者它的实时答案缺一些命令时,列出调试器命令(`name`,可选的一行 `description`)。名字按原样插入,不能重复,不要在名字里补空格;说明按原样显示,不翻译。 + +两者至少声明一个。适配器能实时补全时,不要把整个命令集再抄一遍。 + +## 6. 本地验证 + +1. 对照契约示例 `contract/v1/fixtures/debug-adapter.plugin.json`,并用[编辑器 Schema](../README.md#在编辑器里校验-pluginjson) 检查字段。 +2. `contract/v1/fixtures/debug-adapter-rejections.json` 列出每种拒绝的稳定错误码、出错字段和报告它的层(`schema`、`coreManifest`、`corePlan`、`coreInstall`),写测试时可以直接对照。 +3. 在支持 API 4 的 Lithe 里安装插件,打开一个目标文件,设置断点并启动调试。适配器被拒绝请求时,Lithe 会显示适配器自己的解释,所以适配器返回的错误文字要写得让用户能看懂。 +4. 在设置页确认 `executable` 找到的路径被回显,再把它改成一个不存在的文件,确认得到的是错误而不是悄悄换文件。 + +## 7. 安装后是否需要重启 + +只贡献调试适配器(以及它声明的语言和设置)的插件,在支持热激活的 Lithe 里安装、更新、回退、卸载、启用、停用后**不需要重启**。是否免重启由宿主按它实际能在运行期登记和撤回的贡献类型决定,不是 `plugin.json` 里的字段,所以插件作者不需要也不能声明。带有其他贡献(语言服务器、构建系统、界面等)的插件仍然在重启后生效。 + +## 8. 发布 + +调试适配器插件需要 API 4,只有支持 API 4 的 Lithe 才能安装。旧版 Lithe 在插件市场里会提示升级,而不是安装。 + +- 在 Lithe 稳定版支持 API 4 之前,把你的 GitHub Release 标为**预发布**。Lithe Store 的自动追踪只收正式 Release,预发布需要提交一个手工的索引条目,写法参考 Maven、Gradle 和 LLDB 插件的 0.1.0。 +- 稳定版支持 API 4 后,把 Release 转为正式版,追踪器会自动提交后续版本的索引更新。 +- 发布后不要替换或删除资产;索引固定了它的 SHA-256。 + +## 9. 当前边界 + +- 只有 macOS 宿主实现了调试适配器。 +- 宿主调试入口目前只发起 `launch` 请求;`attach` 可以在清单里声明,但宿主入口还没有完成,不要依赖。 +- 适配器声明是纯数据,没有自定义界面、没有宿主集成点;需要调试之外的能力(构建、测试、运行配置)请另用对应的贡献类型。