Browse Source

Adds visual file history webviews (wip)

main
Eric Amodio 2 years ago
parent
commit
db77e8aebe
22 changed files with 1995 additions and 24 deletions
  1. +29
    -1
      package.json
  2. +1
    -0
      src/constants.ts
  3. +15
    -0
      src/container.ts
  4. +2
    -0
      src/features.ts
  5. +44
    -0
      src/premium/webviews/timeline/protocol.ts
  6. +205
    -0
      src/premium/webviews/timeline/timelineWebview.ts
  7. +218
    -0
      src/premium/webviews/timeline/timelineWebviewView.ts
  8. +5
    -0
      src/subscription.ts
  9. +490
    -0
      src/webviews/apps/premium/timeline/chart.scss
  10. +331
    -0
      src/webviews/apps/premium/timeline/chart.ts
  11. +18
    -0
      src/webviews/apps/premium/timeline/partials/state.free-preview-expired.html
  12. +20
    -0
      src/webviews/apps/premium/timeline/partials/state.free.html
  13. +10
    -0
      src/webviews/apps/premium/timeline/partials/state.plus-trial-expired.html
  14. +11
    -0
      src/webviews/apps/premium/timeline/partials/state.verify-email.html
  15. +5
    -0
      src/webviews/apps/premium/timeline/plugins.d.ts
  16. +59
    -0
      src/webviews/apps/premium/timeline/timeline.html
  17. +165
    -0
      src/webviews/apps/premium/timeline/timeline.scss
  18. +171
    -0
      src/webviews/apps/premium/timeline/timeline.ts
  19. +22
    -16
      src/webviews/apps/shared/theme.ts
  20. +1
    -0
      src/webviews/protocol.ts
  21. +3
    -1
      webpack.config.js
  22. +170
    -6
      yarn.lock

+ 29
- 1
package.json View File

