diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 1c4becd2..b3897222 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -41,7 +41,8 @@ import { activateWrapperCommands } from "./bazel_wrapper_commands"; * @param context The extension context. */ export async function activate(context: vscode.ExtensionContext) { - const workspaceTreeProvider = new BazelWorkspaceTreeProvider(); + const workspaceTreeProvider = + BazelWorkspaceTreeProvider.fromExtensionContext(context); context.subscriptions.push(workspaceTreeProvider); const codeLensProvider = new BazelBuildCodeLensProvider(context); diff --git a/src/extension/resources.ts b/src/extension/resources.ts new file mode 100644 index 00000000..f8b8143d --- /dev/null +++ b/src/extension/resources.ts @@ -0,0 +1,58 @@ +// 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 path from "path"; +import * as vscode from "vscode"; + +export enum IconName { + ANDROID_BINARY = "android_binary", + APPLE_APPLICATION = "apple_application", + APPLE_EXECUTABLE_BUNDLE = "apple_executable_bundle", + APPLE_FRAMEWORK = "apple_framework", + BINARY = "binary", + CONFIG_SETTING = "config_setting", + FILEGROUP = "filegroup", + GENRULE = "genrule", + LIBRARY = "library", + PROTO = "proto", + RESOURCE_BUNDLE = "resource_bundle", + TEST = "test", + TEST_SUITE = "test_suite", +} + +/** + * Helper functions for getting the resource bundled inside this extension. + */ +export class Resources { + public static fromExtensionContext( + context: vscode.ExtensionContext, + ): Resources { + return new Resources(context.extensionPath); + } + + /** + * @param extensionPath The extension path, usually from the extension + * context. + */ + constructor(private readonly extensionPath: string) {} + + /** + * Returns the icon path in string. + * + * @param name The icon file name. + */ + public getIconPath(name: IconName): string { + return path.join(this.extensionPath, "icons", `${name}.svg`); + } +} diff --git a/src/workspace-tree/bazel_package_tree_item.ts b/src/workspace-tree/bazel_package_tree_item.ts index bb9aca52..2004343a 100644 --- a/src/workspace-tree/bazel_package_tree_item.ts +++ b/src/workspace-tree/bazel_package_tree_item.ts @@ -23,6 +23,7 @@ import { getDefaultBazelExecutablePath } from "../extension/configuration"; import { blaze_query } from "../protos"; import { BazelTargetTreeItem } from "./bazel_target_tree_item"; import { IBazelTreeItem } from "./bazel_tree_item"; +import { Resources } from "../extension/resources"; /** A tree item representing a build package. */ export class BazelPackageTreeItem @@ -44,6 +45,7 @@ export class BazelPackageTreeItem * {@code packagePath} should be stripped for the item's label. */ constructor( + private readonly resources: Resources, private readonly workspaceInfo: BazelWorkspaceInfo, private readonly packagePath: string, private readonly parentPackagePath: string, @@ -62,7 +64,11 @@ export class BazelPackageTreeItem sortByRuleName: true, }); const targets = queryResult.target.map((target: blaze_query.ITarget) => { - return new BazelTargetTreeItem(this.workspaceInfo, target); + return new BazelTargetTreeItem( + this.resources, + this.workspaceInfo, + target, + ); }); return (this.directSubpackages as IBazelTreeItem[]).concat(targets); } diff --git a/src/workspace-tree/bazel_target_tree_item.ts b/src/workspace-tree/bazel_target_tree_item.ts index ad5d0069..b79ae680 100644 --- a/src/workspace-tree/bazel_target_tree_item.ts +++ b/src/workspace-tree/bazel_target_tree_item.ts @@ -18,6 +18,7 @@ import { IBazelCommandAdapter, IBazelCommandOptions } from "../bazel"; import { blaze_query } from "../protos"; import { IBazelTreeItem } from "./bazel_tree_item"; import { getBazelRuleIcon } from "./icons"; +import { Resources } from "../extension/resources"; /** A tree item representing a build target. */ export class BazelTargetTreeItem @@ -31,6 +32,7 @@ export class BazelTargetTreeItem * query. */ constructor( + private readonly resources: Resources, private readonly workspaceInfo: BazelWorkspaceInfo, private readonly target: blaze_query.ITarget, ) {} @@ -50,8 +52,12 @@ export class BazelTargetTreeItem return `${targetName} (${this.target.rule.ruleClass})`; } - public getIcon(): vscode.ThemeIcon | string { - return getBazelRuleIcon(this.target); + public getIcon(): string | vscode.ThemeIcon { + const bazelRuleIcon = getBazelRuleIcon(this.target); + if (bazelRuleIcon) { + return this.resources.getIconPath(bazelRuleIcon); + } + return vscode.ThemeIcon.File; } public getTooltip(): string { diff --git a/src/workspace-tree/bazel_tree_item.ts b/src/workspace-tree/bazel_tree_item.ts index 436d5392..31ce7876 100644 --- a/src/workspace-tree/bazel_tree_item.ts +++ b/src/workspace-tree/bazel_tree_item.ts @@ -37,7 +37,7 @@ export interface IBazelTreeItem { getLabel(): string; /** Returns the icon that should be shown next to the tree item. */ - getIcon(): vscode.ThemeIcon | string | undefined; + getIcon(): string | vscode.ThemeIcon; /** * Returns the tooltip that should be displayed when the user hovers over the diff --git a/src/workspace-tree/bazel_workspace_folder_tree_item.ts b/src/workspace-tree/bazel_workspace_folder_tree_item.ts index 40d30ef9..d403c177 100644 --- a/src/workspace-tree/bazel_workspace_folder_tree_item.ts +++ b/src/workspace-tree/bazel_workspace_folder_tree_item.ts @@ -13,13 +13,13 @@ // limitations under the License. import * as vscode from "vscode"; -import { BazelWorkspaceInfo } from "../bazel"; -import { BazelQuery } from "../bazel"; +import { BazelWorkspaceInfo, 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 { Resources } from "../extension/resources"; /** A tree item representing a workspace folder. */ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { @@ -28,7 +28,10 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { * * @param workspaceFolder The workspace folder that the tree item represents. */ - constructor(private workspaceInfo: BazelWorkspaceInfo) {} + constructor( + private readonly resources: Resources, + private readonly workspaceInfo: BazelWorkspaceInfo, + ) {} public mightHaveChildren(): boolean { return true; @@ -123,6 +126,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.resources, this.workspaceInfo, packagePath, parentPackagePath, @@ -184,7 +188,11 @@ export class BazelWorkspaceFolderTreeItem implements IBazelTreeItem { sortByRuleName: true, }); const targets = queryResult.target.map((target: blaze_query.ITarget) => { - return new BazelTargetTreeItem(this.workspaceInfo, target); + return new BazelTargetTreeItem( + this.resources, + this.workspaceInfo, + target, + ); }); return Promise.resolve((topLevelItems as IBazelTreeItem[]).concat(targets)); diff --git a/src/workspace-tree/bazel_workspace_tree_provider.ts b/src/workspace-tree/bazel_workspace_tree_provider.ts index 5e4907e9..f050da47 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 { Resources } from "../extension/resources"; /** * Provides a tree of Bazel build packages and targets for the VS Code explorer @@ -34,12 +35,20 @@ export class BazelWorkspaceTreeProvider private disposables: vscode.Disposable[] = []; + public static fromExtensionContext( + context: vscode.ExtensionContext, + ): BazelWorkspaceTreeProvider { + return new BazelWorkspaceTreeProvider( + Resources.fromExtensionContext(context), + ); + } + /** * Initializes a new tree provider with the given extension context. * * @param context The VS Code extension context. */ - constructor() { + constructor(private readonly resources: Resources) { const buildFilesWatcher = vscode.workspace.createFileSystemWatcher( "**/{BUILD,BUILD.bazel}", false, @@ -126,7 +135,10 @@ export class BazelWorkspaceTreeProvider .map((folder) => { const workspaceInfo = BazelWorkspaceInfo.fromWorkspaceFolder(folder); if (workspaceInfo) { - return new BazelWorkspaceFolderTreeItem(workspaceInfo); + return new BazelWorkspaceFolderTreeItem( + this.resources, + workspaceInfo, + ); } return undefined; }) diff --git a/src/workspace-tree/icons.ts b/src/workspace-tree/icons.ts index c89fe40f..d81424e5 100644 --- a/src/workspace-tree/icons.ts +++ b/src/workspace-tree/icons.ts @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as path from "path"; -import * as vscode from "vscode"; +import { IconName } from "../extension/resources"; import { blaze_query } from "../protos"; /** @@ -25,31 +24,31 @@ import { blaze_query } from "../protos"; * application/extension/framework targets are shown with folder-like icons * because those bundles are conceptually folders. */ -const SPECIFIC_RULE_CLASS_ICONS: Record = { - android_binary: "android_binary", - apple_bundle_import: "resource_bundle", - apple_resource_bundle: "resource_bundle", - config_setting: "config_setting", - filegroup: "filegroup", - genrule: "genrule", - ios_application: "apple_application", - ios_extension: "apple_executable_bundle", - ios_framework: "apple_framework", - macos_application: "apple_application", - macos_bundle: "apple_executable_bundle", - macos_extension: "apple_executable_bundle", - objc_bundle: "resource_bundle", - objc_bundle_library: "resource_bundle", - objc_framework: "apple_framework", - objc_import: "library", - proto_library: "proto", - swift_c_module: "library", - swift_import: "library", - test_suite: "test_suite", - tvos_application: "apple_application", - tvos_extension: "apple_executable_bundle", - watchos_application: "apple_application", - watchos_extension: "apple_executable_bundle", +const SPECIFIC_RULE_CLASS_ICONS: Record = { + android_binary: IconName.ANDROID_BINARY, + apple_bundle_import: IconName.RESOURCE_BUNDLE, + apple_resource_bundle: IconName.RESOURCE_BUNDLE, + config_setting: IconName.CONFIG_SETTING, + filegroup: IconName.FILEGROUP, + genrule: IconName.GENRULE, + ios_application: IconName.APPLE_APPLICATION, + ios_extension: IconName.APPLE_EXECUTABLE_BUNDLE, + ios_framework: IconName.APPLE_FRAMEWORK, + macos_application: IconName.APPLE_APPLICATION, + macos_bundle: IconName.APPLE_EXECUTABLE_BUNDLE, + macos_extension: IconName.APPLE_EXECUTABLE_BUNDLE, + objc_bundle: IconName.RESOURCE_BUNDLE, + objc_bundle_library: IconName.RESOURCE_BUNDLE, + objc_framework: IconName.APPLE_FRAMEWORK, + objc_import: IconName.LIBRARY, + proto_library: IconName.PROTO, + swift_c_module: IconName.LIBRARY, + swift_import: IconName.LIBRARY, + test_suite: IconName.TEST_SUITE, + tvos_application: IconName.APPLE_APPLICATION, + tvos_extension: IconName.APPLE_EXECUTABLE_BUNDLE, + watchos_application: IconName.APPLE_APPLICATION, + watchos_extension: IconName.APPLE_EXECUTABLE_BUNDLE, }; /** @@ -60,23 +59,19 @@ const SPECIFIC_RULE_CLASS_ICONS: Record = { */ export function getBazelRuleIcon( target: blaze_query.ITarget, -): string | vscode.ThemeIcon { - const ruleClass = target.rule.ruleClass; - let iconName = SPECIFIC_RULE_CLASS_ICONS[ruleClass]; - if (!iconName) { - if (ruleClass.endsWith("_binary")) { - iconName = "binary"; - } else if (ruleClass.endsWith("_proto_library")) { - iconName = "proto"; - } else if (ruleClass.endsWith("_library")) { - iconName = "library"; - } else if (ruleClass.endsWith("_test")) { - iconName = "test"; - } - } +): IconName | undefined { + const ruleClass = target.rule?.ruleClass ?? ""; + const iconName = SPECIFIC_RULE_CLASS_ICONS[ruleClass]; if (iconName) { - return path.join(__dirname, "../../../icons", `${iconName}.svg`); - } else { - return vscode.ThemeIcon.File; + return iconName; + } else if (ruleClass.endsWith("_binary")) { + return IconName.BINARY; + } else if (ruleClass.endsWith("_proto_library")) { + return IconName.PROTO; + } else if (ruleClass.endsWith("_library")) { + return IconName.LIBRARY; + } else if (ruleClass.endsWith("_test")) { + return IconName.TEST; } + return undefined; } diff --git a/test/resources.test.ts b/test/resources.test.ts new file mode 100644 index 00000000..bc8f4844 --- /dev/null +++ b/test/resources.test.ts @@ -0,0 +1,20 @@ +import assert = require("assert"); +import { IconName, Resources } from "../src/extension/resources"; +import * as fs from "fs/promises"; +import * as vscode from "vscode"; + +describe("The resources", () => { + const extension = vscode.extensions.getExtension("BazelBuild.vscode-bazel"); + if (!extension) { + assert.fail("Extension not found"); + } + const resources = new Resources(extension.extensionPath); + + it("provides valid icon paths", async () => { + for (const name of Object.values(IconName)) { + await assert.doesNotReject(async () => { + await fs.stat(resources.getIconPath(name)); + }, "invalid icon path"); + } + }); +});