Reapply element already registered race condition fix (#283079)

Fixes microsoft/vscode-pull-request-github#8073
This commit is contained in:
Alex Ross 2025-12-12 18:01:41 +01:00 committed by GitHub
parent ae9f2efbab
commit 950ca05d5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 26 additions and 7 deletions

View File

@ -18,7 +18,7 @@ suite('vscode API - tree', () => {
assertNoRpc();
});
test.skip('TreeView - element already registered', async function () {
test('TreeView - element already registered', async function () {
this.timeout(60_000);
type TreeElement = { readonly kind: 'leaf' };

View File

@ -863,10 +863,23 @@ class ExtHostTreeView<T> extends Disposable {
}
private _createAndRegisterTreeNode(element: T, extTreeItem: vscode.TreeItem, parentNode: TreeNode | Root): TreeNode {
const node = this._createTreeNode(element, extTreeItem, parentNode);
if (extTreeItem.id && this._elements.has(node.item.handle)) {
throw new Error(localize('treeView.duplicateElement', 'Element with id {0} is already registered', extTreeItem.id));
const duplicateHandle = extTreeItem.id ? `${ExtHostTreeView.ID_HANDLE_PREFIX}/${extTreeItem.id}` : undefined;
if (duplicateHandle) {
const existingElement = this._elements.get(duplicateHandle);
if (existingElement) {
if (existingElement !== element) {
throw new Error(localize('treeView.duplicateElement', 'Element with id {0} is already registered', extTreeItem.id));
}
const existingNode = this._nodes.get(existingElement);
if (existingNode) {
const newNode = this._createTreeNode(element, extTreeItem, parentNode);
this._updateNodeCache(element, newNode, existingNode, parentNode);
existingNode.dispose();
return newNode;
}
}
}
const node = this._createTreeNode(element, extTreeItem, parentNode);
this._addNodeToCache(element, node);
this._addNodeToParentCache(node, parentNode);
return node;

View File

@ -693,7 +693,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView {
const treeMenus = this.treeDisposables.add(this.instantiationService.createInstance(TreeMenus, this.id));
this.treeLabels = this.treeDisposables.add(this.instantiationService.createInstance(ResourceLabels, this));
const dataSource = this.instantiationService.createInstance(TreeDataSource, this, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task));
const aligner = this.treeDisposables.add(new Aligner(this.themeService));
const aligner = this.treeDisposables.add(new Aligner(this.themeService, this.logService));
const checkboxStateHandler = this.treeDisposables.add(new CheckboxStateHandler());
const renderer = this.treeDisposables.add(this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler, () => this.manuallyManageCheckboxes));
this.treeDisposables.add(renderer.onDidChangeCheckboxState(e => this._onDidChangeCheckboxState.fire(e)));
@ -1631,7 +1631,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
class Aligner extends Disposable {
private _tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> | undefined;
constructor(private themeService: IThemeService) {
constructor(private themeService: IThemeService, private logService: ILogService) {
super();
}
@ -1649,7 +1649,13 @@ class Aligner extends Disposable {
if (this._tree) {
const root = this._tree.getInput();
const parent: ITreeItem = this._tree.getParentElement(treeItem) || root;
let parent: ITreeItem;
try {
parent = this._tree.getParentElement(treeItem) || root;
} catch (error) {
this.logService.error(`[TreeView] Failed to resolve parent for ${treeItem.handle}`, error);
return false;
}
if (this.hasIconOrCheckbox(parent)) {
return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIconOrCheckbox(c));
}