@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.
- package/lib/browser/decorations/scm-navigator-decorator.d.ts.map +1 -1
- package/lib/browser/decorations/scm-navigator-decorator.js +1 -0
- package/lib/browser/decorations/scm-navigator-decorator.js.map +1 -1
- package/lib/browser/merge-editor/view/merge-editor-panes/merge-editor-pane-header.js +7 -10
- package/lib/browser/merge-editor/view/merge-editor-panes/merge-editor-pane-header.js.map +1 -1
- package/lib/browser/scm-action-button-widget.js +10 -15
- package/lib/browser/scm-action-button-widget.js.map +1 -1
- package/lib/browser/scm-amend-component.js +24 -57
- package/lib/browser/scm-amend-component.js.map +1 -1
- package/lib/browser/scm-commit-widget.d.ts +1 -1
- package/lib/browser/scm-commit-widget.d.ts.map +1 -1
- package/lib/browser/scm-commit-widget.js +9 -10
- package/lib/browser/scm-commit-widget.js.map +1 -1
- package/lib/browser/scm-context-key-service.d.ts +2 -0
- package/lib/browser/scm-context-key-service.d.ts.map +1 -1
- package/lib/browser/scm-context-key-service.js +4 -0
- package/lib/browser/scm-context-key-service.js.map +1 -1
- package/lib/browser/scm-context-key-service.spec.js +7 -0
- package/lib/browser/scm-context-key-service.spec.js.map +1 -1
- package/lib/browser/scm-frontend-module.d.ts.map +1 -1
- package/lib/browser/scm-frontend-module.js +6 -0
- package/lib/browser/scm-frontend-module.js.map +1 -1
- package/lib/browser/scm-history-graph-contribution.d.ts +49 -0
- package/lib/browser/scm-history-graph-contribution.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-contribution.js +280 -0
- package/lib/browser/scm-history-graph-contribution.js.map +1 -0
- package/lib/browser/scm-history-graph-contribution.spec.d.ts +2 -0
- package/lib/browser/scm-history-graph-contribution.spec.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-contribution.spec.js +119 -0
- package/lib/browser/scm-history-graph-contribution.spec.js.map +1 -0
- package/lib/browser/scm-history-graph-helpers.d.ts +59 -6
- package/lib/browser/scm-history-graph-helpers.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-helpers.js +129 -5
- package/lib/browser/scm-history-graph-helpers.js.map +1 -1
- package/lib/browser/scm-history-graph-helpers.spec.d.ts +2 -0
- package/lib/browser/scm-history-graph-helpers.spec.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-helpers.spec.js +154 -0
- package/lib/browser/scm-history-graph-helpers.spec.js.map +1 -0
- package/lib/browser/scm-history-graph-lanes.d.ts +19 -5
- package/lib/browser/scm-history-graph-lanes.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-lanes.js +25 -9
- package/lib/browser/scm-history-graph-lanes.js.map +1 -1
- package/lib/browser/scm-history-graph-lanes.spec.js +62 -25
- package/lib/browser/scm-history-graph-lanes.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-model.d.ts +75 -6
- package/lib/browser/scm-history-graph-model.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-model.js +155 -10
- package/lib/browser/scm-history-graph-model.js.map +1 -1
- package/lib/browser/scm-history-graph-model.spec.js +299 -4
- package/lib/browser/scm-history-graph-model.spec.js.map +1 -1
- package/lib/browser/scm-history-graph-tooltip.d.ts +27 -2
- package/lib/browser/scm-history-graph-tooltip.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-tooltip.js +113 -22
- package/lib/browser/scm-history-graph-tooltip.js.map +1 -1
- package/lib/browser/scm-history-graph-tooltip.spec.d.ts +2 -0
- package/lib/browser/scm-history-graph-tooltip.spec.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-tooltip.spec.js +213 -0
- package/lib/browser/scm-history-graph-tooltip.spec.js.map +1 -0
- package/lib/browser/scm-history-graph-widget.d.ts +36 -2
- package/lib/browser/scm-history-graph-widget.d.ts.map +1 -1
- package/lib/browser/scm-history-graph-widget.js +174 -67
- package/lib/browser/scm-history-graph-widget.js.map +1 -1
- package/lib/browser/scm-history-graph-widget.spec.d.ts +2 -0
- package/lib/browser/scm-history-graph-widget.spec.d.ts.map +1 -0
- package/lib/browser/scm-history-graph-widget.spec.js +273 -0
- package/lib/browser/scm-history-graph-widget.spec.js.map +1 -0
- package/lib/browser/scm-provider.d.ts +9 -1
- package/lib/browser/scm-provider.d.ts.map +1 -1
- package/lib/browser/scm-provider.js.map +1 -1
- package/lib/browser/scm-repositories-widget.js +12 -21
- package/lib/browser/scm-repositories-widget.js.map +1 -1
- package/lib/browser/scm-tree-widget.js +35 -53
- package/lib/browser/scm-tree-widget.js.map +1 -1
- package/lib/common/scm-preferences.d.ts +3 -0
- package/lib/common/scm-preferences.d.ts.map +1 -1
- package/lib/common/scm-preferences.js +24 -0
- package/lib/common/scm-preferences.js.map +1 -1
- package/package.json +7 -7
- package/src/browser/decorations/scm-navigator-decorator.ts +3 -2
- package/src/browser/scm-context-key-service.spec.ts +9 -0
- package/src/browser/scm-context-key-service.ts +6 -0
- package/src/browser/scm-frontend-module.ts +7 -1
- package/src/browser/scm-history-graph-contribution.spec.ts +148 -0
- package/src/browser/scm-history-graph-contribution.ts +273 -0
- package/src/browser/scm-history-graph-helpers.spec.ts +193 -0
- package/src/browser/scm-history-graph-helpers.ts +180 -6
- package/src/browser/scm-history-graph-lanes.spec.ts +70 -25
- package/src/browser/scm-history-graph-lanes.ts +48 -11
- package/src/browser/scm-history-graph-model.spec.ts +378 -12
- package/src/browser/scm-history-graph-model.ts +181 -13
- package/src/browser/scm-history-graph-tooltip.spec.ts +254 -0
- package/src/browser/scm-history-graph-tooltip.ts +144 -24
- package/src/browser/scm-history-graph-widget.spec.ts +322 -0
- package/src/browser/scm-history-graph-widget.tsx +199 -39
- package/src/browser/scm-provider.ts +9 -1
- package/src/browser/style/scm-history-graph.css +53 -2
- 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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
565
|
-
const
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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');
|