Browse Source

Improves commit details following/selection

main
Eric Amodio 2 years ago
parent
commit
cee2da1fc0
7 changed files with 224 additions and 75 deletions
  1. +13
    -8
      src/context.ts
  2. +45
    -9
      src/plus/webviews/graph/graphWebview.ts
  3. +45
    -25
      src/webviews/commitDetails/commitDetailsWebviewView.ts
  4. +74
    -9
      src/webviews/rebase/rebaseEditor.ts
  5. +29
    -15
      src/webviews/webviewBase.ts
  6. +15
    -7
      src/webviews/webviewViewBase.ts
  7. +3
    -2
      src/webviews/webviewWithConfigBase.ts

+ 13
- 8
src/context.ts View File

@ -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<string, unknown>();
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<AllContextKeys>();
export const onDidChangeContext = _onDidChangeContext.event;
export function getContext<T>(key: ContextKeys): T | undefined;
export function getContext<T>(key: ContextKeys, defaultValue: T): T;
export function getContext<T>(key: ContextKeys, defaultValue?: T): T | undefined {
export function getContext<T>(key: AllContextKeys): T | undefined;
export function getContext<T>(key: AllContextKeys, defaultValue: T): T;
export function getContext<T>(key: AllContextKeys, defaultValue?: T): T | undefined {
return (contextStorage.get(key) as T | undefined) ?? defaultValue;
}

+ 45
- 9
src/plus/webviews/graph/graphWebview.ts View File

@ -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<GraphWebview['showActiveSelectionDetails']> | 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;
}

+ 45
- 25
src/webviews/commitDetails/commitDetailsWebviewView.ts View File

@ -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
);
}
@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;
@ -255,7 +261,7 @@ export class CommitDetailsWebviewView extends WebviewViewBase
stashesView.onDidChangeVisibility(this.onStashesViewVisibilityChanged, this),
);
const commit = this.getBestCommitOrStash();
const commit = this._pendingContext?.commit ?? this.getBestCommitOrStash();
this.updateCommit(commit, { immediate: false });
}
@ -333,8 +339,18 @@ export class CommitDetailsWebviewView extends WebviewViewBase
private onActiveLinesChanged(e: LinesChangeEvent) {
if (e.pending) return;
const commit =
e.selections != null ? this.container.lineTracker.getState(e.selections[0].active)?.commit : undefined;
let commit;
if (e.editor == null) {
if (getContext('gitlens:webview:graph:active') || getContext('gitlens:webview:rebaseEditor:active')) {
commit = this._pendingContext?.commit ?? this._context.commit;
if (commit == null) return;
}
}
if (commit == null) {
commit =
e.selections != null ? this.container.lineTracker.getState(e.selections[0].active)?.commit : undefined;
}
this.updateCommit(commit);
}
@ -672,31 +688,35 @@ export class CommitDetailsWebviewView extends WebviewViewBase
let commit;
const { lineTracker } = this.container;
const line = lineTracker.selections?.[0].active;
if (line != null) {
commit = lineTracker.getState(line)?.commit;
}
if (window.activeTextEditor == null) {
if (getContext('gitlens:webview:graph:active') || getContext('gitlens:webview:rebaseEditor:active')) {
commit = this._pendingContext?.commit ?? this._context.commit;
if (commit != null) return commit;
}
if (commit == null) {
const { commitsView } = this.container;
const node = commitsView.activeSelection;
if (
node != null &&
(node instanceof CommitNode ||
node instanceof FileRevisionAsCommitNode ||
node instanceof CommitFileNode)
) {
commit = node.commit;
const { lineTracker } = this.container;
const line = lineTracker.selections?.[0].active;
if (line != null) {
commit = lineTracker.getState(line)?.commit;
if (commit != null) return commit;
}
}
if (commit == null) {
const { stashesView } = this.container;
const node = stashesView.activeSelection;
if (node != null && (node instanceof StashNode || node instanceof StashFileNode)) {
commit = node.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;
}
return commit;

+ 74
- 9
src/webviews/rebase/rebaseEditor.ts View File

@ -1,11 +1,18 @@
import type { CancellationToken, CustomTextEditorProvider, TextDocument, WebviewPanel } from 'vscode';
import type {
CancellationToken,
CustomTextEditorProvider,
TextDocument,
WebviewPanel,
WebviewPanelOnDidChangeViewStateEvent,
} from 'vscode';
import { ConfigurationTarget, Disposable, Position, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode';
import { getNonce } from '@env/crypto';
import { ShowCommitsInViewCommand } from '../../commands';
import { GitActions } from '../../commands/gitCommands.actions';
import { configuration } from '../../configuration';
import { CoreCommands } from '../../constants';
import { ContextKeys, CoreCommands } from '../../constants';
import type { Container } from '../../container';
import { setContext } from '../../context';
import { emojify } from '../../emojis';
import type { GitCommit } from '../../git/models/commit';
import { GitReference } from '../../git/models/reference';
@ -18,8 +25,8 @@ import type { Deferrable } from '../../system/function';
import { debounce } from '../../system/function';
import { join, map } from '../../system/iterable';
import { normalizePath } from '../../system/path';
import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol';
import type { IpcMessage, WebviewFocusChangedParams } from '../protocol';
import { onIpc, WebviewFocusChangedCommandType } from '../protocol';
import type {
Author,
ChangeEntryParams,
@ -125,6 +132,10 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
this._disposable.dispose();
}
private get contextKeyPrefix() {
return `${ContextKeys.WebviewPrefix}rebaseEditor` as const;
}
get enabled(): boolean {
const associations = configuration.inspectAny<
{ [key: string]: string } | { viewType: string; filenamePattern: string }[]
@ -198,13 +209,11 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
subscriptions.push(
panel.onDidDispose(() => {
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<RebaseEditorProvider['onViewFocusChanged']>({
args: { 0: e => `focused=${e.focused}, inputFocused=${e.inputFocused}` },
})
protected onViewFocusChanged(e: WebviewFocusChangedParams): void {
this.setContextKeys(e.focused, e.focused, e.inputFocused);
}
@debug<RebaseEditorProvider['onViewStateChanged']>({
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<State> {
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));

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

@ -30,6 +30,8 @@ function nextIpcId() {
return `host:${ipcSequence}`;
}
export type WebviewIds = 'graph' | 'settings' | 'timeline' | 'welcome';
@logName<WebviewBase<any>>((c, name) => `${name}(${c.id})`)
export abstract class WebviewBase<State> 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<WebviewBase<State>['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<boolean> {
if (this._panel == null || !this.isReady || !this.visible) return Promise.resolve(false);

+ 15
- 7
src/webviews/webviewViewBase.ts View File

@ -32,6 +32,8 @@ function nextIpcId() {
return `host:${ipcSequence}`;
}
export type WebviewViewIds = 'commitDetails' | 'home' | 'timeline';
@logName<WebviewViewBase<any>>((c, name) => `${name}(${c.id})`)
export abstract class WebviewViewBase<State, SerializedState = State> 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<WebviewViewBase<State>['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);

+ 3
- 2
src/webviews/webviewWithConfigBase.ts View File

@ -17,16 +17,17 @@ import {
onIpc,
UpdateConfigurationCommandType,
} from './protocol';
import type { WebviewIds } from './webviewBase';
import { WebviewBase } from './webviewBase';
export abstract class WebviewWithConfigBase<State> extends WebviewBase<State> {
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,
) {

Loading…
Cancel
Save