@ -3717,6 +3717,15 @@
"icon": "$(gear)"
},
{
"command": "gitlens.showTimelinePage",
"title": "Open Visual File History",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-history.svg",
"light": "images/light/icon-history.svg"
}
},
{
"command": "gitlens.showWelcomePage",
"title": "Welcome (Quick Setup)",
"category": "GitLens"
@ -10530,6 +10539,13 @@
"title": "GitLens",
"icon": "images/gitlens-activitybar.svg"
}
],
"panel": [
{
"id": "gitlensPanel",
"title": "GitLens",
"icon": "images/views/history.svg"
}
]
},
"viewsWelcome": [
@ -10599,12 +10615,23 @@
"type": "webview",
"id": "gitlens.views.home",
"name": "Home",
"-when": "!gitlens:disabled",
"when": "!gitlens:disabled",
"contextualTitle": "GitLens",
"icon": "images/gitlens-activitybar.svg",
"visibility": "visible"
}
],
"gitlensPanel": [
{
"type": "webview",
"id": "gitlens.views.timeline",
"name": "Visual File History",
"when": "!gitlens:disabled",
"contextualTitle": "GitLens",
"icon": "images/views/history.svg",
"visibility": "visible"
}
],
"scm": [
{
"id": "gitlens.views.commits",
@ -10881,6 +10908,7 @@
"@vscode/codicons": "0.0.28",
"@vscode/webview-ui-toolkit": "0.9.1",
"ansi-regex": "6.0.1",
"billboard.js": "3.3.2",
"chroma-js": "2.3.0",
"iconv-lite": "0.6.3",
"lodash-es": "4.17.21",

+ 1
- 0
src/constants.ts View File

@ -193,6 +193,7 @@ export const enum Commands {
ShowStashesView = 'gitlens.showStashesView',
ShowTagsView = 'gitlens.showTagsView',
ShowWorktreesView = 'gitlens.showWorktreesView',
ShowTimelinePage = 'gitlens.showTimelinePage',
ShowWelcomePage = 'gitlens.showWelcomePage',
StashApply = 'gitlens.stashApply',
StashSave = 'gitlens.stashSave',

+ 15
- 0
src/container.ts View File

@ -30,6 +30,8 @@ import { LineHoverController } from './hovers/lineHoverController';
import { Keyboard } from './keyboard';
import { Logger } from './logger';
import { SubscriptionService } from './premium/subscription/subscriptionService';
import { TimelineWebview } from './premium/webviews/timeline/timelineWebview';
import { TimelineWebviewView } from './premium/webviews/timeline/timelineWebviewView';
import { StatusBarController } from './statusbar/statusBarController';
import { Storage } from './storage';
import { executeCommand } from './system/command';
@ -171,10 +173,12 @@ export class Container {
context.subscriptions.push((this._codeLensController = new GitCodeLensController(this)));
context.subscriptions.push((this._settingsWebview = new SettingsWebview(this)));
context.subscriptions.push((this._timelineWebview = new TimelineWebview(this)));
context.subscriptions.push((this._welcomeWebview = new WelcomeWebview(this)));
context.subscriptions.push((this._rebaseEditor = new RebaseEditorProvider(this)));
context.subscriptions.push(new ViewFileDecorationProvider());
context.subscriptions.push((this._repositoriesView = new RepositoriesView(this)));
context.subscriptions.push((this._commitsView = new CommitsView(this)));
context.subscriptions.push((this._fileHistoryView = new FileHistoryView(this)));
@ -188,6 +192,7 @@ export class Container {
context.subscriptions.push((this._searchAndCompareView = new SearchAndCompareView(this)));
context.subscriptions.push((this._homeWebviewView = new HomeWebviewView(this)));
context.subscriptions.push((this._timelineView = new TimelineWebviewView(this)));
if (config.terminalLinks.enabled) {
context.subscriptions.push((this._terminalLinks = new GitTerminalLinkProvider(this)));
@ -481,6 +486,16 @@ export class Container {
return this._tagsView;
}
private _timelineView: TimelineWebviewView;
get timelineView() {
return this._timelineView;
}
private _timelineWebview: TimelineWebview;
get timelineWebview() {
return this._timelineWebview;
}
private _tracker: GitDocumentTracker;
get tracker() {
return this._tracker;

+ 2
- 0
src/features.ts View File

@ -2,6 +2,7 @@ import type { RequiredSubscriptionPlans, Subscription } from './subscription';
export const enum Features {
Stashes = 'stashes',
Timeline = 'timeline',
Worktrees = 'worktrees',
}
@ -10,5 +11,6 @@ export type FeatureAccess =
| { allowed: false; subscription: { current: Subscription; required?: RequiredSubscriptionPlans } };
export const enum PremiumFeatures {
Timeline = 'timeline',
Worktrees = 'worktrees',
}

+ 44
- 0
src/premium/webviews/timeline/protocol.ts View File

@ -0,0 +1,44 @@
import type { FeatureAccess } from '../../../features';
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
export interface State {
dataset?: Commit[];
period: Period;
title: string;
uri?: string;
dateFormat: string;
access: FeatureAccess;
}
export interface Commit {
commit: string;
author: string;
date: string;
message: string;
additions: number;
deletions: number;
sort: number;
}
export type Period = `${number}|${'D' | 'M' | 'Y'}`;
export interface DidChangeStateParams {
state: State;
}
export const DidChangeStateNotificationType = new IpcNotificationType<DidChangeStateParams>('timeline/data/didChange');
export interface OpenDataPointParams {
data?: {
id: string;
selected: boolean;
};
}
export const OpenDataPointCommandType = new IpcCommandType<OpenDataPointParams>('timeline/point/click');
export interface UpdatePeriodParams {
period: Period;
}
export const UpdatePeriodCommandType = new IpcCommandType<UpdatePeriodParams>('timeline/period/update');

+ 205
- 0
src/premium/webviews/timeline/timelineWebview.ts View File

@ -0,0 +1,205 @@
'use strict';
import { commands, ProgressLocation, TextEditor, window } from 'vscode';
import { ShowQuickCommitCommandArgs } from '../../../commands';
import { Commands } from '../../../constants';
import type { Container } from '../../../container';
import { PremiumFeatures } from '../../../features';
import { GitUri } from '../../../git/gitUri';
import { createFromDateDelta } from '../../../system/date';
import { debug } from '../../../system/decorators/log';
import { debounce } from '../../../system/function';
import { hasVisibleTextEditor, isTextEditor } from '../../../system/utils';
import { IpcMessage, onIpc } from '../../../webviews/protocol';
import { WebviewBase } from '../../../webviews/webviewBase';
import { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import {
Commit,
DidChangeStateNotificationType,
OpenDataPointCommandType,
State,
UpdatePeriodCommandType,
} from './protocol';
export class TimelineWebview extends WebviewBase<State> {
private _editor: TextEditor | undefined;
private _period: `${number}|${'D' | 'M' | 'Y'}` = '3|M';
constructor(container: Container) {
super(
container,
'gitlens.timeline',
'timeline.html',
'images/gitlens-icon.png',
'Visual File History',
Commands.ShowTimelinePage,
);
this.disposables.push(
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
window.onDidChangeActiveTextEditor(debounce(this.onActiveEditorChanged, 500), this),
);
}
protected override onReady() {
this.onActiveEditorChanged(window.activeTextEditor);
}
protected override async includeBootstrap(): Promise<State> {
return this.getState(undefined);
}
@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;
this._editor = editor;
void this.notifyDidChangeState(editor);
}
private onSubscriptionChanged(_e: SubscriptionChangeEvent) {
void this.notifyDidChangeState(this._editor);
}
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;
const repository = this.container.git.getRepository(this._editor.document.uri);
if (repository == null) return;
const commandArgs: ShowQuickCommitCommandArgs = {
repoPath: repository.path,
sha: params.data.id,
};
void commands.executeCommand(Commands.ShowQuickCommit, commandArgs);
// const commandArgs: DiffWithPreviousCommandArgs = {
// line: 0,
// showOptions: {
// preserveFocus: true,
// preview: true,
// viewColumn: ViewColumn.Beside,
// },
// };
// void commands.executeCommand(
// Commands.DiffWithPrevious,
// new GitUri(gitUri, { repoPath: gitUri.repoPath!, sha: params.data.id }),
// commandArgs,
// );
});
break;
case UpdatePeriodCommandType.method:
onIpc(UpdatePeriodCommandType, e, params => {
this._period = params.period;
void this.notifyDidChangeState(this._editor);
});
break;
}
}
@debug({ args: { 0: e => e?.document.uri.toString(true) } })
private async getState(editor: TextEditor | undefined): Promise<State> {
const access = await this.container.git.access(PremiumFeatures.Timeline);
const dateFormat = this.container.config.defaultDateFormat ?? 'MMMM Do, YYYY h:mma';
if (editor == null || !access.allowed) {
return {
period: this._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 repoPath = gitUri.repoPath!;
const title = gitUri.relativePath;
// this.setTitle(`${this.title} \u2022 ${gitUri.fileName}`);
// this.description = 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(),
}),
]);
if (log == null) {
return {
dataset: [],
period: this._period,
title: title,
uri: editor.document.uri.toString(),
dateFormat: dateFormat,
access: access,
};
}
const name = currentUser?.name ? `${currentUser.name} (you)` : 'You';
const dataset: Commit[] = [];
for (const commit of log.commits.values()) {
const stats = commit.file?.stats;
dataset.push({
author: commit.author.name === 'You' ? name : commit.author.name,
additions: stats?.additions ?? 0,
// changed: stats?.changes ?? 0,
deletions: stats?.deletions ?? 0,
commit: commit.sha,
date: commit.date.toISOString(),
message: commit.message ?? commit.summary,
sort: commit.date.getTime(),
});
}
dataset.sort((a, b) => b.sort - a.sort);
return {
dataset: dataset,
period: this._period,
title: title,
uri: editor.document.uri.toString(),
dateFormat: dateFormat,
access: access,
};
}
private getPeriodDate(): Date {
const period = this._period ?? '3|M';
const [number, unit] = period.split('|');
switch (unit) {
case 'D':
return createFromDateDelta(new Date(), { days: -parseInt(number, 10) });
case 'M':
return createFromDateDelta(new Date(), { months: -parseInt(number, 10) });
case 'Y':
return createFromDateDelta(new Date(), { years: -parseInt(number, 10) });
default:
return createFromDateDelta(new Date(), { months: -3 });
}
}
private async notifyDidChangeState(editor: TextEditor | undefined) {
if (!this.isReady) return false;
return window.withProgress({ location: ProgressLocation.Window }, async () =>
this.notify(DidChangeStateNotificationType, {
state: await this.getState(editor),
}),
);
}
}

+ 218
- 0
src/premium/webviews/timeline/timelineWebviewView.ts View File

@ -0,0 +1,218 @@
'use strict';
import { commands, Disposable, TextEditor, window } from 'vscode';
import { ShowQuickCommitCommandArgs } from '../../../commands';
import { Commands } from '../../../constants';
import { Container } from '../../../container';
import { PremiumFeatures } from '../../../features';
import { GitUri } from '../../../git/gitUri';
import { createFromDateDelta } from '../../../system/date';
import { debug } from '../../../system/decorators/log';
import { debounce } from '../../../system/function';
import { hasVisibleTextEditor, isTextEditor } from '../../../system/utils';
import { IpcMessage, onIpc } from '../../../webviews/protocol';
import { WebviewViewBase } from '../../../webviews/webviewViewBase';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import {
Commit,
DidChangeStateNotificationType,
OpenDataPointCommandType,
State,
UpdatePeriodCommandType,
} from './protocol';
export class TimelineWebviewView extends WebviewViewBase<State> {
private _editor: TextEditor | undefined;
private _period: `${number}|${'D' | 'M' | 'Y'}` = '3|M';
constructor(container: Container) {
super(container, 'gitlens.views.timeline', 'timeline.html', 'Visual File History');
this.disposables.push(this.container.subscription.onDidChange(this.onSubscriptionChanged, this));
}
override dispose() {
super.dispose();
this._disposableVisibility?.dispose();
}
protected override onReady() {
this.onActiveEditorChanged(window.activeTextEditor);
}
protected override async includeBootstrap(): Promise<State> {
return this.getState(undefined);
}
@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;
this._editor = editor;
void this.notifyDidChangeState(editor);
}
private onSubscriptionChanged(_e: SubscriptionChangeEvent) {
void this.notifyDidChangeState(this._editor);
}
private _disposableVisibility: Disposable | undefined;
protected override onVisibilityChanged(visible: boolean) {
if (visible) {
if (this._disposableVisibility == null) {
this._disposableVisibility = window.onDidChangeActiveTextEditor(
debounce(this.onActiveEditorChanged, 500),
this,
);
}
this.onActiveEditorChanged(window.activeTextEditor);
} else {
this._disposableVisibility?.dispose();
this._disposableVisibility = undefined;
this._editor = undefined;
}
}
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;
const repository = this.container.git.getRepository(this._editor.document.uri);
if (repository == null) return;
const commandArgs: ShowQuickCommitCommandArgs = {
repoPath: repository.path,
sha: params.data.id,
};
void commands.executeCommand(Commands.ShowQuickCommit, commandArgs);
// const commandArgs: DiffWithPreviousCommandArgs = {
// line: 0,
// showOptions: {
// preserveFocus: true,
// preview: true,
// viewColumn: ViewColumn.Beside,
// },
// };
// void commands.executeCommand(
// Commands.DiffWithPrevious,
// new GitUri(gitUri, { repoPath: gitUri.repoPath!, sha: params.data.id }),
// commandArgs,
// );
});
break;
case UpdatePeriodCommandType.method:
onIpc(UpdatePeriodCommandType, e, params => {
this._period = params.period;
void this.notifyDidChangeState(this._editor);
});
break;
}
}
@debug({ args: { 0: e => e?.document.uri.toString(true) } })
private async getState(editor: TextEditor | undefined): Promise<State> {
const access = await this.container.git.access(PremiumFeatures.Timeline);
const dateFormat = this.container.config.defaultDateFormat ?? 'MMMM Do, YYYY h:mma';
if (editor == null || !access.allowed) {
return {
period: this._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 repoPath = gitUri.repoPath!;
const title = gitUri.relativePath;
// this.setTitle(`${this.title} \u2022 ${gitUri.fileName}`);
this.description = 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(),
}),
]);
if (log == null) {
return {
dataset: [],
period: this._period,
title: title,
uri: editor.document.uri.toString(),
dateFormat: dateFormat,
access: access,
};
}
const name = currentUser?.name ? `${currentUser.name} (you)` : 'You';
const dataset: Commit[] = [];
for (const commit of log.commits.values()) {
const stats = commit.file?.stats;
dataset.push({
author: commit.author.name === 'You' ? name : commit.author.name,
additions: stats?.additions ?? 0,
// changed: stats?.changes ?? 0,
deletions: stats?.deletions ?? 0,
commit: commit.sha,
date: commit.date.toISOString(),
message: commit.message ?? commit.summary,
sort: commit.date.getTime(),
});
}
dataset.sort((a, b) => b.sort - a.sort);
return {
dataset: dataset,
period: this._period,
title: title,
uri: editor.document.uri.toString(),
dateFormat: dateFormat,
access: access,
};
}
private getPeriodDate(): Date {
const period = this._period ?? '3|M';
const [number, unit] = period.split('|');
switch (unit) {
case 'D':
return createFromDateDelta(new Date(), { days: -parseInt(number, 10) });
case 'M':
return createFromDateDelta(new Date(), { months: -parseInt(number, 10) });
case 'Y':
return createFromDateDelta(new Date(), { years: -parseInt(number, 10) });
default:
return createFromDateDelta(new Date(), { months: -3 });
}
}
private async notifyDidChangeState(editor: TextEditor | undefined) {
if (!this.isReady) return false;
return window.withProgress({ location: { viewId: this.id } }, async () =>
this.notify(DidChangeStateNotificationType, {
state: await this.getState(editor),
}),
);
}
}

