From 95a0dbf0ad8fd187b706e7c1d4fa51a0f26c7ad1 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 20 Sep 2026 18:54:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(viper):=20=E9=80=89=E6=8B=A9=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=8A=A0=E3=80=8C=E9=80=89=E6=8B=A9=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E5=99=A8=E2=80=A6=E3=80=8D=EF=BC=8C=E5=85=B3=E9=97=AD=20VET=20?= =?UTF-8?q?=E5=90=8E=E6=B8=85=E5=8D=95=E4=B8=8E=E9=80=89=E4=B8=AD=E4=BB=8D?= =?UTF-8?q?=E5=8F=AF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 选择列表首项为「选择编译器…」(文件选择对话框 → VET check → 并入清单并切换),其后一条分隔线才是 VET 搜到的环境 - 状态栏在 VET 关闭时显示 VET 已关闭,且点击仍打开选择列表(重启改走命令面板) - EnvironmentManager 新增 AddEnvironment:check 通过的环境无需等下一轮快照即可选中 - 抽出 PickAndCheckExecutable,去掉 check 命令与该入口的重复实现 --- packages/viper/src/environmentManager.ts | 92 ++++++++++++++----- packages/viper/src/extension.ts | 109 +++++++++++++++-------- packages/viper/src/statusBar.ts | 11 ++- 3 files changed, 151 insertions(+), 61 deletions(-) diff --git a/packages/viper/src/environmentManager.ts b/packages/viper/src/environmentManager.ts index a094ad5e..d084c08a 100644 --- a/packages/viper/src/environmentManager.ts +++ b/packages/viper/src/environmentManager.ts @@ -6,7 +6,15 @@ * 并把「选中哪个环境」持久化到扩展状态(globalState)——刻意不写回 .vpj,保持项目文件干净。 */ -import { Disposable, Event, EventEmitter, Memento, QuickPickItem, window, commands } from 'vscode'; +import { + Disposable, + Event, + EventEmitter, + Memento, + QuickPickItem, + QuickPickItemKind, + window, +} from 'vscode'; import { ViperEnvironment } from './types'; @@ -20,11 +28,18 @@ export interface EnvironmentChange { Removed: ViperEnvironment[]; } +/** + * 「手动指定编译器」的处理(由扩展入口注入:弹输入框取路径 + 走 VET 的 check 通道)。 + * 返回被选中的环境表示已处理完;返回 undefined 表示用户取消或校验未通过。 + */ +export type ManualCompilerPicker = () => Promise; + export class EnvironmentManager implements Disposable { private static readonly SelectedIdKey = 'viper.environmentTool.selectedId'; private environments: ViperEnvironment[] = []; private selectedId: string | undefined; + private manualCompilerPicker: ManualCompilerPicker | undefined; private readonly _onDidChangeEnvironments = new EventEmitter(); readonly OnDidChangeEnvironments: Event = this._onDidChangeEnvironments.event; @@ -92,7 +107,8 @@ export class EnvironmentManager implements Disposable { } /** - * 选择环境。 + * 选择编译器。列表结构:首项是「选择编译器…」(手动输入 exe 路径),随后一条分隔线, + * 再之后才是 VET 搜到的环境。VET 关闭时这个列表照样可用(已发现的环境仍能选用)。 * @param environmentId 传入则直接选中该 id;不传则弹 QuickPick 让用户选。 */ async SelectEnvironment(environmentId?: string): Promise { @@ -100,31 +116,59 @@ export class EnvironmentManager implements Disposable { return this.ApplySelection(environmentId); } - const all = this.environments; - if (all.length === 0) { - const picked = await window.showWarningMessage('VET 还没有发现任何 Viper 编译器。', '重启 VET 扫描'); - if (picked === '重启 VET 扫描') { - void commands.executeCommand('viper.restartEnvironmentTool'); - } - return undefined; + const items: CompilerPickItem[] = [ + { + label: '$(file) 选择编译器…', + description: '手动指定 exe 路径', + Manual: true, + }, + ]; + if (this.environments.length > 0) { + items.push({ label: '', kind: QuickPickItemKind.Separator }); + items.push( + ...this.environments.map((environment) => ({ + label: environment.DisplayName, + description: `${environment.Version} · ${environment.Arch} · ${environment.Source}`, + detail: environment.Executable, + picked: environment.Id === this.selectedId, + Environment: environment, + })) + ); } - const items = all.map((environment) => ({ - label: environment.DisplayName, - description: `${environment.Version} · ${environment.Arch} · ${environment.Source}`, - detail: environment.Executable, - picked: environment.Id === this.selectedId, - Environment: environment, - })); - const picked = await window.showQuickPick(items, { - title: '选择 Viper 环境', - placeHolder: '选择要使用的 Viper 编译器(仅保存在扩展状态,不写入 .vpj)', + title: '选择编译器', + placeHolder: '选择要为当前工作区使用的 Viper 编译器', matchOnDescription: true, matchOnDetail: true, }); + if (!picked) { + return undefined; + } + if ('Manual' in picked) { + return this.manualCompilerPicker ? await this.manualCompilerPicker() : undefined; + } + if ('Environment' in picked) { + return this.ApplySelection(picked.Environment.Id); + } + return undefined; + } - return picked ? this.ApplySelection(picked.Environment.Id) : undefined; + /** 注入「手动指定编译器」的处理(扩展入口提供:输入框取路径 + VET check)。 */ + SetManualCompilerPicker(picker: ManualCompilerPicker): void { + this.manualCompilerPicker = picker; + } + + /** + * 把一个刚通过 VET check 的环境并入清单 —— 下一轮全量快照(≤ scanIntervalMs)到达前 + * 就能立刻选中它;快照到达时因为已在表中,diff 不会重复算作新增。 + */ + AddEnvironment(environment: ViperEnvironment): void { + const next = [...this.environments.filter((item) => item.Id !== environment.Id), environment].sort( + CompareEnvironments + ); + this.environments = next; + this._onDidChangeEnvironments.fire({ Environments: next, Added: [environment], Removed: [] }); } dispose(): void { @@ -145,10 +189,18 @@ export class EnvironmentManager implements Disposable { } } +/** 「选择编译器…」项:点击后走手动输入路径。 */ +interface ManualPickItem extends QuickPickItem { + Manual: true; +} + interface EnvironmentQuickPickItem extends QuickPickItem { Environment: ViperEnvironment; } +/** 选择列表上的项:手动项 / VET 搜到的环境 / 分隔线。 */ +type CompilerPickItem = ManualPickItem | EnvironmentQuickPickItem | QuickPickItem; + /** 排序:先按来源分组,再按展示名;保证每轮快照顺序稳定(避免视图抖动)。 */ function CompareEnvironments(left: ViperEnvironment, right: ViperEnvironment): number { if (left.Source !== right.Source) { diff --git a/packages/viper/src/extension.ts b/packages/viper/src/extension.ts index 4e804c3a..bc7af7a6 100644 --- a/packages/viper/src/extension.ts +++ b/packages/viper/src/extension.ts @@ -9,7 +9,7 @@ import { EnvironmentManager } from './environmentManager'; import { EnvironmentTool } from './environmentTool'; import { ProjectManager } from './projectManager'; import { ToolchainManager } from './toolchainManager'; -import { ViperCheckResult } from './types'; +import { ViperCheckResult, ViperEnvironment } from './types'; import { ViperViewProvider } from './viperViewProvider'; import { EnvironmentStatusBar } from './statusBar'; import { ViperApiImpl } from './api'; @@ -98,6 +98,9 @@ export async function activate(context: ExtensionContext): Promise }) ); + // 选择列表里的「选择编译器…」:手动输入路径 → VET check → 并入环境清单并切过去。 + environments.SetManualCompilerPicker(() => PickManualCompiler(environmentTool, environments)); + // 命令。 context.subscriptions.push( RegisterCommandSafe(context, 'viper.createProject', async () => { @@ -244,53 +247,85 @@ async function CreateProjectCommand(): Promise { } /** - * 手动指定一个 exe,经 VET 的 check 通道校验(PE/.PRVERSN + `--version` 前缀为 Viper)。 - * 校验通过后 VET 会把它入库并写回缓存文件,下一轮快照里就会出现在 Environments 视图。 + * 弹文件选择框挑一个 exe,并经 VET 的 check 通道校验。 + * 返回回执;用户取消时返回 undefined(校验失败会在这里给出错误提示)。 */ -async function CheckEnvironmentCommand(tool: EnvironmentTool): Promise { +async function PickAndCheckExecutable(tool: EnvironmentTool): Promise { const picked = await window.showOpenDialog({ - title: '选择要检查的 Viper 可执行文件', + title: '选择 Viper 编译器', canSelectMany: false, - openLabel: '检查', + openLabel: '选择', filters: { 'Viper 可执行文件': ['exe'] }, }); const exePath = picked?.[0]?.fsPath; if (!exePath) { + return undefined; + } + + return window.withProgress( + { location: ProgressLocation.Notification, title: `VET 正在检查 ${path.basename(exePath)}…` }, + async () => { + try { + return await tool.CheckExecutable(exePath); + } catch (e) { + void window.showErrorMessage(`VET 检查失败:${e instanceof Error ? e.message : String(e)}`); + return undefined; + } + } + ); +} + +/** + * 「选择编译器…」:选一个 exe,经 VET check 通过后并入环境清单并立刻切换过去 + * (VET 会把该 exe 入库,后续轮次快照里也会带着它)。 + */ +async function PickManualCompiler( + tool: EnvironmentTool, + environments: EnvironmentManager +): Promise { + const result = await PickAndCheckExecutable(tool); + if (!result) { + return undefined; + } + + const environment = result.Environment; + if (result.Ok && environment) { + environments.AddEnvironment(environment); + const selected = await environments.SelectEnvironment(environment.Id); + void window.showInformationMessage(`已切换到 ${environment.DisplayName}(${environment.Arch})`); + return selected; + } + + void window.showErrorMessage(`VET 检查未通过:${DescribeCheckMessage(result.Message)}`); + return undefined; +} + +/** 手动指定一个 exe 做检查(只检查、不切换),结果通过信息消息回报。 */ +async function CheckEnvironmentCommand(tool: EnvironmentTool): Promise { + const result = await PickAndCheckExecutable(tool); + if (!result) { return; } - await window.withProgress( - { location: ProgressLocation.Notification, title: `VET 正在检查 ${path.basename(exePath)}…` }, - async () => { - let result: ViperCheckResult; - try { - result = await tool.CheckExecutable(exePath); - } catch (e) { - void window.showErrorMessage(`VET 检查失败:${e instanceof Error ? e.message : String(e)}`); - return; - } + const environment = result.Environment; + if (result.Ok && environment) { + void window + .showInformationMessage( + `VET 检查通过:${environment.DisplayName}(${environment.Arch})`, + '选择为当前环境', + '显示详情' + ) + .then((choice) => { + if (choice === '选择为当前环境') { + void commands.executeCommand('viper.selectEnvironment', environment.Id); + } else if (choice === '显示详情') { + void window.showInformationMessage(environment.Executable, { modal: true }); + } + }); + return; + } - const environment = result.Environment; - if (result.Ok && environment) { - void window - .showInformationMessage( - `VET 检查通过:${environment.DisplayName}(${environment.Arch})`, - '选择为当前环境', - '显示详情' - ) - .then((choice) => { - if (choice === '选择为当前环境') { - void commands.executeCommand('viper.selectEnvironment', environment.Id); - } else if (choice === '显示详情') { - void window.showInformationMessage(environment.Executable, { modal: true }); - } - }); - return; - } - - void window.showErrorMessage(`VET 检查未通过:${DescribeCheckMessage(result.Message)}`); - } - ); + void window.showErrorMessage(`VET 检查未通过:${DescribeCheckMessage(result.Message)}`); } /** 把 VET 回执里的 check_msg 固定原因串翻译成可读说明。 */ diff --git a/packages/viper/src/statusBar.ts b/packages/viper/src/statusBar.ts index ec38df76..0f80d548 100644 --- a/packages/viper/src/statusBar.ts +++ b/packages/viper/src/statusBar.ts @@ -39,11 +39,12 @@ export class EnvironmentStatusBar implements Disposable { const count = this.environments.GetEnvironments().length; const stateText = DescribeState(state); - // 手动停止时点击直接把它拉起来(此时选择环境没有意义);其余情况点击打开环境选择器。 - this.item.command = state === 'stopped' ? 'viper.restartEnvironmentTool' : 'viper.selectEnvironment'; + // 点击状态栏始终打开选择列表:VET 关闭时也一样 —— 已发现的环境仍可选用, + // 需要重启 VET 请走命令面板的「Restart VET」。 + this.item.command = 'viper.selectEnvironment'; if (state === 'stopped') { - this.item.text = '$(debug-stop) Viper 环境: 已关闭'; + this.item.text = '$(debug-stop) VET 已关闭'; } else if (selected) { this.item.text = `$(server-environment) Viper 环境: ${selected.DisplayName}`; } else if (count > 0) { @@ -60,7 +61,9 @@ export class EnvironmentStatusBar implements Disposable { selected ? `当前选中: ${selected.DisplayName}\n${selected.Executable}` : '当前选中: (无)', `VET: ${this.tool.GetExecutablePath()}`, '输出: Vipeye VET 输出通道', - state === 'stopped' ? '点击重新启动 VET' : '点击选择环境', + state === 'stopped' + ? '点击选择编译器(VET 已关闭,已发现的环境仍可选用;重启请用 Restart VET)' + : '点击选择编译器', ].join('\n'); this.item.show(); }