Skip to content

Commit 472dee5

Browse files
authored
Another fix for "element with id not registered" error (#281269)
Fixes microsoft/vscode-pull-request-github#8073
1 parent 3015de1 commit 472dee5

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

extensions/vscode-api-tests/src/singlefolder-tests/tree.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,143 @@ suite('vscode API - tree', () => {
103103
assert.fail(error.message);
104104
}
105105
});
106+
107+
test('TreeView - element already registered after refresh', async function () {
108+
this.timeout(60_000);
109+
110+
type ParentElement = { readonly kind: 'parent' };
111+
type ChildElement = { readonly kind: 'leaf'; readonly version: number };
112+
type TreeElement = ParentElement | ChildElement;
113+
114+
class ParentRefreshTreeDataProvider implements vscode.TreeDataProvider<TreeElement> {
115+
private readonly changeEmitter = new vscode.EventEmitter<TreeElement | undefined>();
116+
private readonly rootRequestEmitter = new vscode.EventEmitter<number>();
117+
private readonly childRequestEmitter = new vscode.EventEmitter<number>();
118+
private readonly rootRequests: DeferredPromise<TreeElement[]>[] = [];
119+
private readonly childRequests: DeferredPromise<TreeElement[]>[] = [];
120+
private readonly parentElement: ParentElement = { kind: 'parent' };
121+
private childVersion = 0;
122+
private currentChild: ChildElement = { kind: 'leaf', version: 0 };
123+
124+
readonly onDidChangeTreeData = this.changeEmitter.event;
125+
126+
getChildren(element?: TreeElement): Thenable<TreeElement[]> {
127+
if (!element) {
128+
const deferred = new DeferredPromise<TreeElement[]>();
129+
this.rootRequests.push(deferred);
130+
this.rootRequestEmitter.fire(this.rootRequests.length);
131+
return deferred.p;
132+
}
133+
if (element.kind === 'parent') {
134+
const deferred = new DeferredPromise<TreeElement[]>();
135+
this.childRequests.push(deferred);
136+
this.childRequestEmitter.fire(this.childRequests.length);
137+
return deferred.p;
138+
}
139+
return Promise.resolve([]);
140+
}
141+
142+
getTreeItem(element: TreeElement): vscode.TreeItem {
143+
if (element.kind === 'parent') {
144+
const item = new vscode.TreeItem('parent', vscode.TreeItemCollapsibleState.Collapsed);
145+
item.id = 'parent';
146+
return item;
147+
}
148+
const item = new vscode.TreeItem('duplicate', vscode.TreeItemCollapsibleState.None);
149+
item.id = 'dup';
150+
return item;
151+
}
152+
153+
getParent(element: TreeElement): TreeElement | undefined {
154+
if (element.kind === 'leaf') {
155+
return this.parentElement;
156+
}
157+
return undefined;
158+
}
159+
160+
getCurrentChild(): ChildElement {
161+
return this.currentChild;
162+
}
163+
164+
replaceChild(): ChildElement {
165+
this.childVersion++;
166+
this.currentChild = { kind: 'leaf', version: this.childVersion };
167+
return this.currentChild;
168+
}
169+
170+
async waitForRootRequestCount(count: number): Promise<void> {
171+
while (this.rootRequests.length < count) {
172+
await asPromise(this.rootRequestEmitter.event);
173+
}
174+
}
175+
176+
async waitForChildRequestCount(count: number): Promise<void> {
177+
while (this.childRequests.length < count) {
178+
await asPromise(this.childRequestEmitter.event);
179+
}
180+
}
181+
182+
async resolveNextRootRequest(elements?: TreeElement[]): Promise<void> {
183+
const next = this.rootRequests.shift();
184+
if (!next) {
185+
return;
186+
}
187+
await next.complete(elements ?? [this.parentElement]);
188+
}
189+
190+
async resolveChildRequestAt(index: number, elements?: TreeElement[]): Promise<void> {
191+
const request = this.childRequests[index];
192+
if (!request) {
193+
return;
194+
}
195+
this.childRequests.splice(index, 1);
196+
await request.complete(elements ?? [this.currentChild]);
197+
}
198+
199+
dispose(): void {
200+
this.changeEmitter.dispose();
201+
this.rootRequestEmitter.dispose();
202+
this.childRequestEmitter.dispose();
203+
while (this.rootRequests.length) {
204+
this.rootRequests.shift()!.complete([]);
205+
}
206+
while (this.childRequests.length) {
207+
this.childRequests.shift()!.complete([]);
208+
}
209+
}
210+
}
211+
212+
const provider = new ParentRefreshTreeDataProvider();
213+
disposables.push(provider);
214+
215+
const treeView = vscode.window.createTreeView('test.treeRefresh', { treeDataProvider: provider });
216+
disposables.push(treeView);
217+
218+
const initialChild = provider.getCurrentChild();
219+
const firstReveal = (treeView.reveal(initialChild, { expand: true })
220+
.then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>)
221+
.catch(error => ({ error }));
222+
223+
await provider.waitForRootRequestCount(1);
224+
await provider.resolveNextRootRequest();
225+
226+
await provider.waitForChildRequestCount(1);
227+
const staleChild = provider.getCurrentChild();
228+
const refreshedChild = provider.replaceChild();
229+
const secondReveal = (treeView.reveal(refreshedChild, { expand: true })
230+
.then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>)
231+
.catch(error => ({ error }));
232+
233+
await provider.waitForChildRequestCount(2);
234+
235+
await provider.resolveChildRequestAt(1, [refreshedChild]);
236+
await delay(0);
237+
await provider.resolveChildRequestAt(0, [staleChild]);
238+
239+
const [firstResult, secondResult] = await Promise.all([firstReveal, secondReveal]);
240+
const error = firstResult.error ?? secondResult.error;
241+
if (error && /Element with id .+ is already registered/.test(error.message)) {
242+
assert.fail(error.message);
243+
}
244+
});
106245
});

