@theia/scm 1.75.0-next.8 → 1.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/lib/browser/decorations/scm-navigator-decorator.d.ts.map +1 -1
  2. package/lib/browser/decorations/scm-navigator-decorator.js +1 -0
  3. package/lib/browser/decorations/scm-navigator-decorator.js.map +1 -1
  4. package/lib/browser/merge-editor/view/merge-editor-panes/merge-editor-pane-header.js +7 -10
  5. package/lib/browser/merge-editor/view/merge-editor-panes/merge-editor-pane-header.js.map +1 -1
  6. package/lib/browser/scm-action-button-widget.js +10 -15
  7. package/lib/browser/scm-action-button-widget.js.map +1 -1
  8. package/lib/browser/scm-amend-component.js +24 -57
  9. package/lib/browser/scm-amend-component.js.map +1 -1
  10. package/lib/browser/scm-commit-widget.d.ts +1 -1
  11. package/lib/browser/scm-commit-widget.d.ts.map +1 -1
  12. package/lib/browser/scm-commit-widget.js +9 -10
  13. package/lib/browser/scm-commit-widget.js.map +1 -1
  14. package/lib/browser/scm-context-key-service.d.ts +2 -0
  15. package/lib/browser/scm-context-key-service.d.ts.map +1 -1
  16. package/lib/browser/scm-context-key-service.js +4 -0
  17. package/lib/browser/scm-context-key-service.js.map +1 -1
  18. package/lib/browser/scm-context-key-service.spec.js +7 -0
  19. package/lib/browser/scm-context-key-service.spec.js.map +1 -1
  20. package/lib/browser/scm-frontend-module.d.ts.map +1 -1
  21. package/lib/browser/scm-frontend-module.js +6 -0
  22. package/lib/browser/scm-frontend-module.js.map +1 -1
  23. package/lib/browser/scm-history-graph-contribution.d.ts +49 -0
  24. package/lib/browser/scm-history-graph-contribution.d.ts.map +1 -0
  25. package/lib/browser/scm-history-graph-contribution.js +280 -0
  26. package/lib/browser/scm-history-graph-contribution.js.map +1 -0
  27. package/lib/browser/scm-history-graph-contribution.spec.d.ts +2 -0
  28. package/lib/browser/scm-history-graph-contribution.spec.d.ts.map +1 -0
  29. package/lib/browser/scm-history-graph-contribution.spec.js +119 -0
  30. package/lib/browser/scm-history-graph-contribution.spec.js.map +1 -0
  31. package/lib/browser/scm-history-graph-helpers.d.ts +59 -6
  32. package/lib/browser/scm-history-graph-helpers.d.ts.map +1 -1
  33. package/lib/browser/scm-history-graph-helpers.js +129 -5
  34. package/lib/browser/scm-history-graph-helpers.js.map +1 -1
  35. package/lib/browser/scm-history-graph-helpers.spec.d.ts +2 -0
  36. package/lib/browser/scm-history-graph-helpers.spec.d.ts.map +1 -0
  37. package/lib/browser/scm-history-graph-helpers.spec.js +154 -0
  38. package/lib/browser/scm-history-graph-helpers.spec.js.map +1 -0
  39. package/lib/browser/scm-history-graph-lanes.d.ts +19 -5
  40. package/lib/browser/scm-history-graph-lanes.d.ts.map +1 -1
  41. package/lib/browser/scm-history-graph-lanes.js +25 -9
  42. package/lib/browser/scm-history-graph-lanes.js.map +1 -1
  43. package/lib/browser/scm-history-graph-lanes.spec.js +62 -25
  44. package/lib/browser/scm-history-graph-lanes.spec.js.map +1 -1
  45. package/lib/browser/scm-history-graph-model.d.ts +75 -6
  46. package/lib/browser/scm-history-graph-model.d.ts.map +1 -1
  47. package/lib/browser/scm-history-graph-model.js +155 -10
  48. package/lib/browser/scm-history-graph-model.js.map +1 -1
  49. package/lib/browser/scm-history-graph-model.spec.js +299 -4
  50. package/lib/browser/scm-history-graph-model.spec.js.map +1 -1
  51. package/lib/browser/scm-history-graph-tooltip.d.ts +27 -2
  52. package/lib/browser/scm-history-graph-tooltip.d.ts.map +1 -1
  53. package/lib/browser/scm-history-graph-tooltip.js +113 -22
  54. package/lib/browser/scm-history-graph-tooltip.js.map +1 -1
  55. package/lib/browser/scm-history-graph-tooltip.spec.d.ts +2 -0
  56. package/lib/browser/scm-history-graph-tooltip.spec.d.ts.map +1 -0
  57. package/lib/browser/scm-history-graph-tooltip.spec.js +213 -0
  58. package/lib/browser/scm-history-graph-tooltip.spec.js.map +1 -0
  59. package/lib/browser/scm-history-graph-widget.d.ts +36 -2
  60. package/lib/browser/scm-history-graph-widget.d.ts.map +1 -1
  61. package/lib/browser/scm-history-graph-widget.js +174 -67
  62. package/lib/browser/scm-history-graph-widget.js.map +1 -1
  63. package/lib/browser/scm-history-graph-widget.spec.d.ts +2 -0
  64. package/lib/browser/scm-history-graph-widget.spec.d.ts.map +1 -0
  65. package/lib/browser/scm-history-graph-widget.spec.js +273 -0
  66. package/lib/browser/scm-history-graph-widget.spec.js.map +1 -0
  67. package/lib/browser/scm-provider.d.ts +9 -1
  68. package/lib/browser/scm-provider.d.ts.map +1 -1
  69. package/lib/browser/scm-provider.js.map +1 -1
  70. package/lib/browser/scm-repositories-widget.js +12 -21
  71. package/lib/browser/scm-repositories-widget.js.map +1 -1
  72. package/lib/browser/scm-tree-widget.js +35 -53
  73. package/lib/browser/scm-tree-widget.js.map +1 -1
  74. package/lib/common/scm-preferences.d.ts +3 -0
  75. package/lib/common/scm-preferences.d.ts.map +1 -1
  76. package/lib/common/scm-preferences.js +24 -0
  77. package/lib/common/scm-preferences.js.map +1 -1
  78. package/package.json +7 -7
  79. package/src/browser/decorations/scm-navigator-decorator.ts +3 -2
  80. package/src/browser/scm-context-key-service.spec.ts +9 -0
  81. package/src/browser/scm-context-key-service.ts +6 -0
  82. package/src/browser/scm-frontend-module.ts +7 -1
  83. package/src/browser/scm-history-graph-contribution.spec.ts +148 -0
  84. package/src/browser/scm-history-graph-contribution.ts +273 -0
  85. package/src/browser/scm-history-graph-helpers.spec.ts +193 -0
  86. package/src/browser/scm-history-graph-helpers.ts +180 -6
  87. package/src/browser/scm-history-graph-lanes.spec.ts +70 -25
  88. package/src/browser/scm-history-graph-lanes.ts +48 -11
  89. package/src/browser/scm-history-graph-model.spec.ts +378 -12
  90. package/src/browser/scm-history-graph-model.ts +181 -13
  91. package/src/browser/scm-history-graph-tooltip.spec.ts +254 -0
  92. package/src/browser/scm-history-graph-tooltip.ts +144 -24
  93. package/src/browser/scm-history-graph-widget.spec.ts +322 -0
  94. package/src/browser/scm-history-graph-widget.tsx +199 -39
  95. package/src/browser/scm-provider.ts +9 -1
  96. package/src/browser/style/scm-history-graph.css +53 -2
  97. package/src/common/scm-preferences.ts +27 -0
