|
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<T extends ConfigPath>(section: T, value: ConfigPathValue<T>): ConfigPathValue<T>;
|
|
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<ConfigurationChangeEvent>();
|
|
get onDidChange(): Event<ConfigurationChangeEvent> {
|
|
return this._onDidChange.event;
|
|
}
|
|
|
|
private _onDidChangeAny = new EventEmitter<ConfigurationChangeEvent>();
|
|
get onDidChangeAny(): Event<ConfigurationChangeEvent> {
|
|
return this._onDidChangeAny.event;
|
|
}
|
|
|
|
private _onWillChange = new EventEmitter<ConfigurationChangeEvent>();
|
|
get onWillChange(): Event<ConfigurationChangeEvent> {
|
|
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<ConfigurationOverrides> | 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<T extends ConfigPath>(section: T, scope?: ConfigurationScope | null): ConfigPathValue<T>;
|
|
get<T extends ConfigPath>(
|
|
section: T,
|
|
scope: ConfigurationScope | null | undefined,
|
|
defaultValue: NonNullable<ConfigPathValue<T>>,
|
|
): NonNullable<ConfigPathValue<T>>;
|
|
get<T extends ConfigPath>(
|
|
section: T,
|
|
scope?: ConfigurationScope | null,
|
|
defaultValue?: NonNullable<ConfigPathValue<T>>,
|
|
): ConfigPathValue<T> {
|
|
const value =
|
|
defaultValue === undefined
|
|
? workspace.getConfiguration(configPrefix, scope).get<ConfigPathValue<T>>(section)!
|
|
: workspace.getConfiguration(configPrefix, scope).get<ConfigPathValue<T>>(section, defaultValue)!;
|
|
return this._overrides?.get == null ? value : this._overrides.get<T>(section, value);
|
|
}
|
|
|
|
getAll(skipOverrides?: boolean): Config {
|
|
const config = workspace.getConfiguration().get<Config>(configPrefix)!;
|
|
return skipOverrides || this._overrides?.getAll == null ? config : this._overrides.getAll(config);
|
|
}
|
|
|
|
getAny<T>(section: string, scope?: ConfigurationScope | null): T | undefined;
|
|
getAny<T>(section: string, scope: ConfigurationScope | null | undefined, defaultValue: T): T;
|
|
getAny<T>(section: string, scope?: ConfigurationScope | null, defaultValue?: T): T | undefined {
|
|
return defaultValue === undefined
|
|
? workspace.getConfiguration(undefined, scope).get<T>(section)
|
|
: workspace.getConfiguration(undefined, scope).get<T>(section, defaultValue);
|
|
}
|
|
|
|
changed<T extends ConfigPath>(
|
|
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<T extends ConfigPath, V extends ConfigPathValue<T>>(section: T, scope?: ConfigurationScope | null) {
|
|
return workspace
|
|
.getConfiguration(configPrefix, scope)
|
|
.inspect<V>(section === undefined ? configPrefix : section);
|
|
}
|
|
|
|
inspectAny<T>(section: string, scope?: ConfigurationScope | null) {
|
|
return workspace.getConfiguration(undefined, scope).inspect<T>(section);
|
|
}
|
|
|
|
async migrate<T extends ConfigPath>(
|
|
from: string,
|
|
to: T,
|
|
options: { fallbackValue?: ConfigPathValue<T>; migrationFn?(value: any): ConfigPathValue<T> },
|
|
): Promise<boolean> {
|
|
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 <setting name> 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 <setting name> 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 <setting name> 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<T extends ConfigPath>(
|
|
from: string,
|
|
to: T,
|
|
options: { migrationFn?(value: any): ConfigPathValue<T> },
|
|
): Promise<void> {
|
|
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 <setting name> 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 <setting name> 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 <setting name> is not a registered configuration`
|
|
// if (from !== to) {
|
|
// try {
|
|
// await this.update(from, undefined, ConfigurationTarget.WorkspaceFolder);
|
|
// }
|
|
// catch { }
|
|
// }
|
|
}
|
|
}
|
|
}
|
|
|
|
matches<T extends ConfigPath>(match: T, section: ConfigPath, value: unknown): value is ConfigPathValue<T> {
|
|
return match === section;
|
|
}
|
|
|
|
name<T extends ConfigPath>(section: T): string {
|
|
return section;
|
|
}
|
|
|
|
update<T extends ConfigPath>(
|
|
section: T,
|
|
value: ConfigPathValue<T> | undefined,
|
|
target: ConfigurationTarget,
|
|
): Thenable<void> {
|
|
return workspace.getConfiguration(configPrefix).update(section, value, target);
|
|
}
|
|
|
|
updateAny(
|
|
section: string,
|
|
value: any,
|
|
target: ConfigurationTarget,
|
|
scope?: ConfigurationScope | null,
|
|
): Thenable<void> {
|
|
return workspace
|
|
.getConfiguration(undefined, target === ConfigurationTarget.Global ? undefined : scope!)
|
|
.update(section, value, target);
|
|
}
|
|
|
|
updateEffective<T extends ConfigPath>(section: T, value: ConfigPathValue<T> | undefined): Thenable<void> {
|
|
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<T, Key extends keyof T> = Key extends string
|
|
? T[Key] extends Record<string, any>
|
|
?
|
|
| `${Key}.${SubPath<T[Key], Exclude<keyof T[Key], keyof any[]>> & string}`
|
|
| `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
|
|
: never
|
|
: never;
|
|
|
|
type Path<T> = SubPath<T, keyof T> | keyof T;
|
|
|
|
type PathValue<T, P extends Path<T>> = P extends `${infer Key}.${infer Rest}`
|
|
? Key extends keyof T
|
|
? Rest extends Path<T[Key]>
|
|
? PathValue<T[Key], Rest>
|
|
: never
|
|
: never
|
|
: P extends keyof T
|
|
? T[P]
|
|
: never;
|
|
|
|
type ConfigPath = Path<Config>;
|
|
type ConfigPathValue<P extends ConfigPath> = PathValue<Config, P>;
|