Browse Source

Adopts new webview model for Commit Details

main
Eric Amodio 1 year ago
parent
commit
864982a396
9 changed files with 929 additions and 902 deletions
  1. +7
    -7
      src/container.ts
  2. +27
    -8
      src/eventBus.ts
  3. +2
    -1
      src/git/actions/commit.ts
  4. +2
    -1
      src/git/actions/stash.ts
  5. +99
    -126
      src/webviews/commitDetails/commitDetailsWebview.ts
  6. +19
    -0
      src/webviews/commitDetails/registration.ts
  7. +5
    -2
      src/webviews/webviewController.ts
  8. +15
    -4
      src/webviews/webviewsController.ts

+ 7
- 7
src/container.ts View File

@ -55,7 +55,7 @@ import { ViewCommands } from './views/viewCommands';
import { ViewFileDecorationProvider } from './views/viewDecorationProvider';
import { WorktreesView } from './views/worktreesView';
import { VslsController } from './vsls/vsls';
import { CommitDetailsWebviewView } from './webviews/commitDetails/commitDetailsWebviewView';
import { registerCommitDetailsWebviewView } from './webviews/commitDetails/registration';
import { registerHomeWebviewView } from './webviews/home/registration';
import { RebaseEditorProvider } from './webviews/rebase/rebaseEditor';
import { registerSettingsWebviewCommands, registerSettingsWebviewView } from './webviews/settings/registration';
@ -228,7 +228,11 @@ export class Container {
context.subscriptions.splice(0, 0, new ViewFileDecorationProvider());
context.subscriptions.splice(0, 0, (this._repositoriesView = new RepositoriesView(this)));
context.subscriptions.splice(0, 0, (this._commitDetailsView = new CommitDetailsWebviewView(this)));
context.subscriptions.splice(
0,
0,
(this._commitDetailsView = registerCommitDetailsWebviewView(this._webviews)),
);
context.subscriptions.splice(0, 0, (this._commitsView = new CommitsView(this)));
context.subscriptions.splice(0, 0, (this._fileHistoryView = new FileHistoryView(this)));
context.subscriptions.splice(0, 0, (this._lineHistoryView = new LineHistoryView(this)));
@ -346,12 +350,8 @@ export class Container {
return this._commitsView;
}
private _commitDetailsView: CommitDetailsWebviewView | undefined;
private _commitDetailsView: WebviewViewProxy;
get commitDetailsView() {
if (this._commitDetailsView == null) {
this._context.subscriptions.splice(0, 0, (this._commitDetailsView = new CommitDetailsWebviewView(this)));
}
return this._commitDetailsView;
}

+ 27
- 8
src/eventBus.ts View File

@ -27,15 +27,15 @@ interface GitCacheResetEventArgs {
readonly caches?: GitCaches[];
}
type EventBusEventMap = {
type EventsMapping = {
'commit:selected': CommitSelectedEventArgs;
'file:selected': FileSelectedEventArgs;
'git:cache:reset': GitCacheResetEventArgs;
};
interface EventBusEvent<T extends keyof EventBusEventMap = keyof EventBusEventMap> {
interface EventBusEvent<T extends keyof EventsMapping = keyof EventsMapping> {
name: T;
data: EventBusEventMap[T];
data: EventsMapping[T];
source?: EventBusSource | undefined;
}
@ -49,6 +49,14 @@ export type EventBusOptions = {
source?: EventBusSource;
};
type CacheableEventsMapping = {
'commit:selected': CommitSelectedEventArgs;
'file:selected': FileSelectedEventArgs;
};
const _cacheableEventNames = new Set<keyof CacheableEventsMapping>(['commit:selected', 'file:selected']);
const _cachedEventArgs = new Map<keyof CacheableEventsMapping, CacheableEventsMapping[keyof CacheableEventsMapping]>();
export class EventBus implements Disposable {
private readonly _emitter = new EventEmitter<EventBusEvent>();
private get event() {
@ -59,7 +67,10 @@ export class EventBus implements Disposable {
this._emitter.dispose();
}
fire<T extends keyof EventBusEventMap>(name: T, data: EventBusEventMap[T], options?: EventBusOptions) {
fire<T extends keyof EventsMapping>(name: T, data: EventsMapping[T], options?: EventBusOptions) {
if (canCacheEventArgs(name)) {
_cachedEventArgs.set(name, data as CacheableEventsMapping[typeof name]);
}
this._emitter.fire({
name: name,
data: data,
@ -67,12 +78,16 @@ export class EventBus implements Disposable {
});
}
fireAsync<T extends keyof EventBusEventMap>(name: T, data: EventBusEventMap[T], options?: EventBusOptions) {
fireAsync<T extends keyof EventsMapping>(name: T, data: EventsMapping[T], options?: EventBusOptions) {
queueMicrotask(() => this.fire(name, data, options));
}
on<T extends keyof EventBusEventMap>(
eventName: T,
getCachedEventArgs<T extends keyof CacheableEventsMapping>(name: T): CacheableEventsMapping[T] | undefined {
return _cachedEventArgs.get(name) as CacheableEventsMapping[T] | undefined;
}
on<T extends keyof EventsMapping>(
name: T,
handler: (e: EventBusEvent<T>) => void,
thisArgs?: unknown,
disposables?: Disposable[],
@ -80,7 +95,7 @@ export class EventBus implements Disposable {
return this.event(
// eslint-disable-next-line prefer-arrow-callback
function (e) {
if (eventName !== e.name) return;
if (name !== e.name) return;
handler.call(thisArgs, e as EventBusEvent<T>);
},
thisArgs,
@ -88,3 +103,7 @@ export class EventBus implements Disposable {
);
}
}
function canCacheEventArgs(name: keyof EventsMapping): name is keyof CacheableEventsMapping {
return _cacheableEventNames.has(name as keyof CacheableEventsMapping);
}

+ 2
- 1
src/git/actions/commit.ts View File

@ -589,7 +589,8 @@ export function showDetailsView(
commit: GitRevisionReference | GitCommit,
options?: { pin?: boolean; preserveFocus?: boolean; preserveVisibility?: boolean },
): Promise<void> {
return Container.instance.commitDetailsView.show({ ...options, commit: commit });
const { preserveFocus, ...opts } = { ...options, commit: commit };
return Container.instance.commitDetailsView.show({ preserveFocus: preserveFocus }, opts);
}
export async function showInCommitGraph(

+ 2
- 1
src/git/actions/stash.ts View File

@ -68,5 +68,6 @@ export function showDetailsView(
stash: GitStashReference | GitStashCommit,
options?: { pin?: boolean; preserveFocus?: boolean },
): Promise<void> {
return Container.instance.commitDetailsView.show({ ...options, commit: stash });
const { preserveFocus, ...opts } = { ...options, commit: stash };
return Container.instance.commitDetailsView.show({ preserveFocus: preserveFocus }, opts);
}

src/webviews/commitDetails/commitDetailsWebviewView.ts → src/webviews/commitDetails/commitDetailsWebview.ts View File

@ -30,7 +30,7 @@ import type { ShowInCommitGraphCommandArgs } from '../../plus/webviews/graph/gra
import { executeCommand, executeCoreCommand } from '../../system/command';
import { configuration } from '../../system/configuration';
import type { DateTimeFormat } from '../../system/date';
import { debug, log } from '../../system/decorators/log';
import { debug } from '../../system/decorators/log';
import type { Deferrable } from '../../system/function';
import { debounce } from '../../system/function';
import { map, union } from '../../system/iterable';
@ -41,14 +41,10 @@ import { getSettledValue } from '../../system/promise';
import type { Serialized } from '../../system/serialize';
import { serialize } from '../../system/serialize';
import type { LinesChangeEvent } from '../../trackers/lineTracker';
import { CommitFileNode } from '../../views/nodes/commitFileNode';
import { CommitNode } from '../../views/nodes/commitNode';
import { FileRevisionAsCommitNode } from '../../views/nodes/fileRevisionAsCommitNode';
import { StashFileNode } from '../../views/nodes/stashFileNode';
import { StashNode } from '../../views/nodes/stashNode';
import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol';
import { WebviewViewBase } from '../webviewViewBase';
import type { WebviewController, WebviewProvider } from '../webviewController';
import type { WebviewIds, WebviewViewIds } from '../webviewsController';
import type { CommitDetails, FileActionParams, Preferences, State } from './protocol';
import {
AutolinkSettingsCommandType,
@ -81,29 +77,29 @@ interface Context {
indentGuides: 'none' | 'onHover' | 'always';
}
export class CommitDetailsWebviewView extends WebviewViewBase<State, Serialized<State>> {
export class CommitDetailsWebviewProvider implements WebviewProvider<State, Serialized<State>> {
private _bootstraping = true;
/** The context the webview has */
private _context: Context;
/** The context the webview should have */
private _pendingContext: Partial<Context> | undefined;
private readonly _disposable: Disposable;
private _pinned = false;
constructor(container: Container) {
super(
container,
'gitlens.views.commitDetails',
'commitDetails.html',
'Commit Details',
`${ContextKeys.WebviewViewPrefix}commitDetails`,
'commitDetailsView',
);
constructor(
readonly container: Container,
readonly id: `gitlens.${WebviewIds}` | `gitlens.views.${WebviewViewIds}`,
readonly host: WebviewController<State, Serialized<State>>,
) {
this._context = {
pinned: false,
commit: undefined,
preferences: undefined,
preferences: {
autolinksExpanded: this.container.storage.getWorkspace('views:commitDetails:autolinksExpanded'),
avatars: configuration.get('views.commitDetails.avatars'),
dismissed: this.container.storage.get('views:commitDetails:dismissed'),
files: configuration.get('views.commitDetails.files'),
},
richStateLoaded: false,
formattedMessage: undefined,
autolinkedIssues: undefined,
@ -113,59 +109,56 @@ export class CommitDetailsWebviewView extends WebviewViewBase
indentGuides: configuration.getAny('workbench.tree.renderIndentGuides') ?? 'onHover',
};
this.disposables.push(
this._disposable = Disposable.from(
configuration.onDidChange(this.onConfigurationChanged, this),
configuration.onDidChangeAny(this.onAnyConfigurationChanged, this),
this.container.events.on('commit:selected', debounce(this.onCommitSelected, 250), this),
);
}
onCommitSelected(e: CommitSelectedEvent) {
if (e.data == null) return;
dispose() {
this._disposable.dispose();
}
void this.show(e.data);
}
@log<CommitDetailsWebviewView['show']>({
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;
preserveFocus?: boolean | undefined;
preserveVisibility?: boolean | undefined;
}): Promise<void> {
if (this._pinned && !options?.pin && this.visible) return;
if (options != null) {
let commit;
let pin;
({ commit, pin, ...options } = options);
if (commit == null) {
commit = this.getBestCommitOrStash();
}
if (commit != null && !this._context.commit?.ref.startsWith(commit.ref)) {
if (!isCommit(commit)) {
if (commit.refType === 'stash') {
const stash = await this.container.git.getStash(commit.repoPath);
commit = stash?.commits.get(commit.ref);
} else {
commit = await this.container.git.getCommit(commit.repoPath, commit.ref);
}
}
this.updateCommit(commit, { pinned: pin ?? false });
async canShowWebviewView(
_firstTime: boolean,
options: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
): Promise<boolean> {
let data = args[0] as Partial<CommitSelectedEvent['data']> | undefined;
if (typeof data !== 'object') {
data = undefined;
}
if (this._pinned && !data?.pin && this.host.visible) return false;
let commit;
let pin;
if (data != null) {
if (data.preserveFocus) {
options.preserveFocus = true;
}
({ commit, pin, ...data } = data);
}
if (commit == null) {
commit = this.getBestCommitOrStash();
}
if (commit != null && !this._context.commit?.ref.startsWith(commit.ref)) {
await this.updateCommit(commit, { pinned: pin ?? false });
}
if (options?.preserveVisibility) return;
if (data?.preserveVisibility) return false;
return super.show(options);
return true;
}
protected override async includeBootstrap(): Promise<Serialized<State>> {
private onCommitSelected(e: CommitSelectedEvent) {
if (e.data == null) return;
void this.canShowWebviewView(false, { preserveFocus: e.data.preserveFocus }, e.data);
}
includeBootstrap(): Promise<Serialized<State>> {
this._bootstraping = true;
this._context = { ...this._context, ...this._pendingContext };
@ -174,30 +167,8 @@ export class CommitDetailsWebviewView extends WebviewViewBase
return this.getState(this._context);
}
protected override onInitializing(): Disposable[] | undefined {
if (this._context.preferences == null) {
this.updatePendingContext({
preferences: {
autolinksExpanded: this.container.storage.getWorkspace('views:commitDetails:autolinksExpanded'),
avatars: configuration.get('views.commitDetails.avatars'),
dismissed: this.container.storage.get('views:commitDetails:dismissed'),
files: configuration.get('views.commitDetails.files'),
},
});
}
if (this._context.commit == null) {
const commit = this.getBestCommitOrStash();
if (commit != null) {
this.updateCommit(commit, { immediate: false });
}
}
return undefined;
}
private _visibilityDisposable: Disposable | undefined;
protected override onVisibilityChanged(visible: boolean) {
onVisibilityChanged(visible: boolean) {
this.ensureTrackers();
if (!visible) return;
@ -251,7 +222,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase
(configuration.changed(e, 'views.commitDetails.autolinks') ||
configuration.changed(e, 'views.commitDetails.pullRequests'))
) {
this.updateCommit(this._context.commit, { force: true });
void this.updateCommit(this._context.commit, { force: true });
}
this.updateState();
@ -262,22 +233,27 @@ export class CommitDetailsWebviewView extends WebviewViewBase
this._visibilityDisposable?.dispose();
this._visibilityDisposable = undefined;
if (this._pinned || !this.visible) return;
if (this._pinned || !this.host.visible) return;
const { lineTracker } = this.container;
this._visibilityDisposable = Disposable.from(
this.container.events.on('commit:selected', debounce(this.onCommitSelected, 250), this),
lineTracker.subscribe(this, lineTracker.onDidChangeActiveLines(this.onActiveLinesChanged, this)),
);
const commit = this._pendingContext?.commit ?? this.getBestCommitOrStash();
this.updateCommit(commit, { immediate: false });
}
protected override onReady(): void {
onReady(): void {
this.updateState(false);
}
protected override onMessageReceived(e: IpcMessage) {
onRefresh(_force?: boolean | undefined): void {
if (this._pinned) return;
const commit = this._pendingContext?.commit ?? this.getBestCommitOrStash();
void this.updateCommit(commit, { immediate: false });
}
onMessageReceived(e: IpcMessage) {
switch (e.method) {
case OpenFileOnRemoteCommandType.method:
onIpc(OpenFileOnRemoteCommandType, e, params => void this.openFileOnRemote(params));
@ -359,7 +335,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase
commit =
e.selections != null ? this.container.lineTracker.getState(e.selections[0].active)?.commit : undefined;
}
this.updateCommit(commit);
void this.updateCommit(commit);
}
private _cancellationTokenSource: CancellationTokenSource | undefined = undefined;
@ -461,15 +437,27 @@ export class CommitDetailsWebviewView extends WebviewViewBase
private _commitDisposable: Disposable | undefined;
private updateCommit(
commit: GitCommit | undefined,
private async updateCommit(
commitish: GitCommit | GitRevisionReference | undefined,
options?: { force?: boolean; pinned?: boolean; immediate?: boolean },
) {
// this.commits = [commit];
if (!options?.force && this._context.commit?.sha === commit?.sha) return;
if (!options?.force && this._context.commit?.sha === commitish?.ref) return;
this._commitDisposable?.dispose();
let commit: GitCommit | undefined;
if (isCommit(commitish)) {
commit = commitish;
} else if (commitish != null) {
if (commitish.refType === 'stash') {
const stash = await this.container.git.getStash(commitish.repoPath);
commit = stash?.commits.get(commitish.ref);
} else {
commit = await this.container.git.getCommit(commitish.repoPath, commitish.ref);
}
}
if (commit?.isUncommitted) {
const repository = this.container.git.getRepository(commit.repoPath)!;
this._commitDisposable = Disposable.from(
@ -604,7 +592,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase
private _notifyDidChangeStateDebounced: Deferrable<() => void> | undefined = undefined;
private updateState(immediate: boolean = false) {
if (!this.isReady || !this.visible) return;
if (!this.host.isReady || !this.host.visible) return;
if (immediate) {
void this.notifyDidChangeState();
@ -619,7 +607,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase
}
private async notifyDidChangeState() {
if (!this.isReady || !this.visible) return false;
if (!this.host.isReady || !this.host.visible) return false;
const scope = getLogScope();
@ -630,7 +618,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase
return window.withProgress({ location: { viewId: this.id } }, async () => {
try {
const success = await this.notify(DidChangeNotificationType, {
const success = await this.host.notify(DidChangeNotificationType, {
state: await this.getState(context),
});
if (success) {
@ -653,7 +641,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase
// }
// }
private getBestCommitOrStash(): GitCommit | undefined {
private getBestCommitOrStash(): GitCommit | GitRevisionReference | undefined {
if (this._pinned) return undefined;
let commit;
@ -663,28 +651,13 @@ export class CommitDetailsWebviewView extends WebviewViewBase
const line = lineTracker.selections?.[0].active;
if (line != null) {
commit = lineTracker.getState(line)?.commit;
if (commit != null) return commit;
}
} else if (getContext('gitlens:webview:graph:active') || getContext('gitlens:webview:rebaseEditor:active')) {
commit = this._pendingContext?.commit ?? this._context.commit;
if (commit != null) return commit;
}
const { commitsView } = this.container;
let node = commitsView.activeSelection;
if (
node != null &&
(node instanceof CommitNode || node instanceof FileRevisionAsCommitNode || node instanceof CommitFileNode)
) {
commit = node.commit;
if (commit != null) return commit;
}
const { stashesView } = this.container;
node = stashesView.activeSelection;
if (node != null && (node instanceof StashNode || node instanceof StashFileNode)) {
commit = node.commit;
if (commit != null) return commit;
} else {
commit = this._pendingContext?.commit;
if (commit == null) {
const args = this.container.events.getCachedEventArgs('commit:selected');
commit = args?.commit;
}
}
return commit;
@ -731,12 +704,12 @@ export class CommitDetailsWebviewView extends WebviewViewBase
status: status,
repoPath: repoPath,
icon: {
dark: this._view!.webview.asWebviewUri(
Uri.joinPath(this.container.context.extensionUri, 'images', 'dark', icon),
).toString(),
light: this._view!.webview.asWebviewUri(
Uri.joinPath(this.container.context.extensionUri, 'images', 'light', icon),
).toString(),
dark: this.host
.asWebviewUri(Uri.joinPath(this.host.getRootUri(), 'images', 'dark', icon))
.toString(),
light: this.host
.asWebviewUri(Uri.joinPath(this.host.getRootUri(), 'images', 'light', icon))
.toString(),
},
};
}),

+ 19
- 0
src/webviews/commitDetails/registration.ts View File

@ -0,0 +1,19 @@
import { ContextKeys } from '../../constants';
import type { Serialized } from '../../system/serialize';
import type { WebviewsController } from '../webviewsController';
import type { State } from './protocol';
export function registerCommitDetailsWebviewView(controller: WebviewsController) {
return controller.registerWebviewView<State, Serialized<State>>('gitlens.views.commitDetails', {
fileName: 'commitDetails.html',
title: 'Commit Details',
contextKeyPrefix: `${ContextKeys.WebviewViewPrefix}commitDetails`,
trackingFeature: 'commitDetailsView',
resolveWebviewProvider: async function (container, id, host) {
const { CommitDetailsWebviewProvider } = await import(
/* webpackChunkName: "commitDetails" */ './commitDetailsWebview'
);
return new CommitDetailsWebviewProvider(container, id, host);
},
});
}

+ 5
- 2
src/webviews/webviewController.ts View File

@ -234,14 +234,17 @@ export class WebviewController implements Dispos
this.parent.reveal(this.parent.viewColumn ?? ViewColumn.Active, options?.preserveFocus ?? false);
}
} else if (this.isType('view')) {
const result = await this.provider.canShowWebviewView?.(firstTime, options, ...args);
if (result === false) return;
if (firstTime) {
this.webview.html = await this.getHtml(this.webview);
}
await executeCommand(`${this.id}.focus`, options);
if (firstTime) {
void executeCommand(`${this.id}.focus`, options);
this.provider.onVisibilityChanged?.(true);
}
this.provider.onVisibilityChanged?.(true);
}
}

+ 15
- 4
src/webviews/webviewsController.ts View File

@ -54,6 +54,7 @@ interface WebviewViewMetadata {
readonly id: `gitlens.views.${WebviewViewIds}`;
readonly descriptor: WebviewViewDescriptor<State, SerializedState>;
webview?: WebviewController<State, SerializedState> | undefined;
pendingShowArgs?: Parameters<WebviewViewProxy['show']> | undefined;
}
export interface WebviewViewProxy extends Disposable {
@ -115,11 +116,19 @@ export class WebviewsController implements Disposable {
metadata.webview = webview;
disposables.push(
webview.onDidDispose(() => (metadata.webview = undefined), this),
webview.onDidDispose(() => {
metadata.pendingShowArgs = undefined;
metadata.webview = undefined;
}, this),
webview,
);
await webview.show(true);
if (metadata.pendingShowArgs != null) {
await webview.show(true, ...metadata.pendingShowArgs);
metadata.pendingShowArgs = undefined;
} else {
await webview.show(true);
}
},
}),
);
@ -136,8 +145,10 @@ export class WebviewsController implements Disposable {
},
refresh: async force => metadata.webview?.refresh(force),
// eslint-disable-next-line @typescript-eslint/require-await
show: async (options?: { preserveFocus?: boolean }, ..._args) => {
if (metadata.webview != null) return void metadata.webview.show(false, options);
show: async (options?: { preserveFocus?: boolean }, ...args) => {
if (metadata.webview != null) return void metadata.webview.show(false, options, ...args);
metadata.pendingShowArgs = [options, ...args];
return void executeCommand(`${id}.focus`, options);
},
} satisfies WebviewViewProxy;

Loading…
Cancel
Save