Skip to content

Commit 4e9bc62

Browse files
authored
SCM - history graph improvements (#222102)
* SCM - hover improvements * SCM - create theme colors for history graph * Update tests
1 parent e8e6e1f commit 4e9bc62

File tree

5 files changed

+204
-134
lines changed

5 files changed

+204
-134
lines changed

src/vs/workbench/contrib/scm/browser/media/scm.css

+23
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,26 @@
524524
.scm-repositories-view .scm-provider > .label > .name {
525525
font-weight: normal;
526526
}
527+
528+
/* History item hover */
529+
530+
.monaco-hover.history-item-hover p:first-child {
531+
margin-top: 4px;
532+
}
533+
534+
.monaco-hover.history-item-hover p:last-child {
535+
margin-bottom: 4px;
536+
}
537+
538+
.monaco-hover.history-item-hover hr {
539+
margin-top: 4px;
540+
margin-bottom: 4px;
541+
}
542+
543+
.monaco-hover.history-item-hover hr + p {
544+
margin: 4px 0;
545+
}
546+
547+
.monaco-hover.history-item-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span:not(.codicon) {
548+
margin-bottom: 0 !important;
549+
}

src/vs/workbench/contrib/scm/browser/scmHistory.ts

+48-24
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,43 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { localize } from 'vs/nls';
67
import { lastOrDefault } from 'vs/base/common/arrays';
78
import { deepClone } from 'vs/base/common/objects';
89
import { ThemeIcon } from 'vs/base/common/themables';
10+
import { buttonForeground } from 'vs/platform/theme/common/colorRegistry';
11+
import { chartsBlue, chartsGreen, chartsOrange, chartsPurple, chartsRed, chartsYellow } from 'vs/platform/theme/common/colors/chartsColors';
12+
import { asCssVariable, ColorIdentifier, registerColor } from 'vs/platform/theme/common/colorUtils';
913
import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history';
14+
import { rot } from 'vs/base/common/numbers';
1015

1116
const SWIMLANE_HEIGHT = 22;
1217
const SWIMLANE_WIDTH = 11;
1318
const CIRCLE_RADIUS = 4;
1419
const SWIMLANE_CURVE_RADIUS = 5;
1520

16-
const graphColors = ['#007ACC', '#BC3FBC', '#BF8803', '#CC6633', '#F14C4C', '#16825D'];
17-
18-
function getNextColorIndex(colorIndex: number): number {
19-
return colorIndex < graphColors.length - 1 ? colorIndex + 1 : 1;
20-
}
21-
22-
function getLabelColorIndex(historyItem: ISCMHistoryItem, colorMap: Map<string, number>): number | undefined {
21+
/**
22+
* History graph colors (local, remote, base)
23+
*/
24+
export const historyItemGroupLocal = registerColor('scm.historyGraph.historyItemGroupLocal', chartsBlue, localize('scm.historyGraph.historyItemGroupLocal', "Local history item group color."));
25+
export const historyItemGroupRemote = registerColor('scm.historyGraph.historyItemGroupRemote', chartsPurple, localize('scm.historyItemGroupRemote', "Remote history item group color."));
26+
export const historyItemGroupBase = registerColor('scm.historyGraph.historyItemGroupBase', chartsOrange, localize('scm.historyItemGroupBase', "Base history item group color."));
27+
28+
/**
29+
* History item hover color
30+
*/
31+
export const historyItemGroupHoverLabelForeground = registerColor('scm.historyGraph.historyItemGroupHoverLabelForeground', buttonForeground, localize('scm.historyItemGroupHoverLabelForeground', "History item group hover label foreground color."));
32+
33+
/**
34+
* History graph color registry
35+
*/
36+
export const colorRegistry: ColorIdentifier[] = [
37+
registerColor('scm.historyGraph.green', chartsGreen, localize('scm.historyGraph.green', "The green color used in history graph.")),
38+
registerColor('scm.historyGraph.red', chartsRed, localize('scm.historyGraph.red', "The red color used in history graph.")),
39+
registerColor('scm.historyGraph.yellow', chartsYellow, localize('scm.historyGraph.yellow', "The yellow color used in history graph.")),
40+
];
41+
42+
function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map<string, ColorIdentifier>): ColorIdentifier | undefined {
2343
for (const label of historyItem.labels ?? []) {
2444
const colorIndex = colorMap.get(label.title);
2545
if (colorIndex !== undefined) {
@@ -30,22 +50,22 @@ function getLabelColorIndex(historyItem: ISCMHistoryItem, colorMap: Map<string,
3050
return undefined;
3151
}
3252

33-
function createPath(stroke: string): SVGPathElement {
53+
function createPath(colorIdentifier: string): SVGPathElement {
3454
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3555
path.setAttribute('fill', 'none');
36-
path.setAttribute('stroke', stroke);
3756
path.setAttribute('stroke-width', '1px');
3857
path.setAttribute('stroke-linecap', 'round');
58+
path.style.stroke = asCssVariable(colorIdentifier);
3959

4060
return path;
4161
}
4262

43-
function drawCircle(index: number, radius: number, fill: string): SVGCircleElement {
63+
function drawCircle(index: number, radius: number, colorIdentifier: string): SVGCircleElement {
4464
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
4565
circle.setAttribute('cx', `${SWIMLANE_WIDTH * (index + 1)}`);
4666
circle.setAttribute('cy', `${SWIMLANE_WIDTH}`);
4767
circle.setAttribute('r', `${radius}`);
48-
circle.setAttribute('fill', fill);
68+
circle.style.fill = asCssVariable(colorIdentifier);
4969

5070
return circle;
5171
}
@@ -82,11 +102,11 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV
82102
const circleIndex = inputIndex !== -1 ? inputIndex : inputSwimlanes.length;
83103

84104
// Circle color - use the output swimlane color if present, otherwise the input swimlane color
85-
const circleColorIndex = circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : inputSwimlanes[circleIndex].color;
105+
const circleColor = circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : inputSwimlanes[circleIndex].color;
86106

87107
let outputSwimlaneIndex = 0;
88108
for (let index = 0; index < inputSwimlanes.length; index++) {
89-
const color = graphColors[inputSwimlanes[index].color];
109+
const color = inputSwimlanes[index].color;
90110

91111
// Current commit
92112
if (inputSwimlanes[index].id === historyItem.id) {
@@ -153,7 +173,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV
153173

154174
// Draw -\
155175
const d: string[] = [];
156-
const path = createPath(graphColors[outputSwimlanes[parentOutputIndex].color]);
176+
const path = createPath(outputSwimlanes[parentOutputIndex].color);
157177

158178
// Draw \
159179
d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`);
@@ -169,34 +189,34 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV
169189

170190
// Draw | to *
171191
if (inputIndex !== -1) {
172-
const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), 0, SWIMLANE_HEIGHT / 2, graphColors[inputSwimlanes[inputIndex].color]);
192+
const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), 0, SWIMLANE_HEIGHT / 2, inputSwimlanes[inputIndex].color);
173193
svg.append(path);
174194
}
175195

176196
// Draw | from *
177197
if (historyItem.parentIds.length > 0) {
178-
const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), SWIMLANE_HEIGHT / 2, SWIMLANE_HEIGHT, graphColors[circleColorIndex]);
198+
const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), SWIMLANE_HEIGHT / 2, SWIMLANE_HEIGHT, circleColor);
179199
svg.append(path);
180200
}
181201

182202
// Draw *
183203
if (historyItem.parentIds.length > 1) {
184204
// Multi-parent node
185-
const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, graphColors[circleColorIndex]);
205+
const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, circleColor);
186206
svg.append(circleOuter);
187207

188-
const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, graphColors[circleColorIndex]);
208+
const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, circleColor);
189209
svg.append(circleInner);
190210
} else {
191211
// HEAD
192212
// TODO@lszomoru - implement a better way to determine if the commit is HEAD
193213
if (historyItem.labels?.some(l => ThemeIcon.isThemeIcon(l.icon) && l.icon.id === 'target')) {
194-
const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, graphColors[circleColorIndex]);
214+
const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, circleColor);
195215
svg.append(outerCircle);
196216
}
197217

198218
// Node
199-
const circle = drawCircle(circleIndex, CIRCLE_RADIUS, graphColors[circleColorIndex]);
219+
const circle = drawCircle(circleIndex, CIRCLE_RADIUS, circleColor);
200220
svg.append(circle);
201221
}
202222

@@ -207,7 +227,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV
207227
return svg;
208228
}
209229

210-
export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map<string, number>()): ISCMHistoryItemViewModel[] {
230+
export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map<string, string>()): ISCMHistoryItemViewModel[] {
211231
let colorIndex = -1;
212232
const viewModels: ISCMHistoryItemViewModel[] = [];
213233

@@ -227,7 +247,7 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[],
227247
if (!firstParentAdded) {
228248
outputSwimlanes.push({
229249
id: historyItem.parentIds[0],
230-
color: getLabelColorIndex(historyItem, colorMap) ?? node.color
250+
color: getLabelColorIdentifier(historyItem, colorMap) ?? node.color
231251
});
232252
firstParentAdded = true;
233253
}
@@ -241,11 +261,15 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[],
241261
// Add unprocessed parent(s) to the output
242262
for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) {
243263
// Color index (label -> next color)
244-
colorIndex = getLabelColorIndex(historyItem, colorMap) ?? getNextColorIndex(colorIndex);
264+
let colorIdentifier = getLabelColorIdentifier(historyItem, colorMap);
265+
if (!colorIdentifier) {
266+
colorIndex = rot(colorIndex + 1, colorRegistry.length);
267+
colorIdentifier = colorRegistry[colorIndex];
268+
}
245269

246270
outputSwimlanes.push({
247271
id: historyItem.parentIds[i],
248-
color: colorIndex
272+
color: colorIdentifier
249273
});
250274
}
251275
}

src/vs/workbench/contrib/scm/browser/scmViewPane.ts

+33-11
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'
9595
import { EditOperation } from 'vs/editor/common/core/editOperation';
9696
import { stripIcons } from 'vs/base/common/iconLabels';
9797
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
98-
import { editorSelectionBackground, foreground, inputBackground, inputForeground, listActiveSelectionForeground, registerColor, selectionBackground, transparent } from 'vs/platform/theme/common/colorRegistry';
98+
import { ColorIdentifier, editorSelectionBackground, foreground, inputBackground, inputForeground, listActiveSelectionForeground, registerColor, selectionBackground, transparent } from 'vs/platform/theme/common/colorRegistry';
9999
import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
100100
import { CancellationTokenSource } from 'vs/base/common/cancellation';
101101
import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem';
@@ -108,7 +108,7 @@ import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController
108108
import { ITextModel } from 'vs/editor/common/model';
109109
import { autorun } from 'vs/base/common/observable';
110110
import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
111-
import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory';
111+
import { historyItemGroupBase, historyItemGroupHoverLabelForeground, historyItemGroupLocal, historyItemGroupRemote, renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory';
112112
import { PlaceholderTextContribution } from 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution';
113113
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
114114
import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate';
@@ -1051,7 +1051,7 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer<SCMHistoryItemVi
10511051
const historyItemViewModel = node.element.historyItemViewModel;
10521052
const historyItem = historyItemViewModel.historyItem;
10531053

1054-
const historyItemHover = this.hoverService.setupManagedHover(this.hoverDelegate, templateData.element, this.getTooltip(historyItemViewModel));
1054+
const historyItemHover = this.hoverService.setupManagedHover(this.hoverDelegate, templateData.element, this.getTooltip(node.element));
10551055
templateData.elementDisposables.add(historyItemHover);
10561056

10571057
templateData.graphContainer.textContent = '';
@@ -1081,8 +1081,11 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer<SCMHistoryItemVi
10811081
throw new Error('Should never happen since node is incompressible');
10821082
}
10831083

1084-
private getTooltip(historyItemViewModel: ISCMHistoryItemViewModel): IManagedHoverTooltipMarkdownString {
1085-
const historyItem = historyItemViewModel.historyItem;
1084+
private getTooltip(element: SCMHistoryItemViewModelTreeElement): IManagedHoverTooltipMarkdownString {
1085+
const colorTheme = this.themeService.getColorTheme();
1086+
const historyItem = element.historyItemViewModel.historyItem;
1087+
const currentHistoryItemGroup = element.repository.provider.historyProvider.get()?.currentHistoryItemGroup?.get();
1088+
10861089
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
10871090

10881091
if (historyItem.author) {
@@ -1099,7 +1102,6 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer<SCMHistoryItemVi
10991102
markdown.appendMarkdown(`${historyItem.message}\n\n`);
11001103

11011104
if (historyItem.statistics?.files) {
1102-
const colorTheme = this.themeService.getColorTheme();
11031105
const historyItemAdditionsForegroundColor = colorTheme.getColor(historyItemAdditionsForeground);
11041106
const historyItemDeletionsForegroundColor = colorTheme.getColor(historyItemDeletionsForeground);
11051107

@@ -1122,6 +1124,27 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer<SCMHistoryItemVi
11221124
}
11231125
}
11241126

1127+
if (historyItem.labels) {
1128+
const historyItemGroupLocalColor = colorTheme.getColor(historyItemGroupLocal);
1129+
const historyItemGroupRemoteColor = colorTheme.getColor(historyItemGroupRemote);
1130+
const historyItemGroupBaseColor = colorTheme.getColor(historyItemGroupBase);
1131+
1132+
const historyItemGroupHoverLabelForegroundColor = colorTheme.getColor(historyItemGroupHoverLabelForeground);
1133+
1134+
markdown.appendMarkdown(`\n\n---\n\n`);
1135+
markdown.appendMarkdown(historyItem.labels.map(label => {
1136+
const historyItemGroupHoverLabelBackgroundColor =
1137+
label.title === currentHistoryItemGroup?.name ? historyItemGroupLocalColor :
1138+
label.title === currentHistoryItemGroup?.remote?.name ? historyItemGroupRemoteColor :
1139+
label.title === currentHistoryItemGroup?.base?.name ? historyItemGroupBaseColor :
1140+
undefined;
1141+
1142+
const historyItemGroupHoverLabelIconId = ThemeIcon.isThemeIcon(label.icon) ? label.icon.id : '';
1143+
1144+
return `<span style="color:${historyItemGroupHoverLabelForegroundColor};background-color:${historyItemGroupHoverLabelBackgroundColor};">&nbsp;$(${historyItemGroupHoverLabelIconId})&nbsp;${label.title}&nbsp;</span>`;
1145+
}).join('&nbsp;&nbsp;'));
1146+
}
1147+
11251148
return { markdown, markdownNotSupportedFallback: historyItem.message };
11261149
}
11271150

@@ -3996,15 +4019,14 @@ class SCMTreeHistoryProviderDataSource extends Disposable {
39964019
}
39974020

39984021
// Create the color map
3999-
// TODO@lszomoru - use theme colors
4000-
const colorMap = new Map<string, number>([
4001-
[currentHistoryItemGroup.name, 0]
4022+
const colorMap = new Map<string, ColorIdentifier>([
4023+
[currentHistoryItemGroup.name, historyItemGroupLocal]
40024024
]);
40034025
if (currentHistoryItemGroup.remote) {
4004-
colorMap.set(currentHistoryItemGroup.remote.name, 1);
4026+
colorMap.set(currentHistoryItemGroup.remote.name, historyItemGroupRemote);
40054027
}
40064028
if (currentHistoryItemGroup.base) {
4007-
colorMap.set(currentHistoryItemGroup.base.name, 2);
4029+
colorMap.set(currentHistoryItemGroup.base.name, historyItemGroupBase);
40084030
}
40094031

40104032
return toISCMHistoryItemViewModelArray(historyItemsElement, colorMap)

src/vs/workbench/contrib/scm/common/history.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IObservable } from 'vs/base/common/observable';
77
import { ThemeIcon } from 'vs/base/common/themables';
88
import { URI } from 'vs/base/common/uri';
99
import { IMenu } from 'vs/platform/actions/common/actions';
10+
import { ColorIdentifier } from 'vs/platform/theme/common/colorUtils';
1011
import { ISCMRepository } from 'vs/workbench/contrib/scm/common/scm';
1112

1213
export interface ISCMHistoryProviderMenus {
@@ -88,7 +89,7 @@ export interface ISCMHistoryItem {
8889

8990
export interface ISCMHistoryItemGraphNode {
9091
readonly id: string;
91-
readonly color: number;
92+
readonly color: ColorIdentifier;
9293
}
9394

9495
export interface ISCMHistoryItemViewModel {

0 commit comments

Comments
 (0)