Переглянути джерело

Adds support for multi-instance webview panels

- Prepares Graph, Focus, & Timeline for multi-instance support
main
Eric Amodio 1 рік тому
джерело
коміт
16692734bf
40 змінених файлів з 380 додано та 193 видалено
  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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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 Переглянути файл

@ -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: {

Завантаження…
Відмінити
Зберегти