Browse Source

Adopts new webview model for the Graph

main
Eric Amodio 1 year ago
parent
commit
a21da0838b
8 changed files with 313 additions and 259 deletions
  1. +9
    -0
      package.json
  2. +16
    -25
      src/container.ts
  3. +12
    -0
      src/git/models/reference.ts
  4. +151
    -233
      src/plus/webviews/graph/graphWebview.ts
  5. +61
    -0
      src/plus/webviews/graph/registration.ts
  6. +62
    -0
      src/plus/webviews/graph/statusbar.ts
  7. +1
    -0
      src/telemetry/usageTracker.ts
  8. +1
    -1
      src/webviews/webviewsController.ts

+ 9
- 0
package.json View File

@ -13068,6 +13068,15 @@
"gitlensPanel": [
{
"type": "webview",
"id": "gitlens.views.graph",
"name": "Commit Graph",
"when": "!gitlens:disabled && gitlens:plus:enabled",
"contextualTitle": "GitLens",
"icon": "$(gitlens-graph)",
"visibility": "visible"
},
{
"type": "webview",
"id": "gitlens.views.timeline",
"name": "Visual File History",
"when": "!gitlens:disabled && gitlens:plus:enabled",

+ 16
- 25
src/container.ts View File

@ -23,7 +23,8 @@ import { SubscriptionAuthenticationProvider } from './plus/subscription/authenti
import { ServerConnection } from './plus/subscription/serverConnection';
import { SubscriptionService } from './plus/subscription/subscriptionService';
import { FocusWebview } from './plus/webviews/focus/focusWebview';
import { GraphWebview } from './plus/webviews/graph/graphWebview';
import { registerGraphWebviewCommands, registerGraphWebviewPanel } from './plus/webviews/graph/registration';
import { GraphStatusBarController } from './plus/webviews/graph/statusbar';
import { registerTimelineWebviewPanel, registerTimelineWebviewView } from './plus/webviews/timeline/registration';
import { StatusBarController } from './statusbar/statusBarController';
import type { Storage } from './storage';
@ -211,11 +212,16 @@ export class Container {
context.subscriptions.splice(0, 0, registerTimelineWebviewPanel(this._webviews));
context.subscriptions.splice(0, 0, (this._timelineView = registerTimelineWebviewView(this._webviews)));
context.subscriptions.splice(0, 0, (this._settingsWebview = new SettingsWebview(this)));
context.subscriptions.splice(0, 0, (this._welcomeWebview = new WelcomeWebview(this)));
const graphWebviewPanel = registerGraphWebviewPanel(this._webviews);
context.subscriptions.splice(0, 0, graphWebviewPanel);
context.subscriptions.splice(0, 0, registerGraphWebviewCommands(graphWebviewPanel));
// context.subscriptions.splice(0, 0, (this._graphView = registerGraphWebviewView(this._webviews)));
context.subscriptions.splice(0, 0, new GraphStatusBarController(this));
context.subscriptions.splice(0, 0, new SettingsWebview(this));
context.subscriptions.splice(0, 0, new WelcomeWebview(this));
context.subscriptions.splice(0, 0, (this._rebaseEditor = new RebaseEditorProvider(this)));
context.subscriptions.splice(0, 0, (this._graphWebview = new GraphWebview(this)));
context.subscriptions.splice(0, 0, (this._focusWebview = new FocusWebview(this)));
context.subscriptions.splice(0, 0, new FocusWebview(this));
context.subscriptions.splice(0, 0, new ViewFileDecorationProvider());
@ -451,6 +457,11 @@ export class Container {
}
}
// private _graphView: WebviewViewProxy;
// get graphView() {
// return this._graphView;
// }
private _homeView: HomeWebviewView | undefined;
get homeView() {
if (this._homeView == null) {
@ -566,21 +577,6 @@ export class Container {
return this._subscriptionAuthentication;
}
private _settingsWebview: SettingsWebview;
get settingsWebview() {
return this._settingsWebview;
}
private _graphWebview: GraphWebview;
get graphWebview() {
return this._graphWebview;
}
private _focusWebview: FocusWebview;
get focusWebview() {
return this._focusWebview;
}
private readonly _richRemoteProviders: RichRemoteProviderService;
get richRemoteProviders(): RichRemoteProviderService {
return this._richRemoteProviders;
@ -652,11 +648,6 @@ export class Container {
return this._vsls;
}
private _welcomeWebview: WelcomeWebview;
get welcomeWebview() {
return this._welcomeWebview;
}
private _worktreesView: WorktreesView | undefined;
get worktreesView() {
if (this._worktreesView == null) {

+ 12
- 0
src/git/models/reference.ts View File

@ -262,6 +262,18 @@ export function getNameWithoutRemote(ref: GitReference) {
return ref.name;
}
export function isGitReference(ref: unknown): ref is GitReference {
if (ref == null || typeof ref !== 'object') return false;
const r = ref as GitReference;
return (
typeof r.refType === 'string' &&
typeof r.repoPath === 'string' &&
typeof r.ref === 'string' &&
typeof r.name === 'string'
);
}
export function isBranchReference(ref: GitReference | undefined): ref is GitBranchReference {
return ref?.refType === 'branch';
}

+ 151
- 233
src/plus/webviews/graph/graphWebview.ts View File

@ -1,21 +1,5 @@
import type {
ColorTheme,
ConfigurationChangeEvent,
Event,
StatusBarItem,
WebviewOptions,
WebviewPanelOptions,
} from 'vscode';
import {
CancellationTokenSource,
Disposable,
env,
EventEmitter,
MarkdownString,
StatusBarAlignment,
ViewColumn,
window,
} from 'vscode';
import type { ColorTheme, ConfigurationChangeEvent, Uri } from 'vscode';
import { CancellationTokenSource, Disposable, env, ViewColumn, window } from 'vscode';
import type { CreatePullRequestActionContext } from '../../../api/gitlens';
import { getAvatarUri } from '../../../avatars';
import type {
@ -55,6 +39,7 @@ import {
createReference,
getReferenceFromBranch,
getReferenceLabel,
isGitReference,
isSha,
shortenRevision,
} from '../../../git/models/reference';
@ -77,24 +62,20 @@ import { configuration } from '../../../system/configuration';
import { gate } from '../../../system/decorators/gate';
import { debug } from '../../../system/decorators/log';
import type { Deferrable } from '../../../system/function';
import { debounce, disposableInterval, once } from '../../../system/function';
import { debounce, disposableInterval } from '../../../system/function';
import { find, last } from '../../../system/iterable';
import { updateRecordValue } from '../../../system/object';
import { getSettledValue } from '../../../system/promise';
import { isDarkTheme, isLightTheme } from '../../../system/utils';
import type { WebviewItemContext, WebviewItemGroupContext } from '../../../system/webview';
import { isWebviewItemContext, isWebviewItemGroupContext, serializeWebviewItemContext } from '../../../system/webview';
import type { BranchNode } from '../../../views/nodes/branchNode';
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 { RepositoryFolderNode } from '../../../views/nodes/viewNode';
import type { IpcMessage, IpcMessageParams, IpcNotificationType } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import { WebviewBase } from '../../../webviews/webviewBase';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import type { WebviewIds, WebviewViewIds } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import { arePlusFeaturesEnabled, ensurePlusFeaturesEnabled } from '../../subscription/utils';
import { ensurePlusFeaturesEnabled } from '../../subscription/utils';
import type {
DimMergeCommitsParams,
DismissBannerParams,
@ -186,18 +167,12 @@ const defaultGraphColumnsSettings: GraphColumnsSettings = {
changes: { width: 130, isHidden: true },
};
export class GraphWebview extends WebviewBase<State> {
private _onDidChangeSelection = new EventEmitter<GraphSelectionChangeEvent>();
get onDidChangeSelection(): Event<GraphSelectionChangeEvent> {
return this._onDidChangeSelection.event;
}
export class GraphWebviewProvider implements WebviewProvider<State> {
private _repository?: Repository;
get repository(): Repository | undefined {
private get repository(): Repository | undefined {
return this._repository;
}
set repository(value: Repository | undefined) {
private set repository(value: Repository | undefined) {
if (this._repository === value) {
this.ensureRepositorySubscriptions();
return;
@ -207,20 +182,17 @@ export class GraphWebview extends WebviewBase {
this.resetRepositoryState();
this.ensureRepositorySubscriptions(true);
if (this.isReady) {
if (this.host.isReady) {
this.updateState();
}
}
private _selection: readonly GitRevisionReference[] | undefined;
get selection(): readonly GitRevisionReference[] | undefined {
return this._selection;
}
get activeSelection(): GitRevisionReference | undefined {
private get activeSelection(): GitRevisionReference | undefined {
return this._selection?.[0];
}
private readonly _disposable: Disposable;
private _etagSubscription?: number;
private _etagRepository?: number;
private _firstSelection = true;
@ -232,7 +204,6 @@ export class GraphWebview extends WebviewBase {
private _selectedId?: string;
private _selectedRows: GraphSelectedRows | undefined;
private _showDetailsView: Config['graph']['showDetailsView'];
private _statusBarItem: StatusBarItem | undefined;
private _theme: ColorTheme | undefined;
private _repositoryEventsDisposable: Disposable | undefined;
private _lastFetchedDisposable: Disposable | undefined;
@ -240,91 +211,70 @@ export class GraphWebview extends WebviewBase {
private trialBanner?: boolean;
private isWindowFocused: boolean = true;
constructor(container: Container) {
super(
container,
'gitlens.graph',
'graph.html',
'images/gitlens-icon.png',
'Commit Graph',
`${ContextKeys.WebviewPrefix}graph`,
'graphWebview',
Commands.ShowGraphPage,
);
constructor(
readonly container: Container,
readonly id: `gitlens.${WebviewIds}` | `gitlens.views.${WebviewViewIds}`,
readonly host: WebviewController<State>,
) {
this._showDetailsView = configuration.get('graph.showDetailsView');
this._theme = window.activeColorTheme;
this.ensureRepositorySubscriptions();
this.disposables.push(
this._disposable = Disposable.from(
configuration.onDidChange(this.onConfigurationChanged, this),
once(container.onReady)(() => queueMicrotask(() => this.updateStatusBar())),
onDidChangeContext(key => {
if (key !== ContextKeys.Enabled && key !== ContextKeys.PlusEnabled) return;
this.updateStatusBar();
}),
{ dispose: () => this._statusBarItem?.dispose() },
registerCommand(
Commands.ShowInCommitGraph,
async (
args:
| ShowInCommitGraphCommandArgs
| Repository
| BranchNode
| CommitNode
| CommitFileNode
| StashNode
| TagNode,
) => {
let id;
if (args instanceof Repository) {
this.repository = args;
} else {
this.repository = this.container.git.getRepository(args.ref.repoPath);
id = args.ref.ref;
if (!isSha(id)) {
id = await this.container.git.resolveReference(args.ref.repoPath, id, undefined, {
force: true,
});
}
this.setSelectedRows(id);
}
const preserveFocus = 'preserveFocus' in args ? args.preserveFocus ?? false : false;
if (this._panel == null) {
void this.show({ preserveFocus: preserveFocus });
} else if (id) {
this._panel.reveal(this._panel.viewColumn ?? ViewColumn.Active, preserveFocus ?? false);
if (this._graph?.ids.has(id)) {
void this.notifyDidChangeSelection();
return;
}
this.setSelectedRows(id);
void this.onGetMoreRows({ id: id }, true);
}
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
this.container.git.onDidChangeRepositories(() => void this.host.refresh(true)),
window.onDidChangeActiveColorTheme(this.onThemeChanged, this),
{
dispose: () => {
if (this._repositoryEventsDisposable == null) return;
this._repositoryEventsDisposable.dispose();
this._repositoryEventsDisposable = undefined;
},
),
},
);
}
protected override onWindowFocusChanged(focused: boolean): void {
this.isWindowFocused = focused;
void this.notifyDidChangeWindowFocus();
}
protected override get options(): WebviewPanelOptions & WebviewOptions {
return {
retainContextWhenHidden: true,
enableFindWidget: false,
enableCommandUris: true,
enableScripts: true,
};
dispose() {
this._disposable.dispose();
}
override async show(options?: { column?: ViewColumn; preserveFocus?: boolean }, ...args: unknown[]): Promise<void> {
async canShowWebviewPanel(
firstTime: boolean,
options?: { column?: ViewColumn; preserveFocus?: boolean },
...args: unknown[]
): Promise<boolean> {
this._firstSelection = true;
if (!(await ensurePlusFeaturesEnabled())) return;
if (!(await ensurePlusFeaturesEnabled())) return false;
if (this.container.git.repositoryCount > 1) {
if (options?.column != null) {
options.column = ViewColumn.Active;
}
const context = args[0];
if (context instanceof Repository) {
this.repository = context;
} else if (hasGitReference(context)) {
this.repository = this.container.git.getRepository(context.ref.repoPath);
let id = context.ref.ref;
if (!isSha(id)) {
id = await this.container.git.resolveReference(context.ref.repoPath, id, undefined, {
force: true,
});
}
this.setSelectedRows(id);
if (this._graph != null) {
if (this._graph?.ids.has(id)) {
void this.notifyDidChangeSelection();
return true;
}
void this.onGetMoreRows({ id: id }, true);
}
} else if (this.container.git.repositoryCount > 1) {
const [contexts] = parseCommandContext(Commands.ShowGraphPage, undefined, ...args);
const context = Array.isArray(contexts) ? contexts[0] : contexts;
@ -334,29 +284,28 @@ export class GraphWebview extends WebviewBase {
this.repository = context.node.repo;
}
if (this.repository != null && this.isReady) {
if (this.repository != null && !firstTime && this.host.isReady) {
this.updateState();
}
}
return super.show({ column: ViewColumn.Active, ...options }, ...args);
return true;
}
protected override refresh(force?: boolean): Promise<void> {
onRefresh(force?: boolean) {
this.resetRepositoryState();
if (force) {
this._pendingIpcNotifications.clear();
}
return super.refresh(force);
}
protected override async includeBootstrap(): Promise<State> {
includeBootstrap(): Promise<State> {
return this.getState(true);
}
protected override registerCommands(): Disposable[] {
registerCommands(): Disposable[] {
return [
registerCommand(Commands.RefreshGraph, () => this.refresh(true)),
registerCommand(Commands.RefreshGraph, () => this.host.refresh(true)),
registerCommand('gitlens.graph.push', this.push, this),
registerCommand('gitlens.graph.pull', this.pull, this),
@ -432,29 +381,47 @@ export class GraphWebview extends WebviewBase {
];
}
protected override onInitializing(): Disposable[] | undefined {
this._theme = window.activeColorTheme;
this.ensureRepositorySubscriptions();
return [
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
this.container.git.onDidChangeRepositories(() => void this.refresh(true)),
window.onDidChangeActiveColorTheme(this.onThemeChanged, this),
{
dispose: () => {
if (this._repositoryEventsDisposable == null) return;
this._repositoryEventsDisposable.dispose();
this._repositoryEventsDisposable = undefined;
},
},
];
onWindowFocusChanged(focused: boolean): void {
this.isWindowFocused = focused;
void this.notifyDidChangeWindowFocus();
}
protected override onReady(): void {
onReady(): void {
this.sendPendingIpcNotifications();
}
protected override onMessageReceived(e: IpcMessage) {
onFocusChanged(focused: boolean): void {
if (!focused || this.activeSelection == null || !this.container.commitDetailsView.visible) {
this._showActiveSelectionDetailsDebounced?.cancel();
return;
}
this.showActiveSelectionDetails();
}
onVisibilityChanged(visible: boolean): void {
if (!visible) {
this._showActiveSelectionDetailsDebounced?.cancel();
}
if (visible && this.repository != null && this.repository.etag !== this._etagRepository) {
this.updateState(true);
return;
}
if (visible) {
if (this.host.isReady) {
this.sendPendingIpcNotifications();
}
const { activeSelection } = this;
if (activeSelection == null) return;
this.showActiveSelectionDetails();
}
}
onMessageReceived(e: IpcMessage) {
switch (e.method) {
case ChooseRepositoryCommandType.method:
onIpc(ChooseRepositoryCommandType, e, () => this.onChooseRepository());
@ -527,17 +494,9 @@ export class GraphWebview extends WebviewBase {
}
}
protected override onFocusChanged(focused: boolean): void {
if (!focused || this.activeSelection == null || !this.container.commitDetailsView.visible) {
this._showActiveSelectionDetailsDebounced?.cancel();
return;
}
this.showActiveSelectionDetails();
}
private _showActiveSelectionDetailsDebounced: Deferrable<GraphWebview['showActiveSelectionDetails']> | undefined =
undefined;
private _showActiveSelectionDetailsDebounced:
| Deferrable<GraphWebviewProvider['showActiveSelectionDetails']>
| undefined = undefined;
private showActiveSelectionDetails() {
if (this._showActiveSelectionDetailsDebounced == null) {
@ -565,40 +524,11 @@ export class GraphWebview extends WebviewBase {
);
}
protected override onVisibilityChanged(visible: boolean): void {
if (!visible) {
this._showActiveSelectionDetailsDebounced?.cancel();
}
if (visible && this.repository != null && this.repository.etag !== this._etagRepository) {
this.updateState(true);
return;
}
if (visible) {
if (this.isReady) {
this.sendPendingIpcNotifications();
}
const { activeSelection } = this;
if (activeSelection == null) return;
this.showActiveSelectionDetails();
}
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'graph.showDetailsView')) {
this._showDetailsView = configuration.get('graph.showDetailsView');
}
if (configuration.changed(e, 'graph.statusBar.enabled') || configuration.changed(e, 'plusFeatures.enabled')) {
this.updateStatusBar();
}
// If we don't have an open webview ignore the rest
if (this._panel == null) return;
if (configuration.changed(e, 'graph.commitOrdering')) {
this.updateState();
@ -636,7 +566,7 @@ export class GraphWebview extends WebviewBase {
}
}
@debug<GraphWebview['onRepositoryChanged']>({ args: { 0: e => e.toString() } })
@debug<GraphWebviewProvider['onRepositoryChanged']>({ args: { 0: e => e.toString() } })
private onRepositoryChanged(e: RepositoryChangeEvent) {
if (
!e.changed(
@ -677,7 +607,6 @@ export class GraphWebview extends WebviewBase {
this._etagSubscription = e.etag;
void this.notifyDidChangeSubscription();
this.updateStatusBar();
}
private onThemeChanged(theme: ColorTheme) {
@ -794,7 +723,7 @@ export class GraphWebview extends WebviewBase {
const repoPath = this._graph.repoPath;
async function getAvatar(this: GraphWebview, email: string, id: string) {
async function getAvatar(this: GraphWebviewProvider, email: string, id: string) {
const uri = await getAvatarUri(email, { ref: id, repoPath: repoPath });
this._graph!.avatars.set(email, uri.toString(true));
}
@ -818,7 +747,11 @@ export class GraphWebview extends WebviewBase {
const repoPath = this._graph.repoPath;
async function getRefMetadata(this: GraphWebview, id: string, missingTypes: GraphMissingRefsMetadataType[]) {
async function getRefMetadata(
this: GraphWebviewProvider,
id: string,
missingTypes: GraphMissingRefsMetadataType[],
) {
if (this._refsMetadata == null) {
this._refsMetadata = new Map();
}
@ -1082,7 +1015,8 @@ export class GraphWebview extends WebviewBase {
this.repository = pick.item;
}
private _fireSelectionChangedDebounced: Deferrable<GraphWebview['fireSelectionChanged']> | undefined = undefined;
private _fireSelectionChangedDebounced: Deferrable<GraphWebviewProvider['fireSelectionChanged']> | undefined =
undefined;
private onSelectionChanged(e: UpdateSelectionParams) {
const item = e.selection[0];
@ -1102,7 +1036,6 @@ export class GraphWebview extends WebviewBase {
const commits = commit != null ? [commit] : undefined;
this._selection = commits;
this._onDidChangeSelection.fire({ selection: commits ?? [] });
if (commits == null) return;
@ -1123,7 +1056,8 @@ export class GraphWebview extends WebviewBase {
this._firstSelection = false;
}
private _notifyDidChangeStateDebounced: Deferrable<GraphWebview['notifyDidChangeState']> | undefined = undefined;
private _notifyDidChangeStateDebounced: Deferrable<GraphWebviewProvider['notifyDidChangeState']> | undefined =
undefined;
private getRevisionReference(
repoPath: string | undefined,
@ -1166,7 +1100,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeWindowFocus(): Promise<boolean> {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidChangeWindowFocusNotificationType);
return false;
}
@ -1176,7 +1110,7 @@ export class GraphWebview extends WebviewBase {
});
}
private _notifyDidChangeAvatarsDebounced: Deferrable<GraphWebview['notifyDidChangeAvatars']> | undefined =
private _notifyDidChangeAvatarsDebounced: Deferrable<GraphWebviewProvider['notifyDidChangeAvatars']> | undefined =
undefined;
@debug()
@ -1203,8 +1137,9 @@ export class GraphWebview extends WebviewBase {
});
}
private _notifyDidChangeRefsMetadataDebounced: Deferrable<GraphWebview['notifyDidChangeRefsMetadata']> | undefined =
undefined;
private _notifyDidChangeRefsMetadataDebounced:
| Deferrable<GraphWebviewProvider['notifyDidChangeRefsMetadata']>
| undefined = undefined;
@debug()
private updateRefsMetadata(immediate: boolean = false) {
@ -1229,7 +1164,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeColumns() {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidChangeColumnsNotificationType);
return false;
}
@ -1244,7 +1179,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeRefsVisibility() {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidChangeRefsVisibilityNotificationType);
return false;
}
@ -1258,7 +1193,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeConfiguration() {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidChangeGraphConfigurationNotificationType);
return false;
}
@ -1270,7 +1205,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidFetch() {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidFetchNotificationType);
return false;
}
@ -1305,7 +1240,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeWorkingTree() {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidChangeWorkingTreeNotificationType);
return false;
}
@ -1317,7 +1252,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeSelection() {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidChangeSelectionNotificationType);
return false;
}
@ -1329,7 +1264,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeSubscription() {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidChangeSubscriptionNotificationType);
return false;
}
@ -1343,7 +1278,7 @@ export class GraphWebview extends WebviewBase {
@debug()
private async notifyDidChangeState() {
if (!this.isReady || !this.visible) {
if (!this.host.isReady || !this.host.visible) {
this.addPendingIpcNotification(DidChangeNotificationType);
return false;
}
@ -1351,18 +1286,18 @@ export class GraphWebview extends WebviewBase {
return this.notify(DidChangeNotificationType, { state: await this.getState() });
}
protected override async notify<T extends IpcNotificationType<any>>(
private async notify<T extends IpcNotificationType<any>>(
type: T,
params: IpcMessageParams<T>,
completionId?: string,
): Promise<boolean> {
const msg: IpcMessage = {
id: this.nextIpcId(),
id: this.host.nextIpcId(),
method: type.method,
params: params,
completionId: completionId,
};
const success = await this.postMessage(msg);
const success = await this.host.postMessage(msg);
if (success) {
this._pendingIpcNotifications.clear();
} else {
@ -1412,7 +1347,7 @@ export class GraphWebview extends WebviewBase {
if (typeof msgOrFn === 'function') {
void msgOrFn();
} else {
void this.postMessage(msgOrFn);
void this.host.postMessage(msgOrFn);
}
}
}
@ -1531,6 +1466,7 @@ export class GraphWebview extends WebviewBase {
const excludeRefs: GraphExcludeRefs = {};
const asWebviewUri = (uri: Uri) => this.host.asWebviewUri(uri);
for (const id in storedExcludeRefs) {
const ref: GraphExcludedRef = { ...storedExcludeRefs[id] };
if (ref.type === 'remote' && ref.owner) {
@ -1538,11 +1474,7 @@ export class GraphWebview extends WebviewBase {
if (remote != null) {
ref.avatarUrl = (
(useAvatars ? remote.provider?.avatarUri : undefined) ??
getRemoteIconUri(
this.container,
remote,
this._panel!.webview.asWebviewUri.bind(this._panel!.webview),
)
getRemoteIconUri(this.container, remote, asWebviewUri)
)?.toString(true);
}
}
@ -1790,7 +1722,7 @@ export class GraphWebview extends WebviewBase {
}
this._etagRepository = this.repository?.etag;
this.title = `${this.originalTitle}: ${this.repository.formattedName}`;
this.host.title = `${this.host.originalTitle}: ${this.repository.formattedName}`;
const { defaultItemLimit } = configuration.get('graph');
@ -1804,7 +1736,7 @@ export class GraphWebview extends WebviewBase {
const dataPromise = this.container.git.getCommitsForGraph(
this.repository.path,
this._panel!.webview.asWebviewUri.bind(this._panel!.webview),
uri => this.host.asWebviewUri(uri),
{
include: {
stats: configuration.get('graph.experimental.minimap.enabled') || !columnSettings.changes.isHidden,
@ -1871,7 +1803,7 @@ export class GraphWebview extends WebviewBase {
excludeRefs: data != null ? this.getExcludedRefs(data) ?? {} : {},
excludeTypes: this.getExcludedTypes(data) ?? {},
includeOnlyRefs: data != null ? this.getIncludeOnlyRefs(data) ?? {} : {},
nonce: this.cspNonce,
nonce: this.host.cspNonce,
workingTreeStats: getSettledValue(workingStatsResult) ?? { added: 0, deleted: 0, modified: 0 },
debugging: this.container.debugging,
};
@ -2000,27 +1932,6 @@ export class GraphWebview extends WebviewBase {
}
}
private updateStatusBar() {
const enabled =
configuration.get('graph.statusBar.enabled') && getContext(ContextKeys.Enabled) && arePlusFeaturesEnabled();
if (enabled) {
if (this._statusBarItem == null) {
this._statusBarItem = window.createStatusBarItem('gitlens.graph', StatusBarAlignment.Left, 10000 - 3);
this._statusBarItem.name = 'GitLens Commit Graph';
this._statusBarItem.command = Commands.ShowGraphPage;
this._statusBarItem.text = '$(gitlens-graph)';
this._statusBarItem.tooltip = new MarkdownString('Visualize commits on the Commit Graph ✨');
this._statusBarItem.accessibilityInformation = {
label: `Show the GitLens Commit Graph`,
};
}
this._statusBarItem.show();
} else {
this._statusBarItem?.dispose();
this._statusBarItem = undefined;
}
}
@debug()
private fetch(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'branch');
@ -2723,3 +2634,10 @@ function isGraphItemRefContext(item: unknown, refType?: GitReference['refType'])
function getRepoPathFromBranchOrTagId(id: string): string {
return id.split('|', 1)[0];
}
export function hasGitReference(o: unknown): o is { ref: GitReference } {
if (o == null || typeof o !== 'object') return false;
if (!('ref' in o)) return false;
return isGitReference(o.ref);
}

+ 61
- 0
src/plus/webviews/graph/registration.ts View File

@ -0,0 +1,61 @@
import { Commands, ContextKeys } from '../../../constants';
import type { Repository } from '../../../git/models/repository';
import { registerCommand } from '../../../system/command';
import type { BranchNode } from '../../../views/nodes/branchNode';
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 { ShowInCommitGraphCommandArgs } from './graphWebview';
import type { State } from './protocol';
export function registerGraphWebviewPanel(controller: WebviewsController) {
return controller.registerWebviewPanel<State>(Commands.ShowGraphPage, 'gitlens.graph', {
fileName: 'graph.html',
iconPath: 'images/gitlens-icon.png',
title: 'Commit Graph',
contextKeyPrefix: `${ContextKeys.WebviewPrefix}graph`,
trackingFeature: 'graphWebview',
panelOptions: {
retainContextWhenHidden: true,
enableFindWidget: false,
},
resolveWebviewProvider: async function (container, id, host) {
const { GraphWebviewProvider } = await import(/* webpackChunkName: "graph" */ './graphWebview');
return new GraphWebviewProvider(container, id, host);
},
});
}
export function registerGraphWebviewView(controller: WebviewsController) {
return controller.registerWebviewView<State>('gitlens.views.graph', {
fileName: 'graph.html',
title: 'Commit Graph',
contextKeyPrefix: `${ContextKeys.WebviewViewPrefix}graph`,
trackingFeature: 'graphView',
resolveWebviewProvider: async function (container, id, host) {
const { GraphWebviewProvider } = await import(/* webpackChunkName: "graph" */ './graphWebview');
return new GraphWebviewProvider(container, id, host);
},
});
}
export function registerGraphWebviewCommands(webview: WebviewPanelProxy) {
return registerCommand(
Commands.ShowInCommitGraph,
(
args:
| ShowInCommitGraphCommandArgs
| Repository
| BranchNode
| CommitNode
| CommitFileNode
| StashNode
| TagNode,
) => {
const preserveFocus = 'preserveFocus' in args ? args.preserveFocus ?? false : false;
void webview.show({ preserveFocus: preserveFocus }, args);
},
);
}

+ 62
- 0
src/plus/webviews/graph/statusbar.ts View File

@ -0,0 +1,62 @@
import type { ConfigurationChangeEvent, StatusBarItem } from 'vscode';
import { Disposable, MarkdownString, StatusBarAlignment, window } from 'vscode';
import { Commands, ContextKeys } from '../../../constants';
import type { Container } from '../../../container';
import { getContext, onDidChangeContext } from '../../../context';
import { configuration } from '../../../system/configuration';
import { once } from '../../../system/function';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import { arePlusFeaturesEnabled } from '../../subscription/utils';
export class GraphStatusBarController implements Disposable {
private readonly _disposable: Disposable;
private _statusBarItem: StatusBarItem | undefined;
constructor(container: Container) {
this._disposable = Disposable.from(
configuration.onDidChange(this.onConfigurationChanged, this),
container.subscription.onDidChange(this.onSubscriptionChanged, this),
once(container.onReady)(() => queueMicrotask(() => this.updateStatusBar())),
onDidChangeContext(key => {
if (key !== ContextKeys.Enabled && key !== ContextKeys.PlusEnabled) return;
this.updateStatusBar();
}),
{ dispose: () => this._statusBarItem?.dispose() },
);
}
dispose() {
this._disposable.dispose();
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'graph.statusBar.enabled') || configuration.changed(e, 'plusFeatures.enabled')) {
this.updateStatusBar();
}
}
private onSubscriptionChanged(_e: SubscriptionChangeEvent) {
this.updateStatusBar();
}
private updateStatusBar() {
const enabled =
configuration.get('graph.statusBar.enabled') && getContext(ContextKeys.Enabled) && arePlusFeaturesEnabled();
if (enabled) {
if (this._statusBarItem == null) {
this._statusBarItem = window.createStatusBarItem('gitlens.graph', StatusBarAlignment.Left, 10000 - 3);
this._statusBarItem.name = 'GitLens Commit Graph';
this._statusBarItem.command = Commands.ShowGraphPage;
this._statusBarItem.text = '$(gitlens-graph)';
this._statusBarItem.tooltip = new MarkdownString('Visualize commits on the Commit Graph ✨');
this._statusBarItem.accessibilityInformation = {
label: `Show the GitLens Commit Graph`,
};
}
this._statusBarItem.show();
} else {
this._statusBarItem?.dispose();
this._statusBarItem = undefined;
}
}
}

+ 1
- 0
src/telemetry/usageTracker.ts View File

@ -16,6 +16,7 @@ export type TrackedUsageFeatures =
| 'commitsView'
| 'contributorsView'
| 'fileHistoryView'
| 'graphView'
| 'graphWebview'
| 'homeView'
| 'lineHistoryView'

+ 1
- 1
src/webviews/webviewsController.ts View File

@ -14,7 +14,7 @@ import type { WebviewProvider } from './webviewController';
import { WebviewController } from './webviewController';
export type WebviewIds = 'graph' | 'settings' | 'timeline' | 'welcome' | 'focus';
export type WebviewViewIds = 'commitDetails' | 'home' | 'timeline';
export type WebviewViewIds = 'commitDetails' | 'graph' | 'home' | 'timeline';
export interface WebviewPanelDescriptor<State = any> {
readonly fileName: string;

Loading…
Cancel
Save