Browse Source

Adds experimental activity minibar on Commit Graph

Co-authored-by: Keith Daulton <keith.daulton@gitkraken.com>
main
Eric Amodio 2 years ago
parent
commit
6119fc059f
14 changed files with 1390 additions and 9 deletions
  1. +7
    -0
      package.json
  2. +5
    -0
      src/config.ts
  3. +3
    -0
      src/env/node/git/localGitProvider.ts
  4. +1
    -0
      src/git/models/graph.ts
  5. +16
    -2
      src/plus/github/githubGitProvider.ts
  6. +42
    -2
      src/plus/webviews/graph/graphWebview.ts
  7. +13
    -0
      src/plus/webviews/graph/protocol.ts
  8. +37
    -0
      src/system/date.ts
  9. +10
    -1
      src/system/iterable.ts
  10. +862
    -0
      src/webviews/apps/plus/activity/activity-minibar.ts
  11. +8
    -0
      src/webviews/apps/plus/activity/react.tsx
  12. +257
    -0
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  13. +11
    -0
      src/webviews/apps/plus/graph/graph.scss
  14. +118
    -4
      src/webviews/apps/plus/graph/graph.tsx

+ 7
- 0
package.json View File

@ -2308,6 +2308,13 @@
"markdownDescription": "Specifies whether to show the _Commit Graph_ in the status bar",
"scope": "window",
"order": 100
},
"gitlens.graph.experimental.activityMinibar.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "Specifies whether to show an activity minibar above the _Commit Graph_",
"scope": "window",
"order": 100
}
}
},

+ 5
- 0
src/config.ts View File