+ 5
- 0
src/subscription.ts View File

@ -152,6 +152,11 @@ export function getTimeRemaining(
): number | undefined {
return expiresOn != null ? getDateDifference(Date.now(), new Date(expiresOn), unit) : undefined;
}
export function isSubscriptionPaid(subscription: Optional<Subscription, 'state'>): boolean {
return isSubscriptionPaidPlan(subscription.plan.effective.id);
}
export function isSubscriptionPaidPlan(id: SubscriptionPlanId): id is PaidSubscriptionPlans {
return id !== SubscriptionPlanId.Free && id !== SubscriptionPlanId.FreePlus;
}

+ 490
- 0
src/webviews/apps/premium/timeline/chart.scss View File

@ -0,0 +1,490 @@
.bb {
svg {
// font-size: 12px;
// line-height: 1;
font: 10px sans-serif;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
path,
line {
fill: none;
// stroke: #000;
}
path.domain {
.vscode-dark & {
stroke: var(--color-background--lighten-15);
}
.vscode-light & {
stroke: var(--color-background--darken-15);
}
}
text,
.bb-button {
user-select: none;
// fill: var(--color-foreground--75);
fill: var(--color-view-foreground);
font-size: 11px;
}
.bb-legend-item-tile,
.bb-xgrid-focus,
.bb-ygrid-focus,
.bb-ygrid,
.bb-event-rect,
.bb-bars path {
shape-rendering: crispEdges;
}
.bb-chart-arc {
.bb-gauge-value {
fill: #000;
}
path {
stroke: #fff;
}
rect {
stroke: #fff;
stroke-width: 1;
}
text {
fill: #fff;
font-size: 13px;
}
}
/*-- Axis --*/
.bb-axis {
shape-rendering: crispEdges;
}
// .bb-axis-y,
// .bb-axis-y2 {
// text {
// fill: var(--color-foreground--75);
// }
// }
// .bb-event-rects {
// fill-opacity: 1 !important;
// .bb-event-rect {
// fill: transparent;
// &._active_ {
// fill: rgba(39, 201, 3, 0.05);
// }
// }
// }
// .tick._active_ text {
// fill: #00c83c !important;
// }
/*-- Grid --*/
.bb-grid {
pointer-events: none;
line {
.vscode-dark & {
stroke: var(--color-background--lighten-075);
&.bb-ygrid {
stroke: var(--color-background--lighten-15);
}
}
.vscode-light & {
stroke: var(--color-background--darken-075);
&.bb-ygrid {
stroke: var(--color-background--darken-15);
}
}
}
text {
// fill: #aaa;
fill: var(--color-view-foreground);
}
}
.bb-xgrid {
stroke-dasharray: 2 2;
}
.bb-ygrid {
stroke-dasharray: 2 1;
}
.bb-xgrid-focus {
line {
.vscode-dark & {
stroke: var(--color-background--lighten-30);
}
.vscode-light & {
stroke: var(--color-background--darken-30);
}
}
}
/*-- Text on Chart --*/
.bb-text.bb-empty {
fill: #808080;
font-size: 2em;
}
/*-- Line --*/
.bb-line {
stroke-width: 1px;
}
/*-- Point --*/
.bb-circle {
// opacity: 0.8 !important;
// stroke-width: 0;
// fill-opacity: 0.6;
&._expanded_ {
// stroke-width: 1px;
// stroke: white;
// opacity: 1 !important;
fill-opacity: 1;
stroke-width: 3px;
stroke-opacity: 0.6;
}
}
.bb-selected-circle {
// fill: white;
// stroke-width: 2px;
// opacity: 1 !important;
fill-opacity: 1;
stroke-width: 3px;
stroke-opacity: 0.6;
}
// .bb-circles {
// &.bb-focused {
// opacity: 1;
// }
// &.bb-defocused {
// opacity: 0.3 !important;
// }
// }
// rect.bb-circle,
// use.bb-circle {
// &._expanded_ {
// stroke-width: 3px;
// }
// }
// .bb-selected-circle {
// stroke-width: 2px;
// .vscode-dark & {
// fill: rgba(255, 255, 255, 0.2);
// }
// .vscode-light & {
// fill: rgba(0, 0, 0, 0.1);
// }
// }
/*-- Bar --*/
.bb-bar {
stroke-width: 0;
// opacity: 0.8 !important;
// fill-opacity: 0.6;
&._expanded_ {
fill-opacity: 0.75;
// opacity: 1 !important;
// stroke-width: 3px;
// stroke-opacity: 0.6;
}
}
/*-- Candlestick --*/
.bb-candlestick {
stroke-width: 1px;
&._expanded_ {
fill-opacity: 0.75;
}
}
/*-- Focus --*/
.bb-target.bb-focused,
.bb-circles.bb-focused {
opacity: 1;
}
.bb-target.bb-focused path.bb-line,
.bb-target.bb-focused path.bb-step,
.bb-circles.bb-focused path.bb-line,
.bb-circles.bb-focused path.bb-step {
stroke-width: 2px;
}
.bb-target.bb-defocused,
.bb-circles.bb-defocused {
opacity: 0.3 !important;
}
.bb-target.bb-defocused .text-overlapping,
.bb-circles.bb-defocused .text-overlapping {
opacity: 0.05 !important;
}
/*-- Region --*/
.bb-region {
fill: steelblue;
fill-opacity: 0.1;
}
/*-- Zoom region --*/
.bb-zoom-brush {
.vscode-dark & {
fill: white;
fill-opacity: 0.2;
}
.vscode-light & {
fill: black;
fill-opacity: 0.1;
}
}
/*-- Brush --*/
.bb-brush {
.extent {
fill-opacity: 0.1;
}
}
/*-- Legend --*/
.bb-legend-item {
font-size: 12px;
user-select: none;
}
.bb-legend-item-hidden {
opacity: 0.15;
}
.bb-legend-background {
opacity: 0.75;
fill: white;
stroke: lightgray;
stroke-width: 1;
}
/*-- Title --*/
.bb-title {
font: 14px sans-serif;
}
/*-- Tooltip --*/
.bb-tooltip-container {
z-index: 10;
user-select: none;
}
.bb-tooltip {
display: flex;
border-collapse: collapse;
border-spacing: 0;
background-color: var(--color-hover-background);
color: var(--color-hover-foreground);
empty-cells: show;
opacity: 1;
box-shadow: 7px 7px 12px -9px var(--color-hover-border);
font-size: var(--font-size);
font-family: var(--font-family);
tbody {
border: 1px solid var(--color-hover-border);
}
th {
padding: 0.5rem 1rem;
text-align: left;
}
tr {
&:not(.bb-tooltip-name-additions, .bb-tooltip-name-deletions) {
display: flex;
flex-direction: column-reverse;
td {
&.name {
display: flex;
align-items: center;
font-size: 12px;
font-family: var(--editor-font-family);
background-color: var(--color-hover-statusBarBackground);
border-top: 1px solid var(--color-hover-border);
border-bottom: 1px solid var(--color-hover-border);
padding: 0.5rem;
line-height: 2rem;
&:before {
font: normal normal normal 16px/1 codicon;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
vertical-align: middle;
line-height: 2rem;
padding-right: 0.25rem;
content: '\eafc';
}
span {
display: none;
}
}
&.value {
display: flex;
padding: 0.25rem 2rem 1rem 2rem;
white-space: pre-line;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 450px;
max-height: 450px;
overflow-y: scroll;
overflow-x: hidden;
}
}
}
&.bb-tooltip-name-additions,
&.bb-tooltip-name-deletions {
display: inline-flex;
flex-direction: row-reverse;
justify-content: center;
width: calc(50% - 2rem);
margin: 0px 1rem;
td {
&.name {
text-transform: lowercase;
}
&.value {
margin-right: 0.25rem;
}
}
}
}
// td > span,
// td > svg {
// display: inline-block;
// width: 10px;
// height: 10px;
// margin-right: 6px;
// }
}
/*-- Area --*/
.bb-area {
stroke-width: 0;
opacity: 0.2;
}
/*-- Arc --*/
.bb-chart-arcs-title {
dominant-baseline: middle;
font-size: 1.3em;
}
text.bb-chart-arcs-gauge-title {
dominant-baseline: middle;
font-size: 2.7em;
}
.bb-chart-arcs .bb-chart-arcs-background {
fill: #e0e0e0;
stroke: #fff;
}
.bb-chart-arcs .bb-chart-arcs-gauge-unit {
fill: #000;
font-size: 16px;
}
.bb-chart-arcs .bb-chart-arcs-gauge-max {
fill: #777;
}
.bb-chart-arcs .bb-chart-arcs-gauge-min {
fill: #777;
}
/*-- Radar --*/
.bb-chart-radars .bb-levels polygon {
fill: none;
stroke: #848282;
stroke-width: 0.5px;
}
.bb-chart-radars .bb-levels text {
fill: #848282;
}
.bb-chart-radars .bb-axis line {
stroke: #848282;
stroke-width: 0.5px;
}
.bb-chart-radars .bb-axis text {
font-size: 1.15em;
cursor: default;
}
.bb-chart-radars .bb-shapes polygon {
fill-opacity: 0.2;
stroke-width: 1px;
}
/*-- Button --*/
.bb-button {
position: absolute;
top: 0;
right: 2rem;
border: 1px solid var(--color-button-background);
background-color: var(--color-button-background);
color: var(--color-button-foreground);
font-size: var(--font-size);
font-family: var(--font-family);
.bb-zoom-reset {
display: inline-block;
padding: 0.5rem 1rem;
cursor: pointer;
}
}
}

+ 331
- 0
src/webviews/apps/premium/timeline/chart.ts View File

@ -0,0 +1,331 @@
'use strict';
/*global*/
import { bar, bb, bubble, Chart, ChartOptions, DataItem, selection, zoom } from 'billboard.js';
// import BubbleCompare from 'billboard.js/dist/plugin/billboardjs-plugin-bubblecompare';
import { Commit, State } from '../../../../premium/webviews/timeline/protocol';
import { formatDate, fromNow } from '../../shared/date';
import { Emitter, Event } from '../../shared/events';
export interface DataPointClickEvent {
data: {
id: string;
selected: boolean;
};
}
export class TimelineChart {
private _onDidClickDataPoint = new Emitter<DataPointClickEvent>();
get onDidClickDataPoint(): Event<DataPointClickEvent> {
return this._onDidClickDataPoint.event;
}
private readonly _chart: Chart;
private _commitsByTimestamp = new Map<string, Commit>();
private _authorsByIndex = new Map<number, string>();
private _indexByAuthors = new Map<string, number>();
private _dateFormat: string = undefined!;
constructor(selector: string) {
const config: ChartOptions = {
bindto: selector,
data: {
columns: [],
types: { time: bubble(), additions: bar(), deletions: bar() },
xFormat: '%Y-%m-%dT%H:%M:%S.%LZ',
xLocaltime: false,
selection: {
enabled: selection(),
draggable: false,
grouped: false,
multiple: false,
},
onclick: this.onDataPointClicked.bind(this),
},
axis: {
x: {
type: 'timeseries',
clipPath: false,
localtime: true,
tick: {
// autorotate: true,
centered: true,
culling: false,
fit: false,
format: '%-m/%-d/%Y',
multiline: false,
// rotate: 15,
show: false,
},
},
y: {
max: 0,
padding: {
top: 50,
bottom: 100,
},
show: true,
tick: {
format: (y: number) => this._authorsByIndex.get(y) ?? '',
outer: false,
},
},
y2: {
label: {
text: 'Number of Lines Changed',
position: 'outer-middle',
},
// min: 0,
show: true,
tick: {
outer: true,
// culling: true,
// stepSize: 1,
},
},
},
bar: {
width: 2,
sensitivity: 4,
padding: 2,
},
bubble: {
maxR: 50,
},
grid: {
focus: {
edge: true,
show: true,
y: true,
},
front: true,
lines: {
front: false,
},
x: {
show: false,
},
y: {
show: true,
},
},
legend: {
show: true,
padding: 10,
},
// point: {
// r: 6,
// focus: {
// expand: {
// enabled: true,
// r: 9,
// },
// },
// select: {
// r: 12,
// },
// },
resize: {
auto: true,
},
tooltip: {
grouped: true,
format: {
title: this.getTooltipTitle.bind(this),
name: this.getTooltipName.bind(this),
value: this.getTooltipValue.bind(this),
},
// linked: true, //{ name: 'time' },
show: true,
order: 'desc',
},
zoom: {
enabled: zoom(),
type: 'drag',
rescale: true,
resetButton: true,
extent: [1, 0.01],
x: {
min: 100,
},
// onzoomstart: function(...args: any[]) {
// console.log('onzoomstart', args);
// },
// onzoom: function(...args: any[]) {
// console.log('onzoom', args);
// },
// onzoomend: function(...args: any[]) {
// console.log('onzoomend', args);
// }
},
// plugins: [
// new BubbleCompare({
// minR: 3,
// maxR: 30,
// expandScale: 1.2,
// }),
// ],
};
this._chart = bb.generate(config);
}
private onDataPointClicked(d: DataItem, _element: SVGElement) {
const commit = this._commitsByTimestamp.get(new Date(d.x).toISOString());
if (commit == null) return;
const selected = this._chart.selected(d.id) as unknown as DataItem[];
this._onDidClickDataPoint.fire({
data: {
id: commit.commit,
selected: selected?.[0]?.id === d.id,
},
});
}
reset() {
this._chart.unselect();
this._chart.unzoom();
}
updateChart(state: State) {
this._dateFormat = state.dateFormat;
this._commitsByTimestamp.clear();
this._authorsByIndex.clear();
this._indexByAuthors.clear();
if (state?.dataset == null) {
this._chart.unload();
return;
}
const xs: { [key: string]: string } = {};
const colors: { [key: string]: string } = {};
const names: { [key: string]: string } = {};
const axes: { [key: string]: string } = {};
const types: { [key: string]: string } = {};
const groups: string[][] = [];
const series: { [key: string]: any } = {};
const group = [];
let index = 0;
let commit: Commit;
let author: string;
let date: string;
let additions: number;
let deletions: number;
for (commit of state.dataset) {
({ author, date, additions, deletions } = commit);
if (!this._indexByAuthors.has(author)) {
this._indexByAuthors.set(author, index);
this._authorsByIndex.set(index, author);
index--;
}
const x = 'time';
if (series[x] == null) {
series[x] = [];
series['additions'] = [];
series['deletions'] = [];
xs['additions'] = x;
xs['deletions'] = x;
axes['additions'] = 'y2';
axes['deletions'] = 'y2';
names['additions'] = 'Additions';
names['deletions'] = 'Deletions';
colors['additions'] = 'rgba(73, 190, 71, 1)';
colors['deletions'] = 'rgba(195, 32, 45, 1)';
types['additions'] = bar();
types['deletions'] = bar();
group.push(x);
groups.push(['additions', 'deletions']);
}
const authorX = `${x}.${author}`;
if (series[authorX] == null) {
series[authorX] = [];
series[author] = [];
xs[author] = authorX;
axes[author] = 'y';
names[author] = author;
types[author] = bubble();
group.push(author, authorX);
}
series[x].push(date);
series['additions'].push(additions);
series['deletions'].push(deletions);
series[authorX].push(date);
series[author].push({ /*x: date,*/ y: this._indexByAuthors.get(author), z: additions + deletions });
this._commitsByTimestamp.set(date, commit);
}
this._chart.config('axis.y.tick.values', [...this._authorsByIndex.keys()], false);
this._chart.config('axis.y.min', index - 2, false);
groups.push(group);
this._chart.groups(groups);
const columns = Object.entries(series).map(([key, value]) => [key, ...value]);
this._chart.load({
columns: columns,
xs: xs,
axes: axes,
names: names,
colors: colors,
types: types,
unload: true,
});
}
private getTooltipName(name: string, ratio: number, id: string, index: number) {
if (id === 'additions' || /*id === 'changes' ||*/ id === 'deletions') return name;
const date = new Date(this._chart.data(id)[0].values[index].x);
const commit = this._commitsByTimestamp.get(date.toISOString());
return commit?.commit.slice(0, 8) ?? '00000000';
}
private getTooltipTitle(x: string) {
const date = new Date(x);
const formattedDate = `${capitalize(fromNow(date))} (${formatDate(date, this._dateFormat)})`;
const commit = this._commitsByTimestamp.get(date.toISOString());
if (commit == null) return formattedDate;
return `${commit.author}, ${formattedDate}`;
}
private getTooltipValue(value: any, ratio: number, id: string, index: number) {
if (id === 'additions' || /*id === 'changes' ||*/ id === 'deletions') {
return value === 0 ? undefined! : value;
}
const date = new Date(this._chart.data(id)[0].values[index].x);
const commit = this._commitsByTimestamp.get(date.toISOString());
return commit?.message ?? '???';
}
}
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}

