Browse Source

Reworks webview serialized state

Adds selected repo restoration to Commit Graph
Adds commit restoration to Commit Details
Adds file/period restoration to timeline
main
Eric Amodio 1 year ago
parent
commit
97b6db7d9a
12 changed files with 135 additions and 79 deletions
  1. +27
    -21
      src/plus/webviews/graph/graphWebview.ts
  2. +0
    -1
      src/plus/webviews/graph/protocol.ts
  3. +14
    -7
      src/plus/webviews/timeline/timelineWebview.ts
  4. +13
    -9
      src/webviews/apps/commitDetails/commitDetails.ts
  5. +4
    -2
      src/webviews/apps/plus/focus/focus.ts
  6. +1
    -2
      src/webviews/apps/plus/graph/graph.tsx
  7. +4
    -1
      src/webviews/apps/plus/timeline/timeline.ts
  8. +2
    -1
      src/webviews/apps/rebase/rebase.ts
  9. +1
    -4
      src/webviews/apps/shared/appBase.ts
  10. +35
    -11
      src/webviews/commitDetails/commitDetailsWebview.ts
  11. +10
    -9
      src/webviews/commitDetails/protocol.ts
  12. +24
    -11
      src/webviews/webviewsController.ts

+ 27
- 21
src/plus/webviews/graph/graphWebview.ts View File