@ -388,6 +388,11 @@ export interface GraphConfig {
dateStyle: DateStyle | null;
defaultItemLimit: number;
dimMergeCommits: boolean;
experimental: {
activityMinibar: {
enabled: boolean;
};
};
highlightRowsOnRefHover: boolean;
scrollRowPadding: number;
showDetailsView: 'open' | 'selection' | false;

+ 3
- 0
src/env/node/git/localGitProvider.ts View File

@ -1722,6 +1722,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
repoPath: repoPath,
avatars: avatars,
ids: ids,
includes: options?.include,
branches: branchMap,
remotes: remoteMap,
rows: [],
@ -1742,6 +1743,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
repoPath: repoPath,
avatars: avatars,
ids: ids,
includes: options?.include,
branches: branchMap,
remotes: remoteMap,
rows: [],
@ -2060,6 +2062,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
repoPath: repoPath,
avatars: avatars,
ids: ids,
includes: options?.include,
skippedIds: skippedIds,
branches: branchMap,
remotes: remoteMap,

+ 1
- 0
src/git/models/graph.ts View File

@ -31,6 +31,7 @@ export interface GitGraph {
readonly avatars: Map<string, string>;
/** A set of all "seen" commit ids */
readonly ids: Set<string>;
readonly includes: { stats?: boolean } | undefined;
/** A set of all skipped commit ids -- typically for stash index/untracked commits */
readonly skippedIds?: Set<string>;
readonly branches: Map<string, GitBranch>;

+ 16
- 2
src/plus/github/githubGitProvider.ts View File

@ -40,7 +40,7 @@ import { GitUri } from '../../git/gitUri';
import type { GitBlame, GitBlameAuthor, GitBlameLine, GitBlameLines } from '../../git/models/blame';
import type { BranchSortOptions } from '../../git/models/branch';
import { getBranchId, GitBranch, sortBranches } from '../../git/models/branch';
import type { GitCommitLine } from '../../git/models/commit';
import type { GitCommitLine, GitCommitStats } from '../../git/models/commit';
import { getChangedFilesCount, GitCommit, GitCommitIdentity } from '../../git/models/commit';
import { GitContributor } from '../../git/models/contributor';
import type { GitDiff, GitDiffFilter, GitDiffHunkLine, GitDiffShortStat } from '../../git/models/diff';
@ -1116,12 +1116,13 @@ export class GitHubGitProvider implements GitProvider, Disposable {
ids: Set<string>,
options?: {
branch?: string;
include?: { stats?: boolean };
limit?: number;
mode?: 'single' | 'local' | 'all';
ref?: string;
useAvatars?: boolean;
},
): Promise<GitGraph> {
const includes = { ...options?.include, stats: true }; // stats are always available, so force it
const branchMap = branch != null ? new Map([[branch.name, branch]]) : new Map<string, GitBranch>();
const remoteMap = remote != null ? new Map([[remote.name, remote]]) : new Map<string, GitRemote>();
if (log == null) {
@ -1129,6 +1130,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
repoPath: repoPath,
avatars: avatars,
ids: ids,
includes: includes,
branches: branchMap,
remotes: remoteMap,
rows: [],
@ -1141,6 +1143,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
repoPath: repoPath,
avatars: avatars,
ids: ids,
includes: includes,
branches: branchMap,
remotes: remoteMap,
rows: [],
@ -1155,6 +1158,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
let refRemoteHeads: GitGraphRowRemoteHead[];
let refTags: GitGraphRowTag[];
let contexts: GitGraphRowContexts | undefined;
let stats: GitCommitStats | undefined;
const hasHeadShaAndRemote = branch?.sha != null && remote != null;
@ -1272,6 +1276,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
}),
};
stats = commit.stats;
rows.push({
sha: commit.sha,
parents: commit.parents,
@ -1285,6 +1290,14 @@ export class GitHubGitProvider implements GitProvider, Disposable {
remotes: refRemoteHeads,
tags: refTags,
contexts: contexts,
stats:
stats != null
? {
files: getChangedFilesCount(stats.changedFiles),
additions: stats.additions,
deletions: stats.deletions,
}
: undefined,
});
}
@ -1298,6 +1311,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
repoPath: repoPath,
avatars: avatars,
ids: ids,
includes: includes,
branches: branchMap,
remotes: remoteMap,
rows: rows,

+ 42
- 2
src/plus/webviews/graph/graphWebview.ts View File

@ -112,6 +112,7 @@ import type {
State,
UpdateColumnsParams,
UpdateExcludeTypeParams,
UpdateGraphConfigurationParams,
UpdateRefsVisibilityParams,
UpdateSelectionParams,
} from './protocol';
@ -144,6 +145,7 @@ import {
supportedRefMetadataTypes,
UpdateColumnsCommandType,
UpdateExcludeTypeCommandType,
UpdateGraphConfigurationCommandType,
UpdateIncludeOnlyRefsCommandType,
UpdateRefsVisibilityCommandType,
UpdateSelectionCommandType,
@ -451,6 +453,9 @@ export class GraphWebview extends WebviewBase {
case UpdateColumnsCommandType.method:
onIpc(UpdateColumnsCommandType, e, params => this.onColumnsChanged(params));
break;
case UpdateGraphConfigurationCommandType.method:
onIpc(UpdateGraphConfigurationCommandType, e, params => this.updateGraphConfig(params));
break;
case UpdateRefsVisibilityCommandType.method:
onIpc(UpdateRefsVisibilityCommandType, e, params => this.onRefsVisibilityChanged(params));
break;
@ -467,6 +472,27 @@ export class GraphWebview extends WebviewBase {
break;
}
}
updateGraphConfig(params: UpdateGraphConfigurationParams) {
const config = this.getComponentConfig();
let key: keyof UpdateGraphConfigurationParams['changes'];
for (key in params.changes) {
if (config[key] !== params.changes[key]) {
switch (key) {
case 'activityMinibar':
void configuration.updateEffective(
'graph.experimental.activityMinibar.enabled',
params.changes[key],
);
break;
default:
// TODO:@eamodio add more config options as needed
debugger;
break;
}
}
}
}
protected override onFocusChanged(focused: boolean): void {
if (!focused || this.activeSelection == null || !this.container.commitDetailsView.visible) {
@ -559,9 +585,18 @@ export class GraphWebview extends WebviewBase {
configuration.changed(e, 'graph.showGhostRefsOnRowHover') ||
configuration.changed(e, 'graph.pullRequests.enabled') ||
configuration.changed(e, 'graph.showRemoteNames') ||
configuration.changed(e, 'graph.showUpstreamStatus')
configuration.changed(e, 'graph.showUpstreamStatus') ||
configuration.changed(e, 'graph.experimental.activityMinibar.enabled')
) {
void this.notifyDidChangeConfiguration();
if (
configuration.changed(e, 'graph.experimental.activityMinibar.enabled') &&
configuration.get('graph.experimental.activityMinibar.enabled') &&
!this._graph?.includes?.stats
) {
this.updateState();
}
}
}
@ -1563,6 +1598,7 @@ export class GraphWebview extends WebviewBase {
private getComponentConfig(): GraphComponentConfig {
const config: GraphComponentConfig = {
activityMinibar: configuration.get('graph.experimental.activityMinibar.enabled'),
avatars: configuration.get('graph.avatars'),
dateFormat:
configuration.get('graph.dateFormat') ?? configuration.get('defaultDateFormat') ?? 'short+short',
@ -1666,7 +1702,11 @@ export class GraphWebview extends WebviewBase {
const dataPromise = this.container.git.getCommitsForGraph(
this.repository.path,
this._panel!.webview.asWebviewUri.bind(this._panel!.webview),
{ limit: limit, ref: ref },
{
include: { stats: configuration.get('graph.experimental.activityMinibar.enabled') },
limit: limit,
ref: ref,
},
);
// Check for GitLens+ access and working tree stats

+ 13
- 0
src/plus/webviews/graph/protocol.ts View File

@ -84,7 +84,12 @@ export interface State {
debugging: boolean;
// Props below are computed in the webview (not passed)
activeDay?: number;
activeRow?: string;
visibleDays?: {
top: number;
bottom: number;
};
theming?: { cssVariables: CssVariables; themeOpacityFactor: number };
}
@ -123,6 +128,7 @@ export type GraphTag = Tag;
export type GraphBranch = Head;
export interface GraphComponentConfig {
activityMinibar?: boolean;
avatars?: boolean;
dateFormat: DateTimeFormat | string;
dateStyle: DateStyle;
@ -240,6 +246,13 @@ export const UpdateExcludeTypeCommandType = new IpcCommandType
'graph/fitlers/update/excludeType',
);
export interface UpdateGraphConfigurationParams {
changes: { [key in keyof GraphComponentConfig]?: GraphComponentConfig[key] };
}
export const UpdateGraphConfigurationCommandType = new IpcCommandType<UpdateGraphConfigurationParams>(
'graph/configuration/update',
);
export interface UpdateIncludeOnlyRefsParams {
refs?: GraphIncludeOnlyRef[];
}

+ 37
- 0
src/system/date.ts View File

@ -22,6 +22,8 @@ let defaultLocales: string[] | undefined;
let defaultRelativeTimeFormat: InstanceType<typeof Intl.RelativeTimeFormat> | undefined;
let defaultShortRelativeTimeFormat: InstanceType<typeof Intl.RelativeTimeFormat> | undefined;
const numberFormatCache = new Map<string | undefined, Intl.NumberFormat>();
export function setDefaultDateLocales(locales: string | string[] | null | undefined) {
if (typeof locales === 'string') {
if (locales === 'system') {
@ -32,9 +34,13 @@ export function setDefaultDateLocales(locales: string | string[] | null | undefi
} else {
defaultLocales = locales ?? undefined;
}
defaultRelativeTimeFormat = undefined;
defaultShortRelativeTimeFormat = undefined;
dateTimeFormatCache.clear();
numberFormatCache.clear();
locale = undefined;
}
@ -346,3 +352,34 @@ function formatWithOrdinal(n: number): string {
const v = n % 100;
return `${n}${ordinals[(v - 20) % 10] ?? ordinals[v] ?? ordinals[0]}`;
}
export function formatNumeric(
value: number,
style?: 'decimal' | 'currency' | 'percent' | 'unit' | null | undefined,
locale?: string,
): string {
if (style == null) {
style = 'decimal';
}
const key = `${locale ?? ''}:${style}`;
let formatter = numberFormatCache.get(key);
if (formatter == null) {
const options: Intl.NumberFormatOptions = { localeMatcher: 'best fit', style: style };
let locales;
if (locale == null) {
locales = defaultLocales;
} else if (locale === 'system') {
locales = undefined;
} else {
locales = [locale];
}
formatter = new Intl.NumberFormat(locales, options);
numberFormatCache.set(key, formatter);
}
return formatter.format(value);
}

+ 10
- 1
src/system/iterable.ts View File

@ -107,6 +107,15 @@ export function find(source: Iterable | IterableIterator, predicate: (i
return null;
}
export function findIndex<T>(source: Iterable<T> | IterableIterator<T>, predicate: (item: T) => boolean): number {
let i = 0;
for (const item of source) {
if (predicate(item)) return i;
i++;
}
return -1;
}
export function first<T>(source: Iterable<T> | IterableIterator<T>): T | undefined {
return source[Symbol.iterator]().next().value as T | undefined;
}
@ -114,7 +123,7 @@ export function first(source: Iterable | IterableIterator): T | undefin
export function* flatMap<T, TMapped>(
source: Iterable<T> | IterableIterator<T>,
mapper: (item: T) => Iterable<TMapped>,
): Iterable<TMapped> {
): IterableIterator<TMapped> {
for (const item of source) {
yield* mapper(item);
}

+ 862
- 0
src/webviews/apps/plus/activity/activity-minibar.ts View File

@ -0,0 +1,862 @@
import { css, customElement, FASTElement, html, observable, ref } from '@microsoft/fast-element';
import type { Chart, DataItem, RegionOptions } from 'billboard.js';
import { bb, selection, spline, zoom } from 'billboard.js';
import { groupByMap } from '../../../../system/array';
import { first, flatMap, map, some, union } from '../../../../system/iterable';
import { pluralize } from '../../../../system/string';
import { formatDate, formatNumeric, fromNow } from '../../shared/date';
export interface BranchMarker {
type: 'branch';
name: string;
current?: boolean;
}
export interface RemoteMarker {
type: 'remote';
name: string;
current?: boolean;
}
export interface TagMarker {
type: 'tag';
name: string;
current?: undefined;
}
export type ActivityMarker = BranchMarker | RemoteMarker | TagMarker;
export interface ActivitySearchResultMarker {
type: 'search-result';
sha: string;
}
export interface ActivityStats {
commits: number;
activity?: { additions: number; deletions: number };
files?: number;
sha?: string;
}
export type ActivityStatsSelectedEvent = CustomEvent<ActivityStatsSelectedEventDetail>;
export interface ActivityStatsSelectedEventDetail {
date: Date;
sha?: string;
}
const template = html<ActivityMinibar>`<template>
<div id="chart" ${ref('chart')}></div>
</template>`;
const styles = css`
:host {
display: flex;
position: relative;
width: 100%;
min-height: 24px;
height: 40px;
background: var(--color-background);
z-index: 2000;
}
#chart {
height: 100%;
width: 100%;
overflow: hidden;
position: initial !important;
}
.bb svg {
font: 10px var(--font-family);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
transform: translateX(2.5em) rotateY(180deg);
}
.bb-chart {
width: 100%;
height: 100%;
}
.bb-event-rect {
height: calc(100% + 2px);
transform: translateY(-5px);
}
/*-- Grid --*/
.bb-xgrid-focus line {
stroke: var(--color-activityMinibar-focusLine);
}
/*-- Line --*/
.bb path,
.bb line {
fill: none;
}
/*-- Point --*/
.bb-circle._expanded_ {
fill: var(--color-background);
opacity: 1 !important;
fill-opacity: 1 !important;
stroke-opacity: 1 !important;
stroke-width: 1px;
}
.bb-selected-circle {
fill: var(--color-background);
opacity: 1 !important;
fill-opacity: 1 !important;
stroke-opacity: 1 !important;
stroke-width: 2px;
}
/*-- Bar --*/
.bb-bar {
stroke-width: 0;
}
.bb-bar._expanded_ {
fill-opacity: 0.75;
}
/*-- Regions --*/
.bb-region.visible-area {
fill: var(--color-activityMinibar-visibleAreaBackground);
/* transform: translateY(26px); */
transform: translateY(-4px);
z-index: 0;
}
.bb-region.visible-area > rect {
/* height: 10px; */
height: 100%;
}
/* :host(:hover) .bb-region.visible-area {
fill: var(--color-activityMinibar-visibleAreaHoverBackground);
} */
.bb-region.marker-result {
fill: var(--color-activityMinibar-resultMarker);
transform: translate(-1px, -4px);
z-index: 10;
}
.bb-region.marker-result > rect {
width: 2px;
height: 100%;
}
.bb-region.marker-head {
fill: var(--color-activityMinibar-headMarker);
transform: translate(0px, -4px);
z-index: 5;
}
.bb-region.marker-head > rect {
width: 2px;
height: 100%;
}
.bb-region.marker-upstream {
fill: var(--color-activityMinibar-upstreamMarker);
transform: translate(0px, -4px);
z-index: 4;
}
.bb-region.marker-upstream > rect {
width: 2px;
height: 100%;
}
.bb-region.marker-branch {
fill: var(--color-activityMinibar-branchMarker);
transform: translate(-2px, 26px);
z-index: 3;
}
.bb-region.marker-branch > rect {
width: 2px;
height: 10px;
}
.bb-region.marker-remote {
fill: var(--color-activityMinibar-remoteMarker);
transform: translate(-3px, 31px);
z-index: 2;
}
.bb-region.marker-remote > rect {
width: 2px;
height: 4px;
}
.bb-region.marker-tag {
fill: var(--color-activityMinibar-tagMarker);
transform: translate(1px, 31px);
z-index: 1;
}
.bb-region.marker-tag > rect {
width: 1px;
height: 4px;
}
/*-- Zoom region --*/
/*
:host-context(.vscode-dark) .bb-zoom-brush {
fill: white;
fill-opacity: 0.2;
}
:host-context(.vscode-light) .bb-zoom-brush {
fill: black;
fill-opacity: 0.1;
}
*/
/*-- Brush --*/
/*
.bb-brush .extent {
fill-opacity: 0.1;
}
*/
/*-- Button --*/
/*
.bb-button {
position: absolute;
top: 2px;
right: 0;
color: var(--color-button-foreground);
font-size: var(--font-size);
font-family: var(--font-family);
}
.bb-button .bb-zoom-reset {
display: inline-block;
padding: 0.1rem 0.3rem;
cursor: pointer;
font-family: 'codicon';
font-display: block;
background-color: var(--color-button-background);
border: 1px solid var(--color-button-background);
border-radius: 3px;
}
*/
/*-- Tooltip --*/
.bb-tooltip-container {
top: unset !important;
z-index: 10;
user-select: none;
min-width: 300px;
}
.bb-tooltip {
display: flex;
flex-direction: column;
padding: 0.5rem 1rem;
background-color: var(--color-hover-background);
color: var(--color-hover-foreground);
border: 1px solid var(--color-hover-border);
box-shadow: 0 2px 8px var(--vscode-widget-shadow);
font-size: var(--font-size);
opacity: 1;
white-space: nowrap;
}
.bb-tooltip .header {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 1rem;
}
.bb-tooltip .header--title {
font-weight: 600;
}
.bb-tooltip .header--description {
font-weight: normal;
font-style: italic;
}
.bb-tooltip .changes {
margin: 0.5rem 0;
}
.bb-tooltip .refs {
display: flex;
font-size: 12px;
gap: 0.5rem;
flex-direction: row;
flex-wrap: wrap;
margin: 0.5rem 0;
max-width: fit-content;
}
.bb-tooltip .refs .branch {
border-radius: 3px;
padding: 0 4px;
background-color: var(--color-activityMinibar-branchBackground);
border: 1px solid var(--color-activityMinibar-branchBorder);
color: var(--color-activityMinibar-branchForeground);
}
.bb-tooltip .refs .branch.current {
background-color: var(--color-activityMinibar-headBackground);
border: 1px solid var(--color-activityMinibar-headBorder);
color: var(--color-activityMinibar-headForeground);
}
.bb-tooltip .refs .remote {
border-radius: 3px;
padding: 0 4px;
background-color: var(--color-activityMinibar-remoteBackground);
border: 1px solid var(--color-activityMinibar-remoteBorder);
color: var(--color-activityMinibar-remoteForeground);
}
.bb-tooltip .refs .remote.current {
background-color: var(--color-activityMinibar-upstreamBackground);
border: 1px solid var(--color-activityMinibar-upstreamBorder);
color: var(--color-activityMinibar-upstreamForeground);
}
.bb-tooltip .refs .tag {
border-radius: 3px;
padding: 0 4px;
background-color: var(--color-activityMinibar-tagBackground);
border: 1px solid var(--color-activityMinibar-tagBorder);
color: var(--color-activityMinibar-tagForeground);
}
`;
@customElement({ name: 'activity-minibar', template: template, styles: styles })
export class ActivityMinibar extends FASTElement {
chart!: HTMLDivElement;
private _chart!: Chart;
private _loadTimer: ReturnType<typeof setTimeout> | undefined;
private _markerRegions: Iterable<RegionOptions> | undefined;
private _regions: RegionOptions[] | undefined;
@observable
activeDay: number | undefined;
protected activeDayChanged() {
this.select(this.activeDay);
}
@observable
data: Map<number, ActivityStats | null> | undefined;
protected dataChanged(
_oldVal?: Map<number, ActivityStats | null>,
_newVal?: Map<number, ActivityStats | null>,
markerChanged?: boolean,
) {
if (this._loadTimer) {
clearTimeout(this._loadTimer);
this._loadTimer = undefined;
}
if (markerChanged) {
this._regions = undefined;
this._markerRegions = undefined;
}
this._loadTimer = setTimeout(() => this.loadChart(), 150);
}
@observable
markers: Map<number, ActivityMarker[]> | undefined;
protected markersChanged() {
this.dataChanged(undefined, undefined, true);
}
@observable
searchResults: Map<number, ActivitySearchResultMarker> | undefined;
protected searchResultsChanged() {
this._chart?.regions.remove({ classes: ['marker-result'] });
if (this.searchResults == null) return;
this._chart?.regions.add([...this.getSearchResultsRegions(this.searchResults)]);
}
@observable
visibleDays: { top: number; bottom: number } | undefined;
protected visibleDaysChanged() {
this._chart?.regions.remove({ classes: ['visible-area'] });
if (this.visibleDays == null) return;
this._chart?.regions.add(this.getVisibleAreaRegion(this.visibleDays));
}
override connectedCallback(): void {
super.connectedCallback();
this.loadChart();
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this._chart?.destroy();
this._chart = undefined!;
}
private getInternalChart(): any {
return (this._chart as any).internal;
}
select(date: number | Date | undefined, trackOnly: boolean = false) {
if (date == null) {
this.unselect();
return;
}
const d = this.getData(date);
if (d == null) return;
const internal = this.getInternalChart();
internal.showGridFocus([d]);
if (!trackOnly) {
const { index } = d;
this._chart.$.main.selectAll(`.bb-shape-${index}`).each(function (d2) {
internal.toggleShape?.(this, d2, index);
});
}
}
unselect(date?: number | Date, focus: boolean = false) {
if (focus) {
this.getInternalChart().hideGridFocus();
return;
}
if (date != null) {
const index = this.getIndex(date);
if (index == null) return;
this._chart?.unselect(undefined, [index]);
} else {
this._chart?.unselect();
}
}
private getData(date: number | Date): DataItem<number> | undefined {
date = new Date(date).setHours(0, 0, 0, 0);
return this._chart
?.data()[0]
?.values.find(v => (typeof v.x === 'number' ? v.x : (v.x as any as Date).getTime()) === date);
}
private getIndex(date: number | Date): number | undefined {
return this.getData(date)?.index;
}
private getMarkerRegions() {
if (this._markerRegions == null) {
if (this.markers != null) {
const regions = flatMap(this.markers, ([day, markers]) =>
map<ActivityMarker, RegionOptions>(
markers,
m =>
({
axis: 'x',
start: day,
end: day,
class: m.current
? m.type === 'branch'
? 'marker-head'
: 'marker-upstream'
: `marker-${m.type}`,
} satisfies RegionOptions),
),
);
this._markerRegions = regions;
} else {
this._markerRegions = [];
}
}
return this._markerRegions;
}
private getAllRegions() {
if (this._regions == null) {
let regions: Iterable<RegionOptions> = this.getMarkerRegions();
if (this.visibleDays != null) {
regions = union(regions, [this.getVisibleAreaRegion(this.visibleDays)]);
}
if (this.searchResults != null) {
regions = union(regions, this.getSearchResultsRegions(this.searchResults));
}
this._regions = [...regions];
}
return this._regions;
}
private getSearchResultsRegions(searchResults: NonNullable<typeof this.searchResults>): Iterable<RegionOptions> {
return map(
searchResults.keys(),
day =>
({
axis: 'x',
start: day,
end: day,
class: 'marker-result',
} satisfies RegionOptions),
);
}
private getVisibleAreaRegion(visibleDays: NonNullable<typeof this.visibleDays>): RegionOptions {
return {
axis: 'x',
start: visibleDays.bottom,
end: visibleDays.top,
class: 'visible-area',
} satisfies RegionOptions;
}
private loadChart() {
if (!this.data?.size) {
this._chart?.destroy();
this._chart = undefined!;
return;
}
const hasActivity = some(this.data.values(), v => v?.activity != null);
// Convert the map to an array dates and an array of stats
const dates = [];
const activity: number[] = [];
// const commits: number[] = [];
// const additions: number[] = [];
// const deletions: number[] = [];
const keys = this.data.keys();
const endDay = first(keys)!;
const startDate = new Date();
const endDate = new Date(endDay);
let day;
let stat;
let changesMax = 0;
let adds;
let changes;
let deletes;
const currentDate = startDate;
// eslint-disable-next-line no-unmodified-loop-condition -- currentDate is modified via .setDate
while (currentDate >= endDate) {
day = getDay(currentDate);
stat = this.data.get(day);
dates.push(day);
if (hasActivity) {
adds = stat?.activity?.additions ?? 0;
deletes = stat?.activity?.deletions ?? 0;
changes = adds + deletes;
// additions.push(adds);
// deletions.push(-deletes);
} else {
changes = stat?.commits ?? 0;
// additions.push(0);
// deletions.push(0);
}
changesMax = Math.max(changesMax, changes);
activity.push(changes);
currentDate.setDate(currentDate.getDate() - 1);
}
const regions = this.getAllRegions();
// calculate the max value for the y-axis to avoid flattening the graph because of outlier changes
const p98 = [...activity].sort((a, b) => a - b)[Math.floor(activity.length * 0.98)];
const yMax = p98 + Math.min(changesMax - p98, p98 * 0.02) + 100;
if (this._chart == null) {
this._chart = bb.generate({
bindto: this.chart,
data: {
x: 'date',
xSort: false,
axes: {
activity: 'y',
additions: 'y',
deletions: 'y',
},
columns: [
['date', ...dates],
['activity', ...activity],
// ['additions', ...additions],
// ['deletions', ...deletions],
],
names: {
activity: 'Activity',
// additions: 'Additions',
// deletions: 'Deletions',
},
// hide: ['additions', 'deletions'],
onclick: d => {
if (d.id !== 'activity') return;
const date = new Date(d.x);
const day = getDay(date);
const sha = this.searchResults?.get(day)?.sha ?? this.data?.get(day)?.sha;
queueMicrotask(() => {
this.$emit('selected', {
date: date,
sha: sha,
} satisfies ActivityStatsSelectedEventDetail);
});
},
selection: {
enabled: selection(),
grouped: true,
multiple: false,
// isselectable: d => {
// if (d.id !== 'activity') return false;
// return (this.data?.get(getDay(new Date(d.x)))?.commits ?? 0) > 0;
// },
},
colors: {
activity: 'var(--color-activityMinibar-line0)',
// additions: 'rgba(73, 190, 71, 0.7)',
// deletions: 'rgba(195, 32, 45, 0.7)',
},
groups: [['additions', 'deletions']],
types: {
activity: spline(),
// additions: bar(),
// deletions: bar(),
},
},
area: {
linearGradient: true,
front: true,
below: true,
zerobased: true,
},
axis: {
x: {
show: false,
localtime: true,
type: 'timeseries',
},
y: {
min: 0,
max: yMax,
show: true,
padding: {
// top: 10,
bottom: 8,
},
},
// y2: {
// min: y2Min,
// max: yMax,
// show: true,
// // padding: {
// // top: 10,
// // bottom: 0,
// // },
// },
},
bar: {
zerobased: false,
width: { max: 3 },
},
clipPath: false,
grid: {
front: false,
focus: {
show: true,
},
},
legend: {
show: false,
},
line: {
point: true,
zerobased: true,
},
point: {
show: true,
select: {
r: 5,
},
focus: {
only: true,
expand: {
enabled: true,
r: 3,
},
},
sensitivity: 100,
},
regions: regions,
resize: {
auto: true,
},
spline: {
interpolation: {
type: 'catmull-rom',
},
},
tooltip: {
contents: (data, _defaultTitleFormat, _defaultValueFormat, _color) => {
const date = new Date(data[0].x);
const stat = this.data?.get(getDay(date));
const markers = this.markers?.get(getDay(date));
let groups;
if (markers?.length) {
groups = groupByMap(markers, m => m.type);
}
return /*html*/ `<div class="bb-tooltip">
<div class="header">
<span class="header--title">${formatDate(date, 'MMMM Do, YYYY')}</span>
<span class="header--description">(${capitalize(fromNow(date))})</span>
</div>
<div class="changes">
<span>${
(stat?.commits ?? 0) === 0
? 'No commits'
: `${pluralize('commit', stat?.commits ?? 0, {
format: c => formatNumeric(c),
zero: 'No',
})}, ${pluralize('file', stat?.commits ?? 0, {
format: c => formatNumeric(c),
zero: 'No',
})}${
hasActivity
? `, ${pluralize(
'line',
(stat?.activity?.additions ?? 0) +
(stat?.activity?.deletions ?? 0),
{
format: c => formatNumeric(c),
zero: 'No',
},
)}`
: ''
} changed`
}</span>
</div>
${
groups != null
? /*html*/ `
<div class="refs">
${
groups
?.get('branch')
?.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1))
.map(
m =>
/*html*/ `<span class="branch${m.current ? ' current' : ''}">${
m.name
}</span>`,
)
.join('') ?? ''
}
${
groups
?.get('remote')
?.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1))
?.map(
m =>
/*html*/ `<span class="remote${m.current ? ' current' : ''}">${
m.name
}</span>`,
)
.join('') ?? ''
}
${
groups
?.get('tag')
?.map(m => /*html*/ `<span class="tag">${m.name}</span>`)
.join('') ?? ''
}
</div>`
: ''
}
</div>`;
},
position: (_data, width, _height, element, pos) => {
const { x } = pos;
const rect = (element as HTMLElement).getBoundingClientRect();
let left = rect.right - x;
if (left + width > rect.right) {
left = rect.right - width;
}
return { top: 0, left: left };
},
},
transition: {
duration: 0,
},
zoom: {
enabled: zoom(),
rescale: false,
resetButton: {
text: '',
},
type: 'wheel',
onzoom: () => {
// Reset the active day when zooming because it fails to update properly
queueMicrotask(() => this.activeDayChanged());
},
},
onafterinit: function () {
const xAxis = this.$.main.selectAll<Element, any>('.bb-axis-x').node();
xAxis?.remove();
const yAxis = this.$.main.selectAll<Element, any>('.bb-axis-y').node();
yAxis?.remove();
const grid = this.$.main.selectAll<Element, any>('.bb-grid').node();
grid?.removeAttribute('clip-path');
// Move the regions to be on top of the bars
const bars = this.$.main.selectAll<Element, any>('.bb-chart-bars').node();
const regions = this.$.main.selectAll<Element, any>('.bb-regions').node();
bars?.insertAdjacentElement('afterend', regions!);
},
});
} else {
this._chart.load({
columns: [
['date', ...dates],
['activity', ...activity],
// ['additions', ...additions],
// ['deletions', ...deletions],
],
});
// this._chart.axis.min({ y: 0, y2: y2Min });
this._chart.axis.max({ y: yMax /*, y2: yMax*/ });
this._chart.regions(regions);
}
this.activeDayChanged();
}
}
function getDay(date: number | Date): number {
return new Date(date).setHours(0, 0, 0, 0);
}
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}

+ 8
- 0
src/webviews/apps/plus/activity/react.tsx View File

@ -0,0 +1,8 @@
import { reactWrapper } from '../../shared/components/helpers/react-wrapper';
import { ActivityMinibar as activityMinibarComponent } from './activity-minibar';
export const ActivityMinibar = reactWrapper(activityMinibarComponent, {
events: {
onSelected: 'selected',
},
});

+ 257
- 0
src/webviews/apps/plus/graph/GraphWrapper.tsx View File

@ -8,6 +8,7 @@ import type {
GraphRefOptData,
GraphRow,
GraphZoneType,
Head,
OnFormatCommitDateTime,
} from '@gitkraken/gitkraken-components';
import GraphContainer, { GRAPH_ZONE_TYPE, REF_ZONE_TYPE } from '@gitkraken/gitkraken-components';
@ -35,6 +36,7 @@ import type {
GraphSearchResultsError,
InternalNotificationType,
State,
UpdateGraphConfigurationParams,
UpdateStateCallback,
} from '../../../../plus/webviews/graph/protocol';
import {
@ -63,6 +65,14 @@ import { SearchBox } from '../../shared/components/search/react';
import type { SearchNavigationEventDetail } from '../../shared/components/search/search-box';
import type { DateTimeFormat } from '../../shared/date';
import { formatDate, fromNow } from '../../shared/date';
import type {
ActivityMarker,
ActivityMinibar as ActivityMinibarType,
ActivitySearchResultMarker,
ActivityStats,
ActivityStatsSelectedEventDetail,
} from '../activity/activity-minibar';
import { ActivityMinibar } from '../activity/react';
export interface GraphWrapperProps {
nonce?: string;
@ -88,6 +98,7 @@ export interface GraphWrapperProps {
onEnsureRowPromise?: (id: string, select: boolean) => Promise<DidEnsureRowParams | undefined>;
onExcludeType?: (key: keyof GraphExcludeTypes, value: boolean) => void;
onIncludeOnlyRef?: (all: boolean) => void;
onUpdateGraphConfiguration?: (changes: UpdateGraphConfigurationParams['changes']) => void;
}
const getGraphDateFormatter = (config?: GraphComponentConfig): OnFormatCommitDateTime => {
@ -168,6 +179,7 @@ export function GraphWrapper({
onDismissBanner,
onExcludeType,
onIncludeOnlyRef,
onUpdateGraphConfiguration,
}: GraphWrapperProps) {
const graphRef = useRef<GraphContainer>(null);
@ -180,6 +192,8 @@ export function GraphWrapper({
);
const [selectedRows, setSelectedRows] = useState(state.selectedRows);
const [activeRow, setActiveRow] = useState(state.activeRow);
const [activeDay, setActiveDay] = useState(state.activeDay);
const [visibleDays, setVisibleDays] = useState(state.visibleDays);
const [graphConfig, setGraphConfig] = useState(state.config);
// const [graphDateFormatter, setGraphDateFormatter] = useState(getGraphDateFormatter(config));
const [columns, setColumns] = useState(state.columns);
@ -214,6 +228,8 @@ export function GraphWrapper({
state.workingTreeStats ?? { added: 0, modified: 0, deleted: 0 },
);
const activityMinibar = useRef<ActivityMinibarType | undefined>(undefined);
const ensuredIds = useRef<Set<string>>(new Set());
const ensuredSkippedIds = useRef<Set<string>>(new Set());
@ -328,6 +344,206 @@ export function GraphWrapper({
};
}, [activeRow]);
const activityData = useMemo(() => {
if (!graphConfig?.activityMinibar) return undefined;
// Loops through all the rows and group them by day and aggregate the row.stats
const statsByDayMap = new Map<number, ActivityStats>();
const markersByDay = new Map<number, ActivityMarker[]>();
let rankedShas: {
head: string | undefined;
branch: string | undefined;
remote: string | undefined;
tag: string | undefined;
} = {
head: undefined,
branch: undefined,
remote: undefined,
tag: undefined,
};
let day;
let prevDay;
let head: Head | undefined;
let markers;
let headMarkers;
let remoteMarkers;
let tagMarkers;
let row: GraphRow;
let stat;
let stats;
// Iterate in reverse order so that we can track the HEAD upstream properly
for (let i = rows.length - 1; i >= 0; i--) {
row = rows[i];
stats = row.stats;
day = getDay(row.date);
if (day !== prevDay) {
prevDay = day;
rankedShas = {
head: undefined,
branch: undefined,
remote: undefined,
tag: undefined,
};
}
if (row.heads?.length) {
rankedShas.branch = row.sha;
// eslint-disable-next-line no-loop-func
headMarkers = row.heads.map<ActivityMarker>(h => {
if (h.isCurrentHead) {
head = h;
rankedShas.head = row.sha;
}
return {
type: 'branch',
name: h.name,
current: h.isCurrentHead,
};
});
markers = markersByDay.get(day);
if (markers == null) {
markersByDay.set(day, headMarkers);
} else {
markers.push(...headMarkers);
}
}
if (row.remotes?.length) {
rankedShas.remote = row.sha;
// eslint-disable-next-line no-loop-func
remoteMarkers = row.remotes.map<ActivityMarker>(r => {
let current = false;
if (r.name === head?.name) {
rankedShas.remote = row.sha;
current = true;
}
return {
type: 'remote',
name: `${r.owner}/${r.name}`,
current: current,
};
});
markers = markersByDay.get(day);
if (markers == null) {
markersByDay.set(day, remoteMarkers);
} else {
markers.push(...remoteMarkers);
}
}
if (row.tags?.length) {
rankedShas.tag = row.sha;
tagMarkers = row.tags.map<ActivityMarker>(t => ({
type: 'tag',
name: t.name,
}));
markers = markersByDay.get(day);
if (markers == null) {
markersByDay.set(day, tagMarkers);
} else {
markers.push(...tagMarkers);
}
}
stat = statsByDayMap.get(day);
if (stat == null) {
stat =
stats != null
? {
activity: { additions: stats.additions, deletions: stats.deletions },
commits: 1,
files: stats.files,
sha: row.sha,
}
: {
commits: 1,
sha: row.sha,
};
statsByDayMap.set(day, stat);
} else {
stat.commits++;
stat.sha = rankedShas.head ?? rankedShas.branch ?? rankedShas.remote ?? rankedShas.tag ?? stat.sha;
if (stats != null) {
if (stat.activity == null) {
stat.activity = { additions: stats.additions, deletions: stats.deletions };
} else {
stat.activity.additions += stats.additions;
stat.activity.deletions += stats.deletions;
}
stat.files = (stat.files ?? 0) + stats.files;
}
}
}
return { stats: statsByDayMap, markers: markersByDay };
}, [rows, graphConfig?.activityMinibar]);
const activitySearchResults = useMemo(() => {
if (!graphConfig?.activityMinibar) return undefined;
const searchResultsByDay = new Map<number, ActivitySearchResultMarker>();
if (searchResults?.ids != null) {
let day;
let sha;
let r;
let result;
for ([sha, r] of Object.entries(searchResults.ids)) {
day = getDay(r.date);
result = searchResultsByDay.get(day);
if (result == null) {
searchResultsByDay.set(day, { type: 'search-result', sha: sha });
}
}
}
return searchResultsByDay;
}, [searchResults, graphConfig?.activityMinibar]);
const handleOnActivityStatsSelected = (e: CustomEvent<ActivityStatsSelectedEventDetail>) => {
let { sha } = e.detail;
if (sha == null) {
const date = e.detail.date?.getTime();
if (date == null) return;
// Find closest row to the date
const closest = rows.reduce((prev, curr) =>
Math.abs(curr.date - date) < Math.abs(prev.date - date) ? curr : prev,
);
sha = closest.sha;
}
graphRef.current?.selectCommits([sha], false, true);
};
const handleOnToggleActivityMinibar = (_e: React.MouseEvent) => {
onUpdateGraphConfiguration?.({ activityMinibar: !graphConfig?.activityMinibar });
};
const handleOnGraphMouseLeave = (_event: any) => {
activityMinibar.current?.unselect(undefined, true);
};
const handleOnGraphRowHovered = (_event: any, graphZoneType: GraphZoneType, graphRow: GraphRow) => {
if (graphZoneType === REF_ZONE_TYPE || activityMinibar.current == null) return;
activityMinibar.current?.select(graphRow.date, true);
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
const sha = getActiveRowInfo(activeRow ?? state.activeRow)?.id;
@ -596,6 +812,13 @@ export function GraphWrapper({
}
};
const handleOnGraphVisibleRowsChanged = (top: GraphRow, bottom: GraphRow) => {
setVisibleDays({
top: new Date(top.date).setHours(23, 59, 59, 999),
bottom: new Date(bottom.date).setHours(0, 0, 0, 0),
});
};
const handleOnGraphColumnsReOrdered = (columnsSettings: GraphColumnsSettings) => {
const graphColumnsConfig: GraphColumnsConfig = {};
for (const [columnName, config] of Object.entries(columnsSettings as GraphColumnsConfig)) {
@ -634,6 +857,8 @@ export function GraphWrapper({
// HACK: Ensure the main state is updated since it doesn't come from the extension
state.activeRow = activeKey;
setActiveRow(activeKey);
setActiveDay(active?.date);
onSelectionChange?.(rows);
};
@ -992,6 +1217,18 @@ export function GraphWrapper({
onNavigate={e => handleSearchNavigation(e as CustomEvent<SearchNavigationEventDetail>)}
onOpenInView={() => handleSearchOpenInView()}
/>
<span>
<span className="action-divider"></span>
</span>
<button
type="button"
className="action-button action-button--narrow"
title="Toggle Activity Minibar (experimental)"
aria-label="Toggle Activity Minibar (experimental)"
onClick={handleOnToggleActivityMinibar}
>
<span className="codicon codicon-graph-line action-button__icon"></span>
</button>
</div>
</div>
)}
@ -999,6 +1236,17 @@ export function GraphWrapper({
<div className="progress-bar"></div>
</div>
</header>
{graphConfig?.activityMinibar && (
<ActivityMinibar
ref={activityMinibar as any}
activeDay={activeDay}
data={activityData?.stats}
markers={activityData?.markers}
searchResults={activitySearchResults}
visibleDays={visibleDays}
onSelected={e => handleOnActivityStatsSelected(e as CustomEvent<ActivityStatsSelectedEventDetail>)}
></ActivityMinibar>
)}
<main
id="main"
className={`graph-app__main${!isAccessAllowed ? ' is-gated' : ''}`}
@ -1037,11 +1285,16 @@ export function GraphWrapper({
onDoubleClickGraphRow={handleOnDoubleClickRow}
onDoubleClickGraphRef={handleOnDoubleClickRef}
onGraphColumnsReOrdered={handleOnGraphColumnsReOrdered}
onGraphMouseLeave={activityMinibar.current ? handleOnGraphMouseLeave : undefined}
onGraphRowHovered={activityMinibar.current ? handleOnGraphRowHovered : undefined}
onSelectGraphRows={handleSelectGraphRows}
onToggleRefsVisibilityClick={handleOnToggleRefsVisibilityClick}
onEmailsMissingAvatarUrls={handleMissingAvatars}
onRefsMissingMetadata={handleMissingRefsMetadata}
onShowMoreCommits={handleMoreCommits}
onGraphVisibleRowsChanged={
activityMinibar.current ? handleOnGraphVisibleRowsChanged : undefined
}
platform={clientPlatform}
refMetadataById={refsMetadata}
shaLength={graphConfig?.idLength}
@ -1203,3 +1456,7 @@ function getSearchResultModel(state: State): {
}
return { results: results, resultsError: resultsError };
}
function getDay(date: number | Date): number {
return new Date(date).setHours(0, 0, 0, 0);
}

+ 11
- 0
src/webviews/apps/plus/graph/graph.scss View File

@ -166,6 +166,17 @@ button:not([disabled]),
}
}
.action-button--narrow {
padding: 0;
width: 2.4rem;
height: 2.4rem;
text-align: center;
.codicon[class*='codicon-graph-line'] {
transform: translateX(2px);
}
}
.action-divider {
display: inline-block;
width: 0.1rem;

+ 118
- 4
src/webviews/apps/plus/graph/graph.tsx View File

@ -13,6 +13,7 @@ import type {
GraphMissingRefsMetadata,
InternalNotificationType,
State,
UpdateGraphConfigurationParams,
UpdateStateCallback,
} from '../../../../plus/webviews/graph/protocol';
import {
@ -42,11 +43,12 @@ import {
SearchOpenInViewCommandType,
UpdateColumnsCommandType,
UpdateExcludeTypeCommandType,
UpdateGraphConfigurationCommandType,
UpdateIncludeOnlyRefsCommandType,
UpdateRefsVisibilityCommandType,
UpdateSelectionCommandType,
} from '../../../../plus/webviews/graph/protocol';
import { darken, lighten, mix, opacity } from '../../../../system/color';
import { Color, darken, lighten, mix, opacity } from '../../../../system/color';
import { debounce } from '../../../../system/function';
import type { IpcMessage, IpcNotificationType } from '../../../protocol';
import { onIpc } from '../../../protocol';
@ -115,6 +117,7 @@ export class GraphApp extends App {
onEnsureRowPromise={this.onEnsureRowPromise.bind(this)}
onExcludeType={this.onExcludeType.bind(this)}
onIncludeOnlyRef={this.onIncludeOnlyRef.bind(this)}
onUpdateGraphConfiguration={this.onUpdateGraphConfiguration.bind(this)}
/>,
$root,
);
@ -322,6 +325,26 @@ export class GraphApp extends App {
}
protected override onThemeUpdated(e: ThemeChangeEvent) {
const backgroundColor = Color.from(e.colors.background);
const backgroundLuminance = backgroundColor.getRelativeLuminance();
const foregroundColor = Color.from(e.colors.foreground);
const foregroundLuminance = foregroundColor.getRelativeLuminance();
const themeLuminance = (luminance: number) => {
let min;
let max;
if (foregroundLuminance > backgroundLuminance) {
max = foregroundLuminance;
min = backgroundLuminance;
} else {
min = foregroundLuminance;
max = backgroundLuminance;
}
const percent = luminance / 1;
return percent * (max - min) + min;
};
const bodyStyle = document.body.style;
bodyStyle.setProperty('--graph-theme-opacity-factor', e.isLightTheme ? '0.5' : '1');
@ -343,7 +366,7 @@ export class GraphApp extends App {
e.isLightTheme ? darken(e.colors.background, 10) : lighten(e.colors.background, 10),
);
let color = e.computedStyle.getPropertyValue('--vscode-list-focusOutline').trim();
bodyStyle.setProperty('--color-graph-contrast-border-color', color);
bodyStyle.setProperty('--color-graph-contrast-border', color);
color = e.computedStyle.getPropertyValue('--vscode-list-activeSelectionBackground').trim();
bodyStyle.setProperty('--color-graph-selected-row', color);
color = e.computedStyle.getPropertyValue('--vscode-list-hoverBackground').trim();
@ -359,6 +382,93 @@ export class GraphApp extends App {
bodyStyle.setProperty('--color-graph-text-secondary', opacity(e.colors.foreground, 65));
bodyStyle.setProperty('--color-graph-text-disabled', opacity(e.colors.foreground, 50));
// activity minibar
const resultColor = Color.fromHex('#ffff00');
const headColor = Color.fromHex('#00ff00');
const branchColor = Color.fromHex('#ff7f50');
const tagColor = Color.fromHex('#15a0bf');
color = e.computedStyle.getPropertyValue('--vscode-progressBar-background').trim();
const activityColor = Color.from(color);
// bodyStyle.setProperty('--color-activityMinibar-line0', color);
bodyStyle.setProperty('--color-activityMinibar-line0', activityColor.luminance(themeLuminance(0.5)).toString());
bodyStyle.setProperty(
'--color-activityMinibar-focusLine',
backgroundColor.luminance(themeLuminance(e.isLightTheme ? 0.6 : 0.2)).toString(),
);
color = e.computedStyle.getPropertyValue('--vscode-scrollbarSlider-background').trim();
bodyStyle.setProperty(
'--color-activityMinibar-visibleAreaBackground',
Color.from(color)
.luminance(themeLuminance(e.isLightTheme ? 0.6 : 0.15))
.toString(),
);
color = e.computedStyle.getPropertyValue('--vscode-scrollbarSlider-hoverBackground').trim();
bodyStyle.setProperty(
'--color-activityMinibar-visibleAreaHoverBackground',
Color.from(color)
.luminance(themeLuminance(e.isLightTheme ? 0.6 : 0.15))
.toString(),
);
color = Color.from(e.computedStyle.getPropertyValue('--vscode-list-activeSelectionBackground').trim())
.luminance(themeLuminance(e.isLightTheme ? 0.45 : 0.32))
.toString();
// color = e.computedStyle.getPropertyValue('--vscode-editorCursor-foreground').trim();
bodyStyle.setProperty('--color-activityMinibar-selectedMarker', color);
bodyStyle.setProperty('--color-activityMinibar-highlightedMarker', opacity(color, 60));
bodyStyle.setProperty(
'--color-activityMinibar-resultMarker',
resultColor.luminance(themeLuminance(0.6)).toString(),
);
const pillLabel = foregroundColor.luminance(themeLuminance(e.isLightTheme ? 0 : 1)).toString();
const headBackground = headColor.luminance(themeLuminance(e.isLightTheme ? 0.9 : 0.2)).toString();
const headBorder = headColor.luminance(themeLuminance(e.isLightTheme ? 0.2 : 0.4)).toString();
const headMarker = headColor.luminance(themeLuminance(0.5)).toString();
bodyStyle.setProperty('--color-activityMinibar-headBackground', headBackground);
bodyStyle.setProperty('--color-activityMinibar-headBorder', headBorder);
bodyStyle.setProperty('--color-activityMinibar-headForeground', pillLabel);
bodyStyle.setProperty('--color-activityMinibar-headMarker', opacity(headMarker, 80));
bodyStyle.setProperty('--color-activityMinibar-upstreamBackground', headBackground);
bodyStyle.setProperty('--color-activityMinibar-upstreamBorder', headBorder);
bodyStyle.setProperty('--color-activityMinibar-upstreamForeground', pillLabel);
bodyStyle.setProperty('--color-activityMinibar-upstreamMarker', opacity(headMarker, 60));
const branchBackground = branchColor.luminance(themeLuminance(e.isLightTheme ? 0.8 : 0.3)).toString();
const branchBorder = branchColor.luminance(themeLuminance(e.isLightTheme ? 0.2 : 0.4)).toString();
const branchMarker = branchColor.luminance(themeLuminance(0.6)).toString();
bodyStyle.setProperty('--color-activityMinibar-branchBackground', branchBackground);
bodyStyle.setProperty('--color-activityMinibar-branchBorder', branchBorder);
bodyStyle.setProperty('--color-activityMinibar-branchForeground', pillLabel);
bodyStyle.setProperty('--color-activityMinibar-branchMarker', opacity(branchMarker, 70));
bodyStyle.setProperty('--color-activityMinibar-remoteBackground', opacity(branchBackground, 80));
bodyStyle.setProperty('--color-activityMinibar-remoteBorder', opacity(branchBorder, 80));
bodyStyle.setProperty('--color-activityMinibar-remoteForeground', pillLabel);
bodyStyle.setProperty('--color-activityMinibar-remoteMarker', opacity(branchMarker, 30));
bodyStyle.setProperty(
'--color-activityMinibar-tagBackground',
tagColor.luminance(themeLuminance(e.isLightTheme ? 0.8 : 0.2)).toString(),
);
bodyStyle.setProperty(
'--color-activityMinibar-tagBorder',
tagColor.luminance(themeLuminance(e.isLightTheme ? 0.2 : 0.4)).toString(),
);
bodyStyle.setProperty('--color-activityMinibar-tagForeground', pillLabel);
bodyStyle.setProperty(
'--color-activityMinibar-tagMarker',
opacity(tagColor.luminance(themeLuminance(0.5)).toString(), 60),
);
if (e.isInitializing) return;
this.state.theming = undefined;
@ -420,11 +530,11 @@ export class GraphApp extends App {
'--selected-row': computedStyle.getPropertyValue('--color-graph-selected-row'),
'--selected-row-border': isHighContrastTheme
? `1px solid ${computedStyle.getPropertyValue('--color-graph-contrast-border-color')}`
? `1px solid ${computedStyle.getPropertyValue('--color-graph-contrast-border')}`
: 'none',
'--hover-row': computedStyle.getPropertyValue('--color-graph-hover-row'),
'--hover-row-border': isHighContrastTheme
? `1px dashed ${computedStyle.getPropertyValue('--color-graph-contrast-border-color')}`
? `1px dashed ${computedStyle.getPropertyValue('--color-graph-contrast-border')}`
: 'none',
'--text-selected': computedStyle.getPropertyValue('--color-graph-text-selected'),
@ -545,6 +655,10 @@ export class GraphApp extends App {
);
}
private onUpdateGraphConfiguration(changes: UpdateGraphConfigurationParams['changes']) {
this.sendCommand(UpdateGraphConfigurationCommandType, { changes: changes });
}
private onSelectionChanged(rows: GraphRow[]) {
const selection = rows.map(r => ({ id: r.sha, type: r.type as GitGraphRowType }));
this.sendCommand(UpdateSelectionCommandType, {

Loading…
Cancel
Save