+ 18
- 0
src/webviews/apps/premium/timeline/partials/state.free-preview-expired.html View File

@ -0,0 +1,18 @@
<template id="state:free-preview-expired">
<section>
<!-- <h3>Continue using Premium Features</h3> -->
<p data-visible="public">
The Visual File History is a premium feature, which requires a free account for public repos.
</p>
<p data-visible="private">
The Visual File History is a premium feature, which requires at least a Pro subscription for private repos.
</p>
<p>Create a free account to continue trialing premium features for all code for an additional 7 days.</p>
<vscode-button data-action="command:gitlens.premium.learn">Learn about premium features</vscode-button>
<vscode-button data-action="command:gitlens.premium.loginOrSignUp">Create a free account</vscode-button>
<span class="button-subaction"
>Have an existing account? <a href="command:gitlens.premium.loginOrSignUp">Sign in</a></span
>
<vscode-button data-action="command:gitlens.premium.purchase">Purchase a plan</vscode-button>
</section>
</template>

+ 20
- 0
src/webviews/apps/premium/timeline/partials/state.free.html View File

@ -0,0 +1,20 @@
<template id="state:free">
<section>
<!-- <h3>Try GitLens Premium Features</h3> -->
<p data-visible="public">
The Visual File History is a premium feature, which requires a free account for public repos.
</p>
<p data-visible="private">
The Visual File History is a premium feature, which requires at least a Pro subscription for private repos.
</p>
<vscode-button data-action="command:gitlens.premium.learn">Learn about premium features</vscode-button>
<vscode-button data-action="command:gitlens.premium.startPreviewTrial">Try premium features now</vscode-button>
<vscode-button data-action="command:gitlens.premium.loginOrSignUp">Create a free account</vscode-button>
<span class="button-subaction"
>Have an existing account? <a href="command:gitlens.premium.loginOrSignUp">Sign in</a></span
>
<vscode-button data-action="command:gitlens.premium.showPlans" appearance="secondary"
>View paid plans</vscode-button
>
</section>
</template>