@@ -15,11 +15,12 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import * as React from '@theia/core/shared/react';
18
- import { Virtuoso } from '@theia/core/shared/react-virtuoso';
19
- import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
18
+ import { Virtuoso, VirtuosoHandle } from '@theia/core/shared/react-virtuoso';
19
+ import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify';
20
20
  import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
21
21
  import { LabelProvider } from '@theia/core/lib/browser/label-provider';
22
22
  import { HoverService } from '@theia/core/lib/browser/hover-service';
23
+ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
23
24
  import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
24
25
  import { ScmHistoryGraphModel, HistoryGraphEntry } from './scm-history-graph-model';
25
26
  import { ScmHistoryItemRef, ScmHistoryItemChange } from './scm-provider';
@@ -28,16 +29,22 @@ import { GraphRow } from './scm-history-graph-lanes';
28
29
  import URI from '@theia/core/lib/common/uri';
29
30
  import { nls } from '@theia/core/lib/common/nls';
30
31
  import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
32
+ import { DisposableCollection } from '@theia/core/lib/common/disposable';
33
+ import { Emitter, Event } from '@theia/core/lib/common/event';
34
+ import { DynamicToolbarWidget } from '@theia/core/lib/browser/view-container';
31
35
  import { MenuPath } from '@theia/core/lib/common/menu/menu-types';
32
36
  import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
33
37
  import { OpenerService, open } from '@theia/core/lib/browser/opener-service';
34
38
  import { DiffUris } from '@theia/core/lib/browser/diff-uris';
35
39
  import { ScmContextKeyService } from './scm-context-key-service';
