Browse Source

Adds queue for pending notifications

Allows for background updates to still notify the graph of changes
Removes extra notifications on graph load
main
Eric Amodio 2 years ago
parent
commit
d8c2b56a87
4 changed files with 170 additions and 58 deletions
  1. +135
    -46
      src/plus/webviews/graph/graphWebview.ts
  2. +5
    -0
      src/plus/webviews/graph/protocol.ts
  3. +1
    -1
      src/webviews/protocol.ts
  4. +29
    -11
      src/webviews/webviewBase.ts

+ 135
- 46
src/plus/webviews/graph/graphWebview.ts View File

@ -1,4 +1,12 @@
import type { ColorTheme, ConfigurationChangeEvent, Disposable, Event, StatusBarItem } from 'vscode';
import type {
ColorTheme,
ConfigurationChangeEvent,
Disposable,
Event,
StatusBarItem,
WebviewOptions,
WebviewPanelOptions,
} from 'vscode';
import { CancellationTokenSource, EventEmitter, MarkdownString, StatusBarAlignment, ViewColumn, window } from 'vscode';
import type { CreatePullRequestActionContext } from '../../../api/gitlens';
import { getAvatarUri } from '../../../avatars';
@ -40,8 +48,8 @@ import { isDarkTheme, isLightTheme } from '../../../system/utils';
import type { WebviewItemContext } from '../../../system/webview';
import { isWebviewItemContext, serializeWebviewItemContext } from '../../../system/webview';
import { RepositoryFolderNode } from '../../../views/nodes/viewNode';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { IpcMessage, IpcMessageParams, IpcNotificationType } from '../../../webviews/protocol';
import { WebviewBase } from '../../../webviews/webviewBase';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import { ensurePlusFeaturesEnabled } from '../../subscription/utils';
@ -123,7 +131,7 @@ export class GraphWebview extends WebviewBase {
private _etagSubscription?: number;
private _etagRepository?: number;
private _graph?: GitGraph;
private _pendingNotifyCommits: boolean = false;
private _pendingIpcNotifications = new Map<IpcNotificationType, IpcMessage | (() => Promise<boolean>)>();
private _search: GitSearch | undefined;
private _searchCancellation: CancellationTokenSource | undefined;
private _selectedSha?: string;
@ -148,13 +156,7 @@ export class GraphWebview extends WebviewBase {
);
this.disposables.push(
configuration.onDidChange(this.onConfigurationChanged, this),
{
dispose: () => {
this._statusBarItem?.dispose();
void this._repositoryEventsDisposable?.dispose();
},
},
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
{ dispose: () => this._statusBarItem?.dispose() },
registerCommand(Commands.ShowCommitInGraph, (args: ShowCommitInGraphCommandArgs) => {
this.repository = this.container.git.getRepository(args.repoPath);
this.setSelectedRows(args.sha);
@ -176,6 +178,15 @@ export class GraphWebview extends WebviewBase {
this.onConfigurationChanged();
}
protected override get options(): WebviewPanelOptions & WebviewOptions {
return {
retainContextWhenHidden: true,
enableFindWidget: false,
enableCommandUris: true,
enableScripts: true,
};
}
override async show(options?: { column?: ViewColumn; preserveFocus?: boolean }, ...args: unknown[]): Promise<void> {
if (!(await ensurePlusFeaturesEnabled())) return;
@ -259,13 +270,15 @@ export class GraphWebview extends WebviewBase {
protected override onInitializing(): Disposable[] | undefined {
this._theme = window.activeColorTheme;
return [window.onDidChangeActiveColorTheme(this.onThemeChanged, this)];
return [
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
window.onDidChangeActiveColorTheme(this.onThemeChanged, this),
{ dispose: () => void this._repositoryEventsDisposable?.dispose() },
];
}
protected override onReady(): void {
if (this._pendingNotifyCommits) {
void this.notifyDidChangeCommits();
}
this.sendPendingIpcNotifications();
}
protected override onMessageReceived(e: IpcMessage) {
@ -315,7 +328,10 @@ export class GraphWebview extends WebviewBase {
protected override onVisibilityChanged(visible: boolean): void {
if (visible && this.repository != null && this.repository.etag !== this._etagRepository) {
this.updateState(true);
return;
}
this.sendPendingIpcNotifications();
}
private onConfigurationChanged(e?: ConfigurationChangeEvent) {
@ -345,14 +361,17 @@ export class GraphWebview extends WebviewBase {
}
}
if (configuration.changed(e, 'graph.commitOrdering')) {
// If we don't have an open webview ignore the rest
if (this._panel == null) return;
if (e != null && configuration.changed(e, 'graph.commitOrdering')) {
this.updateState();
return;
}
if (
configuration.changed(e, 'defaultDateFormat') ||
(e != null && configuration.changed(e, 'defaultDateFormat')) ||
configuration.changed(e, 'defaultDateStyle') ||
configuration.changed(e, 'advanced.abbreviatedShaLength') ||
configuration.changed(e, 'graph.avatars') ||
@ -607,7 +626,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private updateState(immediate: boolean = false) {
if (!this.isReady || !this.visible) return;
this._pendingIpcNotifications.clear();
if (immediate) {
void this.notifyDidChangeState();
@ -625,8 +644,6 @@ export class GraphWebview extends WebviewBase {
@debug()
private updateAvatars(immediate: boolean = false) {
if (!this.isReady || !this.visible) return;
if (immediate) {
void this.notifyDidChangeAvatars();
return;
@ -641,9 +658,9 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeAvatars() {
if (!this.isReady || !this.visible) return false;
if (this._graph == null) return;
const data = this._graph!;
const data = this._graph;
return this.notify(DidChangeAvatarsNotificationType, {
avatars: Object.fromEntries(data.avatars),
});
@ -651,7 +668,10 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeColumns() {
if (!this.isReady || !this.visible) return false;
if (!this.isReady || !this.visible) {
this.addPendingIpcNotification(DidChangeColumnsNotificationType);
return false;
}
const columns = this.getColumns();
return this.notify(DidChangeColumnsNotificationType, {
@ -662,7 +682,10 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeConfiguration() {
if (!this.isReady || !this.visible) return false;
if (!this.isReady || !this.visible) {
this.addPendingIpcNotification(DidChangeGraphConfigurationNotificationType);
return false;
}
return this.notify(DidChangeGraphConfigurationNotificationType, {
config: this.getComponentConfig(),
@ -671,32 +694,30 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeCommits(completionId?: string) {
let success = false;
if (this.isReady && this.visible) {
const data = this._graph!;
success = await this.notify(
DidChangeCommitsNotificationType,
{
rows: data.rows,
avatars: Object.fromEntries(data.avatars),
selectedRows: this._selectedRows,
paging: {
startingCursor: data.paging?.startingCursor,
hasMore: data.paging?.hasMore ?? false,
},
},
completionId,
);
}
if (this._graph == null) return;
this._pendingNotifyCommits = !success;
return success;
const data = this._graph;
return this.notify(
DidChangeCommitsNotificationType,
{
rows: data.rows,
avatars: Object.fromEntries(data.avatars),
selectedRows: this._selectedRows,
paging: {
startingCursor: data.paging?.startingCursor,
hasMore: data.paging?.hasMore ?? false,
},
},
completionId,
);
}
@debug()
private async notifyDidChangeSelection() {
if (!this.isReady || !this.visible) return false;
if (!this.isReady || !this.visible) {
this.addPendingIpcNotification(DidChangeSelectionNotificationType);
return false;
}
return this.notify(DidChangeSelectionNotificationType, {
selection: this._selectedRows,
@ -705,7 +726,10 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeSubscription() {
if (!this.isReady || !this.visible) return false;
if (!this.isReady || !this.visible) {
this.addPendingIpcNotification(DidChangeSubscriptionNotificationType);
return false;
}
const access = await this.getGraphAccess();
return this.notify(DidChangeSubscriptionNotificationType, {
@ -716,11 +740,76 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeState() {
if (!this.isReady || !this.visible) return false;
if (!this.isReady || !this.visible) {
this.addPendingIpcNotification(DidChangeNotificationType);
return false;
}
return this.notify(DidChangeNotificationType, { state: await this.getState() });
}
protected override async notify<T extends IpcNotificationType<any>>(
type: T,
params: IpcMessageParams<T>,
completionId?: string,
): Promise<boolean> {
const msg: IpcMessage = {
id: this.nextIpcId(),
method: type.method,
params: params,
completionId: completionId,
};
const success = await this.postMessage(msg);
if (success) {
this._pendingIpcNotifications.clear();
} else {
this.addPendingIpcNotification(type, msg);
}
return success;
}
private readonly _ipcNotificationMap = new Map<IpcNotificationType<any>, () => Promise<boolean>>([
[DidChangeColumnsNotificationType, this.notifyDidChangeColumns],
[DidChangeGraphConfigurationNotificationType, this.notifyDidChangeConfiguration],
[DidChangeNotificationType, this.notifyDidChangeState],
[DidChangeSelectionNotificationType, this.notifyDidChangeSelection],
[DidChangeSubscriptionNotificationType, this.notifyDidChangeSubscription],
]);
private addPendingIpcNotification(type: IpcNotificationType<any>, msg?: IpcMessage) {
if (type === DidChangeNotificationType) {
this._pendingIpcNotifications.clear();
} else if (type.overwriteable) {
this._pendingIpcNotifications.delete(type);
}
let msgOrFn: IpcMessage | (() => Promise<boolean>) | undefined;
if (msg == null) {
msgOrFn = this._ipcNotificationMap.get(type)?.bind(this);
if (msgOrFn == null) {
debugger;
return;
}
} else {
msgOrFn = msg;
}
this._pendingIpcNotifications.set(type, msgOrFn);
}
private sendPendingIpcNotifications() {
if (this._pendingIpcNotifications.size === 0) return;
const ipcs = new Map(this._pendingIpcNotifications);
this._pendingIpcNotifications.clear();
for (const msgOrFn of ipcs.values()) {
if (typeof msgOrFn === 'function') {
void msgOrFn();
} else {
void this.postMessage(msgOrFn);
}
}
}
private getColumns(): Record<GraphColumnName, GraphColumnConfig> | undefined {
return this.container.storage.getWorkspace('graph:columns');
}

+ 5
- 0
src/plus/webviews/graph/protocol.ts View File

@ -148,6 +148,7 @@ export interface DidChangeGraphConfigurationParams {
}
export const DidChangeGraphConfigurationNotificationType = new IpcNotificationType<DidChangeGraphConfigurationParams>(
'graph/configuration/didChange',
true,
);
export interface DidChangeSubscriptionParams {
@ -156,6 +157,7 @@ export interface DidChangeSubscriptionParams {
}
export const DidChangeSubscriptionNotificationType = new IpcNotificationType<DidChangeSubscriptionParams>(
'graph/subscription/didChange',
true,
);
export interface DidChangeAvatarsParams {
@ -171,6 +173,7 @@ export interface DidChangeColumnsParams {
}
export const DidChangeColumnsNotificationType = new IpcNotificationType<DidChangeColumnsParams>(
'graph/columns/didChange',
true,
);
export interface DidChangeCommitsParams {
@ -188,6 +191,7 @@ export interface DidChangeSelectionParams {
}
export const DidChangeSelectionNotificationType = new IpcNotificationType<DidChangeSelectionParams>(
'graph/selection/didChange',
true,
);
export interface DidEnsureCommitParams {
@ -204,4 +208,5 @@ export interface DidSearchCommitsParams {
}
export const DidSearchCommitsNotificationType = new IpcNotificationType<DidSearchCommitsParams>(
'graph/commits/didSearch',
true,
);

+ 1
- 1
src/webviews/protocol.ts View File

@ -9,7 +9,7 @@ export interface IpcMessage {
abstract class IpcMessageType<Params = void> {
_?: Params; // Required for type inferencing to work properly
constructor(public readonly method: string) {}
constructor(public readonly method: string, public readonly overwriteable: boolean = false) {}
}
export type IpcMessageParams<T> = T extends IpcMessageType<infer P> ? P : never;

+ 29
- 11
src/webviews/webviewBase.ts View File

@ -1,4 +1,10 @@
import type { Webview, WebviewPanel, WebviewPanelOnDidChangeViewStateEvent } from 'vscode';
import type {
Webview,
WebviewOptions,
WebviewPanel,
WebviewPanelOnDidChangeViewStateEvent,
WebviewPanelOptions,
} from 'vscode';
import { Disposable, Uri, ViewColumn, window, workspace } from 'vscode';
import { getNonce } from '@env/crypto';
import type { Commands } from '../constants';
@ -49,6 +55,14 @@ export abstract class WebviewBase implements Disposable {
this._disposablePanel?.dispose();
}
protected get options(): WebviewPanelOptions & WebviewOptions {
return {
retainContextWhenHidden: true,
enableFindWidget: true,
enableCommandUris: true,
enableScripts: true,
};
}
private _originalTitle: string | undefined;
get originalTitle(): string | undefined {
return this._originalTitle;
@ -89,12 +103,7 @@ export abstract class WebviewBase implements Disposable {
this.id,
this._title,
{ viewColumn: column, preserveFocus: options?.preserveFocus ?? false },
{
retainContextWhenHidden: true,
enableFindWidget: true,
enableCommandUris: true,
enableScripts: true,
},
this.options,
);
this._panel.iconPath = Uri.file(this.container.context.asAbsolutePath(this.iconPath));
@ -249,20 +258,29 @@ export abstract class WebviewBase implements Disposable {
return html;
}
protected nextIpcId(): string {
return nextIpcId();
}
protected notify<T extends IpcNotificationType<any>>(
type: T,
params: IpcMessageParams<T>,
completionId?: string,
): Thenable<boolean> {
return this.postMessage({ id: nextIpcId(), method: type.method, params: params, completionId: completionId });
): Promise<boolean> {
return this.postMessage({
id: this.nextIpcId(),
method: type.method,
params: params,
completionId: completionId,
});
}
@serialize()
@debug<WebviewBase<State>['postMessage']>({
args: { 0: m => `(id=${m.id}, method=${m.method}${m.completionId ? `, completionId=${m.completionId}` : ''})` },
})
private postMessage(message: IpcMessage): Promise<boolean> {
if (this._panel == null) return Promise.resolve(false);
protected postMessage(message: IpcMessage): Promise<boolean> {
if (this._panel == null || !this.isReady || !this.visible) return Promise.resolve(false);
// It looks like there is a bug where `postMessage` can sometimes just hang infinitely. Not sure why, but ensure we don't hang
return Promise.race<boolean>([

Loading…
Cancel
Save