From aff429c832d4da4499b2e5bdcb85ef46b2df57b9 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 22 May 2025 11:46:25 -0400 Subject: [PATCH 1/2] add command to generate launch configurations --- package.json | 9 + src/WorkspaceContext.ts | 4 +- src/commands.ts | 4 + src/commands/generateLaunchConfigurations.ts | 70 +++++ src/debugger/debugAdapterFactory.ts | 16 +- src/debugger/launch.ts | 160 +++++------ test/unit-tests/debugger/launch.test.ts | 286 +++++++++++++++++++ 7 files changed, 458 insertions(+), 91 deletions(-) create mode 100644 src/commands/generateLaunchConfigurations.ts create mode 100644 test/unit-tests/debugger/launch.test.ts diff --git a/package.json b/package.json index bff0692ad..2cb1a5d59 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,11 @@ } ], "commands": [ + { + "command": "swift.generateLaunchConfigurations", + "title": "Generate Launch Configurations", + "category": "Swift" + }, { "command": "swift.previewDocumentation", "title": "Preview Documentation", @@ -886,6 +891,10 @@ } ], "commandPalette": [ + { + "command": "swift.generateLaunchConfigurations", + "when": "swift.hasPackage" + }, { "command": "swift.previewDocumentation", "when": "swift.supportsDocumentationLivePreview" diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 50e4a23c9..4c30d2c8f 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -101,7 +101,7 @@ export class WorkspaceContext implements vscode.Disposable { .then(async selected => { if (selected === "Update") { this.folders.forEach(ctx => - makeDebugConfigurations(ctx, undefined, true) + makeDebugConfigurations(ctx, { yes: true }) ); } }); @@ -120,7 +120,7 @@ export class WorkspaceContext implements vscode.Disposable { .then(selected => { if (selected === "Update") { this.folders.forEach(ctx => - makeDebugConfigurations(ctx, undefined, true) + makeDebugConfigurations(ctx, { yes: true }) ); } }); diff --git a/src/commands.ts b/src/commands.ts index cff624105..5ee367e6d 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -47,6 +47,7 @@ import { TestKind } from "./TestExplorer/TestKind"; import { pickProcess } from "./commands/pickProcess"; import { openDocumentation } from "./commands/openDocumentation"; import restartLSPServer from "./commands/restartLSPServer"; +import { generateLaunchConfigurations } from "./commands/generateLaunchConfigurations"; /** * References: @@ -105,6 +106,9 @@ export enum Commands { */ export function register(ctx: WorkspaceContext): vscode.Disposable[] { return [ + vscode.commands.registerCommand("swift.generateLaunchConfigurations", () => + generateLaunchConfigurations(ctx) + ), vscode.commands.registerCommand("swift.newFile", uri => newSwiftFile(uri)), vscode.commands.registerCommand(Commands.RESOLVE_DEPENDENCIES, () => resolveDependencies(ctx) diff --git a/src/commands/generateLaunchConfigurations.ts b/src/commands/generateLaunchConfigurations.ts new file mode 100644 index 000000000..d90e623db --- /dev/null +++ b/src/commands/generateLaunchConfigurations.ts @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { makeDebugConfigurations } from "../debugger/launch"; +import { FolderContext } from "../FolderContext"; +import { WorkspaceContext } from "../WorkspaceContext"; +import * as vscode from "vscode"; + +export async function generateLaunchConfigurations(ctx: WorkspaceContext): Promise { + if (ctx.folders.length === 0) { + return false; + } + + if (ctx.folders.length === 1) { + return await makeDebugConfigurations(ctx.folders[0], { force: true, yes: true }); + } + + const quickPickItems: SelectFolderQuickPick[] = ctx.folders.map(folder => ({ + type: "folder", + folder, + label: folder.name, + detail: folder.workspaceFolder.uri.fsPath, + })); + quickPickItems.push({ type: "all", label: "Generate For All Folders" }); + const selection = await vscode.window.showQuickPick(quickPickItems, { + matchOnDetail: true, + placeHolder: "Select a folder to generate launch configurations for", + }); + + if (!selection) { + return false; + } + + const foldersToUpdate: FolderContext[] = []; + if (selection.type === "all") { + foldersToUpdate.push(...ctx.folders); + } else { + foldersToUpdate.push(selection.folder); + } + + return ( + await Promise.all( + foldersToUpdate.map(folder => + makeDebugConfigurations(folder, { force: true, yes: true }) + ) + ) + ).reduceRight((prev, curr) => prev || curr); +} + +type SelectFolderQuickPick = AllQuickPickItem | FolderQuickPickItem; + +interface AllQuickPickItem extends vscode.QuickPickItem { + type: "all"; +} + +interface FolderQuickPickItem extends vscode.QuickPickItem { + type: "folder"; + folder: FolderContext; +} diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index 49adb2fe0..84a68daa3 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -20,8 +20,8 @@ import { registerLoggingDebugAdapterTracker } from "./logTracker"; import { SwiftToolchain } from "../toolchain/toolchain"; import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; import { fileExists } from "../utilities/filesystem"; -import { getLLDBLibPath } from "./lldb"; -import { getErrorDescription } from "../utilities/utilities"; +import { CI_DISABLE_ASLR, getLLDBLibPath } from "./lldb"; +import { getErrorDescription, swiftRuntimeEnv } from "../utilities/utilities"; import configuration from "../configuration"; /** @@ -137,6 +137,13 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration launchConfig.pid = pid; } + // Merge in the Swift runtime environment variables + const runtimeEnv = swiftRuntimeEnv(true); + if (runtimeEnv) { + const existingEnv = launchConfig.env ?? {}; + launchConfig.env = { ...runtimeEnv, existingEnv }; + } + // Delegate to the appropriate debug adapter extension launchConfig.type = DebugAdapter.getLaunchConfigType(toolchain.swiftVersion); if (launchConfig.type === LaunchConfigType.CODE_LLDB) { @@ -164,7 +171,10 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration launchConfig.debugAdapterExecutable = lldbDapPath; } - return launchConfig; + return { + ...launchConfig, + ...CI_DISABLE_ASLR, + }; } private async promptToInstallCodeLLDB(): Promise { diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index cf4cf3b80..e2121093a 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -14,94 +14,105 @@ import * as path from "path"; import * as vscode from "vscode"; +import { isDeepStrictEqual } from "util"; import { FolderContext } from "../FolderContext"; import { BuildFlags } from "../toolchain/BuildFlags"; -import { stringArrayInEnglish, swiftLibraryPathKey, swiftRuntimeEnv } from "../utilities/utilities"; +import { stringArrayInEnglish } from "../utilities/utilities"; import { SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; import { getFolderAndNameSuffix } from "./buildConfig"; import configuration from "../configuration"; -import { CI_DISABLE_ASLR } from "./lldb"; + +/** Options used to configure {@link makeDebugConfigurations}. */ +export interface WriteLaunchConfigurationsOptions { + /** Force the generation of launch configurations regardless of user settings. */ + force?: boolean; + + /** Automatically answer yes to update dialogs. */ + yes?: boolean; +} /** * Edit launch.json based on contents of Swift Package. * Adds launch configurations based on the executables in Package.swift. * * @param ctx folder context to create launch configurations for - * @param yes automatically answer yes to dialogs + * @param options the options used to configure behavior of this function + * @returns a boolean indicating whether or not launch configurations were actually updated */ export async function makeDebugConfigurations( ctx: FolderContext, - message?: string, - yes = false + options: WriteLaunchConfigurationsOptions = {} ): Promise { - if (!configuration.folder(ctx.workspaceFolder).autoGenerateLaunchConfigurations) { + if ( + !options.force && + !configuration.folder(ctx.workspaceFolder).autoGenerateLaunchConfigurations + ) { return false; } + const wsLaunchSection = vscode.workspace.getConfiguration("launch", ctx.folder); const launchConfigs = wsLaunchSection.get("configurations") || []; - // list of keys that can be updated in config merge - const keysToUpdate = [ - "program", - "cwd", - "preLaunchTask", - "type", - "disableASLR", - "initCommands", - `env.${swiftLibraryPathKey()}`, - ]; - const configUpdates: { index: number; config: vscode.DebugConfiguration }[] = []; - - const configs = await createExecutableConfigurations(ctx); - let edited = false; - for (const config of configs) { - const index = launchConfigs.findIndex(c => c.name === config.name); - if (index !== -1) { - // deep clone config and update with keys from calculated config - const newConfig: vscode.DebugConfiguration = JSON.parse( - JSON.stringify(launchConfigs[index]) - ); - updateConfigWithNewKeys(newConfig, config, keysToUpdate); - // if original config is different from new config - if (JSON.stringify(launchConfigs[index]) !== JSON.stringify(newConfig)) { - configUpdates.push({ index: index, config: newConfig }); - } - } else { - launchConfigs.push(config); - edited = true; + // Determine which launch configurations need updating/creating + const configsToCreate: vscode.DebugConfiguration[] = []; + const configsToUpdate: { index: number; config: vscode.DebugConfiguration }[] = []; + for (const generatedConfig of await createExecutableConfigurations(ctx)) { + const index = launchConfigs.findIndex(c => c.name === generatedConfig.name); + if (index === -1) { + configsToCreate.push(generatedConfig); + continue; + } + + // deep clone the existing config and update with keys from generated config + const config = structuredClone(launchConfigs[index]); + updateConfigWithNewKeys(config, generatedConfig, [ + "program", + "cwd", + "preLaunchTask", + "type", + ]); + + // Check to see if the config has changed + if (!isDeepStrictEqual(launchConfigs[index], config)) { + configsToUpdate.push({ index, config }); } } - if (configUpdates.length > 0) { - if (!yes) { + // Create/Update launch configurations if necessary + let needsUpdate = false; + if (configsToCreate.length > 0) { + launchConfigs.push(...configsToCreate); + needsUpdate = true; + } + if (configsToUpdate.length > 0) { + let answer: "Update" | "Cancel" | undefined = options.yes ? "Update" : undefined; + if (!answer) { const configUpdateNames = stringArrayInEnglish( - configUpdates.map(update => update.config.name) + configsToUpdate.map(update => update.config.name) ); - const warningMessage = - message ?? - `The Swift extension would like to update launch configurations '${configUpdateNames}'.`; - const answer = await vscode.window.showWarningMessage( + const warningMessage = `The Swift extension would like to update launch configurations '${configUpdateNames}'.`; + answer = await vscode.window.showWarningMessage( `${ctx.name}: ${warningMessage} Do you want to update?`, "Update", "Cancel" ); - if (answer === "Update") { - yes = true; - } } - if (yes) { - configUpdates.forEach(update => (launchConfigs[update.index] = update.config)); - edited = true; + + if (answer === "Update") { + configsToUpdate.forEach(update => (launchConfigs[update.index] = update.config)); + needsUpdate = true; } } - if (edited) { - await wsLaunchSection.update( - "configurations", - launchConfigs, - vscode.ConfigurationTarget.WorkspaceFolder - ); + if (!needsUpdate) { + return false; } + + await wsLaunchSection.update( + "configurations", + launchConfigs, + vscode.ConfigurationTarget.WorkspaceFolder + ); return true; } @@ -142,8 +153,6 @@ async function createExecutableConfigurations( request: "launch", args: [], cwd: folder, - env: swiftRuntimeEnv(true), - ...CI_DISABLE_ASLR, }; return [ { @@ -182,9 +191,7 @@ export function createSnippetConfiguration( program: path.posix.join(buildDirectory, "debug", snippetName), args: [], cwd: folder, - env: swiftRuntimeEnv(true), runType: "snippet", - ...CI_DISABLE_ASLR, }; } @@ -218,36 +225,17 @@ export async function debugLaunchConfig( }); } -/** Return the base configuration with (nested) keys updated with the new one. */ +/** Update the provided debug configuration with keys from a newly generated configuration. */ function updateConfigWithNewKeys( - baseConfiguration: vscode.DebugConfiguration, - newConfiguration: vscode.DebugConfiguration, + oldConfig: vscode.DebugConfiguration, + newConfig: vscode.DebugConfiguration, keys: string[] ) { - keys.forEach(key => { - // We're manually handling `undefined`s during nested update, so even if the depth - // is restricted to 2, the implementation still looks a bit messy. - if (key.includes(".")) { - const [mainKey, subKey] = key.split(".", 2); - if (baseConfiguration[mainKey] === undefined) { - // { mainKey: unknown | undefined } -> { mainKey: undefined } - baseConfiguration[mainKey] = newConfiguration[mainKey]; - } else if (newConfiguration[mainKey] === undefined) { - const subKeys = Object.keys(baseConfiguration[mainKey]); - if (subKeys.length === 1 && subKeys[0] === subKey) { - // { mainKey: undefined } -> { mainKey: { subKey: unknown } } - baseConfiguration[mainKey] = undefined; - } else { - // { mainKey: undefined } -> { mainKey: { subKey: unknown | undefined, ... } } - baseConfiguration[mainKey][subKey] = undefined; - } - } else { - // { mainKey: { subKey: unknown | undefined } } -> { mainKey: { subKey: unknown | undefined, ... } } - baseConfiguration[mainKey][subKey] = newConfiguration[mainKey][subKey]; - } - } else { - // { key: unknown | undefined } -> { key: unknown | undefined, ... } - baseConfiguration[key] = newConfiguration[key]; + for (const key of keys) { + if (newConfig[key] === undefined) { + delete oldConfig[key]; + continue; } - }); + oldConfig[key] = newConfig[key]; + } } diff --git a/test/unit-tests/debugger/launch.test.ts b/test/unit-tests/debugger/launch.test.ts new file mode 100644 index 000000000..693b4d5e8 --- /dev/null +++ b/test/unit-tests/debugger/launch.test.ts @@ -0,0 +1,286 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import { expect } from "chai"; +import configuration, { FolderConfiguration } from "../../../src/configuration"; +import { makeDebugConfigurations } from "../../../src/debugger/launch"; +import { FolderContext } from "../../../src/FolderContext"; +import { + instance, + MockedObject, + mockFn, + mockGlobalModule, + mockGlobalObject, + mockObject, +} from "../../MockUtils"; +import { Product, SwiftPackage } from "../../../src/SwiftPackage"; +import { SWIFT_LAUNCH_CONFIG_TYPE } from "../../../src/debugger/debugAdapter"; + +suite("Launch Configurations Test", () => { + const mockConfiguration = mockGlobalModule(configuration); + let mockFolderConfiguration: MockedObject; + const mockWorkspace = mockGlobalObject(vscode, "workspace"); + let mockLaunchWSConfig: MockedObject; + + // Create a mock folder to be used by each test + const folderURI = vscode.Uri.file("/path/to/folder"); + const swiftPackage = mockObject({ + executableProducts: Promise.resolve([ + { name: "executable", targets: [], type: { executable: null } }, + ]), + }); + const folder = mockObject({ + folder: folderURI, + workspaceFolder: { + index: 0, + name: "folder", + uri: folderURI, + }, + relativePath: "", + swiftPackage: instance(swiftPackage), + }); + + setup(() => { + mockFolderConfiguration = mockObject({ + autoGenerateLaunchConfigurations: true, + }); + mockConfiguration.folder.returns(mockFolderConfiguration); + mockLaunchWSConfig = mockObject({ + get: mockFn(), + update: mockFn(), + }); + mockWorkspace.getConfiguration.withArgs("launch").returns(instance(mockLaunchWSConfig)); + mockLaunchWSConfig.get.withArgs("configurations").returns([]); + }); + + test("generates launch configurations for executable products", async () => { + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.true; + expect(mockLaunchWSConfig.update).to.have.been.calledWith( + "configurations", + [ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + program: "${workspaceFolder:folder}/.build/debug/executable", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + program: "${workspaceFolder:folder}/.build/release/executable", + preLaunchTask: "swift: Build Release executable", + }, + ], + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + + test("doesn't generate launch configurations if disabled in settings", async () => { + mockFolderConfiguration.autoGenerateLaunchConfigurations = false; + + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.false; + expect(mockLaunchWSConfig.update).to.not.have.been.called; + }); + + test("forces the generation of launch configurations if force is set to true", async () => { + mockFolderConfiguration.autoGenerateLaunchConfigurations = false; + + expect(await makeDebugConfigurations(instance(folder), { force: true, yes: true })).to.be + .true; + expect(mockLaunchWSConfig.update).to.have.been.calledWith( + "configurations", + [ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + program: "${workspaceFolder:folder}/.build/debug/executable", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + program: "${workspaceFolder:folder}/.build/release/executable", + preLaunchTask: "swift: Build Release executable", + }, + ], + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + + test("updates launch configurations that have old lldb/swift-lldb types", async () => { + mockLaunchWSConfig.get.withArgs("configurations").returns([ + { + type: "swift-lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + program: "${workspaceFolder:folder}/.build/debug/executable", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: "lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + program: "${workspaceFolder:folder}/.build/release/executable", + preLaunchTask: "swift: Build Release executable", + }, + ]); + + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.true; + expect(mockLaunchWSConfig.update).to.have.been.calledWith( + "configurations", + [ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + program: "${workspaceFolder:folder}/.build/debug/executable", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + program: "${workspaceFolder:folder}/.build/release/executable", + preLaunchTask: "swift: Build Release executable", + }, + ], + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + + test("doesn't update launch configurations if disabled in settings", async () => { + mockFolderConfiguration.autoGenerateLaunchConfigurations = false; + mockLaunchWSConfig.get.withArgs("configurations").returns([ + { + type: "swift-lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + program: "${workspaceFolder:folder}/.build/debug/executable", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: "lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + program: "${workspaceFolder:folder}/.build/release/executable", + preLaunchTask: "swift: Build Release executable", + }, + ]); + + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.false; + expect(mockLaunchWSConfig.update).to.not.have.been.called; + }); + + test("forces the updating of launch configurations if force is set to true", async () => { + mockFolderConfiguration.autoGenerateLaunchConfigurations = false; + mockLaunchWSConfig.get.withArgs("configurations").returns([ + { + type: "swift-lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + program: "${workspaceFolder:folder}/.build/debug/executable", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: "lldb", + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + program: "${workspaceFolder:folder}/.build/release/executable", + preLaunchTask: "swift: Build Release executable", + }, + ]); + + expect(await makeDebugConfigurations(instance(folder), { force: true, yes: true })).to.be + .true; + expect(mockLaunchWSConfig.update).to.have.been.calledWith( + "configurations", + [ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + program: "${workspaceFolder:folder}/.build/debug/executable", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + program: "${workspaceFolder:folder}/.build/release/executable", + preLaunchTask: "swift: Build Release executable", + }, + ], + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + + test("doesn't update launch configurations if they already exist", async () => { + mockLaunchWSConfig.get.withArgs("configurations").returns([ + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Debug executable", + program: "${workspaceFolder:folder}/.build/debug/executable", + preLaunchTask: "swift: Build Debug executable", + }, + { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: "${workspaceFolder:folder}", + name: "Release executable", + program: "${workspaceFolder:folder}/.build/release/executable", + preLaunchTask: "swift: Build Release executable", + }, + ]); + + expect(await makeDebugConfigurations(instance(folder), { yes: true })).to.be.false; + expect(mockLaunchWSConfig.update).to.not.have.been.called; + }); +}); From 4a9323fca3dea622638b1540c8b383d44e52d969 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 22 May 2025 13:10:47 -0400 Subject: [PATCH 2/2] create a new context key for "hasExecutableProduct" --- package.json | 2 +- src/WorkspaceContext.ts | 5 ++++- src/contextKeys.ts | 15 +++++++++++++++ src/ui/ProjectPanelProvider.ts | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2cb1a5d59..f5a4ca823 100644 --- a/package.json +++ b/package.json @@ -893,7 +893,7 @@ "commandPalette": [ { "command": "swift.generateLaunchConfigurations", - "when": "swift.hasPackage" + "when": "swift.hasExecutableProduct" }, { "command": "swift.previewDocumentation", diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 4c30d2c8f..ab7ad107b 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -228,15 +228,18 @@ export class WorkspaceContext implements vscode.Disposable { updateContextKeys(folderContext: FolderContext | null) { if (!folderContext) { contextKeys.hasPackage = false; + contextKeys.hasExecutableProduct = false; contextKeys.packageHasDependencies = false; return; } Promise.all([ folderContext.swiftPackage.foundPackage, + folderContext.swiftPackage.executableProducts, folderContext.swiftPackage.dependencies, - ]).then(([foundPackage, dependencies]) => { + ]).then(([foundPackage, executableProducts, dependencies]) => { contextKeys.hasPackage = foundPackage; + contextKeys.hasExecutableProduct = executableProducts.length > 0; contextKeys.packageHasDependencies = dependencies.length > 0; }); } diff --git a/src/contextKeys.ts b/src/contextKeys.ts index a520ab582..6e8ee7199 100644 --- a/src/contextKeys.ts +++ b/src/contextKeys.ts @@ -34,6 +34,11 @@ interface ContextKeys { */ hasPackage: boolean; + /** + * Whether the workspace folder contains a Swift package with at least one executable product. + */ + hasExecutableProduct: boolean; + /** * Whether the Swift package has any dependencies to display in the Package Dependencies view. */ @@ -94,6 +99,7 @@ interface ContextKeys { function createContextKeys(): ContextKeys { let isActivated: boolean = false; let hasPackage: boolean = false; + let hasExecutableProduct: boolean = false; let flatDependenciesList: boolean = false; let packageHasDependencies: boolean = false; let packageHasPlugins: boolean = false; @@ -134,6 +140,15 @@ function createContextKeys(): ContextKeys { vscode.commands.executeCommand("setContext", "swift.hasPackage", value); }, + get hasExecutableProduct() { + return hasExecutableProduct; + }, + + set hasExecutableProduct(value: boolean) { + hasExecutableProduct = value; + vscode.commands.executeCommand("setContext", "swift.hasExecutableTarget", value); + }, + get packageHasDependencies() { return packageHasDependencies; }, diff --git a/src/ui/ProjectPanelProvider.ts b/src/ui/ProjectPanelProvider.ts index ff33e1649..aa5f9d49d 100644 --- a/src/ui/ProjectPanelProvider.ts +++ b/src/ui/ProjectPanelProvider.ts @@ -371,6 +371,7 @@ export class ProjectPanelProvider implements vscode.TreeDataProvider { constructor(private workspaceContext: WorkspaceContext) { // default context key to false. These will be updated as folders are given focus contextKeys.hasPackage = false; + contextKeys.hasExecutableProduct = false; contextKeys.packageHasDependencies = false; this.observeTasks(workspaceContext);