Skip to content

Commit bfb8a70

Browse files
KarnabanuZsi-r
andauthored
feat(config): support manual Bazel workspace root selection (#687)
* feat(config): add bazel.workspacePath to manually specify workspace Add a new configuration option `bazel.workspacePath` that allows users to manually specify the Bazel workspace directory path. This is useful when: - Working with multiple MODULE.bazel files in nested directories - The auto-detection picks the wrong workspace root - Using local_path_override with multiple repositories The path can be absolute or relative to the VS Code workspace folder. Fixes #621 * test: add unit tests for getBazelWorkspaceFolder with workspacePath config Add tests to verify: - Auto-detection works when workspacePath is not configured - Configured absolute path is used correctly - Configured relative path is resolved correctly - Invalid configured path falls back to auto-detection - Configured path takes precedence over nested MODULE.bazel * fix: handle Bazel roots nested under workspace folders Prevent parent or unrelated VS Code folders from producing invalid //../... package queries, and resolve tree selections relative to the configured Bazel root. * chore: fix workspace path lint after rebase * fix: complete manual workspace path support * fix: resolve Bazel executable from workspace roots * test: pin Bazel version for nested module * test: allow more time for Bazel queries on CI Workspace-tree integration tests issue several sequential Bazel queries. On slower CI runners, server startup can exceed 10 seconds and cause intermittent timeouts, so raise the suite timeout to 30 seconds. --------- Co-authored-by: Zsi-r <zsr949480148@gmail.com>
1 parent 84484e6 commit bfb8a70

17 files changed

Lines changed: 694 additions & 31 deletions

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ this extension does not automatically _fix_ lint warnings during formatting,
3838
but you can opt into this by enabling the **Bazel: Buildifier Fix on Format**
3939
setting.
4040

41+
### Selecting the Bazel workspace root
42+
43+
The extension automatically searches upward for a `MODULE.bazel`,
44+
`REPO.bazel`, `WORKSPACE.bazel`, or `WORKSPACE` file. If that selects the wrong
45+
root, or if the Bazel root is nested below the folder opened in VS Code, set
46+
`bazel.workspacePath` to the Bazel root. The value can be an absolute path or a
47+
path relative to the VS Code workspace folder.
48+
49+
The extension supports one Bazel root per VS Code workspace folder. To work
50+
with multiple independent Bazel roots, use a VS Code multi-root workspace and
51+
add each Bazel root as a separate folder; `bazel.workspacePath` can then be set
52+
independently for each folder.
53+
4154
### Using a separate output base
4255

4356
By default this extension will use the default output base for running queries. This will cause builds to block queries, potentially causing degraded performance. In Bazel versions since 7.1 it is safe to disable this by changing the `bazel.queriesShareServer` setting to `false`. In earlier versions it can be safely disabled after adding the convenience symlinks to `.bazelignore`, for example:

package-lock.json

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@
175175
"default": true,
176176
"markdownDescription": "Whether to enable Buildifier support for formatting and linting Bazel files. When enabled, provides document formatting and lint diagnostics for BUILD, WORKSPACE, and .bzl files."
177177
},
178+
"bazel.workspacePath": {
179+
"type": "string",
180+
"default": "",
181+
"markdownDescription": "Pins the single Bazel workspace root used for this VS Code workspace folder. The path can be absolute or relative to the VS Code workspace folder. If unset, the extension searches for `MODULE.bazel`, `REPO.bazel`, `WORKSPACE.bazel`, or `WORKSPACE`. To use multiple independent Bazel roots, add each root as a folder in a VS Code multi-root workspace and configure this setting per folder.",
182+
"scope": "resource"
183+
},
178184
"bazel.pathsToIgnore": {
179185
"type": "array",
180186
"items": {
@@ -575,7 +581,6 @@
575581
"protobufjs": "^8.0.1",
576582
"protobufjs-cli": "^2.5.1",
577583
"vscode-languageclient": "^10.0.0",
578-
"vscode-uri": "^3.0.2",
579584
"which": "^6.0.0"
580585
}
581-
}
586+
}

