Browse Source

Improves multi-instance webview support

- Adds split commands to force a new webview (when allowed)
main
Eric Amodio 1 year ago
parent
commit
e18a9312fe
8 changed files with 168 additions and 55 deletions
  1. +64
    -4
      package.json
  2. +6
    -4
      src/plus/webviews/focus/registration.ts
  3. +19
    -2
      src/plus/webviews/graph/graphWebview.ts
  4. +9
    -8
      src/plus/webviews/graph/registration.ts
  5. +6
    -4
      src/plus/webviews/timeline/registration.ts
  6. +26
    -4
      src/plus/webviews/timeline/timelineWebview.ts
  7. +8
    -0
      src/webviews/webviewController.ts
  8. +30
    -29
      src/webviews/webviewsController.ts

+ 64
- 4
package.json View File

@ -4962,6 +4962,12 @@
"icon": "$(layers)" "icon": "$(layers)"
}, },
{ {
"command": "gitlens.focus.split",
"title": "Split Focus View",
"category": "GitLens",
"icon": "$(split-horizontal)"
},
{
"command": "gitlens.showGraph", "command": "gitlens.showGraph",
"title": "Show Commit Graph", "title": "Show Commit Graph",
"category": "GitLens", "category": "GitLens",
@ -4974,6 +4980,12 @@
"icon": "$(gitlens-graph)" "icon": "$(gitlens-graph)"
}, },
{ {
"command": "gitlens.graph.split",
"title": "Split Commit Graph",
"category": "GitLens",
"icon": "$(split-horizontal)"
},
{
"command": "gitlens.showGraphView", "command": "gitlens.showGraphView",
"title": "Show Commit Graph View", "title": "Show Commit Graph View",
"category": "GitLens", "category": "GitLens",
@ -5130,6 +5142,12 @@
"icon": "$(graph-scatter)" "icon": "$(graph-scatter)"
}, },
{ {
"command": "gitlens.timeline.split",
"title": "Split Visual File History",
"category": "GitLens",
"icon": "$(split-horizontal)"
},
{
"command": "gitlens.showStashesView", "command": "gitlens.showStashesView",
"title": "Show Stashes View", "title": "Show Stashes View",
"category": "GitLens" "category": "GitLens"
@ -8332,6 +8350,10 @@
"when": "gitlens:enabled" "when": "gitlens:enabled"
}, },
{ {
"command": "gitlens.focus.split",
"when": "gitlens:enabled && config.gitlens.focus.experimental.allowMultipleInstances"
},
{
"command": "gitlens.showGraph", "command": "gitlens.showGraph",
"when": "gitlens:enabled" "when": "gitlens:enabled"
}, },
@ -8340,6 +8362,10 @@
"when": "gitlens:enabled" "when": "gitlens:enabled"
}, },
{ {
"command": "gitlens.graph.split",
"when": "gitlens:enabled && config.gitlens.graph.experimental.allowMultipleInstances"
},
{
"command": "gitlens.showGraphView", "command": "gitlens.showGraphView",
"when": "gitlens:enabled" "when": "gitlens:enabled"
}, },
@ -8448,6 +8474,10 @@
"when": "gitlens:enabled && gitlens:activeFileStatus =~ /tracked/" "when": "gitlens:enabled && gitlens:activeFileStatus =~ /tracked/"
}, },
{ {
"command": "gitlens.timeline.split",
"when": "gitlens:enabled && config.gitlens.visualHistory.experimental.allowMultipleInstances"
},
{
"command": "gitlens.showTimelineView", "command": "gitlens.showTimelineView",
"when": "gitlens:enabled" "when": "gitlens:enabled"
}, },
@ -10609,23 +10639,38 @@
}, },
{ {
"command": "gitlens.timeline.refresh", "command": "gitlens.timeline.refresh",
"when": "gitlens:webview:timeline:active",
"when": "activeWebviewPanelId === gitlens.timeline",
"group": "navigation@-99" "group": "navigation@-99"
}, },
{ {
"command": "gitlens.graph.refresh", "command": "gitlens.graph.refresh",
"when": "gitlens:webview:graph:active",
"when": "activeWebviewPanelId === gitlens.graph",
"group": "navigation@-99" "group": "navigation@-99"
}, },
{ {
"submenu": "gitlens/graph/configuration", "submenu": "gitlens/graph/configuration",
"when": "gitlens:webview:graph:active",
"when": "activeWebviewPanelId === gitlens.graph",
"group": "navigation@-98" "group": "navigation@-98"
}, },
{ {
"command": "gitlens.focus.refresh", "command": "gitlens.focus.refresh",
"when": "gitlens:webview:focus:active",
"when": "activeWebviewPanelId === gitlens.focus",
"group": "navigation@-98" "group": "navigation@-98"
},
{
"command": "gitlens.focus.split",
"when": "resourceScheme == webview-panel && activeWebviewPanelId === gitlens.focus && config.gitlens.focus.experimental.allowMultipleInstances",
"group": "navigation@-97"
},
{
"command": "gitlens.graph.split",
"when": "resourceScheme == webview-panel && activeWebviewPanelId === gitlens.graph && config.gitlens.graph.experimental.allowMultipleInstances",
"group": "navigation@-97"
},
{
"command": "gitlens.timeline.split",
"when": "resourceScheme == webview-panel && activeWebviewPanelId === gitlens.timeline && config.gitlens.visualHistory.experimental.allowMultipleInstances",
"group": "navigation@-97"
} }
], ],
"editor/title/context": [ "editor/title/context": [
@ -10658,6 +10703,21 @@
"submenu": "gitlens/editor/history", "submenu": "gitlens/editor/history",
"when": "gitlens:enabled && config.gitlens.menus.editorTab.history && isFileSystemResource && resourceScheme =~ /^(?!output$|vscode-(?!remote|vfs$)).*$/", "when": "gitlens:enabled && config.gitlens.menus.editorTab.history && isFileSystemResource && resourceScheme =~ /^(?!output$|vscode-(?!remote|vfs$)).*$/",
"group": "2_a_gitlens_open_file@1" "group": "2_a_gitlens_open_file@1"
},
{
"command": "gitlens.focus.split",
"when": "resourceScheme == webview-panel && activeWebviewPanelId === gitlens.focus && config.gitlens.focus.experimental.allowMultipleInstances",
"group": "6_split_in_group_gitlens@2"
},
{
"command": "gitlens.graph.split",
"when": "resourceScheme == webview-panel && activeWebviewPanelId === gitlens.graph && config.gitlens.graph.experimental.allowMultipleInstances",
"group": "6_split_in_group_gitlens@2"
},
{
"command": "gitlens.timeline.split",
"when": "resourceScheme == webview-panel && activeWebviewPanelId === gitlens.timeline && config.gitlens.visualHistory.experimental.allowMultipleInstances",
"group": "6_split_in_group_gitlens@2"
} }
], ],
"explorer/context": [ "explorer/context": [

+ 6
- 4
src/plus/webviews/focus/registration.ts View File

@ -7,7 +7,7 @@ import type { State } from './protocol';
export function registerFocusWebviewPanel(controller: WebviewsController) { export function registerFocusWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>( return controller.registerWebviewPanel<State>(
{ id: Commands.ShowFocusPage, options: { preserveInstance: false } },
{ id: Commands.ShowFocusPage, options: { preserveInstance: true } },
{ {
id: 'gitlens.focus', id: 'gitlens.focus',
fileName: 'focus.html', fileName: 'focus.html',
@ -32,8 +32,10 @@ export function registerFocusWebviewPanel(controller: WebviewsController) {
export function registerFocusWebviewCommands(panels: WebviewPanelsProxy) { export function registerFocusWebviewCommands(panels: WebviewPanelsProxy) {
return Disposable.from( return Disposable.from(
registerCommand(`${panels.id}.refresh`, () => {
void panels.getActiveOrFirstInstance()?.refresh(true);
}),
registerCommand(`${panels.id}.refresh`, () => void panels.getActiveInstance()?.refresh(true)),
registerCommand(
`${panels.id}.split`,
() => void panels.show({ preserveInstance: false, column: ViewColumn.Beside }),
),
); );
} }

+ 19
- 2
src/plus/webviews/graph/graphWebview.ts View File

@ -84,7 +84,7 @@ import { RepositoryFolderNode } from '../../../views/nodes/viewNode';
import type { IpcMessage, IpcNotificationType } from '../../../webviews/protocol'; import type { IpcMessage, IpcNotificationType } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol'; import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController'; import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import type { WebviewPanelShowCommandArgs } from '../../../webviews/webviewsController';
import type { WebviewPanelShowCommandArgs, WebviewShowOptions } from '../../../webviews/webviewsController';
import { isSerializedState } from '../../../webviews/webviewsController'; import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService'; import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type { import type {
@ -289,6 +289,23 @@ export class GraphWebviewProvider implements WebviewProvider {
this._disposable.dispose(); this._disposable.dispose();
} }
canReuseInstance(_options?: WebviewShowOptions, ...args: unknown[]): boolean | undefined {
if (this.container.git.openRepositoryCount === 1) return true;
const [arg] = args;
let repository: Repository | undefined;
if (isRepository(arg)) {
repository = arg;
} else if (hasGitReference(arg)) {
repository = this.container.git.getRepository(arg.ref.repoPath);
} else if (isSerializedState<State>(arg) && arg.state.selectedRepository != null) {
repository = this.container.git.getRepository(arg.state.selectedRepository);
}
return repository == null ? undefined : repository.uri.toString() === this.repository?.uri.toString();
}
async onShowing( async onShowing(
loading: boolean, loading: boolean,
_options: { column?: ViewColumn; preserveFocus?: boolean }, _options: { column?: ViewColumn; preserveFocus?: boolean },
@ -372,7 +389,7 @@ export class GraphWebviewProvider implements WebviewProvider {
() => () =>
void executeCommand<WebviewPanelShowCommandArgs>( void executeCommand<WebviewPanelShowCommandArgs>(
Commands.ShowGraphPage, Commands.ShowGraphPage,
{ _type: 'WebviewPanelShowOptions', preserveInstance: true },
{ _type: 'WebviewPanelShowOptions' },
this.repository, this.repository,
), ),
), ),

+ 9
- 8
src/plus/webviews/graph/registration.ts View File

@ -19,7 +19,7 @@ import type { ShowInCommitGraphCommandArgs, State } from './protocol';
export function registerGraphWebviewPanel(controller: WebviewsController) { export function registerGraphWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>( return controller.registerWebviewPanel<State>(
{ id: Commands.ShowGraphPage, options: { preserveInstance: false } },
{ id: Commands.ShowGraphPage, options: { preserveInstance: true } },
{ {
id: 'gitlens.graph', id: 'gitlens.graph',
fileName: 'graph.html', fileName: 'graph.html',
@ -69,7 +69,7 @@ export function registerGraphWebviewCommands(container: Container, panels: Webvi
? executeCommand(Commands.ShowGraphView, ...args) ? executeCommand(Commands.ShowGraphView, ...args)
: executeCommand<WebviewPanelShowCommandArgs>( : executeCommand<WebviewPanelShowCommandArgs>(
Commands.ShowGraphPage, Commands.ShowGraphPage,
{ _type: 'WebviewPanelShowOptions', preserveInstance: true },
{ _type: 'WebviewPanelShowOptions' },
undefined, undefined,
...args, ...args,
), ),
@ -80,7 +80,6 @@ export function registerGraphWebviewCommands(container: Container, panels: Webvi
() => () =>
void executeCommand<WebviewPanelShowCommandArgs>(Commands.ShowGraphPage, { void executeCommand<WebviewPanelShowCommandArgs>(Commands.ShowGraphPage, {
_type: 'WebviewPanelShowOptions', _type: 'WebviewPanelShowOptions',
preserveInstance: true,
}), }),
); );
}), }),
@ -123,8 +122,8 @@ export function registerGraphWebviewCommands(container: Container, panels: Webvi
if (configuration.get('graph.layout') === 'panel') { if (configuration.get('graph.layout') === 'panel') {
void container.graphView.show({ preserveFocus: preserveFocus }, args); void container.graphView.show({ preserveFocus: preserveFocus }, args);
} else { } else {
const active = panels.getActiveInstance()?.instanceId;
void panels.show({ preserveFocus: preserveFocus, preserveInstance: active ?? true }, args);
// const active = panels.getActiveInstance()?.instanceId;
void panels.show({ preserveFocus: preserveFocus /*preserveInstance: active ?? true*/ }, args);
} }
}, },
), ),
@ -144,8 +143,10 @@ export function registerGraphWebviewCommands(container: Container, panels: Webvi
void container.graphView.show({ preserveFocus: preserveFocus }, args); void container.graphView.show({ preserveFocus: preserveFocus }, args);
}, },
), ),
registerCommand(`${panels.id}.refresh`, () => {
void panels.getActiveOrFirstInstance()?.refresh(true);
}),
registerCommand(`${panels.id}.refresh`, () => void panels.getActiveInstance()?.refresh(true)),
registerCommand(
`${panels.id}.split`,
() => void panels.show({ preserveInstance: false, column: ViewColumn.Beside }),
),
); );
} }

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

