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/bazel/bazel_workspace_info.ts b/src/bazel/bazel_workspace_info.ts index 7fde6e7e..dfa86a96 100644 --- a/src/bazel/bazel_workspace_info.ts +++ b/src/bazel/bazel_workspace_info.ts @@ -100,8 +100,8 @@ export class BazelWorkspaceInfo { * belong to a workspace folder (for example, a standalone file loaded * into the editor). */ - private constructor( + constructor( public readonly bazelWorkspacePath: string, - public readonly workspaceFolder: vscode.WorkspaceFolder | undefined, + public readonly workspaceFolder?: vscode.WorkspaceFolder, ) {} } 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/extension/extension.ts b/src/extension/extension.ts index b3897222..141c79de 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -42,7 +42,7 @@ import { activateWrapperCommands } from "./bazel_wrapper_commands"; */ export async function activate(context: vscode.ExtensionContext) { const workspaceTreeProvider = - BazelWorkspaceTreeProvider.fromExtensionContext(context); + await BazelWorkspaceTreeProvider.fromExtensionContext(context); context.subscriptions.push(workspaceTreeProvider); const codeLensProvider = new BazelBuildCodeLensProvider(context); @@ -96,11 +96,15 @@ export async function activate(context: vscode.ExtensionContext) { ), // Commands ...activateWrapperCommands(), - vscode.commands.registerCommand("bazel.refreshBazelBuildTargets", () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - completionItemProvider.refresh(); - workspaceTreeProvider.refresh(); - }), + vscode.commands.registerCommand( + "bazel.refreshBazelBuildTargets", + async () => { + await Promise.allSettled([ + completionItemProvider.refresh(), + workspaceTreeProvider.refresh(vscode.workspace.workspaceFolders), + ]); + }, + ), vscode.commands.registerCommand( "bazel.copyTargetToClipboard", bazelCopyTargetToClipboard, diff --git a/src/workspace-tree/bazel_package_tree_item.ts b/src/workspace-tree/bazel_package_tree_item.ts index 2004343a..aeb89aea 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"; import { Resources } from "../extension/resources"; /** A tree item representing a build package. */ @@ -33,11 +29,12 @@ 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. * + * @param querier Querier for getting information inside a Bazel workspace. * @param workspacePath The path to the VS Code workspace folder. * @param packagePath The path to the build package that this item represents. * @param parentPackagePath The path to the build package of the tree item @@ -46,9 +43,10 @@ export class BazelPackageTreeItem */ constructor( private readonly resources: Resources, + private readonly querier: IBazelQuerier, private readonly workspaceInfo: BazelWorkspaceInfo, private readonly packagePath: string, - private readonly parentPackagePath: string, + private readonly parentPackagePath?: string, ) {} public mightHaveChildren(): boolean { @@ -56,13 +54,10 @@ 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.resources, @@ -70,18 +65,27 @@ export class BazelPackageTreeItem target, ); }); - return (this.directSubpackages as IBazelTreeItem[]).concat(targets); + return this.directSubpackages.concat(targets); } public getLabel(): string { - // If this is a top-level package, include the leading double-slash on the - // label. - if (this.parentPackagePath.length === 0) { - return `//${this.packagePath}`; + if (this.parentPackagePath === undefined) { + return this.packagePath; } - // Otherwise, strip off the part of the package path that came from the - // parent item (along with the slash). - return this.packagePath.substring(this.parentPackagePath.length + 1); + // Strip off the part of the package path that came from the + // parent item. + const parentLength = this.parentPackagePath.length; + // (null) + // //a + // + // @repo//foo + // @repo//foo/bar + // + // @repo// + // @repo//foo + const diffIsLeadingSlash = this.packagePath[parentLength] === "/"; + const prefixLength = diffIsLeadingSlash ? parentLength + 1 : parentLength; + return this.packagePath.substring(prefixLength); } public getIcon(): vscode.ThemeIcon { @@ -89,11 +93,11 @@ export class BazelPackageTreeItem } public getTooltip(): string { - return `//${this.packagePath}`; + return this.packagePath; } - public getCommand(): vscode.Command | undefined { - return undefined; + public getCommand(): Promise { + return Promise.resolve(undefined); } public getContextValue(): string { @@ -103,7 +107,7 @@ export class BazelPackageTreeItem public getBazelCommandOptions(): IBazelCommandOptions { return { options: [], - targets: [`//${this.packagePath}`], + targets: [this.packagePath], workspaceInfo: this.workspaceInfo, }; } diff --git a/src/workspace-tree/bazel_target_tree_item.ts b/src/workspace-tree/bazel_target_tree_item.ts index b79ae680..de1c3325 100644 --- a/src/workspace-tree/bazel_target_tree_item.ts +++ b/src/workspace-tree/bazel_target_tree_item.ts @@ -13,11 +13,14 @@ // limitations under the License. import * as vscode from "vscode"; +import * as fs from "fs/promises"; import { BazelWorkspaceInfo, QueryLocation } from "../bazel"; import { IBazelCommandAdapter, IBazelCommandOptions } from "../bazel"; import { blaze_query } from "../protos"; import { IBazelTreeItem } from "./bazel_tree_item"; import { getBazelRuleIcon } from "./icons"; +import { BazelInfo } from "../bazel/bazel_info"; +import { getDefaultBazelExecutablePath } from "../extension/configuration"; import { Resources } from "../extension/resources"; /** A tree item representing a build target. */ @@ -61,16 +64,36 @@ export class BazelTargetTreeItem } public getTooltip(): string { - return `${this.target.rule.name}`; + return this.target.rule.name; } - public getCommand(): vscode.Command | undefined { + public async getCommand(): Promise { + // Resolve the prefix if prefix is + // $(./prebuilts/bazel info output_base)/external/ const location = new QueryLocation(this.target.rule.location); + // Maybe we should cache this to prevent the repeating invocations. + const outputBase = await new BazelInfo( + getDefaultBazelExecutablePath(), + this.workspaceInfo.workspaceFolder.uri.fsPath, + ).getOne("output_base"); + let locationPath = location.path; + // If location is in pattern `${execRoot}/external//...`, then it + // should be a file in local_repository(). Trying to remapping it back to + // the origin source folder by resolve the symlink + // ${execRoot}/external/. + const outputBaseExternalPath = `${outputBase}/external/`; + if (location.path.startsWith(outputBaseExternalPath)) { + const repoPath = location.path.substring(outputBaseExternalPath.length); + const repoPathMatch = repoPath.match(/^([^/]+)\/(.*)$/); + if (repoPathMatch.length === 3) { + const repo = repoPathMatch[1]; + const rest = repoPathMatch[2]; + const realRepo = await fs.realpath(`${outputBaseExternalPath}${repo}`); + locationPath = `${realRepo}/${rest}`; + } + } return { - arguments: [ - vscode.Uri.file(location.path), - { selection: location.range }, - ], + arguments: [vscode.Uri.file(locationPath), { selection: location.range }], command: "vscode.open", title: "Jump to Build Target", }; @@ -87,7 +110,7 @@ export class BazelTargetTreeItem public getBazelCommandOptions(): IBazelCommandOptions { return { options: [], - targets: [`${this.target.rule.name}`], + targets: [this.target.rule.name], workspaceInfo: this.workspaceInfo, }; } diff --git a/src/workspace-tree/bazel_tree_item.ts b/src/workspace-tree/bazel_tree_item.ts index 31ce7876..059f0bc0 100644 --- a/src/workspace-tree/bazel_tree_item.ts +++ b/src/workspace-tree/bazel_tree_item.ts @@ -46,7 +46,7 @@ export interface IBazelTreeItem { getTooltip(): string | undefined; /** Returns the command that should be executed when the item is selected. */ - getCommand(): vscode.Command | undefined; + getCommand(): Promise; /** * Returns an identifying string that is used to filter which commands are diff --git a/src/workspace-tree/bazel_workspace_folder_tree_item.ts b/src/workspace-tree/bazel_workspace_folder_tree_item.ts index d403c177..39400013 100644 --- a/src/workspace-tree/bazel_workspace_folder_tree_item.ts +++ b/src/workspace-tree/bazel_workspace_folder_tree_item.ts @@ -13,12 +13,12 @@ // limitations under the License. import * as vscode from "vscode"; -import { BazelWorkspaceInfo, BazelQuery } from "../bazel"; -import { getDefaultBazelExecutablePath } from "../extension/configuration"; +import { BazelWorkspaceInfo } from "../bazel"; 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"; import { Resources } from "../extension/resources"; /** A tree item representing a workspace folder. */ @@ -26,10 +26,12 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { /** * Initializes a new tree item with the given workspace folder. * + * @param querier Querier for getting information inside a Bazel workspace. * @param workspaceFolder The workspace folder that the tree item represents. */ constructor( private readonly resources: Resources, + private readonly querier: IBazelQuerier, private readonly workspaceInfo: BazelWorkspaceInfo, ) {} @@ -53,8 +55,8 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { return this.workspaceInfo.workspaceFolder.uri.fsPath; } - public getCommand(): vscode.Command | undefined { - return undefined; + public getCommand(): Promise { + return Promise.resolve(undefined); } public getContextValue(): string { @@ -81,8 +83,8 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { packagePaths: string[], startIndex: number, endIndex: number, - treeItems: BazelPackageTreeItem[], - parentPackagePath: string, + treeItems: IBazelTreeItem[], + parentPackagePath?: string, ) { // We can assume that the caller has sorted the packages, so we scan them to // find groupings into which we should traverse more deeply. For example, if @@ -116,7 +118,9 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { // erroneously collapse something like "foo" and "foobar". while ( groupEnd < endIndex && - packagePaths[groupEnd].startsWith(packagePath + "/") + (packagePaths[groupEnd].startsWith(packagePath + "/") || + (packagePaths[groupEnd].startsWith(packagePath) && + packagePath.endsWith("//"))) ) { groupEnd++; } @@ -127,6 +131,7 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { // algorithm again to group its children. const item = new BazelPackageTreeItem( this.resources, + this.querier, this.workspaceInfo, packagePath, parentPackagePath, @@ -160,33 +165,16 @@ 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 topLevelItems: BazelPackageTreeItem[] = []; - this.buildPackageTree( - packagePaths, - 0, - packagePaths.length, - topLevelItems, - "", - ); + const packagePaths = await this.querier.queryPackages(this.workspaceInfo); + const topLevelItems: IBazelTreeItem[] = []; + this.buildPackageTree(packagePaths, 0, packagePaths.length, topLevelItems); // 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.resources, @@ -195,6 +183,6 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { ); }); - return Promise.resolve((topLevelItems as IBazelTreeItem[]).concat(targets)); + return Promise.resolve(topLevelItems.concat(targets)); } } diff --git a/src/workspace-tree/bazel_workspace_tree_provider.ts b/src/workspace-tree/bazel_workspace_tree_provider.ts index f050da47..502e5112 100644 --- a/src/workspace-tree/bazel_workspace_tree_provider.ts +++ b/src/workspace-tree/bazel_workspace_tree_provider.ts @@ -13,10 +13,11 @@ // limitations under the License. import * as vscode from "vscode"; -import { BazelWorkspaceInfo } from "../bazel"; +import { assert } from "../assert"; import { IBazelTreeItem } from "./bazel_tree_item"; import { BazelWorkspaceFolderTreeItem } from "./bazel_workspace_folder_tree_item"; import { Resources } from "../extension/resources"; +import { IBazelQuerier, ProcessBazelQuerier } from "./querier"; /** * Provides a tree of Bazel build packages and targets for the VS Code explorer @@ -31,72 +32,76 @@ export class BazelWorkspaceTreeProvider public readonly onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event; /** The cached toplevel items. */ - private workspaceFolderTreeItems: BazelWorkspaceFolderTreeItem[] | undefined; + private workspaceFolderTreeItems?: IBazelTreeItem[]; private disposables: vscode.Disposable[] = []; - public static fromExtensionContext( + public static async fromExtensionContext( context: vscode.ExtensionContext, - ): BazelWorkspaceTreeProvider { - return new BazelWorkspaceTreeProvider( + ): Promise { + const workspaceTreeProvider = new BazelWorkspaceTreeProvider( Resources.fromExtensionContext(context), + new ProcessBazelQuerier(), + ); + + const refreshWorkspaceFolders = () => + workspaceTreeProvider.refresh(vscode.workspace.workspaceFolders); + + workspaceTreeProvider.disposables.push( + vscode.workspace.onDidChangeWorkspaceFolders(refreshWorkspaceFolders), ); - } - /** - * Initializes a new tree provider with the given extension context. - * - * @param context The VS Code extension context. - */ - constructor(private readonly resources: Resources) { const buildFilesWatcher = vscode.workspace.createFileSystemWatcher( "**/{BUILD,BUILD.bazel}", false, false, false, ); - this.disposables.push( + workspaceTreeProvider.disposables.push( buildFilesWatcher, - buildFilesWatcher.onDidChange(() => this.onBuildFilesChanged()), - buildFilesWatcher.onDidCreate(() => this.onBuildFilesChanged()), - buildFilesWatcher.onDidDelete(() => this.onBuildFilesChanged()), - vscode.workspace.onDidChangeWorkspaceFolders(() => this.refresh()), + buildFilesWatcher.onDidChange(refreshWorkspaceFolders), + buildFilesWatcher.onDidCreate(refreshWorkspaceFolders), + buildFilesWatcher.onDidDelete(refreshWorkspaceFolders), ); - this.updateWorkspaceFolderTreeItems(); + await refreshWorkspaceFolders(); + + return workspaceTreeProvider; } - public getChildren(element?: IBazelTreeItem): Thenable { + /** + * @param querier The interface providing the `bazel query` results. + */ + constructor( + private readonly resources: Resources, + private readonly querier: IBazelQuerier, + ) {} + + public async getChildren( + element?: IBazelTreeItem, + ): Promise { // If we're given an element, we're not asking for the top-level elements, // so just delegate to that element to get its children. if (element) { return element.getChildren(); } - if (this.workspaceFolderTreeItems === undefined) { - this.updateWorkspaceFolderTreeItems(); - } + // Assuming the extension or test cases should call refresh at least once. + assert(this.workspaceFolderTreeItems !== undefined); - if (this.workspaceFolderTreeItems && vscode.workspace.workspaceFolders) { - // If the user has a workspace open and there's only one folder in it, - // then don't show the workspace folder; just show its packages at the top - // level. - if (vscode.workspace.workspaceFolders.length === 1) { - const folderItem = this.workspaceFolderTreeItems[0]; - return folderItem.getChildren(); - } - - // If the user has multiple workspace folders open, then show them as - // individual top level items. - return Promise.resolve(this.workspaceFolderTreeItems); + // If the user has a workspace open and there's only one folder in it, then + // don't show the workspace folder; just show its packages at the top level. + if (this.workspaceFolderTreeItems.length === 1) { + const folderItem = this.workspaceFolderTreeItems[0]; + return folderItem.getChildren(); } - // If the user doesn't have a folder open in the workspace, or none of them - // have Bazel workspaces, don't show anything. - return Promise.resolve([]); + // If the user has multiple or no workspace folders open, then show them as + // individual top level items. + return this.workspaceFolderTreeItems; } - public getTreeItem(element: IBazelTreeItem): vscode.TreeItem { + public async getTreeItem(element: IBazelTreeItem): Promise { const label = element.getLabel(); const collapsibleState = element.mightHaveChildren() ? vscode.TreeItemCollapsibleState.Collapsed @@ -106,51 +111,64 @@ export class BazelWorkspaceTreeProvider treeItem.contextValue = element.getContextValue(); treeItem.iconPath = element.getIcon(); treeItem.tooltip = element.getTooltip(); - treeItem.command = element.getCommand(); + treeItem.command = await element.getCommand(); return treeItem; } - /** Forces a re-query and refresh of the tree's contents. */ - public refresh() { - this.updateWorkspaceFolderTreeItems(); - this.onDidChangeTreeDataEmitter.fire(); - } - - /** - * Called to update the tree when a BUILD file is created, deleted, or - * changed. + /** Forces a re-query and refresh of the tree's contents. * - * @param uri The file system URI of the file that changed. + * Also for initialize or to update the tree when a BUILD file is created, + * deleted, or changed. */ - private onBuildFilesChanged() { - // TODO(allevato): Look into firing the event only for tree items that are - // affected by the change. - this.refresh(); + public async refresh( + workspaceFolders: readonly vscode.WorkspaceFolder[], + ): Promise { + await this.updateWorkspaceFolderTreeItems(workspaceFolders); + this.onDidChangeTreeDataEmitter.fire(); } - /** Refresh the cached BazelWorkspaceFolderTreeItems. */ - private updateWorkspaceFolderTreeItems() { - if (vscode.workspace.workspaceFolders) { - this.workspaceFolderTreeItems = vscode.workspace.workspaceFolders - .map((folder) => { - const workspaceInfo = BazelWorkspaceInfo.fromWorkspaceFolder(folder); - if (workspaceInfo) { - return new BazelWorkspaceFolderTreeItem( - this.resources, - workspaceInfo, - ); - } - return undefined; - }) - .filter((folder) => folder !== undefined); - } else { - this.workspaceFolderTreeItems = []; + private async createWorkspaceFolderTreeItem( + workspaceFolder: vscode.WorkspaceFolder, + ): Promise { + const workspaceInfo = await this.querier.queryWorkspace(workspaceFolder); + if (workspaceInfo === undefined) { + return undefined; } + return new BazelWorkspaceFolderTreeItem( + this.resources, + this.querier, + workspaceInfo, + ); + } + + private async createWorkspaceFolderTreeItems( + workspaceFolders: readonly vscode.WorkspaceFolder[], + ): Promise { + const maybeWorkspaceFolderTreeItems = await Promise.all( + workspaceFolders.map((workspaceFolder) => + this.createWorkspaceFolderTreeItem(workspaceFolder), + ), + ); + return maybeWorkspaceFolderTreeItems.filter( + (folder) => folder !== undefined, + ); + } + + /** + * Update the cached BazelWorkspaceFolderTreeItems and other UI components + * interested in. + */ + private async updateWorkspaceFolderTreeItems( + workspaceFolders?: readonly vscode.WorkspaceFolder[], + ): Promise { + this.workspaceFolderTreeItems = await this.createWorkspaceFolderTreeItems( + workspaceFolders ?? [], + ); - // All the UI to update based on having items. + // Updates other UI components based on the context value for Bazel + // workspace. const haveBazelWorkspace = this.workspaceFolderTreeItems.length !== 0; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - vscode.commands.executeCommand( + void vscode.commands.executeCommand( "setContext", "bazel.haveWorkspace", haveBazelWorkspace, 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, + }); + } +} diff --git a/test/workspace-tree.test.ts b/test/workspace-tree.test.ts new file mode 100644 index 00000000..40a3d451 --- /dev/null +++ b/test/workspace-tree.test.ts @@ -0,0 +1,206 @@ +import * as assert from "assert"; +import * as vscode from "vscode"; +import { BazelWorkspaceInfo } from "../src/bazel"; +import { blaze_query } from "../src/protos"; +import { + BazelWorkspaceTreeProvider, + IBazelQuerier, +} from "../src/workspace-tree"; + +class FakeBazelQuerier implements IBazelQuerier { + constructor( + private readonly packages: string[], + private readonly targets: Map, + ) {} + + queryWorkspace( + workspaceFolder: vscode.WorkspaceFolder, + ): Thenable { + // Assuming query from root for simplest test case. (single root) + return Promise.resolve( + new BazelWorkspaceInfo(workspaceFolder.uri.fsPath, workspaceFolder), + ); + } + + queryPackages(workspaceInfo: BazelWorkspaceInfo): Thenable { + void workspaceInfo; + return Promise.resolve(this.packages); + } + + queryChildrenTargets( + workspaceInfo: BazelWorkspaceInfo, + packagePath: string, + ): Thenable { + void workspaceInfo; + return Promise.resolve(this.targets.get(packagePath)); + } +} + +function fakeWorkspaceFolder(path: string): vscode.WorkspaceFolder { + const uri = vscode.Uri.file(path); + return { + uri, + name: path, + index: 0, + }; +} + +async function workspaceTreeProviderForTest( + querier: IBazelQuerier, + workspaceFolders: vscode.WorkspaceFolder[], +): Promise { + const provider = new BazelWorkspaceTreeProvider(querier); + await provider.refresh(workspaceFolders); + return provider; +} + +describe("The Bazel workspace tree provider", () => { + it("Returns nothing on empty workspace folders", async () => { + const querier = new FakeBazelQuerier([], new Map()); + const workspaceFolders: vscode.WorkspaceFolder[] = []; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + assert.deepStrictEqual(topChildren, []); + }); + + it("Flatten on single workspace folder", async () => { + const querier = new FakeBazelQuerier( + ["//a"], + new Map([ + ["", { target: [] }], + ["//a", { target: [] }], + ]), + ); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + assert.equal(topChildren[0].getLabel(), "//a"); + }); + + it("Not flatten on 2 workspace folders", async () => { + const querier = new FakeBazelQuerier([], new Map([["", { target: [] }]])); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path0"), + fakeWorkspaceFolder("fake/path1"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + assert.equal(topChildren[0].getLabel(), "fake/path0"); + assert.equal(topChildren[1].getLabel(), "fake/path1"); + }); + + it("Can handle root package", async () => { + const querier = new FakeBazelQuerier( + ["//", "//a"], + new Map([ + ["", { target: [] }], + ["//", { target: [] }], + ["//a", { target: [] }], + ]), + ); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + const root = topChildren[0]; + assert.equal(root.getLabel(), "//"); + const rootChildren = await root.getChildren(); + const a = rootChildren[0]; + assert.equal(a.getLabel(), "a"); + }); + + it("Skips non-package folders", async () => { + const querier = new FakeBazelQuerier( + ["//a", "//a/b/c"], + new Map([ + ["", { target: [] }], + ["//a", { target: [] }], + ["//a/b/c", { target: [] }], + ]), + ); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + const a = topChildren[0]; + assert.equal(a.getLabel(), "//a"); + const aChildren = await topChildren[0].getChildren(); + const bc = aChildren[0]; + assert.equal(bc.getLabel(), "b/c"); + }); + + it("Handles external dependencies (single workspace)", async () => { + const querier = new FakeBazelQuerier( + [ + "@repo//", + "@repo2//a", + "@repo2//a/b", + "@repo2//c", + "@repo2//c/d/e", + "@repo3//f", + ], + new Map([ + ["", { target: [] }], + ["@repo//", { target: [] }], + ["@repo2//a", { target: [] }], + ["@repo2//a/b", { target: [] }], + ["@repo2//c", { target: [] }], + ["@repo2//c/d/e", { target: [] }], + ["@repo3//f", { target: [] }], + ]), + ); + const workspaceFolders: vscode.WorkspaceFolder[] = [ + fakeWorkspaceFolder("fake/path"), + ]; + const provider = await workspaceTreeProviderForTest( + querier, + workspaceFolders, + ); + + const topChildren = await provider.getChildren(); + const repo = topChildren[0]; + assert.strictEqual(repo.getLabel(), "@repo//"); + assert.strictEqual((await repo.getChildren()).length, 0); + + const repo2a = topChildren[1]; + assert.strictEqual(repo2a.getLabel(), "@repo2//a"); + const repo2aChildren = await repo2a.getChildren(); + const repo2ab = repo2aChildren[0]; + assert.strictEqual(repo2ab.getLabel(), "b"); + + const repo2c = topChildren[2]; + assert.strictEqual(repo2c.getLabel(), "@repo2//c"); + const repo2cChildren = await repo2c.getChildren(); + const repo2cde = repo2cChildren[0]; + assert.strictEqual(repo2cde.getLabel(), "d/e"); + + const repo3f = topChildren[3]; + assert.strictEqual(repo3f.getLabel(), "@repo3//f"); + }); + + // TODO query target test cases. +});