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:
1 parent
b0f4b8b5fd
commit
5d6fcf7c82
9 files changed
+1059
-32
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 '未启用';
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user