Przeglądaj źródła

Adds support for multi-instance webview panels

- Prepares Graph, Focus, & Timeline for multi-instance support
main
Eric Amodio 1 rok temu
rodzic
commit
16692734bf
40 zmienionych plików z 380 dodań i 193 usunięć
  1. +0
    -1
      src/constants.ts
  2. +25
    -13
      src/container.ts
  3. +1
    -2
      src/plus/webviews/account/accountWebview.ts
  4. +2
    -5
      src/plus/webviews/account/protocol.ts
  5. +6
    -14
      src/plus/webviews/focus/focusWebview.ts
  6. +2
    -5
      src/plus/webviews/focus/protocol.ts
  7. +12
    -3
      src/plus/webviews/focus/registration.ts
  8. +13
    -8
      src/plus/webviews/graph/graphWebview.ts
  9. +2
    -5
      src/plus/webviews/graph/protocol.ts
  10. +16
    -9
      src/plus/webviews/graph/registration.ts
  11. +2
    -5
      src/plus/webviews/timeline/protocol.ts
  12. +12
    -3
      src/plus/webviews/timeline/registration.ts
  13. +24
    -23
      src/plus/webviews/timeline/timelineWebview.ts
  14. +5
    -1
      src/system/webview.ts
  15. +1
    -1
      src/webviews/apps/commitDetails/commitDetails.html
  16. +5
    -1
      src/webviews/apps/home/home.html
  17. +1
    -1
      src/webviews/apps/plus/account/account.html
  18. +5
    -1
      src/webviews/apps/plus/focus/focus.html
  19. +11
    -3
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  20. +1
    -1
      src/webviews/apps/plus/graph/graph.html
  21. +5
    -1
      src/webviews/apps/plus/timeline/timeline.html
  22. +5
    -1
      src/webviews/apps/rebase/rebase.html
  23. +5
    -1
      src/webviews/apps/settings/settings.html
  24. +1
    -1
      src/webviews/apps/welcome/welcome.html
  25. +1
    -2
      src/webviews/commitDetails/commitDetailsWebview.ts
  26. +2
    -4
      src/webviews/commitDetails/protocol.ts
  27. +1
    -2
      src/webviews/home/homeWebview.ts
  28. +2
    -5
      src/webviews/home/protocol.ts
  29. +7
    -0
      src/webviews/protocol.ts
  30. +2
    -4
      src/webviews/rebase/protocol.ts
  31. +2
    -0
      src/webviews/rebase/rebaseEditor.ts
  32. +2
    -5
      src/webviews/settings/protocol.ts
  33. +4
    -4
      src/webviews/settings/registration.ts
  34. +1
    -2
      src/webviews/settings/settingsWebview.ts
  35. +11
    -8
      src/webviews/webviewCommandRegistrar.ts
  36. +30
    -11
      src/webviews/webviewController.ts
  37. +149
    -29
      src/webviews/webviewsController.ts
  38. +2
    -5
      src/webviews/welcome/protocol.ts
  39. +1
    -1
      src/webviews/welcome/registration.ts
  40. +1
    -2
      src/webviews/welcome/welcomeWebview.ts

+ 0
- 1
src/constants.ts Wyświetl plik