+ 10
- 0
src/webviews/apps/premium/timeline/partials/state.plus-trial-expired.html View File

@ -0,0 +1,10 @@
<template id="state:plus-trial-expired">
<section>
<!-- <h3>GitLens Free+</h3> -->
<p>
The Visual File History is a premium feature, which requires at least a Pro subscription for private repos.
</p>
<vscode-button data-action="command:gitlens.premium.learn">Learn about premium features</vscode-button>
<vscode-button data-action="command:gitlens.premium.purchase">Purchase a plan</vscode-button>
</section>
</template>

+ 11
- 0
src/webviews/apps/premium/timeline/partials/state.verify-email.html View File

@ -0,0 +1,11 @@
<template id="state:verify-email">
<section>
<!-- <h3>Please verify your email</h3> -->
<p>To continue using premium GitLens features, please verify the email for the account you created.</p>
<vscode-button data-action="command:gitlens.premium.resendVerification"
>Resend verification email</vscode-button
>
<vscode-button data-action="command:gitlens.premium.validate">Refresh verification status</vscode-button>
<p>All non-premium features will continue to be free without an account.</p>
</section>
</template>

+ 5
- 0
src/webviews/apps/premium/timeline/plugins.d.ts View File

@ -0,0 +1,5 @@
declare module 'billboard.js/dist/plugin/billboardjs-plugin-bubblecompare' {
import BubbleCompare from 'billboard.js/types/plugin/bubblecompare';
export = BubbleCompare;
}

