diff --git a/src/context.ts b/src/context.ts index 9ee9339..1c72dc4 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,17 +1,22 @@ import { commands, EventEmitter } from 'vscode'; import type { ContextKeys } from './constants'; import { CoreCommands } from './constants'; +import type { WebviewIds } from './webviews/webviewBase'; +import type { WebviewViewIds } from './webviews/webviewViewBase'; const contextStorage = new Map(); type WebviewContextKeys = - | `${ContextKeys.WebviewPrefix}${string}:active` - | `${ContextKeys.WebviewPrefix}${string}:focus` - | `${ContextKeys.WebviewPrefix}${string}:inputFocus`; + | `${ContextKeys.WebviewPrefix}${WebviewIds}:active` + | `${ContextKeys.WebviewPrefix}${WebviewIds}:focus` + | `${ContextKeys.WebviewPrefix}${WebviewIds}:inputFocus` + | `${ContextKeys.WebviewPrefix}rebaseEditor:active` + | `${ContextKeys.WebviewPrefix}rebaseEditor:focus` + | `${ContextKeys.WebviewPrefix}rebaseEditor:inputFocus`; type WebviewViewContextKeys = - | `${ContextKeys.WebviewViewPrefix}${string}:focus` - | `${ContextKeys.WebviewViewPrefix}${string}:inputFocus`; + | `${ContextKeys.WebviewViewPrefix}${WebviewViewIds}:focus` + | `${ContextKeys.WebviewViewPrefix}${WebviewViewIds}:inputFocus`; type AllContextKeys = | ContextKeys @@ -23,9 +28,9 @@ type AllContextKeys = const _onDidChangeContext = new EventEmitter(); export const onDidChangeContext = _onDidChangeContext.event; -export function getContext(key: ContextKeys): T | undefined; -export function getContext(key: ContextKeys, defaultValue: T): T; -export function getContext(key: ContextKeys, defaultValue?: T): T | undefined { +export function getContext(key: AllContextKeys): T | undefined; +export function getContext(key: AllContextKeys, defaultValue: T): T; +export function getContext(key: AllContextKeys, defaultValue?: T): T | undefined { return (contextStorage.get(key) as T | undefined) ?? defaultValue; } diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index bb5250e..cceb4f8 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -182,6 +182,10 @@ export class GraphWebview extends WebviewBase { return this._selection; } + get activeSelection(): GitRevisionReference | undefined { + return this._selection?.[0]; + } + private _etagSubscription?: number; private _etagRepository?: number; private _firstSelection = true; @@ -429,23 +433,55 @@ export class GraphWebview extends WebviewBase { } protected override onFocusChanged(focused: boolean): void { - if (focused && this.selection != null) { - void GitActions.Commit.showDetailsView(this.selection[0], { - pin: false, - preserveFocus: true, - preserveVisibility: this._showDetailsView === false, - }); + if (!focused || this.activeSelection == null) { + this._showActiveSelectionDetailsDebounced?.cancel(); + return; } + + this.showActiveSelectionDetails(); + } + + private _showActiveSelectionDetailsDebounced: Deferrable | undefined = + undefined; + + private showActiveSelectionDetails() { + if (this._showActiveSelectionDetailsDebounced == null) { + this._showActiveSelectionDetailsDebounced = debounce(this.showActiveSelectionDetailsCore.bind(this), 250); + } + + this._showActiveSelectionDetailsDebounced(); + } + + private showActiveSelectionDetailsCore() { + const { activeSelection } = this; + if (activeSelection == null) return; + + void GitActions.Commit.showDetailsView(activeSelection, { + pin: false, + preserveFocus: true, + preserveVisibility: this._showDetailsView === false, + }); } protected override onVisibilityChanged(visible: boolean): void { + if (!visible) { + this._showActiveSelectionDetailsDebounced?.cancel(); + } + if (visible && this.repository != null && this.repository.etag !== this._etagRepository) { this.updateState(true); return; } - if (this.isReady && visible) { - this.sendPendingIpcNotifications(); + if (visible) { + if (this.isReady) { + this.sendPendingIpcNotifications(); + } + + const { activeSelection } = this; + if (activeSelection == null) return; + + this.showActiveSelectionDetails(); } } @@ -1944,7 +1980,7 @@ export class GraphWebview extends WebviewBase { refType?: 'revision' | 'stash', ): GitReference | undefined { if (item == null) { - const ref = this.selection?.[0]; + const ref = this.activeSelection; return ref != null && (refType == null || refType === ref.refType) ? ref : undefined; } diff --git a/src/webviews/commitDetails/commitDetailsWebviewView.ts b/src/webviews/commitDetails/commitDetailsWebviewView.ts index 4d6b1e5..4a0f934 100644 --- a/src/webviews/commitDetails/commitDetailsWebviewView.ts +++ b/src/webviews/commitDetails/commitDetailsWebviewView.ts @@ -28,7 +28,7 @@ import { Logger } from '../../logger'; import type { ShowInCommitGraphCommandArgs } from '../../plus/webviews/graph/graphWebview'; import { executeCommand, executeCoreCommand } from '../../system/command'; import type { DateTimeFormat } from '../../system/date'; -import { debug, getLogScope } from '../../system/decorators/log'; +import { debug, getLogScope, log } from '../../system/decorators/log'; import type { Deferrable } from '../../system/function'; import { debounce } from '../../system/function'; import { map, union } from '../../system/iterable'; @@ -116,6 +116,12 @@ export class CommitDetailsWebviewView extends WebviewViewBase({ + args: { + 0: o => + `{"commit":${o?.commit?.ref},"pin":${o?.pin},"preserveFocus":${o?.preserveFocus},"preserveVisibility":${o?.preserveVisibility}}`, + }, + }) override async show(options?: { commit?: GitRevisionReference | GitCommit; pin?: boolean; @@ -255,7 +261,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase { - Disposable.from(...subscriptions).dispose(); - }), - panel.onDidChangeViewState(() => { - if (!context.pendingChange) return; + this.resetContextKeys(); - this.updateState(context); + Disposable.from(...subscriptions).dispose(); }), + panel.onDidChangeViewState(e => this.onViewStateChanged(context, e)), panel.webview.onDidReceiveMessage(e => this.onMessageReceived(context, e)), workspace.onDidChangeTextDocument(e => { if (e.contentChanges.length === 0 || e.document.uri.toString() !== document.uri.toString()) return; @@ -237,6 +246,55 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl } } + private resetContextKeys(): void { + void setContext(`${this.contextKeyPrefix}:inputFocus`, false); + void setContext(`${this.contextKeyPrefix}:focus`, false); + void setContext(`${this.contextKeyPrefix}:active`, false); + } + + private setContextKeys(active: boolean | undefined, focus?: boolean, inputFocus?: boolean): void { + if (active != null) { + void setContext(`${this.contextKeyPrefix}:active`, active); + + if (!active) { + focus = false; + inputFocus = false; + } + } + if (focus != null) { + void setContext(`${this.contextKeyPrefix}:focus`, focus); + } + if (inputFocus != null) { + void setContext(`${this.contextKeyPrefix}:inputFocus`, inputFocus); + } + } + + @debug({ + args: { 0: e => `focused=${e.focused}, inputFocused=${e.inputFocused}` }, + }) + protected onViewFocusChanged(e: WebviewFocusChangedParams): void { + this.setContextKeys(e.focused, e.focused, e.inputFocused); + } + + @debug({ + args: { + 0: c => `${c.id}:${c.document.uri.toString(true)}`, + 1: e => `active=${e.webviewPanel.active}, visible=${e.webviewPanel.visible}`, + }, + }) + protected onViewStateChanged(context: RebaseEditorContext, e: WebviewPanelOnDidChangeViewStateEvent): void { + const { active, visible } = e.webviewPanel; + if (visible) { + this.setContextKeys(active); + } else { + this.resetContextKeys(); + } + + if (!context.pendingChange) return; + + this.updateState(context); + } + private async parseState(context: RebaseEditorContext): Promise { if (context.branchName === undefined) { const branch = await this.container.git.getBranch(context.repoPath); @@ -268,6 +326,13 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl // break; + case WebviewFocusChangedCommandType.method: + onIpc(WebviewFocusChangedCommandType, e, params => { + this.onViewFocusChanged(params); + }); + + break; + case AbortCommandType.method: onIpc(AbortCommandType, e, () => this.abort(context)); diff --git a/src/webviews/webviewBase.ts b/src/webviews/webviewBase.ts index 5656af8..1307019 100644 --- a/src/webviews/webviewBase.ts +++ b/src/webviews/webviewBase.ts @@ -30,6 +30,8 @@ function nextIpcId() { return `host:${ipcSequence}`; } +export type WebviewIds = 'graph' | 'settings' | 'timeline' | 'welcome'; + @logName>((c, name) => `${name}(${c.id})`) export abstract class WebviewBase implements Disposable { protected readonly disposables: Disposable[] = []; @@ -39,11 +41,11 @@ export abstract class WebviewBase implements Disposable { constructor( protected readonly container: Container, - public readonly id: `gitlens.${string}`, + public readonly id: `gitlens.${WebviewIds}`, private readonly fileName: string, private readonly iconPath: string, title: string, - private readonly contextKeyPrefix: `${ContextKeys.WebviewPrefix}${string}`, + private readonly contextKeyPrefix: `${ContextKeys.WebviewPrefix}${WebviewIds}`, private readonly trackingFeature: TrackedUsageFeatures, showCommand: Commands, ) { @@ -164,10 +166,27 @@ export abstract class WebviewBase implements Disposable { this._panel.webview.html = html; } - private resetContextKeys() { - void setContext(`${this.contextKeyPrefix}:active`, false); - void setContext(`${this.contextKeyPrefix}:focus`, false); + private resetContextKeys(): void { void setContext(`${this.contextKeyPrefix}:inputFocus`, false); + void setContext(`${this.contextKeyPrefix}:focus`, false); + void setContext(`${this.contextKeyPrefix}:active`, false); + } + + private setContextKeys(active: boolean | undefined, focus?: boolean, inputFocus?: boolean): void { + if (active != null) { + void setContext(`${this.contextKeyPrefix}:active`, active); + + if (!active) { + focus = false; + inputFocus = false; + } + } + if (focus != null) { + void setContext(`${this.contextKeyPrefix}:focus`, focus); + } + if (inputFocus != null) { + void setContext(`${this.contextKeyPrefix}:inputFocus`, inputFocus); + } } private onPanelDisposed() { @@ -191,8 +210,7 @@ export abstract class WebviewBase implements Disposable { args: { 0: e => `focused=${e.focused}, inputFocused=${e.inputFocused}` }, }) protected onViewFocusChanged(e: WebviewFocusChangedParams): void { - void setContext(`${this.contextKeyPrefix}:focus`, e.focused); - void setContext(`${this.contextKeyPrefix}:inputFocus`, e.inputFocused); + this.setContextKeys(e.focused, e.focused, e.inputFocused); this.onFocusChanged?.(e.focused); } @@ -202,13 +220,7 @@ export abstract class WebviewBase implements Disposable { protected onViewStateChanged(e: WebviewPanelOnDidChangeViewStateEvent): void { const { active, visible } = e.webviewPanel; if (visible) { - // If we are becoming active, delay it a bit to give the UI time to update - if (active) { - setTimeout(() => void setContext(`${this.contextKeyPrefix}:active`, active), 250); - } else { - void setContext(`${this.contextKeyPrefix}:active`, active); - } - + this.setContextKeys(active); this.onActiveChanged?.(active); if (!active) { this.onFocusChanged?.(false); @@ -332,7 +344,9 @@ export abstract class WebviewBase implements Disposable { @serialize() @debug['postMessage']>({ - args: { 0: m => `(id=${m.id}, method=${m.method}${m.completionId ? `, completionId=${m.completionId}` : ''})` }, + args: { + 0: m => `{"id":${m.id},"method":${m.method}${m.completionId ? `,"completionId":${m.completionId}` : ''}}`, + }, }) protected postMessage(message: IpcMessage): Promise { if (this._panel == null || !this.isReady || !this.visible) return Promise.resolve(false); diff --git a/src/webviews/webviewViewBase.ts b/src/webviews/webviewViewBase.ts index e551d02..8b121a3 100644 --- a/src/webviews/webviewViewBase.ts +++ b/src/webviews/webviewViewBase.ts @@ -32,6 +32,8 @@ function nextIpcId() { return `host:${ipcSequence}`; } +export type WebviewViewIds = 'commitDetails' | 'home' | 'timeline'; + @logName>((c, name) => `${name}(${c.id})`) export abstract class WebviewViewBase implements WebviewViewProvider, Disposable { protected readonly disposables: Disposable[] = []; @@ -41,10 +43,10 @@ export abstract class WebviewViewBase implements constructor( protected readonly container: Container, - public readonly id: `gitlens.views.${string}`, + public readonly id: `gitlens.views.${WebviewViewIds}`, protected readonly fileName: string, title: string, - private readonly contextKeyPrefix: `${ContextKeys.WebviewViewPrefix}${string}`, + private readonly contextKeyPrefix: `${ContextKeys.WebviewViewPrefix}${WebviewViewIds}`, private readonly trackingFeature: TrackedUsageFeatures, ) { this._title = title; @@ -159,9 +161,14 @@ export abstract class WebviewViewBase implements this._view.webview.html = html; } - private resetContextKeys() { - void setContext(`${this.contextKeyPrefix}:focus`, false); + private resetContextKeys(): void { void setContext(`${this.contextKeyPrefix}:inputFocus`, false); + void setContext(`${this.contextKeyPrefix}:focus`, false); + } + + private setContextKeys(focus: boolean, inputFocus: boolean): void { + void setContext(`${this.contextKeyPrefix}:focus`, focus); + void setContext(`${this.contextKeyPrefix}:inputFocus`, inputFocus); } private onViewDisposed() { @@ -180,8 +187,7 @@ export abstract class WebviewViewBase implements args: { 0: e => `focused=${e.focused}, inputFocused=${e.inputFocused}` }, }) protected onViewFocusChanged(e: WebviewFocusChangedParams): void { - void setContext(`${this.contextKeyPrefix}:inputFocus`, e.inputFocused); - void setContext(`${this.contextKeyPrefix}:focus`, e.focused); + this.setContextKeys(e.focused, e.inputFocused); this.onFocusChanged?.(e.focused); } @@ -321,7 +327,9 @@ export abstract class WebviewViewBase implements @serialize() @debug['postMessage']>({ - args: { 0: m => `(id=${m.id}, method=${m.method}${m.completionId ? `, completionId=${m.completionId}` : ''})` }, + args: { + 0: m => `{"id":${m.id},"method":${m.method}${m.completionId ? `,"completionId":${m.completionId}` : ''}}`, + }, }) protected postMessage(message: IpcMessage) { if (this._view == null || !this.isReady) return Promise.resolve(false); diff --git a/src/webviews/webviewWithConfigBase.ts b/src/webviews/webviewWithConfigBase.ts index 41d12c1..880def6 100644 --- a/src/webviews/webviewWithConfigBase.ts +++ b/src/webviews/webviewWithConfigBase.ts @@ -17,16 +17,17 @@ import { onIpc, UpdateConfigurationCommandType, } from './protocol'; +import type { WebviewIds } from './webviewBase'; import { WebviewBase } from './webviewBase'; export abstract class WebviewWithConfigBase extends WebviewBase { constructor( container: Container, - id: `gitlens.${string}`, + id: `gitlens.${WebviewIds}`, fileName: string, iconPath: string, title: string, - contextKeyPrefix: `${ContextKeys.WebviewPrefix}${string}`, + contextKeyPrefix: `${ContextKeys.WebviewPrefix}${WebviewIds}`, trackingFeature: TrackedUsageFeatures, showCommand: Commands, ) {