瀏覽代碼

Reworks timeline page to open to a specific editor

Reduces startup impact
main
Eric Amodio 2 年之前
父節點
當前提交
93315e4f20
共有 7 個檔案被更改,包括 255 行新增67 行删除
  1. +35
    -1
      package.json
  2. +2
    -0
      src/constants.ts
  3. +190
    -47
      src/premium/webviews/timeline/timelineWebview.ts
  4. +11
    -1
      src/premium/webviews/timeline/timelineWebviewView.ts
  5. +0
    -3
      src/webviews/apps/premium/timeline/timeline.html
  6. +0
    -3
      src/webviews/apps/premium/timeline/timeline.scss
  7. +17
    -12
      src/webviews/webviewBase.ts

+ 35
- 1
package.json 查看文件

@ -3719,7 +3719,7 @@
},
{
"command": "gitlens.showTimelinePage",
"title": "Open Visual File History",
"title": "Open Visual File History of Active File",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-history.svg",
@ -3727,6 +3727,12 @@
}
},
{
"command": "gitlens.refreshTimelinePage",
"title": "Refresh",
"category": "GitLens",
"icon": "$(refresh)"
},
{
"command": "gitlens.showWelcomePage",
"title": "Welcome (Quick Setup)",
"category": "GitLens"
@ -5841,6 +5847,12 @@
"category": "GitLens"
},
{
"command": "gitlens.views.timeline.openInTab",
"title": "Open in Editor Area",
"category": "GitLens",
"icon": "$(link-external)"
},
{
"command": "gitlens.views.timeline.refresh",
"title": "Refresh",
"category": "GitLens",
@ -5998,6 +6010,14 @@
"when": "false"
},
{
"command": "gitlens.showTimelinePage",
"when": "gitlens:enabled && editorFocus"
},
{
"command": "gitlens.refreshTimelinePage",
"when": "false"
},
{
"command": "gitlens.showBranchesView",
"when": "gitlens:enabled"
},
@ -7398,6 +7418,10 @@
"when": "false"
},
{
"command": "gitlens.views.timeline.openInTab",
"when": "false"
},
{
"command": "gitlens.views.timeline.refresh",
"when": "false"
},
@ -7647,6 +7671,11 @@
"command": "gitlens.clearFileAnnotations",
"when": "gitlens:activeFileStatus =~ /blameable/ && gitlens:annotationStatus == computed && config.gitlens.menus.editorGroup.blame",
"group": "navigation@100"
},
{
"command": "gitlens.refreshTimelinePage",
"when": "gitlens:timelinePage:focused",
"group": "navigation@-99"
}
],
"editor/title/context": [
@ -8399,6 +8428,11 @@
"group": "5_gitlens@0"
},
{
"command": "gitlens.views.timeline.openInTab",
"when": "view =~ /^gitlens\\.views\\.timeline/",
"group": "navigation@98"
},
{
"command": "gitlens.views.timeline.refresh",
"when": "view =~ /^gitlens\\.views\\.timeline/",
"group": "navigation@99"

+ 2
- 0
src/constants.ts 查看文件

@ -193,6 +193,7 @@ export const enum Commands {
ShowStashesView = 'gitlens.showStashesView',
ShowTagsView = 'gitlens.showTagsView',
ShowWorktreesView = 'gitlens.showWorktreesView',
RefreshTimelinePage = 'gitlens.refreshTimelinePage',
ShowTimelinePage = 'gitlens.showTimelinePage',
ShowTimelineView = 'gitlens.showTimelineView',
ShowWelcomePage = 'gitlens.showWelcomePage',
@ -242,6 +243,7 @@ export const enum ContextKeys {
HasRichRemotes = 'gitlens:hasRichRemotes',
HasVirtualFolders = 'gitlens:hasVirtualFolders',
Readonly = 'gitlens:readonly',
TimelinePageFocused = 'gitlens:timelinePage:focused',
Untrusted = 'gitlens:untrusted',
ViewsCanCompare = 'gitlens:views:canCompare',
ViewsCanCompareFile = 'gitlens:views:canCompare:file',

+ 190
- 47
src/premium/webviews/timeline/timelineWebview.ts 查看文件

@ -1,13 +1,15 @@
'use strict';
import { commands, ProgressLocation, TextEditor, window } from 'vscode';
import { commands, Disposable, TextEditor, Uri, window } from 'vscode';
import { ShowQuickCommitCommandArgs } from '../../../commands';
import { Commands } from '../../../constants';
import { Commands, ContextKeys } from '../../../constants';
import type { Container } from '../../../container';
import { setContext } from '../../../context';
import { PremiumFeatures } from '../../../features';
import { GitUri } from '../../../git/gitUri';
import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent } from '../../../git/models';
import { createFromDateDelta } from '../../../system/date';
import { debug } from '../../../system/decorators/log';
import { debounce } from '../../../system/function';
import { debounce, Deferrable } from '../../../system/function';
import { hasVisibleTextEditor, isTextEditor } from '../../../system/utils';
import { IpcMessage, onIpc } from '../../../webviews/protocol';
import { WebviewBase } from '../../../webviews/webviewBase';
@ -16,13 +18,27 @@ import {
Commit,
DidChangeStateNotificationType,
OpenDataPointCommandType,
Period,
State,
UpdatePeriodCommandType,
} from './protocol';
interface Context {
uri: Uri | undefined;
period: Period | undefined;
etagRepository: number | undefined;
etagSubscription: number | undefined;
}
const defaultPeriod: Period = '3|M';
export class TimelineWebview extends WebviewBase<State> {
private _editor: TextEditor | undefined;
private _period: `${number}|${'D' | 'M' | 'Y'}` = '3|M';
private _bootstraping = true;
/** The context the webview has */
private _context: Context;
/** The context the webview should have */
private _pendingContext: Partial<Context> | undefined;
private _originalTitle: string;
constructor(container: Container) {
super(
@ -33,42 +49,84 @@ export class TimelineWebview extends WebviewBase {
'Visual File History',
Commands.ShowTimelinePage,
);
this._originalTitle = this.title;
this._context = {
uri: undefined,
period: defaultPeriod,
etagRepository: 0,
etagSubscription: 0,
};
}
protected override onInitializing(): Disposable[] | undefined {
this._context = {
uri: undefined,
period: defaultPeriod,
etagRepository: 0,
etagSubscription: this.container.subscription.etag,
};
this.disposables.push(
this.updatePendingEditor(window.activeTextEditor);
this._context = { ...this._context, ...this._pendingContext };
this._pendingContext = undefined;
return [
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
window.onDidChangeActiveTextEditor(debounce(this.onActiveEditorChanged, 500), this),
);
this.container.git.onDidChangeRepository(this.onRepositoryChanged, this),
];
}
protected override onReady() {
this.onActiveEditorChanged(window.activeTextEditor);
protected override onShowCommand(uri?: Uri): void {
if (uri != null) {
this.updatePendingUri(uri);
} else {
this.updatePendingEditor(window.activeTextEditor);
}
this._context = { ...this._context, ...this._pendingContext };
this._pendingContext = undefined;
super.onShowCommand();
}
protected override async includeBootstrap(): Promise<State> {
return this.getState(undefined);
this._bootstraping = true;
this._context = { ...this._context, ...this._pendingContext };
this._pendingContext = undefined;
return this.getState(this._context);
}
@debug({ args: false })
private onActiveEditorChanged(editor: TextEditor | undefined) {
if (!this.isReady || (this._editor === editor && editor != null)) return;
if (editor == null && hasVisibleTextEditor()) return;
if (editor != null && !isTextEditor(editor)) return;
protected override registerCommands(): Disposable[] {
return [commands.registerCommand(Commands.RefreshTimelinePage, () => this.refresh())];
}
this._editor = editor;
void this.notifyDidChangeState(editor);
protected override onFocusChanged(focused: boolean): void {
if (focused) {
// If we are becoming focused, delay it a bit to give the UI time to update
setTimeout(() => void setContext(ContextKeys.TimelinePageFocused, focused), 0);
return;
}
void setContext(ContextKeys.TimelinePageFocused, focused);
}
private onSubscriptionChanged(_e: SubscriptionChangeEvent) {
void this.notifyDidChangeState(this._editor);
protected override onVisibilityChanged(visible: boolean) {
if (!visible) return;
// Since this gets called even the first time the webview is shown, avoid sending an update, because the bootstrap has the data
if (this._bootstraping) {
this._bootstraping = false;
return;
}
this.updateState(true);
}
protected override onMessageReceived(e: IpcMessage) {
switch (e.method) {
case OpenDataPointCommandType.method:
onIpc(OpenDataPointCommandType, e, params => {
if (params.data == null || this._editor == null || !params.data.selected) return;
if (params.data == null || !params.data.selected || this._context.uri == null) return;
const repository = this.container.git.getRepository(this._editor.document.uri);
const repository = this.container.git.getRepository(this._context.uri);
if (repository == null) return;
const commandArgs: ShowQuickCommitCommandArgs = {
@ -98,50 +156,70 @@ export class TimelineWebview extends WebviewBase {
case UpdatePeriodCommandType.method:
onIpc(UpdatePeriodCommandType, e, params => {
this._period = params.period;
void this.notifyDidChangeState(this._editor);
if (this.updatePendingContext({ period: params.period })) {
this.updateState(true);
}
});
break;
}
}
@debug({ args: { 0: e => e?.document.uri.toString(true) } })
private async getState(editor: TextEditor | undefined): Promise<State> {
@debug({ args: false })
private onRepositoryChanged(e: RepositoryChangeEvent) {
if (!e.changed(RepositoryChange.Heads, RepositoryChange.Index, RepositoryChangeComparisonMode.Any)) {
return;
}
if (this.updatePendingContext({ etagRepository: e.repository.etag })) {
this.updateState();
}
}
@debug({ args: false })
private onSubscriptionChanged(e: SubscriptionChangeEvent) {
if (this.updatePendingContext({ etagSubscription: e.etag })) {
this.updateState();
}
}
@debug({ args: false })
private async getState(current: Context): Promise<State> {
const access = await this.container.git.access(PremiumFeatures.Timeline);
const period = current.period ?? defaultPeriod;
const dateFormat = this.container.config.defaultDateFormat ?? 'MMMM Do, YYYY h:mma';
if (editor == null || !access.allowed) {
if (current.uri == null || !access.allowed) {
return {
period: this._period,
period: period,
title: 'There are no editors open that can provide file history information',
dateFormat: dateFormat,
access: access,
};
}
const gitUri = await GitUri.fromUri(editor.document.uri);
const gitUri = await GitUri.fromUri(current.uri);
const repoPath = gitUri.repoPath!;
const title = gitUri.relativePath;
// this.setTitle(`${this.title} \u2022 ${gitUri.fileName}`);
// this.description = gitUri.fileName;
this.title = `${this._originalTitle}: ${gitUri.fileName}`;
const [currentUser, log] = await Promise.all([
this.container.git.getCurrentUser(repoPath),
this.container.git.getLogForFile(repoPath, gitUri.fsPath, {
limit: 0,
ref: gitUri.sha,
since: this.getPeriodDate().toISOString(),
since: this.getPeriodDate(period).toISOString(),
}),
]);
if (log == null) {
return {
dataset: [],
period: this._period,
title: title,
uri: editor.document.uri.toString(),
period: period,
title: 'No commits found for the specified time period',
uri: current.uri.toString(),
dateFormat: dateFormat,
access: access,
};
@ -168,17 +246,15 @@ export class TimelineWebview extends WebviewBase {
return {
dataset: dataset,
period: this._period,
period: period,
title: title,
uri: editor.document.uri.toString(),
uri: current.uri.toString(),
dateFormat: dateFormat,
access: access,
};
}
private getPeriodDate(): Date {
const period = this._period ?? '3|M';
private getPeriodDate(period: Period): Date {
const [number, unit] = period.split('|');
switch (unit) {
@ -193,13 +269,80 @@ export class TimelineWebview extends WebviewBase {
}
}
private async notifyDidChangeState(editor: TextEditor | undefined) {
if (!this.isReady) return false;
private updatePendingContext(context: Partial<Context>): boolean {
let changed = false;
for (const [key, value] of Object.entries(context)) {
const current = (this._context as unknown as Record<string, unknown>)[key];
if (
current === value ||
((current instanceof Uri || value instanceof Uri) && (current as any)?.toString() === value?.toString())
) {
continue;
}
if (this._pendingContext == null) {
this._pendingContext = {};
}
(this._pendingContext as Record<string, unknown>)[key] = value;
changed = true;
}
return window.withProgress({ location: ProgressLocation.Window }, async () =>
this.notify(DidChangeStateNotificationType, {
state: await this.getState(editor),
}),
);
return changed;
}
private updatePendingEditor(editor: TextEditor | undefined): boolean {
if (editor == null && hasVisibleTextEditor()) return false;
if (editor != null && !isTextEditor(editor)) return false;
return this.updatePendingUri(editor?.document.uri);
}
private updatePendingUri(uri: Uri | undefined): boolean {
let etag;
if (uri != null) {
const repository = this.container.git.getRepository(uri);
etag = repository?.etag ?? 0;
} else {
etag = 0;
}
return this.updatePendingContext({ uri: uri, etagRepository: etag });
}
private _notifyDidChangeStateDebounced: Deferrable<() => void> | undefined = undefined;
@debug()
private updateState(immediate: boolean = false) {
if (!this.isReady || !this.visible) return;
if (immediate) {
void this.notifyDidChangeState();
return;
}
if (this._notifyDidChangeStateDebounced == null) {
this._notifyDidChangeStateDebounced = debounce(this.notifyDidChangeState.bind(this), 500);
}
this._notifyDidChangeStateDebounced();
}
@debug()
private async notifyDidChangeState() {
if (!this.isReady || !this.visible) return false;
this._notifyDidChangeStateDebounced?.cancel();
const context = { ...this._context, ...this._pendingContext };
return window.withProgress({ location: { viewId: this.id } }, async () => {
const success = await this.notify(DidChangeStateNotificationType, {
state: await this.getState(context),
});
if (success) {
this._context = context;
this._pendingContext = undefined;
}
});
}
}

+ 11
- 1
src/premium/webviews/timeline/timelineWebviewView.ts 查看文件

@ -74,7 +74,10 @@ export class TimelineWebviewView extends WebviewViewBase {
}
protected override registerCommands(): Disposable[] {
return [commands.registerCommand(`${this.id}.refresh`, () => this.refresh(), this)];
return [
commands.registerCommand(`${this.id}.refresh`, () => this.refresh(), this),
commands.registerCommand(`${this.id}.openInTab`, () => this.openInTab(), this),
];
}
protected override onVisibilityChanged(visible: boolean) {
@ -244,6 +247,13 @@ export class TimelineWebviewView extends WebviewViewBase {
}
}
private openInTab() {
const uri = this._context.uri;
if (uri == null) return;
void commands.executeCommand(Commands.ShowTimelinePage, uri);
}
private updatePendingContext(context: Partial<Context>): boolean {
let changed = false;
for (const [key, value] of Object.entries(context)) {

+ 0
- 3
src/webviews/apps/premium/timeline/timeline.html 查看文件

@ -6,9 +6,6 @@
.hidden {
display: none !important;
}
.preload .header {
visibility: hidden;
}
</style>
</head>

+ 0
- 3
src/webviews/apps/premium/timeline/timeline.scss 查看文件

@ -136,7 +136,6 @@ span.button-subaction {
align-items: center;
justify-content: flex-end;
flex: 100% 0 1;
// margin: 1em;
label {
margin: 0 1em;
@ -148,8 +147,6 @@ span.button-subaction {
position: relative;
overflow: hidden;
width: 100%;
// height: calc(100% - 1rem);
// height: 100%;
}
#chart {

+ 17
- 12
src/webviews/webviewBase.ts 查看文件

@ -34,14 +34,6 @@ function nextIpcId() {
return `host:${ipcSequence}`;
}
const emptyCommands: Disposable[] = [
{
dispose: function () {
/* noop */
},
},
];
export abstract class WebviewBase<State> implements Disposable {
protected readonly disposables: Disposable[] = [];
protected isReady: boolean = false;
@ -104,7 +96,8 @@ export abstract class WebviewBase implements Disposable {
this._panel.onDidDispose(this.onPanelDisposed, this),
this._panel.onDidChangeViewState(this.onViewStateChanged, this),
this._panel.webview.onDidReceiveMessage(this.onMessageReceivedCore, this),
...this.registerCommands(),
...(this.onInitializing?.() ?? []),
...(this.registerCommands?.() ?? []),
);
this._panel.webview.html = await this.getHtml(this._panel.webview);
@ -119,19 +112,29 @@ export abstract class WebviewBase implements Disposable {
}
}
protected onInitializing?(): Disposable[] | undefined;
protected onReady?(): void;
protected onMessageReceived?(e: IpcMessage): void;
protected onFocusChanged?(focused: boolean): void;
protected onVisibilityChanged?(visible: boolean): void;
protected registerCommands(): Disposable[] {
return emptyCommands;
}
protected registerCommands?(): Disposable[];
protected includeBootstrap?(): State | Promise<State>;
protected includeHead?(): string | Promise<string>;
protected includeBody?(): string | Promise<string>;
protected includeEndOfBody?(): string | Promise<string>;
protected async refresh(): Promise<void> {
if (this._panel == null) return;
this._panel.webview.html = await this.getHtml(this._panel.webview);
}
private onPanelDisposed() {
this.onVisibilityChanged?.(false);
this.onFocusChanged?.(false);
this._disposablePanel?.dispose();
this._disposablePanel = undefined;
this._panel = undefined;
@ -146,6 +149,8 @@ export abstract class WebviewBase implements Disposable {
`Webview(${this.id}).onViewStateChanged`,
`active=${e.webviewPanel.active}, visible=${e.webviewPanel.visible}`,
);
this.onVisibilityChanged?.(e.webviewPanel.visible);
this.onFocusChanged?.(e.webviewPanel.active);
}
protected onMessageReceivedCore(e: IpcMessage) {

Loading…
取消
儲存