让 Lithe 认识一种语言:识别文件,用包内 TextMate 语法高亮,启动 LSP 语言服务器,输出链接,以及(如果插件提供执行提供者)“运行当前文件”和测试。这些都是数据声明,宿主负责进程生命周期、LSP 会话和停止进程,插件不需要写宿主代码。命令行要到启动时才能确定、或后端需要驱动自己的服务器时,可以让插件后端回答启动命令,并在自己声明的范围内向服务器发请求(见“后端决定启动方式”和“插件自有会话”)。
| 项目 | 内容 |
|---|---|
| 清单字段 | languages、languageServers、executionProviders、outputLinkMatchers |
| 能力 | languages、languageServers、executionProviders、outputLinkMatchers,版本都是 0。languageServers 的后端启动方式需要 backend;会话、输出链接的 resolve、内容提供者都需要 backend |
| 协议 | LSP。后端驱动服务器用的 languageServer/* 宿主方法和通知见下文,它们属于插件后端的 litheFunctions 协议(见插件后端) |
| API 编号 | API-25:语言、语法、LSP。API-12:按 URI scheme 提供的只读文档(documents/registerContentProvider)。API-20:语言服务器按工作区的状态目录及其回收。API-22:语言服务器的准备状态(languageServer/setStatus)和 Run、测试控制台输出里的链接。插件函数提供的编辑器语言功能见编辑器 |
| 权限 | languageServer.launch;executionProviders 声明 run、test 时加 process.run、process.test;languageServer.request(服务器列出 requestMethods 或 forwardNotifications 时才可声明,需要 backend);documents.contentProvider(需要 backend) |
| 激活事件 | onLanguage:<id>;onRun:<id>、onTest:<id>(执行提供者) |
| 桌面端 | 语言服务器启动、会话、输出链接、只读文档内容提供者均已接通;执行提供者未声明。详见下文“桌面端现状” |
languages[] 的 id、fileExtensions、fileNames、projectFileNames 和 dependencies 的规则见契约的 Languages 一节。语法和编辑器配置是纯数据:宿主从包里读取,编辑器直接应用,不启动任何进程。只有语法的插件不需要语言服务器、后端或激活事件。
{
"id": "http",
"displayName": "HTTP Request",
"fileExtensions": ["http", "rest"],
"grammar": { "kind": "textmate", "path": "grammars/http.tmLanguage.json" },
"configuration": {
"lineComment": "#",
"blockComment": ["/*", "*/"],
"brackets": [["{", "}"], ["[", "]"]],
"autoClosingPairs": [["{", "}"], ["\"", "\""]]
}
}grammar.kind只有textmate一种。grammar.path指向包内的.tmLanguage.json(plist 和 YAML 语法要先转成 JSON)。宿主只读地从已安装的包里读取它,交给plugins.declarative.decodeGrammar校验,然后用返回的scopeName注册校验过的source。Core 拒绝的语法是plugin.grammar.invalid,并带出错字段;该语言按纯文本打开,插件其余部分照常工作。- 上限:语法最大 2 MiB,规则(含每个 pattern、repository 项和捕获)最多 16384 条,嵌套 32 层,正则 32 KiB,捕获组 4096 个,
name、contentName1024 字节,include目标和 repository 键 256 字节。 - 正则使用 TextMate 语法所用的 Oniguruma 语法。编辑器的引擎编译不了的模式永不匹配,语法的其余部分照常生效。
include可以是$self、$base、#name(repository 项),或scope、scope#name(另一个语法)。injections和injectionSelector不支持,会被忽略。- 插件语法在启用期间替换编辑器里同一
scopeName的语法。两个插件语言不能注册同一个 scope:先注册的保留,另一个报告后按纯文本打开。 configuration包含lineComment、blockComment(开始和结束)、brackets、autoClosingPairs。每个定界符 1–16 字节且不含控制字符,每个列表最多 32 对。- 插件语法从不在编辑器界面线程上运行。宿主限制每次正则搜索的回溯和每行的耗时,无法在预算内分词的行按纯文本绘制。
字段的完整规则和拒绝条件见契约。
| 字段 | 含义 |
|---|---|
languageID |
已声明的语言。 |
languageIdentifier |
文档的 LSP languageId,默认等于 languageID。 |
launch |
{"kind": "packaged", "path"} 运行包内可执行文件。{"kind": "systemCommand", "commands"} 列出裸命令名,由宿主的工具发现解析(PATH、项目工具链、用户覆盖)。{"kind": "runtime", …} 用包内或宿主发现的运行时启动包内入口文件。{"kind": "backend", …} 由插件后端在每次启动时回答命令行。启动字段的共同规则见契约的 Process start。 |
arguments |
传给服务器的参数,是启动模板。 |
validationArguments |
候选在使用前必须以状态 0 退出的探针;为空时宿主信任候选,由 LSP 握手决定。 |
runtimeDependencies |
宿主要发现并传给服务器的工具,每一项是 requiresTools 列出的工具定义 ID。宿主把每个路径导出为 LITHE_<TOOL>_PATH(用户设置的值优先),并把其目录加到服务器的 PATH 前面。项目本地副本不会被选中。 |
initializationOptions |
作为 LSP initializationOptions 发送的 JSON 对象;其中每个字符串值都是启动模板。 |
hostIntegration |
保留字段,见下一节。 |
requestMethods、commands、serializedCommands、forwardNotifications、state、remote |
后端可以向运行中的服务器发什么,以及宿主怎样保存它的状态,见“插件自有会话”。 |
宿主拥有语言服务器进程的生命周期、LSP 会话,以及在提供者被停用时停止进程。
hostIntegration 是保留字段。Core 不实现任何宿主集成,也不按语言 ID、服务器 ID 或工具名走特殊逻辑;语言在启动之外需要的一切,都由插件自己的后端、提供者和声明给出。声明任何 hostIntegration ID 都返回 plugin.integration.unsupported,字段为 languageServers[<n>].hostIntegration.id。
声明了某种语言的插件拥有这种语言依赖进程的功能:宿主不再为它运行内置工具,插件缺失或被停用时也不会回退到内置实现。插件的 executionProviders 没有提供的功能,对这种语言不可用。
arguments、validationArguments、runtimeArguments 和 initializationOptions 的字符串值可以使用下列变量。包里因此从不出现机器路径,宿主决定状态放在哪里。
| 变量 | 值 |
|---|---|
${pluginDir} |
已安装的只读包的根。每个值最多用一次,后面必须是包内以 / 开头的路径,该路径只能因 ${arch} 而变化。 |
${stateDir} |
宿主为本插件服务器保留的可写目录,卸载插件时删除。不能用于 validationArguments。 |
${workspaceStateDir} |
服务器在当前工作区的可写状态目录。不能用于 validationArguments,也不能用于插件后端。 |
${logDir} |
本次会话的 plugins/<id>/files/ 目录,由宿主作为 logDirectory 提供。只用于语言服务器和插件后端,不用于 validationArguments。 |
${arch} |
宿主 CPU 架构:arm64 或 x86_64。 |
${setting:<id>} |
已声明的插件设置的当前值。不能用于 validationArguments,也不能出现在包路径里。 |
语言服务器可以声明 workspaceConfiguration:一个对象,其中的字符串叶子可以是 ${setting:<id>}。Core 用合并后的设置展开它,按节路径回答服务器的 workspace/configuration 请求,并在 initialized 之后发送 workspace/didChangeConfiguration。immediate 设置变化时,宿主通过 lsp.updateWorkspaceConfiguration 推送新值,运行中的服务器无需重启即可收到。
服务器的命令行取决于启动时才知道的东西(解析出的版本、引导出来的工具、从工程推导的选项)时,不写死 launch,让插件后端每次回答:
"launch": {"kind": "backend", "function": "launch.resolve", "timeoutMilliseconds": 30000}| 项目 | 规则 |
|---|---|
| 适用 | 只有语言服务器能用。插件需要声明 backend。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 |
后端只能通过服务器声明的内容驱动它自己的服务器。服务器可以声明:
| 字段 | 含义 |
|---|---|
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里。 - 服务器列出
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;通知从不被挡住。 - 同一
serializedCommands命令的后续请求在第一个运行或等待时并入它,并得到第一个请求的结果。取消第一个请求会结束所有并入的请求。 - 服务器停止、崩溃、重启或插件被停用时,所有未完成的请求以
plugin.host.languageServerUnavailable结束;后端在didChangeState报告新的generation的running之后再请求。
状态目录与回收(API-20)。 宿主在插件的存储目录下(不在插件包或应用包里)为每个服务器、每个工作区保留一个状态目录,并把它作为 workspaceStateDirectory 交给服务器;停用和更新插件都不会删除。目录内的子目录名由插件决定,每个工作区目录本身的名字由宿主决定,宿主从不查看里面的内容。宿主每次启动服务器时刷新该目录的修改时间,并在启动后于后台回收:先用 Core 的 plugins.languageServers.stateRetention 决定要删除的目录,即超过 maxAgeDays 的,以及其余目录里扣除打开的工作区占用的名额后只保留最新 maxEntries 个以外的。打开的工作区从不被回收,没有声明 state.retention 的插件保留全部。
准备状态。 languageServer/setStatus 报告服务器的准备程度。它与 lithe/function/progress 不同:后者属于正在运行的调用,而导入工程、建索引的准备工作会超过任何一次调用。
后端用这些方法和通知与自己的语言服务器交谈;方法名省略前缀 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 的整数),返回 {};服务器停止时宿主移除它 |
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 已有提供者 |
内容提供者的 scheme 规则:1–64 个小写 ASCII 字母、数字、+、-、.,以字母开头;about、file、http、https、lithe 等由宿主或平台占用的 scheme 被拒绝。提供者只保存绑定,注册时不调用后端;打开文档时才调用函数,结果只读,每次重新打开都会重新请求。
执行提供者让用户运行当前文件、列出并运行测试。run 和 test 各自指定一个 executable:宿主工具路径上的裸 command,或宿主拥有的项目 toolchain ID(如 project-go)。工作目录永远是工作区根。
test 还声明 frameworkID、workspaceLabel、projectMarkers、testFilePatterns 和 workspaceArguments、fileArguments、testCaseArguments。参数模板是 argv 元素,从不是 shell 文本。元素可以是字符串,或 {"value": …, "when": "fileSelected" | "noFileSelected"};条件元素只允许出现在 testCaseArguments 中。
| 变量 | 可用位置 | 值 |
|---|---|---|
${file} |
运行、文件、测试用例 | 工作区相对文件路径;以 - 开头的名字变为 ./<名字> |
${fileDirectory} |
运行、文件、测试用例 | 文件所在目录的 ./<目录>,工作区根为 . |
${testName} |
测试用例 | 去掉首尾空白的测试标识 |
${testName:regex} |
测试用例 | 转义了正则元字符的测试标识 |
${userArguments} |
运行 | 用户的程序参数;必须是整个元素,可以展开为零个元素 |
发现测试时返回 {id: "<语言>:workspace", kind: "workspace"},然后是按路径排序的 {id: "<语言>:file:<路径>", kind: "file", relativeFilePath};没有项目标记时返回空列表。
outputLinkMatchers[] 让插件把 Run 和测试控制台里的文字变成可点击的链接,例如回溯里的一帧或 panic 的位置。宿主不认识任何工具的输出格式:插件声明匹配规则,Core 找出匹配。只声明匹配器本身就是完整的插件;不带 resolve 的匹配器不需要后端。
"outputLinkMatchers": [
{ "id": "traceback", "pattern": "File \"(?P<path>[^\"]+)\", line (?P<line>\\d+)", "scope": "run" }
]| 字段 | 含义 |
|---|---|
id |
插件内的 ID:小写字母开头,后面最多 63 个小写字母、数字、.、_ 或 -,在插件内唯一。每个插件最多 16 个匹配器 |
pattern |
对控制台的一行做匹配的正则,1–512 字节,使用 Rust regex 语法,所以没有反向引用和环视,编译后的大小也有上限。最多命名 8 个分组,每个是 camelCase 名称,且不能匹配空文本 |
scope |
run 或 test:匹配器作用的控制台。缺省表示两者 |
resolve.function |
可选,插件 backend 进程里的函数 ID;需要 backend |
- 不带
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会被转换。- 宿主只对显示出来的行做匹配,不对没有启用该插件的输出做匹配,并忽略文件不存在的链接。
| 功能 | 桌面端 |
|---|---|
| 语言、语法高亮与编辑器配置 | 已支持:语法由 Core 校验后注册到编辑器,分隔符进入注释和括号规则 |
| 语言服务器启动 | 已支持 packaged、包内运行时、systemCommand(只在 PATH 上查找,依赖项目工具链或用户覆盖的启动计划会被拒绝)和后端应答的命令行。后端应答 {kind: "tool"} 在启动时被拒绝,因为工具还没有经过工具链注册表解析 |
| 语言服务器呈现 | 只呈现诊断。补全、悬停、跳转、重命名、格式化没有编辑器界面,插件即使提供了也不会显示 |
插件自有会话与 languageServer/* 方法 |
已接通,策略由 Core 执行 |
执行提供者(run、test、executionProviders) |
未声明。需要它的插件不会被提供安装 |
| 输出链接 | 已支持。Build、Run、Test 控制台的文字变成链接,带 resolve 的匹配器在点击时调用后端 |
| 内容提供者 | 已支持 documents/registerContentProvider |
| 章节 | 内容 |
|---|---|
| Languages | 语言 ID、扩展名、文件名、项目标记、依赖路径 |
| Grammars and editor configuration | TextMate 语法的上限和拒绝规则、注释与括号配置、在后台运行 |
| Language servers | 启动方式(含 backend)、参数、探测参数、运行时依赖 |
| Language server host integrations | 宿主集成(保留字段,Core 不实现) |
| Workspace configuration | workspaceConfiguration 的展开与推送 |
| Plugin-owned sessions | 会话字段、会话策略、gate、状态目录与回收、准备状态 |
| Runtime launch | 用 java 或 node 等工具启动包内入口文件 |
| Tool references | 按工具定义 ID 引用运行时、启动计划里的 tool 启动方式 |
| Launch templates | ${pluginDir}、${stateDir}、${workspaceStateDir} 等模板变量 |
| Backend-resolved launch | 后端回答命令行:resolveParams、应答字段、拒绝码 |
| Language server host methods | languageServer/request、notify、setStatus 和两个通知 |
| Content providers | documents/registerContentProvider:按 URI scheme 提供只读文档 |
| Execution providers | run、test 的可执行文件、参数模板、测试发现 |
| Output link matchers | Run 与测试控制台输出里的链接、outputLinks.match、outputLinks.decodeResolution |
| 文件 | 内容 |
|---|---|
languages/language-packaged-server.plugin.json |
包内语言服务器加运行/测试提供者(PHP) |
languages/language-system-command.plugin.json |
用系统命令启动的语言服务器加运行/测试提供者(Go) |
languages/node-runtime.plugin.json |
用 node 运行时启动,带插件设置 |
languages/language-grammar.plugin.json |
只有语法、不启动进程的插件 |
languages/language-server-backend-launch.plugin.json |
命令行由后端回答、声明会话字段和 languageServer.request 的语言服务器 |
languages/grammars/http.tmLanguage.json |
完整的 TextMate 语法示例 |
languages/command-plans.json |
运行/测试模板和语言服务器启动计划的展开结果的标准答案 |
languages/language-server-launch-resolution.json |
后端解析启动的第一步(languageServerLaunchPlan)和解码应答(plugins.languageServers.decodeLaunch)得到的启动计划或拒绝码 |
run/output-link-matchers.json |
outputLinks.match 的请求与返回的链接,以及 outputLinks.decodeResolution 的结果 |
run/tool-reference-launch.plugin.json |
语言服务器的 runtime 启动按工具 ID interpreter 引用运行时,并带一个 outputLinkMatchers 条目 |
语言相关的拒绝用例在 manifest/invalid-manifests.json。
Languages let Lithe recognize a language: identify its files, highlight them with a TextMate grammar shipped in the package, start an LSP language server, make output links clickable, and, when the plugin provides an execution provider, offer "run the current file" and tests. All of this is data. The host owns process lifecycle, the LSP session, and stopping processes, so a plugin writes no host code. When the command line is known only at start, or the backend must drive its own server, the plugin backend can answer the launch command and send requests to the server within the scope the server declares (see "Backend-resolved launch" and "Plugin-owned sessions").
| Item | Value |
|---|---|
| Manifest fields | languages, languageServers, executionProviders, outputLinkMatchers |
| Capabilities | languages, languageServers, executionProviders, outputLinkMatchers, each at version 0. A languageServers backend launch needs backend; sessions, a matcher's resolve, and content providers need backend too |
| Protocols | LSP. The languageServer/* host methods and notifications below belong to the plugin backend's litheFunctions protocol (see Plugin backend) |
| API IDs | API-25: languages, grammars, LSP. API-12: read-only documents by URI scheme (documents/registerContentProvider). API-20: per-workspace state directories of language servers, and their pruning. API-22: language server preparation status (languageServer/setStatus) and links in Run and Test output. Editor language features supplied by plugin functions are in Editor |
| Permissions | languageServer.launch; process.run, process.test when an execution provider declares run or test; languageServer.request (only when a server lists requestMethods or forwardNotifications; needs backend); documents.contentProvider (needs backend) |
| Activation events | onLanguage:<id>; onRun:<id>, onTest:<id> (execution providers) |
| Desktop | Language server launch, sessions, output links, and read-only content providers are wired; execution providers are not declared. See "Desktop status" below |
The rules for id, fileExtensions, fileNames, projectFileNames, and dependencies are in the contract's Languages section. A grammar and editor configuration are pure data: the host reads them from the package and the editor applies them without starting any process. A plugin with only grammars needs no language server, backend, or activation event.
{
"id": "http",
"displayName": "HTTP Request",
"fileExtensions": ["http", "rest"],
"grammar": { "kind": "textmate", "path": "grammars/http.tmLanguage.json" },
"configuration": {
"lineComment": "#",
"blockComment": ["/*", "*/"],
"brackets": [["{", "}"], ["[", "]"]],
"autoClosingPairs": [["{", "}"], ["\"", "\""]]
}
}grammar.kindis onlytextmate.grammar.pathnames a.tmLanguage.jsonfile in the package (plist and YAML grammars are converted to JSON first). The host reads it read-only from the installed package, passes it toplugins.declarative.decodeGrammar, and registers the returnedsourceunder the returnedscopeName. A grammar Core refuses isplugin.grammar.invalidwith the failing field; the language then opens as plain text and the rest of the plugin keeps working.- Limits: a grammar is at most 2 MiB; at most 16384 rules, counting every pattern, repository entry, and capture; nesting 32 deep; regexes of 32 KiB; 4096 capture groups;
nameandcontentNameof 1024 bytes;includetargets and repository keys of 256 bytes. - Regexes use the Oniguruma syntax TextMate grammars are written in. A pattern the editor's engine cannot compile never matches, and the rest of the grammar still applies.
includeis$self,$base,#namefor a repository entry, orscopeorscope#namefor another grammar.injectionsandinjectionSelectorare not supported and are ignored.- A plugin grammar replaces the editor's own grammar of the same
scopeNamewhile the plugin is enabled. Two plugin languages cannot register the same scope: the first keeps it, and the other opens as plain text. configurationsetslineComment,blockComment(open and close),brackets, andautoClosingPairs. Each delimiter is 1 to 16 bytes without control characters; each list has at most 32 pairs.- Plugin grammars never run on the editor UI thread. The host bounds the backtracking of each regex search and the time one line may take; a line that cannot be tokenized within that budget is drawn as plain text.
| Field | Meaning |
|---|---|
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). {"kind": "runtime", …} starts a package entry file with a packaged or discovered runtime. {"kind": "backend", …} has the plugin backend answer the command line at each start. The shared launch rules are in the contract's Process start. |
arguments |
Arguments passed to the server, written as launch templates. |
validationArguments |
A 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 |
Tools the host discovers and passes to the server, each a tool-definition ID that requiresTools lists. The host exports each path as LITHE_<TOOL>_PATH (a user-set value takes precedence) and prepends its directory to the server's PATH. A project-local copy is never chosen. |
initializationOptions |
A JSON object sent as the LSP initializationOptions. Every string value inside it is a launch template. |
hostIntegration |
Reserved; see the next section. |
requestMethods, commands, serializedCommands, forwardNotifications, state, remote |
What the backend may ask of the running server and how the host keeps its state; see "Plugin-owned sessions". |
The host owns the language server process lifecycle, the LSP session, and stopping the process when the provider is disabled.
hostIntegration is reserved. Core implements no host integration and runs no special logic for a language ID, a server ID, or a tool name; everything a language needs beyond its launch comes from the plugin's own backend, providers, and declarations. Any hostIntegration ID returns plugin.integration.unsupported, at languageServers[<n>].hostIntegration.id.
A plugin that declares a language owns its process-backed features: the host stops running its built-in tools for that language, so a missing or disabled plugin never falls back to them. A feature that none of the plugin's executionProviders supplies is unavailable for the language.
arguments, validationArguments, runtimeArguments, and the string values of initializationOptions may use these variables. A package therefore never contains a machine path, and the host decides where state lives.
| Variable | Value |
|---|---|
${pluginDir} |
Root of the installed, read-only package. Used at most once per value and followed by a /-rooted path inside the package that may vary only by ${arch}. |
${stateDir} |
Writable directory the host keeps for this plugin's server; removed when the plugin is uninstalled. Not available in validationArguments. |
${workspaceStateDir} |
Writable directory for the server's state in the current workspace. Not available in validationArguments or in a plugin backend. |
${logDir} |
The current session's plugins/<id>/files/ directory, supplied by the host as logDirectory. Available to language servers and plugin backends, never in validationArguments. |
${arch} |
Host CPU architecture: arm64 or x86_64. |
${setting:<id>} |
Current value of a declared plugin setting. Not available in validationArguments, and never inside a package path. |
A language server may declare workspaceConfiguration, an object whose string leaves may be ${setting:<id>} templates. Core expands it with the merged settings, answers the server's workspace/configuration requests by section path, and sends workspace/didChangeConfiguration after initialized. When an immediate setting changes, the host sends the new value with lsp.updateWorkspaceConfiguration, and the running server receives it without a restart.
When a server's command line depends on what is known only at start (a version it resolves, a tool it bootstraps, options derived from the project), do not write launch; let the plugin backend answer it each time:
"launch": {"kind": "backend", "function": "launch.resolve", "timeoutMilliseconds": 30000}| Item | Rule |
|---|---|
| Scope | Only a language server can use it. The plugin declares backend. function is a plugin function ID; timeoutMilliseconds is 1000 to 120000 and defaults to 30000 |
| Other fields | 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 |
| Launch plan | plugins.declarative.languageServerLaunchPlan returns the 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, or 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 changed workspace generation, ends the 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, which the host resolves), arguments, environment, initializationOptions, workspaceConfiguration, gates (at most 8), and 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); its sessionPolicy also carries the answer's gates |
| Rejections | plugin.launch.invalidResult, plugin.launch.outsidePackage, plugin.launch.undeclaredTool, plugin.launch.unwritablePath, plugin.launch.invalidRequest, plugin.launch.unavailable, plugin.launch.notBackend |
A plugin backend drives its own server only through what the server declares:
| 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}}; maxEntries is 1 to 64, maxAgeDays 1 to 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. The names the host sends itself (
initialize,initialized,shutdown,exit, the document sync methods such astextDocument/didOpen,workspace/didChangeWorkspaceFolders, and every$/method) cannot appear inrequestMethodsorforwardNotifications. - A server that lists
requestMethodsorforwardNotificationsrequires thelanguageServer.requestpermission; declaring the permission without such a server isplugin.manifest.invalid. - The launch plan's
sessionPolicyis these fields as sorted lists plusserverID(thelanguageID) andgates. The host passes it unchanged tolsp.startServer.sessionPolicy; Core's LSP engine enforces it, so the host cannot widen it and the backend cannot step outside it. - A request must be in
requestMethods, and aworkspace/executeCommandmust carry aparams.commandincommands; anything else isplugin.host.methodNotDeclared. A notification may beworkspace/didChangeConfiguration, or arequestMethodsentry other thanworkspace/executeCommand. The host's own methods stay 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 the server$/cancelRequest. A server's JSON-RPC error does not forward itsdata. - A gate is
{id, until: "notification", method, match}, declared by the launch answer. It opens when the server sends a notification with thatmethodwhose params contain every member ofmatch. Until every gate has opened,requestcalls wait in order, at most 128; the 129th isplugin.host.busy. Notifications are never held. - The first request for a
serializedCommandscommand runs; a later request for the same command, while the first is running or waiting, joins it and gets the first one's outcome. Cancelling the first request ends the joined requests with it. - When the server stops, crashes, restarts, or the plugin is disabled, every pending request ends with
plugin.host.languageServerUnavailable; the backend asks again afterdidChangeStatereportsrunningwith a newgeneration.
State and retention (API-20). 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 it to the server as workspaceStateDirectory. Disabling and updating the plugin do not delete it. The plugin chooses sub-directory names inside it; the host chooses the per-workspace directory names 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, using plugins.languageServers.stateRetention: it deletes directories older than maxAgeDays, and of the rest, all but the newest maxEntries that open workspaces leave free. An open workspace is never pruned. A plugin that declares no state.retention keeps everything.
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.
The backend uses these methods and notifications to talk to its own language servers; the names omit the lithe/host/ prefix. Every languageServer/* method carries context: {contextID, generation} and serverID (the languageID of one of the plugin's servers). A server of another plugin returns plugin.host.foreignContribution.
| API | Method | Purpose | Permission |
|---|---|---|---|
| API-25 | languageServer/request |
Send a JSON-RPC request to the plugin's own language server. Parameters: method, params?, timeoutMilliseconds? (1 to 300000, default 60000). Returns {value} (the server's result, possibly null). Checked by the session policy |
languageServer.request |
| API-25 | languageServer/notify |
Send a notification to the plugin's own language server. Parameters: method, params?. Returns {delivered}; only a workspace/didChangeConfiguration sent before the server initializes is false, and its value is then sent with initialized |
languageServer.request |
| API-22, API-25 | languageServer/setStatus |
Show the plugin's preparation state in the host's status UI: state is preparing, ready, degraded, or failed, with optional message (shown as given, cut to 1024 bytes) and percentage (integer 0 to 100). Returns {}. The host removes it when the server stops |
languageServer.request |
| API-12 | documents/registerContentProvider |
Bind a URI scheme to a backend function: when a document of that scheme opens, 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}; revoke with subscriptions/dispose. No serverID |
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. After the new process starts, the host never delivers the old one's notifications.
| Error code | JSON-RPC | Meaning |
|---|---|---|
plugin.host.methodNotDeclared |
-32024 |
The method, or the 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 |
Content provider schemes are 1 to 64 lowercase ASCII letters, digits, +, -, or ., starting with a letter. Schemes the host or platform uses, such as about, file, http, https, and lithe, are refused. Registration only records the binding and never calls the backend; the function is called when a document opens, the content is read-only, and it is requested again each time the document is reopened.
An execution provider lets the user run the current file and list and run tests. run and test each name an executable: exactly one of a bare command resolved on the host's tool path, or a host-owned project toolchain ID such as project-go. The working directory is always the workspace root.
test also declares frameworkID, workspaceLabel, projectMarkers, testFilePatterns, and the argument templates workspaceArguments, fileArguments, and testCaseArguments. Argument templates are argv elements, never shell text. Each element is a string or {"value": …, "when": "fileSelected" | "noFileSelected"}; conditional elements are only allowed in testCaseArguments.
| Variable | Available in | Value |
|---|---|---|
${file} |
run, file, test case | Workspace-relative file. A name starting with - becomes ./<name> |
${fileDirectory} |
run, file, test case | ./<directory> of the file, or . at the workspace root |
${testName} |
test case | Test identifier with surrounding whitespace removed |
${testName:regex} |
test case | The identifier with regular-expression metacharacters escaped |
${userArguments} |
run | The user's program arguments; must be a whole element and may expand to zero elements |
Discovery returns {id: "<language>:workspace", kind: "workspace"} followed by {id: "<language>:file:<path>", kind: "file", relativeFilePath} for matching files sorted by path, or an empty list when no project marker is present.
outputLinkMatchers[] makes 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. A plugin that declares only matchers is complete; matchers without resolve need no backend.
"outputLinkMatchers": [
{ "id": "traceback", "pattern": "File \"(?P<path>[^\"]+)\", line (?P<line>\\d+)", "scope": "run" }
]| Field | Meaning |
|---|---|
id |
Plugin-local ID: a lowercase letter, then 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 Rust regex syntax (no backreferences, no look-around), and its compiled size is limited. At most 8 named groups, each camelCase, and it 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 process; needs backend |
- Without
resolve, the pattern must capture apathgroup and may capturelineandcolumn(1-based); no other group name is accepted. A relativepathbecomes one candidate per workspace root; an absolute path counts only inside a workspace root, so 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}(captured text by name) and the window'scontext; the backend answers{uri, range?}ornull. - Core owns the decision, so every host links the same text.
outputLinks.matchtakes{scope, platform, caseSensitive?, workspaceRoots, sources: [{pluginID, matchers}], lines}and returns{links}. Each link is{line, start, length, pluginID, matcherID}plus eitherlocation({paths, line?, column?}; the host opens the first existing path) orresolve({function, groups}).startandlengthcount 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 yields at most 8 links. outputLinks.decodeResolutiontakes{reply}(what the function answered) and returns the location to open,{path, line?, column?}with 1-based positions, ornull. A reply opens only afile:URI and accepts only the membersuriandrange; a 0-basedrange.startis converted.- The host matches only lines it displays, never output of a plugin that is not enabled, and ignores a link whose file does not exist.
| Feature | Lithe desktop |
|---|---|
| Languages, grammar highlighting, editor configuration | Supported: Core validates the grammar and the host registers it with the editor; delimiters reach the comment and bracket rules |
| Language server launch | Supported for packaged, a packaged runtime, systemCommand (PATH only; a plan that depends on a project toolchain or a user override is refused), and a backend-answered command line. A backend answer of {kind: "tool"} is refused at start, because the tool is not yet resolved through the toolchain registry |
| Language server presentation | Only diagnostics are shown. Completion, hover, navigation, rename, and formatting have no editor surface; a plugin that provides them sees no effect |
Plugin-owned sessions and languageServer/* methods |
Wired; Core enforces the policy |
Execution providers (run, test, executionProviders) |
Not declared. A plugin that needs them is not offered for installation |
| Output links | Supported. Console text in Build, Run, and Test becomes links; a matcher with resolve calls the backend on click |
| Content providers | documents/registerContentProvider supported |
| Section | Content |
|---|---|
| Languages | Language IDs, extensions, file names, project markers, dependency paths |
| Grammars and editor configuration | TextMate grammar limits and refusal rules, comment and bracket configuration, background execution |
| Language servers | Launch kinds (including backend), arguments, probe arguments, runtime dependencies |
| Language server host integrations | Host integrations (reserved; Core implements none) |
| Workspace configuration | Expanding and pushing workspaceConfiguration |
| Plugin-owned sessions | Session fields, session policy, gates, state directories and pruning, preparation status |
| Runtime launch | Starting a package entry file with a tool such as java or node |
| Tool references | Referencing runtimes by tool-definition ID; the tool launch in a launch plan |
| Launch templates | ${pluginDir}, ${stateDir}, ${workspaceStateDir} and the other template variables |
| Backend-resolved launch | The backend answers the command line: resolveParams, answer fields, rejection codes |
| Language server host methods | languageServer/request, notify, setStatus, and the two notifications |
| Content providers | documents/registerContentProvider: read-only documents by URI scheme |
| Execution providers | run and test executables, argument templates, test discovery |
| Output link matchers | Links in Run and Test console output, outputLinks.match, outputLinks.decodeResolution |
| File | Content |
|---|---|
languages/language-packaged-server.plugin.json |
A packaged language server plus run and test providers (PHP) |
languages/language-system-command.plugin.json |
A language server started by a system command, plus run and test providers (Go) |
languages/node-runtime.plugin.json |
Started with a node runtime, with plugin settings |
languages/language-grammar.plugin.json |
Grammars only; starts no process |
languages/language-server-backend-launch.plugin.json |
A language server whose command line the backend answers, declaring session fields and languageServer.request |
languages/grammars/http.tmLanguage.json |
A complete TextMate grammar example |
languages/command-plans.json |
Golden expansions of run and test templates and language server launch plans |
languages/language-server-launch-resolution.json |
Golden results of the first backend-resolution step (languageServerLaunchPlan) and of decoding the answer (plugins.languageServers.decodeLaunch): a launch plan or a rejection code |
run/output-link-matchers.json |
Requests and returned links of outputLinks.match, and the outputLinks.decodeResolution result for a matcher's resolve reply |
run/tool-reference-launch.plugin.json |
A language server whose runtime launch references the runtime by tool ID interpreter, with an outputLinkMatchers entry |
Language-related rejection cases are in manifest/invalid-manifests.json.