Skip to content

Commit bcb954e

Browse files
authored
Support Swapping the tree model (microsoft#227773)
* Support Swapping the model * remove get model as it is provided by abstract tree
1 parent e9b8677 commit bcb954e

File tree

3 files changed

+304
-101
lines changed

3 files changed

+304
-101
lines changed

src/vs/base/browser/ui/tree/abstractTree.ts

+115-69
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
6666
private autoExpandDisposable: IDisposable = Disposable.None;
6767
private readonly disposables = new DisposableStore();
6868

69-
constructor(private model: ITreeModel<T, TFilterData, TRef>, private dnd: ITreeDragAndDrop<T>) { }
69+
constructor(private modelProvider: () => ITreeModel<T, TFilterData, TRef>, private dnd: ITreeDragAndDrop<T>) { }
7070

7171
getDragURI(node: ITreeNode<T, TFilterData>): string | null {
7272
return this.dnd.getDragURI(node.element);
@@ -99,10 +99,11 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
9999

100100
if (didChangeAutoExpandNode && typeof result !== 'boolean' && result.autoExpand) {
101101
this.autoExpandDisposable = disposableTimeout(() => {
102-
const ref = this.model.getNodeLocation(targetNode);
102+
const model = this.modelProvider();
103+
const ref = model.getNodeLocation(targetNode);
103104

104-
if (this.model.isCollapsed(ref)) {
105-
this.model.setCollapsed(ref, false);
105+
if (model.isCollapsed(ref)) {
106+
model.setCollapsed(ref, false);
106107
}
107108

108109
this.autoExpandNode = undefined;
@@ -120,17 +121,19 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
120121
}
121122

122123
if (result.bubble === TreeDragOverBubble.Up) {
123-
const ref = this.model.getNodeLocation(targetNode);
124-
const parentRef = this.model.getParentNodeLocation(ref);
125-
const parentNode = this.model.getNode(parentRef);
126-
const parentIndex = parentRef && this.model.getListIndex(parentRef);
124+
const model = this.modelProvider();
125+
const ref = model.getNodeLocation(targetNode);
126+
const parentRef = model.getParentNodeLocation(ref);
127+
const parentNode = model.getNode(parentRef);
128+
const parentIndex = parentRef && model.getListIndex(parentRef);
127129

128130
return this.onDragOver(data, parentNode, parentIndex, targetSector, originalEvent, false);
129131
}
130132

131-
const ref = this.model.getNodeLocation(targetNode);
132-
const start = this.model.getListIndex(ref);
133-
const length = this.model.getListRenderCount(ref);
133+
const model = this.modelProvider();
134+
const ref = model.getNodeLocation(targetNode);
135+
const start = model.getListIndex(ref);
136+
const length = model.getListRenderCount(ref);
134137

135138
return { ...result, feedback: range(start, start + length) };
136139
}
@@ -152,15 +155,15 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
152155
}
153156
}
154157

155-
function asListOptions<T, TFilterData, TRef>(model: ITreeModel<T, TFilterData, TRef>, options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
158+
function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T, TFilterData, TRef>, options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
156159
return options && {
157160
...options,
158161
identityProvider: options.identityProvider && {
159162
getId(el) {
160163
return options.identityProvider!.getId(el.element);
161164
}
162165
},
163-
dnd: options.dnd && new TreeNodeListDragAndDrop(model, options.dnd),
166+
dnd: options.dnd && new TreeNodeListDragAndDrop(modelProvider, options.dnd),
164167
multipleSelectionController: options.multipleSelectionController && {
165168
isSelectionSingleChangeEvent(e) {
166169
return options.multipleSelectionController!.isSelectionSingleChangeEvent({ ...e, element: e.element } as any);
@@ -172,6 +175,7 @@ function asListOptions<T, TFilterData, TRef>(model: ITreeModel<T, TFilterData, T
172175
accessibilityProvider: options.accessibilityProvider && {
173176
...options.accessibilityProvider,
174177
getSetSize(node) {
178+
const model = modelProvider();
175179
const ref = model.getNodeLocation(node);
176180
const parentRef = model.getParentNodeLocation(ref);
177181
const parentNode = model.getNode(parentRef);
@@ -560,6 +564,10 @@ export class TreeRenderer<T, TFilterData, TRef, TTemplateData> implements IListR
560564
this.activeIndentNodes = set;
561565
}
562566

567+
setModel(model: ITreeModel<T, TFilterData, TRef>): void {
568+
this.model = model;
569+
}
570+
563571
dispose(): void {
564572
this.renderedNodes.clear();
565573
this.renderedElements.clear();
@@ -1039,15 +1047,13 @@ class FindController<T, TFilterData> implements IDisposable {
10391047

10401048
constructor(
10411049
private tree: AbstractTree<T, TFilterData, any>,
1042-
model: ITreeModel<T, TFilterData, any>,
10431050
private view: List<ITreeNode<T, TFilterData>>,
10441051
private filter: FindFilter<T>,
10451052
private readonly contextViewProvider: IContextViewProvider,
10461053
private readonly options: IFindControllerOptions = {}
10471054
) {
10481055
this._mode = tree.options.defaultFindMode ?? TreeFindMode.Highlight;
10491056
this._matchType = tree.options.defaultFindMatchType ?? TreeFindMatchType.Fuzzy;
1050-
model.onDidSpliceModel(this.onDidSpliceModel, this, this.disposables);
10511057
}
10521058

10531059
updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void {
@@ -1060,6 +1066,10 @@ class FindController<T, TFilterData> implements IDisposable {
10601066
}
10611067
}
10621068

1069+
isOpened(): boolean {
1070+
return !!this.widget;
1071+
}
1072+
10631073
open(): void {
10641074
if (this.widget) {
10651075
this.widget.focus();
@@ -1125,16 +1135,7 @@ class FindController<T, TFilterData> implements IDisposable {
11251135
this.render();
11261136
}
11271137

1128-
private onDidSpliceModel(): void {
1129-
if (!this.widget || this.pattern.length === 0) {
1130-
return;
1131-
}
1132-
1133-
this.tree.refilter();
1134-
this.render();
1135-
}
1136-
1137-
private render(): void {
1138+
render(): void {
11381139
const noMatches = this.filter.totalCount > 0 && this.filter.matchCount === 0;
11391140

11401141
if (this.pattern && noMatches) {
@@ -1272,7 +1273,7 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
12721273

12731274
constructor(
12741275
private readonly tree: AbstractTree<T, TFilterData, TRef>,
1275-
private readonly model: ITreeModel<T, TFilterData, TRef>,
1276+
private model: ITreeModel<T, TFilterData, TRef>,
12761277
private readonly view: List<ITreeNode<T, TFilterData>>,
12771278
renderers: TreeRenderer<T, TFilterData, TRef, any>[],
12781279
private readonly treeDelegate: IListVirtualDelegate<ITreeNode<T, TFilterData>>,
@@ -1552,6 +1553,10 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
15521553
}
15531554
return { stickyScrollMaxItemCount };
15541555
}
1556+
1557+
setModel(model: ITreeModel<T, TFilterData, TRef>): void {
1558+
this.model = model;
1559+
}
15551560
}
15561561

15571562
class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
@@ -2493,7 +2498,14 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
24932498
get onDidFocus(): Event<void> { return this.view.onDidFocus; }
24942499
get onDidBlur(): Event<void> { return this.view.onDidBlur; }
24952500

2496-
get onDidChangeModel(): Event<void> { return Event.signal(this.model.onDidSpliceModel); }
2501+
private readonly onDidSwapModel = this.disposables.add(new Emitter<void>());
2502+
private readonly onDidChangeModelRelay = this.disposables.add(new Relay<void>());
2503+
private readonly onDidSpliceModelRelay = this.disposables.add(new Relay<ITreeModelSpliceEvent<T, TFilterData>>());
2504+
private readonly onDidChangeCollapseStateRelay = this.disposables.add(new Relay<ICollapseStateChangeEvent<T, TFilterData>>());
2505+
private readonly onDidChangeRenderNodeCountRelay = this.disposables.add(new Relay<ITreeNode<T, TFilterData>>());
2506+
private readonly onDidChangeActiveNodesRelay = this.disposables.add(new Relay<ITreeNode<T, TFilterData>[]>());
2507+
2508+
get onDidChangeModel(): Event<void> { return Event.any(this.onDidChangeModelRelay.event, this.onDidSwapModel.event); }
24972509
get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<T, TFilterData>> { return this.model.onDidChangeCollapseState; }
24982510
get onDidChangeRenderNodeCount(): Event<ITreeNode<T, TFilterData>> { return this.model.onDidChangeRenderNodeCount; }
24992511

@@ -2535,56 +2547,19 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
25352547
this.model = this.createModel(_user, _options);
25362548
this.treeDelegate = new ComposedTreeDelegate<T, ITreeNode<T, TFilterData>>(delegate);
25372549

2538-
const onDidChangeCollapseStateRelay = new Relay<ICollapseStateChangeEvent<T, TFilterData>>();
2539-
const onDidChangeActiveNodes = new Relay<ITreeNode<T, TFilterData>[]>();
2540-
const activeNodes = this.disposables.add(new EventCollection(onDidChangeActiveNodes.event));
2550+
const activeNodes = this.disposables.add(new EventCollection(this.onDidChangeActiveNodesRelay.event));
25412551
const renderedIndentGuides = new SetMap<ITreeNode<T, TFilterData>, HTMLDivElement>();
2542-
this.renderers = renderers.map(r => new TreeRenderer<T, TFilterData, TRef, any>(r, this.model, onDidChangeCollapseStateRelay.event, activeNodes, renderedIndentGuides, _options));
2552+
this.renderers = renderers.map(r => new TreeRenderer<T, TFilterData, TRef, any>(r, this.model, this.onDidChangeCollapseStateRelay.event, activeNodes, renderedIndentGuides, _options));
25432553
for (const r of this.renderers) {
25442554
this.disposables.add(r);
25452555
}
25462556

25472557
this.focus = new Trait(() => this.view.getFocusedElements()[0], _options.identityProvider);
25482558
this.selection = new Trait(() => this.view.getSelectedElements()[0], _options.identityProvider);
25492559
this.anchor = new Trait(() => this.view.getAnchorElement(), _options.identityProvider);
2550-
this.view = new TreeNodeList(_user, container, this.treeDelegate, this.renderers, this.focus, this.selection, this.anchor, { ...asListOptions(this.model, _options), tree: this, stickyScrollProvider: () => this.stickyScrollController });
2551-
2552-
this.disposables.add(this.model.onDidSpliceRenderedNodes(({ start, deleteCount, elements }) => this.view.splice(start, deleteCount, elements)));
2553-
2554-
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
2555-
2556-
const onDidModelSplice = Event.forEach(this.model.onDidSpliceModel, e => {
2557-
this.eventBufferer.bufferEvents(() => {
2558-
this.focus.onDidModelSplice(e);
2559-
this.selection.onDidModelSplice(e);
2560-
});
2561-
}, this.disposables);
2562-
2563-
// Make sure the `forEach` always runs
2564-
onDidModelSplice(() => null, null, this.disposables);
2565-
2566-
// Active nodes can change when the model changes or when focus or selection change.
2567-
// We debounce it with 0 delay since these events may fire in the same stack and we only
2568-
// want to run this once. It also doesn't matter if it runs on the next tick since it's only
2569-
// a nice to have UI feature.
2570-
const activeNodesEmitter = this.disposables.add(new Emitter<ITreeNode<T, TFilterData>[]>());
2571-
const activeNodesDebounce = this.disposables.add(new Delayer(0));
2572-
this.disposables.add(Event.any<any>(onDidModelSplice, this.focus.onDidChange, this.selection.onDidChange)(() => {
2573-
activeNodesDebounce.trigger(() => {
2574-
const set = new Set<ITreeNode<T, TFilterData>>();
2560+
this.view = new TreeNodeList(_user, container, this.treeDelegate, this.renderers, this.focus, this.selection, this.anchor, { ...asListOptions(() => this.model, _options), tree: this, stickyScrollProvider: () => this.stickyScrollController });
25752561

2576-
for (const node of this.focus.getNodes()) {
2577-
set.add(node);
2578-
}
2579-
2580-
for (const node of this.selection.getNodes()) {
2581-
set.add(node);
2582-
}
2583-
2584-
activeNodesEmitter.fire([...set.values()]);
2585-
});
2586-
}));
2587-
onDidChangeActiveNodes.input = activeNodesEmitter.event;
2562+
this.setupModel(this.model); // model needs to be setup after the traits have been created
25882563

25892564
if (_options.keyboardSupport !== false) {
25902565
const onKeyDown = Event.chain(this.view.onKeyDown, $ =>
@@ -2599,12 +2574,19 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
25992574

26002575
if ((_options.findWidgetEnabled ?? true) && _options.keyboardNavigationLabelProvider && _options.contextViewProvider) {
26012576
const opts = this.options.findWidgetStyles ? { styles: this.options.findWidgetStyles } : undefined;
2602-
this.findController = new FindController(this, this.model, this.view, filter!, _options.contextViewProvider, opts);
2577+
this.findController = new FindController(this, this.view, filter!, _options.contextViewProvider, opts);
26032578
this.focusNavigationFilter = node => this.findController!.shouldAllowFocus(node);
26042579
this.onDidChangeFindOpenState = this.findController.onDidChangeOpenState;
26052580
this.disposables.add(this.findController);
26062581
this.onDidChangeFindMode = this.findController.onDidChangeMode;
26072582
this.onDidChangeFindMatchType = this.findController.onDidChangeMatchType;
2583+
this.disposables.add(this.onDidSpliceModelRelay.event(() => {
2584+
if (!this.findController!.isOpened() || this.findController!.pattern.length === 0) {
2585+
return;
2586+
}
2587+
this.refilter();
2588+
this.findController!.render();
2589+
}));
26082590
} else {
26092591
this.onDidChangeFindMode = Event.None;
26102592
this.onDidChangeFindMatchType = Event.None;
@@ -3109,6 +3091,69 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
31093091

31103092
protected abstract createModel(user: string, options: IAbstractTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, TRef>;
31113093

3094+
private readonly modelDisposables = new DisposableStore();
3095+
private setupModel(model: ITreeModel<T, TFilterData, TRef>) {
3096+
this.modelDisposables.clear();
3097+
3098+
this.modelDisposables.add(model.onDidSpliceRenderedNodes(({ start, deleteCount, elements }) => this.view.splice(start, deleteCount, elements)));
3099+
3100+
const onDidModelSplice = Event.forEach(model.onDidSpliceModel, e => {
3101+
this.eventBufferer.bufferEvents(() => {
3102+
this.focus.onDidModelSplice(e);
3103+
this.selection.onDidModelSplice(e);
3104+
});
3105+
}, this.modelDisposables);
3106+
3107+
// Make sure the `forEach` always runs
3108+
onDidModelSplice(() => null, null, this.modelDisposables);
3109+
3110+
// Active nodes can change when the model changes or when focus or selection change.
3111+
// We debounce it with 0 delay since these events may fire in the same stack and we only
3112+
// want to run this once. It also doesn't matter if it runs on the next tick since it's only
3113+
// a nice to have UI feature.
3114+
const activeNodesEmitter = this.modelDisposables.add(new Emitter<ITreeNode<T, TFilterData>[]>());
3115+
const activeNodesDebounce = this.modelDisposables.add(new Delayer(0));
3116+
this.modelDisposables.add(Event.any<any>(onDidModelSplice, this.focus.onDidChange, this.selection.onDidChange)(() => {
3117+
activeNodesDebounce.trigger(() => {
3118+
const set = new Set<ITreeNode<T, TFilterData>>();
3119+
3120+
for (const node of this.focus.getNodes()) {
3121+
set.add(node);
3122+
}
3123+
3124+
for (const node of this.selection.getNodes()) {
3125+
set.add(node);
3126+
}
3127+
3128+
activeNodesEmitter.fire([...set.values()]);
3129+
});
3130+
}));
3131+
3132+
this.onDidChangeActiveNodesRelay.input = activeNodesEmitter.event;
3133+
this.onDidChangeModelRelay.input = Event.signal(model.onDidSpliceModel);
3134+
this.onDidChangeCollapseStateRelay.input = model.onDidChangeCollapseState;
3135+
this.onDidChangeRenderNodeCountRelay.input = model.onDidChangeRenderNodeCount;
3136+
}
3137+
3138+
setModel(newModel: ITreeModel<T, TFilterData, TRef>) {
3139+
const oldModel = this.model;
3140+
3141+
this.model = newModel;
3142+
this.setupModel(newModel);
3143+
3144+
this.renderers.forEach(r => r.setModel(newModel));
3145+
this.stickyScrollController?.setModel(newModel);
3146+
3147+
this.view.splice(0, oldModel.getListRenderCount(oldModel.rootRef));
3148+
this.model.refilter();
3149+
3150+
this.onDidSwapModel.fire();
3151+
}
3152+
3153+
getModel(): ITreeModel<T, TFilterData, TRef> {
3154+
return this.model;
3155+
}
3156+
31123157
navigate(start?: TRef): ITreeNavigator<T> {
31133158
return new TreeNavigator(this.view, this.model, start);
31143159
}
@@ -3117,6 +3162,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
31173162
dispose(this.disposables);
31183163
this.stickyScrollController?.dispose();
31193164
this.view.dispose();
3165+
this.modelDisposables.dispose();
31203166
}
31213167
}
31223168

0 commit comments

Comments
 (0)