+ 59
- 0
src/webviews/apps/premium/timeline/timeline.html View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body class="preload">
<div class="container">
<section id="header">
<h2 data-bind="title"></h2>
<h2 data-bind="description"></h2>
<div class="select-container">
<label for="periods">Since</label>
<vscode-dropdown id="periods" name="periods" position="below">
<vscode-option value="7|D">1 week ago</vscode-option>
<vscode-option value="1|M">1 month ago</vscode-option>
<vscode-option value="3|M" selected="true">3 months ago</vscode-option>
<vscode-option value="6|M">6 months ago</vscode-option>
<vscode-option value="9|M">9 months ago</vscode-option>
<vscode-option value="1|Y">1 year ago</vscode-option>
<vscode-option value="2|Y">2 years ago</vscode-option>
<vscode-option value="4|Y">4 years ago</vscode-option>
</vscode-dropdown>
</div>
</section>
<section id="content">
<div id="chart"></div>
</section>
<section id="footer"></section>
</div>
<div id="overlay">
<p>
The
<a href="command:gitlens.openWalkthrough?%22gitlens.premium%7Cgitlens.premium.visualFileHistory%22"
>Visual File History</a
>
allows you to clearly see the history of a file, including: when changes were made, how large they were,
and who made them. Authors who have contributed changes to a file are on one axis, and dates on the
other. Color-coded dots represent points in time where that author made changes to the file, and
vertical lines represent the magnitude of the change in two colors: green for lines added, and red for
lines removed.
</p>
<div id="overlay-slot"></div>
</div>
#{endOfBody}
<style nonce="#{cspNonce}">
@font-face {
font-family: 'codicon';
src: url('#{root}/dist/webviews/codicon.ttf?669d352dfabff8f6eaa466c8ae820e43') format('truetype');
}
</style>
</body>
<!-- prettier-ignore -->
<%= require('html-loader?{"esModule":false}!./partials/state.free.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.free-preview-expired.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.plus-trial-expired.html') %>
<%= require('html-loader?{"esModule":false}!./partials/state.verify-email.html') %>
</html>

+ 165
- 0
src/webviews/apps/premium/timeline/timeline.scss View File

@ -0,0 +1,165 @@
* {
box-sizing: border-box;
}
html {
height: 100%;
font-size: 62.5%;
}
body {
background-color: var(--color-background);
color: var(--color-view-foreground);
font-family: var(--font-family);
height: 100%;
line-height: 1.4;
font-size: 100% !important;
overflow: hidden;
margin: 0 20px 20px 20px;
padding: 0;
}
.container {
display: grid;
grid-template-rows: min-content 1fr min-content;
min-height: 100%;
// width: 100%;
}
section {
display: flex;
flex-direction: column;
padding: 0;
}
h2 {
font-weight: 400;
&[data-bind='title'] {
flex: auto 0 1;
}
&[data-bind='description'] {
flex: auto 1 1;
font-size: 1.3em;
font-weight: 200;
margin-left: 1.5rem;
opacity: 0.7;
}
}
h3 {
border: none;
color: var(--color-view-header-foreground);
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0;
white-space: nowrap;
}
h4 {
font-size: 1.5rem;
font-weight: 400;
margin: 0.5rem 0 1rem 0;
}
a {
text-decoration: none;
&:focus {
outline-color: var(--focus-border);
}
&:hover {
text-decoration: underline;
}
}
b {
font-weight: 600;
}
p {
margin-bottom: 0;
}
vscode-button {
align-self: center;
margin-top: 1.5rem;
max-width: 300px;
width: 100%;
}
span.button-subaction {
align-self: center;
margin-top: 0.75rem;
}
@media (min-width: 640px) {
vscode-button {
align-self: flex-start;
}
span.button-subaction {
align-self: flex-start;
}
}
section#header {
display: flex;
flex-direction: row;
align-items: baseline;
@media all and (max-width: 500px) {
flex-wrap: wrap;
}
}
.select-container {
display: flex;
align-items: center;
justify-content: flex-end;
margin: 1em;
flex: 100% 0 1;
label {
margin-right: 1em;
font-size: var(--font-size);
}
}
// .chart-container {
// height: calc(100% - 0.5em);
// width: 100%;
// overflow: hidden;
// }
#chart {
height: 100%;
width: 100%;
}
[data-visible] {
display: none;
}
#overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-background);
font-size: 1.3em;
min-height: 100%;
padding: 0 2rem 2rem 2rem;
display: none;
grid-template-rows: min-content;
&.subscription-required {
display: grid;
}
}
@import './chart';