src/vs/workbench/api/common/extHostTreeViews.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,13 +314,17 @@ class ExtHostTreeView<T> extends Disposable {
314314

315315
private static readonly LABEL_HANDLE_PREFIX = '0';
316316
private static readonly ID_HANDLE_PREFIX = '1';
317+
private static readonly ROOT_FETCH_KEY = Symbol('extHostTreeViewRoot');
317318

318319
private readonly _dataProvider: vscode.TreeDataProvider<T>;
319320
private readonly _dndController: vscode.TreeDragAndDropController<T> | undefined;
320321

321322
private _roots: TreeNode[] | undefined = undefined;
322323
private _elements: Map<TreeItemHandle, T> = new Map<TreeItemHandle, T>();
323324
private _nodes: Map<T, TreeNode> = new Map<T, TreeNode>();
325+
// Track the latest child-fetch per element so that refresh-triggered cache clears ignore stale results.
326+
// Without these tokens, an earlier getChildren promise resolving after refresh would re-register handles and hit the duplicate-id guard.
327+
private readonly _childrenFetchTokens = new Map<T | typeof ExtHostTreeView.ROOT_FETCH_KEY, number>();
324328

325329
private _visible: boolean = false;
326330
get visible(): boolean { return this._visible; }
@@ -725,14 +729,25 @@ class ExtHostTreeView<T> extends Disposable {
725729
return this._roots;
726730
}
727731

732+
private _getFetchKey(parentElement?: T): T | typeof ExtHostTreeView.ROOT_FETCH_KEY {
733+
return parentElement ?? ExtHostTreeView.ROOT_FETCH_KEY;
734+
}
735+
728736
private async _fetchChildrenNodes(parentElement?: T): Promise<TreeNode[] | undefined> {
729737
// clear children cache
730738
this._addChildrenToClear(parentElement);
739+
const fetchKey = this._getFetchKey(parentElement);
740+
let requestId = this._childrenFetchTokens.get(fetchKey) ?? 0;
741+
requestId++;
742+
this._childrenFetchTokens.set(fetchKey, requestId);
731743

732744
const cts = new CancellationTokenSource(this._refreshCancellationSource.token);
733745

734746
try {
735747
const elements = await this._dataProvider.getChildren(parentElement);
748+
if (this._childrenFetchTokens.get(fetchKey) !== requestId) {
749+
return undefined;
750+
}
736751
const parentNode = parentElement ? this._nodes.get(parentElement) : undefined;
737752

738753
if (cts.token.isCancellationRequested) {
@@ -743,12 +758,18 @@ class ExtHostTreeView<T> extends Disposable {
743758
const treeItems = await Promise.all(coalesce(coalescedElements).map(element => {
744759
return this._dataProvider.getTreeItem(element);
745760
}));
761+
if (this._childrenFetchTokens.get(fetchKey) !== requestId) {
762+
return undefined;
763+
}
746764
if (cts.token.isCancellationRequested) {
747765
return undefined;
748766
}
749767

750768
// createAndRegisterTreeNodes adds the nodes to a cache. This must be done sync so that they get added in the correct order.
751769
const items = treeItems.map((item, index) => item ? this._createAndRegisterTreeNode(coalescedElements[index], item, parentNode) : null);
770+
if (this._childrenFetchTokens.get(fetchKey) !== requestId) {
771+
return undefined;
772+
}
752773

753774
return coalesce(items);
754775
} finally {
@@ -1062,6 +1083,7 @@ class ExtHostTreeView<T> extends Disposable {
10621083
});
10631084
this._nodes.clear();
10641085
this._elements.clear();
1086+
this._childrenFetchTokens.clear();
10651087
}
10661088

10671089
private _clearNodes(nodes: TreeNode[]): void {
@@ -1075,6 +1097,7 @@ class ExtHostTreeView<T> extends Disposable {
10751097
this._nodes.clear();
10761098
dispose(this._nodesToClear);
10771099
this._nodesToClear.clear();
1100+
this._childrenFetchTokens.clear();
10781101
}
10791102

10801103
override dispose() {

0 commit comments

Comments
 (0)