From 3ca663e16a76aa14b3947eda3eba641168a7866e Mon Sep 17 00:00:00 2001 From: Ahbong Chang Date: Sat, 12 Oct 2024 21:18:30 +0800 Subject: [PATCH] refactor(workspace-tree): Add BazelQuerier for workspace tree Separate and abstract the tree view specific bazel queries in prepare for adding test cases. Also pull out the configuration getter since I found it simpler to use (auto complete and check by tsc, and maybe customizing the defaults based on more complicated logic). --- package.json | 2 +- src/bazel/bazel_utils.ts | 16 +++ src/extension/configuration.ts | 8 ++ src/workspace-tree/bazel_package_tree_item.ts | 24 ++-- .../bazel_workspace_folder_tree_item.ts | 32 ++---- .../bazel_workspace_tree_provider.ts | 11 +- src/workspace-tree/index.ts | 1 + src/workspace-tree/querier.ts | 108 ++++++++++++++++++ 8 files changed, 164 insertions(+), 38 deletions(-) create mode 100644 src/workspace-tree/querier.ts diff --git a/package.json b/package.json index bab267bb..f2169f11 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ }, "bazel.commandLine.queryExpression": { "type": "string", - "default": "...:*", + "default": "", "description": "A [query language expression](https://bazel.build/query/language) which determines the packages displayed in the workspace tree and quick picker. The default inspects the entire workspace, but you could narrow it. For example: `//part/you/want/...:*`" }, "bazel.lsp.command": { diff --git a/src/bazel/bazel_utils.ts b/src/bazel/bazel_utils.ts index 8ed174fa..940dc68d 100644 --- a/src/bazel/bazel_utils.ts +++ b/src/bazel/bazel_utils.ts @@ -18,6 +18,22 @@ import * as vscode from "vscode"; import { blaze_query } from "../protos"; import { BazelQuery } from "./bazel_query"; +/** + * Get the absolute path for a queried label. + * + * The queried package path are without leading double slash, while we want to + * provide with leading slash. + * + * @param label The label. + * @returns The label in absolute path. + */ +export function labelFromQueriedToAbsolute(label: string): string { + // External packages are in form `@repo//foo/bar`. + // Main repo relative label are in form `foo/bar`. + // Main repo absolute label are in form `//foo/bar`. + return label.includes("//") ? label : `//${label}`; +} + /** * Get the package label for a build file. * diff --git a/src/extension/configuration.ts b/src/extension/configuration.ts index 92796255..4c626a4b 100644 --- a/src/extension/configuration.ts +++ b/src/extension/configuration.ts @@ -32,3 +32,11 @@ export function getDefaultBazelExecutablePath(): string { } return bazelExecutable; } + +export function getDefaultQueryExpression(): string { + return ( + vscode.workspace + .getConfiguration("bazel.commandLine") + .get("queryExpression") ?? "...:*" + ); +} diff --git a/src/workspace-tree/bazel_package_tree_item.ts b/src/workspace-tree/bazel_package_tree_item.ts index bb9aca52..4b118033 100644 --- a/src/workspace-tree/bazel_package_tree_item.ts +++ b/src/workspace-tree/bazel_package_tree_item.ts @@ -14,15 +14,11 @@ import * as vscode from "vscode"; import { BazelWorkspaceInfo } from "../bazel"; -import { - BazelQuery, - IBazelCommandAdapter, - IBazelCommandOptions, -} from "../bazel"; -import { getDefaultBazelExecutablePath } from "../extension/configuration"; +import { IBazelCommandAdapter, IBazelCommandOptions } from "../bazel"; import { blaze_query } from "../protos"; import { BazelTargetTreeItem } from "./bazel_target_tree_item"; import { IBazelTreeItem } from "./bazel_tree_item"; +import { IBazelQuerier } from "./querier"; /** A tree item representing a build package. */ export class BazelPackageTreeItem @@ -32,7 +28,7 @@ export class BazelPackageTreeItem * The array of subpackages that should be shown directly under this package * item. */ - public directSubpackages: BazelPackageTreeItem[] = []; + public directSubpackages: IBazelTreeItem[] = []; /** * Initializes a new tree item with the given workspace path and package path. @@ -44,6 +40,7 @@ export class BazelPackageTreeItem * {@code packagePath} should be stripped for the item's label. */ constructor( + private readonly querier: IBazelQuerier, private readonly workspaceInfo: BazelWorkspaceInfo, private readonly packagePath: string, private readonly parentPackagePath: string, @@ -54,17 +51,14 @@ export class BazelPackageTreeItem } public async getChildren(): Promise { - const queryResult = await new BazelQuery( - getDefaultBazelExecutablePath(), - this.workspaceInfo.bazelWorkspacePath, - ).queryTargets(`//${this.packagePath}:all`, { - ignoresErrors: true, - sortByRuleName: true, - }); + const queryResult = await this.querier.queryChildrenTargets( + this.workspaceInfo, + this.packagePath, + ); const targets = queryResult.target.map((target: blaze_query.ITarget) => { return new BazelTargetTreeItem(this.workspaceInfo, target); }); - return (this.directSubpackages as IBazelTreeItem[]).concat(targets); + return this.directSubpackages.concat(targets); } public getLabel(): string { diff --git a/src/workspace-tree/bazel_workspace_folder_tree_item.ts b/src/workspace-tree/bazel_workspace_folder_tree_item.ts index 40d30ef9..a27f4e23 100644 --- a/src/workspace-tree/bazel_workspace_folder_tree_item.ts +++ b/src/workspace-tree/bazel_workspace_folder_tree_item.ts @@ -14,12 +14,11 @@ import * as vscode from "vscode"; import { BazelWorkspaceInfo } from "../bazel"; -import { BazelQuery } from "../bazel"; -import { getDefaultBazelExecutablePath } from "../extension/configuration"; import { blaze_query } from "../protos"; import { BazelPackageTreeItem } from "./bazel_package_tree_item"; import { BazelTargetTreeItem } from "./bazel_target_tree_item"; import { IBazelTreeItem } from "./bazel_tree_item"; +import { IBazelQuerier } from "./querier"; /** A tree item representing a workspace folder. */ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { @@ -28,7 +27,10 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { * * @param workspaceFolder The workspace folder that the tree item represents. */ - constructor(private workspaceInfo: BazelWorkspaceInfo) {} + constructor( + private readonly querier: IBazelQuerier, + private readonly workspaceInfo: BazelWorkspaceInfo, + ) {} public mightHaveChildren(): boolean { return true; @@ -78,7 +80,7 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { packagePaths: string[], startIndex: number, endIndex: number, - treeItems: BazelPackageTreeItem[], + treeItems: IBazelTreeItem[], parentPackagePath: string, ) { // We can assume that the caller has sorted the packages, so we scan them to @@ -123,6 +125,7 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { // tree node for the element at groupStart and then recursively call the // algorithm again to group its children. const item = new BazelPackageTreeItem( + this.querier, this.workspaceInfo, packagePath, parentPackagePath, @@ -156,15 +159,7 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { if (!this.workspaceInfo) { return Promise.resolve([] as IBazelTreeItem[]); } - const workspacePath = this.workspaceInfo.workspaceFolder.uri.fsPath; - const packagePaths = await new BazelQuery( - getDefaultBazelExecutablePath(), - workspacePath, - ).queryPackages( - vscode.workspace - .getConfiguration("bazel.commandLine") - .get("queryExpression"), - ); + const packagePaths = await this.querier.queryPackages(this.workspaceInfo); const topLevelItems: BazelPackageTreeItem[] = []; this.buildPackageTree( packagePaths, @@ -176,13 +171,10 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { // Now collect any targets in the directory also (this can fail since // there might not be a BUILD files at this level (but down levels)). - const queryResult = await new BazelQuery( - getDefaultBazelExecutablePath(), - workspacePath, - ).queryTargets(`:all`, { - ignoresErrors: true, - sortByRuleName: true, - }); + const queryResult = await this.querier.queryChildrenTargets( + this.workspaceInfo, + "", + ); const targets = queryResult.target.map((target: blaze_query.ITarget) => { return new BazelTargetTreeItem(this.workspaceInfo, target); }); diff --git a/src/workspace-tree/bazel_workspace_tree_provider.ts b/src/workspace-tree/bazel_workspace_tree_provider.ts index 5e4907e9..daf79751 100644 --- a/src/workspace-tree/bazel_workspace_tree_provider.ts +++ b/src/workspace-tree/bazel_workspace_tree_provider.ts @@ -16,6 +16,7 @@ import * as vscode from "vscode"; import { BazelWorkspaceInfo } from "../bazel"; import { IBazelTreeItem } from "./bazel_tree_item"; import { BazelWorkspaceFolderTreeItem } from "./bazel_workspace_folder_tree_item"; +import { IBazelQuerier, ProcessBazelQuerier } from "./querier"; /** * Provides a tree of Bazel build packages and targets for the VS Code explorer @@ -38,8 +39,11 @@ export class BazelWorkspaceTreeProvider * Initializes a new tree provider with the given extension context. * * @param context The VS Code extension context. + * @param querier The interface providing the `bazel query` results. */ - constructor() { + constructor( + private readonly querier: IBazelQuerier = new ProcessBazelQuerier(), + ) { const buildFilesWatcher = vscode.workspace.createFileSystemWatcher( "**/{BUILD,BUILD.bazel}", false, @@ -126,7 +130,10 @@ export class BazelWorkspaceTreeProvider .map((folder) => { const workspaceInfo = BazelWorkspaceInfo.fromWorkspaceFolder(folder); if (workspaceInfo) { - return new BazelWorkspaceFolderTreeItem(workspaceInfo); + return new BazelWorkspaceFolderTreeItem( + this.querier, + workspaceInfo, + ); } return undefined; }) diff --git a/src/workspace-tree/index.ts b/src/workspace-tree/index.ts index d5ce76df..e25accb3 100644 --- a/src/workspace-tree/index.ts +++ b/src/workspace-tree/index.ts @@ -13,3 +13,4 @@ // limitations under the License. export * from "./bazel_workspace_tree_provider"; +export * from "./querier"; diff --git a/src/workspace-tree/querier.ts b/src/workspace-tree/querier.ts new file mode 100644 index 00000000..5217d8eb --- /dev/null +++ b/src/workspace-tree/querier.ts @@ -0,0 +1,108 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as vscode from "vscode"; +import { + BazelQuery, + BazelWorkspaceInfo, + labelFromQueriedToAbsolute, +} from "../bazel"; +import { + getDefaultBazelExecutablePath, + getDefaultQueryExpression, +} from "../extension/configuration"; +import { blaze_query } from "../protos"; +import { BazelInfo } from "../bazel/bazel_info"; + +/** + * Bazel querier for workspace tree. + * + * The interface defined here is to specifying the operation required for a + * workspace tree instead of all bazel query syntax and options supported. + * + * The function named with queryXxx are all for querying bazel informations. + */ +export interface IBazelQuerier { + /** + * Queries bazel workspace path by given vscode workspace folder. + * + * @param workspaceInfo the Bazel workspace info. + * @returns package name queries in absolute apparent paths. + */ + queryWorkspace( + workspaceFolder: vscode.WorkspaceFolder, + ): Thenable; + + /** + * Queries all Bazel packages in a workspace folder. + * + * @param workspaceInfo the Bazel workspace info. + * @returns package name queries in absolute apparent paths. + */ + queryPackages(workspaceInfo: BazelWorkspaceInfo): Thenable; + + /** + * Queries all children targets of a Bazel package. + * + * @param workspaceInfo the Bazel workspace info. + * @param packagePath the Bazel package path. Could be either in absolute label or + * relative to the opening vscode workspace in `workspaceInfo`. + */ + queryChildrenTargets( + workspaceInfo: BazelWorkspaceInfo, + packagePath: string, + ): Thenable; +} + +/** + * Calling Bazel process for the queries. + */ +export class ProcessBazelQuerier implements IBazelQuerier { + async queryWorkspace( + workspaceFolder: vscode.WorkspaceFolder, + ): Promise { + try { + const bazelWorkspacePath = await new BazelInfo( + getDefaultBazelExecutablePath(), + workspaceFolder.uri.fsPath, + ).getOne("workspace"); + return new BazelWorkspaceInfo(bazelWorkspacePath, workspaceFolder); + } catch { + return undefined; + } + } + + async queryPackages(workspaceInfo: BazelWorkspaceInfo): Promise { + const packages = await new BazelQuery( + getDefaultBazelExecutablePath(), + workspaceInfo.workspaceFolder.uri.fsPath, + ).queryPackages(getDefaultQueryExpression()); + return packages.map(labelFromQueriedToAbsolute); + } + + queryChildrenTargets( + workspaceInfo: BazelWorkspaceInfo, + packagePath: string, + ): Promise { + // Getting all rules without files, thus using :all instead of :*. + const query = `${packagePath}:all`; + return new BazelQuery( + getDefaultBazelExecutablePath(), + workspaceInfo.workspaceFolder.uri.fsPath, + ).queryTargets(query, { + ignoresErrors: true, + sortByRuleName: true, + }); + } +}