diff --git a/src/config.ts b/src/config.ts index 182ed6a..a5e6fd5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -239,268 +239,514 @@ async function findConfigs(uri: vscode.Uri, quiet: boolean): Promise { + // 如果 location 是 vscode.Uri 类型,则直接使用该 URI 查找配置文件,并返回结果和 true 表示 location 是绝对路径 if (location instanceof vscode.Uri) { return [await findConfigs(location, quiet), true]; - } else if (location.match(/^([a-zA-Z0-9+.-]+):/)) { + } + // 如果 location 匹配正则表达式 /^([a-zA-Z0-9+.-]+):/,则将其解析为 URI 并查找配置文件,返回结果和 true 表示 location 是绝对路径 + else if (location.match(/^([a-zA-Z0-9+.-]+):/)) { return [await findConfigs(vscode.Uri.parse(location), quiet), true]; - } else if (path.isAbsolute(location)) { + } + // 如果 location 是绝对路径,则将其转换为文件 URI 并查找配置文件,返回结果和 true 表示 location 是绝对路径 + else if (path.isAbsolute(location)) { return [await findConfigs(vscode.Uri.file(location), quiet), true]; } + // 如果以上条件都不满足,则返回空数组和 false 表示 location 不是绝对路径 return [[], false]; } +/** + * 尝试从指定的位置查找配置文件。 + * 如果找到了配置文件,则返回这些文件的解析结果;如果没有找到,则记录一条错误或信息日志,并返回一个空数组。 + * @param location - 要查找的配置文件的位置,可以是字符串或 vscode.Uri。 + * @param source - 提供配置文件位置的源,通常是调用此函数的模块或函数的名称。 + * @returns 一个包含解析后的文件系统配置数组的 Promise,如果没有找到配置文件,则返回一个空数组。 + */ async function tryFindConfigFiles(location: string | vscode.Uri, source: string): Promise { + // 尝试从指定的位置查找配置文件。 const [found, isAbsolute] = await findConfigFiles(location, true); + // 如果找到了配置文件,则返回这些文件的解析结果。 if (found) return found; + // 如果没有找到配置文件,则记录一条错误或信息日志,并返回一个空数组。 logging[isAbsolute ? 'error' : 'info']`No configs found in '${location}' provided by ${source}`; return []; } +/** + * 获取特定作用域下的配置路径。 + * @param scope - 可选的工作区文件夹对象,用于指定特定的工作区文件夹。如果未提供,则默认为全局作用域。 + * @returns 一个对象,包含全局、工作区和文件夹级别的配置路径数组。 + */ function getConfigPaths(scope?: vscode.WorkspaceFolder): Record<'global' | 'workspace' | 'folder', string[]> { + // 从 VSCode 工作区获取名为 'sshfs' 的配置对象,如果指定了 scope,则仅获取该 scope 下的配置 const config = vscode.workspace.getConfiguration('sshfs', scope); + // 检查 'sshfs' 配置对象中的 'configpaths' 属性,获取其全局、工作区和文件夹级别的值 const inspect = config.inspect('configpaths')!; + // 返回一个包含全局、工作区和文件夹级别的配置路径数组的对象 return { + // 如果 inspect.globalValue 存在,则返回其值,否则返回空数组 global: inspect.globalValue || [], + // 如果 inspect.workspaceValue 存在,则返回其值,否则返回空数组 workspace: inspect.workspaceValue || [], + // 如果 inspect.workspaceFolderValue 存在,则返回其值,否则返回空数组 folder: inspect.workspaceFolderValue || [], }; } +// 定义一个名为 configLayers 的对象,用于存储不同层级的配置文件 let configLayers: { + // 存储全局配置文件的数组 global: FileSystemConfig[]; + // 存储工作区配置文件的数组 workspace: FileSystemConfig[]; + // 存储文件夹级别的配置文件的 Map,其中键为文件夹的 URI,值为配置文件数组 folder: Map; }; /** Only loads `sshfs.configs` into `configLayers`, ignoring `sshfs.configpaths` */ +/** + * 加载全局和工作区的配置文件,并将它们存储在 configLayers 对象中。 + * 这个函数会从 VSCode 的工作区配置中获取 'sshfs.configs' 的值,并将其解析为 FileSystemConfig 数组。 + * 然后,它会将这些配置文件分别存储在 configLayers.global 和 configLayers.workspace 中。 + * 每个配置文件都会被标记为其来源(全局或工作区),并存储在 _locations 和 _location 属性中。 + * @returns 一个 Promise,当所有配置文件都被加载并存储后,它会被解决。 + */ async function loadGlobalOrWorkspaceConfigs(): Promise { + // 从 VSCode 工作区获取名为 'sshfs' 的配置对象 const config = vscode.workspace.getConfiguration('sshfs'); + // 检查 'sshfs' 配置对象中的 'configs' 属性,获取其全局和工作区的值 const inspect = config.inspect('configs')!; + // 将全局配置文件存储在 configLayers.global 中,如果没有全局配置,则存储一个空数组 configLayers.global = inspect.globalValue || []; + // 将工作区配置文件存储在 configLayers.workspace 中,如果没有工作区配置,则存储一个空数组 configLayers.workspace = inspect.workspaceValue || []; + // 遍历全局配置文件,将每个配置文件的 _locations 属性设置为包含其来源(全局)的数组,并将 _location 属性设置为其来源 configLayers.global.forEach(c => c._locations = [c._location = vscode.ConfigurationTarget.Global]); + // 遍历工作区配置文件,将每个配置文件的 _locations 属性设置为包含其来源(工作区)的数组,并将 _location 属性设置为其来源 configLayers.workspace.forEach(c => c._locations = [c._location = vscode.ConfigurationTarget.Workspace]); } /** Loads `sshfs.configs` and (including global/workspace-provided) relative `sshfs.configpaths` into `configLayers` */ +/** + * 加载特定工作区文件夹中的配置文件,并将它们存储在 configLayers 对象中。 + * 这个函数会从 VSCode 的工作区配置中获取 'sshfs.configs' 的值,并将其解析为 FileSystemConfig 数组。 + * 然后,它会将这些配置文件存储在 configLayers.folder 中,键为工作区文件夹的 URI。 + * 此外,它还会检查 'sshfs.configpaths' 配置,以查找其他可能的配置文件路径,并尝试加载这些文件。 + * 如果工作区文件夹的 URI 是断开连接的,函数将跳过加载,并记录一条信息日志。 + * @param folder - 要加载配置文件的工作区文件夹对象。 + * @returns 一个包含解析后的文件系统配置数组的 Promise,如果没有找到配置文件,则返回一个空数组。 + */ async function loadWorkspaceFolderConfigs(folder: vscode.WorkspaceFolder): Promise { + // 如果工作区文件夹的 URI 是断开连接的,则跳过加载,并记录一条信息日志 if (skipDisconnectedUri(folder.uri)) { + // 将空数组设置为工作区文件夹的配置文件 configLayers.folder.set(folder.uri.toString(), []); + // 返回空数组 return []; } + // 从 VSCode 工作区获取名为 'sshfs' 的配置对象,并检查 'configs' 属性 const config = vscode.workspace.getConfiguration('sshfs', folder).inspect('configs'); + // 获取工作区文件夹级别的配置文件,如果没有,则返回空数组 const configs = config && config.workspaceFolderValue || []; + // 如果找到了配置文件,则记录一条调试日志 if (configs.length) { logging.debug`Read ${configs.length} configs from workspace folder ${folder.uri}`; + // 为每个配置文件设置 _locations 和 _location 属性,以标识其来源 configs.forEach(c => c._locations = [c._location = `WorkspaceFolder ${folder.uri}`]); } + // 获取特定工作区文件夹的配置路径 const configPaths = getConfigPaths(folder); + // 遍历所有配置路径,包括全局、工作区和文件夹级别的路径 for (const location of [...configPaths.global, ...configPaths.workspace, ...configPaths.folder]) { + // 如果路径是绝对路径,则跳过 if (path.isAbsolute(location)) continue; + // 将工作区文件夹的 URI 与相对路径拼接,得到绝对路径 const uri = vscode.Uri.joinPath(folder.uri, location); + // 尝试从绝对路径查找配置文件,并记录其来源 const found = await tryFindConfigFiles(uri, `WorkspaceFolder '${folder.uri}'`); + // 如果找到了配置文件,则将其添加到配置文件数组中 if (found) configs.push(...found); } + // 将最终的配置文件数组存储在 configLayers.folder 中,键为工作区文件夹的 URI configLayers.folder.set(folder.uri.toString(), configs); + // 返回配置文件数组 return configs; } +/** + * 应用配置层,合并和处理多个配置层,最终生成一个有效的配置列表。 + * 这个函数会将所有的配置层合并到一个数组中,然后进行去重、合并和扩展操作,最终生成一个有效的配置列表。 + * 如果配置名称无效或存在循环引用,函数会记录错误并跳过相应的配置。 + * @returns 一个包含所有有效配置的数组。 + */ function applyConfigLayers(): void { // Merge all layers into a single array of configs, in order of importance + // 将所有的配置文件合并到一个数组中,按照重要性排序 const all: FileSystemConfig[] = [ + // 遍历所有的工作区文件夹,获取每个文件夹的配置文件 ...(vscode.workspace.workspaceFolders || []).flatMap(ws => configLayers.folder.get(ws.uri.toString()) || []), + // 获取工作区的配置文件 ...configLayers.workspace, + // 获取全局的配置文件 ...configLayers.global, ]; + + // 确保每个配置对象的名称属性都被转换为小写形式,以避免大小写敏感问题 all.forEach(c => c.name = (c.name || '').toLowerCase()); // It being undefined shouldn't happen, but better be safe // Let the user do some cleaning with the raw configs + // 遍历所有的配置文件 for (const conf of all) { + // 如果配置文件没有名称 if (!conf.name) { + // 记录错误信息 logging.error`Skipped an invalid SSH FS config (missing a name field):\n${conf}`; + // 显示错误信息给用户 vscode.window.showErrorMessage(`Skipped an invalid SSH FS config (missing a name field)`); + // 如果配置文件的名称无效 } else if (invalidConfigName(conf.name)) { + // 记录警告信息 logging.warning(`Found a SSH FS config with the invalid name "${conf.name}", prompting user how to handle`); + // 显示警告信息给用户,并提供处理选项 vscode.window.showErrorMessage(`Invalid SSH FS config name: ${conf.name}`, 'Rename', 'Delete', 'Skip').then(async (answer) => { + // 如果用户选择重命名 if (answer === 'Rename') { + // 提示用户输入新的名称 const name = await vscode.window.showInputBox({ prompt: `New name for: ${conf.name}`, validateInput: invalidConfigName, placeHolder: 'New name' }); + // 如果用户输入了新的名称 if (name) { + // 记录重命名的信息 const oldName = conf.name; logging.info`Renaming config "${oldName}" to "${name}"`; + // 更新配置文件的名称 conf.name = name; + // 更新配置文件 return updateConfig(conf, oldName); } + // 如果用户选择删除 } else if (answer === 'Delete') { + // 删除配置文件 return deleteConfig(conf); } + // 如果用户选择跳过 logging.warning`Skipped SSH FS config '${conf.name}'`; + // 显示跳过的信息给用户 vscode.window.showWarningMessage(`Skipped SSH FS config '${conf.name}'`); }); } } // Remove duplicates, merging those where the more specific config has `merge` set (in the order from above) + // 初始化一个空数组,用于存储最终的配置文件 loadedConfigs = []; - for (const conf of all.filter(c => !invalidConfigName(c.name))) { + // 遍历所有过滤后的配置文件,确保它们的名称是有效的 + for (const conf of all.filter(c =>!invalidConfigName(c.name))) { + // 在 loadedConfigs 数组中查找是否已经存在具有相同名称的配置文件 const dup = loadedConfigs.find(d => d.name === conf.name); + // 如果找到了重复的配置文件 if (dup) { + // 如果重复的配置文件设置了 merge 属性为 true if (dup.merge) { + // 记录合并操作的日志信息 logging.debug`\tMerging duplicate ${conf.name} from ${conf._locations}`; + // 将当前配置文件的 _locations 属性添加到重复配置文件的 _locations 属性中 dup._locations = [...dup._locations, ...conf._locations]; + // 使用 Object.assign 方法合并当前配置文件和重复配置文件的属性 Object.assign(dup, { ...conf, ...dup }); + // 如果重复的配置文件没有设置 merge 属性为 true } else { + // 记录忽略操作的日志信息 logging.debug`\tIgnoring duplicate ${conf.name} from ${conf._locations}`; } + // 如果没有找到重复的配置文件 } else { + // 记录添加操作的日志信息 logging.debug`\tAdded configuration ${conf.name} from ${conf._locations}`; + // 将当前配置文件添加到 loadedConfigs 数组中 loadedConfigs.push(conf); } } // Handle configs extending other configs + // 定义一个 BuildData 类型,包含一个 FileSystemConfig 类型的 source 属性,表示原始配置 + // 以及一个可选的 result 属性,表示构建后的配置,和一个可选的 skipped 属性,表示是否跳过构建 type BuildData = { source: FileSystemConfig; result?: FileSystemConfig; skipped?: boolean }; + // 创建一个名为 buildData 的 Map 对象,用于存储 BuildData 类型的数据,键为配置的名称,值为 BuildData 对象 const buildData = new Map(); + // 创建一个名为 building 的数组,用于存储当前正在构建的 BuildData 对象 let building: BuildData[] = []; + // 遍历 loadedConfigs 数组,将每个配置的名称和原始配置对象添加到 buildData 中 loadedConfigs.forEach(c => buildData.set(c.name, { source: c })); + /** + * 获取或构建一个配置对象。 + * 这个函数会根据给定的名称从 buildData 中获取一个 BuildData 对象。如果对象不存在,它会尝试构建一个新的配置对象。 + * 在构建过程中,它会处理配置对象的扩展属性,确保所有依赖的配置对象都被正确地获取或构建。 + * 如果在构建过程中遇到循环引用或其他错误,函数会记录错误信息并返回相应的 BuildData 对象。 + * @param name - 要获取或构建的配置对象的名称。 + * @returns 一个 BuildData 对象,包含原始配置和可能的构建结果。如果配置对象无法构建,返回 undefined。 + */ function getOrBuild(name: string): BuildData | undefined { + // 从 buildData 中获取指定名称的配置数据 const data = buildData.get(name); // Handle special cases (missing, built, skipped or looping) + // 处理特殊情况(缺失、已构建、跳过或循环) if (!data || data.result || data.skipped || building.includes(data)) return data; // Start building the resulting config + // 开始构建结果配置 building.push(data); + // 创建一个新的对象,包含源配置的所有属性 const result = { ...data.source }; // Handle extending + // 处理扩展 let extend = result.extend; + // 如果 extend 是字符串,则将其转换为数组 if (typeof extend === 'string') extend = [extend]; + // 遍历扩展配置名称数组 for (const depName of extend || []) { + // 获取或构建依赖配置数据 const depData = getOrBuild(depName); + // 如果依赖配置数据不存在 if (!depData) { + // 记录错误信息,跳过当前配置的构建 logging.error`\tSkipping "${name}" because it extends unknown config "${depName}"`; + // 从 building 数组中弹出当前配置数据,并标记为已跳过 building.pop()!.skipped = true; + // 返回当前配置数据 return data; - } else if (depData.skipped && !data.skipped) { + } + // 如果依赖配置数据已被跳过,且当前配置数据未被跳过 + else if (depData.skipped && !data.skipped) { + // 记录错误信息,跳过当前配置的构建 logging.error`\tSkipping "${name}" because it extends skipped config "${depName}"`; + // 从 building 数组中弹出当前配置数据,并标记为已跳过 building.pop()!.skipped = true; + // 返回当前配置数据 return data; - } else if (data.skipped || building.includes(depData)) { + } + // 如果当前配置数据已被跳过,或 building 数组中包含依赖配置数据 + else if (data.skipped || building.includes(depData)) { + // 记录错误信息,跳过当前配置的构建 logging.error`\tSkipping "${name}" because it extends config "${depName}" which (indirectly) extends "${name}"`; + // 如果 building 数组长度大于 0,记录检测到的循环 if (building.length) logging.debug`\t\tdetected cycle: ${building.map(b => b.source.name).join(' -> ')} -> ${depName}`; + // 从 building 数组中移除依赖配置数据,并标记为已跳过 building.splice(building.indexOf(depData)).forEach(d => d.skipped = true); + // 返回当前配置数据 return data; } + // 记录扩展信息 logging.debug`\tExtending "${name}" with "${depName}"`; + // 将依赖配置数据的结果合并到当前配置的结果中 Object.assign(result, depData.result); } + // 从 building 数组中移除当前正在处理的配置数据 building.pop(); + // 将源配置数据与处理后的结果合并,生成最终的配置对象 data.result = Object.assign(result, data.source); + // 返回处理后的配置数据 return data; } + // 使用 getOrBuild 函数处理 loadedConfigs 中的每个配置,获取其结果,并过滤掉非 FileSystemConfig 类型的结果 loadedConfigs = loadedConfigs.map(c => getOrBuild(c.name)?.result).filter(isFileSystemConfig); + + // 如果处理后的配置数量少于 buildData 中的配置数量,说明存在一些配置被跳过 if (loadedConfigs.length < buildData.size) { + // 显示错误信息,提示用户有些 SSH FS 配置由于 "extend" 选项不正确而被跳过,并提供查看日志的选项 vscode.window.showErrorMessage(`Skipped some SSH FS configs due to incorrect "extend" options`, 'See logs').then(answer => { - if (answer === 'See logs') OUTPUT_CHANNEL.show(true); + // 如果用户选择查看日志 + if (answer === 'See logs') { + // 显示输出通道 + OUTPUT_CHANNEL.show(true); + } }); } // And we're done + // 记录日志,显示已应用的配置层数以及最终得到的配置数量 logging.info`Applied config layers resulting in ${loadedConfigs.length} configurations`; + // 遍历 UPDATE_LISTENERS 数组,通知每个监听者配置已更新 UPDATE_LISTENERS.forEach(listener => listener(loadedConfigs)); } +// 定义一个名为 LOADING_CONFIGS 的 Promise 类型的变量,用于存储加载文件系统配置的 Promise 对象 export let LOADING_CONFIGS: Promise; +/** + * 加载文件系统配置。 + * 这个函数会从 VSCode 的工作区配置中获取 'sshfs.configs' 的值,并将其解析为 FileSystemConfig 数组。 + * 然后,它会将这些配置文件分别存储在 configLayers.global 和 configLayers.workspace 中。 + * 此外,它还会检查 'sshfs.configpaths' 配置,以查找其他可能的配置文件路径,并尝试加载这些文件。 + * 如果工作区文件夹的 URI 是断开连接的,函数将跳过加载,并记录一条信息日志。 + * @returns 一个包含解析后的文件系统配置数组的 Promise,如果没有找到配置文件,则返回一个空数组。 + */ export async function loadConfigs(): Promise { + // 返回 LOADING_CONFIGS 的 Promise 类型的变量,用于存储加载文件系统配置的 Promise 对象 return LOADING_CONFIGS = catchingPromise(async loaded => { + // 记录日志,显示正在加载配置 logging.info('Loading configurations...'); + // 等待重命名无名称的配置 await renameNameless(); - // Keep all found configs "ordened" by layer, for proper deduplication/merging - // while also allowing partially refreshing (workspaceFolder configs) without having to reload *everything* + // 初始化 configLayers 对象,用于存储不同层级的配置 configLayers = { global: [], workspace: [], folder: new Map() }; - // Fetch global/workspace configs from vscode settings + // 从 VSCode 设置中获取全局和工作区的配置 loadGlobalOrWorkspaceConfigs(); - // Fetch configs from config files defined in global/workspace settings + // 获取配置文件路径 const configpaths = getConfigPaths(); + // 遍历全局配置文件路径,尝试查找并加载配置文件 for (const location of configpaths.global) { configLayers.global.push(...await tryFindConfigFiles(location, 'Global Settings')); } + // 遍历工作区配置文件路径,尝试查找并加载配置文件 for (const location of configpaths.workspace) { configLayers.workspace.push(...await tryFindConfigFiles(location, 'Workspace Settings')); } - // Fetch configs from opened folders + // 遍历已打开的工作区文件夹,加载每个文件夹的配置 for (const folder of vscode.workspace.workspaceFolders || []) { await loadWorkspaceFolderConfigs(folder); } + // 应用配置层,合并和处理多个配置层 applyConfigLayers(); + // 通知加载完成 loaded(loadedConfigs); }); } +// 调用 loadConfigs 函数加载文件系统配置 loadConfigs(); +/** + * 重新加载特定 authority 的工作区文件夹配置。 + * 这个函数会遍历所有的工作区文件夹,找到与给定 authority 匹配的文件夹,然后重新加载它们的配置。 + * 如果没有找到匹配的文件夹,函数会直接返回。 + * @param authority - 要重新加载配置的 authority。 + * @returns 一个 Promise,当所有匹配的工作区文件夹的配置都被重新加载后,它会被解决。 + */ export async function reloadWorkspaceFolderConfigs(authority: string): Promise { + // 将 authority 转换为小写,以便进行不区分大小写的比较 authority = authority.toLowerCase(); + // 使用 Promise.all 并发处理所有匹配的工作区文件夹的配置加载 const promises = (vscode.workspace.workspaceFolders || []).map(workspaceFolder => { - if (workspaceFolder.uri.authority.toLowerCase() !== authority) return; + // 如果工作区文件夹的 authority 与给定的 authority 不匹配,则忽略该文件夹 + if (workspaceFolder.uri.authority.toLowerCase()!== authority) return; + // 记录日志,显示正在重新加载特定 authority 的工作区文件夹配置 logging.info`Reloading workspace folder configs for '${authority}' connection`; + // 返回加载工作区文件夹配置的 Promise return loadWorkspaceFolderConfigs(workspaceFolder); }); + // 如果没有找到匹配的工作区文件夹,则直接返回 if (!promises.length) return; + // 等待所有 Promise 完成 await Promise.all(promises); + // 应用配置层,合并和处理多个配置层 applyConfigLayers(); } +// 监听 VSCode 工作区配置的变化 vscode.workspace.onDidChangeConfiguration(async (e) => { + // 如果配置变化影响到 'sshfs.configpaths',则重新加载所有配置 if (e.affectsConfiguration('sshfs.configpaths')) { logging.info('Config paths changed for global/workspace, reloading configs...'); return loadConfigs(); } + // 检查是否全局配置 'sshfs.configs' 发生变化 let updatedGlobal = e.affectsConfiguration('sshfs.configs'); if (updatedGlobal) { logging.info('Config paths changed for global/workspace, updating layers...'); + // 如果全局配置发生变化,重新加载全局和工作区配置 await loadGlobalOrWorkspaceConfigs(); } + // 初始化变量,用于标记是否有任何配置更新 let updatedAtAll = updatedGlobal; + // 遍历所有工作区文件夹 for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { + // 检查是否全局配置、工作区文件夹特定配置或配置路径发生变化 if (updatedGlobal || e.affectsConfiguration('sshfs.configs', workspaceFolder) || e.affectsConfiguration('sshfs.configpaths', workspaceFolder)) { logging.info(`Configs and/or config paths changed for workspace folder ${workspaceFolder.uri}, updating layers...`); + // 如果有变化,重新加载工作区文件夹的配置 await loadWorkspaceFolderConfigs(workspaceFolder); + // 标记有配置更新 updatedAtAll = true; } } + // 如果有任何配置更新,应用新的配置层 if (updatedAtAll) applyConfigLayers(); }); +/** + * 监听 VSCode 工作区文件夹的变化事件。 + * 当工作区文件夹发生变化时,此事件会被触发。 + * 事件对象包含了被添加和移除的文件夹信息。 + * 当有文件夹被添加或移除时,会重新计算配置,并更新配置层。 + * 如果在重新加载配置过程中发生错误,会记录错误信息,并返回已加载的配置。 + */ vscode.workspace.onDidChangeWorkspaceFolders(event => { + // 使用 catchingPromise 函数来处理异步操作,确保即使发生错误也能正确处理 LOADING_CONFIGS = catchingPromise(async loaded => { + // 记录日志,显示工作区文件夹发生变化,正在重新计算配置 logging.info('Workspace folders changed, recalculating configs with updated workspaceFolder configs...'); + // 遍历被移除的文件夹,从 configLayers.folder 中删除相应的配置 event.removed.forEach(folder => configLayers.folder.delete(folder.uri.toString())); + // 遍历被添加的文件夹,重新加载它们的配置 for (const folder of event.added) await loadWorkspaceFolderConfigs(folder); + // 应用新的配置层 applyConfigLayers(); + // 通知配置加载完成 loaded(loadedConfigs); + // 捕获任何可能发生的错误 }).catch(e => { + // 记录错误信息 logging.error`Error while reloading configs in onDidChangeWorkspaceFolders: ${e}`; + // 返回已加载的配置 return loadedConfigs; }); }); +/** + * 定义一个函数类型 ConfigAlterer,用于修改文件系统配置数组。 + * @param configs - 要修改的文件系统配置数组。 + * @returns 修改后的文件系统配置数组,或者 null 或 false 表示不进行修改。 + */ export type ConfigAlterer = (configs: FileSystemConfig[]) => FileSystemConfig[] | null | false; +/** + * 修改指定位置的文件系统配置。 + * 这个函数会根据给定的位置和修改器函数来更新配置。 + * 如果位置是工作区文件夹,它会使用 VSCode 的配置 API 来更新配置。 + * 如果位置是一个文件路径,它会直接读取并修改该文件。 + * @param location - 要修改的配置的位置,可以是工作区文件夹的 URI 或者是文件路径。 + * @param alterer - 一个函数,用于修改配置数组。 + * @returns 如果修改成功,返回修改后的配置数组;如果没有修改,返回 undefined。 + * @throws 如果在修改过程中发生错误,会抛出相应的错误。 + */ export async function alterConfigs(location: ConfigLocation, alterer: ConfigAlterer) { + // 定义一个 URI 类型的变量 uri,用于存储 URI 地址,初始值为 undefined let uri!: vscode.Uri | undefined; + // 定义一个字符串类型的变量 prettyLocation,用于存储格式化后的位置信息,初始值为 undefined let prettyLocation: string | undefined; + // 如果 location 是字符串且以 'WorkspaceFolder ' 开头,则提取 URI 并设置 prettyLocation if (typeof location === 'string' && location.startsWith('WorkspaceFolder ')) { prettyLocation = location; uri = vscode.Uri.parse(location.substring(16)); location = vscode.ConfigurationTarget.WorkspaceFolder; } + // 根据 location 的类型进行不同的处理 switch (location) { case vscode.ConfigurationTarget.WorkspaceFolder: + // 抛出错误,因为不允许使用 WorkspaceFolder URI 更新 WorkspaceFolder 设置 throw new Error(`Trying to update WorkspaceFolder settings with WorkspaceFolder Uri`); case vscode.ConfigurationTarget.Global: + // 如果 prettyLocation 未设置,则默认为 'Global' prettyLocation ||= 'Global'; case vscode.ConfigurationTarget.Workspace: + // 如果 prettyLocation 未设置,则默认为 'Workspace' prettyLocation ||= 'Workspace'; + // 获取 sshfs 配置对象 const conf = vscode.workspace.getConfiguration('sshfs', uri); + // 检查 configs 配置项 const inspect = conf.inspect('configs')!; // If the array doesn't exist, create a new empty one + // 如果数组不存在,则创建一个新的空数组 const array = inspect[[, 'globalValue', 'workspaceValue', 'workspaceFolderValue'][location]!] || []; + // 使用 alterer 函数修改配置数组 let modified = alterer(array); + // 如果没有修改,则直接返回 if (!modified) return; + // 遍历修改后的配置数组,删除以 '_' 开头的键 modified = modified.map((config) => { const newConfig = { ...config }; for (const key in config) { @@ -508,19 +754,28 @@ export async function alterConfigs(location: ConfigLocation, alterer: ConfigAlte } return newConfig; }); + // 更新配置 await conf.update('configs', modified, location); + // 记录日志,显示已更新配置 logging.debug`\tUpdated configs in ${prettyLocation} Settings`; return; } + // 如果 location 不是字符串,则抛出错误 if (typeof location !== 'string') throw new Error(`Invalid _location field: ${location}`); + // 解析 location 为 URI uri = vscode.Uri.parse(location, true); + // 读取配置文件 const configs = await readConfigFile(uri, true); + // 如果没有找到配置文件,则记录错误并抛出错误 if (!configs) { logging.error`Config file '${uri}' not found while altering configs'`; throw new Error(`Config file '${uri}' not found while altering configs'`); } + // 使用 alterer 函数修改配置数组 let altered = alterer(configs); + // 如果没有修改,则直接返回 if (!altered) return; + // 遍历修改后的配置数组,删除以 '_' 开头的键 altered = altered.map((config) => { const newConfig = { ...config }; for (const key in config) { @@ -528,48 +783,96 @@ export async function alterConfigs(location: ConfigLocation, alterer: ConfigAlte } return newConfig; }); + // 将修改后的配置数组转换为 JSON 字符串并写入文件 const data = Buffer.from(JSON.stringify(altered, null, 4)); try { await fs.writeFile(uri, data); } catch (e) { + // 记录错误 logging.error`Error while writing configs to ${location}: ${e}`; + // 抛出错误 throw e; } + // 记录日志,显示已将修改后的配置写入文件 logging.debug`\tWritten modified configs to ${location}`; + // 重新加载配置 await loadConfigs(); } +/** + * 更新指定的文件系统配置。 + * 这个函数会根据给定的配置对象和可选的旧名称来更新配置。 + * 如果没有提供旧名称,则默认为配置对象的当前名称。 + * 函数会尝试将新配置保存到指定的位置,并在必要时覆盖旧配置。 + * 如果旧名称与新名称不同,函数会尝试查找并覆盖旧配置。 + * 如果没有找到旧配置,它将被添加到配置列表中。 + * @param config - 要更新的文件系统配置对象。 + * @param oldName - 可选的旧配置名称,如果未提供,则默认为 config 对象的 name 属性。 + * @returns 如果更新成功,返回更新后的配置对象;如果没有更新,返回 undefined。 + * @throws 如果在更新过程中发生错误,会抛出相应的错误。 + */ export async function updateConfig(config: FileSystemConfig, oldName = config.name) { + // 从 config 对象中提取 name 和 _location 属性 const { name, _location } = config; + // 如果 name 属性不存在,则抛出错误 if (!name) throw new Error(`The given config has no name field`); + // 如果 _location 属性不存在,则抛出错误 if (!_location) throw new Error(`The given config has no _location field`); + // 记录日志,显示正在保存配置 logging.info`Saving config ${name} to ${_location}`; + // 如果 oldName 不等于 config.name,则记录日志,显示将尝试覆盖旧配置 if (oldName !== config.name) { logging.debug`\tSaving ${name} will try to overwrite old config ${oldName}`; } + // 调用 alterConfigs 函数来修改配置 await alterConfigs(_location, (configs) => { + // 记录日志,显示配置位置的现有配置 logging.debug`\tConfig location '${_location}' has following configs: ${configs.map(c => c.name).join(', ')}`; + // 在 configs 数组中查找名称与 oldName 匹配的配置的索引 const index = configs.findIndex(c => c.name ? c.name.toLowerCase() === oldName.toLowerCase() : false); + // 如果没有找到匹配的配置,则将新配置添加到 configs 数组中 if (index === -1) { logging.debug`\tAdding the new config to the existing configs`; configs.push(config); + // 如果找到了匹配的配置,则用新配置覆盖旧配置 } else { logging.debug`\tOverwriting config '${configs[index].name}' at index ${index} with the new config`; configs[index] = config; } + // 返回修改后的 configs 数组 return configs; }); } +/** + * 删除指定的文件系统配置。 + * 这个函数会根据给定的配置对象来删除配置。 + * 它会尝试在指定的位置找到并删除该配置。 + * 如果配置不存在,函数会抛出错误。 + * @param config - 要删除的文件系统配置对象。 + * @returns 如果删除成功,返回 undefined;如果没有删除,抛出错误。 + * @throws 如果在删除过程中发生错误,会抛出相应的错误。 + */ export async function deleteConfig(config: FileSystemConfig) { + // 从 config 对象中提取 name 和 _location 属性 const { name, _location } = config; + // 如果 name 属性不存在,则抛出错误 if (!name) throw new Error(`The given config has no name field`); + // 如果 _location 属性不存在,则抛出错误 if (!_location) throw new Error(`The given config has no _location field`); + // 记录日志,显示正在删除配置 logging.info`Deleting config ${name} in ${_location}`; + // 调用 alterConfigs 函数来修改配置 await alterConfigs(_location, (configs) => { + // 记录日志,显示配置位置的现有配置 logging.debug`\tConfig location '${_location}' has following configs: ${configs.map(c => c.name).join(', ')}`; + // 在 configs 数组中查找名称与 name 匹配的配置的索引 const index = configs.findIndex(c => c.name ? c.name.toLowerCase() === name.toLowerCase() : false); + // 如果没有找到匹配的配置,则抛出错误 if (index === -1) throw new Error(`Config '${name}' not found in ${_location}`); + // 记录日志,显示正在删除指定索引处的配置 logging.debug`\tDeleting config '${configs[index].name}' at index ${index}`; + // 从 configs 数组中删除指定索引处的配置 configs.splice(index, 1); + // 返回修改后的 configs 数组 return configs; }); } @@ -628,27 +931,62 @@ export function getConfig(input: string): FileSystemConfig | undefined { return parsed; } +/** + * 比较两个值是否匹配。 + * 这个函数会根据值的类型进行不同的比较。 + * 如果值的类型不同,则直接返回 false。 + * 如果值是对象或数组,则会递归地比较它们的每个属性或元素。 + * 如果值是数组,则会比较它们的长度和每个元素。 + * 如果值是对象,则会比较它们的键和对应的值。 + * @param a - 要比较的第一个值。 + * @param b - 要比较的第二个值。 + * @returns 如果两个值匹配,返回 true;否则,返回 false。 + */ function valueMatches(a: any, b: any): boolean { + // 如果 a 和 b 的类型不同,则它们不匹配 if (typeof a !== typeof b) return false; + // 如果 a 和 b 不是对象,则直接比较它们的值 if (typeof a !== 'object') return a === b; + // 如果 a 或 b 是 null 或 undefined,则它们不匹配 if (!a || !b) return a === b; + // 如果 a 和 b 是数组,则比较它们的每个元素 if (Array.isArray(a)) { + // 如果 b 不是数组,则它们不匹配 if (!Array.isArray(b)) return false; + // 如果 a 和 b 的长度不同,则它们不匹配 if (a.length !== b.length) return false; + // 递归地比较数组中的每个元素 return a.every((value, index) => valueMatches(value, b[index])); } + // 获取 a 和 b 的所有键 const keysA = Object.keys(a); const keysB = Object.keys(b); + // 如果 a 和 b 的键的数量不同,则它们不匹配 if (keysA.length !== keysB.length) return false; + // 比较 a 和 b 的每个键对应的值 for (const key of keysA) { + // 如果 a 和 b 的对应键的值不匹配,则它们不匹配 if (!valueMatches(a[key], b[key])) return false; } + // 如果所有的比较都通过了,则 a 和 b 匹配 return true; } +/** + * 检查两个文件系统配置对象是否匹配。 + * 这个函数会比较两个配置对象的所有属性,以确定它们是否完全相同。 + * 如果两个配置对象的所有属性都匹配,则返回 true;否则,返回 false。 + * @param a - 第一个文件系统配置对象。 + * @param b - 第二个文件系统配置对象。 + * @returns 如果两个配置对象匹配,返回 true;否则,返回 false。 + */ export function configMatches(a: FileSystemConfig, b: FileSystemConfig): boolean { - // This is kind of the easiest and most robust way of checking if configs are identical. - // If it wasn't for `loadedConfigs` (and its contents) regularly being fully recreated, we + // This is kind of the easiest and most robust way of checking if two configs are the same. + // If it wasn't for `loadedConfigs` (and its contents) regularly being fully recreated, // could just use === between the two configs. This'll do for now. + // 这是检查配置是否相同的最简单和最可靠的方法。 + // 如果不是因为 `loadedConfigs`(及其内容)经常被完全重新创建,我们 + // 可以直接使用 === 来比较两个配置。目前,这个方法就足够了。 return valueMatches(a, b); } +