@theia/scm 1.75.0-next.35 → 1.75.0-next.39

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 (34) hide show
  1. package/lib/browser/scm-history-graph-contribution.d.ts +5 -0
  2. package/lib/browser/scm-history-graph-contribution.d.ts.map +1 -1
  3. package/lib/browser/scm-history-graph-contribution.js +54 -0
  4. package/lib/browser/scm-history-graph-contribution.js.map +1 -1
  5. package/lib/browser/scm-history-graph-contribution.spec.js +58 -5
  6. package/lib/browser/scm-history-graph-contribution.spec.js.map +1 -1
  7. package/lib/browser/scm-history-graph-helpers.d.ts +25 -0
  8. package/lib/browser/scm-history-graph-helpers.d.ts.map +1 -1
  9. package/lib/browser/scm-history-graph-helpers.js +58 -0
  10. package/lib/browser/scm-history-graph-helpers.js.map +1 -1
  11. package/lib/browser/scm-history-graph-helpers.spec.js +58 -0
  12. package/lib/browser/scm-history-graph-helpers.spec.js.map +1 -1
  13. package/lib/browser/scm-history-graph-model.d.ts +24 -2
  14. package/lib/browser/scm-history-graph-model.d.ts.map +1 -1
  15. package/lib/browser/scm-history-graph-model.js +47 -0
  16. package/lib/browser/scm-history-graph-model.js.map +1 -1
  17. package/lib/browser/scm-history-graph-model.spec.js +102 -0
  18. package/lib/browser/scm-history-graph-model.spec.js.map +1 -1
  19. package/lib/browser/scm-history-graph-widget.d.ts +13 -1
  20. package/lib/browser/scm-history-graph-widget.d.ts.map +1 -1
  21. package/lib/browser/scm-history-graph-widget.js +62 -4
  22. package/lib/browser/scm-history-graph-widget.js.map +1 -1
  23. package/lib/browser/scm-history-graph-widget.spec.js +162 -0
  24. package/lib/browser/scm-history-graph-widget.spec.js.map +1 -1
  25. package/package.json +6 -6
  26. package/src/browser/scm-history-graph-contribution.spec.ts +71 -6
  27. package/src/browser/scm-history-graph-contribution.ts +51 -0
  28. package/src/browser/scm-history-graph-helpers.spec.ts +77 -2
  29. package/src/browser/scm-history-graph-helpers.ts +99 -0
  30. package/src/browser/scm-history-graph-model.spec.ts +131 -0
  31. package/src/browser/scm-history-graph-model.ts +56 -1
  32. package/src/browser/scm-history-graph-widget.spec.ts +199 -1
  33. package/src/browser/scm-history-graph-widget.tsx +96 -5
  34. package/src/browser/style/scm-history-graph.css +10 -0
@@ -15,7 +15,7 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import * as React from '@theia/core/shared/react';
18
- import { Virtuoso } from '@theia/core/shared/react-virtuoso';
18
+ import { Virtuoso, VirtuosoHandle } from '@theia/core/shared/react-virtuoso';
19
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';
@@ -40,7 +40,8 @@ import { ScmContextKeyService } from './scm-context-key-service';
40
40
  import { ScmPreferences } from '../common/scm-preferences';
41
41
  import {
42
42
  laneColor, filterRefsForBadges, getChangeStatus, getFileName, getFilePath, getRepoRelativePath,
43
- getRefBadgeClass, getRefBadgePresentation, isTagRef, isRemoteRef, deduplicateRefs, DeduplicatedRef
43
+ getRefBadgeClass, getRefBadgePresentation, isTagRef, isRemoteRef, deduplicateRefs, DeduplicatedRef,
44
+ buildChangeTreeRows, ChangeTreeFolderRow
44
45
  } from './scm-history-graph-helpers';
45
46
  import { ILogger } from '@theia/core';
46
47
  import { buildHtmlTooltip, buildProviderTooltip } from './scm-history-graph-tooltip';
@@ -60,6 +61,19 @@ const LANE_WIDTH = 22;
60
61
  /** Y position of the commit dot — vertically centered in the row. */
61
62
  const DOT_CY = 11;
62
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
+
63
77
  /** Renders a ref badge as a JSX element for the commit row. */
