@theia/scm 1.75.0-next.18 → 1.75.0-next.21

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 (44) hide show
  1. package/lib/browser/scm-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/scm-frontend-module.js +6 -0
  3. package/lib/browser/scm-frontend-module.js.map +1 -1
  4. package/lib/browser/scm-history-graph-contribution.d.ts +42 -0
  5. package/lib/browser/scm-history-graph-contribution.d.ts.map +1 -0
  6. package/lib/browser/scm-history-graph-contribution.js +220 -0
  7. package/lib/browser/scm-history-graph-contribution.js.map +1 -0
  8. package/lib/browser/scm-history-graph-contribution.spec.d.ts +2 -0
  9. package/lib/browser/scm-history-graph-contribution.spec.d.ts.map +1 -0
  10. package/lib/browser/scm-history-graph-contribution.spec.js +66 -0
  11. package/lib/browser/scm-history-graph-contribution.spec.js.map +1 -0
  12. package/lib/browser/scm-history-graph-helpers.d.ts +8 -0
  13. package/lib/browser/scm-history-graph-helpers.d.ts.map +1 -1
  14. package/lib/browser/scm-history-graph-helpers.js +20 -0
  15. package/lib/browser/scm-history-graph-helpers.js.map +1 -1
  16. package/lib/browser/scm-history-graph-helpers.spec.js +25 -0
  17. package/lib/browser/scm-history-graph-helpers.spec.js.map +1 -1
  18. package/lib/browser/scm-history-graph-model.d.ts +43 -5
  19. package/lib/browser/scm-history-graph-model.d.ts.map +1 -1
  20. package/lib/browser/scm-history-graph-model.js +84 -10
  21. package/lib/browser/scm-history-graph-model.js.map +1 -1
  22. package/lib/browser/scm-history-graph-model.spec.js +145 -4
  23. package/lib/browser/scm-history-graph-model.spec.js.map +1 -1
  24. package/lib/browser/scm-history-graph-widget.d.ts +8 -1
  25. package/lib/browser/scm-history-graph-widget.d.ts.map +1 -1
  26. package/lib/browser/scm-history-graph-widget.js +23 -4
  27. package/lib/browser/scm-history-graph-widget.js.map +1 -1
  28. package/lib/browser/scm-history-graph-widget.spec.js +11 -1
  29. package/lib/browser/scm-history-graph-widget.spec.js.map +1 -1
  30. package/lib/common/scm-preferences.d.ts +3 -0
  31. package/lib/common/scm-preferences.d.ts.map +1 -1
  32. package/lib/common/scm-preferences.js +24 -0
  33. package/lib/common/scm-preferences.js.map +1 -1
  34. package/package.json +6 -6
  35. package/src/browser/scm-frontend-module.ts +7 -1
  36. package/src/browser/scm-history-graph-contribution.spec.ts +83 -0
  37. package/src/browser/scm-history-graph-contribution.ts +220 -0
  38. package/src/browser/scm-history-graph-helpers.spec.ts +34 -1
  39. package/src/browser/scm-history-graph-helpers.ts +25 -0
  40. package/src/browser/scm-history-graph-model.spec.ts +184 -9
  41. package/src/browser/scm-history-graph-model.ts +99 -11
  42. package/src/browser/scm-history-graph-widget.spec.ts +12 -1
  43. package/src/browser/scm-history-graph-widget.tsx +26 -6
  44. package/src/common/scm-preferences.ts +27 -0
@@ -19,12 +19,23 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
19
19
  import { Emitter } from '@theia/core/lib/common/event';
20
20
  import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
21
21
  import { ScmService } from './scm-service';
22
- import { ScmHistoryItem, ScmHistoryProvider, ScmHistoryOptions } from './scm-provider';
22
+ import { ScmHistoryItem, ScmHistoryItemRef, ScmHistoryProvider, ScmHistoryOptions } from './scm-provider';
23
23
  import { computeGraphRows, GraphRow } from './scm-history-graph-lanes';
24
24
  import { getRefColorIndex } from './scm-history-graph-helpers';
25
+ import { ScmPreferences } from '../common/scm-preferences';
25
26
 
26
27
  export const PAGE_SIZE = 50;
27
28
 
