From 00f31d0dff428c91c44294df9118b7e73f4f5db0 Mon Sep 17 00:00:00 2001 From: Yao Jingxi <23722032@bjtu.edu.cn> Date: Sat, 17 Oct 2026 16:51:17 +0800 Subject: [PATCH] docs: add LSP plugin lifecycle and build integration guides --- CONTRIBUTING.md | 4 + README.md | 2 + docs/build-tool-integration.md | 188 ++++++++++++++++ docs/lsp-plugin-lifecycle.md | 385 +++++++++++++++++++++++++++++++++ docs/publishing.md | 4 + 5 files changed, 583 insertions(+) create mode 100644 docs/build-tool-integration.md create mode 100644 docs/lsp-plugin-lifecycle.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89f5357..55bc17b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,4 +15,8 @@ - 不修改已发布版本的 `downloadURL` 与 `sha256`;不删除历史版本(撤回问题版本除外)。 - 插件的 bug 与功能请求请提到插件自己的仓库,本仓库只处理收录相关事务。 +第一次开发语言插件,先读[LSP 插件完整生命周期](docs/lsp-plugin-lifecycle.md), +再按[构建、运行与测试接入指南](docs/build-tool-integration.md)扩展能力, +最后按[发布文档](docs/publishing.md)提交收录。 + 审查标准见 [docs/maintainers.md](docs/maintainers.md#审查清单)。 diff --git a/README.md b/README.md index be8a00d..04d6514 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Lithe 会把仓库地址解析为 | 文档 | 内容 | | --- | --- | +| [LSP 插件完整生命周期](docs/lsp-plugin-lifecycle.md) | 从 Lua 模板开始:清单、服务器打包、激活、状态目录、停止、升级与卸载 | +| [构建、运行与测试接入](docs/build-tool-integration.md) | 可用的执行提供者示例、工程后端边界,以及开发中的 API 3 状态 | | [docs/publishing.md](docs/publishing.md) | 插件作者:如何打包、发布 Release、提交收录和发布新版本 | | [docs/versioning-and-updates.md](docs/versioning-and-updates.md) | 版本号如何追踪、Lithe 如何选择可安装版本、用户如何更新/回退 | | [docs/maintainers.md](docs/maintainers.md) | 维护者:审查清单、下架与撤回、同步 Lithe 契约 | diff --git a/docs/build-tool-integration.md b/docs/build-tool-integration.md new file mode 100644 index 0000000..f505fc5 --- /dev/null +++ b/docs/build-tool-integration.md @@ -0,0 +1,188 @@ +# 为语言插件接入构建、运行与测试 + +LSP 服务器负责语言分析,构建工具负责项目导入、编译、依赖解析与产物。 +一个实用的语言插件应把这两条链路分别验证,再验证它们在保存、切换项目和停止时能配合工作。 +先完成[LSP 插件生命周期教程](lsp-plugin-lifecycle.md),再按本篇增加执行能力。 +本篇的可操作示例以 macOS 声明式插件宿主为范围;Windows 仍有独立扩展实现, +不能凭 Core 校验通过推断已有相同的运行/测试宿主能力。 + +## 1. 先选择当前能发布的接入方式 + +本文于 2026-10-17 对照 Lithe 主仓库及 Lua 模板核对。API 3 的构建系统接口仍在开发; +下面区分已支持的声明与 Core 中尚未形成产品闭环的接口。发布前还要核实目标宿主版本。 + +| 需求 | 当前接入方式 | 能做什么、边界在哪里 | +| --- | --- | --- | +| 补全、跳转、诊断 | API 1/2 `languageServers` | 按 LSP 提供能力,不自动声明编译和测试 | +| 运行文件或固定项目命令 | API 1/2 `executionProviders[].run` | 命令与 argv 模板,工作目录为工作区根;不提供通用目标选择或构建模型 | +| 运行整个工程、文件所在范围或指定测试 | API 1/2 `executionProviders[].test` | 测试项目标记、文件发现和命令规划;不是完整语义测试发现与结构化结果协议 | +| 官方 Java 项目的运行/测试/调试 | 官方 Java 插件的 API 2 `hostIntegration: jdt` | 宿主已有 JDT 集成,不能复制为社区通用扩展点 | +| 通用 Build 工具窗口、目标与依赖模型、增量同步 | API 3 `buildSystems`,开发中 | Core 验证/规划接口预览,尚不能作为市场可安装的社区构建插件发布 | + +当需求只是 `lua <当前文件>`、`go test ./...` 这类固定命令,可以使用现有执行提供者。 +当需求需要 Maven/Gradle/Cargo 的模块、profile、target、classpath、依赖树或任务动态发现, +应使用成熟构建后端和完整模型;不要把一个 `run` 命令包装成已经完成工程集成。 + +## 2. 给 Lua 模板加上运行能力 + +在生命周期教程的最小清单中,替换下面三个顶层字段,其他字段保留: + +```json +{ + "permissions": ["languageServer.launch", "process.run"], + "activationEvents": ["onLanguage:lua", "onRun:lua"], + "executionProviders": [ + { + "languageID": "lua", + "run": { + "executable": { "command": "lua" }, + "arguments": ["${file}", "${userArguments}"] + } + } + ] +} +``` + +这也是[Lua 模板](https://github.com/Lithe-IDEA/lithe-plugin-lua)实际采用的方式。 +用户需要另行安装 Lua 解释器;随包提供的 LuaLS 不等于解释器。 +`process.run` 声明运行权限,`onRun:lua` 声明按运行操作激活,二者都不能省略。 +测试声明同理需要 `process.test` 和 `onTest:`。 + +`executable` 只能选一个:裸 `command` 交给宿主工具路径发现,或使用宿主已经支持的 +项目 `toolchain` ID,例如 Go 的 `project-go`。插件不能发明一个 toolchain ID 就指望宿主实现。 + +所有参数都是独立 argv 元素,不经过 shell: + +| 模板 | 含义与范围 | +| --- | --- | +| `${file}` | 工作区相对路径;以 `-` 开头时宿主加 `./`,适用于运行、文件测试和用例测试 | +| `${fileDirectory}` | 文件所在目录,根目录为 `.`,其他为 `./…` | +| `${userArguments}` | 仅用于运行,必须独占一个元素,展开为用户参数数组 | +| `${testName}` | 指定测试用例名,去除首尾空白 | +| `${testName:regex}` | 将用例名的正则特殊字符转义,适合测试筛选参数 | + +不要在数组里写 `"lua ${file} && echo done"`、额外 shell 引号或依赖变量替换; +那只是一个普通参数。路径中的空格也不需要作者再加引号。API 1/2 执行提供者没有可自行 +设置的 `cwd`、`${projectDir}` 或通用构建设置字段,工作目录固定是工作区根。 + +## 3. 用 Go 说明三个测试范围 + +下面是一个 **Go 语言插件**的顶层字段片段。它来自现有 Go 提供者的命令结构; +需要与声明 `go` 语言及其服务器的清单合并,不能直接粘贴到 Lua 插件中使用。 + +```json +{ + "permissions": ["languageServer.launch", "process.run", "process.test"], + "activationEvents": ["onLanguage:go", "onRun:go", "onTest:go"], + "executionProviders": [ + { + "languageID": "go", + "run": { + "executable": { "toolchain": "project-go" }, + "arguments": ["run", "${file}", "${userArguments}"] + }, + "test": { + "frameworkID": "go", + "workspaceLabel": "All Go Tests", + "projectMarkers": ["go.mod", "go.work"], + "testFilePatterns": ["*_test.go"], + "executable": { "toolchain": "project-go" }, + "workspaceArguments": ["test", "./..."], + "fileArguments": ["test", "${fileDirectory}"], + "testCaseArguments": [ + "test", + { "value": "${fileDirectory}", "when": "fileSelected" }, + { "value": "./...", "when": "noFileSelected" }, + "-run", + "^${testName:regex}$" + ] + } + } + ] +} +``` + +- 工作区有任一 `projectMarkers` 才被识别为测试项目。`testFilePatterns` 是忽略大小写、 + 只支持 `*` 的文件名模式,不是正则或任意 glob。 +- 工作区范围运行 `go test ./...`;“文件”范围运行文件所在的包,符合 Go 的测试单位, + 不承诺只执行该文件。 +- 用例范围有选中文件时限定包,没有文件时使用 `./...`;引用 `${file}` 或 + `${fileDirectory}` 的用例参数必须用 `when: "fileSelected"` 保护。 +- 声明式发现返回工作区和匹配文件,不会仅凭上述字段解析任意框架的测试函数、 + 测试结果树或调试入口。要得到这些能力,需要对应后端及宿主消费支持。 + +这里的 `go run <文件>` 也只运行选中文件,不能替代需要多个源码文件的包运行。 +需求超出模板能力时,应如实说明边界,或等待完整构建模型接入。 + +## 4. 将项目事实留给成熟后端 + +先检查你选用的语言服务器或项目后端是否已经支持工程导入、构建命令和运行目标。 +例如 Java 的模块、classpath、测试/调试目标应使用相应 Java 后端给出的事实, +不要由插件另读构建文件再拼一套容易漂移的 classpath。其他语言也应复用其成熟工具链。 + +一个完整的工程工作流需要确认以下顺序: + +```text +保存文档 → 等待后端接收并同步工程变更 → 构建目标 → 检查构建结果 + → 取得本次构建后的运行路径/产物 → 启动所选入口 +``` + +构建失败时不能继续运行旧产物;项目切换后不能用旧模型启动新项目;取消后不能把迟到结果 +覆盖到新一次操作。上述职责需要真实后端与宿主配合,声明一条命令并不会自动完成。 + +构建工具正常解析项目依赖时,可能需要访问网络并写用户的工具缓存;这与启动插件时临时下载 +语言服务器不同。语言服务器和随包运行时应在打包阶段准备好。项目依赖、构建产物、 +Gradle daemon 状态、Maven 仓库或 Cargo target 等应由对应工具放到其用户缓存或工作区, +不能写入插件包或应用安装目录。每个后台进程都要有明确的停止与清理策略。 + +## 5. API 3 构建系统接口:目前能研究什么 + +**当前状态:开发中的 Core 接口,不是可发布的完整宿主功能。** +截至本次核对,已提交基线支持 API 1/2;开发工作区新增 API 3 的清单验证与纯规划模块。 +macOS/Windows 尚无通用 Build 工具窗口对它的完整消费链路,市场可安装版本选择也暂时跳过 +API 3。因此不要把自己的正式插件升到 API 3,再让用户通过安装市场获得尚未实现的能力。 + +如果你参与 Core/宿主开发,可以阅读开发版本中的 `buildSystems` 契约及 Cargo、Go、 +Gradle、Maven fixtures,并用下列问题检查设计。它们是接口评估项,不能替代产品验收: + +| 模型部分 | 需要回答的问题 | +| --- | --- | +| 检测 | 如何发现根工程、嵌套模块、wrapper,如何排除生成目录? | +| 工具与运行时 | wrapper/系统命令如何选择,所需 JDK/其他运行时如何发现和验证? | +| 选项 | profile、target、模式、平台等如何进入明确的参数与环境,而不是拼接 shell? | +| 工程模型 | 模块、目标、依赖和运行入口来自哪个成熟后端,如何解码与校验? | +| 任务 | 支持哪些范围、是否需要用户参数、是否执行项目代码或访问网络? | +| 同步 | 哪些配置变更触发重导入,如何取消旧请求并拒绝过期结果? | +| 测试与诊断 | 发现、执行、结果映射由谁提供,路径如何对应用户源码? | +| 生命周期 | 谁启动进程、监督超时、终止任务并清理状态,宿主是否已经消费这些计划? | + +Core 的计划接口本身不启动进程、不监视文件、不持久化选择,也不会显示界面。 +即使 fixture 能生成正确命令,宿主仍须实现执行、权限/确认、取消、状态、结果和工具窗口。 +不要把内部 dispatcher 命令写成社区插件可以调用的宿主脚本 API。 + +`languageServers[].hostIntegration` 的 JDT 集成与开发中的构建 `hostIntegration` 是不同接口, +都不是社区作者任意命名即可扩展的机制。现有 JDT 路径限官方插件; +不要复制官方 ID 或修改 `vendor` 来绕过限制。未来正式开放的能力以目标版本契约和宿主为准。 + +## 6. 验证完整工作流 + +打包与清单校验完成后,还应在目标平台、实际宿主中完成: + +1. 分别准备单文件、多文件、嵌套工程和带空格路径的项目,明确插件支持的范围。 +2. 保存修改后运行/测试,确认使用当前文件和目标;将源码改错,确认错误能被看到, + 完整构建集成应阻止继续启动旧产物。 +3. 覆盖工作区、文件和指定用例范围,核对实际命令、工作目录、输出与退出码。 +4. 缺少解释器、工具链版本不符、wrapper 无执行权限或依赖下载失败时,错误能定位到原因。 +5. 取消耗时任务,随后再运行;切换项目与关闭工作区,确认旧任务、锁与后端进程没有残留。 + 若宿主或工具尚不能做到,应报告限制,不能只在文档里宣称已经支持。 +6. 禁用/卸载语言插件后检查该语言的运行、测试与 LSP 都处于正确状态。 + 插件声明的语言由插件负责,不能假定缺失能力会回退到宿主内置实现。 +7. 清理验收启动的应用和辅助进程,再按[LSP 生命周期检查表](lsp-plugin-lifecycle.md#7-把停止失败和卸载也当作功能验收) + 验证重启、升级和卸载。 + +命令规划测试证明参数正确,进程冒烟验证证明工具能够启动,真实项目验收才能证明用户工作流 +成立。发布说明应明确已经验证的平台、宿主、语言服务器及构建工具版本。 + +相关入口:[生命周期教程](lsp-plugin-lifecycle.md) · [发布社区插件](publishing.md) · +[版本追踪](versioning-and-updates.md) · +[Lithe 插件契约](https://github.com/Lithe-IDEA/Lithe-IDEA/blob/preview/shared/contracts/plugins/v1/README.md)。 diff --git a/docs/lsp-plugin-lifecycle.md b/docs/lsp-plugin-lifecycle.md new file mode 100644 index 0000000..dde265f --- /dev/null +++ b/docs/lsp-plugin-lifecycle.md @@ -0,0 +1,385 @@ +# 从零构建一个完整生命周期的 LSP 插件 + +这篇教程面向想给 Lithe 增加语言支持的插件作者。我们用可运行的 +[Lua Support 模板](https://github.com/Lithe-IDEA/lithe-plugin-lua)走完选型、清单、语言服务器打包、 +安装与激活、日常运行、停止、升级和卸载;构建、运行与测试的接入另见 +[构建工具接入指南](build-tool-integration.md)。发布步骤见[发布社区插件](publishing.md)。 + +本文的安装与验收路径以 **macOS 的社区插件市场(Lithe 0.6.1 起)**为基线。 +API 2 示例需要所用宿主明确支持 API 2;不要只凭应用版本号推断支持情况。 +索引能描述 Windows/Linux 的包,不代表对应宿主已经实现相同的插件安装与运行流程。 +Lua 模板目前只提供 macOS arm64、x64 两个包。 + +## 1. 先确定插件与宿主各自负责什么 + +LSP(Language Server Protocol,语言服务器协议)让编辑器通过独立进程获得补全、诊断、 +跳转等能力。Lithe 的声明式插件用 JSON 描述要启动的程序;宿主读取清单、发现运行时并管理 +进程及 LSP 会话。你不需要编写宿主内的 Swift/JavaScript 插件入口。 + +```text +开发与构建:固定上游版本 → 下载并校验 → 组装各平台插件包 → 发布 zip → 索引收录 +用户安装:下载 zip → 校验摘要与清单 → 确认程序及权限 → 安装并记录完整性摘要 +正常运行:启用 → 打开对应语言的文件 → 启动服务器 → 初始化 LSP → 编辑与诊断 +结束使用:禁用/关闭工作区 → 宿主停止会话和进程;更新/卸载 → 替换或移除包 +``` + +| 对象 | 插件作者负责 | Lithe 负责 | +| --- | --- | --- | +| 语言识别 | 文件扩展名、完整文件名、项目标记 | 匹配文件并选择提供者 | +| 语言服务器 | 上游选型、许可、固定版本、启动入口、正确参数 | 校验声明、按需启动、LSP 通信与停止 | +| 运行时 | 声明依赖,或在允许的 API 中随包提供 | 按声明发现、检查并启动运行时 | +| 运行与测试 | 声明命令、参数和测试范围 | 生成计划、执行与展示结果 | +| 可写数据 | 指定上游使用的日志、索引和状态路径 | 为 API 2 状态模板提供平台可写目录 | +| 发布与维护 | 各平台产物、版本升级、兼容与回退验证 | 下载校验、安装审查、插件启停 | + +先选成熟且许可允许分发的语言服务器。项目导入、依赖解析、符号和模块模型应尽量由该语言的 +成熟后端提供。不要为了“接上 LSP”再复制一套工程模型。LSP 支持的具体功能仍取决于服务器 +能力与宿主支持;安装服务器不会自动带来构建、测试、DAP 调试或自定义界面。 + +## 2. 跑通原版模板,再改成自己的插件 + +本地需要 Git、Node.js 22+、`tar`、`zip` 和 `unzip`。打包工具使用 Node.js, +不代表目标语言服务器需要 Node.js;LuaLS 是平台相关的原生程序。 + +```bash +git clone https://github.com/Lithe-IDEA/lithe-plugin-lua.git +cd lithe-plugin-lua +node scripts/package.mjs +unzip -l dist/lua-support-0.3.0-darwin-arm64.zip +shasum -a 256 dist/lua-support-0.3.0-darwin-arm64.zip +``` + +这里的 `0.3.0` 对应本教程使用的模板版本;上游更新后,产物名以模板清单和脚本输出为准。 +脚本会重新生成 `dist/`,下载两个 macOS 架构的 LuaLS、核对固定 SHA-256,并输出每个 zip +的 SHA-256。Linux CI 可以组装 macOS 包,但这只证明打包成功,不能替代在目标 Mac 上运行。 + +模板源码与产物的关系: + +```text +源码仓库 +├── io.github.lithe-idea.lua-support/ +│ ├── plugin.json +│ └── bin/lithe-lua-language-server # 保留可执行权限的启动器 +├── language-server.json # 构建输入:版本、各平台 URL 与 SHA-256 +├── scripts/package.mjs # 下载、校验、解包、组装 zip +└── .github/workflows/ # PR 校验与标签发布 + +产物 lua-support-0.3.0-darwin-arm64.zip +└── io.github.lithe-idea.lua-support/ + ├── plugin.json + ├── bin/lithe-lua-language-server + └── language-server/ # 完整上游资源,不只是可执行文件 + ├── bin/lua-language-server + └── … # 上游依赖、脚本和许可文件 +``` + +派生自己的仓库后,同步修改以下位置: + +1. 包目录名、`plugin.json` 的 `id`、显示名、版本与 `vendor`,使用你自己的稳定反向域名。 +2. `scripts/package.mjs` 的 `PACKAGE_DIR`、`ASSET_PREFIX`,以及与新服务器对应的资源检查。 +3. `language-server.json` 的版本、各平台 URL、摘要和许可信息。它是模板脚本自己的输入, + 不是宿主自动解释的统一下载 API;更换清单结构时也要修改打包脚本。 +4. 启动器中的包内入口、参数和缓存命名空间。不能继续写入 Lua 模板的缓存目录。 +5. CI 中读取清单的目录名、Release 说明、README、许可证与第三方声明。 + +不要只换包目录名:模板 CI 中也有原插件 ID,启动器中也有状态路径。 + +## 3. 写一个最小、完整的 LSP 清单 + +下面是 API 1 的纯 LSP 示例。它沿用模板的目录布局,先只接入 LuaLS;如要运行 Lua 程序, +再按[构建工具指南](build-tool-integration.md)添加运行提供者。示例中的作者和 ID 要换成自己的。 + +```json +{ + "schemaVersion": 2, + "id": "io.github.alice.lua-support", + "displayName": "Lua Support", + "version": "0.1.0", + "pluginAPIVersion": 1, + "vendor": { + "id": "io.github.alice", + "displayName": "Alice", + "signatureRequirement": "none" + }, + "permissions": ["languageServer.launch"], + "activationEvents": ["onLanguage:lua"], + "languages": [ + { + "id": "lua", + "displayName": "Lua", + "fileExtensions": ["lua"], + "projectFileNames": [".luarc.json", ".luarc.jsonc"] + } + ], + "languageServers": [ + { + "languageID": "lua", + "languageIdentifier": "lua", + "launch": { + "kind": "packaged", + "path": "bin/lithe-lua-language-server" + }, + "arguments": [], + "validationArguments": [] + } + ] +} +``` + +三个版本不要混淆:`schemaVersion: 2` 是清单格式,`pluginAPIVersion: 1` 是宿主需要支持的 +插件 API 等级,`version: "0.1.0"` 是你的插件版本。上游 LuaLS 的版本另记在构建输入中。 +只声明实际需要的最低 API;API 1 的服务器参数是字面量,不展开 API 2 模板。 + +`languages[].id`、`languageServers[].languageID` 和 `onLanguage:` 必须对应。 +声明服务器需要 `languageServer.launch` 权限及激活事件;每个语言最多一个服务器。 +事件集合必须与实际提供者精确对应,不能随意增加未使用的事件。权限是能力声明, +不是限制外部程序磁盘或网络访问的操作系统沙箱。 +启用只表示允许激活,打开项目中相应语言的文档才触发服务器启动。 +文件后缀不等于 LSP 的文档标识,必要时单独设置 `languageIdentifier`。 + +### 选择启动方式 + +| 方式 | 适用场景 | 作者要做的事 | +| --- | --- | --- | +| API 1 `packaged` | 随包提供原生服务器或启动器 | 声明相对路径,打包全部资源并保留可执行权限 | +| API 1 `systemCommand` | 明确要求用户自行安装的服务器 | 声明裸命令候选和正确探测参数,记录用户安装要求 | +| API 2 `runtime` | 包内 JS/JAR 由 Node.js/Java 启动 | 声明包内入口及发现的运行时最低版本,或声明随包运行时 | + +例如,下面是 `languageServers[]` 中一个服务器声明的替代方案,要求用户预先安装 `gopls`, +完整清单还必须声明 `go` 语言及相应权限、事件: + +```json +{ + "languageID": "go", + "launch": { "kind": "systemCommand", "commands": ["gopls"] }, + "arguments": ["serve"], + "validationArguments": ["version"], + "runtimeDependencies": [{ "tool": "go" }] +} +``` + +探测参数必须能自行以退出码 0 结束,不能用会持续运行的 LSP 启动参数。若上游没有合适探测 +方式,可以像 Lua 模板一样用空数组,让 LSP 握手决定是否可用。通过探测仍不代表补全已经正常。 + +随包分发时优先使用固定、可复验的服务器。`systemCommand` 不会替用户安装工具,也不能作为 +启动时下载服务器的变通方案。运行时依赖与主入口是两回事:例如随包服务器调用 Go 时, +`runtimeDependencies` 让宿主发现 Go 并传递环境;API 1 支持 `node`、`go`,API 2 增加 `java`。 + +## 4. 在构建阶段准备完整语言服务器 + +沿用模板的处理顺序: + +1. 固定上游发行版本和每个平台/架构的 HTTPS 归档地址,核实许可证及再分发条件。 +2. 将归档 SHA-256 写入版本控制。摘要应通过上游校验资料或经过审查的下载确认, + 构建时不能现下载、现算一个摘要就当作预期值。 +3. 下载后先核对摘要,再解压到临时组装目录。缓存命中也必须重新校验;不匹配立即失败。 +4. 复制服务器所需的全部文件:二进制、脚本、资源、动态库、许可证等;检查声明入口存在。 +5. 检查归档没有符号链接、越界路径和开发机绝对路径,保留启动器与二进制的可执行权限。 +6. 按平台生成 zip,再计算最终 zip 的 SHA-256,交给市场索引固定。 + +上游归档摘要与最终 zip 摘要用途不同:前者保证构建输入,后者保证用户拿到同一个发布包。 +更换语言服务器时不能只替换下载地址,必须同步检查解压布局、入口、依赖、探测方式及许可。 +模板中的归档处理适配 LuaLS 的 tar.gz;其他格式需要在自己的打包脚本中明确实现。 + +社区插件使用自己的 ID 和 `vendor.signatureRequirement: "none"`,通过第三方市场源的 +摘要校验、用户确认和加载时内容复查安装。不要伪造 Lithe 发布者签名,也不要使用保留的 +`dev.lithe.*` 命名空间。官方包的发布者签名是另一条信任路径,签名必须在完整组装后完成。 + +## 5. 将可写状态与安装文件分开 + +Lithe 会在加载时复查社区包内容。服务器如果将日志、索引、锁或生成文件写回插件目录, +下次加载就可能失败;写入应用安装包还会破坏应用签名与增量更新基线。 + +| 数据 | 放在哪里 | 需要注意的生命周期 | +| --- | --- | --- | +| 服务器、运行时、默认配置、许可证 | 只读插件包 | 随插件版本安装和替换 | +| 日志、可重建缓存 | 平台用户缓存目录 | 设置容量/轮转策略,记录清理方法 | +| 项目索引、锁、项目相关状态 | 按工作区隔离的可写目录 | 不同项目、工作树不能共用同一份可变索引 | +| 用户源码与构建产物 | 用户工作区或构建工具指定目录 | 卸载插件不应误删用户工程 | + +API 1 的 Lua 模板通过启动器设置缓存目录,并使用 `exec` 将启动器替换为真实服务器。 +不要把服务器放到后台、`nohup` 或自行创建常驻守护进程,否则宿主难以完整停止它。 +标准输出只承载 LSP 的 JSON-RPC 帧,启动器提示与日志应写标准错误或日志文件。 + +模板中的自定义缓存路径属于启动器自己的约定,**不能假定宿主卸载时自动清理**。 +需要说明残留内容及安全删除方法。若自己的服务器维护工作区索引,还要增加稳定的工作区隔离, +不要直接复用模板的单一缓存目录保存所有项目索引。 + +### API 2:让宿主提供状态目录 + +目标宿主支持 API 2 时,可以将上面的 Lua 示例升到 `pluginAPIVersion: 2`, +把 `languageServers` 替换为以下值,直接运行包内 LuaLS 并传入宿主状态目录: + +```json +[ + { + "languageID": "lua", + "languageIdentifier": "lua", + "launch": { + "kind": "packaged", + "path": "language-server/bin/lua-language-server" + }, + "arguments": [ + "--logpath=${stateDir}", + "--metapath=${workspaceStateDir}" + ], + "validationArguments": [] + } +] +``` + +`${stateDir}` 是宿主为该插件服务器管理的可写状态目录,`${workspaceStateDir}` 进一步 +按工作区隔离。`${pluginDir}` 是只读包根路径;`${arch}` 的值是 `arm64` 或 `x86_64`, +与 Release 资产里的 `x64` 拼写不同。状态模板不能放进 `validationArguments`。 +不要将这些模板写进 API 1 的参数,宿主会按原样传给进程。 + +如果服务器需要 Node.js/Java,API 2 的 `runtime` 可免去启动器。以下是服务器声明中的 +`launch` 片段,入口要替换成自己的真实资源,服务器专用参数仍放在 `arguments`: + +```json +{ + "kind": "runtime", + "tool": "node", + "minimumVersion": "18.17", + "entry": "language-server/server.js" +} +``` + +宿主负责发现并检查运行时;不要再在 `runtimeDependencies` 中重复声明同一个启动工具。 +`runtime` 不声明 `validationArguments`,运行时探测由宿主执行。若改为 `packagedRuntime`, +该字段与 `minimumVersion` 互斥,插件要随包提供运行时、依赖和许可,并分别验证目标架构。 + +API 2 也支持 `initializationOptions` 和声明式 `settings`。只传上游真实支持的选项; +`${setting:}` 只能引用已声明设置,值必须符合类型和范围。设置是插件级、跨工作区共享, +修改后宿主重启相应服务器。详细格式见[正式契约](https://github.com/Lithe-IDEA/Lithe-IDEA/blob/preview/shared/contracts/plugins/v1/README.md#plugin-settings-api-2)。 + +## 6. 本地验证、安装与激活 + +先运行模板现有 CI 中的两层检查:`node scripts/package.mjs` 组装产物,随后用 Lithe Store +的 `readPackageManifests` 和 `checkManifest` 检查包路径、ID、版本和社区信任声明。 +可参考模板的 [CI 工作流](https://github.com/Lithe-IDEA/lithe-plugin-lua/blob/main/.github/workflows/ci.yml)。 +这些检查不会启动服务器,也不等同于完整的 Core 清单语义校验。 +在插件仓库根目录可以按 CI 的方式本地检查产物;下面的相邻目录应使用一个尚未占用的名字: + +```bash +git clone --depth 1 https://github.com/Lithe-IDEA/Lithe-store.git ../lithe-store-validation +node --input-type=module <<'JS' +import { readdirSync, readFileSync } from "node:fs"; +import { readPackageManifests, checkManifest } from "../lithe-store-validation/scripts/validate-index.mjs"; +const manifest = JSON.parse(readFileSync("io.github.lithe-idea.lua-support/plugin.json", "utf8")); +for (const name of readdirSync("dist")) { + const { manifests, unsafe } = readPackageManifests("dist/" + name, manifest.id); + const problems = [...unsafe, ...checkManifest(manifests, { id: manifest.id }, manifest)]; + if (problems.length) throw new Error(name + ": " + problems.join("; ")); + console.log("validated " + name); +} +JS +``` + +派生模板后,把这里读取 `plugin.json` 的目录名也换成自己的 ID。 +如果错误来自跨引用、模板上下文或 API 等级,还需对照契约并通过目标宿主的安装校验。 + +要在正式版 Lithe 中验收社区包,使用[发布流程](publishing.md)在自己的公开 GitHub 仓库 +发布 zip,再准备自己仓库根目录的 `lithe-plugin-index.json`。索引格式与 Store 相同, +下载 URL 指向该仓库的 Release 资产,摘要固定最终 zip;不必先申请进入公共 Store。 +在 Lithe 的“管理插件仓库…”中加入你的仓库 URL,即可通过自己的源安装测试。 +不要把正式版的“从磁盘安装”当作绕过社区来源与确认流程的开发入口;`none` 社区包走市场源。 +开发宿主的本地包例外属于单独信任路径,不能替代社区安装验收。 + +安装前先禁用与你的插件声明同一语言的其他插件,避免提供者冲突。然后: + +1. 在插件市场选择你的源和版本,确认显示的程序、权限、来源都符合预期。 + 安装或更新后按界面提示**重启 Lithe**,让新版本进入本次启动的插件图。 +2. 打开一个小项目中的 `.lua` 文件。应当出现补全、悬停、跳转和诊断; + 不要只检查进程“已运行”。 +3. 保存修改,确认诊断更新;关闭并重开文件,确认没有重复服务器和重复诊断。 +4. 从 Dock 启动 Lithe 再试一次,避免依赖终端启动时偶然继承的 PATH。 +5. 若增加运行/测试,分别验证;LuaLS 已正常工作并不代表用户的 `lua` 已安装。 + +启动后的协议顺序也应能解释清楚:宿主发送 `initialize`,服务器返回能力后接收 +`initialized`,随后按协商能力通过 `didOpen`、`didChange`、`didSave`、`didClose` 同步文档, +补全等请求和诊断沿同一会话传输。正常停止先 `shutdown`,再 `exit`;初始化未完成或关闭 +超时时,宿主可以强停进程。当前 macOS 默认初始化/请求超时为 30 秒、关闭超时为 2 秒。 +取消请求通过 `$/cancelRequest` 通知服务器,服务器仍可能完成计算,不能将取消当作进程已退出。 + +可用这个文件进行最小交互验收: + +```lua +---@param name string +local function greet(name) + return "Hello, " .. name +end + +print(greet("Lithe")) +``` + +悬停查看 `greet` 的签名,跳到定义,再将调用参数改成数字,观察类型诊断。 +具体提示随 LuaLS 版本和项目配置变化,验收时应记录所用版本和配置。 + +## 7. 把停止、失败和卸载也当作功能验收 + +声明式插件没有让作者注册任意 `activate()` / `deactivate()` 回调的机制。 +宿主管理会话、进程和停用;作者负责让服务器保持前台运行、遵守 LSP 生命周期, +并把所有子进程和状态设计成可以停止、重启和隔离。当前 Core 的强停路径面向直接子进程, +不能假定会自动终止服务器任意派生的整棵进程树。下面是发布前需要执行的验收, +不是声明后即可免测的保证。 + +| 操作或故障 | 要检查的结果 | +| --- | --- | +| 仅安装/启用,不打开该语言 | 不因安装就提前启动语言服务器 | +| 编辑、保存、关闭/重开文件 | 文档同步和诊断正常,没有重复会话 | +| 快速切换两个项目/工作区 | 旧结果不串到新项目,索引与锁不互相占用 | +| 服务器异常退出或初始化失败 | 宿主能显示可定位的失败;恢复操作不会留下重复进程 | +| 缺少 Node.js/Go/Java 或版本不足 | 给出可操作的运行时错误,不在启动时自行下载 | +| 修改 API 2 设置 | 新会话使用新值,旧进程退出 | +| 禁用插件、关闭工作区、退出 Lithe | 相应会话与进程被停止,启动器/辅助子进程不残留 | +| 卸载与重新安装 | 卸载先停用,重启后删除包和宿主管理的插件状态;自定义缓存有明确说明 | +| 篡改安装包内一个文件后重新加载 | 内容复查拒绝加载,不能靠服务器自行修改清单“修复” | + +已在本次启动中注册的声明式插件可即时启用/禁用;本次启动后新装的插件要等重启。 +安装、更新、回滚及卸载的文件/版本转换按界面提示在下次启动完成, +卸载请求会先停用已注册插件。验收需要覆盖重启前和重启后的状态。 + +验收完成后关闭测试启动的应用、服务器和子进程。不要把关闭编辑器标签等同于卸载服务器, +也不要假定崩溃后一定会自动重试或所有平台都有相同恢复策略。发现生命周期问题时, +在插件仓库记录日志、宿主版本、包版本、架构和复现步骤;宿主进程管理问题再提交到 Lithe。 + +## 8. 发布、更新与回退演练 + +按[发布社区插件](publishing.md)递增插件版本、发布正式 Release 并首次提交收录。 +模板的 Release 工作流会校验 `v<版本>` 与清单一致;原版示例是 +`node scripts/package.mjs --expect-tag v0.3.0`,派生后使用自己的版本。 + +每次更新服务器或运行时,都要更新固定输入和许可,重建对应平台包,走完交互及生命周期验收。 +发布后的 zip 不可替换,同一版本不能重用;修复要发布新版本。新增可运行程序会触发新的 +安装确认,具体规则见[版本追踪与更新](versioning-and-updates.md)。 + +发布前至少演练一次“旧版 → 新版 → 恢复旧版逻辑”: + +- 保留旧版本资产和索引记录,确保旧宿主仍有兼容版本可装。 +- 新旧服务器使用不兼容索引格式时,隔离状态或提供安全重建;不能让旧版读损坏状态。 +- macOS 的回滚功能使用安装记录中保留的上一版本,重新验证该包并在重启后切换, + 不提供任意历史版本选择,也不会恢复服务器索引或用户数据。应实际演练该路径。 +- 当前版本有问题时,发布更高版本的修复包,必要时在新包里恢复上一个稳定服务器; + 同时申请撤回问题版本,不能只依赖用户机器恰好保留上一版本。 +- 卸载重装也要验证。源码、用户构建产物与自定义缓存的处理必须说明,避免误删用户数据。 + +## 9. 按失败阶段排查 + +| 现象 | 优先检查 | +| --- | --- | +| 市场看不到或不可安装 | 源地址、当前平台资产、`pluginAPIVersion` 是否被宿主支持 | +| `plugin.archive.checksumMismatch` | 是否替换过 Release 资产;索引固定的是否为最终 zip 摘要 | +| `plugin.manifest.invalid` | 未知字段、schema/API 混淆、ID/引用、API 2 字段误放到 API 1 | +| `plugin.permission.undeclared` / `plugin.activation.invalid` | 提供者、权限与事件是否一一对应 | +| `plugin.launch.missing` | 相对路径、大小写、打包完整性与可执行权限 | +| `plugin.runtime.missing` | 运行时发现、最低版本、Dock 启动环境 | +| 有进程但无法补全 | stdio 是否被日志污染、LSP 握手、文档语言标识与上游配置 | +| 用过一次后拒绝加载 | 服务器是否向包内写了日志、索引、锁或生成文件 | +| 禁用后仍有进程 | 启动器是否使用 `exec`,是否后台化或产生未清理辅助进程 | +| 能补全但无法构建/测试 | 是否实际声明并验证了独立的运行/测试或构建集成 | + +字段全集、API 等级和稳定错误码以 Lithe 的 +[插件契约](https://github.com/Lithe-IDEA/Lithe-IDEA/blob/preview/shared/contracts/plugins/v1/README.md)及 +[清单 Schema](https://github.com/Lithe-IDEA/Lithe-IDEA/blob/preview/shared/contracts/plugins/v1/plugin-manifest.schema.json)为准。 +跟随 preview 阅读时要区分已发布能力与开发中的契约,发布前以目标宿主实际支持为准。 diff --git a/docs/publishing.md b/docs/publishing.md index c8aa2fc..00a90a4 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -2,6 +2,10 @@ 这份文档面向插件作者:从打包到被 Lithe 用户看到的完整流程。 +还没有可安装的插件包时,先跟着[LSP 插件完整生命周期教程](lsp-plugin-lifecycle.md) +完成清单、语言服务器构建和运行验收;需要接入命令、测试或构建后端时,阅读 +[构建、运行与测试接入指南](build-tool-integration.md)。 + ```text 你的插件仓库 Lithe-store Lithe 用户 ─────────── ─────────── ─────────