40
+ import { ScmPreferences } from '../common/scm-preferences';
36
41
  import {
37
- laneColor, getChangeStatus, getFileName, getFilePath, getRepoRelativePath,
38
- getRefBadgeClass, isTagRef, isRemoteRef, deduplicateRefs, DeduplicatedRef
42
+ laneColor, filterRefsForBadges, getChangeStatus, getFileName, getFilePath, getRepoRelativePath,
43
+ getRefBadgeClass, getRefBadgePresentation, isTagRef, isRemoteRef, deduplicateRefs, DeduplicatedRef,
44
+ buildChangeTreeRows, ChangeTreeFolderRow
39
45
  } from './scm-history-graph-helpers';
40
- import { buildHtmlTooltip } from './scm-history-graph-tooltip';
46
+ import { ILogger } from '@theia/core';
47
+ import { buildHtmlTooltip, buildProviderTooltip } from './scm-history-graph-tooltip';
41
48
 
42
49
  /** Menu path matching the VS Code 'scm/history/title' contribution point (graph section toolbar). */
43
50
  export const SCM_HISTORY_TITLE_MENU: MenuPath = ['plugin_scm/history/title'];
@@ -54,6 +61,19 @@ const LANE_WIDTH = 22;
54
61
  /** Y position of the commit dot — vertically centered in the row. */
55
62
  const DOT_CY = 11;
56
63
 
64
+ /** Indentation of one level of the change tree, in pixels. */
65
+ const TREE_INDENT = 8;
66
+ /** Shared empty set for history items whose change tree is fully expanded. */
67
+ const EMPTY_FOLDER_SET: ReadonlySet<string> = new Set<string>();
68
+
69
+ /**
70
+ * Indentation for a row of the change tree. Like Theia's tree widget, the depth
71
+ * is applied as padding rather than through a class, as it is not known upfront.
72
+ */
73
+ function indentStyle(depth: number): React.CSSProperties | undefined {
74
+ return depth > 0 ? { paddingLeft: `${4 + depth * TREE_INDENT}px` } : undefined;
75
+ }
76
+
57
77
  /** Renders a ref badge as a JSX element for the commit row. */
58
78
  function renderJsxRefBadge(
59
79
  ref: ScmHistoryItemRef,
@@ -80,29 +100,44 @@ function renderJsxRefBadge(
80
100
  // ── Widget ──────────────────────────────────────────────────────────────────
81
101
 
82
102
  @injectable()
83
- export class ScmHistoryGraphWidget extends ReactWidget {
103
+ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbarWidget {
84
104
 
85
105
  static readonly ID = 'scm-history-graph-widget';
86
106
  static readonly LABEL = nls.localizeByDefault('Graph');
87
107
 
88
108
  @inject(ScmHistoryGraphModel) protected readonly model: ScmHistoryGraphModel;
89
109
  @inject(HoverService) protected readonly hoverService: HoverService;
110
+ @inject(ClipboardService) protected readonly clipboardService: ClipboardService;
90
111
  @inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory;
91
112
  @inject(ScmService) protected readonly scmService: ScmService;
92
113
  @inject(LabelProvider) protected readonly labelProvider: LabelProvider;
93
114
  @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
94
115
  @inject(OpenerService) protected readonly openerService: OpenerService;
95
116
  @inject(ScmContextKeyService) protected readonly scmContextKeys: ScmContextKeyService;
117
+ @inject(ScmPreferences) protected readonly scmPreferences: ScmPreferences;
118
+
119
+ @inject(ILogger) @named('scm:ScmHistoryGraphWidget')
120
+ protected readonly logger: ILogger;
96
121
 
97
122
  protected selectedIndex = -1;
123
+ /** Handle on the virtualized list, used to scroll an entry into view. */
124
+ protected readonly listRef = React.createRef<VirtuosoHandle>();
98
125
  /** Currently selected change row key (`${itemId}-${ci}`), or undefined. */
99
126
  protected selectedChangeKey: string | undefined;
100
127
  /** Map from commit id → loaded changes (undefined = not loaded yet). */
101
128
  protected expandedChanges = new Map<string, ScmHistoryItemChange[] | 'loading'>();
102
129
  /** Set of commit ids that are currently expanded. */
103
130
  protected expandedIds = new Set<string>();
131
+ /** Map from commit id → folder paths collapsed in its change tree. */
132
+ protected collapsedFolders = new Map<string, Set<string>>();
104
133
  /** Map from commit id → in-flight CancellationTokenSource for loadChanges. */
105
134
  protected loadChangesCts = new Map<string, CancellationTokenSource>();
135
+ /** Cleanup for the content of the hover currently being shown. */
136
+ protected readonly toDisposeOnHover = new DisposableCollection();
137
+
138
+ protected readonly onDidChangeToolbarItemsEmitter = new Emitter<void>();
139
+ /** Re-renders the part toolbar on model changes, e.g. to reflect the toggled state of the ref filter picker. */
140
+ readonly onDidChangeToolbarItems: Event<void> = this.onDidChangeToolbarItemsEmitter.event;
106
141
 
107
142
  constructor() {
108
143
  super();
@@ -116,10 +151,22 @@ export class ScmHistoryGraphWidget extends ReactWidget {
116
151
 
117
152
  @postConstruct()
118
153
  protected init(): void {
154
+ this.toDispose.push(this.onDidChangeToolbarItemsEmitter);
119
155
  this.toDispose.push(
120
156
  this.model.onDidChange(() => {
121
157
  this.updateContextKeys();
122
158
  this.update();
159
+ this.onDidChangeToolbarItemsEmitter.fire();
160
+ })
161
+ );
162
+ this.toDispose.push(
163
+ this.model.onDidRequestReveal(index => this.revealIndex(index))
164
+ );
165
+ this.toDispose.push(
166
+ this.scmPreferences.onPreferenceChanged(e => {
167
+ if (e.preferenceName.startsWith('scm.graph.')) {
168
+ this.update();
169
+ }
123
170
  })
124
171
  );
125
172
  this.toDispose.push({
@@ -131,6 +178,15 @@ export class ScmHistoryGraphWidget extends ReactWidget {
131
178
  this.loadChangesCts.clear();
132
179
  }
133
180
  });
181
+ this.toDispose.push(this.toDisposeOnHover);
182
+ this.update();
183
+ }
184
+
185
+ /** Scrolls the entry at `index` into view and selects it. */
186
+ protected revealIndex(index: number): void {
187
+ this.selectedIndex = index;
188
+ this.selectedChangeKey = undefined;
189
+ this.listRef.current?.scrollToIndex({ index, align: 'center' });
134
190
  this.update();
135
191
  }
136
192
 
@@ -138,6 +194,8 @@ export class ScmHistoryGraphWidget extends ReactWidget {
138
194
  const provider = this.model.provider;
139
195
  this.scmContextKeys.scmCurrentHistoryItemRefHasRemote.set(!!provider?.currentHistoryItemRemoteRef);
140
196
  this.scmContextKeys.scmCurrentHistoryItemRefHasBase.set(!!provider?.currentHistoryItemBaseRef);
197
+ // Commands like git.pullRef/git.pushRef gate their enablement on this key.
198
+ this.scmContextKeys.scmCurrentHistoryItemRefInFilter.set(!!provider?.currentHistoryItemRef && this.model.isCurrentRefInFilter());
141
199
  }
142
200
 
143
201
  protected render(): React.ReactNode {
@@ -186,10 +244,11 @@ export class ScmHistoryGraphWidget extends ReactWidget {
186
244
 
187
245
  return (
188
246
  <Virtuoso
247
+ ref={this.listRef}
189
248
  className='scm-history-graph-list'
190
249
  data={entries as HistoryGraphEntry[]}
191
250
  itemContent={(idx, entry) => this.renderRow(entry, idx, svgWidth)}
192
- endReached={hasMore && !loading ? this.handleEndReached : undefined}
251
+ endReached={hasMore && !loading && this.scmPreferences['scm.graph.pageOnScroll'] !== false ? this.handleEndReached : undefined}
193
252
  overscan={500}
194
253
  components={footer ? { Footer: footer } : {}}
195
254
  style={{ overflowX: 'hidden' }}
@@ -208,8 +267,7 @@ export class ScmHistoryGraphWidget extends ReactWidget {
208
267
  const isSelected = idx === this.selectedIndex;
209
268
  const isExpanded = this.expandedIds.has(item.id);
210
269
  const changes = this.expandedChanges.get(item.id);
211
- // The first commit (idx === 0) on lane 0 is treated as HEAD/current
212
- const isHead = idx === 0 && graphRow.lane === 0;
270
+ const isHead = entry.isCurrent;
213
271
 
214
272
  return (
215
273
  <React.Fragment key={item.id}>
@@ -244,14 +302,49 @@ export class ScmHistoryGraphWidget extends ReactWidget {
244
302
  }
245
303
 
246
304
  protected handleRowMouseEnter = (e: React.MouseEvent<HTMLDivElement>, entry: HistoryGraphEntry): void => {
305
+ this.toDisposeOnHover.dispose();
247
306
  this.hoverService.requestHover({
248
- content: buildHtmlTooltip(entry, this.markdownRenderer),
307
+ content: this.buildTooltip(entry),
249
308
  target: e.currentTarget,
250
309
  position: 'right',
251
310
  interactive: true,
252
311
  });
253
312
  };
254
313
 
314
+ /**
315
+ * Prefers the hover content supplied by the provider, so that its own links and commands
316
+ * (author `mailto:`, "Open Commit", remote actions) stay intact, and falls back to the hover
317
+ * derived from the history item's fields for providers that supply none.
318
+ */
319
+ protected buildTooltip(entry: HistoryGraphEntry): HTMLElement {
320
+ const { tooltip } = entry.item;
321
+ if (tooltip) {
322
+ const provided = buildProviderTooltip(tooltip, this.markdownRenderer, this.openerService, this.toDisposeOnHover);
323
+ if (provided) {
324
+ return provided;
325
+ }
326
+ }
327
+ return buildHtmlTooltip(entry, this.markdownRenderer, this.model.provider, {
328
+ openCommit: () => this.openCommitFromTooltip(entry),
329
+ copyCommitHash: () => this.clipboardService.writeText(entry.item.id),
330
+ });
331
+ }
332
+
333
+ /** Selects the commit and expands its changes, closing the hover first. */
334
+ protected openCommitFromTooltip(entry: HistoryGraphEntry): void {
335
+ this.hoverService.cancelHover();
336
+ const idx = this.model.entries.indexOf(entry);
337
+ if (idx < 0) {
338
+ return;
339
+ }
340
+ if (this.expandedIds.has(entry.item.id)) {
341
+ this.selectedIndex = idx;
342
+ this.update();
343
+ } else {
344
+ this.handleRowClick(idx, entry);
345
+ }
346
+ }
347
+
255
348
  protected renderChangesRows(
256
349
  itemId: string,
257
350
  svgWidth: number,
@@ -274,6 +367,18 @@ export class ScmHistoryGraphWidget extends ReactWidget {
274
367
  );
275
368
  }
276
369
 
370
+ if (this.model.viewMode === 'tree') {
371
+ const rootUri = this.scmService.selectedRepository?.provider.rootUri;
372
+ const rows = buildChangeTreeRows(changes, rootUri, this.collapsedFolders.get(itemId) ?? EMPTY_FOLDER_SET);
373
+ return (
374
+ <React.Fragment key={`${itemId}-changes`}>
375
+ {rows.map(row => row.type === 'folder'
376
+ ? this.renderChangeFolderRow(row, itemId, svgWidth, graphRow)
377
+ : this.renderChangeRow(row.change, row.index, itemId, svgWidth, graphRow, row.depth))}
378
+ </React.Fragment>
379
+ );
380
+ }
381
+
277
382
  return (
278
383
  <React.Fragment key={`${itemId}-changes`}>
279
384
  {changes.map((change, ci) =>
@@ -283,18 +388,66 @@ export class ScmHistoryGraphWidget extends ReactWidget {
283
388
  );
284
389
  }
285
390
 
391
+ protected renderChangeFolderRow(
392
+ row: ChangeTreeFolderRow,
393
+ itemId: string,
394
+ svgWidth: number,
395
+ graphRow: GraphRow
396
+ ): React.ReactElement {
397
+ const collapsed = this.isFolderCollapsed(itemId, row.path);
398
+ return (
399
+ <div
400
+ key={`${itemId}-folder-${row.path}`}
401
+ className='scm-history-change-row scm-history-change-folder-row'
402
+ onClick={e => this.handleFolderClick(e, itemId, row.path)}
403
+ role='treeitem'
404
+ aria-expanded={!collapsed}
405
+ tabIndex={-1}
406
+ >
407
+ {this.renderChangeRowSvg(graphRow, svgWidth)}
408
+ <div className='scm-history-change-info' style={indentStyle(row.depth)}>
409
+ <span className={`codicon ${collapsed ? 'codicon-chevron-right' : 'codicon-chevron-down'} scm-history-change-twistie`} />
410
+ <span className='name scm-history-change-name' title={row.path}>{row.label}</span>
411
+ </div>
412
+ </div>
413
+ );
414
+ }
415
+
416
+ protected isFolderCollapsed(itemId: string, path: string): boolean {
417
+ return !!this.collapsedFolders.get(itemId)?.has(path);
418
+ }
419
+
420
+ protected toggleFolder(itemId: string, path: string): void {
421
+ let collapsed = this.collapsedFolders.get(itemId);
422
+ if (!collapsed) {
423
+ collapsed = new Set<string>();
424
+ this.collapsedFolders.set(itemId, collapsed);
425
+ }
426
+ if (!collapsed.delete(path)) {
427
+ collapsed.add(path);
428
+ }
429
+ this.update();
430
+ }
431
+
432
+ protected handleFolderClick = (e: React.MouseEvent, itemId: string, path: string): void => {
433
+ e.stopPropagation();
434
+ this.toggleFolder(itemId, path);
435
+ };
436
+
286
437
  protected renderChangeRow(
287
438
  change: ScmHistoryItemChange,
288
439
  ci: number,
289
440
  itemId: string,
290
441
  svgWidth: number,
291
- graphRow: GraphRow
442
+ graphRow: GraphRow,
443
+ depth = 0
292
444
  ): React.ReactElement {
293
445
  const rootUri = this.scmService.selectedRepository?.provider.rootUri;
294
446
  const uri = change.modifiedUri ?? change.originalUri ?? change.uri;
295
447
  const relativePath = getRepoRelativePath(uri, rootUri);
296
448
  const fileName = getFileName(relativePath);
297
- const dirPath = relativePath.includes('/')
449
+ // In tree mode the folder rows already carry the directory.
450
+ const dirPath = this.model.viewMode === 'list' && relativePath.includes('/')
298
451
  ? relativePath.slice(0, relativePath.lastIndexOf('/'))
299
452
  : '';
300
453
  const status = getChangeStatus(change);
@@ -313,7 +466,7 @@ export class ScmHistoryGraphWidget extends ReactWidget {
313
466
  tabIndex={-1}
314
467
  >
315
468
  {this.renderChangeRowSvg(graphRow, svgWidth)}
316
- <div className='scm-history-change-info'>
469
+ <div className='scm-history-change-info' style={indentStyle(depth)}>
317
470
  <span className={`${fileIcon} file-icon scm-history-change-file-icon`} />
318
471
  <div className='scm-history-change-name-container'>
319
472
  <span className='name scm-history-change-name' title={relativePath}>{fileName}</span>
@@ -347,7 +500,7 @@ export class ScmHistoryGraphWidget extends ReactWidget {
347
500
  open(this.openerService, new URI(change.uri));
348
501
  }
349
502
  } catch (err) {
350
- console.error('ScmHistoryGraphWidget: failed to open change', err);
503
+ this.logger.error('Failed to open change', err);
351
504
  }
352
505
  }
353
506
 
@@ -462,7 +615,9 @@ export class ScmHistoryGraphWidget extends ReactWidget {
462
615
 
463
616
  // Commit lane lines — split at cy=11
464
617
  // Top segment: only drawn if there is an incoming line from above
465
- // (i.e. this commit was referenced as a parent by an earlier row)
618
+ // (i.e. this commit was referenced as a parent by an earlier row).
619
+ // It keeps the color inherited from the chain above, which may differ
620
+ // from the commit's own (ref-based) color.
466
621
  if (row.hasTopLine) {
467
622
  elements.push(
468
623
  <path
@@ -471,7 +626,7 @@ export class ScmHistoryGraphWidget extends ReactWidget {
471
626
  strokeWidth='1px'
472
627
  strokeLinecap='round'
473
628
  d={`M ${commitX} 0 V ${cy}`}
474
- style={{ stroke: commitColor }}
629
+ style={{ stroke: laneColor(row.topColor) }}
475
630
  />
476
631
  );
477
632
  }
@@ -560,11 +715,14 @@ export class ScmHistoryGraphWidget extends ReactWidget {
560
715
  return undefined;
561
716
  }
562
717
 
718
+ const provider = this.model.provider;
719
+ const visibleRefs = filterRefsForBadges(refs, provider, this.scmPreferences['scm.graph.badges'] ?? 'filter', this.model.historyItemRefFilter);
720
+ if (visibleRefs.length === 0) {
721
+ return undefined;
722
+ }
563
723
  const laneColorValue = entry ? laneColor(entry.graphRow.color) : undefined;
564
- const deduplicated = deduplicateRefs(refs);
565
- const style: React.CSSProperties = laneColorValue
566
- ? { backgroundColor: laneColorValue, color: 'var(--theia-scmGraph-historyItemRefForeground, var(--theia-badge-foreground))' }
567
- : {};
724
+ const deduplicated = deduplicateRefs(visibleRefs);
725
+ const badgeForeground = 'var(--theia-scmGraph-historyItemRefForeground, var(--theia-badge-foreground))';
568
726
 
569
727
  const badges: React.ReactElement[] = [];
570
728
  for (const info of deduplicated) {
@@ -576,24 +734,25 @@ export class ScmHistoryGraphWidget extends ReactWidget {
576
734
  const isRemote = isRemoteRef(ref);
577
735
  const onContextMenu = entry ? (e: React.MouseEvent) => this.handleRefBadgeContextMenu(e, entry, ref) : undefined;
578
736
 
579
- if (isTag) {
580
- badges.push(renderJsxRefBadge(ref, 'codicon-tag', false, style, ref.id, onContextMenu));
581
- } else if (isRemote) {
582
- badges.push(renderJsxRefBadge(ref, 'codicon-cloud', true, style, ref.id, onContextMenu));
583
- } else {
584
- badges.push(renderJsxRefBadge(ref, 'codicon-git-branch', true, style, ref.id, onContextMenu));
585
- if (hasBoth && badges.length < 3) {
586
- badges.push(
587
- <span
588
- key={`${ref.id}-cloud`}
589
- className='scm-history-ref-badge scm-history-ref-badge-cloud'
590
- title={ref.description ?? ref.name}
591
- style={style}
592
- >
593
- <i className='codicon codicon-cloud scm-history-ref-icon' />
594
- </span>
595
- );
596
- }
737
+ // Current/remote/base refs are colored by role, others by the row's lane color
738
+ const { iconClass, colorIndex } = getRefBadgePresentation(ref, provider);
739
+ const backgroundColor = colorIndex !== undefined ? laneColor(colorIndex) : laneColorValue;
740
+ const style: React.CSSProperties = backgroundColor
741
+ ? { backgroundColor, color: badgeForeground }
742
+ : {};
743
+
744
+ badges.push(renderJsxRefBadge(ref, iconClass, !isTag, style, ref.id, onContextMenu));
745
+ if (!isTag && !isRemote && hasBoth && badges.length < 3) {
746
+ badges.push(
747
+ <span
748
+ key={`${ref.id}-cloud`}
749
+ className='scm-history-ref-badge scm-history-ref-badge-cloud'
750
+ title={ref.description ?? ref.name}
751
+ style={style}
752
+ >
753
+ <i className='codicon codicon-cloud scm-history-ref-icon' />
754
+ </span>
755
+ );
597
756
  }
598
757
  }
599
758
  return badges;
@@ -650,6 +809,7 @@ export class ScmHistoryGraphWidget extends ReactWidget {
650
809
 
651
810
  if (this.expandedIds.has(item.id)) {
652
811
  this.expandedIds.delete(item.id);
812
+ this.collapsedFolders.delete(item.id);
653
813
  const cts = this.loadChangesCts.get(item.id);
654
814
  if (cts) {
655
815
  cts.cancel();
@@ -690,7 +850,7 @@ export class ScmHistoryGraphWidget extends ReactWidget {
690
850
  }
691
851
  } catch (err) {
692
852
  if (!cts.token.isCancellationRequested) {
693
- console.error('ScmHistoryGraphWidget: failed to load changes', err);
853
+ this.logger.error('Failed to load changes', err);
694
854
  this.expandedChanges.set(item.id, []);
695
855
  this.update();
696
856
  }
@@ -19,6 +19,7 @@
19
19
  import { Disposable, Event } from '@theia/core/lib/common';
20
20
  import URI from '@theia/core/lib/common/uri';
21
21
  import { CancellationToken } from '@theia/core/lib/common/cancellation';
22
+ import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string';
22
23
 
23
24
  export interface ScmProvider extends Disposable {
24
25
  readonly id: string;
@@ -145,7 +146,14 @@ export interface ScmHistoryItem {
145
146
  readonly authorIcon?: string;
146
147
  readonly displayId?: string;
147
148
  readonly timestamp?: number;
148
- readonly tooltip?: string;
149
+ /**
150
+ * Hover content supplied by the provider. When present, it is rendered instead of the
151
+ * hover the history graph derives from the other fields, so that providers stay in control
152
+ * of their own links and commands. May be split into sections, which are rendered separated
153
+ * by horizontal rules. Note that `command:` links are only executed if the containing
154
+ * {@link MarkdownString} declares them via {@link MarkdownString.isTrusted}.
155
+ */
156
+ readonly tooltip?: string | MarkdownString | readonly MarkdownString[];
149
157
  readonly statistics?: ScmHistoryItemStatistics;
150
158
  readonly references?: readonly ScmHistoryItemRef[];
151
159
  }
@@ -200,6 +200,16 @@
200
200
  flex-shrink: 0;
201
201
  }
202
202
 
203
+ /* Folder rows of the change tree ('View as Tree'). */
204
+ .scm-history-change-twistie {
205
+ flex-shrink: 0;
206
+ opacity: 0.8;
207
+ }
208
+
209
+ .scm-history-change-folder-row .scm-history-change-name {
210
+ flex: 1;
211
+ }
212
+
203
213
  .scm-history-change-name-container {
204
214
  flex: 1;
205
215
  display: flex;
@@ -286,18 +296,59 @@
286
296
  opacity: 0.6;
287
297
  }
288
298
 
299
+ /* Markdown rendered into the tooltip keeps the browser's default paragraph margins,
300
+ which spaces the commit hover out far more than the git blame hover it mirrors. */
301
+ .scm-history-tooltip p {
302
+ margin: 0;
303
+ }
304
+
305
+ .scm-history-tooltip p + p {
306
+ margin-top: 4px;
307
+ }
308
+
289
309
  /* ── Tooltip header icon alignment ──────────────────────────────────────── */
310
+ .scm-history-tooltip-header {
311
+ margin-bottom: 4px;
312
+ }
313
+
290
314
  .scm-history-tooltip-header .icon-inline {
291
315
  vertical-align: middle;
292
316
  }
293
317
 
318
+ .scm-history-tooltip-header a {
319
+ color: var(--theia-textLink-foreground);
320
+ text-decoration: none;
321
+ }
322
+
323
+ .scm-history-tooltip-header a:hover {
324
+ text-decoration: underline;
325
+ }
326
+
327
+ .scm-history-tooltip-avatar {
328
+ width: 20px;
329
+ height: 20px;
330
+ border-radius: 50%;
331
+ vertical-align: middle;
332
+ }
333
+
334
+ /* ── Tooltip actions (aligned with the git blame hover command links) ───── */
335
+ .scm-history-tooltip-actions a.scm-history-tooltip-action {
336
+ color: var(--theia-textLink-foreground);
337
+ cursor: pointer;
338
+ text-decoration: none;
339
+ }
340
+
341
+ .scm-history-tooltip-actions a.scm-history-tooltip-action:hover {
342
+ text-decoration: underline;
343
+ }
344
+
294
345
  /* ── Tooltip stat colors (used inside .theia-hover) ─────────────────────── */
295
346
  .scm-history-stat-added {
296
- color: var(--theia-gitDecoration-addedResourceForeground, #388a34);
347
+ color: var(--theia-scmGraph-historyItemHoverAdditionsForeground, var(--theia-gitDecoration-addedResourceForeground, #388a34));
297
348
  }
298
349
 
299
350
  .scm-history-stat-deleted {
300
- color: var(--theia-gitDecoration-deletedResourceForeground, #a1260d);
351
+ color: var(--theia-scmGraph-historyItemHoverDeletionsForeground, var(--theia-gitDecoration-deletedResourceForeground, #a1260d));
301
352
  }
302
353
 
303
354
  /* ── Empty state ─────────────────────────────────────────────────────────── */
@@ -35,12 +35,39 @@ export const scmPreferenceSchema: PreferenceSchema = {
35
35
  ],
36
36
  description: nls.localizeByDefault('Controls the default Source Control repository view mode.'),
37
37
  default: 'list'
38
+ },
39
+ 'scm.graph.badges': {
40
+ type: 'string',
41
+ enum: ['all', 'filter'],
42
+ enumDescriptions: [
43
+ nls.localizeByDefault('Show badges of all history item groups in the Source Control Graph view.'),
44
+ nls.localizeByDefault('Show only the badges of history item groups used as a filter in the Source Control Graph view.')
45
+ ],
46
+ description: nls.localizeByDefault(
47
+ // eslint-disable-next-line max-len
48
+ 'Controls which badges are shown in the Source Control Graph view. The badges are shown on the right side of the graph indicating the names of history item groups.'),
49
+ default: 'filter'
50
+ },
51
+ 'scm.graph.pageOnScroll': {
52
+ type: 'boolean',
53
+ description: nls.localizeByDefault('Controls whether the Source Control Graph view will load the next page of items when you scroll to the end of the list.'),
54
+ default: true
55
+ },
56
+ 'scm.graph.pageSize': {
57
+ type: 'number',
58
+ minimum: 1,
59
+ maximum: 1000,
60
+ description: nls.localizeByDefault('The number of items to show in the Source Control Graph view by default and when loading more items.'),
61
+ default: 50
38
62
  }
39
63
  }
40
64
  };
41
65
 
42
66
  export interface ScmConfiguration {
43
67
  'scm.defaultViewMode': 'tree' | 'list'
68
+ 'scm.graph.badges': 'all' | 'filter'
69
+ 'scm.graph.pageOnScroll': boolean
70
+ 'scm.graph.pageSize': number
44
71
  }
45
72
 
46
73
  export const ScmPreferenceContribution = Symbol('ScmPreferenceContribution');