diff --git a/src/webviews/apps/settings/settings.ts b/src/webviews/apps/settings/settings.ts index 8513ccc..522f4c1 100644 --- a/src/webviews/apps/settings/settings.ts +++ b/src/webviews/apps/settings/settings.ts @@ -1,7 +1,7 @@ /*global document IntersectionObserver*/ import './settings.scss'; import type { AutolinkReference } from '../../../config'; -import type { IpcMessage } from '../../protocol'; +import type { IpcMessage, UpdateConfigurationParams } from '../../protocol'; import { DidChangeConfigurationNotificationType, DidGenerateConfigurationPreviewNotificationType, @@ -163,7 +163,9 @@ export class SettingsApp extends App { private applyChanges() { this.sendCommand(UpdateConfigurationCommandType, { changes: { ...this._changes }, - removes: Object.keys(this._changes).filter(k => this._changes[k] === undefined), + removes: Object.keys(this._changes).filter( + (k): k is UpdateConfigurationParams['removes'][0] => this._changes[k] === undefined, + ), scope: this.getSettingsScope(), }); diff --git a/src/webviews/protocol.ts b/src/webviews/protocol.ts index 1a4979c..8170d82 100644 --- a/src/webviews/protocol.ts +++ b/src/webviews/protocol.ts @@ -64,7 +64,7 @@ export interface UpdateConfigurationParams { changes: { [key in ConfigPath | CustomConfigPath]?: ConfigPathValue | CustomConfigPathValue; }; - removes: string[]; + removes: (keyof { [key in ConfigPath | CustomConfigPath]?: ConfigPathValue })[]; scope?: 'user' | 'workspace'; uri?: string; } @@ -113,3 +113,10 @@ const customConfigKeys: readonly CustomConfigPath[] = [ export function isCustomConfigKey(key: string): key is CustomConfigPath { return customConfigKeys.includes(key as CustomConfigPath); } + +export function assertsConfigKeyValue( + key: T, + value: unknown, +): asserts value is ConfigPathValue { + // Noop +} diff --git a/src/webviews/settings/settingsWebview.ts b/src/webviews/settings/settingsWebview.ts index 88bea50..2fcd8de 100644 --- a/src/webviews/settings/settingsWebview.ts +++ b/src/webviews/settings/settingsWebview.ts @@ -1,14 +1,59 @@ -import type { ViewColumn } from 'vscode'; -import { workspace } from 'vscode'; +import type { ConfigurationChangeEvent, Disposable, ViewColumn } from 'vscode'; +import { ConfigurationTarget, workspace } from 'vscode'; +import type { CoreConfiguration } from '../../constants'; +import { extensionPrefix } from '../../constants'; +import type { Container } from '../../container'; +import { CommitFormatter } from '../../git/formatters/commitFormatter'; +import { GitCommit, GitCommitIdentity } from '../../git/models/commit'; +import { GitFileChange, GitFileIndexStatus } from '../../git/models/file'; +import { PullRequest, PullRequestState } from '../../git/models/pullRequest'; +import type { ConfigPath } from '../../system/configuration'; import { configuration } from '../../system/configuration'; -import { DidOpenAnchorNotificationType } from '../protocol'; -import type { WebviewProvider } from '../webviewController'; -import { WebviewProviderWithConfigBase } from '../webviewWithConfigBase'; +import { map } from '../../system/iterable'; +import { Logger } from '../../system/logger'; +import type { CustomConfigPath, IpcMessage } from '../protocol'; +import { + assertsConfigKeyValue, + DidChangeConfigurationNotificationType, + DidGenerateConfigurationPreviewNotificationType, + DidOpenAnchorNotificationType, + GenerateConfigurationPreviewCommandType, + isCustomConfigKey, + onIpc, + UpdateConfigurationCommandType, +} from '../protocol'; +import type { WebviewController, WebviewProvider } from '../webviewController'; import type { State } from './protocol'; -export class SettingsWebviewProvider extends WebviewProviderWithConfigBase implements WebviewProvider { +export class SettingsWebviewProvider implements WebviewProvider { + private readonly _disposable: Disposable; private _pendingJumpToAnchor: string | undefined; + constructor(protected readonly container: Container, protected readonly host: WebviewController) { + this._disposable = configuration.onDidChangeAny(this.onAnyConfigurationChanged, this); + } + + dispose() { + this._disposable.dispose(); + } + + includeBootstrap(): State { + const scopes: ['user' | 'workspace', string][] = [['user', 'User']]; + if (workspace.workspaceFolders?.length) { + scopes.push(['workspace', 'Workspace']); + } + + return { + timestamp: Date.now(), + version: this.container.version, + // Make sure to get the raw config, not from the container which has the modes mixed in + config: configuration.getAll(true), + customSettings: this.getCustomSettings(), + scope: 'user', + scopes: scopes, + }; + } + onShowing?( loading: boolean, _options: { column?: ViewColumn; preserveFocus?: boolean }, @@ -33,21 +78,11 @@ export class SettingsWebviewProvider extends WebviewProviderWithConfigBase { + const target = + params.scope === 'workspace' ? ConfigurationTarget.Workspace : ConfigurationTarget.Global; + + let key: keyof typeof params.changes; + for (key in params.changes) { + let value = params.changes[key]; + + if (isCustomConfigKey(key)) { + const customSetting = this.customSettings.get(key); + if (customSetting != null) { + if (typeof value === 'boolean') { + await customSetting.update(value); + } else { + debugger; + } + } + + continue; + } + + assertsConfigKeyValue(key, value); + + const inspect = configuration.inspect(key)!; + + if (value != null) { + if (params.scope === 'workspace') { + if (value === inspect.workspaceValue) continue; + } else { + if (value === inspect.globalValue && value !== inspect.defaultValue) continue; + + if (value === inspect.defaultValue) { + value = undefined; + } + } + } + + await configuration.update(key, value, target); + } + + for (const key of params.removes) { + await configuration.update(key as ConfigPath, undefined, target); + } + }); + break; + + case GenerateConfigurationPreviewCommandType.method: + Logger.debug(`Webview(${this.host.id}).onMessageReceived: method=${e.method}`); + + onIpc(GenerateConfigurationPreviewCommandType, e, async params => { + switch (params.type) { + case 'commit': + case 'commit-uncommitted': { + const commit = new GitCommit( + this.container, + '~/code/eamodio/vscode-gitlens-demo', + 'fe26af408293cba5b4bfd77306e1ac9ff7ccaef8', + new GitCommitIdentity('You', 'eamodio@gmail.com', new Date('2016-11-12T20:41:00.000Z')), + new GitCommitIdentity('You', 'eamodio@gmail.com', new Date('2020-11-01T06:57:21.000Z')), + params.type === 'commit-uncommitted' ? 'Uncommitted changes' : 'Supercharged', + ['3ac1d3f51d7cf5f438cc69f25f6740536ad80fef'], + params.type === 'commit-uncommitted' ? 'Uncommitted changes' : 'Supercharged', + new GitFileChange( + '~/code/eamodio/vscode-gitlens-demo', + 'code.ts', + GitFileIndexStatus.Modified, + ), + undefined, + [], + ); + + let includePullRequest = false; + switch (params.key) { + case configuration.name('currentLine.format'): + includePullRequest = configuration.get('currentLine.pullRequests.enabled'); + break; + case configuration.name('statusBar.format'): + includePullRequest = configuration.get('statusBar.pullRequests.enabled'); + break; + } + + let pr: PullRequest | undefined; + if (includePullRequest) { + pr = new PullRequest( + { id: 'github', name: 'GitHub', domain: 'github.com', icon: 'github' }, + { + name: 'Eric Amodio', + avatarUrl: 'https://avatars1.githubusercontent.com/u/641685?s=32&v=4', + url: 'https://github.com/eamodio', + }, + '1', + 'Supercharged', + 'https://github.com/gitkraken/vscode-gitlens/pulls/1', + PullRequestState.Merged, + new Date('Sat, 12 Nov 2016 19:41:00 GMT'), + undefined, + new Date('Sat, 12 Nov 2016 20:41:00 GMT'), + ); + } + + let preview; + try { + preview = CommitFormatter.fromTemplate(params.format, commit, { + dateFormat: configuration.get('defaultDateFormat'), + pullRequestOrRemote: pr, + messageTruncateAtNewLine: true, + }); + } catch { + preview = 'Invalid format'; + } + + await this.host.notify( + DidGenerateConfigurationPreviewNotificationType, + { preview: preview }, + e.completionId, + ); + } + } + }); + break; + } + } + + private onAnyConfigurationChanged(e: ConfigurationChangeEvent) { + if (!configuration.changedAny(e, extensionPrefix)) { + const notify = configuration.changedAny(e, [ + ...map(this.customSettings.values(), s => s.name), + ]); + if (!notify) return; + } + + void this.notifyDidChangeConfiguration(); + } + + private _customSettings: Map | undefined; + private get customSettings() { + if (this._customSettings == null) { + this._customSettings = new Map([ + [ + 'rebaseEditor.enabled', + { + name: 'workbench.editorAssociations', + enabled: () => this.container.rebaseEditor.enabled, + update: this.container.rebaseEditor.setEnabled, + }, + ], + [ + 'currentLine.useUncommittedChangesFormat', + { + name: 'currentLine.uncommittedChangesFormat', + enabled: () => configuration.get('currentLine.uncommittedChangesFormat') != null, + update: async enabled => + configuration.updateEffective( + 'currentLine.uncommittedChangesFormat', + // eslint-disable-next-line no-template-curly-in-string + enabled ? '✏️ ${ago}' : null, + ), + }, + ], + ]); + } + return this._customSettings; + } + + protected getCustomSettings(): Record { + const customSettings: Record = Object.create(null); + for (const [key, setting] of this.customSettings) { + customSettings[key] = setting.enabled(); + } + return customSettings; + } + + private notifyDidChangeConfiguration() { + // Make sure to get the raw config, not from the container which has the modes mixed in + return this.host.notify(DidChangeConfigurationNotificationType, { + config: configuration.getAll(true), + customSettings: this.getCustomSettings(), + }); + } +} + +interface CustomSetting { + name: ConfigPath | CoreConfiguration; + enabled: () => boolean; + update: (enabled: boolean) => Promise; } diff --git a/src/webviews/webviewWithConfigBase.ts b/src/webviews/webviewWithConfigBase.ts deleted file mode 100644 index fc506f1..0000000 --- a/src/webviews/webviewWithConfigBase.ts +++ /dev/null @@ -1,233 +0,0 @@ -import type { ConfigurationChangeEvent, Disposable } from 'vscode'; -import { ConfigurationTarget } from 'vscode'; -import type { CoreConfiguration } from '../constants'; -import { extensionPrefix } from '../constants'; -import type { Container } from '../container'; -import { CommitFormatter } from '../git/formatters/commitFormatter'; -import { GitCommit, GitCommitIdentity } from '../git/models/commit'; -import { GitFileChange, GitFileIndexStatus } from '../git/models/file'; -import { PullRequest, PullRequestState } from '../git/models/pullRequest'; -import type { ConfigPath } from '../system/configuration'; -import { configuration } from '../system/configuration'; -import { map } from '../system/iterable'; -import { Logger } from '../system/logger'; -import type { CustomConfigPath, IpcMessage } from './protocol'; -import { - DidChangeConfigurationNotificationType, - DidGenerateConfigurationPreviewNotificationType, - GenerateConfigurationPreviewCommandType, - isCustomConfigKey, - onIpc, - UpdateConfigurationCommandType, -} from './protocol'; -import type { WebviewController, WebviewProvider } from './webviewController'; - -export abstract class WebviewProviderWithConfigBase implements WebviewProvider { - private readonly _disposable: Disposable; - - constructor(protected readonly container: Container, protected readonly host: WebviewController) { - this._disposable = configuration.onDidChangeAny(this.onAnyConfigurationChanged, this); - } - - dispose() { - this._disposable.dispose(); - } - - onActiveChanged(active: boolean): void { - // Anytime the webview becomes active, make sure it has the most up-to-date config - if (active) { - void this.notifyDidChangeConfiguration(); - } - } - - onMessageReceived(e: IpcMessage): void { - if (e == null) return; - - switch (e.method) { - case UpdateConfigurationCommandType.method: - Logger.debug(`Webview(${this.host.id}).onMessageReceived: method=${e.method}`); - - onIpc(UpdateConfigurationCommandType, e, async params => { - const target = - params.scope === 'workspace' ? ConfigurationTarget.Workspace : ConfigurationTarget.Global; - - let key: keyof typeof params.changes; - for (key in params.changes) { - let value = params.changes[key]; - - if (isCustomConfigKey(key)) { - const customSetting = this.customSettings.get(key); - if (customSetting != null) { - if (typeof value === 'boolean') { - await customSetting.update(value); - } else { - debugger; - } - } - - continue; - } - - const inspect = configuration.inspect(key)!; - - if (value != null) { - if (params.scope === 'workspace') { - if (value === inspect.workspaceValue) continue; - } else { - if (value === inspect.globalValue && value !== inspect.defaultValue) continue; - - if (value === inspect.defaultValue) { - value = undefined; - } - } - } - - await configuration.update(key as any, value, target); - } - - for (const key of params.removes) { - await configuration.update(key as any, undefined, target); - } - }); - break; - - case GenerateConfigurationPreviewCommandType.method: - Logger.debug(`Webview(${this.host.id}).onMessageReceived: method=${e.method}`); - - onIpc(GenerateConfigurationPreviewCommandType, e, async params => { - switch (params.type) { - case 'commit': - case 'commit-uncommitted': { - const commit = new GitCommit( - this.container, - '~/code/eamodio/vscode-gitlens-demo', - 'fe26af408293cba5b4bfd77306e1ac9ff7ccaef8', - new GitCommitIdentity('You', 'eamodio@gmail.com', new Date('2016-11-12T20:41:00.000Z')), - new GitCommitIdentity('You', 'eamodio@gmail.com', new Date('2020-11-01T06:57:21.000Z')), - params.type === 'commit-uncommitted' ? 'Uncommitted changes' : 'Supercharged', - ['3ac1d3f51d7cf5f438cc69f25f6740536ad80fef'], - params.type === 'commit-uncommitted' ? 'Uncommitted changes' : 'Supercharged', - new GitFileChange( - '~/code/eamodio/vscode-gitlens-demo', - 'code.ts', - GitFileIndexStatus.Modified, - ), - undefined, - [], - ); - - let includePullRequest = false; - switch (params.key) { - case configuration.name('currentLine.format'): - includePullRequest = configuration.get('currentLine.pullRequests.enabled'); - break; - case configuration.name('statusBar.format'): - includePullRequest = configuration.get('statusBar.pullRequests.enabled'); - break; - } - - let pr: PullRequest | undefined; - if (includePullRequest) { - pr = new PullRequest( - { id: 'github', name: 'GitHub', domain: 'github.com', icon: 'github' }, - { - name: 'Eric Amodio', - avatarUrl: 'https://avatars1.githubusercontent.com/u/641685?s=32&v=4', - url: 'https://github.com/eamodio', - }, - '1', - 'Supercharged', - 'https://github.com/gitkraken/vscode-gitlens/pulls/1', - PullRequestState.Merged, - new Date('Sat, 12 Nov 2016 19:41:00 GMT'), - undefined, - new Date('Sat, 12 Nov 2016 20:41:00 GMT'), - ); - } - - let preview; - try { - preview = CommitFormatter.fromTemplate(params.format, commit, { - dateFormat: configuration.get('defaultDateFormat'), - pullRequestOrRemote: pr, - messageTruncateAtNewLine: true, - }); - } catch { - preview = 'Invalid format'; - } - - await this.host.notify( - DidGenerateConfigurationPreviewNotificationType, - { preview: preview }, - e.completionId, - ); - } - } - }); - break; - } - } - - private onAnyConfigurationChanged(e: ConfigurationChangeEvent) { - if (!configuration.changedAny(e, extensionPrefix)) { - const notify = configuration.changedAny(e, [ - ...map(this.customSettings.values(), s => s.name), - ]); - if (!notify) return; - } - - void this.notifyDidChangeConfiguration(); - } - - private _customSettings: Map | undefined; - private get customSettings() { - if (this._customSettings == null) { - this._customSettings = new Map([ - [ - 'rebaseEditor.enabled', - { - name: 'workbench.editorAssociations', - enabled: () => this.container.rebaseEditor.enabled, - update: this.container.rebaseEditor.setEnabled, - }, - ], - [ - 'currentLine.useUncommittedChangesFormat', - { - name: 'currentLine.uncommittedChangesFormat', - enabled: () => configuration.get('currentLine.uncommittedChangesFormat') != null, - update: async enabled => - configuration.updateEffective( - 'currentLine.uncommittedChangesFormat', - // eslint-disable-next-line no-template-curly-in-string - enabled ? '✏️ ${ago}' : null, - ), - }, - ], - ]); - } - return this._customSettings; - } - - protected getCustomSettings(): Record { - const customSettings: Record = Object.create(null); - for (const [key, setting] of this.customSettings) { - customSettings[key] = setting.enabled(); - } - return customSettings; - } - - private notifyDidChangeConfiguration() { - // Make sure to get the raw config, not from the container which has the modes mixed in - return this.host.notify(DidChangeConfigurationNotificationType, { - config: configuration.getAll(true), - customSettings: this.getCustomSettings(), - }); - } -} - -interface CustomSetting { - name: ConfigPath | CoreConfiguration; - enabled: () => boolean; - update: (enabled: boolean) => Promise; -}