Skip to content

Commit a99ad9a

Browse files
authored
SCM - refactor Repositories view (microsoft#252791)
* Initial refactoring * More observables * Fix define-class-fields-check
1 parent e9f1d6a commit a99ad9a

File tree

2 files changed

+174
-91
lines changed

2 files changed

+174
-91
lines changed

src/vs/workbench/contrib/scm/browser/media/scm.css

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,6 @@
493493

494494
/* Repositories */
495495

496-
.scm-repositories-view .scm-provider {
497-
margin: 0 12px 0 20px;
498-
overflow: hidden;
499-
}
500-
501496
.scm-repositories-view .scm-provider > .label > .name {
502497
font-weight: normal;
503498
}

src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts

Lines changed: 174 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,29 @@
55

66
import './media/scm.css';
77
import { localize } from '../../../../nls.js';
8-
import { Event } from '../../../../base/common/event.js';
98
import { ViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPane.js';
109
import { append, $ } from '../../../../base/browser/dom.js';
11-
import { IListVirtualDelegate, IListContextMenuEvent, IListEvent, IListRenderer } from '../../../../base/browser/ui/list/list.js';
10+
import { IListVirtualDelegate, IIdentityProvider } from '../../../../base/browser/ui/list/list.js';
11+
import { IAsyncDataSource, ITreeEvent, ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js';
12+
import { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js';
1213
import { ISCMRepository, ISCMViewService } from '../common/scm.js';
1314
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
1415
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
1516
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
1617
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
1718
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
18-
import { WorkbenchList } from '../../../../platform/list/browser/listService.js';
19+
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
1920
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
2021
import { IViewDescriptorService } from '../../../common/views.js';
2122
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
2223
import { RepositoryActionRunner, RepositoryRenderer } from './scmRepositoryRenderer.js';
2324
import { collectContextMenuActions, getActionViewItemProvider } from './util.js';
2425
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
2526
import { Iterable } from '../../../../base/common/iterator.js';
26-
import { DisposableStore } from '../../../../base/common/lifecycle.js';
2727
import { MenuId } from '../../../../platform/actions/common/actions.js';
2828
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
29+
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
30+
import { autorun, IObservable, observableSignalFromEvent } from '../../../../base/common/observable.js';
2931

3032
class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
3133

@@ -38,10 +40,56 @@ class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
3840
}
3941
}
4042

43+
class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource<SCMRepositoriesViewModel, ISCMRepository> {
44+
async getChildren(inputOrElement: SCMRepositoriesViewModel | ISCMRepository): Promise<Iterable<ISCMRepository>> {
45+
if (inputOrElement instanceof SCMRepositoriesViewModel) {
46+
return inputOrElement.repositories;
47+
}
48+
return [];
49+
}
50+
51+
hasChildren(inputOrElement: SCMRepositoriesViewModel | ISCMRepository): boolean {
52+
return inputOrElement instanceof SCMRepositoriesViewModel;
53+
}
54+
}
55+
56+
class RepositoryTreeIdentityProvider implements IIdentityProvider<ISCMRepository> {
57+
getId(element: ISCMRepository): string {
58+
return element.provider.id;
59+
}
60+
}
61+
62+
class SCMRepositoriesViewModel extends Disposable {
63+
readonly onDidChangeRepositoriesSignal: IObservable<void>;
64+
readonly onDidChangeVisibleRepositoriesSignal: IObservable<void>;
65+
66+
constructor(
67+
@ISCMViewService private readonly scmViewService: ISCMViewService
68+
) {
69+
super();
70+
71+
this.onDidChangeRepositoriesSignal = observableSignalFromEvent(this,
72+
this.scmViewService.onDidChangeRepositories);
73+
this.onDidChangeVisibleRepositoriesSignal = observableSignalFromEvent(this,
74+
this.scmViewService.onDidChangeVisibleRepositories);
75+
}
76+
77+
get repositories(): ISCMRepository[] {
78+
return this.scmViewService.repositories;
79+
}
80+
}
81+
4182
export class SCMRepositoriesViewPane extends ViewPane {
4283

43-
private list!: WorkbenchList<ISCMRepository>;
44-
private readonly disposables = new DisposableStore();
84+
private tree!: WorkbenchCompressibleAsyncDataTree<SCMRepositoriesViewModel, ISCMRepository, any>;
85+
private treeViewModel!: SCMRepositoriesViewModel;
86+
private treeDataSource!: RepositoryTreeDataSource;
87+
private treeIdentityProvider!: RepositoryTreeIdentityProvider;
88+
89+
private readonly visibleCountObs: IObservable<number>;
90+
private readonly providerCountBadgeObs: IObservable<'hidden' | 'auto' | 'visible'>;
91+
92+
private readonly visibilityDisposables = new DisposableStore();
4593

4694
constructor(
4795
options: IViewPaneOptions,
@@ -57,88 +105,109 @@ export class SCMRepositoriesViewPane extends ViewPane {
57105
@IHoverService hoverService: IHoverService
58106
) {
59107
super({ ...options, titleMenuId: MenuId.SCMSourceControlTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
108+
109+
this.visibleCountObs = observableConfigValue('scm.repositories.visible', 10, this.configurationService);
110+
this.providerCountBadgeObs = observableConfigValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge', 'hidden', this.configurationService);
60111
}
61112

62113
protected override renderBody(container: HTMLElement): void {
63114
super.renderBody(container);
64115

65-
const listContainer = append(container, $('.scm-view.scm-repositories-view'));
66-
67-
const updateProviderCountVisibility = () => {
68-
const value = this.configurationService.getValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge');
69-
listContainer.classList.toggle('hide-provider-counts', value === 'hidden');
70-
listContainer.classList.toggle('auto-provider-counts', value === 'auto');
71-
};
72-
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility));
73-
updateProviderCountVisibility();
74-
75-
const delegate = new ListDelegate();
76-
const renderer = this.instantiationService.createInstance(RepositoryRenderer, MenuId.SCMSourceControlInline, getActionViewItemProvider(this.instantiationService));
77-
const identityProvider = { getId: (r: ISCMRepository) => r.provider.id };
78-
79-
this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Main`, listContainer, delegate, [renderer as IListRenderer<ISCMRepository, any>], {
80-
identityProvider,
81-
horizontalScrolling: false,
82-
overrideStyles: this.getLocationBasedColors().listOverrideStyles,
83-
accessibilityProvider: {
84-
getAriaLabel(r: ISCMRepository) {
85-
return r.provider.label;
86-
},
87-
getWidgetAriaLabel() {
88-
return localize('scm', "Source Control Repositories");
89-
}
116+
const treeContainer = append(container, $('.scm-view.scm-repositories-view'));
117+
118+
// scm.providerCountBadge setting
119+
this._register(autorun(reader => {
120+
const providerCountBadge = this.providerCountBadgeObs.read(reader);
121+
treeContainer.classList.toggle('hide-provider-counts', providerCountBadge === 'hidden');
122+
treeContainer.classList.toggle('auto-provider-counts', providerCountBadge === 'auto');
123+
}));
124+
125+
this.createTree(treeContainer);
126+
127+
this.onDidChangeBodyVisibility(visible => {
128+
if (!visible) {
129+
this.visibilityDisposables.clear();
130+
return;
90131
}
91-
}) as WorkbenchList<ISCMRepository>;
92132

93-
this._register(this.list);
94-
this._register(this.list.onDidChangeSelection(this.onListSelectionChange, this));
95-
this._register(this.list.onDidChangeFocus(this.onDidChangeFocus, this));
96-
this._register(this.list.onContextMenu(this.onListContextMenu, this));
133+
this.treeViewModel = this.instantiationService.createInstance(SCMRepositoriesViewModel);
134+
this._register(this.treeViewModel);
97135

98-
this._register(this.scmViewService.onDidChangeRepositories(this.onDidChangeRepositories, this));
99-
this._register(this.scmViewService.onDidChangeVisibleRepositories(this.updateListSelection, this));
136+
// Initial rendering
137+
this.tree.setInput(this.treeViewModel);
100138

101-
if (this.orientation === Orientation.VERTICAL) {
102-
this._register(this.configurationService.onDidChangeConfiguration(e => {
103-
if (e.affectsConfiguration('scm.repositories.visible')) {
104-
this.updateBodySize();
105-
}
139+
// scm.repositories.visible setting
140+
this.visibilityDisposables.add(autorun(reader => {
141+
const visibleCount = this.visibleCountObs.read(reader);
142+
this.updateBodySize(visibleCount);
106143
}));
107-
}
108-
109-
this.onDidChangeRepositories();
110-
this.updateListSelection();
111-
}
112144

113-
private onDidChangeRepositories(): void {
114-
this.list.splice(0, this.list.length, this.scmViewService.repositories);
115-
this.updateBodySize();
116-
}
145+
// onDidChangeRepositoriesSignal
146+
this.visibilityDisposables.add(autorun(async reader => {
147+
this.treeViewModel.onDidChangeRepositoriesSignal.read(reader);
148+
await this.updateChildren();
149+
}));
117150

118-
override focus(): void {
119-
super.focus();
120-
this.list.domFocus();
151+
// onDidChangeVisibleRepositoriesSignal
152+
this.visibilityDisposables.add(autorun(async reader => {
153+
this.treeViewModel.onDidChangeVisibleRepositoriesSignal.read(reader);
154+
this.updateTreeSelection();
155+
}));
156+
}, this, this._store);
121157
}
122158

123159
protected override layoutBody(height: number, width: number): void {
124160
super.layoutBody(height, width);
125-
this.list.layout(height, width);
161+
this.tree.layout(height, width);
126162
}
127163

128-
private updateBodySize(): void {
129-
if (this.orientation === Orientation.HORIZONTAL) {
130-
return;
131-
}
164+
override focus(): void {
165+
super.focus();
166+
this.tree.domFocus();
167+
}
132168

133-
const visibleCount = this.configurationService.getValue<number>('scm.repositories.visible');
134-
const empty = this.list.length === 0;
135-
const size = Math.min(this.list.length, visibleCount) * 22;
169+
private createTree(container: HTMLElement): void {
170+
this.treeIdentityProvider = new RepositoryTreeIdentityProvider();
171+
this.treeDataSource = this.instantiationService.createInstance(RepositoryTreeDataSource);
172+
this._register(this.treeDataSource);
173+
174+
const compressionEnabled = observableConfigValue('scm.compactFolders', true, this.configurationService);
175+
176+
this.tree = this.instantiationService.createInstance(
177+
WorkbenchCompressibleAsyncDataTree,
178+
'SCM Repositories',
179+
container,
180+
new ListDelegate(),
181+
{
182+
isIncompressible: () => true
183+
},
184+
[
185+
this.instantiationService.createInstance(RepositoryRenderer, MenuId.SCMSourceControlInline, getActionViewItemProvider(this.instantiationService))
186+
],
187+
this.treeDataSource,
188+
{
189+
identityProvider: this.treeIdentityProvider,
190+
horizontalScrolling: false,
191+
compressionEnabled: compressionEnabled.get(),
192+
overrideStyles: this.getLocationBasedColors().listOverrideStyles,
193+
accessibilityProvider: {
194+
getAriaLabel(r: ISCMRepository) {
195+
return r.provider.label;
196+
},
197+
getWidgetAriaLabel() {
198+
return localize('scm', "Source Control Repositories");
199+
}
200+
}
201+
}
202+
) as WorkbenchCompressibleAsyncDataTree<SCMRepositoriesViewModel, ISCMRepository, any>;
203+
this._register(this.tree);
136204

137-
this.minimumBodySize = visibleCount === 0 ? 22 : size;
138-
this.maximumBodySize = visibleCount === 0 ? Number.POSITIVE_INFINITY : empty ? Number.POSITIVE_INFINITY : size;
205+
this._register(this.tree.onDidChangeSelection(this.onTreeSelectionChange, this));
206+
this._register(this.tree.onDidChangeFocus(this.onTreeDidChangeFocus, this));
207+
this._register(this.tree.onContextMenu(this.onTreeContextMenu, this));
139208
}
140209

141-
private onListContextMenu(e: IListContextMenuEvent<ISCMRepository>): void {
210+
private onTreeContextMenu(e: ITreeContextMenuEvent<ISCMRepository>): void {
142211
if (!e.element) {
143212
return;
144213
}
@@ -148,37 +217,57 @@ export class SCMRepositoriesViewPane extends ViewPane {
148217
const menu = menus.repositoryContextMenu;
149218
const actions = collectContextMenuActions(menu);
150219

220+
const disposables = new DisposableStore();
151221
const actionRunner = new RepositoryActionRunner(() => {
152-
return this.list.getSelectedElements();
222+
return this.tree.getSelection();
153223
});
154-
actionRunner.onWillRun(() => this.list.domFocus());
224+
disposables.add(actionRunner);
225+
disposables.add(actionRunner.onWillRun(() => this.tree.domFocus()));
155226

156227
this.contextMenuService.showContextMenu({
157228
actionRunner,
158229
getAnchor: () => e.anchor,
159230
getActions: () => actions,
160231
getActionsContext: () => provider,
161-
onHide: () => actionRunner.dispose()
232+
onHide: () => disposables.dispose()
162233
});
163234
}
164235

165-
private onListSelectionChange(e: IListEvent<ISCMRepository>): void {
236+
private onTreeSelectionChange(e: ITreeEvent<ISCMRepository>): void {
166237
if (e.browserEvent && e.elements.length > 0) {
167-
const scrollTop = this.list.scrollTop;
238+
const scrollTop = this.tree.scrollTop;
168239
this.scmViewService.visibleRepositories = e.elements;
169-
this.list.scrollTop = scrollTop;
240+
this.tree.scrollTop = scrollTop;
170241
}
171242
}
172243

173-
private onDidChangeFocus(e: IListEvent<ISCMRepository>): void {
244+
private onTreeDidChangeFocus(e: ITreeEvent<ISCMRepository>): void {
174245
if (e.browserEvent && e.elements.length > 0) {
175246
this.scmViewService.focus(e.elements[0]);
176247
}
177248
}
178249

179-
private updateListSelection(): void {
180-
const oldSelection = this.list.getSelection();
181-
const oldSet = new Set(Iterable.map(oldSelection, i => this.list.element(i)));
250+
private async updateChildren(): Promise<void> {
251+
await this.tree.updateChildren(this.treeViewModel);
252+
this.updateBodySize(this.visibleCountObs.get());
253+
}
254+
255+
private updateBodySize(visibleCount: number): void {
256+
if (this.orientation === Orientation.HORIZONTAL) {
257+
return;
258+
}
259+
260+
const empty = this.scmViewService.repositories.length === 0;
261+
const size = Math.min(this.scmViewService.repositories.length, visibleCount) * 22;
262+
263+
this.minimumBodySize = visibleCount === 0 ? 22 : size;
264+
this.maximumBodySize = visibleCount === 0 ? Number.POSITIVE_INFINITY : empty ? Number.POSITIVE_INFINITY : size;
265+
}
266+
267+
private updateTreeSelection(): void {
268+
const oldSelection = this.tree.getSelection();
269+
const oldSet = new Set(oldSelection);
270+
182271
const set = new Set(this.scmViewService.visibleRepositories);
183272
const added = new Set(Iterable.filter(set, r => !oldSet.has(r)));
184273
const removed = new Set(Iterable.filter(oldSet, r => !set.has(r)));
@@ -187,25 +276,24 @@ export class SCMRepositoriesViewPane extends ViewPane {
187276
return;
188277
}
189278

190-
const selection = oldSelection
191-
.filter(i => !removed.has(this.list.element(i)));
279+
const selection = oldSelection.filter(repo => !removed.has(repo));
192280

193-
for (let i = 0; i < this.list.length; i++) {
194-
if (added.has(this.list.element(i))) {
195-
selection.push(i);
281+
for (const repo of this.scmViewService.repositories) {
282+
if (added.has(repo)) {
283+
selection.push(repo);
196284
}
197285
}
198286

199-
this.list.setSelection(selection);
287+
this.tree.setSelection(selection);
200288

201-
if (selection.length > 0 && selection.indexOf(this.list.getFocus()[0]) === -1) {
202-
this.list.setAnchor(selection[0]);
203-
this.list.setFocus([selection[0]]);
289+
if (selection.length > 0 && !this.tree.getFocus().includes(selection[0])) {
290+
this.tree.setAnchor(selection[0]);
291+
this.tree.setFocus([selection[0]]);
204292
}
205293
}
206294

207295
override dispose(): void {
208-
this.disposables.dispose();
296+
this.visibilityDisposables.dispose();
209297
super.dispose();
210298
}
211299
}

0 commit comments

Comments
 (0)