export * from './config'; import type { ConfigurationChangeEvent, ConfigurationScope, Event, ExtensionContext } from 'vscode'; import { ConfigurationTarget, EventEmitter, workspace } from 'vscode'; import type { Config } from './config'; import { areEqual } from './system/object'; const configPrefix = 'gitlens'; interface ConfigurationOverrides { get(section: T, value: ConfigPathValue): ConfigPathValue; getAll(config: Config): Config; onChange(e: ConfigurationChangeEvent): ConfigurationChangeEvent; } export class Configuration { static configure(context: ExtensionContext): void { context.subscriptions.push( workspace.onDidChangeConfiguration(configuration.onConfigurationChanged, configuration), ); } private _onDidChange = new EventEmitter(); get onDidChange(): Event { return this._onDidChange.event; } private _onDidChangeAny = new EventEmitter(); get onDidChangeAny(): Event { return this._onDidChangeAny.event; } private _onWillChange = new EventEmitter(); get onWillChange(): Event { return this._onWillChange.event; } private onConfigurationChanged(e: ConfigurationChangeEvent) { if (!e.affectsConfiguration(configPrefix)) { this._onDidChangeAny.fire(e); return; } this._onWillChange.fire(e); if (this._overrides?.onChange != null) { e = this._overrides.onChange(e); } this._onDidChangeAny.fire(e); this._onDidChange.fire(e); } private _overrides: Partial | undefined; applyOverrides(overrides: ConfigurationOverrides): void { this._overrides = overrides; } clearOverrides(): void { if (this._overrides == null) return; // Don't clear the "onChange" override as we need to keep it until the stack unwinds (so the the event propagates with the override) this._overrides.get = undefined; this._overrides.getAll = undefined; queueMicrotask(() => (this._overrides = undefined)); } get(section: T, scope?: ConfigurationScope | null): ConfigPathValue; get( section: T, scope: ConfigurationScope | null | undefined, defaultValue: NonNullable>, ): NonNullable>; get( section: T, scope?: ConfigurationScope | null, defaultValue?: NonNullable>, ): ConfigPathValue { const value = defaultValue === undefined ? workspace.getConfiguration(configPrefix, scope).get>(section)! : workspace.getConfiguration(configPrefix, scope).get>(section, defaultValue)!; return this._overrides?.get == null ? value : this._overrides.get(section, value); } getAll(skipOverrides?: boolean): Config { const config = workspace.getConfiguration().get(configPrefix)!; return skipOverrides || this._overrides?.getAll == null ? config : this._overrides.getAll(config); } getAny(section: string, scope?: ConfigurationScope | null): T | undefined; getAny(section: string, scope: ConfigurationScope | null | undefined, defaultValue: T): T; getAny(section: string, scope?: ConfigurationScope | null, defaultValue?: T): T | undefined { return defaultValue === undefined ? workspace.getConfiguration(undefined, scope).get(section) : workspace.getConfiguration(undefined, scope).get(section, defaultValue); } changed( e: ConfigurationChangeEvent | undefined, section: T | T[], scope?: ConfigurationScope | null | undefined, ): boolean { if (e == null) return true; return Array.isArray(section) ? section.some(s => e.affectsConfiguration(`${configPrefix}.${s}`, scope!)) : e.affectsConfiguration(`${configPrefix}.${section}`, scope!); } inspect>(section: T, scope?: ConfigurationScope | null) { return workspace .getConfiguration(configPrefix, scope) .inspect(section === undefined ? configPrefix : section); } inspectAny(section: string, scope?: ConfigurationScope | null) { return workspace.getConfiguration(undefined, scope).inspect(section); } async migrate( from: string, to: T, options: { fallbackValue?: ConfigPathValue; migrationFn?(value: any): ConfigPathValue }, ): Promise { const inspection = configuration.inspect(from as any); if (inspection === undefined) return false; let migrated = false; if (inspection.globalValue !== undefined) { await this.update( to, options.migrationFn != null ? options.migrationFn(inspection.globalValue) : inspection.globalValue, ConfigurationTarget.Global, ); migrated = true; // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` // if (from !== to) { // try { // await this.update(from, undefined, ConfigurationTarget.Global); // } // catch { } // } } if (inspection.workspaceValue !== undefined) { await this.update( to, options.migrationFn != null ? options.migrationFn(inspection.workspaceValue) : inspection.workspaceValue, ConfigurationTarget.Workspace, ); migrated = true; // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` // if (from !== to) { // try { // await this.update(from, undefined, ConfigurationTarget.Workspace); // } // catch { } // } } if (inspection.workspaceFolderValue !== undefined) { await this.update( to, options.migrationFn != null ? options.migrationFn(inspection.workspaceFolderValue) : inspection.workspaceFolderValue, ConfigurationTarget.WorkspaceFolder, ); migrated = true; // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` // if (from !== to) { // try { // await this.update(from, undefined, ConfigurationTarget.WorkspaceFolder); // } // catch { } // } } if (!migrated && options.fallbackValue !== undefined) { await this.update(to, options.fallbackValue, ConfigurationTarget.Global); migrated = true; } return migrated; } async migrateIfMissing( from: string, to: T, options: { migrationFn?(value: any): ConfigPathValue }, ): Promise { const fromInspection = configuration.inspect(from as any); if (fromInspection === undefined) return; const toInspection = configuration.inspect(to); if (fromInspection.globalValue !== undefined) { if (toInspection === undefined || toInspection.globalValue === undefined) { await this.update( to, options.migrationFn != null ? options.migrationFn(fromInspection.globalValue) : fromInspection.globalValue, ConfigurationTarget.Global, ); // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` // if (from !== to) { // try { // await this.update(from, undefined, ConfigurationTarget.Global); // } // catch { } // } } } if (fromInspection.workspaceValue !== undefined) { if (toInspection === undefined || toInspection.workspaceValue === undefined) { await this.update( to, options.migrationFn != null ? options.migrationFn(fromInspection.workspaceValue) : fromInspection.workspaceValue, ConfigurationTarget.Workspace, ); // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` // if (from !== to) { // try { // await this.update(from, undefined, ConfigurationTarget.Workspace); // } // catch { } // } } } if (fromInspection.workspaceFolderValue !== undefined) { if (toInspection === undefined || toInspection.workspaceFolderValue === undefined) { await this.update( to, options.migrationFn != null ? options.migrationFn(fromInspection.workspaceFolderValue) : fromInspection.workspaceFolderValue, ConfigurationTarget.WorkspaceFolder, ); // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` // if (from !== to) { // try { // await this.update(from, undefined, ConfigurationTarget.WorkspaceFolder); // } // catch { } // } } } } matches(match: T, section: ConfigPath, value: unknown): value is ConfigPathValue { return match === section; } name(section: T): string { return section; } update( section: T, value: ConfigPathValue | undefined, target: ConfigurationTarget, ): Thenable { return workspace.getConfiguration(configPrefix).update(section, value, target); } updateAny( section: string, value: any, target: ConfigurationTarget, scope?: ConfigurationScope | null, ): Thenable { return workspace .getConfiguration(undefined, target === ConfigurationTarget.Global ? undefined : scope!) .update(section, value, target); } updateEffective(section: T, value: ConfigPathValue | undefined): Thenable { const inspect = configuration.inspect(section)!; if (inspect.workspaceFolderValue !== undefined) { if (value === inspect.workspaceFolderValue) return Promise.resolve(undefined); return configuration.update(section, value, ConfigurationTarget.WorkspaceFolder); } if (inspect.workspaceValue !== undefined) { if (value === inspect.workspaceValue) return Promise.resolve(undefined); return configuration.update(section, value, ConfigurationTarget.Workspace); } if (inspect.globalValue === value || (inspect.globalValue === undefined && value === inspect.defaultValue)) { return Promise.resolve(undefined); } return configuration.update( section, areEqual(value, inspect.defaultValue) ? undefined : value, ConfigurationTarget.Global, ); } } export const configuration = new Configuration(); type SubPath = Key extends string ? T[Key] extends Record ? | `${Key}.${SubPath> & string}` | `${Key}.${Exclude & string}` : never : never; type Path = SubPath | keyof T; type PathValue> = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? Rest extends Path ? PathValue : never : never : P extends keyof T ? T[P] : never; type ConfigPath = Path; type ConfigPathValue

= PathValue;