From c3737330b7ab6dba53f9fe116892dfc6b6ca5f4a Mon Sep 17 00:00:00 2001 From: yetao Date: Fri, 25 Oct 2024 17:50:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor=F0=9F=8E=A8:=20=20(=E9=98=85=E8=AF=BB?= =?UTF-8?q?=E4=BB=A3=E7=A0=81)=EF=BC=9A=E8=A1=A5=E5=85=85manager.ts?= =?UTF-8?q?=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.ts | 35 ++++++ src/manager.ts | 286 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 317 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index b831cd6..d8e2aa5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -172,6 +172,41 @@ export function activate(context: vscode.ExtensionContext) { * @param handler - 命令处理程序的配置 * @returns 注册的命令处理程序 */ + /** 流程图 + @startuml + start + :注册命令处理程序; + if (handler.promptOptions && (!arg || typeof arg === 'string')) then (yes) + :使用 pickComplex 函数从管理器中选择一个复杂的对象,并将其赋值给 arg; + endif + if (typeof arg === 'string') then (yes) + :调用 handleString 方法处理; + stop + endif + if (!arg) then (yes) + :直接返回; + stop + endif + if (arg instanceof vscode.Uri) then (yes) + :调用 handleUri 方法处理; + stop + endif + if ('handleInput' in arg) then (yes) + :调用 handleTerminal 方法处理; + stop + endif + if ('client' in arg) then (yes) + :调用 handleConnection 方法处理; + stop + endif + if ('name' in arg) then (yes) + :调用 handleConfig 方法处理; + stop + endif + :记录警告信息; + stop + @enduml + */ function registerCommandHandler(name: string, handler: CommandHandler) { /** * 处理命令的输入参数 diff --git a/src/manager.ts b/src/manager.ts index eea4771..4e5c086 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -1,204 +1,399 @@ +// 导入 common/fileSystemConfig 模块中的 FileSystemConfig 类型 import type { FileSystemConfig } from 'common/fileSystemConfig'; +// 导入 common/webviewMessages 模块中的 Navigation 类型 import type { Navigation } from 'common/webviewMessages'; +// 导入 vscode 模块,这是 VS Code 扩展 API 的入口点 import * as vscode from 'vscode'; +// 从./config 模块中导入 getConfig、loadConfigs 和 LOADING_CONFIGS 函数或常量 import { getConfig, loadConfigs, LOADING_CONFIGS } from './config'; +// 从./flags 模块中导入 getFlagBoolean 函数 import { getFlagBoolean } from './flags'; +// 从./connection 模块中导入 Connection 和 ConnectionManager 类 import { Connection, ConnectionManager } from './connection'; +// 从./logging 模块中导入 Logging 和 LOGGING_NO_STACKTRACE 常量 import { Logging, LOGGING_NO_STACKTRACE } from './logging'; +// 从./pseudoTerminal 模块中导入 isSSHPseudoTerminal、replaceVariables 和 replaceVariablesRecursive 函数 import { isSSHPseudoTerminal, replaceVariables, replaceVariablesRecursive } from './pseudoTerminal'; +// 导入./sshFileSystem 模块中的 SSHFileSystem 类型 import type { SSHFileSystem } from './sshFileSystem'; +// 从./utils 模块中导入 catchingPromise 和 joinCommands 函数 import { catchingPromise, joinCommands } from './utils'; +/** + * 将命令参数转换为名称字符串 + * @param arg - 命令参数,可以是字符串、FileSystemConfig 对象或 Connection 对象 + * @returns 转换后的名称字符串 + */ function commandArgumentToName(arg?: string | FileSystemConfig | Connection): string { + // 如果参数不存在,则返回 'undefined' if (!arg) return 'undefined'; + // 如果参数是字符串,则直接返回该字符串 if (typeof arg === 'string') return arg; + // 如果参数是 Connection 对象,并且包含 'client' 属性,则返回 'Connection(...)' 格式的字符串 if ('client' in arg) return `Connection(${arg.actualConfig.name})`; + // 否则,返回 'FileSystemConfig(...)' 格式的字符串 return `FileSystemConfig(${arg.name})`; } +/** + * SSHShellTaskOptions 接口定义了用于执行 SSH 命令的任务选项 + * 它继承自 vscode.TaskDefinition 接口,并添加了一些特定的属性 + */ interface SSHShellTaskOptions extends vscode.TaskDefinition { + //远程主机的名称或 IP 地址 host: string; + //要在远程主机上执行的命令 command: string; + //命令执行的工作目录,可选 workingDirectory?: string; } +/** + * 定义一个接口,继承自 vscode.TerminalLink 并添加了一个可选的 uri 属性 + * 这个接口用于表示终端链接的 URI + */ interface TerminalLinkUri extends vscode.TerminalLink { + //链接的 URI,可选 uri?: vscode.Uri; } +/** + * 定义一个类,实现了 vscode.TaskProvider 接口和 vscode.TerminalLinkProvider 接口 + * 这个类用于管理 SSH 连接和执行命令任务 + */ export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider { + //存储所有已创建的 SSHFileSystem 实例的数组 protected fileSystems: SSHFileSystem[] = []; + //存储正在创建的 SSHFileSystem 实例的 Promise 的对象 + //这个对象的键是文件系统的名称,值是创建文件系统的 Promise protected creatingFileSystems: { [name: string]: Promise } = {}; + //一个公共的只读属性,用于管理连接的 ConnectionManager 实例 public readonly connectionManager = new ConnectionManager(); + /** + * Manager 类负责管理 SSH 连接和文件系统 + * 它监听工作区文件夹的变化,以便在必要时断开或连接 SSH 文件系统 + * @constructor + * @param {vscode.ExtensionContext} context - 扩展上下文 + */ constructor(public readonly context: vscode.ExtensionContext) { // In a multi-workspace environment, when the non-main folder gets removed, // it might be one of ours, which we should then disconnect if it's // the only one left for the given config (name) // When one gets added, it gets connected on-demand (using stat() etc) + // 在多工作区环境中,当非主文件夹被移除时, + // 它可能是我们的文件夹之一,如果它是给定配置(名称)的唯一文件夹,我们应该断开它的连接 + // 当一个文件夹被添加时,它会按需连接(使用 stat() 等) vscode.workspace.onDidChangeWorkspaceFolders((e) => { + // 从 VSCode 工作区获取当前的 workspaceFolders,如果没有则默认为空数组 const { workspaceFolders = [] } = vscode.workspace; + // 遍历所有被移除的文件夹 e.removed.forEach(async (folder) => { + // 如果移除的文件夹 URI 方案不是 'ssh',则直接返回,不进行任何处理 if (folder.uri.scheme !== 'ssh') return; + // 检查是否存在其他 workspaceFolders 具有相同的 authority(即主机名) if (workspaceFolders.find(f => f.uri.authority === folder.uri.authority)) return; + // 在 fileSystems 数组中查找是否存在与移除的文件夹 authority 匹配的 SSHFileSystem 实例 const fs = this.fileSystems.find(fs => fs.authority === folder.uri.authority); + // 如果找到了匹配的 SSHFileSystem 实例,则调用其 disconnect 方法来断开连接 if (fs) fs.disconnect(); }); }); } + + /** + * 创建一个新的 SSH 文件系统 + * @param {string} name - 文件系统的名称 + * @param {FileSystemConfig} config - 文件系统的配置 + * @returns {Promise} - 当文件系统创建完成时解析的 Promise + */ public async createFileSystem(name: string, config?: FileSystemConfig): Promise { - await LOADING_CONFIGS; // Prevent race condition on startup, and wait for any current config reload to finish + await LOADING_CONFIGS; // Prevent race condition on startup, and wait for any current config reload to finish 防止启动时的竞态条件,并等待当前配置重载完成 + // 检查是否已经存在一个具有指定名称的 SSHFileSystem 实例 const existing = this.fileSystems.find(fs => fs.authority === name); + // 如果存在,则直接返回该实例 if (existing) return existing; + // 声明一个名为 con 的变量,用于存储即将创建的 Connection 实例 let con: Connection | undefined; + // 尝试从 creatingFileSystems 中获取已存在的 Promise,如果不存在则创建一个新的 Promise return this.creatingFileSystems[name] ||= catchingPromise(async (resolve, reject) => { + // 如果没有提供配置,则从默认配置中获取 config ||= getConfig(name); + // 如果没有找到配置,则抛出错误 if (!config) throw new Error(`Couldn't find a configuration with the name '${name}'`); + // 创建一个新的连接 const con = await this.connectionManager.createConnection(name, config); + // 更新连接管理器中的连接信息 this.connectionManager.update(con, con => con.pendingUserCount++); + // 获取连接的实际配置 config = con.actualConfig; + // 导入 getSFTP 和 SSHFileSystem 模块 const { getSFTP } = await import('./connect'); const { SSHFileSystem } = await import('./sshFileSystem'); + // 使用连接的实际配置创建 SFTP 会话 // Create the actual SFTP session (using the connection's actualConfig, otherwise it'll reprompt for passwords etc) const sftp = await getSFTP(con.client, con.actualConfig); + // 创建一个新的 SSH 文件系统实例 const fs = new SSHFileSystem(name, sftp, con.actualConfig); + // 记录日志,表示已创建 SSH 文件系统并正在读取根目录 Logging.info`Created SSHFileSystem for ${name}, reading root directory...`; + // 更新连接管理器中的文件系统列表 this.connectionManager.update(con, con => con.filesystems.push(fs)); + // 将新创建的文件系统添加到 fileSystems 数组中 this.fileSystems.push(fs); + // 从 creatingFileSystems 中删除该文件系统的 Promise delete this.creatingFileSystems[name]; + // 监听文件系统的关闭事件,当文件系统关闭时,从 fileSystems 数组中删除该文件系统,并更新连接管理器中的文件系统列表 fs.onClose(() => { + // 从 fileSystems 中移除已关闭的文件系统 this.fileSystems = this.fileSystems.filter(f => f !== fs); + // 更新连接管理器中的文件系统列表 this.connectionManager.update(con, con => con.filesystems = con.filesystems.filter(f => f !== fs)); }); + // 刷新文件资源管理器,以显示新创建的文件系统 vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer'); // con.client.once('close', hadError => !fs.closing && this.promptReconnect(name)); + // 更新连接管理器中的连接信息 this.connectionManager.update(con, con => con.pendingUserCount--); // Sanity check that we can access the home directory + // 检查是否可以访问主目录 const [flagCH] = getFlagBoolean('CHECK_HOME', true, config.flags); if (flagCH) try { + // 构建主目录的 URI const homeUri = vscode.Uri.parse(`ssh://${name}/${con.home}`); + // 获取主目录的状态 const stat = await fs.stat(homeUri); + // 如果主目录不是一个目录,则抛出错误 if (!(stat.type & vscode.FileType.Directory)) { throw vscode.FileSystemError.FileNotADirectory(homeUri); } } catch (e) { + // 构建错误消息 let message = `Couldn't read the home directory '${con.home}' on the server for SSH FS '${name}', this might be a sign of bad permissions`; + // 如果错误是文件系统错误,则更新错误消息 if (e instanceof vscode.FileSystemError) { message = `The home directory '${con.home}' in SSH FS '${name}' is not a directory, this might be a sign of bad permissions`; } + // 记录错误日志 Logging.error(e); + // 显示错误消息并提供选项 const answer = await vscode.window.showWarningMessage(message, 'Stop', 'Ignore'); + // 如果用户选择停止,则拒绝 Promise 并抛出错误 if (answer === 'Okay') return reject(new Error('User stopped filesystem creation after unaccessible home directory error')); } + // 解决 Promise,返回创建的文件系统实例 return resolve(fs); - }).catch((e) => { + }).catch((e) => { // 捕获 Promise 中的错误 + // 如果存在连接,则更新连接管理器中的连接信息 if (con) this.connectionManager.update(con, con => con.pendingUserCount--); // I highly doubt resolve(fs) will error + // 如果错误为空,则删除 creatingFileSystems 中的 Promise 并断开连接 if (!e) { delete this.creatingFileSystems[name]; this.commandDisconnect(name); throw e; } + // 记录错误日志 Logging.error`Error while connecting to SSH FS ${name}:\n${e}`; + // 显示错误消息并提供选项 vscode.window.showErrorMessage(`Error while connecting to SSH FS ${name}:\n${e.message}`, 'Retry', 'Configure', 'Ignore').then((chosen) => { + // 从 creatingFileSystems 中删除该文件系统的 Promise delete this.creatingFileSystems[name]; + // 根据用户的选择执行相应的操作 if (chosen === 'Retry') { + // 重试创建文件系统 this.createFileSystem(name).catch(() => { }); } else if (chosen === 'Configure') { + // 配置文件系统 this.commandConfigure(config || name); } else { + // 断开文件系统的连接 this.commandDisconnect(name); } }); + // 抛出错误 throw e; }); } + /** + * 创建一个新的终端实例 + * @param {string} name - 终端的名称 + * @param {FileSystemConfig | Connection} config - 文件系统的配置或连接对象 + * @param {vscode.Uri} uri - URI 对象,指定终端的工作目录 + * @returns {Promise} - 当终端创建完成时解析的 Promise + */ public async createTerminal(name: string, config?: FileSystemConfig | Connection, uri?: vscode.Uri): Promise { const { createTerminal } = await import('./pseudoTerminal'); // Create connection (early so we have .actualConfig.root) + // 创建连接(提前创建以便我们有.actualConfig.root) const con = (config && 'client' in config) ? config : await this.connectionManager.createConnection(config?.name || name, config); // Create pseudo terminal + // 创建伪终端 + // 更新连接管理器中的连接信息,将 pendingUserCount 加 1,表示有一个用户正在使用该连接 this.connectionManager.update(con, con => con.pendingUserCount++); + // 使用提供的连接和工作目录创建一个伪终端实例,并等待其完成 const pty = await createTerminal({ connection: con, workingDirectory: uri?.path || con.actualConfig.root }); + // 当伪终端关闭时,更新连接管理器中的连接信息,从 terminals 数组中移除已关闭的伪终端 pty.onDidClose(() => this.connectionManager.update(con, con => con.terminals = con.terminals.filter(t => t !== pty))); + // 更新连接管理器中的连接信息,将新创建的伪终端添加到 terminals 数组中,并将 pendingUserCount 减 1 this.connectionManager.update(con, con => (con.terminals.push(pty), con.pendingUserCount--)); // Create and show the graphical representation + // 创建并显示图形表示 + // 使用 vscode.window.createTerminal 方法创建一个新的终端实例,并传入一个包含终端名称和伪终端实例的对象 const terminal = vscode.window.createTerminal({ name, pty }); + // 将新创建的终端实例赋值给伪终端实例的 terminal 属性,以便后续使用 pty.terminal = terminal; + // 调用终端实例的 show 方法,显示终端窗口 terminal.show(); } + /** + * 获取当前活动的文件系统列表 + * @returns {readonly SSHFileSystem[]} - 活动的文件系统列表 + */ public getActiveFileSystems(): readonly SSHFileSystem[] { return this.fileSystems; } + /** + * 根据 URI 获取对应的 SSH 文件系统实例 + * @param {vscode.Uri} uri - 要获取文件系统的 URI + * @returns {SSHFileSystem | null} - 找到的 SSH 文件系统实例,如果没有找到则返回 null + */ public getFs(uri: vscode.Uri): SSHFileSystem | null { + // 在 fileSystems 数组中查找 authority 与给定 URI 的 authority 相匹配的 SSHFileSystem 实例 const fs = this.fileSystems.find(f => f.authority === uri.authority); + // 如果找到了匹配的文件系统实例,则返回该实例 if (fs) return fs; + // 如果没有找到匹配的文件系统实例,则返回 null return null; } + /** + * 提示用户重新连接指定名称的 SSH 文件系统 + * @param {string} name - 要重新连接的文件系统的名称 + * @returns {Promise} - 当提示完成时解析的 Promise + */ public async promptReconnect(name: string) { + // 尝试从默认配置中获取指定名称的配置 const config = getConfig(name); + // 如果没有找到配置,则直接返回 if (!config) return; + // 显示一个警告消息,提示用户 SSH 文件系统已断开连接,并提供忽略和断开连接的选项 const choice = await vscode.window.showWarningMessage(`SSH FS ${config.label || config.name} disconnected`, 'Ignore', 'Disconnect'); - if (choice === 'Disconnect') this.commandDisconnect(name); + // 根据用户的选择执行相应的操作 + if (choice === 'Disconnect') this.commandDisconnect(name); // 如果用户选择断开连接,则调用 commandDisconnect 方法断开指定名称的文件系统连接 } /* TaskProvider */ + /** + * 提供任务列表 + * @param token - 可选的取消令牌 + * @returns 任务数组,如果没有任务则返回空数组 + */ public provideTasks(token?: vscode.CancellationToken | undefined): vscode.ProviderResult { return []; } + /** + * 解析并执行一个任务 + * @param {vscode.Task} task - 要解析的任务 + * @param {vscode.CancellationToken | undefined} token - 可选的取消令牌 + * @returns {Promise} - 解析后的任务 + */ public async resolveTask(task: vscode.Task, token?: vscode.CancellationToken | undefined): Promise { return new vscode.Task( - task.definition, // Can't replace/modify this, otherwise we're not contributing to "this" task + // 任务定义,不能替换/修改这个,否则我们就不是在为“这个”任务做贡献 + task.definition, // Can't replace/modify this, otherwise we're not contributing to "this" task 不能替换/修改这个,否则我们就不是在为“这个”任务做贡献 + // 任务作用域为工作区 vscode.TaskScope.Workspace, + // 任务名称,格式为“SSH Task '任务名'” `SSH Task '${task.name}'`, + // 任务类型为“ssh” 'ssh', + // 自定义执行器,用于执行任务 new vscode.CustomExecution(async (resolved: SSHShellTaskOptions) => { const { createTerminal, createTextTerminal } = await import('./pseudoTerminal'); try { + // 检查任务描述中是否缺少“host”字段 if (!resolved.host) throw new Error('Missing field \'host\' in task description'); + // 检查任务描述中是否缺少“command”字段 if (!resolved.command) throw new Error('Missing field \'command\' in task description'); + // 根据主机名创建连接 const connection = await this.connectionManager.createConnection(resolved.host); + // 递归替换变量 resolved = await replaceVariablesRecursive(resolved, value => replaceVariables(value, connection.actualConfig)); let { command, workingDirectory } = resolved; + // 根据标志位确定是否使用 Windows 命令分隔符 const [useWinCmdSep] = getFlagBoolean('WINDOWS_COMMAND_SEPARATOR', connection.shellConfig.isWindows, connection.actualConfig.flags); + // 命令分隔符 const separator = useWinCmdSep ? ' && ' : '; '; let { taskCommand = '$COMMAND' } = connection.actualConfig; + // 连接命令 taskCommand = joinCommands(taskCommand, separator)!; + // 如果命令中包含“$COMMAND”占位符,则替换为实际命令 if (taskCommand.includes('$COMMAND')) { command = taskCommand.replace(/\$COMMAND/g, command); } else { + // 如果命令中缺少“$COMMAND”占位符,则记录错误日志 const message = `The taskCommand '${taskCommand}' is missing the '$COMMAND' placeholder!`; Logging.warning(message, LOGGING_NO_STACKTRACE); + // 输出错误信息 command = `echo "Missing '$COMMAND' placeholder"`; } + // 如果存在工作目录,则将其转换为远程路径 //if (workingDirectory) workingDirectory = this.getRemotePath(config, workingDirectory); + // 更新连接管理器中的连接信息 this.connectionManager.update(connection, con => con.pendingUserCount++); + // 创建终端 const pty = await createTerminal({ command, workingDirectory, connection }); + // 更新连接管理器中的连接信息 this.connectionManager.update(connection, con => (con.pendingUserCount--, con.terminals.push(pty))); + // 监听终端关闭事件 pty.onDidClose(() => this.connectionManager.update(connection, con => con.terminals = con.terminals.filter(t => t !== pty))); + // 返回终端 return pty; } catch (e) { + // 如果发生错误,创建一个文本终端并输出错误信息 return createTextTerminal(`Error: ${e.message || e}`); } }) ) } /* TerminalLinkProvider */ + /** + * 提供终端链接 + * @param context - 终端链接上下文 + * @param token - 取消令牌 + * @returns 终端链接 URI 数组,如果没有链接则返回 undefined + */ public provideTerminalLinks(context: vscode.TerminalLinkContext, token: vscode.CancellationToken): TerminalLinkUri[] | undefined { + // 从上下文中提取当前行和终端对象 const { line, terminal } = context; + // 从终端对象中获取创建选项 const { creationOptions } = terminal; + // 如果创建选项中没有 'pty' 属性,则返回 if (!('pty' in creationOptions)) return; + // 从创建选项中获取 'pty' 属性 const { pty } = creationOptions; + // 如果 'pty' 不是 SSH 伪终端,则返回 if (!isSSHPseudoTerminal(pty)) return; + // 从连接管理器中获取活动连接,并查找包含当前 'pty' 的连接 const conn = this.connectionManager.getActiveConnections().find(c => c.terminals.includes(pty)); + // 如果没有找到连接,则返回 if (!conn) return; // Connection died, which means the terminal should also be closed already? + // 初始化一个数组来存储终端链接 URI const links: TerminalLinkUri[] = []; + // 定义一个正则表达式来匹配路径 const PATH_REGEX = /\/\S+/g; + // 循环匹配当前行中的路径 while (true) { + // 使用正则表达式执行匹配 const match = PATH_REGEX.exec(line); + // 如果没有匹配到,则退出循环 if (!match) break; + // 提取匹配到的文件路径 let [filepath] = match; + // 如果文件路径以 '~' 开头,则将其替换为连接的主目录 if (filepath.startsWith('~')) filepath = conn.home + filepath.substring(1); + // 根据连接的实际配置名称和文件路径创建一个 URI const uri = vscode.Uri.parse(`ssh://${conn.actualConfig.name}/${filepath}`); + // 将链接信息添加到数组中 links.push({ uri, startIndex: match.index, @@ -206,91 +401,174 @@ export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider tooltip: '[SSH FS] Open file', }); } + // 返回找到的链接 return links; } + /** + * 处理终端链接 + * @param link - 终端链接 URI 对象 + * @returns 当处理完成时解析的 Promise + */ public async handleTerminalLink(link: TerminalLinkUri): Promise { + // 如果链接的 URI 不存在,则直接返回 if (!link.uri) return; + // 显示链接指向的文档 await vscode.window.showTextDocument(link.uri); } /* Commands (stuff for e.g. context menu for ssh-configs tree) */ + /** + * 连接到指定的 SSH 文件系统 + * @param {FileSystemConfig} config - 文件系统的配置 + * @returns {Promise} - 当连接完成时解析的 Promise + */ public async commandConnect(config: FileSystemConfig) { + // 记录接收到的连接命令信息 Logging.info`Command received to connect ${config.name}`; + // 获取当前工作区的文件夹 const folders = vscode.workspace.workspaceFolders!; + // 在工作区文件夹中查找是否已经存在指定名称的 SSH 文件系统 const folder = folders && folders.find(f => f.uri.scheme === 'ssh' && f.uri.authority === config.name); + // 如果已经存在,则刷新文件资源管理器 if (folder) return vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer'); + // 获取文件系统的根目录,默认值为 '/' let { root = '/' } = config; + // 如果根目录以 '~' 开头,则将其替换为连接的主目录 if (root.startsWith('~')) { const con = await this.connectionManager.createConnection(config.name, config); root = con.home + root.substring(1); } + // 如果根目录以 '/' 开头,则去除开头的 '/' if (root.startsWith('/')) root = root.substring(1); + // 更新工作区文件夹,添加新的 SSH 文件系统 vscode.workspace.updateWorkspaceFolders(folders ? folders.length : 0, 0, { + // 使用 SSH URI 格式 uri: vscode.Uri.parse(`ssh://${config.name}/${root}`), + // 文件夹名称,格式为 "SSH FS - 标签或名称" name: `SSH FS - ${config.label || config.name}`, }); } + /** + * 断开与指定 SSH 文件系统的连接 + * @param target - 要断开连接的文件系统的名称或连接对象 + * @returns {Promise} - 当断开连接完成时解析的 Promise + */ public commandDisconnect(target: string | Connection) { + // 记录接收到的断开连接命令信息 Logging.info`Command received to disconnect ${commandArgumentToName(target)}`; + // 初始化一个数组来存储要断开连接的连接对象 let cons: Connection[]; + // 如果目标是一个对象并且包含 'client' 属性,则将其视为一个连接对象 if (typeof target === 'object' && 'client' in target) { + // 将目标连接对象添加到数组中 cons = [target]; + // 获取目标连接对象的实际配置名称 target = target.actualConfig.name; } else { + // 如果目标是一个字符串,则获取所有活动连接中名称与目标匹配的连接对象 cons = this.connectionManager.getActiveConnections() .filter(con => con.actualConfig.name === target); } + // 遍历所有要断开连接的连接对象,调用连接管理器的 closeConnection 方法来关闭它们 for (const con of cons) this.connectionManager.closeConnection(con); + // 获取所有活动连接中名称与目标相同的其他连接对象 const others = this.connectionManager.getActiveConnections().filter(c => c.actualConfig.name === target); + // 如果没有其他相同名称的文件系统连接,则直接返回 if (others && others.some(c => c.filesystems.length)) return; // No other filesystems of the same name left anymore, so remove all related workspace folders + // 如果没有其他相同名称的文件系统连接,则移除所有相关的工作区文件夹 const folders = vscode.workspace.workspaceFolders || []; + // 初始化一个变量来存储要移除的工作区文件夹的起始索引 let start: number = folders.length; + // 初始化一个数组来存储移除指定文件夹后剩余的工作区文件夹 let left: vscode.WorkspaceFolder[] = []; + // 遍历所有工作区文件夹 for (const folder of folders) { + // 如果文件夹的 URI 方案是 'ssh' 并且权限与目标名称相同,则更新起始索引 if (folder.uri.scheme === 'ssh' && folder.uri.authority === target) { + // 更新 start 变量,使其为当前 folder.index 和之前的 start 值中的较小值 start = Math.min(folder.index, start); } else if (folder.index > start) { + // 如果文件夹的索引大于起始索引,则将其添加到剩余文件夹数组中 left.push(folder); } }; + // 如果工作区文件夹的数量没有变化,则直接返回 if (folders.length === left.length) return; + // 使用 updateWorkspaceFolders 方法从工作区中移除指定的文件夹 vscode.workspace.updateWorkspaceFolders(start, folders.length - start, ...left); } + /** + * 打开一个终端窗口,连接到指定的 SSH 文件系统 + * @param target - 文件系统的配置或连接对象 + * @param uri - 可选的 URI 对象,指定终端的工作目录 + * @returns {Promise} - 当终端创建完成时解析的 Promise + */ public async commandTerminal(target: FileSystemConfig | Connection, uri?: vscode.Uri) { + // 记录接收到的打开终端命令信息 Logging.info`Command received to open a terminal for ${commandArgumentToName(target)}${uri ? ` in ${uri}` : ''}`; + // 从目标对象中获取配置信息 const config = 'client' in target ? target.actualConfig : target; try { + // 尝试创建一个终端 await this.createTerminal(config.label || config.name, target, uri); } catch (e) { + // 如果创建终端时发生错误,记录错误信息 Logging.error`Error while creating terminal:\n${e}`; + // 向用户显示错误信息,并提供重试和忽略选项 const choice = await vscode.window.showErrorMessage( `Couldn't start a terminal for ${config.name}: ${e.message || e}`, { title: 'Retry' }, { title: 'Ignore', isCloseAffordance: true }); + // 如果用户选择重试,则再次调用 commandTerminal 方法 if (choice && choice.title === 'Retry') return this.commandTerminal(target, uri); } } + /** + * 配置指定的 SSH 文件系统连接 + * @param target - 要配置的文件系统的名称或配置对象 + * @returns {Promise} - 当配置完成时解析的 Promise + */ public async commandConfigure(target: string | FileSystemConfig) { + // 记录接收到的配置命令信息 Logging.info`Command received to configure ${typeof target === 'string' ? target : target.name}`; + // 如果目标是一个对象 if (typeof target === 'object') { + // 检查对象是否有 _location 或 _locations 属性 if (!target._location && !target._locations.length) { + // 如果没有,显示错误信息 vscode.window.showErrorMessage('Cannot configure a config-less connection!'); return; } + // 打开设置界面,导航到编辑配置页面 this.openSettings({ config: target, type: 'editconfig' }); return; } + // 如果目标是一个字符串,将其转换为小写 target = target.toLowerCase(); + // 加载所有配置 let configs = await loadConfigs(); + // 过滤出名称与目标匹配的配置 configs = configs.filter(c => c.name === target); + // 如果没有找到匹配的配置 if (configs.length === 0) { + // 显示错误信息 vscode.window.showErrorMessage(`Found no matching configs for '${target}'`); + // 记录错误信息 return Logging.error`Unexpectedly found no matching configs for '${target}' in commandConfigure?`; } + // 如果只有一个匹配的配置,直接使用它 const config = configs.length === 1 ? configs[0] : configs; + // 打开设置界面,导航到编辑配置页面 this.openSettings({ config, type: 'editconfig' }); } + /** + * 打开设置界面 + * @param navigation - 导航对象,用于指定要导航到的设置页面 + * @returns 一个 Promise,当设置界面打开完成时解决 + */ public async openSettings(navigation?: Navigation) { + // 导入 webview 模块中的 open 和 navigate 函数 const { open, navigate } = await import('./webview'); + // 如果提供了 navigation 对象,则导航到指定的设置页面,否则直接打开设置界面 return navigation ? navigate(navigation) : open(); } }