From 6eff0bed10e1ccde5c819d2b7cbdfb7472f6b2cf Mon Sep 17 00:00:00 2001 From: Kelvin Schoofs Date: Sat, 25 Mar 2023 18:49:16 +0100 Subject: [PATCH] Add the ability for configs to extend other configs (#268) --- CHANGELOG.md | 3 +++ common/src/fileSystemConfig.ts | 2 ++ src/config.ts | 49 ++++++++++++++++++++++++++++++++-- src/logging.ts | 4 +-- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dce85f..2480cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ - Loading config files from the VS Code settings in remote workspaces is now supported - All layers, including (remote) workspace folders should fully support the `sshfs.configpaths` setting - Although this can change, for workspace folders, paths specified in the global/workspace settings are also scanned +- Add a new `extend` config option that allows a config to extend one or more other configs (#268) + - The extension will automatically detect and report missing or cyclic dependencies, skipping them + - Note that if a config tries to extend a non-existing config, it will be skipped and an error will also be shown ### Development changes diff --git a/common/src/fileSystemConfig.ts b/common/src/fileSystemConfig.ts index b234392..ae256c3 100644 --- a/common/src/fileSystemConfig.ts +++ b/common/src/fileSystemConfig.ts @@ -89,6 +89,8 @@ export interface FileSystemConfig extends ConnectConfig { group?: string; /** Whether to merge this "lower" config (e.g. from workspace settings) into higher configs (e.g. from global settings) */ merge?: boolean; + /** Names of other configs to merge into this config. Errors if not found. Later entries have priority. Settings defined in this config itself have even higher priority */ + extend?: string | string[]; /** Path on the remote server that should be opened by default when creating a terminal or using the `Add as Workspace folder` command/button. Defaults to `/` */ root?: string; /** A name of a PuTTY session, or `true` to find the PuTTY session from the host address */ diff --git a/src/config.ts b/src/config.ts index 0790d8f..386c8fa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,10 +1,10 @@ -import { ConfigLocation, FileSystemConfig, invalidConfigName, parseConnectionString } from 'common/fileSystemConfig'; +import { ConfigLocation, FileSystemConfig, invalidConfigName, isFileSystemConfig, parseConnectionString } from 'common/fileSystemConfig'; import { ParseError, parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'; import * as path from 'path'; import * as vscode from 'vscode'; import { MANAGER } from './extension'; -import { Logging } from './logging'; +import { Logging, OUTPUT_CHANNEL } from './logging'; import { catchingPromise } from './utils'; const fs = vscode.workspace.fs; @@ -230,6 +230,51 @@ function applyConfigLayers(): void { loadedConfigs.push(conf); } } + // Handle configs extending other configs + type BuildData = { source: FileSystemConfig; result?: FileSystemConfig; skipped?: boolean }; + const buildData = new Map(); + let building: BuildData[] = []; + loadedConfigs.forEach(c => buildData.set(c.name, { source: c })); + function getOrBuild(name: string): BuildData | undefined { + 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; + 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.pop()!.skipped = true; + return data; + } else if (depData.skipped && !data.skipped) { + logging.error`\tSkipping "${name}" because it extends skipped config "${depName}"`; + building.pop()!.skipped = true; + return data; + } else if (data.skipped || building.includes(depData)) { + logging.error`\tSkipping "${name}" because it extends config "${depName}" which (indirectly) extends "${name}"`; + if (building.length) logging.debug`\t\tdetected cycle: ${building.map(b => b.source.name).join(' -> ')} -> ${depName}`; + building.splice(building.indexOf(depData)).forEach(d => d.skipped = true); + return data; + } + logging.debug`\tExtending "${name}" with "${depName}"`; + Object.assign(result, depData.result); + } + building.pop(); + data.result = Object.assign(result, data.source); + return data; + } + loadedConfigs = loadedConfigs.map(c => getOrBuild(c.name)?.result).filter(isFileSystemConfig); + if (loadedConfigs.length < buildData.size) { + 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); + }); + } + // And we're done logging.info`Applied config layers resulting in ${loadedConfigs.length} configurations`; UPDATE_LISTENERS.forEach(listener => listener(loadedConfigs)); } diff --git a/src/logging.ts b/src/logging.ts index a36ddce..7c405b7 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -17,7 +17,7 @@ export function setDebug(debug: boolean) { } } -const outputChannel = vscode.window.createOutputChannel('SSH FS'); +export const OUTPUT_CHANNEL = vscode.window.createOutputChannel('SSH FS'); export interface LoggingOptions { /** @@ -106,7 +106,7 @@ class Logger { // There is no parent, we're responsible for actually logging the message const space = ' '.repeat(Math.max(0, 8 - type.length)); const msg = `[${type}]${space}${prefix}${message}${suffix}` - outputChannel.appendLine(msg); + OUTPUT_CHANNEL.appendLine(msg); // VS Code issue where console.debug logs twice in the Debug Console if (type.toLowerCase() === 'debug') type = 'log'; if (DEBUG) (console[type.toLowerCase()] || console.log).call(console, msg);