fix(core): resolve self-root .code-workspace ConfigScope id collision - #116
Open
jianfulin wants to merge 1 commit into
Open
fix(core): resolve self-root .code-workspace ConfigScope id collision#116jianfulin wants to merge 1 commit into
jianfulin wants to merge 1 commit into
Conversation
Opening a .code-workspace whose folders array only declares its own
directory (e.g. "folders": [{ "path": "." }]) produces a workspace
scope and a folder scope with the same uri.toString() id. Because
provider.ts keys groupManagers by scope.id, the second Map.set()
silently overwrites the first, but configScopes still lists both
scopes. Every load then reads the same project .vscode/virtualTab.json
twice and writes the doubled result back, compounding on each
activation (confirmed live: 41 -> 82 group entries after one reopen).
ConfigScopeDiscovery.discover() now builds folder scopes first and
skips adding the workspace scope when its id already collides with a
folder scope's id, keeping the folder scope as the sole canonical
source for that project. provider.ts adds a defense-in-depth
assertUniqueScopeIds() guard before groupManagers is populated, so any
future collision fails safe (drops + warns) instead of silently
overwriting. Ordinary multi-root workspaces are unaffected.
Verified with 3 full close/reopen cycles against a real affected
project using a locally packaged test build; group count stayed
stable throughout (previously doubled after a single reopen).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(core): self-root .code-workspace causes ConfigScope id collision → duplicated/doubling groups
Problem
When a
.code-workspacefile declares only its own directory as the sole folder:{ "folders": [{ "path": "." }] }(a common "self-root" pattern — e.g. opening
MyProject/MyProject.code-workspacewherefoldersis just["."]) — every activation of the extension causes the persisted group data in that project's.vscode/virtualTab.jsonto double.Observed on two independent real projects using this workspace layout: one grew from 41 → 82 top-level-group-entries after a single close/reopen; a second grew similarly. Left unchecked over months of normal use, one file had accumulated to 8× its correct size (7 real groups → 56 duplicated entries). The VS Code tree view then also visually splits into multiple repeated
Project: Xscope headers, each showing the same duplicated group set.Root cause
ConfigScopeDiscovery.discover()builds:workspacescope,id = uri.toString()of the.code-workspacefile's parent directoryfolderscope pervscode.workspace.workspaceFolders[i],id = folder.uri.toString()When
foldersis["."], the workspace file's parent directory is that single folder, so:This breaks the implicit invariant that
ConfigScope.idis unique within onediscover()result.Downstream,
provider.tsbuildsgroupManagers: Map<string, GroupManager>keyed byscope.id:Because discovery order is workspace-scope-first, then folder-scopes, the second
.set()(folder manager) silently overwrites the first (workspace manager) — the Map ends up with only one manager, pointing at the project's.vscode/virtualTab.json, butconfigScopesstill has two entries with the same id.loadGroups()(no-arg / full reload path) then iteratesconfigScopesand doesgroupManagers.get(scope.id)once per scope:Both iterations resolve to the same
GroupManager/file, so the same N persisted groups get read twice into memory (2N).saveGroupsImmediate()buckets byscope.idthe same way, so both scope "buckets" collapse into the same target file, and the doubled 2N gets written straight back to disk. Next activation reads 2N, doubles it to 4N, writes it back — compounding indefinitely.Evidence that pinpoints "read twice" rather than "two legitimately different scopes merged"
For one affected project, after reopening once:
.vscode/virtualTab.jsonglobalStorage/.../workspace-config/.vscode/virtualTab.json)If this were "workspace-config (41) + folder file (41) legitimately merged", the union would show ~40 overlapping ids plus a couple of scope-unique ones (that's roughly what was observed a layer up, before this precise mechanism was isolated). But the exact symptom reproduced live was: every id appearing precisely twice, confirming the same on-disk file was loaded twice in a single
loadGroups()pass — not two distinct files merging.Fix (minimal / hotfix scope)
src/core/ConfigScopeDiscovery.ts— build folder scopes first, then only add the workspace scope if its id doesn't already collide with a folder scope's id (i.e. skip the workspace scope for the self-root alias case, keep the folder scope as sole canonical source):Ordinary multi-root workspaces (where the
.code-workspaceparent directory differs from every declared folder) are unaffected — the workspace scope is still added, in its original position.src/provider.ts— defense in depth: a privateassertUniqueScopeIds()guard runs right afterConfigScopeDiscovery.discover()in both the constructor andreinitializeScopes(), beforegroupManagersis populated. If a duplicate id is ever found (e.g. from a future discovery rule that reintroduces a collision), it logs a warning and drops the colliding (later) scope rather than lettingMap.set()silently overwrite — fails safe instead of failing silently.src/test/unit/ConfigScopeDiscovery.test.ts— added 4 cases: self-root alias collapses to one folder scope; resulting scope ids are unique; a mixed multi-root where one folder happens to equal the workspace root still keeps all folder scopes and only skips the colliding workspace scope; and a true multi-root (workspace parent different from every folder) is unaffected.What this hotfix intentionally does NOT cover
migrateLegacyWorkspaceConfig()— it happens to no longer be invoked for the self-root alias case as a side effect of (1) (no workspace-type scope exists for that case anymore), but the function itself is untouched.virtualTab.jsonneed a one-off manual cleanup, e.g. dedupe-by-id, keep-first-occurrence).(scopeId, groupId)runtime identity so two legitimately different scopes can safely share a group id, optimistic-lock retry correctness, generic multi-root duplicate-id UI handling) are out of scope for this hotfix.Verification performed
tsc --noEmit— no errors..code-workspaceproject, and did 3 full close/reopen cycles. Group count stayed stable across all 3 (previously it had doubled after just 1 reopen on the same project/version prior to the fix).