Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Rust Core 负责校验声明、识别工程、展开每一条命令行,并把
插件可以自由描述(不需要改 Core):

- **任何构建工具**:ID、标记文件、任务、选项、运行入口、测试过滤器、诊断正则、依赖命令都是数据。
- **可执行文件**:按偏好顺序列出候选,有四种:
- **可执行文件**:按偏好顺序列出候选,有四种基础来源:
- `projectFile`:工程里的 Wrapper(例如 `gradlew`),可以要求某些文件存在。它一律被视为执行仓库代码。
- `environment`:某个环境变量下的相对路径,例如 `MAVEN_HOME` 下的 `bin/mvn`。变量必须列在 `environmentInputs` 里,宿主只传这些变量。
- `command`:在 `PATH` 上查找的裸命令名。
Expand Down Expand Up @@ -365,3 +365,31 @@ macOS 接入规则(2026-10-18):
- `shared/contracts/plugins/v1/`
- `shared/contracts/rust-core-api.md`
- `scripts/verify-shared-contracts.sh`


### 统一的候选解析扩展

在原有自动发现来源之上,可选 `executableResolution: 1` 增加 setting 来源与
确定性排序;候选仍声明在同一个 executables 数组里,不增加新的 Build 覆盖路径。
数组顺序是默认优先级,不需要路径覆盖的插件不用声明设置。
共享类型与拓扑排序位于 `plugins::executables`,macOS 由
`ExecutableCandidateResolver` 注入文件系统、环境、Runtime 和工具发现能力,
区分跳过、解析成功和终止失败。Provider 的可选 tool 提示继续复用宿主解析结果;
无显式选择的自动发现失败仍可让 Provider 自行查找,显式失败则停止本次模型或依赖加载。
这个限制也覆盖前置 Runtime 先失败的情况:Provider 回退前检查整个调用计划里的
工具和 Runtime 是否有非空 setting,不能仅依赖失败候选的 terminal 标志。
例如用户选了工具 A,先解析的 Runtime 不可用时,应报告 Runtime 错误;不能丢掉
A,发出不带 tool 的请求。空 setting 仍允许自动发现失败后交给 Provider。
只在解析失败时检查这项约束,前面候选成功后不会被后面未使用的 setting 阻止。
Runtime 每个候选先构造自己的环境,把所选目录加入 PATH,再以同一环境探测和
执行;失败候选的 PATH 不传给下一个候选。否则同目录 helper 会在探测时缺失,
合法的 Runtime 就会被误判为版本不足。
设置值非法时 Core 直接拒绝计划,普通设置的容错显示规则不变。
自定义 Runtime 候选复用同一机制;其余进程模块的分步迁移边界见
[插件设置面](2026-10-21-plugin-settings-surfaces.md)。

`BuildToolProviderResolutionTests` 使用真实 Core 计划、MacBuildToolResolver 和
BuildSystemService 验证上述组合:前置 Runtime 缺失或版本不足时,工具或后置
Runtime 的显式选择均阻止模型和依赖 Provider 请求;全部设置为空时仍发送无 tool 的请求。
`MacBuildToolResolverTests.customRuntimeProbeAndExecutionUseTheSameSiblingHelper`
用临时可执行脚本验证带空格目录里的 Runtime 在探测和执行时都能找到同目录 helper。
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,14 @@ macOS 宿主接受级别 1 到 3(`supportedDeclarativeAPIVersions = [1, 2, 3]`
- `rust/lithe-core/src/plugins/marketplace.rs`
- `shared/contracts/plugins/v1/`
- `shared/contracts/rust-core-api.md`


### 共享可执行文件候选能力

`executableResolution: 1` 是候选解析机制的可选能力,不是所有构建插件都要列的新字段。
只有使用 setting 来源或 id/priority/insertBefore/insertAfter 时才声明;自动发现数组
保持原序和原来的 buildSystems 1。macOS 先通过 Build 与自定义 Runtime 适配接入,
Windows 暂不声明;Language Server、Execution Provider 的新候选语法需各自后续接入后
才可开放。底层源类型、排序与终止失败规则共用,启动安全约束仍属于各适配器。
已发布 Cargo 0.1.1 的 buildSystems 2 输入通过兼容适配转换为同一个候选列表,
不维持独立 configuredExecutable 输出;细节见[插件设置面](2026-10-21-plugin-settings-surfaces.md)。
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,73 @@ macOS 设置窗口现在可以由插件“注册”页面:插件在 `plugin.js

为了保证“立即生效”真的不需要新进程,Core 在校验时拒绝把 `immediate` 设置用在命令行参数或 `initializationOptions` 里。

### 5. 宿主能力版本

macOS 的 `hostCapabilities` 声明 `settings: [1, 2]` 和 `languageServers: [1, 2]`。Windows 还没有这些界面和推送,所以仍只声明 1,需要 v2 的插件在 Windows 上会被市场隐藏,而不是装上后行为不完整。
### 5. 构建工具的可执行文件路径

路径选择使用同一个 `invocation.executables` 数组。插件需要用户覆盖时,声明
`kind: "setting"`、`settingID`,引用全局 file 设置(reload 或 nextRun);不需要
覆盖的插件保持原来的自动发现列表,不额外声明设置。该通用机制按
`executableResolution: 1` 协商,不要求所有构建插件升级。

Core 先校验完整候选集、排定顺序,再按平台过滤。数组从前到后是首选写法;
可选 priority 以较大值优先,同分按原序;insertBefore/insertAfter 使用同集合的稳定
id 建立相对顺序约束。插入约束优先,不能在同一候选混用 priority 或两个插入字段;
重复 id、未知锚点、自环与循环都拒绝。契约正文定义完整的确定性算法,供后续使用方复用。

macOS 的 `ExecutableCandidateResolver`(候选文件解析器)由宿主注入文件系统、
环境变量、Runtime 和工具发现服务,返回跳过、已解析或终止失败。设置值来自 Core
检查后的同一操作快照;空值跳过,非空无效值直接拒绝,不能经过普通设置解析的
“无效则回默认值”。普通设置的显示与配置容错不变。平台检查权限与版本,显式路径
失败时 Provider 不得独立发现另一安装。带空格路径保持完整参数,父目录进入 PATH。

共享源类型和排序在 `plugins::executables`;本次 Build 与自定义 Runtime Dependencies
使用共享解析器,已有 Build Provider 继续通过 ServerProcess 适配和 tool 提示复用。
Language Server、Execution Provider 等后续可接入相同机制,各自保留包内文件边界、
自动启动限制和工作区信任;当前不在这些清单位置开放新候选语法。
参考 xiaoyumuxi 的 #1318、#1356、#1382 以及 Maven 插件 #3,宿主仍只负责通用机制,
Provider 继续接受宿主选中的工具,不重新实现一套覆盖路径。

已发布预览 Cargo 0.1.1 使用旧 `executableSetting`(buildSystems 2)。保留只读的兼容
输入适配:转换为数组首位 setting 候选,后续只走共享解析器,不再返回
configuredExecutable。新插件必须用第五种候选和新能力;既有发行包不覆盖,迁移以新版本发布。

路径偏好仍写既有用户存储,候选计划只在操作内存活,没有额外缓存或安装目录写入,
不影响签名和 Sparkle delta。Core、Swift 与独立 Cargo CI 覆盖统一的选择和失败语义。

### 6. 宿主能力版本

macOS 的 `hostCapabilities` 声明 `settings: [1, 2]`、`languageServers: [1, 2]` 和 `buildSystems: [1, 2]`。Windows 还没有这些界面和推送,所以仍只声明 1,需要 v2 的插件在 Windows 上会被市场隐藏,而不是装上后行为不完整。

### 7. 路径选择按钮保留完整宽度

插件详情页比完整设置窗口窄。路径输入框若固定占用 220 点,同时右侧作用域和
生效标签保持完整,SwiftUI 会把“选择…”按钮压成没有文字的灰条。通用路径
控件因此固定按钮的自然宽度,让输入框随剩余空间缩短;共享设置行的标题列
以原来的 230 点为上限,窄面板时也允许缩短。宽面板仍保持原来的列宽。
缩放只调整布局,不替换输入框实例,以免丢失尚未提交的路径。

这与 IDEA Community 的文本框加浏览按钮采用相同的空间分配:检查的版本是
`c7f91397daa3a961b4e78bc634fe467a0a7d9ade`,源码为
`platform/platform-api/src/com/intellij/openapi/ui/TextFieldWithBrowseButton.java`
及 `ComponentWithBrowseButton.java`。后者把文本框放在 `BorderLayout.CENTER`,
把 `FixedSizeButton` 放在 `BorderLayout.EAST`,让输入部分承担宽度变化。
macOS 保留既有原生控件和主题,只在通用 owner 中修复压缩规则,不为 Cargo
增加专门的宽度或页面。

### 8. 插件详情不能越过分隔线

分隔容器分配了确定矩形,不代表子内容一定接受这个宽度。插件详情的表单
有自己的最小宽度;窗口缩小后,原来的根容器会按更宽的内容居中,导致名称、
页签和卡片跨进左侧列表,分隔线看起来穿过了详情。

详情根视图必须接受分隔容器给出的宽度,并在自己的边界内绘制。设置页的
原生表单在宽面板填满可用宽度;面板不足以容纳完整设置行时,使用同一个
滚动容器横向查看,保留输入框、选择器和右侧状态标签。只裁剪超出的表单会
让部分设置无法访问;提高整个设置窗口的最小宽度又会限制其他页面,因此
不采用这两种单独修补方式。

宽度只在详情正文的小容器里读取,用于分配可见区域。已构建的正文作为值
传入,不在几何回调里重新加载插件、重建列表或保存宽度。拖动和持久化仍由
既有 `LitheSplitPaneView` 负责,窄宽切换不替换表单实例。

## 考虑过的备选方案

Expand All @@ -79,6 +143,11 @@ macOS 的 `hostCapabilities` 声明 `settings: [1, 2]` 和 `languageServers: [1,

插件已经可以声明网页 UI,理论上可以自己画设置页。但网页无法和宿主设置窗口的搜索、分组、项目/全局切换保持一致,用户偏好也是原生优先。网页 UI 仍可用于复杂的工具窗口,设置项本身走原生控件。

### 顶层增加可执行文件覆盖旁路

单独增加 executableSetting/configuredExecutable 会让 Build 的设置路径绕过候选顺序,
其它进程模块也不能复用。只为已发布预览包保留输入适配,新 API 使用统一候选与宿主能力注入。

### 宿主自己合并取值

Swift 和 Windows 各写一遍合并规则最直接,但两边一旦有细微差异,界面显示的值和实际启动使用的值就会不同。所以合并只在 Core 做,宿主只负责存储和显示。
Expand All @@ -97,6 +166,8 @@ Swift 和 Windows 各写一遍合并规则最直接,但两边一旦有细微
- `cargo test -p lithe-core --lib plugin_configuration`:会话按 section 应答、更新推送、宿主会话拒绝更新。
- `./scripts/test-macos.sh --filter PluginSettingsTests`:项目层存储、作用域规则、页面注册、配置展开。
- `./scripts/test-macos.sh --filter SettingsSidebarLayoutTests`:静态页面仍各归一个分组。
- `./scripts/test-macos.sh --filter PluginSettingsLayoutTests`:480、640、900 点宽的深浅主题下,路径输入框和完整选择按钮都在容器内,按钮可调用选择器并写入所选路径。
- `./scripts/test-macos.sh --filter PluginDetailLayoutTests`:实际 Java 插件表单在深浅主题中往返缩放,详情边界始终跟随分隔条,窄面板下右侧设置可经原生横向滚动到达。
- 共享夹具:`shared/contracts/plugins/v1/fixtures/settings-v2.plugin.json`。

## 适用范围
Expand All @@ -108,5 +179,6 @@ Swift 和 Windows 各写一遍合并规则最直接,但两边一旦有细微
- `macos/Sources/Lithe/Models/AppModel/AppModel+PluginSettings.swift`
- `macos/Sources/Lithe/Platform/MacOS/Plugins/MacPluginSettingsStore.swift`
- `macos/Sources/Lithe/Views/App/PluginSettingsSection.swift`
- `macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`
- `macos/Sources/Lithe/Views/App/ContributedSettingsPageView.swift`
- `shared/contracts/plugins/v1/README.md`
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions docs/plugin-executable-layout-visual-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 插件路径控件和详情分栏的原生截图

以下图像使用 `PluginSettingsLayoutTests`、`PluginDetailLayoutTests` 中相同的
SwiftUI/AppKit 视图、示例数据、尺寸和主题。原布局取自 PR 基线
`fe0b31c71f692bbb60bf938da9c47498bbd1d0ec` 的三个视图文件;当前布局包含
`fc653d5c0` 和 `589906520` 两个独立 UI 修复提交。此次 Runtime/Provider 修复
没有继续修改布局实现。

截图由原生视图绘制生成,测试宿主的左栏是占位文字;不代表完整产品窗口或
真实桌面合成验收。前后图均在初始未滚动的位置采集。原布局只用于渲染对比;
当前布局另外运行完整的几何、输入实例、横向滚动与路径选择断言。

## 路径控件,480pt 宿主

原布局里的输入框固定占宽,按钮文字被挤掉。当前布局保留选择按钮的自然宽度,
标题与输入框随可用空间缩短,右侧的作用域与生效标签仍可见。

| 主题 | PR 基线 | 当前布局 |
| --- | --- | --- |
| 浅色 | ![浅色原路径控件](assets/plugin-executable-layout/before-path-480-light.png) | ![浅色修复后路径控件](assets/plugin-executable-layout/after-path-480-light.png) |
| 深色 | ![深色原路径控件](assets/plugin-executable-layout/before-path-480-dark.png) | ![深色修复后路径控件](assets/plugin-executable-layout/after-path-480-dark.png) |

## 插件详情,700 × 700pt 分栏宿主

原布局的详情内容越过分隔线进入左栏。当前布局接受分栏宽度,内容留在分隔线
右侧;窄设置表单可横向滚动。回归还验证 900、1000 和 1100pt 宽度,以及
窗口高度变化时的顶部对齐和输入框实例保持。

| 主题 | PR 基线 | 当前布局 |
| --- | --- | --- |
| 浅色 | ![浅色原插件详情](assets/plugin-executable-layout/before-detail-700-light.png) | ![浅色修复后插件详情](assets/plugin-executable-layout/after-detail-700-light.png) |
| 深色 | ![深色原插件详情](assets/plugin-executable-layout/before-detail-700-dark.png) | ![深色修复后插件详情](assets/plugin-executable-layout/after-detail-700-dark.png) |

## 验证出口

在仓库根目录运行完整断言及当前布局截图导出:

```sh
LITHE_PLUGIN_SETTINGS_LAYOUT_CAPTURES="$PWD/.artifacts/plugin-layout-check" \
LITHE_PLUGIN_DETAIL_LAYOUT_CAPTURES="$PWD/.artifacts/plugin-layout-check" \
./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- \
--filter 'PluginSettingsLayoutTests|PluginDetailLayoutTests'
```

标准导出的详情图在横向滚动到末端后采集,便于检查右侧设置是否仍可访问。
本表另在同一视图布局完成后、滚动前采集,以保持前后位置一致。
截图是文档静态材料,不被产品加载,也不进入应用资源包、安装目录或工作树
资源复用清单。测试窗口由各用例关闭,不保留临时应用或后台进程。
1 change: 1 addition & 0 deletions macos/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2109,6 +2109,7 @@
"Reset Page Zoom (100%)" = "Reset Page Zoom (100%)";
"Page Zoom" = "Page Zoom";

"The configured build tool is not executable: %@" = "The configured build tool is not executable: %@";
/* Embedded HTML preview browser: camera and microphone. */
"“%@” wants to use your camera." = "“%@” wants to use your camera.";
"“%@” wants to use your microphone." = "“%@” wants to use your microphone.";
Expand Down
1 change: 1 addition & 0 deletions macos/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -3686,6 +3686,7 @@
"Reset Page Zoom (100%)" = "恢复页面缩放(100%)";
"Page Zoom" = "页面缩放";

"The configured build tool is not executable: %@" = "配置的构建工具无法执行:%@";
/* 内嵌 HTML 预览浏览器:摄像头和麦克风。 */
"“%@” wants to use your camera." = "“%@”想使用你的摄像头。";
"“%@” wants to use your microphone." = "“%@”想使用你的麦克风。";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ struct RustBuildSystemOperations: BuildSystemPlanning {
return outcome.value.languageImport
}

func invocationPlan(_ target: BuildSystemTarget, root: String) throws -> BuildInvocation {
func invocationPlan(
_ target: BuildSystemTarget, root: String, settings: [String: ToolingJSONValue] = [:]
) throws -> BuildInvocation {
let outcome: Outcome<InvocationResult> = try execute(
"build.invocationPlan",
target: target,
request: InvocationRequest(root: root, platform: "macos")
request: InvocationRequest(root: root, platform: "macos", settings: settings)
)
return try outcome.value.invocation.invocation()
}
Expand Down Expand Up @@ -289,6 +291,7 @@ struct RustBuildSystemOperations: BuildSystemPlanning {
private struct InvocationRequest: Encodable {
let root: String
let platform: String
let settings: [String: ToolingJSONValue]
}

private struct VersionRequest: Encodable {
Expand Down Expand Up @@ -470,8 +473,10 @@ struct RustBuildSystemOperations: BuildSystemPlanning {
let variable: String?
let command: String?
let runtime: String?
let settingID: String?

func executable() throws -> BuildInvocation.Executable {
if kind == "setting", let settingID { return .setting(id: settingID, path: path) }
switch (kind, path, variable, command, runtime) {
case ("projectFile", let path?, _, _, _):
return .projectFile(path: path, requires: requires ?? [])
Expand Down
Loading
Loading