Add the ability for configs to extend other configs (#268)

pull/373/head
Kelvin Schoofs 2 years ago
parent ee73f1ab38
commit 6eff0bed10

@ -30,6 +30,9 @@
- Loading config files from the VS Code settings in remote workspaces is now supported - 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 - 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 - 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 ### Development changes

@ -89,6 +89,8 @@ export interface FileSystemConfig extends ConnectConfig {
group?: string; group?: string;
/** Whether to merge this "lower" config (e.g. from workspace settings) into higher configs (e.g. from global settings) */ /** Whether to merge this "lower" config (e.g. from workspace settings) into higher configs (e.g. from global settings) */
merge?: boolean; 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 `/` */ /** 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; root?: string;
/** A name of a PuTTY session, or `true` to find the PuTTY session from the host address */ /** A name of a PuTTY session, or `true` to find the PuTTY session from the host address */

@ -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 { ParseError, parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
import * as path from 'path'; import * as path from 'path';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { MANAGER } from './extension'; import { MANAGER } from './extension';
import { Logging } from './logging'; import { Logging, OUTPUT_CHANNEL } from './logging';
import { catchingPromise } from './utils'; import { catchingPromise } from './utils';
const fs = vscode.workspace.fs; const fs = vscode.workspace.fs;
@ -230,6 +230,51 @@ function applyConfigLayers(): void {
loadedConfigs.push(conf); loadedConfigs.push(conf);
} }
} }
// Handle configs extending other configs
type BuildData = { source: FileSystemConfig; result?: FileSystemConfig; skipped?: boolean };
const buildData = new Map<string, BuildData>();
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`; logging.info`Applied config layers resulting in ${loadedConfigs.length} configurations`;
UPDATE_LISTENERS.forEach(listener => listener(loadedConfigs)); UPDATE_LISTENERS.forEach(listener => listener(loadedConfigs));
} }

@ -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 { export interface LoggingOptions {
/** /**
@ -106,7 +106,7 @@ class Logger {
// There is no parent, we're responsible for actually logging the message // There is no parent, we're responsible for actually logging the message
const space = ' '.repeat(Math.max(0, 8 - type.length)); const space = ' '.repeat(Math.max(0, 8 - type.length));
const msg = `[${type}]${space}${prefix}${message}${suffix}` 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 // VS Code issue where console.debug logs twice in the Debug Console
if (type.toLowerCase() === 'debug') type = 'log'; if (type.toLowerCase() === 'debug') type = 'log';
if (DEBUG) (console[type.toLowerCase()] || console.log).call(console, msg); if (DEBUG) (console[type.toLowerCase()] || console.log).call(console, msg);

Loading…
Cancel
Save