src/bazel/bazel_availability.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ function fileExistsSync(filename: string): boolean {
2121
*/
2222
export function checkBazelIsAvailable(): boolean {
2323
const bazelExecutable = getBazelExecutablePath();
24-
const workspaceFolderPath =
25-
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
24+
const workspaceFolders = vscode.workspace.workspaceFolders ?? [];
2625

27-
// Check if the program exists as a relative path of the workspace
28-
const pathExists = workspaceFolderPath
29-
? fileExistsSync(path.join(workspaceFolderPath, bazelExecutable))
30-
: false;
26+
// Relative executable paths are evaluated from the directory where Bazel
27+
// commands run. Prefer each resolved Bazel root (including a root pinned by
28+
// bazel.workspacePath), and retain the VS Code folder as a fallback when no
29+
// Bazel workspace can be resolved.
30+
const pathExists = workspaceFolders.some((workspaceFolder) => {
31+
const workspaceFolderPath = workspaceFolder.uri.fsPath;
32+
const bazelWorkspacePath = getBazelWorkspaceFolder(workspaceFolderPath);
33+
return fileExistsSync(
34+
path.resolve(bazelWorkspacePath ?? workspaceFolderPath, bazelExecutable),
35+
);
36+
});
3137

3238
if (!pathExists) {
3339
try {

src/bazel/bazel_utils.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414

1515
import * as fs from "fs";
1616
import * as path from "path";
17+
import * as vscode from "vscode";
1718
import { blaze_query } from "../protos";
18-
import { getPathsToIgnore } from "../extension/configuration";
19+
import { getPathsToIgnore, getWorkspacePath } from "../extension/configuration";
1920
import { logError } from "../extension/logger";
2021
import { BazelQuery } from "./bazel_query";
2122

@@ -138,10 +139,99 @@ function findAncestorFile(
138139
return undefined;
139140
}
140141

142+
/**
143+
* Resolves the manually configured workspace path to an absolute path.
144+
*
145+
* @param fsPath The path to a file, used to determine the VS Code workspace folder.
146+
* @returns The resolved absolute workspace path, or undefined if not configured or invalid.
147+
*/
148+
function resolveConfiguredWorkspacePath(fsPath: string): string | undefined {
149+
const uri = vscode.Uri.file(fsPath);
150+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
151+
const configuredPath = getWorkspacePath(uri);
152+
153+
if (!configuredPath) {
154+
return undefined;
155+
}
156+
157+
let resolvedPath: string;
158+
159+
// Check if it's an absolute path
160+
if (path.isAbsolute(configuredPath)) {
161+
resolvedPath = configuredPath;
162+
} else if (workspaceFolder) {
163+
// Resolve relative path from VS Code workspace folder
164+
resolvedPath = path.join(workspaceFolder.uri.fsPath, configuredPath);
165+
} else {
166+
// No workspace folder, try to resolve from the file's directory
167+
resolvedPath = path.resolve(path.dirname(fsPath), configuredPath);
168+
}
169+
170+
// Verify the path exists and is a directory.
171+
try {
172+
const stat = fs.statSync(resolvedPath);
173+
if (!stat.isDirectory()) {
174+
logError(
175+
"Configured Bazel workspace path is not a directory",
176+
false,
177+
`Path: ${resolvedPath}`,
178+
);
179+
return undefined;
180+
}
181+
} catch {
182+
logError(
183+
"Configured Bazel workspace path does not exist",
184+
false,
185+
`Path: ${resolvedPath}`,
186+
);
187+
return undefined;
188+
}
189+
190+
const candidateIsInWorkspace =
191+
getBazelWorkspaceRelativePath(resolvedPath, fsPath) !== undefined;
192+
let candidateContainsWorkspace = false;
193+
try {
194+
candidateContainsWorkspace =
195+
fs.statSync(fsPath).isDirectory() &&
196+
getBazelWorkspaceRelativePath(fsPath, resolvedPath) !== undefined;
197+
} catch {
198+
// A nonexistent candidate cannot contain the configured workspace.
199+
}
200+
if (!candidateIsInWorkspace && !candidateContainsWorkspace) {
201+
return undefined;
202+
}
203+
204+
const workspaceFiles = [
205+
"MODULE.bazel",
206+
"REPO.bazel",
207+
"WORKSPACE.bazel",
208+
"WORKSPACE",
209+
];
210+
for (const file of workspaceFiles) {
211+
try {
212+
fs.accessSync(path.join(resolvedPath, file), fs.constants.F_OK);
213+
return resolvedPath;
214+
} catch {
215+
// File not found, continue.
216+
}
217+
}
218+
219+
logError(
220+
"Configured Bazel workspace path has no workspace marker file",
221+
false,
222+
`Path: ${resolvedPath}`,
223+
`Expected one of: ${workspaceFiles.join(", ")}`,
224+
);
225+
return undefined;
226+
}
227+
141228
/**
142229
* Search for the path to the directory that has the Bazel WORKSPACE file for
143230
* the given file.
144231
*
232+
* If a workspace path is manually configured via `bazel.workspacePath`, it will
233+
* be used instead of auto-detection.
234+
*
145235
* If multiple directories along the path to the file have workspace files,
146236
* the lowest path is returned.
147237
*
@@ -150,6 +240,13 @@ function findAncestorFile(
150240
* otherwise undefined.
151241
*/
152242
export function getBazelWorkspaceFolder(fsPath: string): string | undefined {
243+
// First, check if a workspace path is manually configured
244+
const configuredWorkspace = resolveConfiguredWorkspacePath(fsPath);
245+
if (configuredWorkspace) {
246+
return configuredWorkspace;
247+
}
248+
249+
// Fall back to auto-detection
153250
const workspaceFile = findAncestorFile(fsPath, [
154251
"MODULE.bazel",
155252
"REPO.bazel",
@@ -159,6 +256,26 @@ export function getBazelWorkspaceFolder(fsPath: string): string | undefined {
159256
return workspaceFile ? path.dirname(workspaceFile) : undefined;
160257
}
161258

259+
/**
260+
* Returns a Bazel-compatible path relative to a workspace root.
261+
*
262+
* If the candidate is outside the workspace root, returns undefined.
263+
*/
264+
export function getBazelWorkspaceRelativePath(
265+
workspaceRoot: string,
266+
candidatePath: string,
267+
): string | undefined {
268+
const relativePath = path.relative(workspaceRoot, candidatePath);
269+
if (
270+
path.isAbsolute(relativePath) ||
271+
relativePath === ".." ||
272+
relativePath.startsWith(`..${path.sep}`)
273+
) {
274+
return undefined;
275+
}
276+
return relativePath.replace(/\\/g, "/");
277+
}
278+
162279
/**
163280
* Finds the nearest Bazel package file (BUILD or BUILD.bazel) for the given file path
164281
* by searching up the directory tree, but only if it's within the current Bazel workspace.

src/definition/bazel_goto_definition_provider.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
TextDocument,
2121
Uri,
2222
} from "vscode";
23-
import { Utils } from "vscode-uri";
2423
import { BazelQuery, BazelWorkspaceInfo, QueryLocation } from "../bazel";
2524
import { getBazelExecutablePath } from "../extension/configuration";
2625
import { blaze_query } from "../protos";
@@ -86,7 +85,10 @@ export class BazelGotoDefinitionProvider implements DefinitionProvider {
8685
const range = document.getWordRangeAtPosition(position, LABEL_REGEX);
8786
const targetText = document.getText(range);
8887

89-
const location = await targetToUri(targetText, Utils.dirname(document.uri));
88+
const location = await targetToUri(
89+
targetText,
90+
Uri.file(workspaceInfo.bazelWorkspacePath),
91+
);
9092

9193
return location
9294
? [

src/extension/configuration.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ export function getPathsToIgnore(): string[] {
5757
return getConfigurationWithDefault<string[]>("bazel", "pathsToIgnore");
5858
}
5959

60+
/**
61+
* Gets the manually specified Bazel workspace path from the workspace
62+
* configuration.
63+
*
64+
* @param scopeUri Optional URI to scope the configuration lookup to a specific
65+
* resource.
66+
* @returns The manually specified workspace path, or an empty string if not set.
67+
*/
68+
export function getWorkspacePath(scopeUri?: vscode.Uri): string {
69+
const config = vscode.workspace.getConfiguration("bazel", scopeUri);
70+
return (config.get<string>("workspacePath") || "").trim();
71+
}
72+
6073
export function getStartupOptions(): string[] {
6174
return getConfigurationWithDefault<string[]>(
6275
"bazel.commandLine",

src/workspace-tree/bazel_workspace_folder_tree_item.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import * as path from "path";
1615
import * as vscode from "vscode";
17-
import { BazelWorkspaceInfo, BazelQuery } from "../bazel";
16+
import {
17+
BazelWorkspaceInfo,
18+
BazelQuery,
19+
getBazelWorkspaceRelativePath,
20+
} from "../bazel";
1821
import {
1922
getBazelExecutablePath,
2023
getQueryExpression,
2124
} from "../extension/configuration";
25+
import { logError } from "../extension/logger";
2226
import { blaze_query } from "../protos";
2327
import { BazelPackageTreeItem } from "./bazel_package_tree_item";
2428
import { BazelTargetTreeItem } from "./bazel_target_tree_item";
@@ -202,9 +206,26 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem {
202206
}
203207
const bazelWorkspacePath = this.workspaceInfo.bazelWorkspacePath;
204208
const workspaceFolderPath = this.workspaceInfo.workspaceFolder.uri.fsPath;
205-
const relativePath = path
206-
.relative(bazelWorkspacePath, workspaceFolderPath)
207-
.replace(/\\/g, "/");
209+
let relativePath = getBazelWorkspaceRelativePath(
210+
bazelWorkspacePath,
211+
workspaceFolderPath,
212+
);
213+
if (relativePath === undefined) {
214+
const workspacePathFromFolder = getBazelWorkspaceRelativePath(
215+
workspaceFolderPath,
216+
bazelWorkspacePath,
217+
);
218+
if (workspacePathFromFolder === undefined) {
219+
logError(
220+
"Configured Bazel workspace is unrelated to the VS Code folder",
221+
false,
222+
`Bazel workspace: ${bazelWorkspacePath}`,
223+
`VS Code folder: ${workspaceFolderPath}`,
224+
);
225+
return [];
226+
}
227+
relativePath = "";
228+
}
208229

209230
const queryExpression = getQueryExpression();
210231
// When the VS Code folder is a subdirectory of the Bazel workspace,

src/workspace-tree/workspace_tree_provider.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
// limitations under the License.
1414

1515
import * as vscode from "vscode";
16-
import * as path from "path";
17-
import { BazelWorkspaceInfo } from "../bazel";
16+
17+
import { BazelWorkspaceInfo, getBazelWorkspaceRelativePath } from "../bazel";
1818
import { IBazelTreeItem } from "./bazel_tree_item";
1919
import { BazelWorkspaceFolderTreeItem } from "./bazel_workspace_folder_tree_item";
2020
import { BazelPackageTreeItem } from "./bazel_package_tree_item";
@@ -331,11 +331,11 @@ export class BazelWorkspaceTreeProvider
331331
return undefined; // File does not belong to a detected bazel workspace
332332
}
333333

334-
const relativeFilePath = path.relative(
335-
workspaceFolderVSCode.uri.fsPath,
334+
const relativeFilePath = getBazelWorkspaceRelativePath(
335+
workspaceFolderTreeItem.getWorkspaceInfo().bazelWorkspacePath,
336336
fileUri.fsPath,
337337
);
338-
if (!relativeFilePath) {
338+
if (relativeFilePath === undefined || relativeFilePath === "") {
339339
return undefined; // Sanity check, should never happen
340340
}
341341

0 commit comments

Comments
 (0)