64
78
  function renderJsxRefBadge(
65
79
  ref: ScmHistoryItemRef,
@@ -106,12 +120,16 @@ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbar
106
120
  protected readonly logger: ILogger;
107
121
 
108
122
  protected selectedIndex = -1;
123
+ /** Handle on the virtualized list, used to scroll an entry into view. */
124
+ protected readonly listRef = React.createRef<VirtuosoHandle>();
109
125
  /** Currently selected change row key (`${itemId}-${ci}`), or undefined. */
110
126
  protected selectedChangeKey: string | undefined;
111
127
  /** Map from commit id → loaded changes (undefined = not loaded yet). */
112
128
  protected expandedChanges = new Map<string, ScmHistoryItemChange[] | 'loading'>();
113
129
  /** Set of commit ids that are currently expanded. */
114
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>>();
115
133
  /** Map from commit id → in-flight CancellationTokenSource for loadChanges. */
116
134
  protected loadChangesCts = new Map<string, CancellationTokenSource>();
117
135
  /** Cleanup for the content of the hover currently being shown. */
@@ -141,6 +159,9 @@ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbar
141
159
  this.onDidChangeToolbarItemsEmitter.fire();
142
160
  })
143
161
  );
162
+ this.toDispose.push(
163
+ this.model.onDidRequestReveal(index => this.revealIndex(index))
164
+ );
144
165
  this.toDispose.push(
145
166
  this.scmPreferences.onPreferenceChanged(e => {
146
167
  if (e.preferenceName.startsWith('scm.graph.')) {
@@ -161,6 +182,14 @@ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbar
161
182
  this.update();
162
183
  }
163
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' });
190
+ this.update();
191
+ }
192
+
164
193
  protected updateContextKeys(): void {
165
194
  const provider = this.model.provider;
166
195
  this.scmContextKeys.scmCurrentHistoryItemRefHasRemote.set(!!provider?.currentHistoryItemRemoteRef);
@@ -215,6 +244,7 @@ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbar
215
244
 
216
245
  return (
217
246
  <Virtuoso
247
+ ref={this.listRef}
218
248
  className='scm-history-graph-list'
219
249
  data={entries as HistoryGraphEntry[]}
220
250
  itemContent={(idx, entry) => this.renderRow(entry, idx, svgWidth)}
@@ -337,6 +367,18 @@ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbar
337
367
  );
338
368
  }
339
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
+
340
382
  return (
341
383
  <React.Fragment key={`${itemId}-changes`}>
342
384
  {changes.map((change, ci) =>
@@ -346,18 +388,66 @@ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbar
346
388
  );
347
389
  }
348
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
+
349
437
  protected renderChangeRow(
350
438
  change: ScmHistoryItemChange,
351
439
  ci: number,
352
440
  itemId: string,
353
441
  svgWidth: number,
354
- graphRow: GraphRow
442
+ graphRow: GraphRow,
443
+ depth = 0
355
444
  ): React.ReactElement {
356
445
  const rootUri = this.scmService.selectedRepository?.provider.rootUri;
357
446
  const uri = change.modifiedUri ?? change.originalUri ?? change.uri;
358
447
  const relativePath = getRepoRelativePath(uri, rootUri);
359
448
  const fileName = getFileName(relativePath);
360
- const dirPath = relativePath.includes('/')
449
+ // In tree mode the folder rows already carry the directory.
450
+ const dirPath = this.model.viewMode === 'list' && relativePath.includes('/')
361
451
  ? relativePath.slice(0, relativePath.lastIndexOf('/'))
362
452
  : '';
363
453
  const status = getChangeStatus(change);
@@ -376,7 +466,7 @@ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbar
376
466
  tabIndex={-1}
377
467
  >
378
468
  {this.renderChangeRowSvg(graphRow, svgWidth)}
379
- <div className='scm-history-change-info'>
469
+ <div className='scm-history-change-info' style={indentStyle(depth)}>
380
470
  <span className={`${fileIcon} file-icon scm-history-change-file-icon`} />
381
471
  <div className='scm-history-change-name-container'>
382
472
  <span className='name scm-history-change-name' title={relativePath}>{fileName}</span>
@@ -719,6 +809,7 @@ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbar
719
809
 
720
810
  if (this.expandedIds.has(item.id)) {
721
811
  this.expandedIds.delete(item.id);
812
+ this.collapsedFolders.delete(item.id);
722
813
  const cts = this.loadChangesCts.get(item.id);
723
814
  if (cts) {
724
815
  cts.cancel();
@@ -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;