@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.
- package/lib/browser/scm-history-graph-contribution.d.ts +5 -0
- package/lib/browser/scm-history-graph-contribution.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-contribution.js +54 -0
- package/lib/browser/scm-history-graph-contribution.js.map +1 -1
- package/lib/browser/scm-history-graph-contribution.spec.js +58 -5
- package/lib/browser/scm-history-graph-contribution.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-helpers.d.ts +25 -0
- package/lib/browser/scm-history-graph-helpers.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-helpers.js +58 -0
- package/lib/browser/scm-history-graph-helpers.js.map +1 -1
- package/lib/browser/scm-history-graph-helpers.spec.js +58 -0
- package/lib/browser/scm-history-graph-helpers.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-model.d.ts +24 -2
- package/lib/browser/scm-history-graph-model.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-model.js +47 -0
- package/lib/browser/scm-history-graph-model.js.map +1 -1
- package/lib/browser/scm-history-graph-model.spec.js +102 -0
- package/lib/browser/scm-history-graph-model.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-widget.d.ts +13 -1
- package/lib/browser/scm-history-graph-widget.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-widget.js +62 -4
- package/lib/browser/scm-history-graph-widget.js.map +1 -1
- package/lib/browser/scm-history-graph-widget.spec.js +162 -0
- package/lib/browser/scm-history-graph-widget.spec.js.map +1 -1
- package/package.json +6 -6
- package/src/browser/scm-history-graph-contribution.spec.ts +71 -6
- package/src/browser/scm-history-graph-contribution.ts +51 -0
- package/src/browser/scm-history-graph-helpers.spec.ts +77 -2
- package/src/browser/scm-history-graph-helpers.ts +99 -0
- package/src/browser/scm-history-graph-model.spec.ts +131 -0
- package/src/browser/scm-history-graph-model.ts +56 -1
- package/src/browser/scm-history-graph-widget.spec.ts +199 -1
- package/src/browser/scm-history-graph-widget.tsx +96 -5
- 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
|
-
|
|
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;
|