5
5
6
6
import './media/scm.css' ;
7
7
import { localize } from '../../../../nls.js' ;
8
- import { Event } from '../../../../base/common/event.js' ;
9
8
import { ViewPane , IViewPaneOptions } from '../../../browser/parts/views/viewPane.js' ;
10
9
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' ;
12
13
import { ISCMRepository , ISCMViewService } from '../common/scm.js' ;
13
14
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' ;
14
15
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js' ;
15
16
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js' ;
16
17
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js' ;
17
18
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' ;
19
20
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js' ;
20
21
import { IViewDescriptorService } from '../../../common/views.js' ;
21
22
import { IOpenerService } from '../../../../platform/opener/common/opener.js' ;
22
23
import { RepositoryActionRunner , RepositoryRenderer } from './scmRepositoryRenderer.js' ;
23
24
import { collectContextMenuActions , getActionViewItemProvider } from './util.js' ;
24
25
import { Orientation } from '../../../../base/browser/ui/sash/sash.js' ;
25
26
import { Iterable } from '../../../../base/common/iterator.js' ;
26
- import { DisposableStore } from '../../../../base/common/lifecycle.js' ;
27
27
import { MenuId } from '../../../../platform/actions/common/actions.js' ;
28
28
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' ;
29
31
30
32
class ListDelegate implements IListVirtualDelegate < ISCMRepository > {
31
33
@@ -38,10 +40,56 @@ class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
38
40
}
39
41
}
40
42
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
+
41
82
export class SCMRepositoriesViewPane extends ViewPane {
42
83
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 ( ) ;
45
93
46
94
constructor (
47
95
options : IViewPaneOptions ,
@@ -57,88 +105,109 @@ export class SCMRepositoriesViewPane extends ViewPane {
57
105
@IHoverService hoverService : IHoverService
58
106
) {
59
107
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 ) ;
60
111
}
61
112
62
113
protected override renderBody ( container : HTMLElement ) : void {
63
114
super . renderBody ( container ) ;
64
115
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 ;
90
131
}
91
- } ) as WorkbenchList < ISCMRepository > ;
92
132
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 ) ;
97
135
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 ) ;
100
138
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 ) ;
106
143
} ) ) ;
107
- }
108
-
109
- this . onDidChangeRepositories ( ) ;
110
- this . updateListSelection ( ) ;
111
- }
112
144
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
+ } ) ) ;
117
150
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 ) ;
121
157
}
122
158
123
159
protected override layoutBody ( height : number , width : number ) : void {
124
160
super . layoutBody ( height , width ) ;
125
- this . list . layout ( height , width ) ;
161
+ this . tree . layout ( height , width ) ;
126
162
}
127
163
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
+ }
132
168
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 ) ;
136
204
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 ) ) ;
139
208
}
140
209
141
- private onListContextMenu ( e : IListContextMenuEvent < ISCMRepository > ) : void {
210
+ private onTreeContextMenu ( e : ITreeContextMenuEvent < ISCMRepository > ) : void {
142
211
if ( ! e . element ) {
143
212
return ;
144
213
}
@@ -148,37 +217,57 @@ export class SCMRepositoriesViewPane extends ViewPane {
148
217
const menu = menus . repositoryContextMenu ;
149
218
const actions = collectContextMenuActions ( menu ) ;
150
219
220
+ const disposables = new DisposableStore ( ) ;
151
221
const actionRunner = new RepositoryActionRunner ( ( ) => {
152
- return this . list . getSelectedElements ( ) ;
222
+ return this . tree . getSelection ( ) ;
153
223
} ) ;
154
- actionRunner . onWillRun ( ( ) => this . list . domFocus ( ) ) ;
224
+ disposables . add ( actionRunner ) ;
225
+ disposables . add ( actionRunner . onWillRun ( ( ) => this . tree . domFocus ( ) ) ) ;
155
226
156
227
this . contextMenuService . showContextMenu ( {
157
228
actionRunner,
158
229
getAnchor : ( ) => e . anchor ,
159
230
getActions : ( ) => actions ,
160
231
getActionsContext : ( ) => provider ,
161
- onHide : ( ) => actionRunner . dispose ( )
232
+ onHide : ( ) => disposables . dispose ( )
162
233
} ) ;
163
234
}
164
235
165
- private onListSelectionChange ( e : IListEvent < ISCMRepository > ) : void {
236
+ private onTreeSelectionChange ( e : ITreeEvent < ISCMRepository > ) : void {
166
237
if ( e . browserEvent && e . elements . length > 0 ) {
167
- const scrollTop = this . list . scrollTop ;
238
+ const scrollTop = this . tree . scrollTop ;
168
239
this . scmViewService . visibleRepositories = e . elements ;
169
- this . list . scrollTop = scrollTop ;
240
+ this . tree . scrollTop = scrollTop ;
170
241
}
171
242
}
172
243
173
- private onDidChangeFocus ( e : IListEvent < ISCMRepository > ) : void {
244
+ private onTreeDidChangeFocus ( e : ITreeEvent < ISCMRepository > ) : void {
174
245
if ( e . browserEvent && e . elements . length > 0 ) {
175
246
this . scmViewService . focus ( e . elements [ 0 ] ) ;
176
247
}
177
248
}
178
249
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
+
182
271
const set = new Set ( this . scmViewService . visibleRepositories ) ;
183
272
const added = new Set ( Iterable . filter ( set , r => ! oldSet . has ( r ) ) ) ;
184
273
const removed = new Set ( Iterable . filter ( oldSet , r => ! set . has ( r ) ) ) ;
@@ -187,25 +276,24 @@ export class SCMRepositoriesViewPane extends ViewPane {
187
276
return ;
188
277
}
189
278
190
- const selection = oldSelection
191
- . filter ( i => ! removed . has ( this . list . element ( i ) ) ) ;
279
+ const selection = oldSelection . filter ( repo => ! removed . has ( repo ) ) ;
192
280
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 ) ;
196
284
}
197
285
}
198
286
199
- this . list . setSelection ( selection ) ;
287
+ this . tree . setSelection ( selection ) ;
200
288
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 ] ] ) ;
204
292
}
205
293
}
206
294
207
295
override dispose ( ) : void {
208
- this . disposables . dispose ( ) ;
296
+ this . visibilityDisposables . dispose ( ) ;
209
297
super . dispose ( ) ;
210
298
}
211
299
}
0 commit comments