feat(viper): 选择列表加「选择编译器…」,关闭 VET 后清单与选中仍可用

- 选择列表首项为「选择编译器…」(文件选择对话框 → VET check → 并入清单并切换),其后一条分隔线才是 VET 搜到的环境
- 状态栏在 VET 关闭时显示 VET 已关闭,且点击仍打开选择列表(重启改走命令面板)
- EnvironmentManager 新增 AddEnvironment:check 通过的环境无需等下一轮快照即可选中
- 抽出 PickAndCheckExecutable,去掉 check 命令与该入口的重复实现
This commit is contained in:
TermiNexus committed 2026-09-20 18:54:09 +08:00
1 parent ad3fd9810c
commit 95a0dbf0ad
3 files changed
+122 -32

No files matched your search

+68 -16
View File
@@ -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<ViperEnvironment | undefined>;
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<EnvironmentChange>();
readonly OnDidChangeEnvironments: Event<EnvironmentChange> = this._onDidChangeEnvironments.event;
@@ -92,7 +107,8 @@ export class EnvironmentManager implements Disposable {
}
/**
* 选择环境。
* 选择编译器。列表结构:首项是「选择编译器…」(手动输入 exe 路径),随后一条分隔线,
* 再之后才是 VET 搜到的环境。VET 关闭时这个列表照样可用(已发现的环境仍能选用)。
* @param environmentId 传入则直接选中该 id;不传则弹 QuickPick 让用户选。
*/
async SelectEnvironment(environmentId?: string): Promise<ViperEnvironment | undefined> {
@@ -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 = all.map<EnvironmentQuickPickItem>((environment) => ({
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<EnvironmentQuickPickItem>((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) {
+47 -12
View File
@@ -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<ViperApiImpl>
})
);
// 选择列表里的「选择编译器…」:手动输入路径 → VET check → 并入环境清单并切过去。
environments.SetManualCompilerPicker(() => PickManualCompiler(environmentTool, environments));
// 命令。
context.subscriptions.push(
RegisterCommandSafe(context, 'viper.createProject', async () => {
@@ -244,29 +247,63 @@ async function CreateProjectCommand(): Promise<void> {
}
/**
* 手动指定一个 exe,经 VET 的 check 通道校验(PE/.PRVERSN + `--version` 前缀为 Viper)。
* 校验通过后 VET 会把它入库并写回缓存文件,下一轮快照里就会出现在 Environments 视图。
* 弹文件选择框挑一个 exe,并经 VET 的 check 通道校验。
* 返回回执;用户取消时返回 undefined(校验失败会在这里给出错误提示)。
*/
async function CheckEnvironmentCommand(tool: EnvironmentTool): Promise<void> {
async function PickAndCheckExecutable(tool: EnvironmentTool): Promise<ViperCheckResult | undefined> {
const picked = await window.showOpenDialog({
title: '选择要检查的 Viper 可执行文件',
title: '选择 Viper 编译器',
canSelectMany: false,
openLabel: '检查',
openLabel: '选择',
filters: { 'Viper 可执行文件': ['exe'] },
});
const exePath = picked?.[0]?.fsPath;
if (!exePath) {
return;
return undefined;
}
await window.withProgress(
return window.withProgress(
{ location: ProgressLocation.Notification, title: `VET 正在检查 ${path.basename(exePath)}…` },
async () => {
let result: ViperCheckResult;
try {
result = await tool.CheckExecutable(exePath);
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<ViperEnvironment | undefined> {
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<void> {
const result = await PickAndCheckExecutable(tool);
if (!result) {
return;
}
@@ -289,8 +326,6 @@ async function CheckEnvironmentCommand(tool: EnvironmentTool): Promise<void> {
}
void window.showErrorMessage(`VET 检查未通过:${DescribeCheckMessage(result.Message)}`);
}
);
}
/** 把 VET 回执里的 check_msg 固定原因串翻译成可读说明。 */
+7 -4
View File
@@ -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();
}