29
+ export const ScmHistoryGraphModelProvider = Symbol('ScmHistoryGraphModelProvider');
30
+ /**
31
+ * Resolves the {@link ScmHistoryGraphModel} singleton on first use. The model
32
+ * starts loading history and subscribing to provider events as soon as it is
33
+ * instantiated, so clients that may not need it (e.g. command contributions
34
+ * instantiated at application start) must inject this provider instead of the
35
+ * model itself.
36
+ */
37
+ export type ScmHistoryGraphModelProvider = () => ScmHistoryGraphModel;
38
+
28
39
  export interface HistoryGraphEntry {
29
40
  readonly item: ScmHistoryItem;
30
41
  readonly graphRow: GraphRow;
@@ -36,6 +47,7 @@ export interface HistoryGraphEntry {
36
47
  export class ScmHistoryGraphModel {
37
48
 
38
49
  @inject(ScmService) protected readonly scmService: ScmService;
50
+ @inject(ScmPreferences) protected readonly scmPreferences: ScmPreferences;
39
51
 
40
52
  protected readonly toDispose = new DisposableCollection();
41
53
  protected readonly toDisposeOnProviderChange = new DisposableCollection();
@@ -45,6 +57,8 @@ export class ScmHistoryGraphModel {
45
57
  protected _loading = false;
46
58
  protected _hasAttemptedLoad = false;
47
59
  protected _provider: ScmHistoryProvider | undefined;
60
+ /** Explicitly picked ref ids to filter the graph by; `undefined` = auto (current/remote/base). */
61
+ protected _historyItemRefFilter: string[] | undefined;
48
62
 
49
63
  protected readonly onDidChangeEmitter = new Emitter<void>();
50
64
  readonly onDidChange = this.onDidChangeEmitter.event;
@@ -57,6 +71,11 @@ export class ScmHistoryGraphModel {
57
71
  Disposable.create(() => this.toDisposeOnProviderChange.dispose()),
58
72
  this.onDidChangeEmitter,
59
73
  this.scmService.onDidChangeSelectedRepository(() => this.refresh()),
74
+ this.scmPreferences.onPreferenceChanged(e => {
75
+ if (e.preferenceName === 'scm.graph.pageSize') {
76
+ this.reload();
77
+ }
78
+ }),
60
79
  ]);
61
80
  this.refresh();
62
81
  }
@@ -70,6 +89,32 @@ export class ScmHistoryGraphModel {
70
89
  return this._provider;
71
90
  }
72
91
 
92
+ /** The explicitly picked ref ids filtering the graph, or `undefined` in auto mode. */
93
+ get historyItemRefFilter(): readonly string[] | undefined {
94
+ return this._historyItemRefFilter;
95
+ }
96
+
97
+ /**
98
+ * Sets the ref ids to filter the graph by (`undefined` returns to auto
99
+ * mode) and reloads the graph.
100
+ */
101
+ setHistoryItemRefFilter(refIds: readonly string[] | undefined): void {
102
+ this._historyItemRefFilter = refIds && refIds.length > 0 ? [...refIds] : undefined;
103
+ this.reload();
104
+ }
105
+
106
+ /**
107
+ * Whether the current history item ref is part of the graph's filter.
108
+ * Always true in auto mode, which includes the current ref.
109
+ */
110
+ isCurrentRefInFilter(): boolean {
111
+ if (!this._historyItemRefFilter) {
112
+ return true;
113
+ }
114
+ const currentId = this._provider?.currentHistoryItemRef?.id;
115
+ return currentId !== undefined && this._historyItemRefFilter.includes(currentId);
116
+ }
117
+
73
118
  get entries(): readonly HistoryGraphEntry[] {
74
119
  return this._entries;
75
120
  }
@@ -92,13 +137,14 @@ export class ScmHistoryGraphModel {
92
137
  }
93
138
 
94
139
  refresh(): void {
95
- this.cancelSource.cancel();
96
- this.cancelSource = new CancellationTokenSource();
97
-
98
140
  this.toDisposeOnProviderChange.dispose();
99
141
 
100
142
  const repo = this.scmService.selectedRepository;
101
143
  const hp = repo?.provider.historyProvider;
144
+ if (hp !== this._provider) {
145
+ // The repository changed — a filter picked for the old provider does not apply.
146
+ this._historyItemRefFilter = undefined;
147
+ }
102
148
  this._provider = hp;
103
149
 
104
150
  if (this._provider) {
@@ -106,7 +152,10 @@ export class ScmHistoryGraphModel {
106
152
  this._provider.onDidChangeCurrentHistoryItemRefs(() => this.refresh())
107
153
  );
108
154
  this.toDisposeOnProviderChange.push(
109
- this._provider.onDidChangeHistoryItemRefs(() => this.refresh())
155
+ this._provider.onDidChangeHistoryItemRefs(e => {
156
+ this.pruneHistoryItemRefFilter(e.removed);
157
+ this.refresh();
158
+ })
110
159
  );
111
160
  } else if (repo) {
112
161
  // historyProvider is not yet available; listen for provider changes
@@ -116,6 +165,31 @@ export class ScmHistoryGraphModel {
116
165
  );
117
166
  }
118
167
 
168
+ this.reload();
169
+ }
170
+
171
+ /**
172
+ * Drops refs that no longer exist from the explicit filter, so that
173
+ * deleting or renaming a filtered ref does not leave the graph stuck
174
+ * requesting history for it. When the last filtered ref is removed,
175
+ * the filter falls back to auto mode.
176
+ */
177
+ protected pruneHistoryItemRefFilter(removed: readonly ScmHistoryItemRef[]): void {
178
+ if (!this._historyItemRefFilter || removed.length === 0) {
179
+ return;
180
+ }
181
+ const removedIds = new Set(removed.map(ref => ref.id));
182
+ const remaining = this._historyItemRefFilter.filter(id => !removedIds.has(id));
183
+ if (remaining.length !== this._historyItemRefFilter.length) {
184
+ this._historyItemRefFilter = remaining.length > 0 ? remaining : undefined;
185
+ }
186
+ }
187
+
188
+ /** Clears the loaded entries and loads the first page again from the current provider. */
189
+ protected reload(): void {
190
+ this.cancelSource.cancel();
191
+ this.cancelSource = new CancellationTokenSource();
192
+
119
193
  this._entries = [];
120
194
  this._hasMore = false;
121
195
 
@@ -144,10 +218,11 @@ export class ScmHistoryGraphModel {
144
218
 
145
219
  const token = this.cancelSource.token;
146
220
  try {
221
+ const pageSize = this.pageSize;
147
222
  const historyItemRefs = this.getCurrentHistoryItemRefs();
148
223
  const options: ScmHistoryOptions = {
149
224
  skip: this._entries.length,
150
- limit: PAGE_SIZE,
225
+ limit: pageSize,
151
226
  historyItemRefs: historyItemRefs.length > 0 ? historyItemRefs : undefined,
152
227
  };
153
228
  const items = await this._provider.provideHistoryItems(options, token);
@@ -157,7 +232,7 @@ export class ScmHistoryGraphModel {
157
232
  }
158
233
 
159
234
  const fetchedItems: ScmHistoryItem[] = items ?? [];
160
- this._hasMore = fetchedItems.length >= PAGE_SIZE;
235
+ this._hasMore = fetchedItems.length >= pageSize;
161
236
 
162
237
  // Filter out any items already loaded so the graph does not show duplicates.
163
238
  const existingIds = new Set(this._entries.map(e => e.item.id));
@@ -189,13 +264,22 @@ export class ScmHistoryGraphModel {
189
264
  }
190
265
  }
191
266
 
267
+ /** The configured page size (`scm.graph.pageSize`). */
268
+ protected get pageSize(): number {
269
+ return this.scmPreferences['scm.graph.pageSize'] ?? PAGE_SIZE;
270
+ }
271
+
192
272
  /**
193
273
  * Resolves the ref-based color index of an item from its references,
194
- * preferring current (0) over remote (1) over base (2).
274
+ * preferring current (0) over remote (1) over base (2). Refs excluded by
275
+ * an explicit filter get no role color, mirroring VS Code's color map.
195
276
  */
196
277
  protected resolveColorIndex(item: ScmHistoryItem): number | undefined {
197
278
  let result: number | undefined;
198
279
  for (const ref of item.references ?? []) {
280
+ if (this._historyItemRefFilter && !this._historyItemRefFilter.includes(ref.id)) {
281
+ continue;
282
+ }
199
283
  const index = getRefColorIndex(ref, this._provider);
200
284
  if (index !== undefined && (result === undefined || index < result)) {
201
285
  result = index;
@@ -205,11 +289,15 @@ export class ScmHistoryGraphModel {
205
289
  }
206
290
 
207
291
  /**
208
- * Returns the revisions of the current branch ref, its remote tracking ref,
209
- * and the merge-base ref to pass to `provideHistoryItems`. Providers walk
210
- * history starting from these revisions.
292
+ * Returns the refs to pass to `provideHistoryItems`: the explicitly picked
293
+ * ref ids when a filter is active, otherwise (auto mode) the revisions of
294
+ * the current branch ref, its remote tracking ref, and the merge-base ref.
295
+ * Providers walk history starting from these refs.
211
296
  */
212
297
  protected getCurrentHistoryItemRefs(): string[] {
298
+ if (this._historyItemRefFilter) {
299
+ return [...this._historyItemRefFilter];
300
+ }
213
301
  if (!this._provider) {
214
302
  return [];
215
303
  }
@@ -48,15 +48,17 @@ describe('ScmHistoryGraphWidget context keys', () => {
48
48
  let widget: ScmHistoryGraphWidget;
49
49
  let scmContextKeys: ScmContextKeyService;
50
50
  let provider: Partial<ScmHistoryProvider> | undefined;
51
+ let currentRefInFilter: boolean;
51
52
 
52
53
  beforeEach(() => {
53
54
  restoreJSDOM = enableJSDOM();
54
55
  scmContextKeys = createScmContextKeyService();
56
+ currentRefInFilter = true;
55
57
  widget = new ScmHistoryGraphWidget();
56
58
  const raw = widget as unknown as Record<string, unknown>;
57
59
  raw.scmContextKeys = scmContextKeys;
58
60
  Object.defineProperty(raw, 'model', {
59
- get: () => ({ provider })
61
+ get: () => ({ provider, isCurrentRefInFilter: () => currentRefInFilter })
60
62
  });
61
63
  });
62
64
 
@@ -90,6 +92,15 @@ describe('ScmHistoryGraphWidget context keys', () => {
90
92
  expect(scmContextKeys.scmCurrentHistoryItemRefInFilter.get()).to.equal(false);
91
93
  });
92
94
 
95
+ it('should clear scmCurrentHistoryItemRefInFilter when the filter excludes the current ref', () => {
96
+ provider = {
97
+ currentHistoryItemRef: { id: 'refs/heads/main', name: 'main' }
98
+ };
99
+ currentRefInFilter = false;
100
+ updateContextKeys();
101
+ expect(scmContextKeys.scmCurrentHistoryItemRefInFilter.get()).to.equal(false);
102
+ });
103
+
93
104
  it('should set scmCurrentHistoryItemRefHasRemote when the provider has a remote ref', () => {
94
105
  provider = {
95
106
  currentHistoryItemRef: { id: 'refs/heads/main', name: 'main' },
@@ -30,13 +30,16 @@ import URI from '@theia/core/lib/common/uri';
30
30
  import { nls } from '@theia/core/lib/common/nls';
31
31
  import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
32
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';
33
35
  import { MenuPath } from '@theia/core/lib/common/menu/menu-types';
34
36
  import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
35
37
  import { OpenerService, open } from '@theia/core/lib/browser/opener-service';
36
38
  import { DiffUris } from '@theia/core/lib/browser/diff-uris';
37
39
  import { ScmContextKeyService } from './scm-context-key-service';
40
+ import { ScmPreferences } from '../common/scm-preferences';
38
41
  import {
39
- laneColor, getChangeStatus, getFileName, getFilePath, getRepoRelativePath,
42
+ laneColor, filterRefsForBadges, getChangeStatus, getFileName, getFilePath, getRepoRelativePath,
40
43
  getRefBadgeClass, getRefBadgePresentation, isTagRef, isRemoteRef, deduplicateRefs, DeduplicatedRef
41
44
  } from './scm-history-graph-helpers';
42
45
  import { buildHtmlTooltip, buildProviderTooltip } from './scm-history-graph-tooltip';
@@ -82,7 +85,7 @@ function renderJsxRefBadge(
82
85
  // ── Widget ──────────────────────────────────────────────────────────────────
83
86
 
84
87
  @injectable()
85
- export class ScmHistoryGraphWidget extends ReactWidget {
88
+ export class ScmHistoryGraphWidget extends ReactWidget implements DynamicToolbarWidget {
86
89
 
87
90
  static readonly ID = 'scm-history-graph-widget';
88
91
  static readonly LABEL = nls.localizeByDefault('Graph');
@@ -96,6 +99,7 @@ export class ScmHistoryGraphWidget extends ReactWidget {
96
99
  @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
97
100
  @inject(OpenerService) protected readonly openerService: OpenerService;
98
101
  @inject(ScmContextKeyService) protected readonly scmContextKeys: ScmContextKeyService;
102
+ @inject(ScmPreferences) protected readonly scmPreferences: ScmPreferences;
99
103
 
100
104
  protected selectedIndex = -1;
101
105
  /** Currently selected change row key (`${itemId}-${ci}`), or undefined. */
@@ -109,6 +113,10 @@ export class ScmHistoryGraphWidget extends ReactWidget {
109
113
  /** Cleanup for the content of the hover currently being shown. */
110
114
  protected readonly toDisposeOnHover = new DisposableCollection();
111
115
 
116
+ protected readonly onDidChangeToolbarItemsEmitter = new Emitter<void>();
117
+ /** Re-renders the part toolbar on model changes, e.g. to reflect the toggled state of the ref filter picker. */
118
+ readonly onDidChangeToolbarItems: Event<void> = this.onDidChangeToolbarItemsEmitter.event;
119
+
112
120
  constructor() {
113
121
  super();
114
122
  this.id = ScmHistoryGraphWidget.ID;
@@ -121,10 +129,19 @@ export class ScmHistoryGraphWidget extends ReactWidget {
121
129
 
122
130
  @postConstruct()
123
131
  protected init(): void {
132
+ this.toDispose.push(this.onDidChangeToolbarItemsEmitter);
124
133
  this.toDispose.push(
125
134
  this.model.onDidChange(() => {
126
135
  this.updateContextKeys();
127
136
  this.update();
137
+ this.onDidChangeToolbarItemsEmitter.fire();
138
+ })
139
+ );
140
+ this.toDispose.push(
141
+ this.scmPreferences.onPreferenceChanged(e => {
142
+ if (e.preferenceName.startsWith('scm.graph.')) {
143
+ this.update();
144
+ }
128
145
  })
129
146
  );
130
147
  this.toDispose.push({
@@ -144,9 +161,8 @@ export class ScmHistoryGraphWidget extends ReactWidget {
144
161
  const provider = this.model.provider;
145
162
  this.scmContextKeys.scmCurrentHistoryItemRefHasRemote.set(!!provider?.currentHistoryItemRemoteRef);
146
163
  this.scmContextKeys.scmCurrentHistoryItemRefHasBase.set(!!provider?.currentHistoryItemBaseRef);
147
- // The graph has no ref filter yet, so the current ref is always considered part of it.
148
164
  // Commands like git.pullRef/git.pushRef gate their enablement on this key.
149
- this.scmContextKeys.scmCurrentHistoryItemRefInFilter.set(!!provider?.currentHistoryItemRef);
165
+ this.scmContextKeys.scmCurrentHistoryItemRefInFilter.set(!!provider?.currentHistoryItemRef && this.model.isCurrentRefInFilter());
150
166
  }
151
167
 
152
168
  protected render(): React.ReactNode {
@@ -198,7 +214,7 @@ export class ScmHistoryGraphWidget extends ReactWidget {
198
214
  className='scm-history-graph-list'
199
215
  data={entries as HistoryGraphEntry[]}
200
216
  itemContent={(idx, entry) => this.renderRow(entry, idx, svgWidth)}
201
- endReached={hasMore && !loading ? this.handleEndReached : undefined}
217
+ endReached={hasMore && !loading && this.scmPreferences['scm.graph.pageOnScroll'] !== false ? this.handleEndReached : undefined}
202
218
  overscan={500}
203
219
  components={footer ? { Footer: footer } : {}}
204
220
  style={{ overflowX: 'hidden' }}
@@ -606,8 +622,12 @@ export class ScmHistoryGraphWidget extends ReactWidget {
606
622
  }
607
623
 
608
624
  const provider = this.model.provider;
625
+ const visibleRefs = filterRefsForBadges(refs, provider, this.scmPreferences['scm.graph.badges'] ?? 'filter', this.model.historyItemRefFilter);
626
+ if (visibleRefs.length === 0) {
627
+ return undefined;
628
+ }
609
629
  const laneColorValue = entry ? laneColor(entry.graphRow.color) : undefined;
610
- const deduplicated = deduplicateRefs(refs);
630
+ const deduplicated = deduplicateRefs(visibleRefs);
611
631
  const badgeForeground = 'var(--theia-scmGraph-historyItemRefForeground, var(--theia-badge-foreground))';
612
632
 
613
633
  const badges: React.ReactElement[] = [];
@@ -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');