终端集成
终端是开发者与工具链之间的最后一道桥。插件通过 window.createTerminal 创建并控制终端,可以自动执行构建、安装依赖、启动服务等任何命令行操作,把重复的手动敲击变成一条命令。
创建终端
vscode.window.createTerminal 是终端操作的入口,返回一个 Terminal 对象:
import * as vscode from 'vscode';
// 最小创建:只有名称,使用系统默认 shell
const term = vscode.window.createTerminal('构建终端');
term.show(); // 立即显示并聚焦常用参数
| 参数 | 类型 | 说明 |
|---|---|---|
name | string | 终端标题,显示在终端标签上 |
shellPath | string | 指定 shell 可执行文件路径,如 C:\Windows\System32\cmd.exe |
shellArgs | string[] | 传给 shell 的参数数组 |
cwd | string | 终端启动时的工作目录 |
env | Record<string, string> | 注入额外的环境变量 |
hideFromUser | boolean | 创建后立即隐藏,配合 sendText 静默执行 |
isTransient | boolean | 关闭后自动清理,不驻留在终端列表中 |
import * as vscode from 'vscode';
import * as os from 'os';
import * as path from 'path';
// 带完整配置的终端:指定 shell、工作目录与环境变量
const buildTerm = vscode.window.createTerminal({
name: 'Node 构建',
shellPath: os.platform() === 'win32'
? path.join(process.env.ProgramFiles || 'C:/Program Files', 'nodejs/node.exe')
: '/usr/bin/node',
cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
env: {
NODE_ENV: 'production',
MY_PLUGIN_FLAG: '1'
}
});
buildTerm.show();创建模式的差异
createTerminal 可以传字符串名或 TerminalOptions 对象。传字符串时其余配置全部走默认值;传对象时逐个字段生效。还有一个关键差异:终端创建是异步的,但 API 是同步返回句柄——Terminal 对象立即可用,只是内部的 shell 进程还在启动中。
发送命令
terminal.sendText(text, addNewLine) 向终端写入命令。addNewLine 默认为 true,会在末尾自动追加回车(等价于按下 Enter),控制台命令通常依赖这一点触发执行:
const term = vscode.window.createTerminal('命令执行');
term.show();
// 第二条命令不换行:拼接在同一行
term.sendText('echo "abc"', false);
term.sendText(' && echo "def"', false);
term.sendText('', true); // 手动回车触发执行多命令换行分隔
sendText 支持在字符串内直接使用 \n 或 \r\n 换行,一次调用发送多条命令:
// 三条命令一次发送:进入目录、安装依赖、启动服务
term.sendText(`cd ${projectPath}\nnpm install\nnpm run dev`);注意换行符的分隔语义:\n 在终端中等价于按一次 Enter,命令会逐条执行,这与 addNewLine 参数无关——addNewLine 只在字符串末尾追加回车。
等待命令完成
sendText 本身不返回 Promise,无法直接知道命令何时结束。常见的做法是监听输出或使用轮询:
import * as vscode from 'vscode';
async function runWithOutput(term: vscode.Terminal, command: string): Promise<void> {
// 监听终端输出,匹配完成标志
const listener = vscode.window.onDidChangeTerminalState((t) => {
// state.isInteractedWith 表示用户是否交互过,不是可靠完成标志
});
term.sendText(command);
// 更可靠的方案:用 processId 配合轮询,或使用 Task API(见任务系统篇)
await new Promise((resolve) => setTimeout(resolve, 1000));
listener.dispose();
}真正需要"命令执行完再做下一步"的场景,应优先使用 Task API(vscode.tasks.executeTask),它在任务完成时触发 onDidEndTask 事件。
获取进程 ID
terminal.processId 返回一个 Promise,解析为该终端 shell 进程的操作系统 PID,是连接终端与外部工具(如调试器、进程管理)的关键:
async function inspectTerminal(term: vscode.Terminal) {
try {
const pid = await term.processId;
if (pid !== undefined) {
vscode.window.showInformationMessage(`终端 ${term.name} 的进程 ID 是 ${pid}`);
// 拿到 PID 后可调用系统命令查询进程详情
// 例如 Windows: tasklist /FI "PID eq 1234"
}
} catch (err) {
vscode.window.showWarningMessage('无法获取进程 ID,终端可能已关闭');
}
}注意三点:
| 场景 | 行为 |
|---|---|
| 终端未启动完成 | Promise 挂起,直到进程就绪 |
| 终端已关闭 | 解析为 undefined |
| 扩展宿主侧 | 进程 ID 是宿主进程的 PID,不受终端关闭影响 |
终端生命周期事件
window 命名空间提供三个生命周期事件,让插件感知终端的开、关、切换:
| 事件 | 触发时机 |
|---|---|
onDidOpenTerminal | 任何终端创建后(包括用户手动开的) |
onDidCloseTerminal | 终端关闭时 |
onDidChangeActiveTerminal | 当前活动终端切换时 |
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 终端打开:记录并提示
context.subscriptions.push(
vscode.window.onDidOpenTerminal((term) => {
console.log(`终端打开: ${term.name}`);
})
);
// 终端关闭:释放相关资源
context.subscriptions.push(
vscode.window.onDidCloseTerminal((term) => {
console.log(`终端关闭: ${term.name}`);
})
);
// 活动终端切换
context.subscriptions.push(
vscode.window.onDidChangeActiveTerminal((term) => {
const name = term?.name ?? '无';
vscode.window.setStatusBarMessage(`当前终端: ${name}`, 3000);
})
);
}区分"自己创建的"终端
生命周期事件会收到所有终端,包括用户手动创建的。如果只想跟踪自己创建的终端,维护一个名称集合即可:
const managed = new Set<string>();
function createManagedTerminal(name: string): vscode.Terminal {
const term = vscode.window.createTerminal(name);
managed.add(term.name);
return term;
}
vscode.window.onDidCloseTerminal((term) => {
if (managed.delete(term.name)) {
console.log(`我创建的终端 ${term.name} 已关闭`);
}
});关闭与清理
terminal.dispose() 关闭终端并回收资源。关闭后继续调用 sendText 会静默失败,应在使用前检查终端状态:
import * as vscode from 'vscode';
export function closeTerminalsByName(names: string[]) {
vscode.window.terminals.forEach((term) => {
if (names.includes(term.name)) {
term.dispose();
}
});
}
// 关闭全部由本插件管理的终端
export function closeManaged() {
vscode.window.terminals
.filter((t) => t.name.startsWith('插件·'))
.forEach((t) => t.dispose());
}vscode.window.terminals 是当前所有活动终端的数组,可用于遍历管理。
综合示例:一键执行脚本
把上述 API 串成一个完整插件:状态栏按钮一键在终端中运行 npm 脚本,命令面板支持选择脚本。
package.json
{
"name": "quick-run-terminal",
"displayName": "Quick Run in Terminal",
"version": "0.1.0",
"engines": { "vscode": "^1.80.0" },
"categories": ["Other"],
"activationEvents": ["onStartupFinished"],
"main": "./out/extension.js",
"contributes": {
"commands": [
{ "command": "quickrun.dev", "title": "在终端运行: npm run dev" },
{ "command": "quickrun.build", "title": "在终端运行: npm run build" },
{ "command": "quickrun.choose", "title": "在终端运行: 选择脚本..." }
],
"menus": {
"commandPalette": [
{ "command": "quickrun.dev", "when": "workspaceFolderCount > 0" },
{ "command": "quickrun.build", "when": "workspaceFolderCount > 0" },
{ "command": "quickrun.choose", "when": "workspaceFolderCount > 0" }
]
}
},
"scripts": { "compile": "tsc -p ./" },
"devDependencies": {
"@types/vscode": "^1.80.0",
"@types/node": "^18.0.0",
"typescript": "^5.0.0"
}
}extension.ts
import * as vscode from 'vscode';
import * as path from 'path';
// 全局复用同一个终端,避免每次命令都新建
let sharedTerminal: vscode.Terminal | undefined;
// 获取工作区根目录(取第一个文件夹)
function workspaceRoot(): string | undefined {
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
}
// 获取或创建共享终端
function getSharedTerminal(): vscode.Terminal {
if (!sharedTerminal || sharedTerminal.exitStatus) {
// 终端已关闭(exitStatus 非空)时重建
sharedTerminal = vscode.window.createTerminal('QuickRun');
}
return sharedTerminal;
}
// 在终端中执行 npm 脚本
function runNpmScript(script: string) {
const root = workspaceRoot();
if (!root) {
vscode.window.showWarningMessage('请先打开一个工作区文件夹');
return;
}
const term = getSharedTerminal();
term.show(true); // 聚焦终端
// 进入项目根目录后执行脚本
term.sendText(`cd "${root}"`);
term.sendText(`npm run ${script}`);
}
export function activate(context: vscode.ExtensionContext) {
// 状态栏按钮:一键运行 npm run dev
const devButton = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
100
);
devButton.text = '$(play) 运行 dev';
devButton.tooltip = '在终端中运行 npm run dev';
devButton.command = 'quickrun.dev';
devButton.show();
// 注册三个命令
context.subscriptions.push(
vscode.commands.registerCommand('quickrun.dev', () => runNpmScript('dev')),
vscode.commands.registerCommand('quickrun.build', () => runNpmScript('build'))
);
// 命令面板:QuickPick 选择要运行的脚本
context.subscriptions.push(
vscode.commands.registerCommand('quickrun.choose', async () => {
const scripts = ['dev', 'build', 'test', 'lint', 'preview'];
const picked = await vscode.window.showQuickPick(scripts, {
placeHolder: '选择要运行的 npm 脚本',
title: '在终端中运行'
});
if (picked) {
runNpmScript(picked);
}
})
);
// 终端生命周期兜底:共享终端被关闭时置空,下次命令自动重建
context.subscriptions.push(
vscode.window.onDidCloseTerminal((term) => {
if (term === sharedTerminal) {
sharedTerminal = undefined;
}
})
);
context.subscriptions.push(devButton);
}
export function deactivate() {}关键点回顾
| API | 用途 |
|---|---|
createTerminal | 创建终端,可配 shell、cwd、env |
sendText | 发送命令,\n 分隔多条,addNewLine 控制末尾回车 |
processId | 异步获取 shell 进程 PID |
onDidOpen/Close/ChangeActiveTerminal | 感知终端开、关、切换 |
dispose / exitStatus | 关闭终端、判断终端是否已退出 |
整个流程:状态栏或命令面板触发命令 → 获取共享终端 → 切换到项目根目录 → 发送 npm 脚本。重复的手工操作被压缩成一次点击。