Ver a proveniência

Adds support for multi-instance webview panels

- Prepares Graph, Focus, & Timeline for multi-instance support
main
Eric Amodio há 1 ano
ascendente
cometimento
16692734bf
40 ficheiros alterados com 380 adições e 193 eliminações
  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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

@ -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 Ver ficheiro

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

Carregando…
Cancelar
Guardar