feat(viper): 接入 VET 环境发现,支持停止/查看输出与粘性选择

- 新增 EnvironmentTool / EnvironmentManager:常驻 scan 进程客户端(启动、心跳确认、快照解析、异常退出的指数退避重启)与全量快照 diff
- 侧边栏新增 Environments 视图(按 source 分组、选中项打勾);状态栏只保留 VET 环境项,移除硬编码 C:\v.exe
- 默认复用上次使用的环境 uuid,VET 未给出该 uuid 时取发现的第一个,一个都没发现时回退配置/兜底路径
- VET 的 stdout 与 stderr 全量转发到 Vipeye VET 输出通道,彻底移除日志文件落盘
- 新增 Stop VET / Restart VET / Show VET Output / Select Environment 命令,并补回被回写覆盖丢失的 contributes
This commit is contained in:
TermiNexus committed 2026-09-20 18:19:18 +08:00
1 parent b0f4b8b5fd
commit 5d6fcf7c82
9 files changed
+1059 -32

No files matched your search

+91 -1
View File
@@ -77,6 +77,10 @@
"id": "viper.projectView",
"name": "Project"
},
{
"id": "viper.environmentView",
"name": "Environments"
},
{
"id": "viper.toolchainView",
"name": "Toolchain"
@@ -118,6 +122,30 @@
"title": "Open Settings",
"category": "Viper"
},
{
"command": "viper.selectEnvironment",
"title": "Select Environment",
"category": "Viper",
"icon": "$(server-environment)"
},
{
"command": "viper.restartEnvironmentTool",
"title": "Restart VET",
"category": "Viper",
"icon": "$(debug-restart)"
},
{
"command": "viper.stopEnvironmentTool",
"title": "Stop VET",
"category": "Viper",
"icon": "$(debug-stop)"
},
{
"command": "viper.showEnvironmentLog",
"title": "Show VET Output",
"category": "Viper",
"icon": "$(output)"
},
{
"command": "viper.refreshView",
"title": "Refresh",
@@ -132,6 +160,18 @@
],
"menus": {
"commandPalette": [
{
"command": "viper.selectEnvironment"
},
{
"command": "viper.restartEnvironmentTool"
},
{
"command": "viper.stopEnvironmentTool"
},
{
"command": "viper.showEnvironmentLog"
},
{
"command": "viper.openVpjAsJson",
"when": "resourceExtname == .vpj"
@@ -154,7 +194,7 @@
"view/title": [
{
"command": "viper.refreshView",
"when": "view == viper.projectView || view == viper.toolchainView || view == viper.settingsView",
"when": "view == viper.projectView || view == viper.environmentView || view == viper.toolchainView || view == viper.settingsView",
"group": "navigation"
}
]
@@ -162,6 +202,56 @@
"configuration": {
"title": "Viper",
"properties": {
"viper.environmentTool.enabled": {
"type": "boolean",
"default": true,
"description": "启用 VET(Viper Environment Tool)常驻扫盘发现 Viper 编译器。",
"scope": "window"
},
"viper.environmentTool.path": {
"type": "string",
"default": "D:\\Users\\TermiNexus\\Desktop\\TransPyC\\VET\\.tpv_build\\output\\VET.exe",
"description": "VET 可执行文件路径(Viper 自身实现的环境发现工具)。",
"scope": "window"
},
"viper.environmentTool.scanPaths": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "传给 VET 的 --path 扫描目录。留空则让 VET 自行建立常用目录表;一旦填写则完全替换它(不再自动建表)。",
"scope": "window"
},
"viper.environmentTool.confirmSeconds": {
"type": "number",
"default": 10,
"minimum": 0,
"description": "VET 心跳确认周期(秒,对应 --need-confirm);0 表示关闭确认(不建议:关闭后 VET 不会因超时自杀)。",
"scope": "window"
},
"viper.environmentTool.confirmTimeoutMs": {
"type": "number",
"default": 5000,
"minimum": 0,
"description": "VET 每轮确认窗口长度(毫秒,对应 --need-confirm-timeout);窗口内没收到 {\"ok\":1} 就会退出。",
"scope": "window"
},
"viper.environmentTool.scanIntervalMs": {
"type": "number",
"default": 10000,
"minimum": 0,
"maximum": 86400000,
"description": "VET 每轮扫盘间隔(毫秒,对应 --sleep)。",
"scope": "window"
},
"viper.environmentTool.maxRestarts": {
"type": "number",
"default": 5,
"minimum": 0,
"description": "VET 异常退出后的自动重启次数上限(指数退避,单次最多等 30 秒);超过后通知错误并停止重启。",
"scope": "window"
},
"viper.toolchain.path": {
"type": "string",
"default": "C:\\v.exe",
+158
View File
@@ -0,0 +1,158 @@
/*
* environmentManager.ts
* Viper 环境(编译器)清单与选中状态。
*
* 数据来源是 VET 每轮输出的**全量**快照,所以这里要自己 diff(Added / Removed),
* 并把「选中哪个环境」持久化到扩展状态(globalState)——刻意不写回 .vpj,保持项目文件干净。
*/
import { Disposable, Event, EventEmitter, Memento, QuickPickItem, window, commands } from 'vscode';
import { ViperEnvironment } from './types';
/** 一次快照带来的变化。 */
export interface EnvironmentChange {
/** 当前全部环境(已排序)。 */
Environments: ViperEnvironment[];
/** 本轮新增。 */
Added: ViperEnvironment[];
/** 本轮移除。 */
Removed: ViperEnvironment[];
}
export class EnvironmentManager implements Disposable {
private static readonly SelectedIdKey = 'viper.environmentTool.selectedId';
private environments: ViperEnvironment[] = [];
private selectedId: string | undefined;
private readonly _onDidChangeEnvironments = new EventEmitter<EnvironmentChange>();
readonly OnDidChangeEnvironments: Event<EnvironmentChange> = this._onDidChangeEnvironments.event;
private readonly _onDidChangeSelection = new EventEmitter<ViperEnvironment | undefined>();
readonly OnDidChangeSelection: Event<ViperEnvironment | undefined> = this._onDidChangeSelection.event;
constructor(private readonly globalState: Memento) {
this.selectedId = globalState.get<string>(EnvironmentManager.SelectedIdKey);
}
/** 应用一轮全量快照,返回 diff 结果。 */
ApplySnapshot(environments: ViperEnvironment[]): EnvironmentChange {
const previousIds = new Set(this.environments.map((environment) => environment.Id));
const next = [...environments].sort(CompareEnvironments);
const nextIds = new Set(next.map((environment) => environment.Id));
const added = next.filter((environment) => !previousIds.has(environment.Id));
const removed = this.environments.filter((environment) => !nextIds.has(environment.Id));
this.environments = next;
this.ResolveStickySelection();
const change: EnvironmentChange = { Environments: next, Added: added, Removed: removed };
this._onDidChangeEnvironments.fire(change);
return change;
}
/**
* 选定「本次生效的环境」:
* 1. 优先复用上次用的那个 exe(按 VET 给的 uuid 在快照里找);
* 2. 找不到(第一次用、或那个 exe 已被删/移走)则用发现的第一个;
* 3. VET 一个都没给出时保留上次记录,等它下一轮回来。
* 结果写回 globalState,作为下一次的「上次用的」。
*/
private ResolveStickySelection(): void {
const preferred = this.environments.find((environment) => environment.Id === this.selectedId)
?? this.environments[0];
if (!preferred || preferred.Id === this.selectedId) {
return;
}
this.selectedId = preferred.Id;
void this.globalState.update(EnvironmentManager.SelectedIdKey, preferred.Id);
this._onDidChangeSelection.fire(preferred);
}
/** 当前全部环境。 */
GetEnvironments(): ViperEnvironment[] {
return this.environments;
}
/** 当前选中的环境 id。 */
GetSelectedId(): string | undefined {
return this.selectedId;
}
/** 当前选中的环境(若已被 VET 移除则为 undefined)。 */
GetSelectedEnvironment(): ViperEnvironment | undefined {
if (!this.selectedId) {
return undefined;
}
return this.environments.find((environment) => environment.Id === this.selectedId);
}
/**
* 选择环境。
* @param environmentId 传入则直接选中该 id;不传则弹 QuickPick 让用户选。
*/
async SelectEnvironment(environmentId?: string): Promise<ViperEnvironment | undefined> {
if (environmentId) {
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) => ({
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)',
matchOnDescription: true,
matchOnDetail: true,
});
return picked ? this.ApplySelection(picked.Environment.Id) : undefined;
}
dispose(): void {
this._onDidChangeEnvironments.dispose();
this._onDidChangeSelection.dispose();
}
private async ApplySelection(environmentId: string): Promise<ViperEnvironment | undefined> {
const environment = this.environments.find((item) => item.Id === environmentId);
if (!environment) {
return undefined;
}
this.selectedId = environmentId;
await this.globalState.update(EnvironmentManager.SelectedIdKey, environmentId);
this._onDidChangeSelection.fire(environment);
return environment;
}
}
interface EnvironmentQuickPickItem extends QuickPickItem {
Environment: ViperEnvironment;
}
/** 排序:先按来源分组,再按展示名;保证每轮快照顺序稳定(避免视图抖动)。 */
function CompareEnvironments(left: ViperEnvironment, right: ViperEnvironment): number {
if (left.Source !== right.Source) {
return left.Source.localeCompare(right.Source);
}
return left.DisplayName.localeCompare(right.DisplayName);
}
+463
View File
@@ -0,0 +1,463 @@
/*
* environmentTool.ts
* VET(Viper Environment Tool)常驻进程客户端。
*
* VET 是一个用 Viper 自身写的扫盘发现器(见 TransPyC/VET),协议如下:
* VET.exe scan --cache <path> [--sleep <毫秒>]
* [--need-confirm <秒> [--need-confirm-timeout <毫秒>]] [--path <dir>]...
* - stdout:每轮输出一行**完整**的编译器 JSON 数组(所以增量变化要自己 diff)
* - stderr:滚动日志(量很大,原样进输出通道)
* - --need-confirm:每 <秒> 秒一个确认窗口(窗口长度 = --need-confirm-timeout,缺省 5s),
* 窗口内必须从 stdin 收到一行 {"ok":1};否则 VET 在 stdout 输出 {"exit":1,...} 并以退出码 1 终止。
*
* 本类负责:进程生命周期(自动重启 + 指数退避 + 重启上限)、心跳确认、输出转发、快照解析。
*
* 输出:VET 的 stdout 与 stderr **原样**转发到 "Vipeye VET" 输出通道,不落盘。
*/
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { commands, Disposable, Event, EventEmitter, OutputChannel, window, workspace } from 'vscode';
import { ViperEnvironment } from './types';
/**
* VET 进程状态。
* disabled —— 配置里 viper.environmentTool.enabled = false,扩展不启动它
* stopped —— 用户手动执行了「停止 VET」命令(与 disabled 区分,便于状态栏给出「点击可重新启动」)
* starting / running / restarting / failed —— 进程生命周期与自动重启
*/
export type EnvironmentToolState = 'disabled' | 'stopped' | 'starting' | 'running' | 'restarting' | 'failed';
/** 进程存活超过该时长即认为这一轮是健康的,重启计数清零。 */
const HealthyRunMs = 30000;
/** 两次心跳之间的最小间隔,避免同一轮重复发送。 */
const MinConfirmIntervalMs = 1500;
export class EnvironmentTool implements Disposable {
private process: ChildProcessWithoutNullStreams | undefined;
private state: EnvironmentToolState = 'disabled';
private restarts = 0;
private startedAt = 0;
private lastConfirmAt = 0;
private stdoutBuffer = '';
private stderrBuffer = '';
private restartTimer: NodeJS.Timeout | undefined;
private watchdogTimer: NodeJS.Timeout | undefined;
private disposed = false;
/** 上次记录过的退出信息,避免状态栏/提示重复报同一件事。 */
private lastExitSummary = '';
private readonly channel: OutputChannel;
private readonly _onDidChangeEnvironments = new EventEmitter<ViperEnvironment[]>();
/** 每轮 stdout 快照(全量)。 */
readonly OnDidChangeEnvironments: Event<ViperEnvironment[]> = this._onDidChangeEnvironments.event;
private readonly _onDidChangeState = new EventEmitter<EnvironmentToolState>();
readonly OnDidChangeState: Event<EnvironmentToolState> = this._onDidChangeState.event;
constructor(private readonly storageDir: string, channel: OutputChannel) {
this.channel = channel;
}
/** 当前进程状态。 */
GetState(): EnvironmentToolState {
return this.state;
}
/** 缓存文件路径(交给 VET 自己维护)。 */
GetCachePath(): string {
return path.join(this.storageDir, 'vet-cache.json');
}
/** 当前配置里的 VET 可执行文件路径。 */
GetExecutablePath(): string {
return this.ReadConfig().exePath;
}
/** 启动(已启动则先停掉再起)。 */
Start(): void {
this.StopProcess();
this.restarts = 0;
this.lastExitSummary = '';
this.Spawn();
}
/** 手动重启(命令入口):重置重启计数,让用户能自己解围。 */
Restart(): void {
this.AppendLog('[vscode] 手动重启 VET');
this.Start();
}
/**
* 手动停止(命令入口):停掉进程并置为 stopped(区别于配置禁用),不再自动重启。
* 之后用「重启 VET」即可恢复(Restart → Start 会重新 Spawn)。
* 说明:不清空已发现的环境与选中项 —— VET 只是发现器,关掉它不影响已经选定的编译器。
*/
Stop(): void {
this.AppendLog('[vscode] 手动停止 VET');
this.StopProcess();
this.restarts = 0;
this.SetState('stopped');
}
/** 追加一行日志(进 "Vipeye VET" 输出通道),供扩展其它部分记录与 VET 相关的信息。 */
AppendLog(line: string): void {
this.channel.appendLine(line);
}
/** 显示输出通道。 */
ShowLog(): void {
this.channel.show(true);
}
dispose(): void {
this.disposed = true;
this.StopProcess();
this._onDidChangeEnvironments.dispose();
this._onDidChangeState.dispose();
this.channel.dispose();
}
// ===== 进程生命周期 =====
private Spawn(): void {
if (this.disposed) {
return;
}
const config = this.ReadConfig();
if (!config.enabled) {
this.SetState('disabled');
this.AppendLog('[vscode] viper.environmentTool.enabled = false,不启动 VET');
return;
}
if (!fs.existsSync(config.exePath)) {
// 路径配错不会自己好,直接失败并提示,不做无意义的重启。
this.SetState('failed');
this.AppendLog(`[vscode] 找不到 VET 可执行文件: ${config.exePath}`);
void window
.showErrorMessage(
`VET 可执行文件不存在:${config.exePath}。请检查设置 viper.environmentTool.path。`,
'打开设置'
)
.then((picked) => {
if (picked === '打开设置') {
void commands.executeCommand('workbench.action.openSettings', 'viper.environmentTool.path');
}
});
return;
}
// VET 要把发现缓存写到该目录;目录不存在时缓存写失败(只告警),会导致每轮都从空缓存重扫。
try {
fs.mkdirSync(this.storageDir, { recursive: true });
} catch {
// 建不出目录不阻断启动(VET 仍能运行,只是缓存不可用)。
}
this.SetState('starting');
const args = this.BuildArgs(config);
this.AppendLog(`[vscode] 启动: ${config.exePath} ${args.join(' ')}`);
let child: ChildProcessWithoutNullStreams;
try {
child = spawn(config.exePath, args, { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
} catch (e) {
this.AppendLog(`[vscode] 启动失败: ${String(e)}`);
this.HandleUnexpectedExit(-1, 'spawn-error');
return;
}
this.process = child;
this.startedAt = Date.now();
this.lastConfirmAt = 0;
this.stdoutBuffer = '';
this.stderrBuffer = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => this.HandleStdout(chunk));
child.stderr.on('data', (chunk: string) => this.HandleStderr(chunk));
child.on('error', (err: Error) => {
this.AppendLog(`[vscode] 进程错误: ${err.message}`);
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
this.SetState('failed');
void window.showErrorMessage(`无法启动 VET(${config.exePath}):${err.message}`);
}
});
child.on('exit', (code, signal) => {
if (this.process === child) {
this.process = undefined;
}
const aliveMs = Date.now() - this.startedAt;
this.AppendLog(`[vscode] VET 退出: code=${code} signal=${signal ?? '-'} 存活 ${aliveMs} ms`);
if (this.disposed) {
return;
}
this.HandleUnexpectedExit(code ?? -1, signal ?? 'exit');
});
this.EnsureWatchdog();
this.SetState('running');
}
/** 组装 scan 子命令参数。 */
private BuildArgs(config: EnvironmentToolConfig): string[] {
const args = ['scan', '--cache', this.GetCachePath(), '--sleep', String(config.scanIntervalMs)];
if (config.confirmSeconds > 0) {
args.push('--need-confirm', String(config.confirmSeconds));
args.push('--need-confirm-timeout', String(config.confirmTimeoutMs));
}
// 一旦出现 --path,VET 就不再自动建常用目录表,所以仅在显式配置时追加。
for (const scanPath of config.scanPaths) {
if (scanPath && scanPath.trim().length > 0) {
args.push('--path', scanPath);
}
}
return args;
}
private StopProcess(): void {
if (this.restartTimer) {
clearTimeout(this.restartTimer);
this.restartTimer = undefined;
}
if (this.watchdogTimer) {
clearInterval(this.watchdogTimer);
this.watchdogTimer = undefined;
}
const child = this.process;
this.process = undefined;
if (!child) {
return;
}
this.AppendLog('[vscode] 请求停止 VET 进程');
child.stdout.removeAllListeners();
child.stderr.removeAllListeners();
child.removeAllListeners('exit');
child.kill();
}
/**
* 非主动退出:按「重启上限 + 指数退避」处理,并且每次都通知用户。
* @param code 退出码(-1 表示启动失败等非进程退出情形)
* @param reason 退出原因描述
*/
private HandleUnexpectedExit(code: number, reason: string): void {
const config = this.ReadConfig();
const aliveMs = Date.now() - this.startedAt;
if (aliveMs >= HealthyRunMs) {
this.restarts = 0;
}
const summary = `code=${code} reason=${reason}`;
if (summary !== this.lastExitSummary) {
this.lastExitSummary = summary;
void window
.showErrorMessage(`VET 异常退出(${summary})。`, '查看日志')
.then((picked) => {
if (picked === '查看日志') {
this.ShowLog();
}
});
}
if (this.restarts >= config.maxRestarts) {
this.SetState('failed');
this.AppendLog(
`[vscode] 已连续异常退出 ${this.restarts} 次,达到上限 ${config.maxRestarts},不再自动重启`
);
void window
.showErrorMessage(
`VET 已连续异常退出 ${this.restarts} 次(上限 ${config.maxRestarts}),不再自动重启。`,
'重启 VET',
'打开设置'
)
.then((picked) => {
if (picked === '重启 VET') {
this.Restart();
} else if (picked === '打开设置') {
void import('vscode').then((vscode) =>
vscode.commands.executeCommand('workbench.action.openSettings', 'viper.environmentTool')
);
}
});
return;
}
this.restarts++;
const delay = Math.min(30000, 1000 * 2 ** (this.restarts - 1));
this.SetState('restarting');
this.AppendLog(`[vscode] 第 ${this.restarts}/${config.maxRestarts} 次重启,${delay} ms 后重试`);
this.restartTimer = setTimeout(() => {
this.restartTimer = undefined;
this.Spawn();
}, delay);
}
// ===== stdout / stderr =====
private HandleStdout(chunk: string): void {
this.stdoutBuffer += chunk;
let newlineIndex = this.stdoutBuffer.indexOf('\n');
while (newlineIndex >= 0) {
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
if (line.length > 0) {
this.HandleStdoutLine(line);
}
newlineIndex = this.stdoutBuffer.indexOf('\n');
}
}
private HandleStdoutLine(line: string): void {
// stdout 是数据通道(每轮一行完整 JSON 数组)。原样进 "Vipeye VET",与 stderr 对照排查。
this.channel.appendLine(`[stdout] ${line}`);
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch {
// 非 JSON 的 stdout(例如 VET 的 [OOM] 文本)已在上面原样输出。
return;
}
if (Array.isArray(parsed)) {
this._onDidChangeEnvironments.fire(this.ToEnvironments(parsed));
return;
}
// {"exit":1, "msg":"timeout/ok!=1"} —— VET 因确认失败而终止。
const exitInfo = parsed as { exit?: number; msg?: string };
this.AppendLog(`[vscode] VET 报告退出: exit=${exitInfo.exit ?? '?'} msg=${exitInfo.msg ?? ''}`);
}
/** 把 VET 输出的 JSON 条目规整为强类型(字段名转 PascalCase,缺字段给空串)。 */
private ToEnvironments(entries: unknown[]): ViperEnvironment[] {
return entries
.filter((entry): entry is Record<string, unknown> => typeof entry === 'object' && entry !== null)
.map((entry) => this.ToEnvironment(entry))
.filter((environment) => environment.Id.length > 0 || environment.Executable.length > 0);
}
private ToEnvironment(entry: Record<string, unknown>): ViperEnvironment {
return {
Id: String(entry.id ?? ''),
Executable: String(entry.executable ?? ''),
Version: String(entry.version ?? ''),
Arch: String(entry.arch ?? ''),
Source: String(entry.source ?? ''),
DisplayName: String(entry.displayName ?? entry.executable ?? ''),
};
}
private HandleStderr(chunk: string): void {
this.stderrBuffer += chunk;
let newlineIndex = this.stderrBuffer.indexOf('\n');
while (newlineIndex >= 0) {
const line = this.stderrBuffer.slice(0, newlineIndex).replace(/\r$/, '');
this.stderrBuffer = this.stderrBuffer.slice(newlineIndex + 1);
if (line.length > 0) {
this.HandleStderrLine(line);
}
newlineIndex = this.stderrBuffer.indexOf('\n');
}
}
private HandleStderrLine(line: string): void {
// stderr 全部原样进 "Vipeye VET"(含 [scan] 进度行),不做过滤。
this.channel.appendLine(line);
// 每轮结束会提示进入确认窗口,这时才回 {"ok":1}(窗口只有几秒,必须立刻回)。
if (line.includes('本轮期限') || line.includes('[confirm] 请在')) {
this.SendConfirm();
}
}
/** 向 VET 的 stdin 写入一行 {"ok":1}。 */
private SendConfirm(): void {
const child = this.process;
if (!child || !child.stdin.writable) {
return;
}
// 提示是两行(请在… / 当前… 本轮期限…),间隔很短;去重避免同一窗口重复发送。
if (Date.now() - this.lastConfirmAt < MinConfirmIntervalMs) {
return;
}
this.lastConfirmAt = Date.now();
try {
child.stdin.write('{"ok":1}\n');
this.AppendLog('[vscode] 已发送心跳 {"ok":1}');
} catch (e) {
this.AppendLog(`[vscode] 心跳写入失败: ${String(e)}`);
}
}
/**
* 心跳兜底:万一没识别到确认提示(例如 VET 日志措辞变化),
* 超过「确认周期 + 5 秒」没有发送过就补一次,避免 VET 因超时自杀。
*/
private EnsureWatchdog(): void {
if (this.watchdogTimer) {
return;
}
this.watchdogTimer = setInterval(() => {
const config = this.ReadConfig();
if (config.confirmSeconds <= 0 || !this.process) {
return;
}
if (Date.now() - this.lastConfirmAt > (config.confirmSeconds + 5) * 1000) {
this.SendConfirm();
}
}, 2000);
}
// ===== 状态 =====
private SetState(next: EnvironmentToolState): void {
if (this.state === next) {
return;
}
this.state = next;
this._onDidChangeState.fire(next);
}
private ReadConfig(): EnvironmentToolConfig {
const config = workspace.getConfiguration('viper.environmentTool');
return {
enabled: config.get<boolean>('enabled', true),
exePath: config.get<string>('path', '').trim(),
scanPaths: config.get<string[]>('scanPaths', []),
confirmSeconds: config.get<number>('confirmSeconds', 10),
confirmTimeoutMs: config.get<number>('confirmTimeoutMs', 5000),
scanIntervalMs: config.get<number>('scanIntervalMs', 10000),
maxRestarts: Math.max(0, config.get<number>('maxRestarts', 5)),
};
}
}
interface EnvironmentToolConfig {
enabled: boolean;
exePath: string;
scanPaths: string[];
confirmSeconds: number;
confirmTimeoutMs: number;
scanIntervalMs: number;
maxRestarts: number;
}
+77 -9
View File
@@ -4,10 +4,12 @@
*/
import { commands, ExtensionContext, window, workspace } from 'vscode';
import { EnvironmentManager } from './environmentManager';
import { EnvironmentTool } from './environmentTool';
import { ProjectManager } from './projectManager';
import { ToolchainManager } from './toolchainManager';
import { ViperViewProvider } from './viperViewProvider';
import { StatusBarManager } from './statusBar';
import { EnvironmentStatusBar } from './statusBar';
import { ViperApiImpl } from './api';
import { VpjEditorProvider } from './vpjEditorProvider';
@@ -15,6 +17,12 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
const projects = new ProjectManager();
const toolchain = new ToolchainManager();
// VET(Viper Environment Tool)常驻进程:扫盘发现编译器,每轮输出全量 JSON 快照。
// stdout 与 stderr 都原样转发到该输出通道(不落盘)。
const vetChannel = window.createOutputChannel('Vipeye VET');
const environmentTool = new EnvironmentTool(context.globalStorageUri.fsPath, vetChannel);
const environments = new EnvironmentManager(context.globalState);
// 初始扫描项目文件。
await projects.DiscoverProjects();
@@ -22,19 +30,21 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
const watcher = projects.WatchProjectFiles();
context.subscriptions.push(watcher);
// 注册三个树视图。
const projectView = new ViperViewProvider('viper.projectView', projects, toolchain);
const toolchainView = new ViperViewProvider('viper.toolchainView', projects, toolchain);
const settingsView = new ViperViewProvider('viper.settingsView', projects, toolchain);
// 注册四个树视图。
const projectView = new ViperViewProvider('viper.projectView', projects, toolchain, environments, environmentTool);
const toolchainView = new ViperViewProvider('viper.toolchainView', projects, toolchain, environments, environmentTool);
const settingsView = new ViperViewProvider('viper.settingsView', projects, toolchain, environments, environmentTool);
const environmentView = new ViperViewProvider('viper.environmentView', projects, toolchain, environments, environmentTool);
context.subscriptions.push(
window.registerTreeDataProvider('viper.projectView', projectView),
window.registerTreeDataProvider('viper.toolchainView', toolchainView),
window.registerTreeDataProvider('viper.settingsView', settingsView)
window.registerTreeDataProvider('viper.settingsView', settingsView),
window.registerTreeDataProvider('viper.environmentView', environmentView)
);
// 状态栏。
context.subscriptions.push(new StatusBarManager(toolchain));
// 状态栏:只保留 VET 环境项(编译器位置完全由发现结果决定)。
context.subscriptions.push(new EnvironmentStatusBar(environments, environmentTool));
// 注册 .vpj 自定义编辑器。
context.subscriptions.push(VpjEditorProvider.Register(context));
@@ -50,6 +60,42 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
})
);
// VET 快照 → 环境清单(自己 diff;VET 每轮都给全量)。
context.subscriptions.push(
environmentTool.OnDidChangeEnvironments((snapshot) => {
const change = environments.ApplySnapshot(snapshot);
const summary = [`本轮: 共 ${change.Environments.length} 个`];
if (change.Added.length > 0) {
summary.push(`+${change.Added.length}`);
}
if (change.Removed.length > 0) {
summary.push(`-${change.Removed.length}`);
}
environmentTool.AppendLog(`[vscode] ${summary.join(' ')}`);
// 生效环境同步给工具链:有环境就用它(上次用的那个 → 否则发现的第一个),
// 一个都没发现时回退到配置/硬编码的兜底路径。
const selected = environments.GetSelectedEnvironment();
if (selected) {
toolchain.ApplyEnvironment(selected.Executable, selected.Version);
} else {
toolchain.ClearEnvironmentOverride();
}
}),
environments.OnDidChangeEnvironments(() => environmentView.Refresh()),
// 进程状态变化(含手动停止)会影响 Environments 视图的空态文案与状态栏。
environmentTool.OnDidChangeState(() => environmentView.Refresh()),
environments.OnDidChangeSelection((selected) => {
if (selected) {
toolchain.ApplyEnvironment(selected.Executable, selected.Version);
} else {
toolchain.ClearEnvironmentOverride();
}
environmentView.Refresh();
})
);
// 命令。
context.subscriptions.push(
RegisterCommandSafe(context, 'viper.createProject', async () => {
@@ -72,6 +118,19 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
projectView.Refresh();
toolchainView.Refresh();
settingsView.Refresh();
environmentView.Refresh();
}),
RegisterCommandSafe(context, 'viper.selectEnvironment', async (environmentId?: unknown) => {
await environments.SelectEnvironment(typeof environmentId === 'string' ? environmentId : undefined);
}),
RegisterCommandSafe(context, 'viper.restartEnvironmentTool', () => {
environmentTool.Restart();
}),
RegisterCommandSafe(context, 'viper.stopEnvironmentTool', () => {
environmentTool.Stop();
}),
RegisterCommandSafe(context, 'viper.showEnvironmentLog', () => {
environmentTool.ShowLog();
}),
RegisterCommandSafe(context, 'viper.openVpjAsJson', async (uri?: import('vscode').Uri) => {
const target = uri ?? window.activeTextEditor?.document.uri;
@@ -84,15 +143,24 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
})
);
// 监听配置变更,刷新工具链。
// 监听配置变更,刷新工具链 / 重启 VET。
context.subscriptions.push(
workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('viper.toolchain')) {
toolchain.Refresh();
}
if (e.affectsConfiguration('viper.environmentTool')) {
environmentTool.Restart();
}
})
);
// 生命周期:随扩展一起释放(dispose 会停掉 VET 进程并关闭日志流)。
context.subscriptions.push(environmentTool, environments);
// 启动 VET 扫盘发现。
environmentTool.Start();
// 导出 API。
const api = new ViperApiImpl(projects, toolchain);
return api;
+73 -17
View File
@@ -1,35 +1,91 @@
/*
* statusBar.ts
* 右下角状态栏:显示当前 Viper 编译器路径与版本。
* 右下角状态栏:显示 VET 发现的 Viper 环境与当前生效项。
*/
import { StatusBarAlignment, StatusBarItem, window } from 'vscode';
import { ToolchainManager } from './toolchainManager';
import { Disposable } from 'vscode';
import { Disposable, StatusBarAlignment, StatusBarItem, window } from 'vscode';
import { EnvironmentManager } from './environmentManager';
import { EnvironmentTool, EnvironmentToolState } from './environmentTool';
export class StatusBarManager implements Disposable {
/**
* 环境状态栏:显示 VET 发现/选中了哪个编译器,点击打开环境选择器。
* 这里只保留这一项:编译器位置完全由 VET 发现结果决定,不再显示硬编码路径。
*/
export class EnvironmentStatusBar implements Disposable {
private readonly item: StatusBarItem;
private readonly toolchain: ToolchainManager;
private readonly disposable: Disposable;
private readonly disposables: Disposable[] = [];
constructor(toolchain: ToolchainManager) {
this.toolchain = toolchain;
this.item = window.createStatusBarItem(StatusBarAlignment.Right, 100);
this.item.command = 'viper.selectToolchain';
constructor(private readonly environments: EnvironmentManager, private readonly tool: EnvironmentTool) {
this.item = window.createStatusBarItem(StatusBarAlignment.Right, 99);
this.item.command = 'viper.selectEnvironment';
this.disposables.push(
environments.OnDidChangeEnvironments(() => this.Update()),
environments.OnDidChangeSelection(() => this.Update()),
tool.OnDidChangeState(() => this.Update())
);
this.disposable = toolchain.OnDidChangeToolchain(() => this.Update());
this.Update();
this.item.show();
}
private Update(): void {
const tool = this.toolchain.GetToolchain();
this.item.text = `$(tools) Viper: ${tool.Path} (${tool.Version})`;
this.item.tooltip = `Viper Toolchain\nPath: ${tool.Path}\nVersion: ${tool.Version}\nSource: ${tool.Source}\nClick to change`;
const state = this.tool.GetState();
if (state === 'disabled') {
this.item.hide();
return;
}
const selected = this.environments.GetSelectedEnvironment();
const count = this.environments.GetEnvironments().length;
const stateText = DescribeState(state);
// 手动停止时点击直接把它拉起来(此时选择环境没有意义);其余情况点击打开环境选择器。
this.item.command = state === 'stopped' ? 'viper.restartEnvironmentTool' : 'viper.selectEnvironment';
if (state === 'stopped') {
this.item.text = '$(debug-stop) Viper 环境: 已关闭';
} else if (selected) {
this.item.text = `$(server-environment) Viper 环境: ${selected.DisplayName}`;
} else if (count > 0) {
this.item.text = `$(server-environment) Viper 环境: 未选择 (${count})`;
} else if (state === 'running' || state === 'starting') {
this.item.text = '$(sync~spin) Viper 环境: 扫描中…';
} else {
this.item.text = `$(warning) Viper 环境: ${stateText}`;
}
this.item.tooltip = [
`VET 状态: ${stateText}`,
`发现环境: ${count}`,
selected ? `当前选中: ${selected.DisplayName}\n${selected.Executable}` : '当前选中: (无)',
`VET: ${this.tool.GetExecutablePath()}`,
'输出: Vipeye VET 输出通道',
state === 'stopped' ? '点击重新启动 VET' : '点击选择环境',
].join('\n');
this.item.show();
}
dispose(): void {
this.item.dispose();
this.disposable.dispose();
for (const disposable of this.disposables) {
disposable.dispose();
}
}
}
function DescribeState(state: EnvironmentToolState): string {
switch (state) {
case 'running':
return '运行中';
case 'starting':
return '启动中';
case 'restarting':
return '重启中';
case 'stopped':
return '已关闭(手动)';
case 'failed':
return '已停止(连续失败)';
default:
return '未启用';
}
}
+28
View File
@@ -41,6 +41,11 @@ export class ToolchainManager {
/** 重新解析当前工具链(配置变更后调用)。 */
Refresh(): void {
// 环境覆盖优先:用户已经用 VET 选了环境时,配置变更不应把它顶掉。
if (this.current.Source === 'env') {
return;
}
const next = this.ResolveToolchain();
if (next.Path !== this.current.Path || next.Version !== this.current.Version) {
this.current = next;
@@ -48,6 +53,29 @@ export class ToolchainManager {
}
}
/**
* 用 VET 选中的环境覆盖当前工具链(仅内存,不写配置、不写 .vpj)。
* 这样现有的工具链视图/状态栏/API 都能立刻反映「我选的编译器」。
*/
ApplyEnvironment(path: string, version: string): void {
if (this.current.Source === 'env' && this.current.Path === path && this.current.Version === version) {
return;
}
this.current = { Path: path, Version: version, Source: 'env' };
this._onDidChange.fire(this.current);
}
/** 清除环境覆盖,回到「配置项 → 硬编码」的解析结果。 */
ClearEnvironmentOverride(): void {
if (this.current.Source !== 'env') {
return;
}
this.current = this.ResolveToolchain();
this._onDidChange.fire(this.current);
}
private ResolveToolchain(): ToolchainInfo {
const config = workspace.getConfiguration('viper.toolchain');
const configuredPath = config.get<string>('path');
+12
View File
@@ -32,6 +32,16 @@ export interface ToolchainInfo {
Source: 'hardcoded' | 'config' | 'env' | 'path';
}
/** VET(Viper Environment Tool)发现的一个编译器条目。字段名与 VET 的 stdout JSON 一致。 */
export interface ViperEnvironment {
Id: string; // VET 分配的稳定 id(GUID)
Executable: string; // 编译器可执行文件绝对路径
Version: string; // 版本字符串(来自 .PRVERSN 段与 --version 前缀)
Arch: string; // 架构,如 x86_64
Source: string; // 来源分类,如 workspace / global / ...
DisplayName: string; // 展示名,如「Viper 0.0.3-dev.428」
}
/** 暴露给其他扩展的 API。 */
export interface ViperApi {
/** 获取当前工作区的项目配置。 */
@@ -61,5 +71,7 @@ export interface ViperTreeNode {
Icon?: string;
Children?: ViperTreeNode[];
Command?: string;
/** 命令参数(点击该节点时随命令一起传入)。 */
CommandArguments?: unknown[];
ContextValue?: string;
}
+66 -4
View File
@@ -1,12 +1,14 @@
/*
* viperViewProvider.ts
* 左侧活动栏三个视图的 TreeDataProvider 实现。
* 左侧活动栏各视图的 TreeDataProvider 实现。
*/
import { Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { EnvironmentManager } from './environmentManager';
import { EnvironmentTool } from './environmentTool';
import { ProjectManager } from './projectManager';
import { ToolchainManager } from './toolchainManager';
import { ViperTreeNode } from './types';
import { ViperEnvironment, ViperTreeNode } from './types';
/** 通用树视图提供者,按视图 ID 返回不同节点集。 */
export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
@@ -16,7 +18,9 @@ export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
constructor(
private readonly viewId: string,
private readonly projects: ProjectManager,
private readonly toolchain: ToolchainManager
private readonly toolchain: ToolchainManager,
private readonly environments: EnvironmentManager,
private readonly tool: EnvironmentTool
) {}
Refresh(): void {
@@ -31,7 +35,7 @@ export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
item.iconPath = element.Icon;
}
if (element.Command) {
item.command = { command: element.Command, title: element.Label };
item.command = { command: element.Command, title: element.Label, arguments: element.CommandArguments };
}
if (element.ContextValue) {
item.contextValue = element.ContextValue;
@@ -54,6 +58,8 @@ export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
return this.BuildToolchainNodes();
case 'viper.settingsView':
return this.BuildSettingsNodes();
case 'viper.environmentView':
return this.BuildEnvironmentNodes();
default:
return [];
}
@@ -164,6 +170,62 @@ export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
];
}
/** 环境视图:按 VET 给出的 source 分组(workspace / global / ...)。 */
private BuildEnvironmentNodes(): ViperTreeNode[] {
const all = this.environments.GetEnvironments();
const selectedId = this.environments.GetSelectedId();
if (all.length === 0) {
// 手动关闭 VET 时不能显示"扫描中…"(那是误导),改为明确告知并引导重新启动。
const stopped = this.tool.GetState() === 'stopped';
return [
{
Label: stopped ? 'VET 已关闭' : '未发现 Viper 编译器',
Description: stopped ? '点击重新启动' : 'VET 扫描中…',
Tooltip: stopped ? 'VET 已被手动停止,点击重新启动扫描' : '点击可重启 VET 扫描',
Icon: stopped ? '$(debug-stop)' : '$(sync~spin)',
Command: 'viper.restartEnvironmentTool',
},
];
}
const groups = new Map<string, ViperEnvironment[]>();
for (const environment of all) {
const source = environment.Source || 'unknown';
const bucket = groups.get(source);
if (bucket) {
bucket.push(environment);
} else {
groups.set(source, [environment]);
}
}
const nodes: ViperTreeNode[] = [];
for (const [source, environments] of groups) {
nodes.push({
Label: source,
Description: `${environments.length}`,
Tooltip: `${environments.length} 个环境(来源:${source})`,
Icon: '$(folder)',
Children: environments.map((environment) => this.ConvertEnvironmentToNode(environment, selectedId)),
});
}
return nodes;
}
private ConvertEnvironmentToNode(environment: ViperEnvironment, selectedId: string | undefined): ViperTreeNode {
const isSelected = environment.Id === selectedId;
return {
Label: environment.DisplayName,
Description: `${environment.Version} · ${environment.Arch}${isSelected ? ' · 已选中' : ''}`,
Tooltip: `${environment.Executable}\n来源: ${environment.Source}\nID: ${environment.Id}`,
Icon: isSelected ? '$(check)' : '$(server-process)',
Command: 'viper.selectEnvironment',
CommandArguments: [environment.Id],
};
}
private ToCollapsibleState(children?: ViperTreeNode[]): TreeItemCollapsibleState {
return children && children.length > 0 ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None;
}
+91 -1
View File
@@ -116,6 +116,10 @@
"id": "viper.projectView",
"name": "Project"
},
{
"id": "viper.environmentView",
"name": "Environments"
},
{
"id": "viper.toolchainView",
"name": "Toolchain"
@@ -267,6 +271,30 @@
"title": "Open Settings",
"category": "Viper"
},
{
"command": "viper.selectEnvironment",
"title": "Select Environment",
"category": "Viper",
"icon": "$(server-environment)"
},
{
"command": "viper.restartEnvironmentTool",
"title": "Restart VET",
"category": "Viper",
"icon": "$(debug-restart)"
},
{
"command": "viper.stopEnvironmentTool",
"title": "Stop VET",
"category": "Viper",
"icon": "$(debug-stop)"
},
{
"command": "viper.showEnvironmentLog",
"title": "Show VET Output",
"category": "Viper",
"icon": "$(output)"
},
{
"command": "viper.refreshView",
"title": "Refresh",
@@ -325,6 +353,18 @@
"command": "viper.showFunctionFlowChart",
"when": "editorLangId == viper"
},
{
"command": "viper.selectEnvironment"
},
{
"command": "viper.restartEnvironmentTool"
},
{
"command": "viper.stopEnvironmentTool"
},
{
"command": "viper.showEnvironmentLog"
},
{
"command": "viper.openVpjAsJson",
"when": "resourceExtname == .vpj"
@@ -347,7 +387,7 @@
"view/title": [
{
"command": "viper.refreshView",
"when": "view == viper.projectView || view == viper.toolchainView || view == viper.settingsView",
"when": "view == viper.projectView || view == viper.environmentView || view == viper.toolchainView || view == viper.settingsView",
"group": "navigation"
}
]
@@ -364,6 +404,56 @@
"type": "object",
"title": "Vipeye",
"properties": {
"viper.environmentTool.enabled": {
"type": "boolean",
"default": true,
"description": "启用 VET(Viper Environment Tool)常驻扫盘发现 Viper 编译器。",
"scope": "window"
},
"viper.environmentTool.path": {
"type": "string",
"default": "D:\\Users\\TermiNexus\\Desktop\\TransPyC\\VET\\.tpv_build\\output\\VET.exe",
"description": "VET 可执行文件路径(Viper 自身实现的环境发现工具)。",
"scope": "window"
},
"viper.environmentTool.scanPaths": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "传给 VET 的 --path 扫描目录。留空则让 VET 自行建立常用目录表;一旦填写则完全替换它(不再自动建表)。",
"scope": "window"
},
"viper.environmentTool.confirmSeconds": {
"type": "number",
"default": 10,
"minimum": 0,
"description": "VET 心跳确认周期(秒,对应 --need-confirm);0 表示关闭确认(不建议:关闭后 VET 不会因超时自杀)。",
"scope": "window"
},
"viper.environmentTool.confirmTimeoutMs": {
"type": "number",
"default": 5000,
"minimum": 0,
"description": "VET 每轮确认窗口长度(毫秒,对应 --need-confirm-timeout);窗口内没收到 {\"ok\":1} 就会退出。",
"scope": "window"
},
"viper.environmentTool.scanIntervalMs": {
"type": "number",
"default": 10000,
"minimum": 0,
"maximum": 86400000,
"description": "VET 每轮扫盘间隔(毫秒,对应 --sleep)。",
"scope": "window"
},
"viper.environmentTool.maxRestarts": {
"type": "number",
"default": 5,
"minimum": 0,
"description": "VET 异常退出后的自动重启次数上限(指数退避,单次最多等 30 秒);超过后通知错误并停止重启。",
"scope": "window"
},
"viper.analysis.autoImportCompletions": {
"type": "boolean",
"default": true,