终端高级操作
基础篇解决了"创建终端、发命令、听事件",本篇深入终端的进阶能力:两类终端配置的差异、在终端输出中嵌入可点击链接、监听终端输出流,以及多终端的精细化管理。
两类终端配置
createTerminal 接受两类配置对象:TerminalOptions 与 ExtensionTerminalOptions。它们的本质区别在于谁在跑 shell。
| 类型 | 特点 | 适用场景 |
|---|---|---|
TerminalOptions | 由 VS Code 内部启动 shell 进程(真正的终端) | 绝大多数场景:跑命令、跑脚本 |
ExtensionTerminalOptions | shell 进程由插件自己启动,VS Code 只负责渲染输入输出 | 对接 REPL、交互式程序、自定义协议 |
TerminalOptions 字段
import * as vscode from 'vscode';
const term = vscode.window.createTerminal({
name: 'Gradle 构建',
shellPath: '/usr/bin/zsh', // shell 可执行文件
shellArgs: ['--login'], // shell 参数
cwd: '/path/to/project', // 工作目录
env: { JAVA_HOME: '/usr/lib/jvm/17' }, // 注入环境变量
hideFromUser: true, // 创建后不主动显示
isTransient: true, // 命令结束后自动清理
location: vscode.TerminalLocation.Panel, // 出现在终端面板
iconPath: vscode.Uri.file('/icons/build.png'), // 终端标签图标
color: new vscode.ThemeColor('terminal.ansiGreen'), // 终端强调色
message: '构建准备中...', // 创建时显示的消息
strictEnv: false // 是否只用 env 字段,丢弃继承的环境
});| 字段 | 说明 |
|---|---|
name | 终端标题 |
shellPath / shellArgs | shell 路径与启动参数 |
cwd | 启动目录 |
env | 附加环境变量,默认与继承的环境合并 |
strictEnv | true 时忽略继承环境,只保留 env 声明的变量 |
hideFromUser | 创建后立即隐藏,配合后台命令使用 |
isTransient | 标记为临时终端,命令执行完自动回收 |
location | 终端出现的位置(面板或编辑器区) |
iconPath / color | 终端标签的图标与强调色 |
message | 终端创建后立即回显的消息 |
ExtensionTerminalOptions 字段
import * as vscode from 'vscode';
import { spawn, ChildProcess } from 'child_process';
// 插件自己启动 node REPL 进程,交给 VS Code 渲染
const child: ChildProcess = spawn('node', ['-i'], {
stdio: ['pipe', 'pipe', 'pipe']
});
const term = vscode.window.createTerminal({
name: 'Node REPL',
pty: {
onDidWrite: (data) => {
// 收到键盘输入(含回车)回调
child.stdin.write(data);
},
open: () => {
// 伪终端打开,可以写入欢迎信息
child.stdout.on('data', (chunk) => this.write(chunk.toString()));
child.stderr.on('data', (chunk) => this.write(chunk.toString()));
},
close: () => {
// 用户关闭终端,终止子进程
child.kill();
},
handleInput: (data) => {
// 可选:处理鼠标/控制序列输入
},
setDimensions: (cols, rows) => {
// 可选:终端尺寸变化
}
}
});核心差异:ExtensionTerminalOptions 没有 shellPath、cwd、env 等字段,取而代之的是一个 pty 对象,插件通过它把子进程的输入输出桥接到终端 UI。
终端内链接识别
window.registerTerminalLinkProvider 让插件在终端输出文本中识别自定义链接,用户点击时执行动作。典型场景:识别错误日志中的文件路径、任务 ID、哈希值。
注册链接提供者
import * as vscode from 'vscode';
import * as path from 'path';
const provider: vscode.TerminalLinkProvider = {
// 在终端行文本中查找候选链接
provideTerminalLinks(context: vscode.TerminalLinkContext) {
const line = context.line;
// 匹配形如 issue/1234 的文本
const regex = /\bissue\/(\d{3,6})\b/g;
const links: vscode.TerminalLink[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(line)) !== null) {
links.push({
// 起始与结束位置(按字符索引,不含换行符)
startIndex: match.index,
length: match[0].length,
// 自定义数据,handleTerminalLink 里取出
tooltip: `打开 Issue #${match[1]}`,
data: match[1]
});
}
return links;
},
// 用户点击链接时回调
async handleTerminalLink(link: vscode.TerminalLink) {
const issueId = link.data as string;
const uri = vscode.Uri.parse(
`https://github.com/myorg/myrepo/issues/${issueId}`
);
await vscode.env.openExternal(uri);
}
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.window.registerTerminalLinkProvider(provider)
);
}链接位置计算
startIndex 与 length 是链接在行文本中的字符偏移。注意多字节字符:VS Code 按 UTF-16 code unit 计算偏移,中文等字符占两个 unit。复杂场景下应遍历字符串并正确累加偏移:
function findLinks(line: string): vscode.TerminalLink[] {
const links: vscode.TerminalLink[] = [];
// 匹配相对路径加行号,如 src/main.ts:42
const regex = /([\w./-]+\.\w+):(\d+)/g;
let m: RegExpExecArray | null;
while ((m = regex.exec(line)) !== null) {
const startIndex = m.index;
const length = m[0].length;
links.push({
startIndex,
length,
tooltip: `打开文件 ${m[1]} 第 ${m[2]} 行`,
data: { file: m[1], line: parseInt(m[2], 10) }
});
}
return links;
}显示、隐藏与聚焦
terminal.show() 控制终端的可见性:
| 调用 | 行为 |
|---|---|
show() | 显示终端并聚焦到终端面板 |
show(true) | 显示并抢占焦点(从编辑区切走) |
show(false) | 显示但不抢焦点,后台运行 |
hide() | 隐藏终端(不关闭进程) |
dispose() | 真正关闭终端 |
import * as vscode from 'vscode';
let bgTerm: vscode.Terminal | undefined;
// 后台启动构建:隐藏执行,完成后弹窗通知
export function startBackgroundBuild() {
bgTerm = vscode.window.createTerminal({
name: '后台构建',
hideFromUser: true,
isTransient: true
});
bgTerm.sendText('npm run build');
vscode.window.showInformationMessage('构建已在后台启动');
}
// 需要看输出时再调出
export function revealBackgroundBuild() {
if (bgTerm) {
bgTerm.show(true);
}
}hideFromUser: true 与 show(false) 的区别:前者在创建时就不打扰用户,后者在已有可见终端时再隐藏。后台任务完成后配合 onDidChangeTerminalState 或定时检查 exitStatus 通知用户结果。
多终端管理
工作区可能同时存在多个终端,插件需要区分和管理它们:
import * as vscode from 'vscode';
export function listTerminals(): string[] {
return vscode.window.terminals.map((t) => `${t.name} (${t.creationOptions.name ?? '默认'})`);
}
// 按名称查找终端
export function findTerminal(name: string): vscode.Terminal | undefined {
return vscode.window.terminals.find((t) => t.name === name);
}
// 聚焦指定名称的终端
export function focusTerminal(name: string): boolean {
const term = findTerminal(name);
if (term) {
term.show(true);
return true;
}
return false;
}
// 关闭除指定名称外的所有终端
export function closeOthers(keep: string) {
vscode.window.terminals.forEach((t) => {
if (t.name !== keep) {
t.dispose();
}
});
}终端分组
terminal.createTerminal 的 location 支持 TerminalLocation.Editor,让终端像编辑器一样打开成标签页,适合需要同时观察多个输出的场景:
const term = vscode.window.createTerminal({
name: '日志监控',
location: vscode.TerminalLocation.Editor
});
term.show();监听终端输出
onDidWriteData 在终端有数据写入时触发,参数是写入的原始文本。这是解析命令输出、检测完成标志的途径:
import * as vscode from 'vscode';
export function watchTerminalOutput(term: vscode.Terminal) {
// 一次性监听:匹配 "BUILD SUCCESSFUL" 后通知
const listener = vscode.window.onDidWriteTerminalData((e) => {
// e.terminal 判断是哪台终端
if (e.terminal !== term) return;
const lines = e.data.split(/\r?\n/);
for (const line of lines) {
if (line.includes('BUILD SUCCESSFUL')) {
vscode.window.showInformationMessage('构建成功!');
listener.dispose(); // 只监听一次
} else if (line.includes('BUILD FAILED')) {
vscode.window.showErrorMessage('构建失败,详见终端输出');
listener.dispose();
}
}
});
}onDidWriteTerminalData 的事件对象结构:
| 字段 | 说明 |
|---|---|
terminal | 产生数据的终端对象 |
data | 写入的文本块,可能是半行(流式输出) |
拼接半行输出
终端输出是流式的,一行可能被拆成多次 onDidWriteTerminalData。要可靠地匹配"整行"内容,需要自行拼接缓冲:
class OutputWatcher {
private buffer = '';
private listener: vscode.Disposable;
constructor(term: vscode.Terminal, private onLine: (line: string) => void) {
this.listener = vscode.window.onDidWriteTerminalData((e) => {
if (e.terminal !== term) return;
this.buffer += e.data;
// 按行切分,保留最后的不完整片段
const parts = this.buffer.split(/\r?\n/);
this.buffer = parts.pop() ?? '';
for (const line of parts) {
this.onLine(line);
}
});
}
dispose() {
this.listener.dispose();
}
}
// 使用:等待 "ready" 字样
const watcher = new OutputWatcher(term, (line) => {
if (line.includes('ready')) {
console.log('服务已就绪');
}
});注意 \r\n 是 Windows 终端的标准换行,split(/\r?\n/) 兼容两种换行。
综合示例:构建监控器
一个综合示例:后台执行构建,实时解析输出,构建失败时自动在终端中高亮错误位置并通知用户。
import * as vscode from 'vscode';
import * as path from 'path';
class BuildMonitor {
private term: vscode.Terminal | undefined;
private buffer = '';
private watcher: vscode.Disposable | undefined;
// 启动构建(后台)
startBuild() {
const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!root) return;
// 复用旧终端,否则新建
this.term = this.term && !this.term.exitStatus
? this.term
: vscode.window.createTerminal({
name: 'Build Monitor',
hideFromUser: true,
isTransient: true
});
// 挂载输出监听
this.watcher?.dispose();
this.buffer = '';
this.watcher = vscode.window.onDidWriteTerminalData((e) => {
if (e.terminal !== this.term) return;
this.buffer += e.data;
const parts = this.buffer.split(/\r?\n/);
this.buffer = parts.pop() ?? '';
for (const line of parts) {
this.onBuildLine(line);
}
});
this.term.sendText(`cd "${root}" && npm run build`);
vscode.window.showInformationMessage('构建已开始(后台运行)');
}
// 解析每一行输出
private onBuildLine(line: string) {
// 错误行:ERROR in ./src/foo.ts:12:5
const errMatch = line.match(/ERROR in (.+):(\d+):(\d+)/);
if (errMatch) {
const [, filePath, lineNo, colNo] = errMatch;
const uri = vscode.Uri.file(
path.isAbsolute(filePath)
? filePath
: path.join(vscode.workspace.workspaceFolders![0].uri.fsPath, filePath)
);
// 打开错误文件并定位
vscode.window.showTextDocument(uri).then((editor) => {
const pos = new vscode.Position(parseInt(lineNo, 10) - 1, parseInt(colNo, 10) - 1);
editor.selection = new vscode.Selection(pos, pos);
editor.revealRange(editor.selection);
});
vscode.window.showErrorMessage(`构建失败:${filePath}:${lineNo}`);
}
// 成功标志
if (/built in|compiled successfully/i.test(line)) {
vscode.window.showInformationMessage('构建成功');
}
}
// 手动查看输出
reveal() {
this.term?.show(true);
}
dispose() {
this.watcher?.dispose();
}
}
export function activate(context: vscode.ExtensionContext) {
const monitor = new BuildMonitor();
context.subscriptions.push(
vscode.commands.registerCommand('buildmonitor.start', () => monitor.startBuild()),
vscode.commands.registerCommand('buildmonitor.reveal', () => monitor.reveal()),
monitor
);
}
export function deactivate() {}要点串联:hideFromUser 让构建在后台静默运行,onDidWriteTerminalData 流式解析输出,失败时用 showTextDocument 把用户带到出错的文件位置,成功时弹窗提示——终端变成了插件的"耳朵"和"手"。