@ -86,6 +86,7 @@ import { RepositoryFolderNode } from '../../../views/nodes/viewNode';
import type { IpcMessage, IpcNotificationType } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type {
BranchState,
@ -266,19 +267,19 @@ export class GraphWebviewProvider implements WebviewProvider {
async onShowing(
loading: boolean,
_options: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
...args: [Repository, { ref: GitReference }, { state: Partial<State> }] | unknown[]
): Promise<boolean> {
this._firstSelection = true;
const context = args[0];
if (isRepository(context)) {
this.repository = context;
} else if (hasGitReference(context)) {
this.repository = this.container.git.getRepository(context.ref.repoPath);
const [arg] = args;
if (isRepository(arg)) {
this.repository = arg;
} else if (hasGitReference(arg)) {
this.repository = this.container.git.getRepository(arg.ref.repoPath);
let id = context.ref.ref;
let id = arg.ref.ref;
if (!isSha(id)) {
id = await this.container.git.resolveReference(context.ref.repoPath, id, undefined, {
id = await this.container.git.resolveReference(arg.ref.repoPath, id, undefined, {
force: true,
});
}
@ -293,18 +294,24 @@ export class GraphWebviewProvider implements WebviewProvider {
void this.onGetMoreRows({ id: id }, true);
}
} else if (this.container.git.repositoryCount > 1) {
const [contexts] = parseCommandContext(Commands.ShowGraph, undefined, ...args);
const context = Array.isArray(contexts) ? contexts[0] : contexts;
if (context.type === 'scm' && context.scm.rootUri != null) {
this.repository = this.container.git.getRepository(context.scm.rootUri);
} else if (context.type === 'viewItem' && context.node instanceof RepositoryFolderNode) {
this.repository = context.node.repo;
} else {
if (isSerializedState<State>(arg) && arg.state.selectedRepository != null) {
this.repository = this.container.git.getRepository(arg.state.selectedRepository);
}
if (this.repository != null && !loading && this.host.ready) {
this.updateState();
if (this.repository == null && this.container.git.repositoryCount > 1) {
const [contexts] = parseCommandContext(Commands.ShowGraph, undefined, ...args);
const context = Array.isArray(contexts) ? contexts[0] : contexts;
if (context.type === 'scm' && context.scm.rootUri != null) {
this.repository = this.container.git.getRepository(context.scm.rootUri);
} else if (context.type === 'viewItem' && context.node instanceof RepositoryFolderNode) {
this.repository = context.node.repo;
}
if (this.repository != null && !loading && this.host.ready) {
this.updateState();
}
}
}
@ -1758,7 +1765,7 @@ export class GraphWebviewProvider implements WebviewProvider {
private async getState(deferRows?: boolean): Promise<State> {
if (this.container.git.repositoryCount === 0) {
return { timestamp: Date.now(), debugging: this.container.debugging, allowed: true, repositories: [] };
return { timestamp: Date.now(), allowed: true, repositories: [] };
}
if (this.trialBanner == null) {
@ -1771,7 +1778,7 @@ export class GraphWebviewProvider implements WebviewProvider {
if (this.repository == null) {
this.repository = this.container.git.getBestRepositoryOrFirst();
if (this.repository == null) {
return { timestamp: Date.now(), debugging: this.container.debugging, allowed: true, repositories: [] };
return { timestamp: Date.now(), allowed: true, repositories: [] };
}
}
@ -1886,7 +1893,6 @@ export class GraphWebviewProvider implements WebviewProvider {
includeOnlyRefs: data != null ? this.getIncludeOnlyRefs(data) ?? {} : {},
nonce: this.host.cspNonce,
workingTreeStats: getSettledValue(workingStatsResult) ?? { added: 0, deleted: 0, modified: 0 },
debugging: this.container.debugging,
};
}

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

@ -113,7 +113,6 @@ export interface State {
excludeRefs?: GraphExcludeRefs;
excludeTypes?: GraphExcludeTypes;
includeOnlyRefs?: GraphIncludeOnlyRefs;
debugging: boolean;
// Props below are computed in the webview (not passed)
activeDay?: number;

+ 14
- 7
src/plus/webviews/timeline/timelineWebview.ts View File

@ -17,11 +17,13 @@ import type { Deferrable } from '../../../system/function';
import { debounce } from '../../../system/function';
import { filter } from '../../../system/iterable';
import { hasVisibleTextEditor, isTextEditor } from '../../../system/utils';
import type { ViewFileNode } from '../../../views/nodes/viewNode';
import { isViewFileNode } from '../../../views/nodes/viewNode';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import { updatePendingContext } from '../../../webviews/webviewController';
import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type { Commit, Period, State } from './protocol';
import { DidChangeNotificationType, OpenDataPointCommandType, UpdatePeriodCommandType } from './protocol';
@ -80,14 +82,19 @@ export class TimelineWebviewProvider implements WebviewProvider {
onShowing(
loading: boolean,
_options: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
...args: [Uri | ViewFileNode | { state: Partial<State> }] | unknown[]
): boolean {
const [uriOrNode] = args;
if (uriOrNode != null) {
if (uriOrNode instanceof Uri) {
this.updatePendingUri(uriOrNode);
} else if (isViewFileNode(uriOrNode)) {
this.updatePendingUri(uriOrNode.uri);
const [arg] = args;
if (arg != null) {
if (arg instanceof Uri) {
this.updatePendingUri(arg);
} else if (isViewFileNode(arg)) {
this.updatePendingUri(arg.uri);
} else if (isSerializedState<State>(arg)) {
this.updatePendingContext({
period: arg.state.period,
uri: arg.state.uri != null ? Uri.parse(arg.state.uri) : undefined,
});
}
} else {
this.updatePendingEditor(window.activeTextEditor);

+ 13
- 9
src/webviews/apps/commitDetails/commitDetails.ts View File

@ -46,14 +46,12 @@ import '../shared/components/list/file-change-list-item';
const uncommittedSha = '0000000000000000000000000000000000000000';
type CommitState = SomeNonNullable<Serialized<State>, 'selected'>;
export class CommitDetailsApp extends App<Serialized<State>> {
constructor() {
super('CommitDetailsApp');
}
override onInitialize() {
this.state = this.getState() ?? this.state;
this.renderContent();
}
@ -124,7 +122,7 @@ export class CommitDetailsApp extends App> {
// break;
case DidChangeNotificationType.method:
onIpc(DidChangeNotificationType, msg, params => {
assertsSerialized<typeof params.state>(params.state);
assertsSerialized<State>(params.state);
this.state = params.state;
this.setState(this.state);
@ -137,6 +135,15 @@ export class CommitDetailsApp extends App> {
}
}
protected override setState(state: Partial<Serialized<State>>) {
super.setState({
selected:
state.selected != null
? { ...state.selected, autolinks: undefined, files: undefined, stats: undefined }
: undefined,
});
}
async onExplainCommit(e: MouseEvent) {
const el = e.target as HTMLButtonElement;
if (el.getAttribute('aria-busy') === 'true') return;
@ -517,11 +524,8 @@ export class CommitDetailsApp extends App> {
isTree = layout === ViewFilesLayout.Tree;
}
const stashAttr = state.selected.isStash
? 'stash '
: state.selected.sha === uncommittedSha
? 'uncommitted '
: '';
const stashAttr =
state.selected.stashNumber != null ? 'stash ' : state.selected.sha === uncommittedSha ? 'uncommitted ' : '';
if (isTree) {
const tree = makeHierarchical(
@ -592,7 +596,7 @@ export class CommitDetailsApp extends App> {
const $el = document.querySelector<HTMLElement>('[data-region="author"]');
if ($el == null) return;
if (state.selected?.isStash === true) {
if (state.selected?.stashNumber != null) {
$el.innerHTML = /*html*/ `
<div class="commit-stashed">
<span class="commit-stashed__media"><code-icon class="commit-stashed__icon" icon="inbox"></code-icon></span>

+ 4
- 2
src/webviews/apps/plus/focus/focus.ts View File

@ -114,13 +114,15 @@ export class FocusApp extends App {
switch (msg.method) {
case DidChangeStateNotificationType.method:
onIpc(DidChangeStateNotificationType, msg, params => {
this.setState({ ...this.state, ...params.state });
this.state = { ...this.state, ...params.state };
this.setState(this.state);
this.renderContent();
});
break;
case DidChangeSubscriptionNotificationType.method:
onIpc(DidChangeSubscriptionNotificationType, msg, params => {
this.setState({ ...this.state, subscription: params.subscription, isPlus: params.isPlus });
this.state = { ...this.state, subscription: params.subscription, isPlus: params.isPlus };
this.setState(this.state);
this.renderContent();
});
break;

+ 1
- 2
src/webviews/apps/plus/graph/graph.tsx View File

@ -478,9 +478,8 @@ export class GraphApp extends App {
this.log(`setState()`);
const themingChanged = this.ensureTheming(state);
// Avoid calling the base for now, since we aren't using the vscode state
this.state = state;
// super.setState(state);
super.setState({ selectedRepository: state.selectedRepository });
this.callback?.(this.state, type, themingChanged);
}

+ 4
- 1
src/webviews/apps/plus/timeline/timeline.ts View File

@ -28,7 +28,6 @@ export class TimelineApp extends App {
protected override onInitialize() {
provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeDropdown(), vsCodeOption());
this.state = this.getState() ?? this.state;
this.updateState();
}
@ -65,6 +64,10 @@ export class TimelineApp extends App {
}
}
protected override setState(state: Partial<State>) {
super.setState({ period: state.period, uri: state.uri });
}
private onActionClicked(e: MouseEvent, target: HTMLElement) {
const action = target.dataset.action;
if (action?.startsWith('command:')) {

+ 2
- 1
src/webviews/apps/rebase/rebase.ts View File

@ -326,7 +326,8 @@ class RebaseEditor extends App {
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeNotificationType, msg, params => {
this.setState(params.state);
this.state = params.state;
this.setState(this.state);
this.refresh(this.state);
});
break;

+ 1
- 4
src/webviews/apps/shared/appBase.ts View File

@ -220,10 +220,7 @@ export abstract class App
return promise;
}
protected setState(state: State) {
this.state = state;
if (state == null) return;
protected setState(state: Partial<State>) {
this._api.setState(state);
}

+ 35
- 11
src/webviews/commitDetails/commitDetailsWebview.ts View File

@ -24,7 +24,7 @@ import { serializeIssueOrPullRequest } from '../../git/models/issue';
import type { PullRequest } from '../../git/models/pullRequest';
import { serializePullRequest } from '../../git/models/pullRequest';
import type { GitRevisionReference } from '../../git/models/reference';
import { getReferenceFromRevision, shortenRevision } from '../../git/models/reference';
import { createReference, getReferenceFromRevision, shortenRevision } from '../../git/models/reference';
import type { GitRemote } from '../../git/models/remote';
import type { ShowInCommitGraphCommandArgs } from '../../plus/webviews/graph/protocol';
import { executeCommand, executeCoreCommand } from '../../system/command';
@ -47,6 +47,7 @@ import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol';
import type { WebviewController, WebviewProvider } from '../webviewController';
import { updatePendingContext } from '../webviewController';
import { isSerializedState } from '../webviewsController';
import type { CommitDetails, DidExplainCommitParams, FileActionParams, Preferences, State } from './protocol';
import {
AutolinkSettingsCommandType,
@ -147,10 +148,34 @@ export class CommitDetailsWebviewProvider implements WebviewProvider
async onShowing(
_loading: boolean,
options: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
...args: [Partial<CommitSelectedEvent['data']> | { state: Partial<Serialized<State>> }] | unknown[]
): Promise<boolean> {
let data = args[0] as Partial<CommitSelectedEvent['data']> | undefined;
if (typeof data !== 'object') {
let data: Partial<CommitSelectedEvent['data']> | undefined;
const [arg] = args;
if (isSerializedState<Serialized<State>>(arg)) {
const { selected } = arg.state;
if (selected?.repoPath != null && selected?.sha != null) {
if (selected.stashNumber != null) {
data = {
commit: createReference(selected.sha, selected.repoPath, {
refType: 'stash',
name: selected.message,
number: selected.stashNumber,
}),
};
} else {
data = {
commit: createReference(selected.sha, selected.repoPath, {
refType: 'revision',
message: selected.message,
}),
};
}
}
} else if (arg != null && typeof arg === 'object') {
data = arg as Partial<CommitSelectedEvent['data']> | undefined;
} else {
data = undefined;
}
@ -702,16 +727,14 @@ export class CommitDetailsWebviewProvider implements WebviewProvider
if (this._pendingContext == null) return false;
const context = { ...this._context, ...this._pendingContext };
this._context = context;
this._pendingContext = undefined;
return window.withProgress({ location: { viewId: this.host.id } }, async () => {
try {
const success = await this.host.notify(DidChangeNotificationType, {
await this.host.notify(DidChangeNotificationType, {
state: await this.getState(context),
});
if (success) {
this._context = context;
this._pendingContext = undefined;
}
} catch (ex) {
Logger.error(scope, ex);
debugger;
@ -777,12 +800,13 @@ export class CommitDetailsWebviewProvider implements WebviewProvider
}
return {
repoPath: commit.repoPath,
sha: commit.sha,
shortSha: commit.shortSha,
isStash: commit.refType === 'stash',
message: formattedMessage,
author: { ...commit.author, avatar: avatarUri?.toString(true) },
// committer: { ...commit.committer, avatar: committerAvatar?.toString(true) },
message: formattedMessage,
stashNumber: commit.refType === 'stash' ? commit.number : undefined,
files: commit.files?.map(({ status, repoPath, path, originalPath }) => {
const icon = getGitFileStatusIcon(status);
return {

+ 10
- 9
src/webviews/commitDetails/protocol.ts View File

@ -13,30 +13,31 @@ export const messageHeadlineSplitterToken = '\x00\n\x00';
export type FileShowOptions = TextDocumentShowOptions;
export type CommitDetailsDismissed = 'sidebar';
export type CommitSummary = {
export interface CommitSummary {
sha: string;
shortSha: string;
// summary: string;
message: string;
author: GitCommitIdentityShape & { avatar: string | undefined };
// committer: GitCommitIdentityShape & { avatar: string | undefined };
isStash: boolean;
};
repoPath: string;
stashNumber?: string;
}
export type CommitDetails = CommitSummary & {
export interface CommitDetails extends CommitSummary {
autolinks?: Autolink[];
files?: (GitFileChangeShape & { icon: { dark: string; light: string } })[];
stats?: GitCommitStats;
};
}
export type Preferences = {
export interface Preferences {
autolinksExpanded?: boolean;
avatars?: boolean;
dismissed?: CommitDetailsDismissed[];
files?: Config['views']['commitDetails']['files'];
};
}
export type State = {
export interface State {
timestamp: number;
pinned: boolean;
@ -56,7 +57,7 @@ export type State = {
position: number;
hint?: string;
};
};
}
export type ShowCommitDetailsViewCommandArgs = string[];

+ 24
- 11
src/webviews/webviewsController.ts View File

@ -110,7 +110,7 @@ export class WebviewsController implements Disposable {
{
resolveWebviewView: async (
webviewView: WebviewView,
_context: WebviewViewResolveContext<SerializedState>,
context: WebviewViewResolveContext<SerializedState>,
token: CancellationToken,
) => {
if (canResolveProvider != null) {
@ -153,9 +153,14 @@ export class WebviewsController implements Disposable {
controller,
);
if (registration.pendingShowArgs != null) {
await controller.show(true, ...registration.pendingShowArgs);
registration.pendingShowArgs = undefined;
let args = registration.pendingShowArgs;
registration.pendingShowArgs = undefined;
if (args == null && isSerializedState<State>(context)) {
args = [undefined, context];
}
if (args != null) {
await controller.show(true, ...args);
} else {
await controller.show(true);
}
@ -216,7 +221,7 @@ export class WebviewsController implements Disposable {
const disposables: Disposable[] = [];
const { container } = this;
let serialized: { panel: WebviewPanel; state: SerializedState } | undefined;
let serializedPanel: WebviewPanel | undefined;
async function show(
options?: { column?: ViewColumn; preserveFocus?: boolean },
@ -242,11 +247,11 @@ export class WebviewsController implements Disposable {
let { controller } = registration;
if (controller == null) {
let panel;
if (serialized != null) {
if (serializedPanel != null) {
Logger.debug(scope, `Restoring webview panel (${descriptor.id})`);
panel = serialized.panel;
serialized = undefined;
panel = serializedPanel;
serializedPanel = undefined;
} else {
Logger.debug(scope, `Creating webview panel (${descriptor.id})`);
@ -288,11 +293,15 @@ export class WebviewsController implements Disposable {
}
async function deserializeWebviewPanel(panel: WebviewPanel, state: SerializedState) {
// TODO@eamodio: We aren't currently using the state, but we should start storing maybe both "client" and "server" state
// TODO@eamodio: We are currently storing nothing or way too much in serialized state. We should start storing maybe both "client" and "server" state
// Where as right now our webviews are only saving "client" state, e.g. the entire state sent to the webview, rather than key pieces of state
// We probably need to separate state into actual "state" and all the data that is sent to the webview, e.g. for the Graph state might be the selected repo, selected sha, etc vs the entire data set to render the Graph
serialized = { panel: panel, state: state };
await show({ column: panel.viewColumn, preserveFocus: true });
serializedPanel = panel;
if (state != null) {
await show({ column: panel.viewColumn, preserveFocus: true }, { state: state });
} else {
await show({ column: panel.viewColumn, preserveFocus: true });
}
}
const disposable = Disposable.from(
@ -320,3 +329,7 @@ export class WebviewsController implements Disposable {
} satisfies WebviewPanelProxy;
}
}
export function isSerializedState<State>(o: unknown): o is { state: Partial<State> } {
return o != null && typeof o === 'object' && 'state' in o && o.state != null && typeof o.state === 'object';
}

Loading…
Cancel
Save