diff --git a/src/extension.ts b/src/extension.ts index e251054..b831cd6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,161 +1,416 @@ +// 导入 FileSystemConfig 类型,该类型定义了文件系统的配置 import type { FileSystemConfig } from 'common/fileSystemConfig'; +// 导入 vscode 模块,该模块提供了 VS Code 扩展 API import * as vscode from 'vscode'; +// 导入 loadConfigs 和 reloadWorkspaceFolderConfigs 函数,这些函数用于加载和重新加载配置 import { loadConfigs, reloadWorkspaceFolderConfigs } from './config'; +// 导入 Connection 类型,该类型定义了连接的配置 import type { Connection } from './connection'; +// 导入 FileSystemRouter 类,该类用于处理文件系统的路由 import { FileSystemRouter } from './fileSystemRouter'; +// 导入 Logging 类和 setDebug 函数,这些用于设置日志记录和调试模式 import { Logging, setDebug } from './logging'; +// 导入 Manager 类,该类用于管理文件系统的连接和配置 import { Manager } from './manager'; +// 导入 SSHPseudoTerminal 类型,该类型定义了 SSH 伪终端的配置 import type { SSHPseudoTerminal } from './pseudoTerminal'; +// 导入 ConfigTreeProvider 和 ConnectionTreeProvider 类,这些类用于提供树形视图的数据 import { ConfigTreeProvider, ConnectionTreeProvider } from './treeViewManager'; +// 导入 PickComplexOptions 类型、pickComplex 函数、pickConnection 函数、setAsAbsolutePath 函数和 setupWhenClauseContexts 函数,这些函数和类型用于处理复杂的选择和设置绝对路径等操作 import { PickComplexOptions, pickComplex, pickConnection, setAsAbsolutePath, setupWhenClauseContexts } from './ui-utils'; +/** + * 定义一个命令处理器接口。 + * 命令处理器用于处理各种命令,例如处理字符串、URI、配置、连接和终端等。 + */ interface CommandHandler { - /** If set, a string/undefined prompts using the given options. - * If the input was a string, promptOptions.nameFilter is set to it */ + /** + * 如果设置了,则使用给定的选项提示字符串/未定义。 + * 如果输入是字符串,则将 promptOptions.nameFilter 设置为它。 + */ promptOptions: PickComplexOptions; + + /** + * 处理字符串的可选方法。 + * @param string - 要处理的字符串。 + */ handleString?(string: string): void; + + /** + * 处理 URI 的可选方法。 + * @param uri - 要处理的 URI。 + */ handleUri?(uri: vscode.Uri): void; + + /** + * 处理文件系统配置的可选方法。 + * @param config - 要处理的文件系统配置。 + */ handleConfig?(config: FileSystemConfig): void; + + /** + * 处理连接的可选方法。 + * @param connection - 要处理的连接。 + */ handleConnection?(connection: Connection): void; + + /** + * 处理 SSH 伪终端的可选方法。 + * @param terminal - 要处理的 SSH 伪终端。 + */ handleTerminal?(terminal: SSHPseudoTerminal): void; } + /** `findConfigs` in config.ts ignores URIs for still-connecting connections */ +/** + * 定义一个全局变量 MANAGER,用于存储 Manager 类的实例。 + * 该变量在扩展激活时被初始化,用于管理 SSH 文件系统的连接和配置。 + */ export let MANAGER: Manager | undefined; +/** + * 激活 VS Code 扩展。 + * + * 此函数在扩展被激活时调用,负责初始化扩展的各种功能和组件。 + * + * @param context - 扩展上下文,包含扩展的各种资源和服务。 + */ export function activate(context: vscode.ExtensionContext) { + // 通过 VS Code API 获取名为 'Kelvin.vscode-sshfs' 的扩展实例 const extension = vscode.extensions.getExtension('Kelvin.vscode-sshfs'); + // 尝试获取扩展的版本号,如果扩展不存在,则版本号为 undefined const version = extension?.packageJSON?.version; + // 使用 Logging 模块记录扩展激活的信息,包括版本和模式 Logging.info`Extension activated, version ${version}, mode ${context.extensionMode}`; + // 使用 Logging 模块记录当前运行的 VS Code 版本和进程版本信息 Logging.debug`Running VS Code version ${vscode.version} ${process.versions}`; + // 检查环境变量 VSCODE_SSHFS_DEBUG 是否设置为 true,以启用调试模式 setDebug(process.env.VSCODE_SSHFS_DEBUG?.toLowerCase() === 'true'); + // 从全局状态中获取版本历史记录,如果没有则初始化为空数组 const versionHistory = context.globalState.get<[string, number, number][]>('versionHistory', []); + // 获取版本历史记录中的最后一个版本 const lastVersion = versionHistory[versionHistory.length - 1]; + + // 检查是否存在上一个版本记录 if (!lastVersion) { + // 尝试从全局状态中获取旧版本记录(在 v1.21.0 之前的版本中使用) const classicLastVersion = context.globalState.get('lastVersion'); + // 如果存在旧版本记录 if (classicLastVersion) { + // 记录日志,表明正在从旧版本记录切换到新版本历史记录 Logging.debug`Previously used ${classicLastVersion}, switching over to new version history`; + // 将旧版本记录添加到新版本历史记录中 versionHistory.push([classicLastVersion, Date.now(), Date.now()]); } else { + // 记录日志,表明没有检测到之前的版本,可能是全新安装或安装了 v1.21.0 之前的版本 Logging.debug`No previous version detected. Fresh or pre-v1.21.0 installation?`; } + // 将当前版本添加到版本历史记录中 versionHistory.push([version, Date.now(), Date.now()]); - } else if (lastVersion[0] !== version) { + // 如果存在上一个版本记录,但是版本号与当前版本不同 + } else if (lastVersion[0]!== version) { + // 记录日志,表明之前使用的是不同的版本,现在是切换到当前版本后的首次启动 Logging.debug`Previously used ${lastVersion[0]}, currently first launch since switching to ${version}`; + // 将当前版本添加到版本历史记录中 versionHistory.push([version, Date.now(), Date.now()]); + // 如果存在上一个版本记录,且版本号与当前版本相同 } else { + // 更新上一个版本记录的最后更新时间为当前时间 lastVersion[2] = Date.now(); } + // 使用 Logging 模块记录版本历史记录,将版本历史记录中的每个版本号、创建时间和最后更新时间用冒号连接起来,并用大于号分隔 Logging.info`Version history: ${versionHistory.map(v => v.join(':')).join(' > ')}`; + // 将更新后的版本历史记录保存到全局状态中,以便下次扩展激活时可以读取 context.globalState.update('versionHistory', versionHistory); // Really too bad we *need* the ExtensionContext for relative resources // I really don't like having to pass context to *everything*, so let's do it this way + // 真的很糟糕,我们确实需要扩展上下文(ExtensionContext)来获取相对资源。 + // 我真的不喜欢必须将上下文传递给所有东西,所以让我们这样做吧。 + // 将 context.asAbsolutePath 方法绑定到当前上下文中,并赋值给 setAsAbsolutePath 变量 setAsAbsolutePath(context.asAbsolutePath.bind(context)); + // 创建一个 Manager 实例,并将其赋值给全局变量 MANAGER const manager = MANAGER = new Manager(context); + // 定义一个函数,用于将订阅添加到 context.subscriptions 数组中 const subscribe = context.subscriptions.push.bind(context.subscriptions) as typeof context.subscriptions.push; + + // 定义一个函数,用于注册命令 const registerCommand = (command: string, callback: (...args: any[]) => any, thisArg?: any) => subscribe(vscode.commands.registerCommand(command, callback, thisArg)); + // 注册一个文件系统提供程序,用于处理 ssh 协议的文件系统 subscribe(vscode.workspace.registerFileSystemProvider('ssh', new FileSystemRouter(manager), { isCaseSensitive: true })); + + // 注册一个树视图,用于展示 sshfs 配置 subscribe(vscode.window.createTreeView('sshfs-configs', { treeDataProvider: new ConfigTreeProvider(), showCollapseAll: true })); + + // 创建一个 ConnectionTreeProvider 实例,用于展示连接信息 const connectionTreeProvider = new ConnectionTreeProvider(manager.connectionManager); + + // 注册一个树视图,用于展示 sshfs 连接 subscribe(vscode.window.createTreeView('sshfs-connections', { treeDataProvider: connectionTreeProvider, showCollapseAll: true })); + + // 注册一个任务提供程序,用于处理 ssh 任务 subscribe(vscode.tasks.registerTaskProvider('ssh-shell', manager)); + + // 注册一个终端链接提供程序 subscribe(vscode.window.registerTerminalLinkProvider(manager)); + // 设置条件语句的上下文 setupWhenClauseContexts(manager.connectionManager); + /** + * 注册一个命令处理程序 + * @param name - 命令的名称 + * @param handler - 命令处理程序的配置 + * @returns 注册的命令处理程序 + */ function registerCommandHandler(name: string, handler: CommandHandler) { + /** + * 处理命令的输入参数 + * @param arg - 输入参数,可以是字符串、文件系统配置、连接、SSH 伪终端或 URI + * @returns 处理结果 + */ const callback = async (arg?: string | FileSystemConfig | Connection | SSHPseudoTerminal | vscode.Uri) => { + // 如果存在 promptOptions 并且输入参数为空或为字符串 if (handler.promptOptions && (!arg || typeof arg === 'string')) { - arg = await pickComplex(manager, { ...handler.promptOptions, nameFilter: arg }); + // 使用 pickComplex 函数从管理器中选择一个复杂的对象,并将其赋值给 arg + arg = await pickComplex(manager, {...handler.promptOptions, nameFilter: arg }); } + // 如果输入参数是字符串,则调用 handleString 方法处理 if (typeof arg === 'string') return handler.handleString?.(arg); + // 如果输入参数为空,则直接返回 if (!arg) return; + // 如果输入参数是 URI 类型,则调用 handleUri 方法处理 if (arg instanceof vscode.Uri) { return handler.handleUri?.(arg); - } else if ('handleInput' in arg) { + } + // 如果输入参数存在 handleInput 属性,则调用 handleTerminal 方法处理 + else if ('handleInput' in arg) { return handler.handleTerminal?.(arg); - } else if ('client' in arg) { + } + // 如果输入参数存在 client 属性,则调用 handleConnection 方法处理 + else if ('client' in arg) { return handler.handleConnection?.(arg); - } else if ('name' in arg) { + } + // 如果输入参数存在 name 属性,则调用 handleConfig 方法处理 + else if ('name' in arg) { return handler.handleConfig?.(arg); } + // 如果以上条件都不满足,则记录一条警告信息 Logging.warning(`CommandHandler for '${name}' could not handle input '${arg}'`); }; + // 注册命令 registerCommand(name, callback); + } + /** + * 注册一个命令,用于打开创建新配置文件的设置界面 + * 当用户执行此命令时,会调用 manager.openSettings 方法,并传入一个对象参数 { type: 'newconfig' } + */ // sshfs.new() registerCommand('sshfs.new', () => manager.openSettings({ type: 'newconfig' })); + /** + * 注册一个命令处理程序,用于添加新的配置文件或连接 + * @param name - 命令的名称 + * @param handler - 命令处理程序的配置 + * @returns 注册的命令处理程序 + */ // sshfs.add(target?: string | FileSystemConfig) registerCommandHandler('sshfs.add', { + /** + * 定义了命令执行时的提示选项 + * promptConfigs: true 表示提示用户选择配置文件 + * promptConnections: true 表示提示用户选择连接 + * promptInstantConnection: true 表示提示用户是否立即连接 + */ promptOptions: { promptConfigs: true, promptConnections: true, promptInstantConnection: true }, + /** + * 处理配置文件的函数 + * @param config - 配置文件对象 + * @returns 处理结果 + */ handleConfig: config => manager.commandConnect(config), }); + /** + * 注册一个命令处理程序,用于断开 SSHFS 连接 + * @param name - 命令的名称 + * @param handler - 命令处理程序的配置 + * @returns 注册的命令处理程序 + */ // sshfs.disconnect(target: string | FileSystemConfig | Connection) registerCommandHandler('sshfs.disconnect', { + /** + * 定义了命令执行时的提示选项 + * promptConnections: true 表示提示用户选择连接 + */ promptOptions: { promptConnections: true }, + /** + * 处理字符串输入的函数 + * @param name - 连接的名称 + * @returns 处理结果 + */ handleString: name => manager.commandDisconnect(name), + /** + * 处理配置文件的函数 + * @param config - 配置文件对象 + * @returns 处理结果 + */ handleConfig: config => manager.commandDisconnect(config.name), + /** + * 处理连接对象的函数 + * @param con - 连接对象 + * @returns 处理结果 + */ handleConnection: con => manager.commandDisconnect(con), }); + /** + * 注册一个命令,用于断开所有的 SSHFS 连接 + * 当用户执行此命令时,会调用 manager.connectionManager.closeConnection 方法,关闭所有的活动连接 + */ // sshfs.disconnectAll() registerCommand('sshfs.disconnectAll', () => { const conns = manager.connectionManager; + // 遍历所有的活动连接,并调用 closeConnection 方法关闭它们 // Does not close pending connections (yet?) conns.getActiveConnections().forEach(conn => conns.closeConnection(conn, 'command:disconnectAll')); }); // sshfs.terminal(target?: FileSystemConfig | Connection | vscode.Uri) registerCommandHandler('sshfs.terminal', { + /** + * 定义了命令执行时的提示选项 + * promptConfigs: true 表示提示用户选择配置文件 + * promptConnections: true 表示提示用户选择连接 + * promptInstantConnection: true 表示提示用户是否立即连接 + */ promptOptions: { promptConfigs: true, promptConnections: true, promptInstantConnection: true }, + /** + * 处理配置文件的函数 + * @param config - 配置文件对象 + * @returns 处理结果 + */ handleConfig: config => manager.commandTerminal(config), + /** + * 处理连接对象的函数 + * @param con - 连接对象 + * @returns 处理结果 + */ handleConnection: con => manager.commandTerminal(con), + /** + * 处理 URI 的函数 + * @param uri - URI 对象 + * @returns 处理结果 + */ handleUri: async uri => { + // 根据 URI 的 authority 部分选择连接 const con = await pickConnection(manager, uri.authority); + // 如果找到了连接,则打开终端 con && manager.commandTerminal(con, uri); }, }); + /** + * 注册一个命令处理程序,用于聚焦(显示)SSHFS 终端 + * @param name - 命令的名称 + * @param handler - 命令处理程序的配置 + * @returns 注册的命令处理程序 + */ // sshfs.focusTerminal(target?: SSHPseudoTerminal) registerCommandHandler('sshfs.focusTerminal', { + /** + * 定义了命令执行时的提示选项 + * promptTerminals: true 表示提示用户选择终端 + */ promptOptions: { promptTerminals: true }, + /** + * 处理终端对象的函数 + * @param terminal - 终端对象 + * @returns 处理结果 + */ handleTerminal: ({ terminal }) => terminal?.show(false), }); + /** + * 注册一个命令处理程序,用于关闭 SSHFS 终端 + * @param name - 命令的名称 + * @param handler - 命令处理程序的配置 + * @returns 注册的命令处理程序 + */ // sshfs.closeTerminal(target?: SSHPseudoTerminal) registerCommandHandler('sshfs.closeTerminal', { + /** + * 定义了命令执行时的提示选项 + * promptTerminals: true 表示提示用户选择终端 + */ promptOptions: { promptTerminals: true }, + /** + * 处理终端对象的函数 + * @param terminal - 终端对象 + * @returns 处理结果 + */ handleTerminal: terminal => terminal.close(), }); + /** + * 注册一个命令处理程序,用于配置 SSHFS 连接 + * @param name - 命令的名称 + * @param handler - 命令处理程序的配置 + * @returns 注册的命令处理程序 + */ // sshfs.configure(target?: string | FileSystemConfig) registerCommandHandler('sshfs.configure', { + /** + * 定义了命令执行时的提示选项 + * promptConfigs: true 表示提示用户选择配置文件 + */ promptOptions: { promptConfigs: true }, + /** + * 处理配置文件的函数 + * @param config - 配置文件对象 + * @returns 处理结果 + */ handleConfig: config => manager.commandConfigure(config), }); + /** + * 注册一个命令,用于重新加载配置文件 + * 当用户执行此命令时,会调用 loadConfigs 函数 + */ // sshfs.reload() registerCommand('sshfs.reload', loadConfigs); + /** + * 注册一个命令,用于打开插件的设置界面 + * 当用户执行此命令时,会调用 manager.openSettings 方法 + */ // sshfs.settings() registerCommand('sshfs.settings', () => manager.openSettings()); + /** + * 注册一个命令,用于刷新连接树视图 + * 当用户执行此命令时,会调用 connectionTreeProvider.refresh() 方法 + */ // sshfs.refresh() registerCommand('sshfs.refresh', () => connectionTreeProvider.refresh()); + /** + * 订阅连接管理器的 onConnectionAdded 事件,当有新连接添加时,自动重新加载工作区文件夹配置 + * @param con - 新添加的连接对象 + */ subscribe(manager.connectionManager.onConnectionAdded(async con => { + // 当有新连接添加时,重新加载与该连接名称相关的工作区文件夹配置 await reloadWorkspaceFolderConfigs(con.actualConfig.name); })); }