@ -7,7 +7,7 @@ import type { State } from './protocol';
export function registerTimelineWebviewPanel(controller: WebviewsController) { export function registerTimelineWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>( return controller.registerWebviewPanel<State>(
{ id: Commands.ShowTimelinePage, options: { preserveInstance: false } },
{ id: Commands.ShowTimelinePage, options: { preserveInstance: true } },
{ {
id: 'gitlens.timeline', id: 'gitlens.timeline',
fileName: 'timeline.html', fileName: 'timeline.html',
@ -52,8 +52,10 @@ export function registerTimelineWebviewView(controller: WebviewsController) {
export function registerTimelineWebviewCommands(panels: WebviewPanelsProxy) { export function registerTimelineWebviewCommands(panels: WebviewPanelsProxy) {
return Disposable.from( return Disposable.from(
registerCommand(`${panels.id}.refresh`, () => {
void panels.getActiveOrFirstInstance()?.refresh(true);
}),
registerCommand(`${panels.id}.refresh`, () => void panels.getActiveInstance()?.refresh(true)),
registerCommand(
`${panels.id}.split`,
() => void panels.show({ preserveInstance: false, column: ViewColumn.Beside }),
),
); );
} }

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

@ -1,4 +1,4 @@
import type { TextEditor, ViewColumn } from 'vscode';
import type { TextEditor } from 'vscode';
import { Disposable, Uri, window } from 'vscode'; import { Disposable, Uri, window } from 'vscode';
import { Commands } from '../../../constants'; import { Commands } from '../../../constants';
import type { Container } from '../../../container'; import type { Container } from '../../../container';
@ -23,7 +23,7 @@ import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol'; import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController'; import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import { updatePendingContext } from '../../../webviews/webviewController'; import { updatePendingContext } from '../../../webviews/webviewController';
import type { WebviewPanelShowCommandArgs } from '../../../webviews/webviewsController';
import type { WebviewPanelShowCommandArgs, WebviewShowOptions } from '../../../webviews/webviewsController';
import { isSerializedState } from '../../../webviews/webviewsController'; import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService'; import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type { Commit, Period, State } from './protocol'; import type { Commit, Period, State } from './protocol';
@ -87,9 +87,31 @@ export class TimelineWebviewProvider implements WebviewProvider {
void this.notifyDidChangeState(true); void this.notifyDidChangeState(true);
} }
canReuseInstance(
_options?: WebviewShowOptions,
...args: [Uri | ViewFileNode | { state: Partial<State> }] | unknown[]
): boolean | undefined {
let uri: Uri | undefined;
const [arg] = args;
if (arg != null) {
if (arg instanceof Uri) {
uri = arg;
} else if (isViewFileNode(arg)) {
uri = arg.uri;
} else if (isSerializedState<State>(arg) && arg.state.uri != null) {
uri = Uri.parse(arg.state.uri);
}
} else {
uri = window.activeTextEditor?.document.uri;
}
return uri == null ? undefined : uri.toString() === this._context.uri?.toString();
}
onShowing( onShowing(
loading: boolean, loading: boolean,
_options: { column?: ViewColumn; preserveFocus?: boolean },
_options: WebviewShowOptions | undefined,
...args: [Uri | ViewFileNode | { state: Partial<State> }] | unknown[] ...args: [Uri | ViewFileNode | { state: Partial<State> }] | unknown[]
): boolean { ): boolean {
const [arg] = args; const [arg] = args;
@ -138,7 +160,7 @@ export class TimelineWebviewProvider implements WebviewProvider {
void executeCommand<WebviewPanelShowCommandArgs>( void executeCommand<WebviewPanelShowCommandArgs>(
Commands.ShowTimelinePage, Commands.ShowTimelinePage,
{ _type: 'WebviewPanelShowOptions', preserveInstance: true },
{ _type: 'WebviewPanelShowOptions' },
this._context.uri, this._context.uri,
); );
}, },

+ 8
- 0
src/webviews/webviewController.ts View File

@ -41,6 +41,7 @@ type GetParentType = T
: never; : never;
export interface WebviewProvider<State, SerializedState = State> extends Disposable { export interface WebviewProvider<State, SerializedState = State> extends Disposable {
canReuseInstance?(options?: WebviewShowOptions, ...args: unknown[]): boolean | undefined;
onShowing?(loading: boolean, options: WebviewShowOptions, ...args: unknown[]): boolean | Promise<boolean>; onShowing?(loading: boolean, options: WebviewShowOptions, ...args: unknown[]): boolean | Promise<boolean>;
registerCommands?(): Disposable[]; registerCommands?(): Disposable[];
@ -261,6 +262,13 @@ export class WebviewController<
return this._disposed ? false : this.parent.visible; return this._disposed ? false : this.parent.visible;
} }
canReuseInstance(options?: WebviewShowOptions, ...args: unknown[]): boolean | undefined {
if (!this.isEditor()) return undefined;
if (options?.column != null && options.column !== this.parent.viewColumn) return false;
return this.provider.canReuseInstance?.(options, ...args);
}
@debug({ args: false }) @debug({ args: false })
async show(loading: boolean, options?: WebviewShowOptions, ...args: unknown[]) { async show(loading: boolean, options?: WebviewShowOptions, ...args: unknown[]) {
if (options == null) { if (options == null) {

+ 30
- 29
src/webviews/webviewsController.ts View File

@ -58,7 +58,6 @@ export interface WebviewPanelsProxy extends Disposable {
readonly id: WebviewIds; readonly id: WebviewIds;
readonly instances: Iterable<WebviewPanelProxy>; readonly instances: Iterable<WebviewPanelProxy>;
getActiveInstance(): WebviewPanelProxy | undefined; getActiveInstance(): WebviewPanelProxy | undefined;
getActiveOrFirstInstance(): WebviewPanelProxy | undefined;
show(options?: WebviewPanelsShowOptions, ...args: unknown[]): Promise<void>; show(options?: WebviewPanelsShowOptions, ...args: unknown[]): Promise<void>;
} }
@ -269,15 +268,37 @@ export class WebviewsController implements Disposable {
column = ViewColumn.Active; column = ViewColumn.Active;
} }
let preserveInstance: string | boolean;
// eslint-disable-next-line prefer-const
({ preserveInstance, ...options } = { preserveInstance: true, ...options });
let controller: WebviewController<State, SerializedState, WebviewPanelDescriptor> | undefined; let controller: WebviewController<State, SerializedState, WebviewPanelDescriptor> | undefined;
if (!descriptor.allowMultipleInstances || preserveInstance === true) {
controller = getActiveOrFirstController(registration.controllers);
} else if (preserveInstance != null && typeof preserveInstance === 'string') {
controller = registration.controllers?.get(preserveInstance);
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;
}
if (c.canReuseInstance(options, ...args) === true) {
controller = c;
break;
}
}
if (controller == null && options?.preserveInstance === true) {
controller = active ?? first;
}
}
}
} else {
controller = first(registration.controllers)?.[1];
}
} }
if (controller == null) { if (controller == null) {
@ -383,10 +404,6 @@ export class WebviewsController implements Disposable {
const controller = find(registration.controllers.values(), c => c.active ?? false); const controller = find(registration.controllers.values(), c => c.active ?? false);
return controller != null ? convertToWebviewPanelProxy(controller) : undefined; return controller != null ? convertToWebviewPanelProxy(controller) : undefined;
}, },
getActiveOrFirstInstance: function () {
const controller = getActiveOrFirstController(registration.controllers);
return controller != null ? convertToWebviewPanelProxy(controller) : undefined;
},
dispose: function () { dispose: function () {
disposable.dispose(); disposable.dispose();
}, },
@ -416,22 +433,6 @@ interface WebviewViewShowOptions {
export type WebviewShowOptions = WebviewPanelShowOptions | WebviewViewShowOptions; export type WebviewShowOptions = WebviewPanelShowOptions | WebviewViewShowOptions;
function getActiveOrFirstController<State, SerializedState>(
controllers: Map<string | undefined, WebviewController<State, SerializedState, WebviewPanelDescriptor>> | undefined,
) {
if (!controllers?.size) return undefined;
if (controllers.size === 1) return first(controllers.values());
let firstController;
for (const controller of controllers.values()) {
if (controller.active) return controller;
firstController ??= controller;
}
return firstController;
}
function convertToWebviewPanelProxy<State, SerializedState>( function convertToWebviewPanelProxy<State, SerializedState>(
controller: WebviewController<State, SerializedState, WebviewPanelDescriptor>, controller: WebviewController<State, SerializedState, WebviewPanelDescriptor>,
): WebviewPanelProxy { ): WebviewPanelProxy {

Loading…
Cancel
Save