Skip to content

VS Code ↔ Bazel workspace root divergence #684

Description

@cbandera

This is a (AI generated) meta-issue, collecting related work and documenting the resulting design decision.

Based on: bazel-contrib/vscode-bazel@84484e6 (master, 2026-08-19) — all file:line references below are against this revision.
Related: #625, #621, #472, #428, #383, #416, #608, #607, #638, #446

TL;DR

The extension currently has two independent, inconsistently-implemented mechanisms for deciding "what is the Bazel workspace" for a given piece of UI: one resolved per open file, one resolved once per VS Code folder. Several open issues and one open PR (#625) are all symptoms of these two mechanisms disagreeing, or of auto-detection being structurally unable to find the right root at all. This issue proposes:

  1. Adopting a single new setting, bazel.workspacePath (originally proposed in Feat/manual workspace path #625), as the supported way to pin one Bazel workspace root per VS Code folder whenever auto-detection is ambiguous or impossible.
  2. Explicitly scoping that setting to solve the root-resolution problem only, and explicitly not trying to make one VS Code folder host several independent, simultaneously-active Bazel roots — that case is delegated to VS Code's native multi-root workspaces, which the setting's resource scope already supports per folder.
  3. Sequencing the remaining implementation work needed to make that guarantee hold for every feature in the extension.
  4. Flagging Handle external dependencies #416 ("Handle external dependencies") as a related but orthogonal problem that this design must not make harder, even though it shares a root cause with getBazelWorkspaceFolder for multiple MODULE.bazel files #621/bazel 8 + bzlmod and child modules resolution #472.

Problem statement

Two anchoring mechanisms

BazelWorkspaceInfo (src/bazel/bazel_workspace_info.ts) is the shared type every feature uses to learn "where is the Bazel workspace root," but it is populated in two structurally different ways:

Mechanism Entry point Resolves from Re-evaluated
Dynamic, per-file BazelWorkspaceInfo.fromDocument(document)getBazelWorkspaceFolder(uri.fsPath) The specific open file, walking upward through ancestor directories until a MODULE.bazel/REPO.bazel/WORKSPACE.bazel/WORKSPACE file is found Every call, per file
Static, per-folder BazelWorkspaceInfo.fromWorkspaceFolder(s)getBazelWorkspaceFolder(workspaceFolder.uri.fsPath) The VS Code workspace folder root itself, same upward walk Once per folder (effectively)

Both funnel through the same getBazelWorkspaceFolder() in bazel_utils.ts, and that function can only search upward. That single fact explains most of the bug reports below:

  • If the file (or the VS Code folder root) is at or below the true Bazel root, the upward walk finds it (possibly the wrong ancestor if there's a nested MODULE.bazel in between — see Scenario S5).
  • If the true Bazel root is a descendant of the VS Code folder root, no upward walk from the folder root can ever find it. This is not a bug to patch, it is an structural impossibility of the current algorithm (Scenario S4).

Feature classification: which mechanism does each feature use

Feature Mechanism Source
Go to definition dynamic bazel_goto_definition_provider.ts:80
Buildifier format dynamic buildifier_format_provider.ts:33
Buildifier lint diagnostics dynamic buildifier_diagnostics_manager.ts:84
Completion provider hybrid (see note) bazel_completion_provider.ts
CodeLens provider dynamic code_lens_provider.ts:77
Document symbol provider dynamic bazel_target_symbol_provider.ts:28
Quickpick (invoked from editor) dynamic bazel_quickpick.ts:108
Bazel Targets tree view static bazel_workspace_tree_provider.ts:207
Build / Test / Run wrapper commands static bazel_wrapper_commands.ts (6 call sites, all fromWorkspaceFolders())
Tasks (tasks.json bazel task provider) static tasks.ts:100-102
${command:...} task/launch variables static command_variables.ts (3 call sites)
Quickpick (invoked from command palette) static bazel_quickpick.ts:105

Static features are, by construction, single-Bazel-root-per-VS-Code-folder. There is no per-file variant of "Bazel: Build Target" — it always resolves against the folder that owns the active editor (or the picked folder in a multi-root window), never against "the nearest root to whichever file is focused." No config option changes this without a much larger rewrite of every one of these commands.

Dynamic features can, in principle, support several independent Bazel roots coexisting under one VS Code folder — each open file resolves its own nearest root. This is a real capability some users rely on today (unrelated sibling checkouts opened together), and it's important not to regress it while fixing the ambiguous-nesting case (S5 below).

Note on the completion provider (post-#638): it's no longer purely dynamic. Lookup (provideCompletionItems) still resolves the workspace per file via fromDocument (dynamic), but its target cache (targetsMap) is populated per VS Code folder via fromWorkspaceFolder/getAll() (static) and keyed by that folder-resolved bazelWorkspacePath. In Scenario S5, a file physically inside a nested override'd module resolves (dynamically, nearest-wins) to the nested module's own path, which never matches the cache key populated for the outer root — so completions silently come back empty for files in that subtree today. This is a fresh, concrete instance of exactly the bug class this design is about; see "Still needed," item 7.

Scenarios identified

# Layout Auto-detection today Notes
S1 VS Code folder root == Bazel root ✅ correct Baseline case, no config needed.
S2 N VS Code folders (multi-root workspace), each == its own Bazel root ✅ correct bazel.workspacePath is scope: "resource", so each folder can also be configured independently if needed.
S3 Bazel root is an ancestor of the VS Code folder (folder opened in a subdirectory of the workspace) ✅ mostly correct since #608 #608 (merged) fixed 4 call sites that used the VS Code folder path instead of the discovered Bazel root as the query/spawn cwd. Static features scope their queries to the subfolder for performance.
S4 VS Code folder is an ancestor of the Bazel root (root opened above a nested MODULE.bazel) ❌ impossible for static features; dynamic features only work if the specific open file happens to already be under the nested root Upward search from the folder root structurally cannot descend into it. This is issue #472's "vscode-workspace containing bazel_root/MODULE.bazel" case, and the one Karnabanu tested against #625.
S5 Multiple MODULE.bazel/WORKSPACE files nested under one VS Code folder, where the nearer one is not the one that should be used (typically a local_path_override'd internal dependency whose own MODULE.bazel lacks the root's overrides) ❌ "nearest wins" picks the wrong root This is #621 and #472's core complaint. Correct behavior differs by file: files under the override'd sub-module still need queries to run with the root's overrides in scope.
S6 A directory within the workspace tree is structurally a separate Bazel repository (local_repository/bzlmod module override) that users nonetheless expect to browse/build/navigate as if it were part of the main workspace ⚠️ partially broken independent of root-resolution This is #416's territory — see below. It is entangled with S5 (same directory layout) but is a different axis of the problem: which cwd to run Bazel from vs. which label prefix (// vs @repo//) a file's targets live under.

#416 and its relationship to this problem

#416 ("Handle external dependencies") observes that the tree explorer, coverage, quickpick, and codelens all hard-code //...-rooted queries (//...:*, kind(rule, //pkg:all), etc.), so packages that live under an external repository (@repo//...) never show up, even though the repository is physically nested inside the workspace directory tree via local_repository/local_path_override.

This is the same directory layout as Scenario S5, but a different bug: even with a perfectly resolved bazelWorkspacePath and a correct cwd, bazel query //internal_dependency/...:* from the root will not return the override'd module's targets, because Bazel does not traverse repository boundaries with an unprefixed //... pattern — the correct query is @internal_dependency//...:*. No amount of cwd/root pinning fixes this; it requires the query-construction and label-formatting logic (package labels, tree grouping, codelens, coverage path mapping) to become repository-aware, which is exactly what #416 already scopes out as follow-up work.

Conclusion: #416 should stay a separate issue, but this design must not implement bazel.workspacePath in a way that forecloses solving #416 later — e.g. it must not assume every file under the pinned root belongs to the // label namespace of that root (see "Does this let dynamic features simplify?" below).

Related GitHub issues and PRs

Ref Title State Relevance
#625 feat(config): add bazel.workspacePath open Implements the proposed bazel.workspacePath setting; fixes root resolution (S4/S5) but not the static-feature relative-path math when the root diverges from the folder.
Zsi-r/vscode-bazel#1 fix: handle Bazel roots nested under workspace folders open, external fork, PR against #625's branch Supplemental fix: adds a containment-aware relative-path helper, fixes the //../... invalid-query bug in the tree view (S4), and fixes editor→tree sync to use bazelWorkspacePath instead of the VS Code folder path.
#621 getBazelWorkspaceFolder for multiple MODULE.bazel files open Primary motivating issue for #625 (Scenario S5).
#472 bazel 8 + bzlmod and child modules resolution open Second motivating issue for #625; also the source of the S4 layout Karnabanu tested.
#428 Extension cannot find bazel root without a WORKSPACE file open Related root-detection gap in bzlmod-only projects; worth re-testing once #625 lands since MODULE.bazel is already searched.
#383 Extension adds MODULE.bazel and MODULE.bazel.lock in local repositories open Likely a symptom of the extension (buildifier/bzlmod tooling) mis-treating a nested local_repository directory as its own root boundary — same root cause class as S5/S6.
#416 Handle external dependencies open Related but orthogonal — label/repository-namespace resolution, not cwd/root resolution. See above.
#607 BazelQuery uses VS Code workspace folder path instead of Bazel workspace root in multi-root workspaces closed Fixed by #608. Scenario S3.
#608 fix: use bazelWorkspacePath instead of workspaceFolder.uri.fsPath (merged) merged Fixed 4 static-feature call sites for S3. Established the pattern #625/Zsi-r#1 extend to S4.
#638 fix: silence workspace error in non-Bazel repos (merged 2026-08-19) merged Not a root-resolution fix, but adds BazelWorkspaceInfo.getAll() and makes the completion provider iterate/cache per VS Code folder instead of prompting via fromWorkspaceFolders(). Reinforces the one-root-per-folder model this design assumes, and exposes a new S5 gap in the completion provider (see feature table note and "Still needed" #7). Closes #446.
#446 "Failed to find a Bazel Workspace file" on non bazel repos that include BUILD files closed by #638 Adjacent: graceful-degradation when getBazelWorkspaceFolder legitimately finds nothing, not a divergence case. Listed for completeness since it shares BazelWorkspaceInfo machinery.

Adjacent but distinct axis (query/label scoping within a single, correctly-resolved root — not covered by this design decision): #598, #480 (closed), #618 (closed), #459 (closed), #174.

Design decision

Proposal

The design decision being made here is to make explicit, intentional, and the only supported setup an assumption the codebase has always implicitly leaned on but never stated or enforced: there is exactly one Bazel workspace root per VS Code workspace folder. Every scenario in this document where the extension misbehaves is a case where that assumption silently didn't hold (S4, S5) or held only by luck (S1, S3). Adopting it explicitly means the extension no longer tries to infer a single correct answer in cases where none can be inferred — it either finds one unambiguously, or it requires the user to state it.

Adding bazel.workspacePath (resource-scoped string, "" default falling back to auto-detect) is the logical consequence of that decision: it's simply the mechanism by which a user states the root when the extension can't determine it unambiguously on its own (S4, S5). When set, it deterministically pins exactly one Bazel workspace root for that VS Code folder, for both static and dynamic features.

Argumentation for why multi-root-in-one-folder is out of scope by design

Making a single VS Code folder host N independently-selectable Bazel roots simultaneously is a separate problem from root divergence but would make correct handling of the root divergence even harder. This design document does not try to support this because:

  1. Every static feature (tree view, build/test/run, tasks, command variables) is one-root-per-folder by construction; making it multi-root would mean redesigning those features to disambiguate "which root" per invocation (new UI, new commands), not a config-shape change.
  2. VS Code already has a first-class primitive for "several roots, each with its own settings": multi-root workspaces. bazel.workspacePath's scope: "resource" means each folder in a multi-root workspace can already carry its own value (or none, falling back to auto-detect). A user who genuinely has two unrelated, simultaneously-needed Bazel roots inside what is today one VS Code folder can split it into two folders in a .code-workspace file and get full, correct support for both, today.
  3. Bazel itself is designed around monorepos, not federations of unrelated workspaces sharing a directory. When a directory tree contains more than one MODULE.bazel/WORKSPACE, the far more common real-world case (and the one every motivating issue here — getBazelWorkspaceFolder for multiple MODULE.bazel files #621, bazel 8 + bzlmod and child modules resolution #472, Handle external dependencies #416 — actually describes) is not "two independent Bazel projects that happen to be nested," it's one root workspace that deliberately stitches the others in via local_path_override/local_repository. That's a single logical workspace with one correct root, not a genuine multi-root scenario. So the set of users who need true, simultaneous multi-root support (independent projects, no stitching root, both needed live at once) is smaller than it might first appear — most "multiple MODULE.bazel files under one folder" reports are actually S5, which pinning already solves correctly, not a case that needs multi-root support at all.

So: multi-root-in-one-folder is intentionally not handled by pinning; it's handled by asking users to use multi-root VS Code workspaces, which the extension already supports per-folder. Given point 3, we expect this to affect very few real users — most people who think they need it actually have a stitched monorepo (S5) and just need to point bazel.workspacePath at the stitching root. This should be stated explicitly in the setting's documentation (markdownDescription in package.json) so it isn't rediscovered as a bug report later.

Explicitly out of scope for this design decision (separate follow-ups)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions