Browse Source

Adds typings for webview show args

Adds getBestInstance to WebviewPanelsProxy
Favors active controllers
Favors open tabs if panel is hidden on showing item in graph
main
Eric Amodio 1 year ago
parent
commit
071655e390
13 changed files with 293 additions and 185 deletions
  1. +1
    -1
      src/commands/base.ts
  2. +5
    -10
      src/commands/showView.ts
  3. +9
    -6
      src/container.ts
  4. +8
    -7
      src/plus/webviews/graph/graphWebview.ts
  5. +53
    -13
      src/plus/webviews/graph/registration.ts
  6. +11
    -4
      src/plus/webviews/timeline/registration.ts
  7. +8
    -8
      src/plus/webviews/timeline/timelineWebview.ts
  8. +7
    -4
      src/webviews/commitDetails/commitDetailsWebview.ts
  9. +5
    -2
      src/webviews/commitDetails/registration.ts
  10. +5
    -3
      src/webviews/settings/registration.ts
  11. +4
    -3
      src/webviews/settings/settingsWebview.ts
  12. +46
    -31
      src/webviews/webviewController.ts
  13. +131
    -93
      src/webviews/webviewsController.ts

+ 1
- 1
src/commands/base.ts View File

@ -235,7 +235,7 @@ export type CommandContext =
| CommandViewNodeContext
| CommandViewNodesContext;
function isScm(scm: any): scm is SourceControl {
export function isScm(scm: any): scm is SourceControl {
if (scm == null) return false;
return (

+ 5
- 10
src/commands/showView.ts View File

@ -1,5 +1,6 @@
import { Commands } from '../constants';
import type { Container } from '../container';
import type { GraphWebviewShowingArgs } from '../plus/webviews/graph/registration';
import { command } from '../system/command';
import type { CommandContext } from './base';
import { Command } from './base';
@ -28,11 +29,11 @@ export class ShowViewCommand extends Command {
]);
}
protected override preExecute(context: CommandContext, ...args: any[]) {
protected override preExecute(context: CommandContext, ...args: unknown[]) {
return this.execute(context, ...args);
}
async execute(context: CommandContext, ...args: any[]) {
async execute(context: CommandContext, ...args: unknown[]) {
const command = context.command as Commands;
switch (command) {
case Commands.ShowBranchesView:
@ -49,14 +50,8 @@ export class ShowViewCommand extends Command {
return this.container.homeView.show();
case Commands.ShowAccountView:
return this.container.accountView.show();
case Commands.ShowGraphView: {
let commandArgs = args;
if (context.type === 'scm' && context.scm?.rootUri != null) {
const repo = this.container.git.getRepository(context.scm.rootUri);
commandArgs = repo != null ? [repo, ...args] : args;
}
return this.container.graphView.show(undefined, ...commandArgs);
}
case Commands.ShowGraphView:
return this.container.graphView.show(undefined, ...(args as GraphWebviewShowingArgs));
case Commands.ShowLineHistoryView:
return this.container.lineHistoryView.show();
case Commands.ShowRemotesView:

+ 9
- 6
src/container.ts View File

@ -28,12 +28,14 @@ import { ServerConnection } from './plus/gk/serverConnection';
import { IntegrationAuthenticationService } from './plus/integrationAuthentication';
import { registerAccountWebviewView } from './plus/webviews/account/registration';
import { registerFocusWebviewCommands, registerFocusWebviewPanel } from './plus/webviews/focus/registration';
import type { GraphWebviewShowingArgs } from './plus/webviews/graph/registration';
import {
registerGraphWebviewCommands,
registerGraphWebviewPanel,
registerGraphWebviewView,
} from './plus/webviews/graph/registration';
import { GraphStatusBarController } from './plus/webviews/graph/statusbar';
import type { TimelineWebviewShowingArgs } from './plus/webviews/timeline/registration';
import {
registerTimelineWebviewCommands,
registerTimelineWebviewPanel,
@ -70,6 +72,7 @@ import { ViewFileDecorationProvider } from './views/viewDecorationProvider';
import { WorkspacesView } from './views/workspacesView';
import { WorktreesView } from './views/worktreesView';
import { VslsController } from './vsls/vsls';
import type { CommitDetailsWebviewShowingArgs } from './webviews/commitDetails/registration';
import {
registerCommitDetailsWebviewView,
registerGraphDetailsWebviewView,
@ -343,7 +346,7 @@ export class Container {
return this._accountAuthentication;
}
private readonly _accountView: WebviewViewProxy;
private readonly _accountView: WebviewViewProxy<[]>;
get accountView() {
return this._accountView;
}
@ -394,7 +397,7 @@ export class Container {
return this._commitsView;
}
private readonly _commitDetailsView: WebviewViewProxy;
private readonly _commitDetailsView: WebviewViewProxy<CommitDetailsWebviewShowingArgs>;
get commitDetailsView() {
return this._commitDetailsView;
}
@ -499,17 +502,17 @@ export class Container {
}
}
private readonly _graphDetailsView: WebviewViewProxy;
private readonly _graphDetailsView: WebviewViewProxy<CommitDetailsWebviewShowingArgs>;
get graphDetailsView() {
return this._graphDetailsView;
}
private readonly _graphView: WebviewViewProxy;
private readonly _graphView: WebviewViewProxy<GraphWebviewShowingArgs>;
get graphView() {
return this._graphView;
}
private readonly _homeView: WebviewViewProxy;
private readonly _homeView: WebviewViewProxy<[]>;
get homeView() {
return this._homeView;
}
@ -642,7 +645,7 @@ export class Container {
return this._telemetry;
}
private readonly _timelineView: WebviewViewProxy;
private readonly _timelineView: WebviewViewProxy<TimelineWebviewShowingArgs>;
get timelineView() {
return this._timelineView;
}

+ 8
- 7
src/plus/webviews/graph/graphWebview.ts View File

@ -83,7 +83,7 @@ import { isWebviewItemContext, isWebviewItemGroupContext, serializeWebviewItemCo
import { RepositoryFolderNode } from '../../../views/nodes/abstract/repositoryFolderNode';
import type { IpcMessage, IpcNotificationType } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import type { WebviewController, WebviewProvider, WebviewShowingArgs } from '../../../webviews/webviewController';
import type { WebviewPanelShowCommandArgs } from '../../../webviews/webviewsController';
import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../gk/account/subscriptionService';
@ -172,6 +172,7 @@ import {
UpdateRefsVisibilityCommandType,
UpdateSelectionCommandType,
} from './protocol';
import type { GraphWebviewShowingArgs } from './registration';
const defaultGraphColumnsSettings: GraphColumnsSettings = {
ref: { width: 130, isHidden: false, order: 0 },
@ -193,7 +194,7 @@ const compactGraphColumnsSettings: GraphColumnsSettings = {
sha: { width: 130, isHidden: false, order: 6 },
};
export class GraphWebviewProvider implements WebviewProvider<State> {
export class GraphWebviewProvider implements WebviewProvider<State, State, GraphWebviewShowingArgs> {
private _repository?: Repository;
private get repository(): Repository | undefined {
return this._repository;
@ -251,7 +252,7 @@ export class GraphWebviewProvider implements WebviewProvider {
constructor(
private readonly container: Container,
private readonly host: WebviewController<State>,
private readonly host: WebviewController<State, State, GraphWebviewShowingArgs>,
) {
this._showDetailsView = configuration.get('graph.showDetailsView');
this._theme = window.activeColorTheme;
@ -289,7 +290,7 @@ export class GraphWebviewProvider implements WebviewProvider {
this._disposable.dispose();
}
canReuseInstance(...args: unknown[]): boolean | undefined {
canReuseInstance(...args: WebviewShowingArgs<GraphWebviewShowingArgs, State>): boolean | undefined {
if (this.container.git.openRepositoryCount === 1) return true;
const [arg] = args;
@ -306,14 +307,14 @@ export class GraphWebviewProvider implements WebviewProvider {
return repository?.uri.toString() === this.repository?.uri.toString() ? true : undefined;
}
getSplitArgs(): unknown[] {
return [this.repository];
getSplitArgs(): WebviewShowingArgs<GraphWebviewShowingArgs, State> {
return this.repository != null ? [this.repository] : [];
}
async onShowing(
loading: boolean,
_options: { column?: ViewColumn; preserveFocus?: boolean },
...args: [Repository, { ref: GitReference }, { state: Partial<State> }] | unknown[]
...args: WebviewShowingArgs<GraphWebviewShowingArgs, State>
): Promise<boolean> {
this._firstSelection = true;

+ 53
- 13
src/plus/webviews/graph/registration.ts View File

@ -1,10 +1,13 @@
import { Disposable, ViewColumn } from 'vscode';
import { isScm } from '../../../commands/base';
import { Commands } from '../../../constants';
import type { Container } from '../../../container';
import type { GitReference } from '../../../git/models/reference';
import type { Repository } from '../../../git/models/repository';
import { executeCommand, executeCoreCommand, registerCommand } from '../../../system/command';
import { configuration } from '../../../system/configuration';
import { getContext } from '../../../system/context';
import { ViewNode } from '../../../views/nodes/abstract/viewNode';
import type { BranchNode } from '../../../views/nodes/branchNode';
import type { CommitFileNode } from '../../../views/nodes/commitFileNode';
import type { CommitNode } from '../../../views/nodes/commitNode';
@ -17,8 +20,10 @@ import type {
} from '../../../webviews/webviewsController';
import type { ShowInCommitGraphCommandArgs, State } from './protocol';
export type GraphWebviewShowingArgs = [Repository | { ref: GitReference }];
export function registerGraphWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(
return controller.registerWebviewPanel<State, State, GraphWebviewShowingArgs>(
{ id: Commands.ShowGraphPage, options: { preserveInstance: true } },
{
id: 'gitlens.graph',
@ -43,7 +48,7 @@ export function registerGraphWebviewPanel(controller: WebviewsController) {
}
export function registerGraphWebviewView(controller: WebviewsController) {
return controller.registerWebviewView<State>(
return controller.registerWebviewView<State, State, GraphWebviewShowingArgs>(
{
id: 'gitlens.views.graph',
fileName: 'graph.html',
@ -62,18 +67,45 @@ export function registerGraphWebviewView(controller: WebviewsController) {
);
}
export function registerGraphWebviewCommands(container: Container, panels: WebviewPanelsProxy) {
export function registerGraphWebviewCommands<T>(
container: Container,
panels: WebviewPanelsProxy<GraphWebviewShowingArgs, T>,
) {
return Disposable.from(
registerCommand(Commands.ShowGraph, (...args: unknown[]) =>
configuration.get('graph.layout') === 'panel'
? executeCommand(Commands.ShowGraphView, ...args)
: executeCommand<WebviewPanelShowCommandArgs>(
Commands.ShowGraphPage,
{ _type: 'WebviewPanelShowOptions' },
undefined,
...args,
),
),
registerCommand(Commands.ShowGraph, (...args: unknown[]) => {
const [arg] = args;
let showInGraphArg;
if (isScm(arg)) {
if (arg.rootUri != null) {
const repo = container.git.getRepository(arg.rootUri);
if (repo != null) {
showInGraphArg = repo;
}
}
args = [];
} else if (arg instanceof ViewNode) {
if (arg.is('repo-folder')) {
showInGraphArg = arg.repo;
}
args = [];
}
if (showInGraphArg != null) {
return executeCommand(Commands.ShowInCommitGraph, showInGraphArg);
}
if (configuration.get('graph.layout') === 'panel') {
return executeCommand(Commands.ShowGraphView, ...args);
}
return executeCommand<WebviewPanelShowCommandArgs>(
Commands.ShowGraphPage,
{ _type: 'WebviewPanelShowOptions' },
undefined,
...args,
);
}),
registerCommand(`${panels.id}.switchToEditorLayout`, async () => {
await configuration.updateEffective('graph.layout', 'editor');
queueMicrotask(
@ -120,6 +152,14 @@ export function registerGraphWebviewCommands(container: Container, panels: Webvi
) => {
const preserveFocus = 'preserveFocus' in args ? args.preserveFocus ?? false : false;
if (configuration.get('graph.layout') === 'panel') {
if (!container.graphView.visible) {
const instance = panels.getBestInstance({ preserveFocus: preserveFocus }, args);
if (instance != null) {
void instance.show({ preserveFocus: preserveFocus }, args);
return;
}
}
void container.graphView.show({ preserveFocus: preserveFocus }, args);
} else {
void panels.show({ preserveFocus: preserveFocus }, args);

+ 11
- 4
src/plus/webviews/timeline/registration.ts View File

@ -1,12 +1,16 @@
import type { Uri } from 'vscode';
import { Disposable, ViewColumn } from 'vscode';
import { Commands } from '../../../constants';
import { registerCommand } from '../../../system/command';
import { configuration } from '../../../system/configuration';
import type { ViewFileNode } from '../../../views/nodes/abstract/viewFileNode';
import type { WebviewPanelsProxy, WebviewsController } from '../../../webviews/webviewsController';
import type { State } from './protocol';
export type TimelineWebviewShowingArgs = [Uri | ViewFileNode];
export function registerTimelineWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(
return controller.registerWebviewPanel<State, State, TimelineWebviewShowingArgs>(
{ id: Commands.ShowTimelinePage, options: { preserveInstance: true } },
{
id: 'gitlens.timeline',
@ -31,7 +35,7 @@ export function registerTimelineWebviewPanel(controller: WebviewsController) {
}
export function registerTimelineWebviewView(controller: WebviewsController) {
return controller.registerWebviewView<State>(
return controller.registerWebviewView<State, State, TimelineWebviewShowingArgs>(
{
id: 'gitlens.views.timeline',
fileName: 'timeline.html',
@ -50,9 +54,12 @@ export function registerTimelineWebviewView(controller: WebviewsController) {
);
}
export function registerTimelineWebviewCommands(panels: WebviewPanelsProxy) {
export function registerTimelineWebviewCommands<T>(panels: WebviewPanelsProxy<TimelineWebviewShowingArgs, T>) {
return Disposable.from(
registerCommand(Commands.ShowInTimeline, (...args: unknown[]) => void panels.show(undefined, ...args)),
registerCommand(
Commands.ShowInTimeline,
(...args: TimelineWebviewShowingArgs) => void panels.show(undefined, ...args),
),
registerCommand(`${panels.id}.refresh`, () => void panels.getActiveInstance()?.refresh(true)),
registerCommand(
`${panels.id}.split`,

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

@ -17,17 +17,17 @@ 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/abstract/viewFileNode';
import { isViewFileNode } from '../../../views/nodes/abstract/viewFileNode';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import type { WebviewController, WebviewProvider, WebviewShowingArgs } from '../../../webviews/webviewController';
import { updatePendingContext } from '../../../webviews/webviewController';
import type { WebviewShowOptions } from '../../../webviews/webviewsController';
import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../gk/account/subscriptionService';
import type { Commit, Period, State } from './protocol';
import { DidChangeNotificationType, OpenDataPointCommandType, UpdatePeriodCommandType } from './protocol';
import type { TimelineWebviewShowingArgs } from './registration';
interface Context {
uri: Uri | undefined;
@ -39,7 +39,7 @@ interface Context {
const defaultPeriod: Period = '3|M';
export class TimelineWebviewProvider implements WebviewProvider<State> {
export class TimelineWebviewProvider implements WebviewProvider<State, State, TimelineWebviewShowingArgs> {
private _bootstraping = true;
/** The context the webview has */
private _context: Context;
@ -49,7 +49,7 @@ export class TimelineWebviewProvider implements WebviewProvider {
constructor(
private readonly container: Container,
private readonly host: WebviewController<State>,
private readonly host: WebviewController<State, State, TimelineWebviewShowingArgs>,
) {
this._context = {
uri: undefined,
@ -87,7 +87,7 @@ export class TimelineWebviewProvider implements WebviewProvider {
void this.notifyDidChangeState(true);
}
canReuseInstance(...args: [Uri | ViewFileNode | { state: Partial<State> }] | unknown[]): boolean | undefined {
canReuseInstance(...args: WebviewShowingArgs<TimelineWebviewShowingArgs, State>): boolean | undefined {
let uri: Uri | undefined;
const [arg] = args;
@ -106,14 +106,14 @@ export class TimelineWebviewProvider implements WebviewProvider {
return uri?.toString() === this._context.uri?.toString() ? true : undefined;
}
getSplitArgs(): unknown[] {
return [this._context.uri];
getSplitArgs(): WebviewShowingArgs<TimelineWebviewShowingArgs, State> {
return this._context.uri != null ? [this._context.uri] : [];
}
onShowing(
loading: boolean,
_options: WebviewShowOptions | undefined,
...args: [Uri | ViewFileNode | { state: Partial<State> }] | unknown[]
...args: WebviewShowingArgs<TimelineWebviewShowingArgs, State>
): boolean {
const [arg] = args;
if (arg != null) {

+ 7
- 4
src/webviews/commitDetails/commitDetailsWebview.ts View File

@ -47,7 +47,7 @@ import { serialize } from '../../system/serialize';
import type { LinesChangeEvent } from '../../trackers/lineTracker';
import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol';
import type { WebviewController, WebviewProvider } from '../webviewController';
import type { WebviewController, WebviewProvider, WebviewShowingArgs } from '../webviewController';
import { updatePendingContext } from '../webviewController';
import { isSerializedState } from '../webviewsController';
import type {
@ -84,6 +84,7 @@ import {
UnstageFileCommandType,
UpdatePreferencesCommandType,
} from './protocol';
import type { CommitDetailsWebviewShowingArgs } from './registration';
type RepositorySubscription = { repo: Repository; subscription: Disposable };
@ -106,7 +107,9 @@ interface Context {
wip: Wip | undefined;
}
export class CommitDetailsWebviewProvider implements WebviewProvider<State, Serialized<State>> {
export class CommitDetailsWebviewProvider
implements WebviewProvider<State, Serialized<State>, CommitDetailsWebviewShowingArgs>
{
private _bootstraping = true;
/** The context the webview has */
private _context: Context;
@ -119,7 +122,7 @@ export class CommitDetailsWebviewProvider implements WebviewProvider
constructor(
private readonly container: Container,
private readonly host: WebviewController<State, Serialized<State>>,
private readonly host: WebviewController<State, Serialized<State>, CommitDetailsWebviewShowingArgs>,
private readonly options: { attachedTo: 'default' | 'graph' },
) {
this._context = {
@ -170,7 +173,7 @@ export class CommitDetailsWebviewProvider implements WebviewProvider
async onShowing(
_loading: boolean,
options: { column?: ViewColumn; preserveFocus?: boolean },
...args: [Partial<CommitSelectedEvent['data']> | { state: Partial<Serialized<State>> }] | unknown[]
...args: WebviewShowingArgs<CommitDetailsWebviewShowingArgs, Serialized<State>>
): Promise<boolean> {
let data: Partial<CommitSelectedEvent['data']> | undefined;

+ 5
- 2
src/webviews/commitDetails/registration.ts View File

@ -1,9 +1,12 @@
import type { CommitSelectedEvent } from '../../eventBus';
import type { Serialized } from '../../system/serialize';
import type { WebviewsController } from '../webviewsController';
import type { State } from './protocol';
export type CommitDetailsWebviewShowingArgs = [Partial<CommitSelectedEvent['data']>];
export function registerCommitDetailsWebviewView(controller: WebviewsController) {
return controller.registerWebviewView<State, Serialized<State>>(
return controller.registerWebviewView<State, Serialized<State>, CommitDetailsWebviewShowingArgs>(
{
id: 'gitlens.views.commitDetails',
fileName: 'commitDetails.html',
@ -25,7 +28,7 @@ export function registerCommitDetailsWebviewView(controller: WebviewsController)
}
export function registerGraphDetailsWebviewView(controller: WebviewsController) {
return controller.registerWebviewView<State, Serialized<State>>(
return controller.registerWebviewView<State, Serialized<State>, CommitDetailsWebviewShowingArgs>(
{
id: 'gitlens.views.graphDetails',
fileName: 'commitDetails.html',

+ 5
- 3
src/webviews/settings/registration.ts View File

@ -4,8 +4,10 @@ import { registerCommand } from '../../system/command';
import type { WebviewPanelsProxy, WebviewsController } from '../webviewsController';
import type { State } from './protocol';
export type SettingsWebviewShowingArgs = [string];
export function registerSettingsWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(
return controller.registerWebviewPanel<State, State, SettingsWebviewShowingArgs>(
{ id: Commands.ShowSettingsPage },
{
id: 'gitlens.settings',
@ -28,7 +30,7 @@ export function registerSettingsWebviewPanel(controller: WebviewsController) {
);
}
export function registerSettingsWebviewCommands(panels: WebviewPanelsProxy) {
export function registerSettingsWebviewCommands<T>(panels: WebviewPanelsProxy<SettingsWebviewShowingArgs, T>) {
return Disposable.from(
...[
Commands.ShowSettingsPageAndJumpToBranchesView,
@ -53,7 +55,7 @@ export function registerSettingsWebviewCommands(panels: WebviewPanelsProxy) {
[, anchor] = match;
}
return registerCommand(c, (...args: any[]) => void panels.show(undefined, anchor, ...args));
return registerCommand(c, () => void panels.show(undefined, ...(anchor ? [anchor] : [])));
}),
);
}

+ 4
- 3
src/webviews/settings/settingsWebview.ts View File

@ -24,14 +24,15 @@ import {
} from '../protocol';
import type { WebviewController, WebviewProvider } from '../webviewController';
import type { State } from './protocol';
import type { SettingsWebviewShowingArgs } from './registration';
export class SettingsWebviewProvider implements WebviewProvider<State> {
export class SettingsWebviewProvider implements WebviewProvider<State, State, SettingsWebviewShowingArgs> {
private readonly _disposable: Disposable;
private _pendingJumpToAnchor: string | undefined;
constructor(
protected readonly container: Container,
protected readonly host: WebviewController<State>,
protected readonly host: WebviewController<State, State, SettingsWebviewShowingArgs>,
) {
this._disposable = configuration.onDidChangeAny(this.onAnyConfigurationChanged, this);
}
@ -64,7 +65,7 @@ export class SettingsWebviewProvider implements WebviewProvider {
onShowing?(
loading: boolean,
_options: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
...args: SettingsWebviewShowingArgs
): boolean | Promise<boolean> {
const anchor = args[0];
if (anchor && typeof anchor === 'string') {

+ 46
- 31
src/webviews/webviewController.ts View File

@ -40,14 +40,21 @@ type GetParentType = T
? WebviewView
: never;
export interface WebviewProvider<State, SerializedState = State> extends Disposable {
export type WebviewShowingArgs<T extends unknown[], SerializedState> = T | [{ state: Partial<SerializedState> }] | [];
export interface WebviewProvider<State, SerializedState = State, ShowingArgs extends unknown[] = unknown[]>
extends Disposable {
/**
* Determines whether the webview instance can be reused
* @returns `true` if the webview should be reused, `false` if it should NOT be reused, and `undefined` if it *could* be reused but not ideal
*/
canReuseInstance?(...args: unknown[]): boolean | undefined;
getSplitArgs?(): unknown[];
onShowing?(loading: boolean, options: WebviewShowOptions, ...args: unknown[]): boolean | Promise<boolean>;
canReuseInstance?(...args: WebviewShowingArgs<ShowingArgs, SerializedState>): boolean | undefined;
getSplitArgs?(): WebviewShowingArgs<ShowingArgs, SerializedState>;
onShowing?(
loading: boolean,
options: WebviewShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
): boolean | Promise<boolean>;
registerCommands?(): Disposable[];
includeBootstrap?(): SerializedState | Promise<SerializedState>;
@ -65,25 +72,26 @@ export interface WebviewProvider extends Disposa
onWindowFocusChanged?(focused: boolean): void;
}
type WebviewPanelController<State, SerializedState = State> = WebviewController<
type WebviewPanelController<
State,
SerializedState,
WebviewPanelDescriptor
>;
type WebviewViewController<State, SerializedState = State> = WebviewController<
SerializedState = State,
ShowingArgs extends unknown[] = unknown[],
> = WebviewController<State, SerializedState, ShowingArgs, WebviewPanelDescriptor>;
type WebviewViewController<
State,
SerializedState,
WebviewViewDescriptor
>;
SerializedState = State,
ShowingArgs extends unknown[] = unknown[],
> = WebviewController<State, SerializedState, ShowingArgs, WebviewViewDescriptor>;
@logName<WebviewController<any>>(c => `WebviewController(${c.id}${c.instanceId != null ? `|${c.instanceId}` : ''})`)
export class WebviewController<
State,
SerializedState = State,
ShowingArgs extends unknown[] = unknown[],
Descriptor extends WebviewPanelDescriptor | WebviewViewDescriptor = WebviewPanelDescriptor | WebviewViewDescriptor,
> implements Disposable
{
static async create<State, SerializedState = State>(
static async create<State, SerializedState = State, ShowingArgs extends unknown[] = unknown[]>(
container: Container,
commandRegistrar: WebviewCommandRegistrar,
descriptor: WebviewPanelDescriptor,
@ -91,10 +99,10 @@ export class WebviewController<
parent: WebviewPanel,
resolveProvider: (
container: Container,
controller: WebviewController<State, SerializedState>,
) => Promise<WebviewProvider<State, SerializedState>>,
): Promise<WebviewController<State, SerializedState, WebviewPanelDescriptor>>;
static async create<State, SerializedState = State>(
controller: WebviewController<State, SerializedState, ShowingArgs>,
) => Promise<WebviewProvider<State, SerializedState, ShowingArgs>>,
): Promise<WebviewController<State, SerializedState, ShowingArgs, WebviewPanelDescriptor>>;
static async create<State, SerializedState = State, ShowingArgs extends unknown[] = unknown[]>(
container: Container,
commandRegistrar: WebviewCommandRegistrar,
descriptor: WebviewViewDescriptor,
@ -102,10 +110,10 @@ export class WebviewController<
parent: WebviewView,
resolveProvider: (
container: Container,
controller: WebviewController<State, SerializedState>,
) => Promise<WebviewProvider<State, SerializedState>>,
): Promise<WebviewController<State, SerializedState, WebviewViewDescriptor>>;
static async create<State, SerializedState = State>(
controller: WebviewController<State, SerializedState, ShowingArgs>,
) => Promise<WebviewProvider<State, SerializedState, ShowingArgs>>,
): Promise<WebviewController<State, SerializedState, ShowingArgs, WebviewViewDescriptor>>;
static async create<State, SerializedState = State, ShowingArgs extends unknown[] = unknown[]>(
container: Container,
commandRegistrar: WebviewCommandRegistrar,
descriptor: WebviewPanelDescriptor | WebviewViewDescriptor,
@ -113,10 +121,10 @@ export class WebviewController<
parent: WebviewPanel | WebviewView,
resolveProvider: (
container: Container,
controller: WebviewController<State, SerializedState>,
) => Promise<WebviewProvider<State, SerializedState>>,
): Promise<WebviewController<State, SerializedState>> {
const controller = new WebviewController<State, SerializedState>(
controller: WebviewController<State, SerializedState, ShowingArgs>,
) => Promise<WebviewProvider<State, SerializedState, ShowingArgs>>,
): Promise<WebviewController<State, SerializedState, ShowingArgs>> {
const controller = new WebviewController<State, SerializedState, ShowingArgs>(
container,
commandRegistrar,
descriptor,
@ -141,7 +149,7 @@ export class WebviewController<
}
private disposable: Disposable | undefined;
private /*readonly*/ provider!: WebviewProvider<State, SerializedState>;
private /*readonly*/ provider!: WebviewProvider<State, SerializedState, ShowingArgs>;
private readonly webview: Webview;
private constructor(
@ -152,8 +160,8 @@ export class WebviewController<
public readonly parent: GetParentType<Descriptor>,
resolveProvider: (
container: Container,
controller: WebviewController<State, SerializedState>,
) => Promise<WebviewProvider<State, SerializedState>>,
controller: WebviewController<State, SerializedState, ShowingArgs>,
) => Promise<WebviewProvider<State, SerializedState, ShowingArgs>>,
) {
this.id = descriptor.id;
this.webview = parent.webview;
@ -267,21 +275,28 @@ export class WebviewController<
return this._disposed ? false : this.parent.visible;
}
canReuseInstance(options?: WebviewShowOptions, ...args: unknown[]): boolean | undefined {
canReuseInstance(
options?: WebviewShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
): boolean | undefined {
if (!this.isEditor()) return undefined;
if (options?.column != null && options.column !== this.parent.viewColumn) return false;
return this.provider.canReuseInstance?.(...args);
}
getSplitArgs(): unknown[] {
getSplitArgs(): WebviewShowingArgs<ShowingArgs, SerializedState> {
if (!this.isEditor()) return [];
return this.provider.getSplitArgs?.() ?? [];
}
@debug({ args: false })
async show(loading: boolean, options?: WebviewShowOptions, ...args: unknown[]) {
async show(
loading: boolean,
options?: WebviewShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
) {
if (options == null) {
options = {};
}

+ 131
- 93
src/webviews/webviewsController.ts View File

@ -18,7 +18,7 @@ import { Logger } from '../system/logger';
import { getNewLogScope } from '../system/logger.scope';
import type { TrackedUsageFeatures } from '../telemetry/usageTracker';
import { WebviewCommandRegistrar } from './webviewCommandRegistrar';
import type { WebviewProvider } from './webviewController';
import type { WebviewProvider, WebviewShowingArgs } from './webviewController';
import { WebviewController } from './webviewController';
export interface WebviewPanelDescriptor {
@ -36,29 +36,39 @@ export interface WebviewPanelDescriptor {
readonly allowMultipleInstances?: boolean;
}
interface WebviewPanelRegistration<State, SerializedState = State> {
interface WebviewPanelRegistration<State, SerializedState = State, ShowingArgs extends unknown[] = unknown[]> {
readonly descriptor: WebviewPanelDescriptor;
controllers?:
| Map<string | undefined, WebviewController<State, SerializedState, WebviewPanelDescriptor>>
| Map<string | undefined, WebviewController<State, SerializedState, ShowingArgs, WebviewPanelDescriptor>>
| undefined;
}
export interface WebviewPanelProxy extends Disposable {
export interface WebviewPanelProxy<ShowingArgs extends unknown[] = unknown[], SerializedState = unknown>
extends Disposable {
readonly id: WebviewIds;
readonly instanceId: string | undefined;
readonly ready: boolean;
readonly active: boolean;
readonly visible: boolean;
canReuseInstance(
options?: WebviewPanelShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
): boolean | undefined;
close(): void;
refresh(force?: boolean): Promise<void>;
show(options?: WebviewPanelShowOptions, ...args: unknown[]): Promise<void>;
show(options?: WebviewPanelShowOptions, ...args: WebviewShowingArgs<ShowingArgs, SerializedState>): Promise<void>;
}
export interface WebviewPanelsProxy extends Disposable {
export interface WebviewPanelsProxy<ShowingArgs extends unknown[] = unknown[], SerializedState = unknown>
extends Disposable {
readonly id: WebviewIds;
readonly instances: Iterable<WebviewPanelProxy>;
getActiveInstance(): WebviewPanelProxy | undefined;
show(options?: WebviewPanelsShowOptions, ...args: unknown[]): Promise<void>;
readonly instances: Iterable<WebviewPanelProxy<ShowingArgs, SerializedState>>;
getActiveInstance(): WebviewPanelProxy<ShowingArgs, SerializedState> | undefined;
getBestInstance(
options?: WebviewPanelShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
): WebviewPanelProxy<ShowingArgs, SerializedState> | undefined;
show(options?: WebviewPanelsShowOptions, ...args: WebviewShowingArgs<ShowingArgs, SerializedState>): Promise<void>;
splitActiveInstance(options?: WebviewPanelsShowOptions): Promise<void>;
}
@ -75,18 +85,20 @@ export interface WebviewViewDescriptor {
};
}
interface WebviewViewRegistration<State, SerializedState = State> {
interface WebviewViewRegistration<State, SerializedState = State, ShowingArgs extends unknown[] = unknown[]> {
readonly descriptor: WebviewViewDescriptor;
controller?: WebviewController<State, SerializedState, WebviewViewDescriptor> | undefined;
pendingShowArgs?: Parameters<WebviewViewProxy['show']> | undefined;
controller?: WebviewController<State, SerializedState, ShowingArgs, WebviewViewDescriptor> | undefined;
pendingShowArgs?:
| [WebviewViewShowOptions | undefined, WebviewShowingArgs<ShowingArgs, SerializedState>]
| undefined;
}
export interface WebviewViewProxy extends Disposable {
export interface WebviewViewProxy<ShowingArgs extends unknown[], SerializedState = unknown> extends Disposable {
readonly id: WebviewViewIds;
readonly ready: boolean;
readonly visible: boolean;
refresh(force?: boolean): Promise<void>;
show(options?: WebviewViewShowOptions, ...args: unknown[]): Promise<void>;
show(options?: WebviewViewShowOptions, ...args: WebviewShowingArgs<ShowingArgs, SerializedState>): Promise<void>;
}
export class WebviewsController implements Disposable {
@ -110,16 +122,16 @@ export class WebviewsController implements Disposable {
2: false,
},
})
registerWebviewView<State, SerializedState = State>(
registerWebviewView<State, SerializedState = State, ShowingArgs extends unknown[] = unknown[]>(
descriptor: WebviewViewDescriptor,
resolveProvider: (
container: Container,
controller: WebviewController<State, SerializedState>,
) => Promise<WebviewProvider<State, SerializedState>>,
): WebviewViewProxy {
controller: WebviewController<State, SerializedState, ShowingArgs>,
) => Promise<WebviewProvider<State, SerializedState, ShowingArgs>>,
): WebviewViewProxy<ShowingArgs, SerializedState> {
const scope = getNewLogScope(`WebviewView(${descriptor.id})`);
const registration: WebviewViewRegistration<State, SerializedState> = { descriptor: descriptor };
const registration: WebviewViewRegistration<State, SerializedState, ShowingArgs> = { descriptor: descriptor };
this._views.set(descriptor.id, registration);
const disposables: Disposable[] = [];
@ -170,18 +182,14 @@ export class WebviewsController implements Disposable {
controller,
);
let args = registration.pendingShowArgs;
let [options, args] = registration.pendingShowArgs ?? [];
registration.pendingShowArgs = undefined;
if (args == null && isSerializedState<State>(context)) {
args = [undefined, context];
args = [{ state: context.state }];
}
Logger.debug(scope, 'Showing view');
if (args != null) {
await controller.show(true, ...args);
} else {
await controller.show(true);
}
await controller.show(true, options, ...(args ?? []));
},
},
descriptor.webviewHostOptions != null ? { webviewOptions: descriptor.webviewHostOptions } : undefined,
@ -204,17 +212,20 @@ export class WebviewsController implements Disposable {
refresh: function (force?: boolean) {
return registration.controller != null ? registration.controller.refresh(force) : Promise.resolve();
},
show: function (options?: WebviewViewShowOptions, ...args: unknown[]) {
show: function (
options?: WebviewViewShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
) {
Logger.debug(scope, 'Showing view');
if (registration.controller != null) {
return registration.controller.show(false, options, ...args);
}
registration.pendingShowArgs = [options, ...args];
registration.pendingShowArgs = [options, args];
return Promise.resolve(void executeCoreCommand(`${descriptor.id}.focus`, options));
},
} satisfies WebviewViewProxy;
} satisfies WebviewViewProxy<ShowingArgs, SerializedState>;
}
@debug<WebviewsController['registerWebviewPanel']>({
@ -225,7 +236,7 @@ export class WebviewsController implements Disposable {
3: false,
},
})
registerWebviewPanel<State, SerializedState = State>(
registerWebviewPanel<State, SerializedState = State, ShowingArgs extends unknown[] = unknown[]>(
command: {
id: Commands;
options?: WebviewPanelsShowOptions;
@ -233,12 +244,12 @@ export class WebviewsController implements Disposable {
descriptor: WebviewPanelDescriptor,
resolveProvider: (
container: Container,
controller: WebviewController<State, SerializedState>,
) => Promise<WebviewProvider<State, SerializedState>>,
): WebviewPanelsProxy {
controller: WebviewController<State, SerializedState, ShowingArgs>,
) => Promise<WebviewProvider<State, SerializedState, ShowingArgs>>,
): WebviewPanelsProxy<ShowingArgs, SerializedState> {
const scope = getNewLogScope(`WebviewPanel(${descriptor.id})`);
const registration: WebviewPanelRegistration<State, SerializedState> = { descriptor: descriptor };
const registration: WebviewPanelRegistration<State, SerializedState, ShowingArgs> = { descriptor: descriptor };
this._panels.set(descriptor.id, registration);
const disposables: Disposable[] = [];
@ -246,7 +257,10 @@ export class WebviewsController implements Disposable {
let serializedPanel: WebviewPanel | undefined;
async function show(options?: WebviewPanelsShowOptions, ...args: unknown[]): Promise<void> {
async function show(
options?: WebviewPanelsShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
): Promise<void> {
const { descriptor } = registration;
if (descriptor.plusFeature) {
if (!(await ensurePlusFeaturesEnabled())) return;
@ -260,49 +274,7 @@ export class WebviewsController implements Disposable {
column = ViewColumn.Active;
}
let controller: WebviewController<State, SerializedState, WebviewPanelDescriptor> | undefined;
if (registration.controllers?.size) {
if (descriptor.allowMultipleInstances) {
if (options?.preserveInstance !== false) {
if (options?.preserveInstance != null && typeof options.preserveInstance === 'string') {
controller = registration.controllers.get(options.preserveInstance);
}
if (controller == null) {
let active;
let first;
for (const c of registration.controllers.values()) {
first ??= c;
if (c.active) {
active = c;
}
const canReuse = c.canReuseInstance(options, ...args);
if (canReuse === true) {
// If the webview says it should be reused, use it
controller = c;
break;
} else if (canReuse === false) {
// If the webview says it should not be reused don't and clear it from being first/active
if (first === c) {
first = undefined;
}
if (active === c) {
active = undefined;
}
}
}
if (controller == null && options?.preserveInstance === true) {
controller = active ?? first;
}
}
}
} else {
controller = first(registration.controllers)?.[1];
}
}
let controller = getBestController(registration, options, ...args);
if (controller == null) {
let panel: WebviewPanel;
if (serializedPanel != null) {
@ -366,14 +338,10 @@ export class WebviewsController implements Disposable {
// 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
serializedPanel = panel;
Logger.debug(scope, `Deserializing panel state=${state != null ? '<state>' : 'undefined'}`);
if (state != null) {
await show(
{ column: panel.viewColumn, preserveFocus: true, preserveInstance: false },
{ state: state },
);
} else {
await show({ column: panel.viewColumn, preserveFocus: true, preserveInstance: false });
}
await show(
{ column: panel.viewColumn, preserveFocus: true, preserveInstance: false },
...(state != null ? [{ state: state }] : []),
);
}
const disposable = Disposable.from(
@ -386,10 +354,10 @@ export class WebviewsController implements Disposable {
(...args: unknown[]) => {
if (hasWebviewPanelShowOptions(args)) {
const [{ _type, ...opts }, ...rest] = args;
return show({ ...command.options, ...opts }, ...rest);
return show({ ...command.options, ...opts }, ...(rest as ShowingArgs));
}
return show({ ...command.options }, ...args);
return show({ ...command.options }, ...(args as ShowingArgs));
},
this,
),
@ -408,6 +376,13 @@ export class WebviewsController implements Disposable {
const controller = find(registration.controllers.values(), c => c.active ?? false);
return controller != null ? convertToWebviewPanelProxy(controller) : undefined;
},
getBestInstance: function (
options?: WebviewPanelShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
) {
const controller = getBestController(registration, options, ...args);
return controller != null ? convertToWebviewPanelProxy(controller) : undefined;
},
splitActiveInstance: function (options?: WebviewPanelsShowOptions) {
const controller =
registration.controllers != null
@ -420,7 +395,7 @@ export class WebviewsController implements Disposable {
disposable.dispose();
},
show: show,
} satisfies WebviewPanelsProxy;
} satisfies WebviewPanelsProxy<ShowingArgs, SerializedState>;
}
}
@ -445,15 +420,21 @@ interface WebviewViewShowOptions {
export type WebviewShowOptions = WebviewPanelShowOptions | WebviewViewShowOptions;
function convertToWebviewPanelProxy<State, SerializedState>(
controller: WebviewController<State, SerializedState, WebviewPanelDescriptor>,
): WebviewPanelProxy {
function convertToWebviewPanelProxy<State, SerializedState, ShowingArgs extends unknown[] = unknown[]>(
controller: WebviewController<State, SerializedState, ShowingArgs, WebviewPanelDescriptor>,
): WebviewPanelProxy<ShowingArgs, SerializedState> {
return {
id: controller.id,
instanceId: controller.instanceId,
ready: controller.ready,
active: controller.active ?? false,
visible: controller.visible,
canReuseInstance: function (
options?: WebviewPanelShowOptions,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
) {
return controller.canReuseInstance(options, ...args);
},
close: function () {
controller.parent.dispose();
},
@ -463,12 +444,69 @@ function convertToWebviewPanelProxy(
refresh: function (force?: boolean) {
return controller.refresh(force);
},
show: function (options?: WebviewPanelShowOptions, ...args: unknown[]) {
show: function (options?: WebviewPanelShowOptions, ...args: WebviewShowingArgs<ShowingArgs, SerializedState>) {
return controller.show(false, options, ...args);
},
};
}
function getBestController<State, SerializedState, ShowingArgs extends unknown[]>(
registration: WebviewPanelRegistration<State, SerializedState, ShowingArgs>,
options: WebviewPanelsShowOptions | undefined,
...args: WebviewShowingArgs<ShowingArgs, SerializedState>
) {
let controller;
if (registration.controllers?.size) {
if (registration.descriptor.allowMultipleInstances) {
if (options?.preserveInstance !== false) {
if (options?.preserveInstance != null && typeof options.preserveInstance === 'string') {
controller = registration.controllers.get(options.preserveInstance);
}
if (controller == null) {
let active;
let first;
// Sort active controllers first
const sortedControllers = [...registration.controllers.values()].sort(
(a, b) => (a.active ? -1 : 1) - (b.active ? -1 : 1),
);
for (const c of sortedControllers) {
first ??= c;
if (c.active) {
active = c;
}
const canReuse = c.canReuseInstance(options, ...args);
if (canReuse === true) {
// If the webview says it should be reused, use it
controller = c;
break;
} else if (canReuse === false) {
// If the webview says it should not be reused don't and clear it from being first/active
if (first === c) {
first = undefined;
}
if (active === c) {
active = undefined;
}
}
}
if (controller == null && options?.preserveInstance === true) {
controller = active ?? first;
}
}
}
} else {
controller = first(registration.controllers)?.[1];
}
}
return controller;
}
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