+ 171
- 0
src/webviews/apps/premium/timeline/timeline.ts View File

@ -0,0 +1,171 @@
'use strict';
/*global*/
import './timeline.scss';
import { provideVSCodeDesignSystem, vsCodeButton, vsCodeDropdown, vsCodeOption } from '@vscode/webview-ui-toolkit';
import {
DidChangeStateNotificationType,
OpenDataPointCommandType,
State,
UpdatePeriodCommandType,
} from '../../../../premium/webviews/timeline/protocol';
import { SubscriptionPlanId, SubscriptionState } from '../../../../subscription';
import { ExecuteCommandType, IpcMessage, onIpc } from '../../../protocol';
import { App } from '../../shared/appBase';
import { DOM } from '../../shared/dom';
import { DataPointClickEvent, TimelineChart } from './chart';
export class TimelineApp extends App<State> {
private _chart: TimelineChart | undefined;
constructor() {
super('TimelineApp');
}
protected override onInitialize() {
provideVSCodeDesignSystem().register({
register: function (container: any, context: any) {
vsCodeButton().register(container, context);
vsCodeDropdown().register(container, context);
vsCodeOption().register(container, context);
},
});
this.updateState();
}
protected override onBind() {
const disposables = super.onBind?.() ?? [];
disposables.push(
DOM.on('[data-action]', 'click', (e, target: HTMLElement) => this.onActionClicked(e, target)),
DOM.on(document, 'keydown', (e: KeyboardEvent) => this.onKeyDown(e)),
DOM.on(document.getElementById('periods')! as HTMLSelectElement, 'change', (e, target) =>
this.onPeriodChanged(e, target),
),
);
return disposables;
}
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data as IpcMessage;
switch (msg.method) {
case DidChangeStateNotificationType.method:
this.log(`${this.appName}.onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeStateNotificationType, msg, params => {
this.state = params.state;
this.updateState();
});
break;
default:
super.onMessageReceived?.(e);
}
}
private onActionClicked(e: MouseEvent, target: HTMLElement) {
const action = target.dataset.action;
if (action?.startsWith('command:')) {
this.sendCommand(ExecuteCommandType, { command: action.slice(8) });
}
}
private onChartDataPointClicked(e: DataPointClickEvent) {
this.sendCommand(OpenDataPointCommandType, e);
}
private onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape' || e.key === 'Esc') {
this._chart?.reset();
}
}
private onPeriodChanged(e: Event, element: HTMLSelectElement) {
const value = element.options[element.selectedIndex].value;
assertPeriod(value);
this.log(`${this.appName}.onPeriodChanged: name=${element.name}, value=${value}`);
this.sendCommand(UpdatePeriodCommandType, { period: value });
}
private updateState(): void {
const $overlay = document.getElementById('overlay') as HTMLDivElement;
$overlay.classList.toggle('subscription-required', !this.state.access.allowed);
const $slot = document.getElementById('overlay-slot') as HTMLDivElement;
if (!this.state.access.allowed) {
const { current: subscription, required } = this.state.access.subscription;
const requiresPublic = required === SubscriptionPlanId.FreePlus;
const options = { visible: { public: requiresPublic, private: !requiresPublic } };
if (subscription.account?.verified === false) {
DOM.insertTemplate('state:verify-email', $slot, options);
return;
}
switch (subscription.state) {
case SubscriptionState.Free:
DOM.insertTemplate('state:free', $slot, options);
break;
case SubscriptionState.FreePreviewExpired:
DOM.insertTemplate('state:free-preview-expired', $slot, options);
break;
case SubscriptionState.FreePlusTrialExpired:
DOM.insertTemplate('state:plus-trial-expired', $slot, options);
break;
}
return;
}
$slot.innerHTML = '';
if (this._chart == null) {
this._chart = new TimelineChart('#chart');
this._chart.onDidClickDataPoint(this.onChartDataPointClicked, this);
}
let { title } = this.state;
let description = '';
const index = title.lastIndexOf('/');
if (index >= 0) {
const name = title.substring(index + 1);
description = title.substring(0, index);
title = name;
}
for (const [key, value] of Object.entries({ title: title, description: description })) {
const $el = document.querySelector(`[data-bind="${key}"]`);
if ($el != null) {
$el.textContent = String(value);
}
}
const $periods = document.getElementById('periods') as HTMLSelectElement;
if ($periods != null) {
const period = this.state?.period;
for (let i = 0, len = $periods.options.length; i < len; ++i) {
if ($periods.options[i].value === period) {
$periods.selectedIndex = i;
break;
}
}
}
this._chart.updateChart(this.state);
}
}
function assertPeriod(period: string): asserts period is `${number}|${'D' | 'M' | 'Y'}` {
const [value, unit] = period.split('|');
if (isNaN(Number(value)) || (unit !== 'D' && unit !== 'M' && unit !== 'Y')) {
throw new Error(`Invalid period: ${period}`);
}
}
new TimelineApp();

+ 22
- 16
src/webviews/apps/shared/theme.ts View File

@ -8,22 +8,19 @@ export function initializeAndWatchThemeColors() {
const bodyStyle = body.style;
const font = computedStyle.getPropertyValue('--vscode-font-family').trim();
if (font) {
bodyStyle.setProperty('--font-family', font);
bodyStyle.setProperty('--font-size', computedStyle.getPropertyValue('--vscode-font-size').trim());
bodyStyle.setProperty('--font-weight', computedStyle.getPropertyValue('--vscode-font-weight').trim());
} else {
bodyStyle.setProperty(
'--font-family',
computedStyle.getPropertyValue('--vscode-editor-font-family').trim(),
);
bodyStyle.setProperty('--font-size', computedStyle.getPropertyValue('--vscode-editor-font-size').trim());
bodyStyle.setProperty(
'--font-weight',
computedStyle.getPropertyValue('--vscode-editor-font-weight').trim(),
);
}
bodyStyle.setProperty('--font-family', computedStyle.getPropertyValue('--vscode-font-family').trim());
bodyStyle.setProperty('--font-size', computedStyle.getPropertyValue('--vscode-font-size').trim());
bodyStyle.setProperty('--font-weight', computedStyle.getPropertyValue('--vscode-font-weight').trim());
bodyStyle.setProperty(
'--editor-font-family',
computedStyle.getPropertyValue('--vscode-editor-font-family').trim(),
);
bodyStyle.setProperty('--editor-font-size', computedStyle.getPropertyValue('--vscode-editor-font-size').trim());
bodyStyle.setProperty(
'--editor-font-weight',
computedStyle.getPropertyValue('--vscode-editor-font-weight').trim(),
);
let color = computedStyle.getPropertyValue('--vscode-editor-background').trim();
bodyStyle.setProperty('--color-background', color);
@ -78,6 +75,15 @@ export function initializeAndWatchThemeColors() {
color = computedStyle.getPropertyValue('--vscode-sideBarSectionHeader-foreground').trim();
bodyStyle.setProperty('--color-view-header-foreground', color);
color = computedStyle.getPropertyValue('--vscode-editorHoverWidget-background').trim();
bodyStyle.setProperty('--color-hover-background', color);
color = computedStyle.getPropertyValue('--vscode-editorHoverWidget-border').trim();
bodyStyle.setProperty('--color-hover-border', color);
color = computedStyle.getPropertyValue('--vscode-editorHoverWidget-foreground').trim();
bodyStyle.setProperty('--color-hover-foreground', color);
color = computedStyle.getPropertyValue('--vscode-editorHoverWidget-statusBarBackground').trim();
bodyStyle.setProperty('--color-hover-statusBarBackground', color);
};
const observer = new MutationObserver(onColorThemeChanged);

+ 1
- 0
src/webviews/protocol.ts View File

@ -76,6 +76,7 @@ export interface DidGenerateConfigurationPreviewParams {
completionId: string;
preview: string;
}
export const DidGenerateConfigurationPreviewNotificationType =
new IpcNotificationType<DidGenerateConfigurationPreviewParams>('configuration/didPreview');

+ 3
- 1
webpack.config.js View File

@ -260,6 +260,7 @@ function getWebviewsConfig(mode, env) {
getHtmlPlugin('home', false, mode, env),
getHtmlPlugin('rebase', false, mode, env),
getHtmlPlugin('settings', false, mode, env),
getHtmlPlugin('timeline', true, mode, env),
getHtmlPlugin('welcome', false, mode, env),
getCspHtmlPlugin(mode, env),
new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
@ -302,6 +303,7 @@ function getWebviewsConfig(mode, env) {
home: './home/home.ts',
rebase: './rebase/rebase.ts',
settings: './settings/settings.ts',
timeline: './premium/timeline/timeline.ts',
welcome: './welcome/welcome.ts',
},
mode: mode,
@ -435,7 +437,7 @@ function getCspHtmlPlugin(mode, env) {
mode !== 'production'
? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
: ['#{cspSource}', "'nonce-#{cspNonce}'"],
'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'"],
'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-hashes'"],
'font-src': ['#{cspSource}'],
},
{

+ 170
- 6
yarn.lock View File

@ -872,6 +872,24 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
billboard.js@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/billboard.js/-/billboard.js-3.3.2.tgz#c5ba3b946aa4e98ad22e2aabafa4cec5042b2630"
integrity sha512-uqNQy8rb/wrt6Yscign8ILh/VsBr55P4WXJugQMNHF1ksSE3kRBMj1Bz3ac7o6hB3O94ztQhbmHcS5evMTpvtA==
dependencies:
d3-axis "^3.0.0"
d3-brush "^3.0.0"
d3-drag "^3.0.0"
d3-dsv "^3.0.1"
d3-ease "^3.0.1"
d3-interpolate "^3.0.1"
d3-scale "^4.0.2"
d3-selection "^3.0.0"
d3-shape "^3.1.0"
d3-time-format "^4.1.0"
d3-transition "^3.0.1"
d3-zoom "^3.0.0"
bin-build@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bin-build/-/bin-build-3.0.0.tgz#c5780a25a8a9f966d8244217e6c1f5082a143861"
@ -1270,6 +1288,11 @@ colorette@^2.0.14:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==
commander@7, commander@^7.0.0, commander@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@8.3.0, commander@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
@ -1285,11 +1308,6 @@ commander@^6.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commander@^7.0.0, commander@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -1450,6 +1468,142 @@ cwebp-bin@^7.0.1:
bin-build "^3.0.0"
bin-wrapper "^4.0.1"
"d3-array@2 - 3", "d3-array@2.10.0 - 3":
version "3.1.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.1.tgz#7797eb53ead6b9083c75a45a681e93fc41bc468c"
integrity sha512-33qQ+ZoZlli19IFiQx4QEpf2CBEayMRzhlisJHSCsSUbDXv6ZishqS1x7uFVClKG4Wr7rZVHvaAttoLow6GqdQ==
dependencies:
internmap "1 - 2"
d3-axis@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
d3-brush@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
dependencies:
d3-dispatch "1 - 3"
d3-drag "2 - 3"
d3-interpolate "1 - 3"
d3-selection "3"
d3-transition "3"
"d3-color@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a"
integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw==
"d3-dispatch@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
"d3-drag@2 - 3", d3-drag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
dependencies:
d3-dispatch "1 - 3"
d3-selection "3"
d3-dsv@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
dependencies:
commander "7"
iconv-lite "0.6"
rw "1"
"d3-ease@1 - 3", d3-ease@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
"d3-format@1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
"d3-path@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e"
integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==
d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
dependencies:
d3-array "2.10.0 - 3"
d3-format "1 - 3"
d3-interpolate "1.2.0 - 3"
d3-time "2.1.1 - 3"
d3-time-format "2 - 4"
"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
d3-shape@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556"
integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==
dependencies:
d3-path "1 - 3"
"d3-time-format@2 - 4", d3-time-format@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
dependencies:
d3-time "1 - 3"
"d3-time@1 - 3", "d3-time@2.1.1 - 3":
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975"
integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==
dependencies:
d3-array "2 - 3"
"d3-timer@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
"d3-transition@2 - 3", d3-transition@3, d3-transition@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
dependencies:
d3-color "1 - 3"
d3-dispatch "1 - 3"
d3-ease "1 - 3"
d3-interpolate "1 - 3"
d3-timer "1 - 3"
d3-zoom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
dependencies:
d3-dispatch "1 - 3"
d3-drag "2 - 3"
d3-interpolate "1 - 3"
d3-selection "2 - 3"
d3-transition "2 - 3"
date-fns@^2.16.1:
version "2.28.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
@ -3011,7 +3165,7 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
iconv-lite@0.6.3:
iconv-lite@0.6, iconv-lite@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@ -3136,6 +3290,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
interpret@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
@ -4962,6 +5121,11 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
rw@1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=
rxjs@^6.6.3:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"

Loading…
Cancel
Save