@ -223,7 +223,6 @@ export const enum Commands {
RefreshFocus = 'gitlens.focus.refresh',
RefreshGraph = 'gitlens.graph.refresh',
RefreshHover = 'gitlens.refreshHover',
RefreshTimelinePage = 'gitlens.timeline.refresh',
ResetAvatarCache = 'gitlens.resetAvatarCache',
ResetAIKey = 'gitlens.resetAIKey',
ResetSuppressedWarnings = 'gitlens.resetSuppressedWarnings',

+ 25
- 13
src/container.ts Wyświetl plik

@ -27,14 +27,18 @@ import { ServerConnection } from './plus/gk/serverConnection';
import { IntegrationAuthenticationService } from './plus/integrationAuthentication';
import { SubscriptionService } from './plus/subscription/subscriptionService';
import { registerAccountWebviewView } from './plus/webviews/account/registration';
import { registerFocusWebviewPanel } from './plus/webviews/focus/registration';
import { registerFocusWebviewCommands, registerFocusWebviewPanel } from './plus/webviews/focus/registration';
import {
registerGraphWebviewCommands,
registerGraphWebviewPanel,
registerGraphWebviewView,
} from './plus/webviews/graph/registration';
import { GraphStatusBarController } from './plus/webviews/graph/statusbar';
import { registerTimelineWebviewPanel, registerTimelineWebviewView } from './plus/webviews/timeline/registration';
import {
registerTimelineWebviewCommands,
registerTimelineWebviewPanel,
registerTimelineWebviewView,
} from './plus/webviews/timeline/registration';
import { scheduleAddMissingCurrentWorkspaceRepos, WorkspacesService } from './plus/workspaces/workspacesService';
import { StatusBarController } from './statusbar/statusBarController';
import { executeCommand } from './system/command';
@ -73,7 +77,7 @@ import {
import { registerHomeWebviewView } from './webviews/home/registration';
import { RebaseEditorProvider } from './webviews/rebase/rebaseEditor';
import { registerSettingsWebviewCommands, registerSettingsWebviewPanel } from './webviews/settings/registration';
import type { WebviewPanelProxy, WebviewViewProxy } from './webviews/webviewsController';
import type { WebviewViewProxy } from './webviews/webviewsController';
import { WebviewsController } from './webviews/webviewsController';
import { registerWelcomeWebviewPanel } from './webviews/welcome/registration';
@ -220,20 +224,29 @@ export class Container {
this._disposables.push((this._codeLensController = new GitCodeLensController(this)));
this._disposables.push((this._webviews = new WebviewsController(this)));
this._disposables.push(registerTimelineWebviewPanel(this._webviews));
this._disposables.push((this._timelineView = registerTimelineWebviewView(this._webviews)));
this._disposables.push((this._graphPanel = registerGraphWebviewPanel(this._webviews)));
this._disposables.push(registerGraphWebviewCommands(this, this._graphPanel));
const graphPanels = registerGraphWebviewPanel(this._webviews);
this._disposables.push(graphPanels);
this._disposables.push(registerGraphWebviewCommands(this, graphPanels));
this._disposables.push((this._graphView = registerGraphWebviewView(this._webviews)));
this._disposables.push(new GraphStatusBarController(this));
const settingsWebviewPanel = registerSettingsWebviewPanel(this._webviews);
this._disposables.push(settingsWebviewPanel);
this._disposables.push(registerSettingsWebviewCommands(settingsWebviewPanel));
this._disposables.push(registerWelcomeWebviewPanel(this._webviews));
const focusPanels = registerFocusWebviewPanel(this._webviews);
this._disposables.push(focusPanels);
this._disposables.push(registerFocusWebviewCommands(focusPanels));
const timelinePanels = registerTimelineWebviewPanel(this._webviews);
this._disposables.push(timelinePanels);
this._disposables.push(registerTimelineWebviewCommands(timelinePanels));
this._disposables.push((this._timelineView = registerTimelineWebviewView(this._webviews)));
this._disposables.push((this._rebaseEditor = new RebaseEditorProvider(this)));
this._disposables.push(registerFocusWebviewPanel(this._webviews));
const settingsPanels = registerSettingsWebviewPanel(this._webviews);
this._disposables.push(settingsPanels);
this._disposables.push(registerSettingsWebviewCommands(settingsPanels));
this._disposables.push(registerWelcomeWebviewPanel(this._webviews));
this._disposables.push(new ViewFileDecorationProvider());
@ -491,7 +504,6 @@ export class Container {
return this._graphDetailsView;
}
private readonly _graphPanel: WebviewPanelProxy;
private readonly _graphView: WebviewViewProxy;
get graphView() {
return this._graphView;

+ 1
- 2
src/plus/webviews/account/accountWebview.ts Wyświetl plik

@ -87,8 +87,7 @@ export class AccountWebviewProvider implements WebviewProvider {
const subscriptionResult = await this.getSubscription(subscription);
return {
webviewId: this.host.id,
timestamp: Date.now(),
...this.host.baseWebviewState,
webroot: this.host.getWebRoot(),
subscription: subscriptionResult.subscription,
avatar: subscriptionResult.avatar,

+ 2
- 5
src/plus/webviews/account/protocol.ts Wyświetl plik

@ -1,11 +1,8 @@
import type { WebviewIds, WebviewViewIds } from '../../../constants';
import type { Subscription } from '../../../subscription';
import type { WebviewState } from '../../../webviews/protocol';
import { IpcNotificationType } from '../../../webviews/protocol';
export interface State {
webviewId: WebviewIds | WebviewViewIds;
timestamp: number;
export interface State extends WebviewState {
webroot?: string;
subscription: Subscription;
avatar?: string;

+ 6
- 14
src/plus/webviews/focus/focusWebview.ts Wyświetl plik

@ -24,7 +24,7 @@ import type { GitWorktree } from '../../../git/models/worktree';
import { getWorktreeForBranch } from '../../../git/models/worktree';
import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser';
import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider';
import { executeCommand, registerCommand } from '../../../system/command';
import { executeCommand } from '../../../system/command';
import { debug } from '../../../system/decorators/log';
import { Logger } from '../../../system/logger';
import { getLogScope } from '../../../system/logger.scope';
@ -115,10 +115,6 @@ export class FocusWebviewProvider implements WebviewProvider {
this._disposable.dispose();
}
registerCommands(): Disposable[] {
return [registerCommand(Commands.RefreshFocus, () => this.host.refresh(true))];
}
onMessageReceived(e: IpcMessage) {
switch (e.method) {
case OpenBranchCommandType.method:
@ -433,7 +429,7 @@ export class FocusWebviewProvider implements WebviewProvider {
@debug()
private async getState(force?: boolean, deferState?: boolean): Promise<State> {
const webviewId = this.host.id;
const baseState = this.host.baseWebviewState;
this._etag = this.container.git.etag;
if (this.container.git.isDiscoveringRepositories) {
@ -447,8 +443,7 @@ export class FocusWebviewProvider implements WebviewProvider {
const access = await this.getAccess(force);
if (access.allowed !== true) {
return {
webviewId: webviewId,
timestamp: Date.now(),
...baseState,
access: access,
};
}
@ -460,8 +455,7 @@ export class FocusWebviewProvider implements WebviewProvider {
if (!hasConnectedRepos) {
return {
webviewId: webviewId,
timestamp: Date.now(),
...baseState,
access: access,
repos: githubRepos.map(r => serializeRepoWithRichRemote(r)),
};
@ -478,8 +472,7 @@ export class FocusWebviewProvider implements WebviewProvider {
async function getStateCore() {
const [prsResult, issuesResult, enrichedItems] = await statePromise;
return {
webviewId: webviewId,
timestamp: Date.now(),
...baseState,
access: access,
repos: repos,
pullRequests: getSettledValue(prsResult)?.map(pr => ({
@ -508,8 +501,7 @@ export class FocusWebviewProvider implements WebviewProvider {
});
return {
webviewId: webviewId,
timestamp: Date.now(),
...baseState,
access: access,
repos: repos,
};

+ 2
- 5
src/plus/webviews/focus/protocol.ts Wyświetl plik

@ -1,14 +1,11 @@
import type { WebviewIds, WebviewViewIds } from '../../../constants';
import type { FeatureAccess } from '../../../features';
import type { IssueShape } from '../../../git/models/issue';
import type { PullRequestShape } from '../../../git/models/pullRequest';
import type { WebviewState } from '../../../webviews/protocol';
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
import type { EnrichedItem } from '../../focus/focusService';
export interface State {
webviewId: WebviewIds | WebviewViewIds;
timestamp: number;
export interface State extends WebviewState {
access: FeatureAccess;
pullRequests?: PullRequestResult[];
issues?: IssueResult[];

+ 12
- 3
src/plus/webviews/focus/registration.ts Wyświetl plik

@ -1,11 +1,12 @@
import { ViewColumn } from 'vscode';
import { Disposable, ViewColumn } from 'vscode';
import { Commands } from '../../../constants';
import type { WebviewsController } from '../../../webviews/webviewsController';
import { registerCommand } from '../../../system/command';
import type { WebviewPanelsProxy, WebviewsController } from '../../../webviews/webviewsController';
import type { State } from './protocol';
export function registerFocusWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(
Commands.ShowFocusPage,
{ id: Commands.ShowFocusPage },
{
id: 'gitlens.focus',
fileName: 'focus.html',
@ -26,3 +27,11 @@ export function registerFocusWebviewPanel(controller: WebviewsController) {
},
);
}
export function registerFocusWebviewCommands(panels: WebviewPanelsProxy) {
return Disposable.from(
registerCommand(`${panels.id}.refresh`, () => {
void panels.getActiveOrFirstInstance()?.refresh(true);
}),
);
}

+ 13
- 8
src/plus/webviews/graph/graphWebview.ts Wyświetl plik

@ -84,6 +84,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 type { WebviewPanelShowCommandArgs } from '../../../webviews/webviewsController';
import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type {
@ -363,13 +364,17 @@ export class GraphWebviewProvider implements WebviewProvider {
registerCommands(): Disposable[] {
return [
registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true)),
...(this.host.isView()
? [
registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true)),
registerCommand(
`${this.host.id}.openInTab`,
() => void executeCommand(Commands.ShowGraphPage, this.repository),
() =>
void executeCommand<WebviewPanelShowCommandArgs>(
Commands.ShowGraphPage,
undefined,
this.repository,
),
),
]
: []),
@ -668,7 +673,7 @@ export class GraphWebviewProvider implements WebviewProvider {
private showActiveSelectionDetailsCore() {
const { activeSelection } = this;
if (activeSelection == null) return;
if (activeSelection == null || !this.host.active) return;
this.container.events.fire(
'commit:selected',
@ -1209,6 +1214,7 @@ export class GraphWebviewProvider implements WebviewProvider {
this._selection = commits;
if (commits == null) return;
if (!this._firstSelection && !this.host.active) return;
this.container.events.fire(
'commit:selected',
@ -1899,13 +1905,13 @@ export class GraphWebviewProvider implements WebviewProvider {
private async getState(deferRows?: boolean): Promise<State> {
if (this.container.git.repositoryCount === 0) {
return { webviewId: this.host.id, timestamp: Date.now(), allowed: true, repositories: [] };
return { ...this.host.baseWebviewState, allowed: true, repositories: [] };
}
if (this.repository == null) {
this.repository = this.container.git.getBestRepositoryOrFirst();
if (this.repository == null) {
return { webviewId: this.host.id, timestamp: Date.now(), allowed: true, repositories: [] };
return { ...this.host.baseWebviewState, allowed: true, repositories: [] };
}
}
@ -1990,8 +1996,7 @@ export class GraphWebviewProvider implements WebviewProvider {
}
return {
webviewId: this.host.id,
timestamp: Date.now(),
...this.host.baseWebviewState,
windowFocused: this.isWindowFocused,
repositories: formatRepositories(this.container.git.openRepositories),
selectedRepository: this.repository.path,

+ 2
- 5
src/plus/webviews/graph/protocol.ts Wyświetl plik

@ -23,7 +23,6 @@ import type {
WorkDirStats,
} from '@gitkraken/gitkraken-components';
import type { Config, DateStyle } from '../../../config';
import type { WebviewIds, WebviewViewIds } from '../../../constants';
import type { RepositoryVisibility } from '../../../git/gitProvider';
import type { GitTrackingState } from '../../../git/models/branch';
import type { GitGraphRowType } from '../../../git/models/graph';
@ -38,6 +37,7 @@ import type { GitSearchResultData, SearchQuery } from '../../../git/search';
import type { Subscription } from '../../../subscription';
import type { DateTimeFormat } from '../../../system/date';
import type { WebviewItemContext, WebviewItemGroupContext } from '../../../system/webview';
import type { WebviewState } from '../../../webviews/protocol';
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
export type { GraphRefType } from '@gitkraken/gitkraken-components';
@ -81,10 +81,7 @@ export type GraphMinimapMarkerTypes =
export const supportedRefMetadataTypes: GraphRefMetadataType[] = ['upstream', 'pullRequest', 'issue'];
export interface State {
webviewId: WebviewIds | WebviewViewIds;
timestamp: number;
export interface State extends WebviewState {
windowFocused?: boolean;
repositories?: GraphRepository[];
selectedRepository?: string;

+ 16
- 9
src/plus/webviews/graph/registration.ts Wyświetl plik

@ -10,12 +10,16 @@ import type { CommitFileNode } from '../../../views/nodes/commitFileNode';
import type { CommitNode } from '../../../views/nodes/commitNode';
import type { StashNode } from '../../../views/nodes/stashNode';
import type { TagNode } from '../../../views/nodes/tagNode';
import type { WebviewPanelProxy, WebviewsController } from '../../../webviews/webviewsController';
import type {
WebviewPanelShowCommandArgs,
WebviewPanelsProxy,
WebviewsController,
} from '../../../webviews/webviewsController';
import type { ShowInCommitGraphCommandArgs, State } from './protocol';
export function registerGraphWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(
Commands.ShowGraphPage,
{ id: Commands.ShowGraphPage },
{
id: 'gitlens.graph',
fileName: 'graph.html',
@ -57,18 +61,18 @@ export function registerGraphWebviewView(controller: WebviewsController) {
);
}
export function registerGraphWebviewCommands(container: Container, webview: WebviewPanelProxy) {
export function registerGraphWebviewCommands(container: Container, panels: WebviewPanelsProxy) {
return Disposable.from(
registerCommand(Commands.ShowGraph, (...args: any[]) =>
registerCommand(Commands.ShowGraph, (...args: unknown[]) =>
configuration.get('graph.layout') === 'panel'
? executeCommand(Commands.ShowGraphView, ...args)
: executeCommand(Commands.ShowGraphPage, ...args),
: executeCommand<WebviewPanelShowCommandArgs>(Commands.ShowGraphPage, undefined, ...args),
),
registerCommand('gitlens.graph.switchToEditorLayout', async () => {
registerCommand(`${panels.id}.switchToEditorLayout`, async () => {
await configuration.updateEffective('graph.layout', 'editor');
queueMicrotask(() => void executeCommand(Commands.ShowGraphPage));
queueMicrotask(() => void executeCommand<WebviewPanelShowCommandArgs>(Commands.ShowGraphPage));
}),
registerCommand('gitlens.graph.switchToPanelLayout', async () => {
registerCommand(`${panels.id}.switchToPanelLayout`, async () => {
await configuration.updateEffective('graph.layout', 'panel');
queueMicrotask(async () => {
await executeCoreCommand('gitlens.views.graph.resetViewLocation');
@ -107,7 +111,7 @@ export function registerGraphWebviewCommands(container: Container, webview: Webv
if (configuration.get('graph.layout') === 'panel') {
void container.graphView.show({ preserveFocus: preserveFocus }, args);
} else {
void webview.show({ preserveFocus: preserveFocus }, args);
void panels.show({ preserveFocus: preserveFocus }, args);
}
},
),
@ -127,5 +131,8 @@ export function registerGraphWebviewCommands(container: Container, webview: Webv
void container.graphView.show({ preserveFocus: preserveFocus }, args);
},
),
registerCommand(`${panels.id}.refresh`, () => {
void panels.getActiveOrFirstInstance()?.refresh(true);
}),
);
}

+ 2
- 5
src/plus/webviews/timeline/protocol.ts Wyświetl plik

@ -1,11 +1,8 @@
import type { WebviewIds, WebviewViewIds } from '../../../constants';
import type { FeatureAccess } from '../../../features';
import type { WebviewState } from '../../../webviews/protocol';
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
export interface State {
webviewId: WebviewIds | WebviewViewIds;
timestamp: number;
export interface State extends WebviewState {
dataset?: Commit[];
period: Period;
title?: string;

+ 12
- 3
src/plus/webviews/timeline/registration.ts Wyświetl plik

@ -1,11 +1,12 @@
import { ViewColumn } from 'vscode';
import { Disposable, ViewColumn } from 'vscode';
import { Commands } from '../../../constants';
import type { WebviewsController } from '../../../webviews/webviewsController';
import { registerCommand } from '../../../system/command';
import type { WebviewPanelsProxy, WebviewsController } from '../../../webviews/webviewsController';
import type { State } from './protocol';
export function registerTimelineWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(
Commands.ShowTimelinePage,
{ id: Commands.ShowTimelinePage },
{
id: 'gitlens.timeline',
fileName: 'timeline.html',
@ -46,3 +47,11 @@ export function registerTimelineWebviewView(controller: WebviewsController) {
},
);
}
export function registerTimelineWebviewCommands(panels: WebviewPanelsProxy) {
return Disposable.from(
registerCommand(`${panels.id}.refresh`, () => {
void panels.getActiveOrFirstInstance()?.refresh(true);
}),
);
}

+ 24
- 23
src/plus/webviews/timeline/timelineWebview.ts Wyświetl plik

@ -1,5 +1,5 @@
import type { TextEditor, ViewColumn } from 'vscode';
import { commands, Disposable, Uri, window } from 'vscode';
import { Disposable, Uri, window } from 'vscode';
import { Commands } from '../../../constants';
import type { Container } from '../../../container';
import type { CommitSelectedEvent, FileSelectedEvent } from '../../../eventBus';
@ -9,7 +9,7 @@ import { GitUri } from '../../../git/gitUri';
import { getChangedFilesCount } from '../../../git/models/commit';
import type { RepositoryChangeEvent } from '../../../git/models/repository';
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
import { registerCommand } from '../../../system/command';
import { executeCommand, registerCommand } from '../../../system/command';
import { configuration } from '../../../system/configuration';
import { createFromDateDelta } from '../../../system/date';
import { debug } from '../../../system/decorators/log';
@ -23,6 +23,7 @@ import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import { updatePendingContext } from '../../../webviews/webviewController';
import type { WebviewPanelShowCommandArgs } from '../../../webviews/webviewsController';
import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type { Commit, Period, State } from './protocol';
@ -127,14 +128,24 @@ export class TimelineWebviewProvider implements WebviewProvider {
}
registerCommands(): Disposable[] {
if (this.host.isEditor()) {
return [registerCommand(Commands.RefreshTimelinePage, () => this.host.refresh(true))];
}
return [
registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true), this),
registerCommand(`${this.host.id}.openInTab`, () => this.openInTab(), this),
];
return this.host.isView()
? [
registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true), this),
registerCommand(
`${this.host.id}.openInTab`,
() => {
if (this._context.uri == null) return;
void executeCommand<WebviewPanelShowCommandArgs>(
Commands.ShowTimelinePage,
{ _type: 'WebviewPanelShowOptions' },
this._context.uri,
);
},
this,
),
]
: [];
}
onVisibilityChanged(visible: boolean) {
@ -280,8 +291,7 @@ export class TimelineWebviewProvider implements WebviewProvider {
if (current.uri == null || gitUri == null || repoPath == null || access.allowed === false) {
const access = await this.container.git.access(PlusFeatures.Timeline, repoPath);
return {
webviewId: this.host.id,
timestamp: Date.now(),
...this.host.baseWebviewState,
period: period,
title: gitUri?.relativePath,
sha: gitUri?.shortSha,
@ -303,8 +313,7 @@ export class TimelineWebviewProvider implements WebviewProvider {
if (log == null) {
return {
webviewId: this.host.id,
timestamp: Date.now(),
...this.host.baseWebviewState,
dataset: [],
period: period,
title: gitUri.relativePath,
@ -362,8 +371,7 @@ export class TimelineWebviewProvider implements WebviewProvider {
dataset.sort((a, b) => b.sort - a.sort);
return {
webviewId: this.host.id,
timestamp: Date.now(),
...this.host.baseWebviewState,
dataset: dataset,
period: period,
title: gitUri.relativePath,
@ -441,13 +449,6 @@ export class TimelineWebviewProvider implements WebviewProvider {
if (!this.host.isView()) return task();
return window.withProgress({ location: { viewId: this.host.id } }, task);
}
private openInTab() {
const uri = this._context.uri;
if (uri == null) return;
void commands.executeCommand(Commands.ShowTimelinePage, uri);
}
}
function getPeriodDate(period: Period): Date | undefined {

+ 5
- 1
src/system/webview.ts Wyświetl plik

@ -3,12 +3,16 @@ import type { WebviewIds, WebviewViewIds } from '../constants';
export function createWebviewCommandLink(
command: `${WebviewIds | WebviewViewIds}.${string}`,
webviewId: WebviewIds | WebviewViewIds,
webviewInstanceId: string | undefined,
): string {
return `command:${command}?${encodeURIComponent(JSON.stringify({ webview: webviewId } satisfies WebviewContext))}`;
return `command:${command}?${encodeURIComponent(
JSON.stringify({ webview: webviewId, webviewInstance: webviewInstanceId } satisfies WebviewContext),
)}`;
}
export interface WebviewContext {
webview: WebviewIds | WebviewViewIds;
webviewInstance: string | undefined;
}
export function isWebviewContext(item: object | null | undefined): item is WebviewContext {

+ 1
- 1
src/webviews/apps/commitDetails/commitDetails.html Wyświetl plik

@ -19,7 +19,7 @@
<body
class="preload"
data-placement="#{placement}"
data-vscode-context='{ "preventDefaultContextMenuItems": true, "webview": "#{webviewId}" }'
data-vscode-context='{ "preventDefaultContextMenuItems": true, "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<gl-commit-details-app id="app"></gl-commit-details-app>
#{endOfBody}

+ 5
- 1
src/webviews/apps/home/home.html Wyświetl plik

@ -16,7 +16,11 @@
</style>
</head>
<body class="home preload" data-placement="#{placement}" data-vscode-context='{ "webview": "#{webviewId}" }'>
<body
class="home preload"
data-placement="#{placement}"
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<div class="home__nav">
<nav class="inline-nav" id="links" aria-label="Help and Resources">
<div class="inline-nav__group">

+ 1
- 1
src/webviews/apps/plus/account/account.html Wyświetl plik

@ -14,7 +14,7 @@
<body
class="account scrollable preload"
data-placement="#{placement}"
data-vscode-context='{ "webview": "#{webviewId}" }'
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<account-content id="account-content"></account-content>

+ 5
- 1
src/webviews/apps/plus/focus/focus.html Wyświetl plik

@ -20,7 +20,11 @@
</style>
</head>
<body class="preload" data-placement="#{placement}" data-vscode-context='{ "webview": "#{webviewId}" }'>
<body
class="preload"
data-placement="#{placement}"
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<gl-focus-app id="app"></gl-focus-app>
#{endOfBody}
</body>

+ 11
- 3
src/webviews/apps/plus/graph/GraphWrapper.tsx Wyświetl plik

@ -1020,7 +1020,11 @@ export function GraphWrapper({
<div className="titlebar__group">
{(isBehind || isAhead) && (
<a
href={createWebviewCommandLink(`gitlens.graph.${action}`, state.webviewId)}
href={createWebviewCommandLink(
`gitlens.graph.${action}`,
state.webviewId,
state.webviewInstanceId,
)}
className={`action-button${isBehind ? ' is-behind' : ''}${isAhead ? ' is-ahead' : ''}`}
title={tooltip}
>
@ -1045,7 +1049,7 @@ export function GraphWrapper({
</a>
)}
<a
href={createWebviewCommandLink('gitlens.graph.fetch', state.webviewId)}
href={createWebviewCommandLink('gitlens.graph.fetch', state.webviewId, state.webviewInstanceId)}
className="action-button"
title={fetchTooltip}
>
@ -1106,7 +1110,11 @@ export function GraphWrapper({
<span className="codicon codicon-chevron-right"></span>
</span>
<a
href={createWebviewCommandLink('gitlens.graph.switchToAnotherBranch', state.webviewId)}
href={createWebviewCommandLink(
'gitlens.graph.switchToAnotherBranch',
state.webviewId,
state.webviewInstanceId,
)}
className="action-button"
title="Switch to Another Branch..."
aria-label="Switch to Another Branch..."

+ 1
- 1
src/webviews/apps/plus/graph/graph.html Wyświetl plik

@ -22,7 +22,7 @@
<body
class="graph-app scrollable"
data-placement="#{placement}"
data-vscode-context='{ "preventDefaultContextMenuItems": true, "webview": "#{webviewId}" }'
data-vscode-context='{ "preventDefaultContextMenuItems": true, "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<div id="root" class="graph-app__container"></div>
#{endOfBody}

+ 5
- 1
src/webviews/apps/plus/timeline/timeline.html Wyświetl plik

@ -16,7 +16,11 @@
</style>
</head>
<body class="preload" data-placement="#{placement}" data-vscode-context='{ "webview": "#{webviewId}" }'>
<body
class="preload"
data-placement="#{placement}"
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<gk-feature-gate id="subscription-gate" class="scrollable"
><p slot="feature">
Visualize the evolution of a file, including when changes were made, how large they were, and who made

+ 5
- 1
src/webviews/apps/rebase/rebase.html Wyświetl plik

@ -11,7 +11,11 @@
</style>
</head>
<body class="scrollable preload" data-placement="#{placement}" data-vscode-context='{ "webview": "#{webviewId}" }'>
<body
class="scrollable preload"
data-placement="#{placement}"
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<div class="container">
<header>
<h2>GitLens Interactive Rebase</h2>

+ 5
- 1
src/webviews/apps/settings/settings.html Wyświetl plik

@ -11,7 +11,11 @@
</style>
</head>
<body class="scrollable preload" data-placement="#{placement}" data-vscode-context='{ "webview": "#{webviewId}" }'>
<body
class="scrollable preload"
data-placement="#{placement}"
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<!-- <canvas class="snow"></canvas>
<img
class="snow__trigger snow__trigger--fixed-right snow__trigger--flipped"

+ 1
- 1
src/webviews/apps/welcome/welcome.html Wyświetl plik

@ -19,7 +19,7 @@
<body
class="welcome scrollable preload"
data-placement="#{placement}"
data-vscode-context='{ "webview": "#{webviewId}" }'
data-vscode-context='{ "webview": "#{webviewId}", "webviewInstance": "#{webviewInstanceId}" }'
>
<!-- <canvas class="snow"></canvas>
<img

+ 1
- 2
src/webviews/commitDetails/commitDetailsWebview.ts Wyświetl plik

@ -553,9 +553,8 @@ export class CommitDetailsWebviewProvider implements WebviewProvider
}
const state = serialize<State>({
...this.host.baseWebviewState,
mode: current.mode,
webviewId: this.host.id,
timestamp: Date.now(),
commit: details,
navigationStack: current.navigationStack,
pinned: current.pinned,

+ 2
- 4
src/webviews/commitDetails/protocol.ts Wyświetl plik

@ -1,13 +1,13 @@
import type { TextDocumentShowOptions } from 'vscode';
import type { Autolink } from '../../annotations/autolinks';
import type { Config } from '../../config';
import type { WebviewIds, WebviewViewIds } from '../../constants';
import type { GitCommitIdentityShape, GitCommitStats } from '../../git/models/commit';
import type { GitFileChangeShape } from '../../git/models/file';
import type { IssueOrPullRequest } from '../../git/models/issue';
import type { PullRequestShape } from '../../git/models/pullRequest';
import type { DateTimeFormat } from '../../system/date';
import type { Serialized } from '../../system/serialize';
import type { WebviewState } from '../protocol';
import { IpcCommandType, IpcNotificationType } from '../protocol';
export const messageHeadlineSplitterToken = '\x00\n\x00';
@ -55,9 +55,7 @@ export interface Wip {
repositoryCount: number;
}
export interface State {
webviewId: WebviewIds | WebviewViewIds;
timestamp: number;
export interface State extends WebviewState {
mode: Mode;
pinned: boolean;

+ 1
- 2
src/webviews/home/homeWebview.ts Wyświetl plik

@ -48,8 +48,7 @@ export class HomeWebviewProvider implements WebviewProvider {
private getState(): State {
return {
webviewId: this.host.id,
timestamp: Date.now(),
...this.host.baseWebviewState,
repositories: this.getRepositoriesState(),
webroot: this.host.getWebRoot(),
};

+ 2
- 5
src/webviews/home/protocol.ts Wyświetl plik

@ -1,10 +1,7 @@
import type { WebviewIds, WebviewViewIds } from '../../constants';
import type { WebviewState } from '../protocol';
import { IpcNotificationType } from '../protocol';
export interface State {
webviewId: WebviewIds | WebviewViewIds;
timestamp: number;
export interface State extends WebviewState {
repositories: DidChangeRepositoriesParams;
webroot?: string;
}

+ 7
- 0
src/webviews/protocol.ts Wyświetl plik

@ -1,4 +1,5 @@
import type { Config } from '../config';
import type { CustomEditorIds, WebviewIds, WebviewViewIds } from '../constants';
import type { ConfigPath, ConfigPathValue, Path, PathValue } from '../system/configuration';
export interface IpcMessage {
@ -123,3 +124,9 @@ export function assertsConfigKeyValue(
): asserts value is ConfigPathValue<T> {
// Noop
}
export interface WebviewState<Id extends WebviewIds | WebviewViewIds | CustomEditorIds = WebviewIds | WebviewViewIds> {
webviewId: Id;
webviewInstanceId: string | undefined;
timestamp: number;
}

+ 2
- 4
src/webviews/rebase/protocol.ts Wyświetl plik

@ -1,10 +1,8 @@
import type { CustomEditorIds } from '../../constants';
import type { WebviewState } from '../protocol';
import { IpcCommandType, IpcNotificationType } from '../protocol';
export interface State {
webviewId: CustomEditorIds;
timestamp: number;
export interface State extends WebviewState<CustomEditorIds> {
branch: string;
onto: { sha: string; commit?: Commit } | undefined;

+ 2
- 0
src/webviews/rebase/rebaseEditor.ts Wyświetl plik

@ -599,6 +599,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
const html = replaceWebviewHtmlTokens(
utf8TextDecoder.decode(bytes),
'gitlens.rebase',
undefined,
context.panel.webview.cspSource,
getNonce(),
context.panel.webview.asWebviewUri(this.container.context.extensionUri).toString(),
@ -693,6 +694,7 @@ async function parseRebaseTodo(
return {
webviewId: 'gitlens.rebase',
webviewInstanceId: undefined,
timestamp: Date.now(),
branch: context.branchName ?? '',
onto: onto

+ 2
- 5
src/webviews/settings/protocol.ts Wyświetl plik

@ -1,10 +1,7 @@
import type { Config } from '../../config';
import type { WebviewIds, WebviewViewIds } from '../../constants';
export interface State {
webviewId: WebviewIds | WebviewViewIds;
timestamp: number;
import type { WebviewState } from '../protocol';
export interface State extends WebviewState {
version: string;
config: Config;
customSettings?: Record<string, boolean>;

+ 4
- 4
src/webviews/settings/registration.ts Wyświetl plik

@ -1,12 +1,12 @@
import { Disposable, ViewColumn } from 'vscode';
import { Commands } from '../../constants';
import { registerCommand } from '../../system/command';
import type { WebviewPanelProxy, WebviewsController } from '../webviewsController';
import type { WebviewPanelsProxy, WebviewsController } from '../webviewsController';
import type { State } from './protocol';
export function registerSettingsWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(
Commands.ShowSettingsPage,
{ id: Commands.ShowSettingsPage },
{
id: 'gitlens.settings',
fileName: 'settings.html',
@ -28,7 +28,7 @@ export function registerSettingsWebviewPanel(controller: WebviewsController) {
);
}
export function registerSettingsWebviewCommands(webview: WebviewPanelProxy) {
export function registerSettingsWebviewCommands(panels: WebviewPanelsProxy) {
return Disposable.from(
...[
Commands.ShowSettingsPageAndJumpToBranchesView,
@ -53,7 +53,7 @@ export function registerSettingsWebviewCommands(webview: WebviewPanelProxy) {
[, anchor] = match;
}
return registerCommand(c, (...args: any[]) => void webview.show(undefined, anchor, ...args));
return registerCommand(c, (...args: any[]) => void panels.show(undefined, anchor, ...args));
}),
);
}

+ 1
- 2
src/webviews/settings/settingsWebview.ts Wyświetl plik

@ -47,8 +47,7 @@ export class SettingsWebviewProvider implements WebviewProvider {
}
return {
webviewId: this.host.id,
timestamp: Date.now(),
...this.host.baseWebviewState,
version: this.container.version,
// Make sure to get the raw config, not from the container which has the modes mixed in
config: configuration.getAll(true),

+ 11
- 8
src/webviews/webviewCommandRegistrar.ts Wyświetl plik

@ -19,6 +19,7 @@ export class WebviewCommandRegistrar implements Disposable {
registerCommand<T extends WebviewProvider<any>>(
provider: T,
id: string,
instanceId: string | undefined,
command: string,
callback: CommandCallback,
) {
@ -35,11 +36,11 @@ export class WebviewCommandRegistrar implements Disposable {
return;
}
const handler = handlers.get(item.webview);
const key = item.webviewInstance ? `${item.webview}:${item.webviewInstance}` : item.webview;
const handler = handlers.get(key);
if (handler == null) {
throw new Error(
`Unable to find Command '${command}' registration for Webview '${item.webview}'`,
);
throw new Error(`Unable to find Command '${command}' registration for Webview '${key}'`);
}
handler.callback.call(handler.thisArg, item);
@ -51,15 +52,17 @@ export class WebviewCommandRegistrar implements Disposable {
this._commandRegistrations.set(command, registration);
}
if (registration.handlers.has(id)) {
throw new Error(`Command '${command}' has already been registered for Webview '${id}'`);
const key = instanceId ? `${id}:${instanceId}` : id;
if (registration.handlers.has(key)) {
throw new Error(`Command '${command}' has already been registered for Webview '${key}'`);
}
registration.handlers.set(id, { callback: callback, thisArg: provider });
registration.handlers.set(key, { callback: callback, thisArg: provider });
return {
dispose: () => {
registration!.handlers.delete(id);
registration!.handlers.delete(key);
if (registration!.handlers.size === 0) {
this._commandRegistrations.delete(command);
registration!.subscription.dispose();

+ 30
- 11
src/webviews/webviewController.ts Wyświetl plik

@ -9,10 +9,16 @@ import { debug, logName } from '../system/decorators/log';
import { serialize } from '../system/decorators/serialize';
import { isPromise } from '../system/promise';
import type { WebviewContext } from '../system/webview';
import type { IpcMessage, IpcMessageParams, IpcNotificationType, WebviewFocusChangedParams } from './protocol';
import type {
IpcMessage,
IpcMessageParams,
IpcNotificationType,
WebviewFocusChangedParams,
WebviewState,
} from './protocol';
import { ExecuteCommandType, onIpc, WebviewFocusChangedCommandType, WebviewReadyCommandType } from './protocol';
import type { WebviewCommandCallback, WebviewCommandRegistrar } from './webviewCommandRegistrar';
import type { WebviewPanelDescriptor, WebviewViewDescriptor } from './webviewsController';
import type { WebviewPanelDescriptor, WebviewShowOptions, WebviewViewDescriptor } from './webviewsController';
const maxSmallIntegerV8 = 2 ** 30; // Max number that can be stored in V8's smis (small integers)
const utf8TextDecoder = new TextDecoder('utf8');
@ -35,11 +41,7 @@ type GetParentType = T
: never;
export interface WebviewProvider<State, SerializedState = State> extends Disposable {
onShowing?(
loading: boolean,
options: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
): boolean | Promise<boolean>;
onShowing?(loading: boolean, options: WebviewShowOptions, ...args: unknown[]): boolean | Promise<boolean>;
registerCommands?(): Disposable[];
includeBootstrap?(): SerializedState | Promise<SerializedState>;
@ -79,6 +81,7 @@ export class WebviewController<
container: Container,
commandRegistrar: WebviewCommandRegistrar,
descriptor: WebviewPanelDescriptor,
instanceId: string | undefined,
parent: WebviewPanel,
resolveProvider: (
container: Container,
@ -89,6 +92,7 @@ export class WebviewController<
container: Container,
commandRegistrar: WebviewCommandRegistrar,
descriptor: WebviewViewDescriptor,
instanceId: string | undefined,
parent: WebviewView,
resolveProvider: (
container: Container,
@ -99,6 +103,7 @@ export class WebviewController<
container: Container,
commandRegistrar: WebviewCommandRegistrar,
descriptor: WebviewPanelDescriptor | WebviewViewDescriptor,
instanceId: string | undefined,
parent: WebviewPanel | WebviewView,
resolveProvider: (
container: Container,
@ -109,6 +114,7 @@ export class WebviewController<
container,
commandRegistrar,
descriptor,
instanceId,
parent,
resolveProvider,
);
@ -136,6 +142,7 @@ export class WebviewController<
private readonly container: Container,
private readonly _commandRegistrar: WebviewCommandRegistrar,
private readonly descriptor: Descriptor,
public readonly instanceId: string | undefined,
public readonly parent: GetParentType<Descriptor>,
resolveProvider: (
container: Container,
@ -187,7 +194,7 @@ export class WebviewController<
}
registerWebviewCommand<T extends Partial<WebviewContext>>(command: string, callback: WebviewCommandCallback<T>) {
return this._commandRegistrar.registerCommand(this.provider, this.id, command, callback);
return this._commandRegistrar.registerCommand(this.provider, this.id, this.instanceId, command, callback);
}
private _initializing: Promise<void> | undefined;
@ -255,7 +262,7 @@ export class WebviewController<
}
@debug({ args: false })
async show(loading: boolean, options?: { column?: ViewColumn; preserveFocus?: boolean }, ...args: unknown[]) {
async show(loading: boolean, options?: WebviewShowOptions, ...args: unknown[]) {
if (options == null) {
options = {};
}
@ -290,6 +297,14 @@ export class WebviewController<
setContextKeys(this.descriptor.contextKeyPrefix, this.active);
}
get baseWebviewState(): WebviewState {
return {
webviewId: this.id,
webviewInstanceId: this.instanceId,
timestamp: Date.now(),
};
}
private readonly _cspNonce = getNonce();
get cspNonce(): string {
return this._cspNonce;
@ -458,7 +473,8 @@ export class WebviewController<
const html = replaceWebviewHtmlTokens(
utf8TextDecoder.decode(bytes),
this.descriptor.id,
this.id,
this.instanceId,
webview.cspSource,
this._cspNonce,
this.asWebviewUri(this.getRootUri()).toString(),
@ -560,6 +576,7 @@ export class WebviewController<
export function replaceWebviewHtmlTokens<SerializedState>(
html: string,
webviewId: string,
webviewInstanceId: string | undefined,
cspSource: string,
cspNonce: string,
root: string,
@ -571,7 +588,7 @@ export function replaceWebviewHtmlTokens(
endOfBody?: string,
) {
return html.replace(
/#{(head|body|endOfBody|webviewId|placement|cspSource|cspNonce|root|webroot)}/g,
/#{(head|body|endOfBody|webviewId|webviewInstanceId|placement|cspSource|cspNonce|root|webroot)}/g,
(_substring: string, token: string) => {
switch (token) {
case 'head':
@ -588,6 +605,8 @@ export function replaceWebviewHtmlTokens(
}${endOfBody ?? ''}`;
case 'webviewId':
return webviewId;
case 'webviewInstanceId':
return webviewInstanceId ?? '';
case 'placement':
return placement;
case 'cspSource':

+ 149
- 29
src/webviews/webviewsController.ts Wyświetl plik

@ -7,11 +7,13 @@ import type {
WebviewViewResolveContext,
} from 'vscode';
import { Disposable, Uri, ViewColumn, window } from 'vscode';
import { uuid } from '@env/crypto';
import type { Commands, WebviewIds, WebviewTypes, WebviewViewIds, WebviewViewTypes } from '../constants';
import type { Container } from '../container';
import { ensurePlusFeaturesEnabled } from '../plus/subscription/utils';
import { executeCoreCommand, registerCommand } from '../system/command';
import { debug } from '../system/decorators/log';
import { find, first, map } from '../system/iterable';
import { Logger } from '../system/logger';
import { getLogScope } from '../system/logger.scope';
import type { TrackedUsageFeatures } from '../telemetry/usageTracker';
@ -30,20 +32,34 @@ export interface WebviewPanelDescriptor {
readonly column?: ViewColumn;
readonly webviewOptions?: WebviewOptions;
readonly webviewHostOptions?: WebviewPanelOptions;
readonly allowMultipleInstances?: boolean;
}
interface WebviewPanelRegistration<State, SerializedState = State> {
readonly descriptor: WebviewPanelDescriptor;
controller?: WebviewController<State, SerializedState, WebviewPanelDescriptor> | undefined;
controllers?:
| Map<string | undefined, WebviewController<State, SerializedState, WebviewPanelDescriptor>>
| undefined;
}
export interface WebviewPanelProxy extends Disposable {
readonly id: WebviewIds;
readonly instanceId: string | undefined;
readonly ready: boolean;
readonly active: boolean;
readonly visible: boolean;
close(): void;
refresh(force?: boolean): Promise<void>;
show(options?: { column?: ViewColumn; preserveFocus?: boolean }, ...args: unknown[]): Promise<void>;
show(options?: WebviewPanelShowOptions, ...args: unknown[]): Promise<void>;
}
export interface WebviewPanelsProxy extends Disposable {
readonly id: WebviewIds;
readonly instances: Iterable<WebviewPanelProxy>;
getActiveInstance(): WebviewPanelProxy | undefined;
getActiveOrFirstInstance(): WebviewPanelProxy | undefined;
show(options?: WebviewPanelsShowOptions, ...args: unknown[]): Promise<void>;
}
export interface WebviewViewDescriptor {
@ -70,7 +86,7 @@ export interface WebviewViewProxy extends Disposable {
readonly ready: boolean;
readonly visible: boolean;
refresh(force?: boolean): Promise<void>;
show(options?: { preserveFocus?: boolean }, ...args: unknown[]): Promise<void>;
show(options?: WebviewViewShowOptions, ...args: unknown[]): Promise<void>;
}
export class WebviewsController implements Disposable {
@ -141,6 +157,7 @@ export class WebviewsController implements Disposable {
this.container,
this._commandRegistrar,
descriptor,
undefined,
webviewView,
resolveProvider,
);
@ -188,15 +205,18 @@ export class WebviewsController implements Disposable {
dispose: function () {
disposable.dispose();
},
refresh: async force => registration.controller?.refresh(force),
// eslint-disable-next-line @typescript-eslint/require-await
show: async (options?: { preserveFocus?: boolean }, ...args) => {
if (registration.controller != null) return void registration.controller.show(false, options, ...args);
refresh: function (force?: boolean) {
return registration.controller != null ? registration.controller.refresh(force) : Promise.resolve();
},
show: function (options?: WebviewViewShowOptions, ...args: unknown[]) {
if (registration.controller != null) {
return registration.controller.show(false, options, ...args);
}
Logger.debug(scope, `Showing webview view (${descriptor.id})`);
registration.pendingShowArgs = [options, ...args];
return void executeCoreCommand(`${descriptor.id}.focus`, options);
return Promise.resolve(void executeCoreCommand(`${descriptor.id}.focus`, options));
},
} satisfies WebviewViewProxy;
}
@ -210,14 +230,17 @@ export class WebviewsController implements Disposable {
},
})
registerWebviewPanel<State, SerializedState = State>(
command: Commands,
command: {
id: Commands;
options?: WebviewPanelsShowOptions;
},
descriptor: WebviewPanelDescriptor,
resolveProvider: (
container: Container,
controller: WebviewController<State, SerializedState>,
) => Promise<WebviewProvider<State, SerializedState>>,
canResolveProvider?: () => boolean | Promise<boolean>,
): WebviewPanelProxy {
): WebviewPanelsProxy {
const scope = getLogScope();
const registration: WebviewPanelRegistration<State, SerializedState> = { descriptor: descriptor };
@ -228,10 +251,7 @@ export class WebviewsController implements Disposable {
let serializedPanel: WebviewPanel | undefined;
async function show(
options?: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
): Promise<void> {
async function show(options?: WebviewPanelsShowOptions, ...args: unknown[]): Promise<void> {
if (canResolveProvider != null) {
if ((await canResolveProvider()) === false) return;
}
@ -249,9 +269,19 @@ export class WebviewsController implements Disposable {
column = ViewColumn.Active;
}
let { controller } = registration;
let preserveInstance: string | boolean;
// eslint-disable-next-line prefer-const
({ preserveInstance, ...options } = { preserveInstance: true, ...options });
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 (controller == null) {
let panel;
let panel: WebviewPanel;
if (serializedPanel != null) {
Logger.debug(scope, `Restoring webview panel (${descriptor.id})`);
@ -282,23 +312,26 @@ export class WebviewsController implements Disposable {
container,
commandRegistrar,
descriptor,
descriptor.allowMultipleInstances ? uuid() : undefined,
panel,
resolveProvider,
);
registration.controller = controller;
registration.controllers ??= new Map();
registration.controllers.set(controller.instanceId, controller);
disposables.push(
controller.onDidDispose(() => {
Logger.debug(scope, `Disposing webview panel (${descriptor.id})`);
registration.controller = undefined;
registration.controllers?.delete(controller!.instanceId);
}),
controller,
);
await controller.show(true, options, ...args);
} else {
Logger.debug(scope, `Showing webview panel (${descriptor.id})`);
Logger.debug(scope, `Showing webview panel (${descriptor.id}, ${controller.instanceId}})`);
await controller.show(false, options, ...args);
}
}
@ -309,9 +342,12 @@ 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;
if (state != null) {
await show({ column: panel.viewColumn, preserveFocus: true }, { state: state });
await show(
{ column: panel.viewColumn, preserveFocus: true, preserveInstance: false },
{ state: state },
);
} else {
await show({ column: panel.viewColumn, preserveFocus: true });
await show({ column: panel.viewColumn, preserveFocus: true, preserveInstance: false });
}
}
@ -320,27 +356,111 @@ export class WebviewsController implements Disposable {
window.registerWebviewPanelSerializer(descriptor.id, {
deserializeWebviewPanel: deserializeWebviewPanel,
}),
registerCommand(command, (...args) => show(undefined, ...args), this),
registerCommand(
command.id,
(...args: unknown[]) => {
if (hasWebviewPanelShowOptions(args)) {
const [{ _type, ...opts }, ...rest] = args;
return show({ ...command.options, ...opts }, ...rest);
}
return show({ ...command.options }, ...args);
},
this,
),
);
this.disposables.push(disposable);
return {
id: descriptor.id,
get ready() {
return registration.controller?.ready ?? false;
get instances() {
if (!registration.controllers?.size) return [];
return map(registration.controllers.values(), c => convertToWebviewPanelProxy(c));
},
get visible() {
return registration.controller?.visible ?? false;
getActiveInstance: function () {
if (!registration.controllers?.size) return undefined;
const controller = find(registration.controllers.values(), c => c.active ?? false);
return controller != null ? convertToWebviewPanelProxy(controller) : undefined;
},
getActiveOrFirstInstance: function () {
const controller = getActiveOrFirstController(registration.controllers);
return controller != null ? convertToWebviewPanelProxy(controller) : undefined;
},
dispose: function () {
disposable.dispose();
},
close: () => void registration.controller?.parent.dispose(),
refresh: async force => registration.controller?.refresh(force),
show: show,
} satisfies WebviewPanelProxy;
} satisfies WebviewPanelsProxy;
}
}
interface WebviewPanelShowOptions {
column?: ViewColumn;
preserveFocus?: boolean;
}
interface WebviewPanelsShowOptions extends WebviewPanelShowOptions {
preserveInstance?: string | boolean;
}
export type WebviewPanelShowCommandArgs = [
WebviewPanelsShowOptions & { _type: 'WebviewPanelShowOptions' },
...args: unknown[],
];
interface WebviewViewShowOptions {
column?: never;
preserveFocus?: boolean;
}
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>(
controller: WebviewController<State, SerializedState, WebviewPanelDescriptor>,
): WebviewPanelProxy {
return {
id: controller.id,
instanceId: controller.instanceId,
ready: controller.ready,
active: controller.active ?? false,
visible: controller.visible,
close: function () {
controller.parent.dispose();
},
dispose: function () {
controller.dispose();
},
refresh: function (force?: boolean) {
return controller.refresh(force);
},
show: function (options?: WebviewPanelShowOptions, ...args: unknown[]) {
return controller.show(false, options, ...args);
},
};
}
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';
}
function hasWebviewPanelShowOptions(args: unknown[]): args is WebviewPanelShowCommandArgs {
const [arg] = args;
return arg != null && typeof arg === 'object' && '_type' in arg && arg._type === 'WebviewPanelShowOptions';
}

+ 2
- 5
src/webviews/welcome/protocol.ts Wyświetl plik

@ -1,11 +1,8 @@
import type { Config } from '../../config';
import type { WebviewIds, WebviewViewIds } from '../../constants';
import type { WebviewState } from '../protocol';
import { IpcCommandType, IpcNotificationType } from '../protocol';
export interface State {
webviewId: WebviewIds | WebviewViewIds;
timestamp: number;
export interface State extends WebviewState {
version: string;
config: {
codeLens: Config['codeLens']['enabled'];

+ 1
- 1
src/webviews/welcome/registration.ts Wyświetl plik

@ -5,7 +5,7 @@ import type { State } from './protocol';
export function registerWelcomeWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(
Commands.ShowWelcomePage,
{ id: Commands.ShowWelcomePage },
{
id: 'gitlens.welcome',
fileName: 'welcome.html',

+ 1
- 2
src/webviews/welcome/welcomeWebview.ts Wyświetl plik

@ -65,8 +65,7 @@ export class WelcomeWebviewProvider implements WebviewProvider {
}
private async getState(subscription?: Subscription): Promise<State> {
return {
webviewId: this.host.id,
timestamp: Date.now(),
...this.host.baseWebviewState,
version: this.container.version,
// Make sure to get the raw config so to avoid having the mode mixed in
config: {

Ładowanie…
Anuluj
Zapisz