diff --git a/src/pseudoTerminal.ts b/src/pseudoTerminal.ts index 5debc33..43e7fee 100644 --- a/src/pseudoTerminal.ts +++ b/src/pseudoTerminal.ts @@ -1,92 +1,207 @@ +// 导入 EnvironmentVariable 和 FileSystemConfig 类型,用于定义环境变量和文件系统配置 import type { EnvironmentVariable, FileSystemConfig } from 'common/fileSystemConfig'; +// 导入 path 模块,用于处理路径 import * as path from 'path'; +// 导入 ClientChannel 和 PseudoTtyOptions 类型,用于定义客户端通道和伪终端选项 import type { ClientChannel, PseudoTtyOptions } from 'ssh2'; +// 导入 vscode 模块,用于访问 VS Code 的 API import * as vscode from 'vscode'; +// 导入 getFlagBoolean 函数,用于获取布尔类型的标志 import { getFlagBoolean } from './flags'; +// 导入 Connection 类型,用于定义连接 import type { Connection } from './connection'; +// 导入 Logging 和 LOGGING_NO_STACKTRACE 常量,用于日志记录 import { Logging, LOGGING_NO_STACKTRACE } from './logging'; +// 导入 environmentToExportString、joinCommands、mergeEnvironment 和 toPromise 函数,用于处理环境变量、命令拼接、环境合并和 Promise 转换 import { environmentToExportString, joinCommands, mergeEnvironment, toPromise } from './utils'; +// 定义伪终端的高度和宽度 const [HEIGHT, WIDTH] = [480, 640]; +// 定义伪终端的选项对象 const PSEUDO_TTY_OPTIONS: Partial = { - height: HEIGHT, width: WIDTH, term: 'xterm-256color', + // 伪终端的高度 + height: HEIGHT, + // 伪终端的宽度 + width: WIDTH, + // 伪终端的类型 + term: 'xterm-256color', }; +/** + * 定义一个 SSH 伪终端接口,继承自 vscode.Pseudoterminal 接口 + * 该接口扩展了一些额外的属性和方法,用于处理 SSH 连接相关的操作 + */ export interface SSHPseudoTerminal extends vscode.Pseudoterminal { - onDidClose: vscode.Event; // Redeclaring that it isn't undefined + /** + * 当终端关闭时触发的事件 + * 该事件会返回一个数字,表示终端关闭的原因 + */ + onDidClose: vscode.Event; // 重新声明它不是未定义的 + /** + * 当终端打开时触发的事件 + * 该事件不会返回任何值 + */ onDidOpen: vscode.Event; - handleInput(data: string): void; // We don't support/need read-only terminals for now + /** + * 处理输入数据的方法 + * 该方法会接收一个字符串,表示用户输入的数据 + * 我们目前不支持只读终端 + */ + handleInput(data: string): void; // 我们现在不支持/不需要只读终端 + /** + * 终端的状态 + * 可能的值有:'opening'(正在打开)、'open'(已打开)、'closed'(已关闭)、'wait-to-close'(等待关闭) + */ status: 'opening' | 'open' | 'closed' | 'wait-to-close'; + /** + * 与终端关联的连接对象 + * 该对象包含了与 SSH 服务器建立连接所需的信息 + */ connection: Connection; /** Could be undefined if it only gets created during psy.open() instead of beforehand */ + /** + * 可选的客户端通道对象 + * 如果在调用 createTerminal 之前没有创建,则该属性可能为 undefined + */ channel?: ClientChannel; /** Either set by the code calling createTerminal, otherwise "calculated" and hopefully found */ + /** + * 可选的 vscode.Terminal 对象 + * 该对象可能是在调用 createTerminal 时设置的,否则会根据需要“计算”并找到 + */ terminal?: vscode.Terminal; } + +/** + * 检查给定的终端对象是否是 SSHPseudoTerminal 类型。 + * + * @param terminal - 要检查的终端对象。 + * @returns 如果终端对象是 SSHPseudoTerminal 类型,则返回 true,否则返回 false。 + */ export function isSSHPseudoTerminal(terminal: vscode.Pseudoterminal): terminal is SSHPseudoTerminal { + // 将终端对象强制转换为 SSHPseudoTerminal 类型 const term = terminal as SSHPseudoTerminal; + // 检查转换后的对象是否具有 connection、status 和 handleInput 属性 return !!(term.connection && term.status && term.handleInput); } +/** + * 定义了创建终端时可以配置的选项。 + */ export interface TerminalOptions { + /** + * 与终端关联的连接对象。 + * 该对象包含了与 SSH 服务器建立连接所需的信息。 + */ connection: Connection; + /** + * 环境变量数组。 + * 这些变量将被设置在远程终端的环境中。 + */ environment?: EnvironmentVariable[]; /** If absent, this defaults to config.root if present, otherwise whatever the remote shell picks as default */ + /** + * 工作目录。 + * 如果不存在,默认值为配置中的根目录(如果存在),否则由远程 shell 选择默认目录。 + */ workingDirectory?: string; /** The command to run in the remote shell. If undefined, a (regular interactive) shell is started instead by running $SHELL*/ + /** + * 要在远程 shell 中运行的命令。 + * 如果未定义,则通过运行 $SHELL 启动一个(常规交互式)shell。 + */ command?: string; } +/** + * 替换字符串中的变量。 + * + * @param value - 要替换变量的字符串。 + * @param config - 文件系统配置对象。 + * @returns 替换变量后的字符串。 + */ export function replaceVariables(value: string, config: FileSystemConfig): string { return value.replace(/\$\{(.*?)\}/g, (str, match: string) => { + // 如果变量不是以 "remote" 开头,则直接返回原字符串 if (!match.startsWith('remote')) return str; // Our variables always start with "remote" // https://github.com/microsoft/vscode/blob/bebd06640734c37f6d5f1a82b13297ce1d297dd1/src/vs/workbench/services/configurationResolver/common/variableResolver.ts#L156 + // 从变量中提取键和参数 const [key, argument] = match.split(':') as [string, string?]; + // 获取当前文件的 URI const getFilePath = (): vscode.Uri => { + // 获取当前活动编辑器的文档 URI const uri = vscode.window.activeTextEditor?.document?.uri; + // 如果 URI 存在且协议为 ssh,则返回该 URI if (uri && uri.scheme === 'ssh') return uri; + // 如果 URI 存在但协议不是 ssh,则抛出错误 if (uri) throw new Error(`Variable ${str}: Active editor is not a ssh:// file`); + // 如果 URI 不存在,则抛出错误 throw new Error(`Variable ${str} can not be resolved. Please open an editor.`); } + // 获取当前文件所在的工作区文件夹的 URI const getFolderPathForFile = (): vscode.Uri => { + // 获取当前文件的 URI const filePath = getFilePath(); + // 从工作区中获取包含该文件的文件夹的 URI const uri = vscode.workspace.getWorkspaceFolder(filePath)?.uri; + // 如果找到了文件夹,则返回其 URI if (uri) return uri; + // 如果没有找到文件夹,则抛出错误 throw new Error(`Variable ${str}: can not find workspace folder of '${filePath}'.`); } + // 获取工作区中的所有文件夹 const { workspaceFolders = [] } = vscode.workspace; + // 过滤出所有以 "ssh" 开头的文件夹 const sshFolders = workspaceFolders.filter(ws => ws.uri.scheme === 'ssh'); + // 如果只有一个 "ssh" 文件夹,则返回该文件夹,否则返回 undefined const sshFolder = sshFolders.length === 1 ? sshFolders[0] : undefined; + // 获取指定参数的文件夹 URI const getFolderUri = (): vscode.Uri => { + // 获取当前工作区的所有文件夹,默认值为空数组 const { workspaceFolders = [] } = vscode.workspace; + // 如果参数存在 if (argument) { + // 在工作区文件夹中查找名称与参数匹配的文件夹 const uri = workspaceFolders.find(ws => ws.name === argument)?.uri; + // 如果找到了匹配的文件夹,并且其 URI 协议为 ssh,则返回该 URI if (uri && uri.scheme === 'ssh') return uri; + // 如果找到了匹配的文件夹,但 URI 协议不是 ssh,则抛出错误 if (uri) throw new Error(`Variable ${str}: Workspace folder '${argument}' is not a ssh:// folder`); + // 如果没有找到匹配的文件夹,则抛出错误 throw new Error(`Variable ${str} can not be resolved. No such folder '${argument}'.`); } + // 如果存在唯一的 ssh 文件夹,则返回其 URI if (sshFolder) return sshFolder.uri; + // 如果存在多个 ssh 文件夹,则抛出错误 if (sshFolders.length > 1) { throw new Error(`Variable ${str} can not be resolved in a multi ssh:// folder workspace. Scope this variable using ':' and a workspace folder name.`); } + // 如果没有找到 ssh 文件夹,则抛出错误 throw new Error(`Variable ${str} can not be resolved. Please open an ssh:// folder.`); }; + // 根据不同的键,返回不同的路径信息 switch (key) { + // 处理 remoteWorkspaceRoot 和 remoteWorkspaceFolder 变量,返回工作区根目录或文件夹的路径 case 'remoteWorkspaceRoot': case 'remoteWorkspaceFolder': return getFolderUri().path; + // 处理 remoteWorkspaceRootFolderName 和 remoteWorkspaceFolderBasename 变量,返回工作区根目录或文件夹的名称 case 'remoteWorkspaceRootFolderName': case 'remoteWorkspaceFolderBasename': return path.basename(getFolderUri().path); + // 处理 remoteFile 变量,返回当前文件的路径 case 'remoteFile': return getFilePath().path; + // 处理 remoteFileWorkspaceFolder 变量,返回当前文件所在工作区文件夹的路径 case 'remoteFileWorkspaceFolder': return getFolderPathForFile().path; + // 处理 remoteRelativeFile 变量,返回当前文件相对于工作区根目录或指定文件夹的路径 case 'remoteRelativeFile': if (sshFolder || argument) return path.relative(getFolderUri().path, getFilePath().path); return getFilePath().path; + // 处理 remoteRelativeFileDirname 变量,返回当前文件所在目录相对于工作区根目录或指定文件夹的路径 case 'remoteRelativeFileDirname': { const dirname = path.dirname(getFilePath().path); if (sshFolder || argument) { @@ -95,21 +210,28 @@ export function replaceVariables(value: string, config: FileSystemConfig): strin } return dirname; } + // 处理 remoteFileDirname 变量,返回当前文件所在目录的路径 case 'remoteFileDirname': return path.dirname(getFilePath().path); + // 处理 remoteFileExtname 变量,返回当前文件的扩展名 case 'remoteFileExtname': return path.extname(getFilePath().path); + // 处理 remoteFileBasename 变量,返回当前文件的文件名 case 'remoteFileBasename': return path.basename(getFilePath().path); + // 处理 remoteFileBasenameNoExtension 变量,返回当前文件的文件名(不包含扩展名) case 'remoteFileBasenameNoExtension': { const basename = path.basename(getFilePath().path); return (basename.slice(0, basename.length - path.extname(basename).length)); } + // 处理 remoteFileDirnameBasename 变量,返回当前文件所在目录的文件名 case 'remoteFileDirnameBasename': return path.basename(path.dirname(getFilePath().path)); + // 处理 remotePathSeparator 变量,返回路径分隔符(POSIX 风格) case 'remotePathSeparator': // Not sure if we even need/want this variable, but sure return path.posix.sep; + // 如果变量未被识别,则记录警告并返回原始字符串 default: const msg = `Unrecognized task variable '${str}' starting with 'remote', ignoring`; Logging.warning(msg, LOGGING_NO_STACKTRACE); @@ -119,174 +241,348 @@ export function replaceVariables(value: string, config: FileSystemConfig): strin }); } + +/** + * 递归地替换对象中的变量。 + * + * @param object - 要替换变量的对象。 + * @param handler - 用于替换变量的函数。 + * @returns 替换变量后的对象。 + */ export async function replaceVariablesRecursive(object: T, handler: (value: string) => string | Promise): Promise { + // 如果对象是字符串,则直接调用处理函数进行替换 if (typeof object === 'string') return handler(object) as any; + // 如果对象是数组,则递归地替换数组中的每个元素 if (Array.isArray(object)) return object.map(v => this.replaceVariablesRecursive(v, handler)) as any; + // 如果对象是对象且不是正则表达式或日期对象,则递归地替换对象中的每个键值对 if (typeof object == 'object' && object !== null && !(object instanceof RegExp) && !(object instanceof Date)) { // ^ Same requirements VS Code applies: https://github.com/microsoft/vscode/blob/bebd06640734c37f6d5f1a82b13297ce1d297dd1/src/vs/base/common/types.ts#L34 const result: any = {}; for (let key in object) { + // 递归地替换键和值 const value = await replaceVariablesRecursive(object[key], handler); key = await replaceVariablesRecursive(key, handler); result[key] = value; } return result; } + // 如果对象不符合上述条件,则直接返回原对象 return object; } +/** + * 创建一个 SSH 伪终端。 + * + * @param options - 终端选项。 + * @returns 一个 Promise,成功时解析为 SSHPseudoTerminal 实例。 + * @throws 如果创建终端失败,则抛出错误。 + */ export async function createTerminal(options: TerminalOptions): Promise { + // 从 options 中提取 connection 对象 const { connection } = options; + // 从 connection 中提取 actualConfig、client 和 shellConfig 对象 const { actualConfig, client, shellConfig } = connection; + // 创建一个用于发送数据的事件发射器 const onDidWrite = new vscode.EventEmitter(); + // 创建一个用于接收关闭事件的事件发射器 const onDidClose = new vscode.EventEmitter(); + // 创建一个用于接收打开事件的事件发射器 const onDidOpen = new vscode.EventEmitter(); + // 初始化终端对象为 undefined let terminal: vscode.Terminal | undefined; // Won't actually open the remote terminal until pseudo.open(dims) is called + // 创建一个 SSH 伪终端实例 const pseudo: SSHPseudoTerminal = { + // 终端的当前状态,初始值为 'opening' status: 'opening', + // 终端的连接对象 connection, + // 用于发送数据的事件发射器 onDidWrite: onDidWrite.event, + // 用于接收关闭事件的事件发射器 onDidClose: onDidClose.event, + // 用于接收打开事件的事件发射器 onDidOpen: onDidOpen.event, close() { + // 获取伪终端的当前状态和连接通道 const { channel, status } = pseudo; + // 如果伪终端已经关闭,则直接返回 if (status === 'closed') return; + // 如果连接通道存在,则发送关闭信号 if (channel) { + // 更新伪终端状态为关闭 pseudo.status = 'closed'; + // 发送 INT 信号 channel.signal!('INT'); + // 发送 SIGINT 信号 channel.signal!('SIGINT'); + // 写入控制字符 \x03,通常用于中断进程 channel.write('\x03'); + // 关闭连接通道 channel.close(); + // 将伪终端的连接通道设置为 undefined pseudo.channel = undefined; } + // 如果伪终端的状态为 'wait-to-close',则执行清理操作 if (status === 'wait-to-close') { + // 释放终端资源 pseudo.terminal?.dispose(); + // 将终端设置为 undefined pseudo.terminal = undefined; + // 更新伪终端状态为关闭 pseudo.status = 'closed'; + // 触发关闭事件 onDidClose.fire(0); } }, + /** + * 打开 SSH 伪终端。 + * + * @param dims - 终端的尺寸信息。 + * @remarks + * 这个方法会尝试打开一个远程终端会话,并在成功时触发 onDidOpen 事件。 + * 如果终端已经处于打开状态,此方法将不会执行任何操作。 + */ async open(dims) { + // 触发 onDidWrite 事件,发送正在连接的消息 onDidWrite.fire(`Connecting to ${actualConfig.label || actualConfig.name}...\r\n`); try { + // 检查是否需要使用 Windows 命令分隔符 const [useWinCmdSep] = getFlagBoolean('WINDOWS_COMMAND_SEPARATOR', shellConfig.isWindows, actualConfig.flags); + // 根据是否使用 Windows 命令分隔符设置分隔符 const separator = useWinCmdSep ? ' && ' : '; '; + // 初始化命令列表 let commands: string[] = []; + // 设置默认的 shell 命令 let SHELL = '$SHELL'; + // 如果是 Windows 环境,则使用指定的 shell if (shellConfig.isWindows) SHELL = shellConfig.shell; // Add exports for environment variables if needed + // 合并连接的环境变量和选项中的环境变量 const env = mergeEnvironment(connection.environment, options.environment); + // 将环境变量导出为字符串并添加到命令列表中 commands.push(environmentToExportString(env, shellConfig.setEnv)); // Beta feature to add a "code " command in terminals to open the file locally + // 如果启用了远程命令功能,则执行相应的操作 if (getFlagBoolean('REMOTE_COMMANDS', false, actualConfig.flags)[0] && shellConfig.setupRemoteCommands) { + // 获取远程命令配置并执行 const rcCmds = await shellConfig.setupRemoteCommands(connection); + // 如果有远程命令,则将其添加到命令列表中 if (rcCmds?.length) commands.push(joinCommands(rcCmds, separator)!); } // Push the actual command or (default) shell command with replaced variables + // 如果用户指定了命令,则将其添加到命令列表中 if (options.command) { + // 使用实际配置中的变量替换命令中的变量,并将结果添加到命令列表中 commands.push(replaceVariables(options.command.replace(/$SHELL/g, SHELL), actualConfig)); - } else { + } + // 如果用户没有指定命令,则使用配置中的终端命令 + else { + // 将实际配置中的终端命令使用分隔符连接起来 const tc = joinCommands(actualConfig.terminalCommand, separator); + // 如果连接后的终端命令存在,则使用它,否则使用默认的 shell 命令 let cmd = tc ? replaceVariables(tc.replace(/$SHELL/g, SHELL), actualConfig) : SHELL; + // 将最终确定的命令添加到命令列表中 commands.push(cmd); } // There isn't a proper way of setting the working directory, but this should work in most cases + // 从选项中获取工作目录,如果没有则使用实际配置中的根目录 let { workingDirectory } = options; + // 如果用户没有指定工作目录,则使用实际配置中的根目录 workingDirectory = workingDirectory || actualConfig.root; + // 使用分隔符连接命令列表,生成最终的命令字符串 let cmd = joinCommands(commands, separator)!; + // 检查是否指定了工作目录 if (workingDirectory) { + // 如果命令字符串中包含 ${workingDirectory},则替换为实际的工作目录 if (cmd.includes('${workingDirectory}')) { + // 将命令字符串中的 ${workingDirectory} 替换为实际的工作目录 cmd = cmd.replace(/\${workingDirectory}/g, workingDirectory); - } else { - // TODO: Maybe replace with `connection.home`? Especially with Windows not supporting ~ + } + // TODO: Maybe replace with `connection.home`? Especially with Windows not supporting ~ + // 如果命令字符串中不包含 ${workingDirectory} + else { + // 对于 Windows 系统,不支持 ~ 作为工作目录的前缀,因此抛出错误 if (workingDirectory.startsWith('~')) { if (shellConfig.isWindows) + // 在 Windows 系统中,不支持 ~ 作为工作目录的前缀,因此抛出错误 throw new Error(`Working directory '${workingDirectory}' starts with ~ for a Windows shell`); // So `cd "~/a/b/..." apparently doesn't work, but `~/"a/b/..."` does // `"~"` would also fail but `~/""` works fine it seems + // 在非 Windows 系统中,将 ~ 替换为用户的主目录 workingDirectory = `~/"${workingDirectory.slice(2)}"`; - } else { + } + // 如果工作目录不是以 / 开头(在 Windows 系统中),则将其包围在双引号中 + else { + // 如果是 Windows 系统,并且工作目录以 / 开头,后面跟着一个字母和冒号(例如 /C:) if (shellConfig.isWindows && workingDirectory.match(/^\/[a-zA-Z]:/)) + // 去掉工作目录最前面的斜杠 workingDirectory = workingDirectory.slice(1); + // 将工作目录包围在双引号中 workingDirectory = `"${workingDirectory}"`; + } + // 将 cd 命令和原命令连接起来,形成新的命令字符串 cmd = joinCommands([`cd ${workingDirectory}`, ...commands], separator)!; - } - } else { + + } + // 如果没有指定工作目录 + else { + // 从命令字符串中移除 ${workingDirectory} cmd = cmd.replace(/\${workingDirectory}/g, ''); } - const pseudoTtyOptions: Partial = { ...PSEUDO_TTY_OPTIONS, cols: dims?.columns, rows: dims?.rows }; + + // 根据提供的维度(dims)创建一个伪终端选项对象(pseudoTtyOptions),其中包含列数(cols)和行数(rows) + const pseudoTtyOptions: Partial = {...PSEUDO_TTY_OPTIONS, cols: dims?.columns, rows: dims?.rows }; + // 记录一条调试信息,指示正在为指定的连接配置名称启动 shell,并显示要执行的命令(cmd) Logging.debug(`Starting shell for ${connection.actualConfig.name}: ${cmd}`); + // 使用提供的命令(cmd)和伪终端选项(pseudoTtyOptions)在客户端上执行一个命令,并等待结果 const channel = await toPromise(cb => client.exec(cmd, { pty: pseudoTtyOptions }, cb)); + // 如果没有成功创建通道(channel),则抛出一个错误,指示无法创建远程终端 if (!channel) throw new Error('Could not create remote terminal'); + // 将创建的通道(channel)分配给伪终端对象(pseudo)的 channel 属性 pseudo.channel = channel; + // 获取当前时间,作为命令执行的开始时间 const startTime = Date.now(); + // 监听通道的退出事件 channel.once('exit', (code, signal, _, description) => { + // 记录一条调试信息,指示终端会话已关闭,并包含退出代码、信号、描述和伪终端的状态 Logging.debug`Terminal session closed: ${{ code, signal, description, status: pseudo.status }}`; + // 如果退出代码存在,并且终端在一秒内失败(如果这不是一个任务),则保持终端打开,以便用户看到错误 if (code && (Date.now() < startTime + 1000) && !options.command) { // Terminal failed within a second, let's keep it open for the user to see the error (if this isn't a task) + // 向终端写入错误代码和可能的信号信息 onDidWrite.fire(`Got error code ${code}${signal ? ` with signal ${signal}` : ''}\r\n`); + // 如果有额外的描述信息,也将其写入终端 if (description) onDidWrite.fire(`Extra info: ${description}\r\n`); + // 提示用户按任意键关闭终端 onDidWrite.fire('Press a key to close the terminal\r\n'); + // 提示用户可能还有更多的标准输出/标准错误信息 onDidWrite.fire('Possible more stdout/stderr below:\r\n'); + // 将伪终端的状态设置为等待关闭 pseudo.status = 'wait-to-close'; } else { + // 触发终端关闭事件,并传递退出代码 onDidClose.fire(code || 0); + // 将伪终端的状态设置为关闭 pseudo.status = 'closed'; } }); + // 监听通道的可读事件 channel.once('readable', () => { // Inform others (e.g. createTaskTerminal) that the terminal is ready to be used + // 如果伪终端的状态是“opening”,则将其状态更新为“open” if (pseudo.status === 'opening') pseudo.status = 'open'; + // 触发 onDidOpen 事件,表示终端已打开 onDidOpen.fire(); }); + // 监听通道的数据事件 channel.on('data', chunk => onDidWrite.fire(chunk.toString())); + // 监听通道的标准错误数据事件 channel.stderr!.on('data', chunk => onDidWrite.fire(chunk.toString())); // TODO: ^ Keep track of stdout's color, switch to red, output, then switch back? } catch (e) { + // 记录一条错误信息,指示在启动 SSH 终端时发生了错误,并包含错误的详细信息 Logging.error`Error starting SSH terminal:\n${e}`; + // 将错误信息写入终端,以便用户看到 onDidWrite.fire(`Error starting SSH terminal:\r\n${e}\r\n`); + // 触发终端关闭事件,并传递退出代码 1,表示终端因错误而关闭 onDidClose.fire(1); + // 将伪终端的状态设置为“closed”,表示终端已关闭 pseudo.status = 'closed'; + // 如果存在通道,则销毁它,以释放资源 pseudo.channel?.destroy(); + // 将通道设置为 undefined,表明当前没有活动的通道 pseudo.channel = undefined; } - }, - get terminal(): vscode.Terminal | undefined { - return terminal ||= vscode.window.terminals.find(t => 'pty' in t.creationOptions && t.creationOptions.pty === pseudo); - }, - set terminal(term: vscode.Terminal | undefined) { - terminal = term; - }, - setDimensions(dims) { - pseudo.channel?.setWindow!(dims.rows, dims.columns, HEIGHT, WIDTH); - }, - handleInput(data) { - if (pseudo.status === 'wait-to-close') return pseudo.close(); - pseudo.channel?.write(data); - }, + }, + /** + * 获取与伪终端关联的 vscode.Terminal 实例。 + * 如果实例不存在,则在窗口的终端列表中查找。 + * @returns {vscode.Terminal | undefined} 找到的终端实例,如果没有找到则返回 undefined。 + */ + get terminal(): vscode.Terminal | undefined { + return terminal ||= vscode.window.terminals.find(t => 'pty' in t.creationOptions && t.creationOptions.pty === pseudo); + }, + /** + * 设置与伪终端关联的 vscode.Terminal 实例。 + * @param {vscode.Terminal | undefined} term - 要设置的终端实例,如果为 undefined,则清除当前关联。 + */ + set terminal(term: vscode.Terminal | undefined) { + terminal = term; + }, + /** + * 设置伪终端的尺寸。 + * @param {Object} dims - 包含行数和列数的对象。 + */ + setDimensions(dims) { + pseudo.channel?.setWindow!(dims.rows, dims.columns, HEIGHT, WIDTH); + }, + /** + * 处理输入到伪终端的数据。 + * 如果伪终端的状态是 'wait-to-close',则关闭伪终端。 + * 否则,将数据写入伪终端的通道。 + * @param {string} data - 要写入的输入数据。 + */ + handleInput(data) { + if (pseudo.status === 'wait-to-close') return pseudo.close(); + pseudo.channel?.write(data); + }, }; return pseudo; } +/** + * 定义一个文本终端接口,继承自 vscode.Pseudoterminal 接口 + */ export interface TextTerminal extends vscode.Pseudoterminal { + /** + * 向终端写入文本 + * @param text - 要写入的文本 + */ write(text: string): void; + /** + * 关闭终端 + * @param code - 退出代码,可选参数 + */ close(code?: number): void; - onDidClose: vscode.Event; // Redeclaring that it isn't undefined + /** + * 当终端关闭时触发的事件 + * @type {vscode.Event} - 退出代码 + */ + onDidClose: vscode.Event; // 重新声明它不是未定义的 Redeclaring that it isn't undefined + + /** + * 当终端打开时触发的事件 + * @type {vscode.Event} - 无参数 + */ onDidOpen: vscode.Event; } +/** + * 创建一个文本终端实例 + * @param initialText - 初始化时要写入终端的文本,可选参数 + * @returns {TextTerminal} - 创建的文本终端实例 + */ export function createTextTerminal(initialText?: string): TextTerminal { + // 创建一个事件发射器,用于在终端中写入文本 const onDidWrite = new vscode.EventEmitter(); + // 创建一个事件发射器,用于在终端关闭时触发 const onDidClose = new vscode.EventEmitter(); + // 创建一个事件发射器,用于在终端打开时触发 const onDidOpen = new vscode.EventEmitter(); return { + // 向终端写入文本 write: onDidWrite.fire.bind(onDidWrite), + // 关闭终端 close: onDidClose.fire.bind(onDidClose), + // 当有文本写入终端时触发的事件 onDidWrite: onDidWrite.event, + // 当终端关闭时触发的事件 onDidClose: onDidClose.event, + // 当终端打开时触发的事件 onDidOpen: onDidOpen.event, + // 打开终端,如果提供了 initialText,则将其写入终端 open: () => initialText && (onDidWrite.fire(initialText + '\r\n'), onDidClose.fire(1)), }; }