From a3c3dd3e1ce8db5699aaa6831387cbecc01079ad Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Wed, 29 Jan 2025 13:41:10 -0500 Subject: [PATCH 1/9] Project Panel Convert the dependencies panel in to the new project panel. This rolls the dependencies up in to its own top level item in the tree, and places it along side Targets, Tasks, Commands and Snippets. Tasks, Commands and Snippets can be run directly from the panel, and update their icon to show their running status. Clicking a test target will run all the tests in the target. --- assets/test/targets/Package.swift | 44 ++ .../targets/Plugins/PluginTarget/main.swift | 9 + .../targets/Snippets/AnotherSnippet.swift | 1 + assets/test/targets/Snippets/Snippet.swift | 1 + .../CommandPluginTarget.swift | 0 .../Sources/ExecutableTarget/main.swift | 1 + .../Sources/LibraryTarget/Targets.swift | 6 + .../Tests/TargetsTests/TargetsTests.swift | 4 + package.json | 29 +- src/SwiftPackage.ts | 2 +- src/SwiftSnippets.ts | 47 +- src/TestExplorer/TestRunner.ts | 9 +- src/WorkspaceContext.ts | 51 ++ src/commands.ts | 24 +- src/commands/build.ts | 33 +- src/commands/openInExternalEditor.ts | 2 +- src/commands/openInWorkspace.ts | 2 +- .../{runParallelTests.ts => runAllTests.ts} | 12 +- src/commands/runTask.ts | 43 ++ src/extension.ts | 16 +- src/ui/PackageDependencyProvider.ts | 269 -------- src/ui/ProjectPanelProvider.ts | 594 ++++++++++++++++++ .../commands/dependency.test.ts | 16 +- .../ui/PackageDependencyProvider.test.ts | 166 ----- .../ui/ProjectPanelProvider.test.ts | 267 ++++++++ .../ui/PackageDependencyProvider.test.ts | 2 +- 26 files changed, 1143 insertions(+), 507 deletions(-) create mode 100644 assets/test/targets/Package.swift create mode 100644 assets/test/targets/Plugins/PluginTarget/main.swift create mode 100644 assets/test/targets/Snippets/AnotherSnippet.swift create mode 100644 assets/test/targets/Snippets/Snippet.swift create mode 100644 assets/test/targets/Sources/CommandPluginTarget/CommandPluginTarget.swift create mode 100644 assets/test/targets/Sources/ExecutableTarget/main.swift create mode 100644 assets/test/targets/Sources/LibraryTarget/Targets.swift create mode 100644 assets/test/targets/Tests/TargetsTests/TargetsTests.swift rename src/commands/{runParallelTests.ts => runAllTests.ts} (80%) create mode 100644 src/commands/runTask.ts delete mode 100644 src/ui/PackageDependencyProvider.ts create mode 100644 src/ui/ProjectPanelProvider.ts delete mode 100644 test/integration-tests/ui/PackageDependencyProvider.test.ts create mode 100644 test/integration-tests/ui/ProjectPanelProvider.test.ts diff --git a/assets/test/targets/Package.swift b/assets/test/targets/Package.swift new file mode 100644 index 000000000..7b79bf9c8 --- /dev/null +++ b/assets/test/targets/Package.swift @@ -0,0 +1,44 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "targets", + products: [ + .library( + name: "LibraryTarget", + targets: ["LibraryTarget"] + ), + .executable( + name: "ExecutableTarget", + targets: ["ExecutableTarget"] + ), + .plugin( + name: "PluginTarget", + targets: ["PluginTarget"] + ), + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-markdown.git", branch: "main"), + .package(path: "../defaultPackage"), + ], + targets: [ + .target( + name: "LibraryTarget" + ), + .executableTarget( + name: "ExecutableTarget" + ), + .plugin( + name: "PluginTarget", + capability: .command( + intent: .custom(verb: "testing", description: "A plugin for testing plugins") + ) + ), + .testTarget( + name: "TargetsTests", + dependencies: ["LibraryTarget"] + ), + ] +) diff --git a/assets/test/targets/Plugins/PluginTarget/main.swift b/assets/test/targets/Plugins/PluginTarget/main.swift new file mode 100644 index 000000000..8a2a8680f --- /dev/null +++ b/assets/test/targets/Plugins/PluginTarget/main.swift @@ -0,0 +1,9 @@ +import PackagePlugin +import Foundation + +@main +struct MyCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) throws { + print("Plugin Target Hello World") + } +} \ No newline at end of file diff --git a/assets/test/targets/Snippets/AnotherSnippet.swift b/assets/test/targets/Snippets/AnotherSnippet.swift new file mode 100644 index 000000000..25f53dfa6 --- /dev/null +++ b/assets/test/targets/Snippets/AnotherSnippet.swift @@ -0,0 +1 @@ +print("Another Snippet Hello World") \ No newline at end of file diff --git a/assets/test/targets/Snippets/Snippet.swift b/assets/test/targets/Snippets/Snippet.swift new file mode 100644 index 000000000..cdd7d267c --- /dev/null +++ b/assets/test/targets/Snippets/Snippet.swift @@ -0,0 +1 @@ +print("Snippet Hello World") \ No newline at end of file diff --git a/assets/test/targets/Sources/CommandPluginTarget/CommandPluginTarget.swift b/assets/test/targets/Sources/CommandPluginTarget/CommandPluginTarget.swift new file mode 100644 index 000000000..e69de29bb diff --git a/assets/test/targets/Sources/ExecutableTarget/main.swift b/assets/test/targets/Sources/ExecutableTarget/main.swift new file mode 100644 index 000000000..2fcea7ab3 --- /dev/null +++ b/assets/test/targets/Sources/ExecutableTarget/main.swift @@ -0,0 +1 @@ +print("Executable Target Hello World!") \ No newline at end of file diff --git a/assets/test/targets/Sources/LibraryTarget/Targets.swift b/assets/test/targets/Sources/LibraryTarget/Targets.swift new file mode 100644 index 000000000..1cddee56f --- /dev/null +++ b/assets/test/targets/Sources/LibraryTarget/Targets.swift @@ -0,0 +1,6 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +func foo() { + print("foo") +} \ No newline at end of file diff --git a/assets/test/targets/Tests/TargetsTests/TargetsTests.swift b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift new file mode 100644 index 000000000..4a2d903e8 --- /dev/null +++ b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift @@ -0,0 +1,4 @@ +import Testing + +@Test func example() async throws { +} diff --git a/package.json b/package.json index af7d020b5..12143c7f0 100644 --- a/package.json +++ b/package.json @@ -268,6 +268,11 @@ "command": "swift.runAllTestsParallel", "title": "Run All Tests in Parallel", "category": "Test" + }, + { + "command": "swift.runAllTests", + "title": "Run All Tests", + "category": "Test" } ], "configuration": [ @@ -971,50 +976,50 @@ "view/title": [ { "command": "swift.updateDependencies", - "when": "view == packageDependencies", + "when": "view == projectPanel", "group": "navigation@1" }, { "command": "swift.resolveDependencies", - "when": "view == packageDependencies", + "when": "view == projectPanel", "group": "navigation@2" }, { "command": "swift.resetPackage", - "when": "view == packageDependencies", + "when": "view == projectPanel", "group": "navigation@3" }, { "command": "swift.flatDependenciesList", - "when": "view == packageDependencies && !swift.flatDependenciesList", + "when": "view == projectPanel && !swift.flatDependenciesList", "group": "navigation@4" }, { "command": "swift.nestedDependenciesList", - "when": "view == packageDependencies && swift.flatDependenciesList", + "when": "view == projectPanel && swift.flatDependenciesList", "group": "navigation@5" } ], "view/item/context": [ { "command": "swift.useLocalDependency", - "when": "view == packageDependencies && viewItem == remote" + "when": "view == projectPanel && viewItem == remote" }, { "command": "swift.uneditDependency", - "when": "view == packageDependencies && viewItem == editing" + "when": "view == projectPanel && viewItem == editing" }, { "command": "swift.openInWorkspace", - "when": "view == packageDependencies && viewItem == editing" + "when": "view == projectPanel && viewItem == editing" }, { "command": "swift.openInWorkspace", - "when": "view == packageDependencies && viewItem == local" + "when": "view == projectPanel && viewItem == local" }, { "command": "swift.openExternal", - "when": "view == packageDependencies && viewItem != local" + "when": "view == projectPanel && (viewItem == 'editing' || viewItem == 'remote')" } ] }, @@ -1211,8 +1216,8 @@ "views": { "explorer": [ { - "id": "packageDependencies", - "name": "Package Dependencies", + "id": "projectPanel", + "name": "Swift Project", "icon": "$(archive)", "when": "swift.hasPackage" } diff --git a/src/SwiftPackage.ts b/src/SwiftPackage.ts index ed8b23c53..8df08f560 100644 --- a/src/SwiftPackage.ts +++ b/src/SwiftPackage.ts @@ -41,7 +41,7 @@ export interface Target { c99name: string; path: string; sources: string[]; - type: "executable" | "test" | "library" | "snippet"; + type: "executable" | "test" | "library" | "snippet" | "plugin"; } /** Swift Package Manager dependency */ diff --git a/src/SwiftSnippets.ts b/src/SwiftSnippets.ts index f7fa4cccb..0c0cb6f88 100644 --- a/src/SwiftSnippets.ts +++ b/src/SwiftSnippets.ts @@ -48,29 +48,44 @@ export function setSnippetContextKey(ctx: WorkspaceContext) { * If current file is a Swift Snippet run it * @param ctx Workspace Context */ -export async function runSnippet(ctx: WorkspaceContext): Promise { - return await debugSnippetWithOptions(ctx, { noDebug: true }); +export async function runSnippet( + ctx: WorkspaceContext, + snippet?: string +): Promise { + return await debugSnippetWithOptions(ctx, { noDebug: true }, snippet); } /** * If current file is a Swift Snippet run it in the debugger * @param ctx Workspace Context */ -export async function debugSnippet(ctx: WorkspaceContext): Promise { - return await debugSnippetWithOptions(ctx, {}); +export async function debugSnippet( + ctx: WorkspaceContext, + snippet?: string +): Promise { + return await debugSnippetWithOptions(ctx, {}, snippet); } export async function debugSnippetWithOptions( ctx: WorkspaceContext, - options: vscode.DebugSessionOptions + options: vscode.DebugSessionOptions, + snippet?: string ): Promise { + // create build task + let snippetName: string; + if (snippet) { + snippetName = snippet; + } else if (ctx.currentDocument) { + snippetName = path.basename(ctx.currentDocument.fsPath, ".swift"); + } else { + return false; + } + const folderContext = ctx.currentFolder; - if (!ctx.currentDocument || !folderContext) { - return; + if (!folderContext) { + return false; } - // create build task - const snippetName = path.basename(ctx.currentDocument.fsPath, ".swift"); const snippetBuildTask = createSwiftTask( ["build", "--product", snippetName], `Build ${snippetName}`, @@ -84,26 +99,28 @@ export async function debugSnippetWithOptions( }, ctx.toolchain ); - + const snippetDebugConfig = createSnippetConfiguration(snippetName, folderContext); try { + ctx.buildStarted(snippetName, snippetDebugConfig, options); + // queue build task and when it is complete run executable in the debugger return await folderContext.taskQueue .queueOperation(new TaskOperation(snippetBuildTask)) .then(result => { if (result === 0) { - const snippetDebugConfig = createSnippetConfiguration( - snippetName, - folderContext - ); return debugLaunchConfig( folderContext.workspaceFolder, snippetDebugConfig, options ); } + }) + .then(result => { + ctx.buildFinished(snippetName, snippetDebugConfig, options); + return result; }); } catch { // ignore error if task failed to run - return; + return false; } } diff --git a/src/TestExplorer/TestRunner.ts b/src/TestExplorer/TestRunner.ts index d9fae8a14..d78f4462c 100644 --- a/src/TestExplorer/TestRunner.ts +++ b/src/TestExplorer/TestRunner.ts @@ -527,6 +527,8 @@ export class TestRunner { * @returns When complete */ async runHandler() { + this.workspaceContext.testsStarted(this.folderContext, this.testKind); + const runState = new TestRunnerTestRunState(this.testRun); const cancellationDisposable = this.testRun.token.onCancellationRequested(() => { @@ -551,6 +553,8 @@ export class TestRunner { cancellationDisposable.dispose(); await this.testRun.end(); + + this.workspaceContext.testsFinished(this.folderContext, this.testKind); } /** Run test session without attaching to a debugger */ @@ -978,11 +982,6 @@ export class TestRunner { ); } - // show test results pane - vscode.commands.executeCommand( - "testing.showMostRecentOutput" - ); - const terminateSession = vscode.debug.onDidTerminateDebugSession(() => { this.workspaceContext.outputChannel.logDiagnostic( diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index b3a635815..eed2d6683 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -33,6 +33,7 @@ import { SwiftToolchain } from "./toolchain/toolchain"; import { DiagnosticsManager } from "./DiagnosticsManager"; import { DocumentationManager } from "./documentation/DocumentationManager"; import { DocCDocumentationRequest, ReIndexProjectRequest } from "./sourcekit-lsp/extensions"; +import { TestKind } from "./TestExplorer/TestKind"; /** * Context for whole workspace. Holds array of contexts for each workspace folder @@ -53,6 +54,17 @@ export class WorkspaceContext implements vscode.Disposable { private lastFocusUri: vscode.Uri | undefined; private initialisationFinished = false; + private readonly testStartEmitter = new vscode.EventEmitter(); + private readonly testFinishEmitter = new vscode.EventEmitter(); + + public onDidStartTests = this.testStartEmitter.event; + public onDidFinishTests = this.testFinishEmitter.event; + + private readonly buildStartEmitter = new vscode.EventEmitter(); + private readonly buildFinishEmitter = new vscode.EventEmitter(); + public onDidStartBuild = this.buildStartEmitter.event; + public onDidFinishBuild = this.buildFinishEmitter.event; + private constructor( extensionContext: vscode.ExtensionContext, public tempFolder: TemporaryFolder, @@ -336,6 +348,30 @@ export class WorkspaceContext implements vscode.Disposable { await this.fireEvent(folderContext, FolderOperation.focus); } + public testsFinished(folder: FolderContext, kind: TestKind) { + this.testFinishEmitter.fire({ kind, folder }); + } + + public testsStarted(folder: FolderContext, kind: TestKind) { + this.testStartEmitter.fire({ kind, folder }); + } + + public buildStarted( + targetName: string, + launchConfig: vscode.DebugConfiguration, + options: vscode.DebugSessionOptions + ) { + this.buildStartEmitter.fire({ targetName, launchConfig, options }); + } + + public buildFinished( + targetName: string, + launchConfig: vscode.DebugConfiguration, + options: vscode.DebugSessionOptions + ) { + this.buildFinishEmitter.fire({ targetName, launchConfig, options }); + } + /** * catch workspace folder changes and add or remove folders based on those changes * @param event workspace folder event @@ -594,6 +630,19 @@ export class WorkspaceContext implements vscode.Disposable { private swiftFileObservers = new Set<(listener: SwiftFileEvent) => unknown>(); } +/** Test events for test run begin/end */ +interface TestEvent { + kind: TestKind; + folder: FolderContext; +} + +/** Build events for build + run start/stop */ +interface BuildEvent { + targetName: string; + launchConfig: vscode.DebugConfiguration; + options: vscode.DebugSessionOptions; +} + /** Workspace Folder Operation types */ export enum FolderOperation { // Package folder has been added @@ -612,6 +661,8 @@ export enum FolderOperation { workspaceStateUpdated = "workspaceStateUpdated", // .build/workspace-state.json has been updated packageViewUpdated = "packageViewUpdated", + // Package plugins list has been updated + pluginsUpdated = "pluginsUpdated", } /** Workspace Folder Event */ diff --git a/src/commands.ts b/src/commands.ts index b88e6cecb..a883210d7 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -14,7 +14,7 @@ import * as vscode from "vscode"; import { WorkspaceContext } from "./WorkspaceContext"; -import { PackageNode } from "./ui/PackageDependencyProvider"; +import { PackageNode } from "./ui/ProjectPanelProvider"; import { SwiftToolchain } from "./toolchain/toolchain"; import { debugSnippet, runSnippet } from "./SwiftSnippets"; import { showToolchainSelectionQuickPick } from "./ui/ToolchainSelection"; @@ -38,8 +38,10 @@ import { updateDependencies } from "./commands/dependencies/update"; import { runPluginTask } from "./commands/runPluginTask"; import { runTestMultipleTimes } from "./commands/testMultipleTimes"; import { newSwiftFile } from "./commands/newFile"; -import { runAllTestsParallel } from "./commands/runParallelTests"; +import { runAllTests, runAllTestsParallel } from "./commands/runAllTests"; import { updateDependenciesViewList } from "./commands/dependencies/updateDepViewList"; +import { runTask } from "./commands/runTask"; +import { TestKind } from "./TestExplorer/TestKind"; /** * References: @@ -77,7 +79,10 @@ export enum Commands { RESET_PACKAGE = "swift.resetPackage", USE_LOCAL_DEPENDENCY = "swift.useLocalDependency", UNEDIT_DEPENDENCY = "swift.uneditDependency", + RUN_TASK = "swift.runTask", RUN_PLUGIN_TASK = "swift.runPluginTask", + RUN_SNIPPET = "swift.runSnippet", + DEBUG_SNIPPET = "swift.debugSnippet", PREVIEW_DOCUMENTATION = "swift.previewDocumentation", } @@ -93,8 +98,8 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { vscode.commands.registerCommand(Commands.UPDATE_DEPENDENCIES, () => updateDependencies(ctx) ), - vscode.commands.registerCommand(Commands.RUN, () => runBuild(ctx)), - vscode.commands.registerCommand(Commands.DEBUG, () => debugBuild(ctx)), + vscode.commands.registerCommand(Commands.RUN, target => runBuild(ctx, target)), + vscode.commands.registerCommand(Commands.DEBUG, target => debugBuild(ctx, target)), vscode.commands.registerCommand(Commands.CLEAN_BUILD, () => cleanBuild(ctx)), vscode.commands.registerCommand(Commands.RUN_TESTS_MULTIPLE_TIMES, item => { if (ctx.currentFolder) { @@ -115,9 +120,12 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { return openPackage(ctx.toolchain.swiftVersion, ctx.currentFolder.folder); } }), - vscode.commands.registerCommand("swift.runSnippet", () => runSnippet(ctx)), - vscode.commands.registerCommand("swift.debugSnippet", () => debugSnippet(ctx)), + vscode.commands.registerCommand(Commands.RUN_SNIPPET, target => runSnippet(ctx, target)), + vscode.commands.registerCommand(Commands.DEBUG_SNIPPET, target => + debugSnippet(ctx, target) + ), vscode.commands.registerCommand(Commands.RUN_PLUGIN_TASK, () => runPluginTask()), + vscode.commands.registerCommand(Commands.RUN_TASK, name => runTask(ctx, name)), vscode.commands.registerCommand("swift.restartLSPServer", () => ctx.languageClientManager.restart() ), @@ -159,6 +167,10 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { "swift.runAllTestsParallel", async () => await runAllTestsParallel(ctx) ), + vscode.commands.registerCommand( + "swift.runAllTests", + async (testKind: TestKind) => await runAllTests(ctx, testKind) + ), vscode.commands.registerCommand( Commands.PREVIEW_DOCUMENTATION, async () => await ctx.documentation.launchDocumentationPreview() diff --git a/src/commands/build.ts b/src/commands/build.ts index 03e66489a..1ce2744d3 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -18,19 +18,20 @@ import { createSwiftTask, SwiftTaskProvider } from "../tasks/SwiftTaskProvider"; import { debugLaunchConfig, getLaunchConfiguration } from "../debugger/launch"; import { executeTaskWithUI } from "./utilities"; import { FolderContext } from "../FolderContext"; +import { Target } from "../SwiftPackage"; /** * Executes a {@link vscode.Task task} to run swift target. */ -export async function runBuild(ctx: WorkspaceContext) { - return await debugBuildWithOptions(ctx, { noDebug: true }); +export async function runBuild(ctx: WorkspaceContext, target?: string) { + return await debugBuildWithOptions(ctx, { noDebug: true }, target); } /** * Executes a {@link vscode.Task task} to debug swift target. */ -export async function debugBuild(ctx: WorkspaceContext) { - return await debugBuildWithOptions(ctx, {}); +export async function debugBuild(ctx: WorkspaceContext, target?: string) { + return await debugBuildWithOptions(ctx, {}, target); } /** @@ -70,7 +71,8 @@ export async function folderCleanBuild(folderContext: FolderContext) { */ export async function debugBuildWithOptions( ctx: WorkspaceContext, - options: vscode.DebugSessionOptions + options: vscode.DebugSessionOptions, + targetName?: string ) { const current = ctx.currentFolder; if (!current) { @@ -80,13 +82,19 @@ export async function debugBuildWithOptions( return; } - const file = vscode.window.activeTextEditor?.document.fileName; - if (!file) { - ctx.outputChannel.appendLine("debugBuildWithOptions: No active text editor"); - return; + let target: Target | undefined; + if (targetName) { + target = current.swiftPackage.targets.find(target => target.name === targetName); + } else { + const file = vscode.window.activeTextEditor?.document.fileName; + if (!file) { + ctx.outputChannel.appendLine("debugBuildWithOptions: No active text editor"); + return; + } + + target = current.swiftPackage.getTarget(file); } - const target = current.swiftPackage.getTarget(file); if (!target) { ctx.outputChannel.appendLine("debugBuildWithOptions: No active target"); return; @@ -101,6 +109,9 @@ export async function debugBuildWithOptions( const launchConfig = getLaunchConfiguration(target.name, current); if (launchConfig) { - return debugLaunchConfig(current.workspaceFolder, launchConfig, options); + ctx.buildStarted(target.name, launchConfig, options); + const result = await debugLaunchConfig(current.workspaceFolder, launchConfig, options); + ctx.buildFinished(target.name, launchConfig, options); + return result; } } diff --git a/src/commands/openInExternalEditor.ts b/src/commands/openInExternalEditor.ts index 29f4114ab..6dc621765 100644 --- a/src/commands/openInExternalEditor.ts +++ b/src/commands/openInExternalEditor.ts @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { PackageNode } from "../ui/PackageDependencyProvider"; +import { PackageNode } from "../ui/ProjectPanelProvider"; /** * Opens the supplied `PackageNode` externally using the default application. diff --git a/src/commands/openInWorkspace.ts b/src/commands/openInWorkspace.ts index 7b4b9601d..dda3903c0 100644 --- a/src/commands/openInWorkspace.ts +++ b/src/commands/openInWorkspace.ts @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { PackageNode } from "../ui/PackageDependencyProvider"; +import { PackageNode } from "../ui/ProjectPanelProvider"; /** * Open a local package in workspace diff --git a/src/commands/runParallelTests.ts b/src/commands/runAllTests.ts similarity index 80% rename from src/commands/runParallelTests.ts rename to src/commands/runAllTests.ts index 11970b803..acf0c847e 100644 --- a/src/commands/runParallelTests.ts +++ b/src/commands/runAllTests.ts @@ -17,15 +17,13 @@ import { TestKind } from "../TestExplorer/TestKind"; import { WorkspaceContext } from "../WorkspaceContext"; import { flattenTestItemCollection } from "../TestExplorer/TestUtils"; -export async function runAllTestsParallel(ctx: WorkspaceContext) { +export async function runAllTests(ctx: WorkspaceContext, testKind: TestKind) { const testExplorer = ctx.currentFolder?.testExplorer; if (testExplorer === undefined) { return; } - const profile = testExplorer.testRunProfiles.find( - profile => profile.label === TestKind.parallel - ); + const profile = testExplorer.testRunProfiles.find(profile => profile.label === testKind); if (profile === undefined) { return; } @@ -36,4 +34,10 @@ export async function runAllTestsParallel(ctx: WorkspaceContext) { new vscode.TestRunRequest(tests, undefined, profile), tokenSource.token ); + + await vscode.commands.executeCommand("testing.showMostRecentOutput"); +} + +export async function runAllTestsParallel(ctx: WorkspaceContext) { + await runAllTests(ctx, TestKind.parallel); } diff --git a/src/commands/runTask.ts b/src/commands/runTask.ts new file mode 100644 index 000000000..3bca73535 --- /dev/null +++ b/src/commands/runTask.ts @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 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 { WorkspaceContext } from "../WorkspaceContext"; +import { TaskOperation } from "../tasks/TaskQueue"; +import { SwiftPluginTaskProvider } from "../tasks/SwiftPluginTaskProvider"; + +export const runTask = async (ctx: WorkspaceContext, name: string) => { + if (!ctx.currentFolder) { + return; + } + + const tasks = await vscode.tasks.fetchTasks(); + let task = tasks.find(task => task.name === name); + if (!task) { + const pluginTaskProvider = new SwiftPluginTaskProvider(ctx); + const pluginTasks = await pluginTaskProvider.provideTasks( + new vscode.CancellationTokenSource().token + ); + task = pluginTasks.find(task => task.name === name); + } + + if (!task) { + vscode.window.showErrorMessage(`Task "${name}" not found`); + return; + } + + return ctx.currentFolder.taskQueue + .queueOperation(new TaskOperation(task)) + .then(result => result === 0); +}; diff --git a/src/extension.ts b/src/extension.ts index 661696230..15e45be46 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,7 +18,7 @@ import "source-map-support/register"; import * as vscode from "vscode"; import * as commands from "./commands"; import * as debug from "./debugger/launch"; -import { PackageDependenciesProvider } from "./ui/PackageDependencyProvider"; +import { ProjectPanelProvider } from "./ui/ProjectPanelProvider"; import { SwiftTaskProvider } from "./tasks/SwiftTaskProvider"; import { FolderOperation, WorkspaceContext } from "./WorkspaceContext"; import { FolderContext } from "./FolderContext"; @@ -160,13 +160,13 @@ export async function activate(context: vscode.ExtensionContext): Promise { ); }); - // dependency view - const dependenciesProvider = new PackageDependenciesProvider(workspaceContext); - const dependenciesView = vscode.window.createTreeView("packageDependencies", { - treeDataProvider: dependenciesProvider, + // project panel provider + const projectPanelProvider = new ProjectPanelProvider(workspaceContext); + const dependenciesView = vscode.window.createTreeView("projectPanel", { + treeDataProvider: projectPanelProvider, showCollapseAll: true, }); - dependenciesProvider.observeFolders(dependenciesView); + projectPanelProvider.observeFolders(dependenciesView); // observer that will resolve package and build launch configurations const resolvePackageObserver = workspaceContext.onDidChangeFolders( @@ -189,6 +189,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { } else { await resolveFolderDependencies(folder, true); } + if ( workspace.toolchain.swiftVersion.isGreaterThanOrEqual( new Version(5, 6, 0) @@ -201,6 +202,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { async () => { await folder.loadSwiftPlugins(); workspace.updatePluginContextKey(); + folder.fireEvent(FolderOperation.pluginsUpdated); } ); } @@ -252,7 +254,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { testExplorerObserver, swiftModuleDocumentProvider, dependenciesView, - dependenciesProvider, + projectPanelProvider, logObserver, languageStatusItem, pluginTaskProvider, diff --git a/src/ui/PackageDependencyProvider.ts b/src/ui/PackageDependencyProvider.ts deleted file mode 100644 index 6cd038935..000000000 --- a/src/ui/PackageDependencyProvider.ts +++ /dev/null @@ -1,269 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VS Code Swift open source project -// -// Copyright (c) 2021 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 * as fs from "fs/promises"; -import * as path from "path"; -import configuration from "../configuration"; -import { WorkspaceContext } from "../WorkspaceContext"; -import { FolderOperation } from "../WorkspaceContext"; -import contextKeys from "../contextKeys"; -import { Dependency, ResolvedDependency } from "../SwiftPackage"; - -/** - * References: - * - * - Contributing views: - * https://code.visualstudio.com/api/references/contribution-points#contributes.views - * - Contributing welcome views: - * https://code.visualstudio.com/api/references/contribution-points#contributes.viewsWelcome - * - Implementing a TreeView: - * https://code.visualstudio.com/api/extension-guides/tree-view - */ - -/** - * Returns a {@link FileNode} for every file or subdirectory - * in the given directory. - */ -async function getChildren(directoryPath: string, parentId?: string): Promise { - const contents = await fs.readdir(directoryPath); - const results: FileNode[] = []; - const excludes = configuration.excludePathsFromPackageDependencies; - for (const fileName of contents) { - if (excludes.includes(fileName)) { - continue; - } - const filePath = path.join(directoryPath, fileName); - const stats = await fs.stat(filePath); - results.push(new FileNode(fileName, filePath, stats.isDirectory(), parentId)); - } - return results.sort((first, second) => { - if (first.isDirectory === second.isDirectory) { - // If both nodes are of the same type, sort them by name. - return first.name.localeCompare(second.name); - } else { - // Otherwise, sort directories first. - return first.isDirectory ? -1 : 1; - } - }); -} - -/** - * A package in the Package Dependencies {@link vscode.TreeView TreeView}. - */ -export class PackageNode { - private id: string; - - constructor( - private dependency: ResolvedDependency, - private childDependencies: (dependency: Dependency) => ResolvedDependency[], - private parentId?: string - ) { - this.id = - (this.parentId ? `${this.parentId}->` : "") + - `${this.name}-${this.dependency.version ?? ""}`; - } - - get name(): string { - return this.dependency.identity; - } - - get location(): string { - return this.dependency.location; - } - - get type(): string { - return this.dependency.type; - } - - get path(): string { - return this.dependency.path ?? ""; - } - - toTreeItem(): vscode.TreeItem { - const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); - item.id = this.id; - item.description = this.dependency.version; - item.iconPath = - this.dependency.type === "editing" - ? new vscode.ThemeIcon("edit") - : new vscode.ThemeIcon("package"); - item.contextValue = this.dependency.type; - item.accessibilityInformation = { label: `Package ${this.name}` }; - item.tooltip = this.path; - return item; - } - - async getChildren(): Promise { - const [childDeps, files] = await Promise.all([ - this.childDependencies(this.dependency), - getChildren(this.dependency.path, this.id), - ]); - const childNodes = childDeps.map( - dep => new PackageNode(dep, this.childDependencies, this.id) - ); - - // Show dependencies first, then files. - return [...childNodes, ...files]; - } -} - -/** - * A file or directory in the Package Dependencies {@link vscode.TreeView TreeView}. - */ -export class FileNode { - private id: string; - - constructor( - public name: string, - public path: string, - public isDirectory: boolean, - private parentId?: string - ) { - this.id = (this.parentId ? `${this.parentId}->` : "") + `${this.path}`; - } - - toTreeItem(): vscode.TreeItem { - const item = new vscode.TreeItem( - this.name, - this.isDirectory - ? vscode.TreeItemCollapsibleState.Collapsed - : vscode.TreeItemCollapsibleState.None - ); - item.id = this.id; - item.resourceUri = vscode.Uri.file(this.path); - item.tooltip = this.path; - if (!this.isDirectory) { - item.command = { - command: "vscode.open", - arguments: [item.resourceUri], - title: "Open File", - }; - item.accessibilityInformation = { label: `File ${this.name}` }; - } else { - item.accessibilityInformation = { label: `Folder ${this.name}` }; - } - return item; - } - - async getChildren(): Promise { - return await getChildren(this.path, this.id); - } -} - -/** - * A node in the Package Dependencies {@link vscode.TreeView TreeView}. - * - * Can be either a {@link PackageNode} or a {@link FileNode}. - */ -type TreeNode = PackageNode | FileNode; - -/** - * A {@link vscode.TreeDataProvider TreeDataProvider} for the Package Dependencies {@link vscode.TreeView TreeView}. - */ -export class PackageDependenciesProvider implements vscode.TreeDataProvider { - private didChangeTreeDataEmitter = new vscode.EventEmitter< - TreeNode | undefined | null | void - >(); - private workspaceObserver?: vscode.Disposable; - - onDidChangeTreeData = this.didChangeTreeDataEmitter.event; - - constructor(private workspaceContext: WorkspaceContext) { - // default context key to false. These will be updated as folders are given focus - contextKeys.hasPackage = false; - contextKeys.packageHasDependencies = false; - } - - dispose() { - this.workspaceObserver?.dispose(); - } - - observeFolders(treeView: vscode.TreeView) { - this.workspaceObserver = this.workspaceContext.onDidChangeFolders( - ({ folder, operation }) => { - switch (operation) { - case FolderOperation.focus: - if (!folder) { - return; - } - treeView.title = `Package Dependencies (${folder.name})`; - this.didChangeTreeDataEmitter.fire(); - break; - case FolderOperation.unfocus: - treeView.title = `Package Dependencies`; - this.didChangeTreeDataEmitter.fire(); - break; - case FolderOperation.workspaceStateUpdated: - case FolderOperation.resolvedUpdated: - case FolderOperation.packageViewUpdated: - if (!folder) { - return; - } - if (folder === this.workspaceContext.currentFolder) { - this.didChangeTreeDataEmitter.fire(); - } - } - } - ); - } - - getTreeItem(element: TreeNode): vscode.TreeItem { - return element.toTreeItem(); - } - - async getChildren(element?: TreeNode): Promise { - const folderContext = this.workspaceContext.currentFolder; - if (!folderContext) { - return []; - } - if (!element) { - if (contextKeys.flatDependenciesList) { - const existenceMap = new Map(); - const gatherChildren = ( - dependencies: ResolvedDependency[] - ): ResolvedDependency[] => { - const result: ResolvedDependency[] = []; - for (const dep of dependencies) { - if (!existenceMap.has(dep.identity)) { - result.push(dep); - existenceMap.set(dep.identity, true); - } - const childDeps = folderContext.swiftPackage.childDependencies(dep); - result.push(...gatherChildren(childDeps)); - } - return result; - }; - - const rootDeps = folderContext.swiftPackage.rootDependencies(); - const allDeps = gatherChildren(rootDeps); - return allDeps.map(dependency => new PackageNode(dependency, () => [])); - } else { - return folderContext.swiftPackage - .rootDependencies() - .map( - dependency => - new PackageNode( - dependency, - folderContext.swiftPackage.childDependencies.bind( - folderContext.swiftPackage - ) - ) - ); - } - } else { - return await element.getChildren(); - } - } -} diff --git a/src/ui/ProjectPanelProvider.ts b/src/ui/ProjectPanelProvider.ts new file mode 100644 index 000000000..457aa1551 --- /dev/null +++ b/src/ui/ProjectPanelProvider.ts @@ -0,0 +1,594 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 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 * as fs from "fs/promises"; +import * as path from "path"; +import configuration from "../configuration"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { FolderOperation } from "../WorkspaceContext"; +import contextKeys from "../contextKeys"; +import { Dependency, ResolvedDependency, Target } from "../SwiftPackage"; +import { SwiftPluginTaskProvider } from "../tasks/SwiftPluginTaskProvider"; +import { TestKind } from "../TestExplorer/TestKind"; + +/** + * References: + * + * - Contributing views: + * https://code.visualstudio.com/api/references/contribution-points#contributes.views + * - Contributing welcome views: + * https://code.visualstudio.com/api/references/contribution-points#contributes.viewsWelcome + * - Implementing a TreeView: + * https://code.visualstudio.com/api/extension-guides/tree-view + */ + +/** + * Returns a {@link FileNode} for every file or subdirectory + * in the given directory. + */ +async function getChildren(directoryPath: string, parentId?: string): Promise { + const contents = await fs.readdir(directoryPath); + const results: FileNode[] = []; + const excludes = configuration.excludePathsFromPackageDependencies; + for (const fileName of contents) { + if (excludes.includes(fileName)) { + continue; + } + const filePath = path.join(directoryPath, fileName); + const stats = await fs.stat(filePath); + results.push(new FileNode(fileName, filePath, stats.isDirectory(), parentId)); + } + return results.sort((first, second) => { + if (first.isDirectory === second.isDirectory) { + // If both nodes are of the same type, sort them by name. + return first.name.localeCompare(second.name); + } else { + // Otherwise, sort directories first. + return first.isDirectory ? -1 : 1; + } + }); +} + +/** + * A package in the Package Dependencies {@link vscode.TreeView TreeView}. + */ +export class PackageNode { + private id: string; + + constructor( + private dependency: ResolvedDependency, + private childDependencies: (dependency: Dependency) => ResolvedDependency[], + private parentId?: string + ) { + this.id = + (this.parentId ? `${this.parentId}->` : "") + + `${this.name}-${this.dependency.version ?? ""}`; + } + + get name(): string { + return this.dependency.identity; + } + + get location(): string { + return this.dependency.location; + } + + get type(): string { + return this.dependency.type; + } + + get path(): string { + return this.dependency.path ?? ""; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); + item.id = this.id; + item.description = this.dependency.version; + item.iconPath = + this.dependency.type === "editing" + ? new vscode.ThemeIcon("edit") + : new vscode.ThemeIcon("package"); + item.contextValue = this.dependency.type; + item.accessibilityInformation = { label: `Package ${this.name}` }; + item.tooltip = this.path; + return item; + } + + async getChildren(): Promise { + const [childDeps, files] = await Promise.all([ + this.childDependencies(this.dependency), + getChildren(this.dependency.path, this.id), + ]); + const childNodes = childDeps.map( + dep => new PackageNode(dep, this.childDependencies, this.id) + ); + + // Show dependencies first, then files. + return [...childNodes, ...files]; + } +} + +/** + * A file or directory in the Package Dependencies {@link vscode.TreeView TreeView}. + */ +export class FileNode { + private id: string; + + constructor( + public name: string, + public path: string, + public isDirectory: boolean, + private parentId?: string + ) { + this.id = (this.parentId ? `${this.parentId}->` : "") + `${this.path}`; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem( + this.name, + this.isDirectory + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + ); + item.id = this.id; + item.resourceUri = vscode.Uri.file(this.path); + item.tooltip = this.path; + if (!this.isDirectory) { + item.command = { + command: "vscode.open", + arguments: [item.resourceUri], + title: "Open File", + }; + item.accessibilityInformation = { label: `File ${this.name}` }; + } else { + item.accessibilityInformation = { label: `Folder ${this.name}` }; + } + return item; + } + + async getChildren(): Promise { + return await getChildren(this.path, this.id); + } +} + +class TaskNode { + constructor( + public type: string, + public name: string, + private active: boolean + ) {} + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.None); + item.id = `${this.type}-${this.name}`; + item.iconPath = new vscode.ThemeIcon(this.active ? "sync~spin" : "play"); + item.contextValue = "task"; + item.accessibilityInformation = { label: this.name }; + item.command = { + command: "swift.runTask", + arguments: [this.name], + title: "Run Task", + }; + return item; + } + + getChildren(): TreeNode[] { + return []; + } +} + +class CommandNode { + constructor( + public command: string, + public name: string, + public args: unknown[] | undefined, + private active: boolean + ) {} + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.None); + item.id = `${this.name}-${this.command}-${(this.args ?? []).join("-")}`; + item.iconPath = new vscode.ThemeIcon(this.active ? "sync~spin" : "play"); + item.contextValue = "command"; + item.accessibilityInformation = { label: this.name }; + item.command = { + command: this.command, + arguments: this.args, + title: this.name, + }; + return item; + } + + getChildren(): TreeNode[] { + return []; + } +} + +class TargetNode { + constructor( + public target: Target, + private activeTasks: Set + ) {} + + get name(): string { + return this.target.name; + } + + toTreeItem(): vscode.TreeItem { + const name = this.target.name; + const hasChildren = this.getChildren().length > 0; + const item = new vscode.TreeItem( + name, + hasChildren + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.None + ); + item.id = name; + item.iconPath = new vscode.ThemeIcon(this.icon()); + item.contextValue = "target"; + item.accessibilityInformation = { label: name }; + return item; + } + + private icon(): string { + switch (this.target.type) { + case "executable": + return "output"; + case "library": + return "library"; + case "test": + return "test-view-icon"; + case "snippet": + return "notebook"; + case "plugin": + return "plug"; + } + } + + getChildren(): TreeNode[] { + switch (this.target.type) { + case "executable": + return [ + new CommandNode( + "swift.run", + "Run", + [this.target.name], + this.activeTasks.has(`${this.target.name} noDebug`) + ), + new CommandNode( + "swift.debug", + "Debug", + [this.target.name], + this.activeTasks.has(this.target.name) + ), + ]; + case "test": + return [ + new CommandNode( + "swift.runAllTests", + TestKind.standard, + [TestKind.standard], + this.activeTasks.has(TestKind.standard) + ), + new CommandNode( + "swift.runAllTests", + TestKind.debug, + [TestKind.debug], + this.activeTasks.has(TestKind.debug) + ), + new CommandNode( + "swift.runAllTests", + TestKind.parallel, + [TestKind.parallel], + this.activeTasks.has(TestKind.parallel) + ), + new CommandNode( + "swift.runAllTests", + TestKind.coverage, + [TestKind.coverage], + this.activeTasks.has(TestKind.coverage) + ), + ]; + case "snippet": + case "library": + case "plugin": + return []; + } + } +} + +class HeaderNode { + constructor( + private id: string, + public name: string, + private icon: string, + private _getChildren: () => Promise + ) {} + + get path(): string { + return ""; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); + item.id = `${this.id}-${this.name}`; + item.iconPath = new vscode.ThemeIcon(this.icon); + item.contextValue = "header"; + item.accessibilityInformation = { label: this.name }; + return item; + } + + getChildren(): Promise { + return this._getChildren(); + } +} + +/** + * A node in the Package Dependencies {@link vscode.TreeView TreeView}. + * + * Can be either a {@link PackageNode}, {@link FileNode}, {@link TargetNode}, {@link TaskNode} or {@link HeaderNode}. + */ +type TreeNode = PackageNode | FileNode | HeaderNode | TaskNode | TargetNode | CommandNode; + +/** + * A {@link vscode.TreeDataProvider TreeDataProvider} for project dependencies, tasks and commands {@link vscode.TreeView TreeView}. + */ +export class ProjectPanelProvider implements vscode.TreeDataProvider { + private didChangeTreeDataEmitter = new vscode.EventEmitter< + TreeNode | undefined | null | void + >(); + private workspaceObserver?: vscode.Disposable; + private disposables: vscode.Disposable[] = []; + private activeTasks: Set = new Set(); + + onDidChangeTreeData = this.didChangeTreeDataEmitter.event; + + constructor(private workspaceContext: WorkspaceContext) { + // default context key to false. These will be updated as folders are given focus + contextKeys.hasPackage = false; + contextKeys.packageHasDependencies = false; + + this.observeTasks(workspaceContext); + } + + dispose() { + this.workspaceObserver?.dispose(); + } + + observeTasks(ctx: WorkspaceContext) { + this.disposables.push( + vscode.tasks.onDidStartTask(e => { + const taskId = e.execution.task.detail ?? e.execution.task.name; + this.activeTasks.add(taskId); + this.didChangeTreeDataEmitter.fire(); + }), + vscode.tasks.onDidEndTask(e => { + const taskId = e.execution.task.detail ?? e.execution.task.name; + this.activeTasks.delete(taskId); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidStartBuild(e => { + const taskId = e.options.noDebug ? `${e.targetName} noDebug` : e.targetName; + this.activeTasks.add(taskId); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidFinishBuild(e => { + const taskId = e.options.noDebug ? `${e.targetName} noDebug` : e.targetName; + this.activeTasks.delete(taskId); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidStartTests(e => { + const taskId = e.kind; + this.activeTasks.add(taskId); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidFinishTests(e => { + const taskId = e.kind; + this.activeTasks.delete(taskId); + this.didChangeTreeDataEmitter.fire(); + }) + ); + } + + observeFolders(treeView: vscode.TreeView) { + this.workspaceObserver = this.workspaceContext.onDidChangeFolders( + ({ folder, operation }) => { + switch (operation) { + case FolderOperation.focus: + if (!folder) { + return; + } + treeView.title = `Swift Project (${folder.name})`; + this.didChangeTreeDataEmitter.fire(); + break; + case FolderOperation.unfocus: + treeView.title = `Swift Project`; + this.didChangeTreeDataEmitter.fire(); + break; + case FolderOperation.workspaceStateUpdated: + case FolderOperation.resolvedUpdated: + case FolderOperation.packageViewUpdated: + case FolderOperation.pluginsUpdated: + if (!folder) { + return; + } + if (folder === this.workspaceContext.currentFolder) { + this.didChangeTreeDataEmitter.fire(); + } + } + } + ); + } + + getTreeItem(element: TreeNode): vscode.TreeItem { + return element.toTreeItem(); + } + + async getChildren(element?: TreeNode): Promise { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + + if (element) { + return element.getChildren(); + } + + const dependencies = this.dependencies(); + const snippets = this.snippets(); + const commands = await this.commands(); + + // TODO: Control ordering + return [ + ...(dependencies.length > 0 + ? [ + new HeaderNode( + "dependencies", + "Dependencies", + "circuit-board", + this.wrapInAsync(this.dependencies.bind(this)) + ), + ] + : []), + new HeaderNode("targets", "Targets", "book", this.wrapInAsync(this.targets.bind(this))), + new HeaderNode("tasks", "Tasks", "debug-continue-small", this.tasks.bind(this)), + ...(snippets.length > 0 + ? [ + new HeaderNode("snippets", "Snippets", "notebook", () => + Promise.resolve(snippets) + ), + ] + : []), + ...(commands.length > 0 + ? [ + new HeaderNode("commands", "Commands", "debug-line-by-line", () => + Promise.resolve(commands) + ), + ] + : []), + ]; + } + + private dependencies(): TreeNode[] { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + const pkg = folderContext.swiftPackage; + if (contextKeys.flatDependenciesList) { + const existenceMap = new Map(); + const gatherChildren = (dependencies: ResolvedDependency[]): ResolvedDependency[] => { + const result: ResolvedDependency[] = []; + for (const dep of dependencies) { + if (!existenceMap.has(dep.identity)) { + result.push(dep); + existenceMap.set(dep.identity, true); + } + const childDeps = pkg.childDependencies(dep); + result.push(...gatherChildren(childDeps)); + } + return result; + }; + + const rootDeps = pkg.rootDependencies(); + const allDeps = gatherChildren(rootDeps); + return allDeps.map(dependency => new PackageNode(dependency, () => [])); + } else { + const childDeps = pkg.childDependencies.bind(pkg); + return pkg.rootDependencies().map(dep => new PackageNode(dep, childDeps)); + } + } + + private targets(): TreeNode[] { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + return ( + folderContext.swiftPackage.targets + // Snipepts are shown under the Snippets header + .filter(target => target.type !== "snippet") + .map(target => new TargetNode(target, this.activeTasks)) + .sort((a, b) => a.name.localeCompare(b.name)) + ); + } + + private async tasks(): Promise { + const tasks = await vscode.tasks.fetchTasks(); + return ( + tasks + // Plugin tasks are shown under the Commands header + .filter(task => task.source !== "swift-plugin") + .map( + task => + new TaskNode( + "task", + task.name, + this.activeTasks.has(task.detail ?? task.name) + ) + ) + .sort((a, b) => a.name.localeCompare(b.name)) + ); + } + + private async commands(): Promise { + const provider = new SwiftPluginTaskProvider(this.workspaceContext); + const tasks = await provider.provideTasks(new vscode.CancellationTokenSource().token); + return tasks + .map( + task => + new TaskNode( + "command", + task.name, + this.activeTasks.has(task.detail ?? task.name) + ) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + } + + private snippets(): TreeNode[] { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + return folderContext.swiftPackage.targets + .filter(target => target.type === "snippet") + .flatMap( + target => + new HeaderNode(`snippet-${target.name}`, target.name, "symbol-snippet", () => + Promise.resolve([ + new CommandNode( + "swift.runSnippet", + "Run", + [target.name], + this.activeTasks.has(`${target.name} noDebug`) + ), + new CommandNode( + "swift.debugSnippet", + "Debug", + [target.name], + this.activeTasks.has(target.name) + ), + ]) + ) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + } + + private wrapInAsync(fn: () => T): () => Promise { + return async () => fn(); + } +} diff --git a/test/integration-tests/commands/dependency.test.ts b/test/integration-tests/commands/dependency.test.ts index afa7e40e4..c12407041 100644 --- a/test/integration-tests/commands/dependency.test.ts +++ b/test/integration-tests/commands/dependency.test.ts @@ -14,10 +14,7 @@ import { expect } from "chai"; import * as vscode from "vscode"; -import { - PackageDependenciesProvider, - PackageNode, -} from "../../../src/ui/PackageDependencyProvider"; +import { PackageNode, ProjectPanelProvider } from "../../../src/ui/ProjectPanelProvider"; import { testAssetUri } from "../../fixtures"; import { FolderContext } from "../../../src/FolderContext"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; @@ -57,12 +54,12 @@ suite("Dependency Commmands Test Suite", function () { }); suite("Swift: Use Local Dependency", function () { - let treeProvider: PackageDependenciesProvider; + let treeProvider: ProjectPanelProvider; setup(async () => { await workspaceContext.focusFolder(depsContext); await executeTaskAndWaitForResult((await getBuildAllTask(depsContext)) as SwiftTask); - treeProvider = new PackageDependenciesProvider(workspaceContext); + treeProvider = new ProjectPanelProvider(workspaceContext); }); teardown(() => { @@ -70,8 +67,11 @@ suite("Dependency Commmands Test Suite", function () { }); async function getDependency() { - const items = await treeProvider.getChildren(); - return items.find(n => n.name === "swift-markdown") as PackageNode; + const headers = await treeProvider.getChildren(); + const header = headers.find(n => n.name === "Dependencies") as PackageNode; + expect(header).to.not.be.undefined; + const children = await header.getChildren(); + return children.find(n => n.name === "swift-markdown") as PackageNode; } // Wait for the dependency to switch to the expected state. diff --git a/test/integration-tests/ui/PackageDependencyProvider.test.ts b/test/integration-tests/ui/PackageDependencyProvider.test.ts deleted file mode 100644 index 72b7ddaa3..000000000 --- a/test/integration-tests/ui/PackageDependencyProvider.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VS Code Swift open source project -// -// Copyright (c) 2024 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 { expect } from "chai"; -import * as vscode from "vscode"; -import * as path from "path"; -import { - PackageDependenciesProvider, - PackageNode, -} from "../../../src/ui/PackageDependencyProvider"; -import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities/tasks"; -import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; -import { testAssetPath } from "../../fixtures"; -import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; -import contextKeys from "../../../src/contextKeys"; -import { FolderContext } from "../../../src/FolderContext"; -import { WorkspaceContext } from "../../../src/WorkspaceContext"; - -suite("PackageDependencyProvider Test Suite", function () { - let workspaceContext: WorkspaceContext; - let folderContext: FolderContext; - let treeProvider: PackageDependenciesProvider; - this.timeout(3 * 60 * 1000); // Allow up to 3 minutes to build - - activateExtensionForSuite({ - async setup(ctx) { - workspaceContext = ctx; - await waitForNoRunningTasks(); - folderContext = await folderInRootWorkspace("dependencies", workspaceContext); - await executeTaskAndWaitForResult((await getBuildAllTask(folderContext)) as SwiftTask); - await folderContext.reload(); - treeProvider = new PackageDependenciesProvider(workspaceContext); - await workspaceContext.focusFolder(folderContext); - }, - async teardown() { - contextKeys.flatDependenciesList = false; - treeProvider.dispose(); - }, - }); - - setup(async () => { - await workspaceContext.focusFolder(folderContext); - }); - - test("Includes remote dependency", async () => { - contextKeys.flatDependenciesList = false; - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "swift-markdown") as PackageNode; - expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined; - expect(dep?.location).to.equal("https://github.com/swiftlang/swift-markdown.git"); - assertPathsEqual( - dep?.path, - path.join(testAssetPath("dependencies"), ".build/checkouts/swift-markdown") - ); - }); - - test("Includes local dependency", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "defaultpackage") as PackageNode; - expect( - dep, - `Expected to find defaultPackage, but instead items were ${items.map(n => n.name)}` - ).to.not.be.undefined; - assertPathsEqual(dep?.location, testAssetPath("defaultPackage")); - assertPathsEqual(dep?.path, testAssetPath("defaultPackage")); - }); - - test("Lists local dependency file structure", async () => { - contextKeys.flatDependenciesList = false; - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "defaultpackage") as PackageNode; - expect( - dep, - `Expected to find defaultPackage, but instead items were ${items.map(n => n.name)}` - ).to.not.be.undefined; - - const folders = await treeProvider.getChildren(dep); - const folder = folders.find(n => n.name === "Sources"); - expect(folder).to.not.be.undefined; - - assertPathsEqual(folder?.path, path.join(testAssetPath("defaultPackage"), "Sources")); - - const childFolders = await treeProvider.getChildren(folder); - const childFolder = childFolders.find(n => n.name === "PackageExe"); - expect(childFolder).to.not.be.undefined; - - assertPathsEqual( - childFolder?.path, - path.join(testAssetPath("defaultPackage"), "Sources/PackageExe") - ); - - const files = await treeProvider.getChildren(childFolder); - const file = files.find(n => n.name === "main.swift"); - expect(file).to.not.be.undefined; - - assertPathsEqual( - file?.path, - path.join(testAssetPath("defaultPackage"), "Sources/PackageExe/main.swift") - ); - }); - - test("Lists remote dependency file structure", async () => { - contextKeys.flatDependenciesList = false; - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "swift-markdown") as PackageNode; - expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined; - - const folders = await treeProvider.getChildren(dep); - const folder = folders.find(n => n.name === "Sources"); - expect(folder).to.not.be.undefined; - - const depPath = path.join(testAssetPath("dependencies"), ".build/checkouts/swift-markdown"); - assertPathsEqual(folder?.path, path.join(depPath, "Sources")); - - const childFolders = await treeProvider.getChildren(folder); - const childFolder = childFolders.find(n => n.name === "CAtomic"); - expect(childFolder).to.not.be.undefined; - - assertPathsEqual(childFolder?.path, path.join(depPath, "Sources/CAtomic")); - - const files = await treeProvider.getChildren(childFolder); - const file = files.find(n => n.name === "CAtomic.c"); - expect(file).to.not.be.undefined; - - assertPathsEqual(file?.path, path.join(depPath, "Sources/CAtomic/CAtomic.c")); - }); - - test("Shows a flat dependency list", async () => { - contextKeys.flatDependenciesList = true; - const items = await treeProvider.getChildren(); - expect(items.length).to.equal(3); - expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; - expect(items.find(n => n.name === "swift-cmark")).to.not.be.undefined; - expect(items.find(n => n.name === "defaultpackage")).to.not.be.undefined; - }); - - test("Shows a nested dependency list", async () => { - contextKeys.flatDependenciesList = false; - const items = await treeProvider.getChildren(); - expect(items.length).to.equal(2); - expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; - expect(items.find(n => n.name === "defaultpackage")).to.not.be.undefined; - }); - - function assertPathsEqual(path1: string | undefined, path2: string | undefined) { - expect(path1).to.not.be.undefined; - expect(path2).to.not.be.undefined; - // Convert to vscode.Uri to normalize paths, including drive letter capitalization on Windows. - expect(vscode.Uri.file(path1!).fsPath).to.equal(vscode.Uri.file(path2!).fsPath); - } -}); diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts new file mode 100644 index 000000000..336f974c2 --- /dev/null +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -0,0 +1,267 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 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 { expect } from "chai"; +import * as vscode from "vscode"; +import * as path from "path"; +import { ProjectPanelProvider, PackageNode, FileNode } from "../../../src/ui/ProjectPanelProvider"; +import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities/tasks"; +import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; +import { testAssetPath } from "../../fixtures"; +import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; +import contextKeys from "../../../src/contextKeys"; + +suite("ProjectPanelProvider Test Suite", function () { + let treeProvider: ProjectPanelProvider; + this.timeout(2 * 60 * 1000); // Allow up to 2 minutes to build + + activateExtensionForSuite({ + async setup(ctx) { + const workspaceContext = ctx; + await waitForNoRunningTasks(); + await folderInRootWorkspace("defaultPackage", workspaceContext); + const folderContext = await folderInRootWorkspace("targets", workspaceContext); + await executeTaskAndWaitForResult((await getBuildAllTask(folderContext)) as SwiftTask); + await folderContext.loadSwiftPlugins(); + treeProvider = new ProjectPanelProvider(workspaceContext); + await workspaceContext.focusFolder(folderContext); + }, + async teardown() { + contextKeys.flatDependenciesList = false; + treeProvider.dispose(); + }, + testAssets: ["targets"], + }); + + test("Includes top level nodes", async () => { + const commands = await treeProvider.getChildren(); + const commandNames = commands.map(n => n.name); + expect(commandNames).to.deep.equal([ + "Dependencies", + "Targets", + "Tasks", + "Snippets", + "Commands", + ]); + }); + + suite("Targets", () => { + test("Includes targets", async () => { + const targets = await getHeaderChildren("Targets"); + const targetNames = targets.map(target => target.name); + expect( + targetNames, + `Expected to find dependencies target, but instead items were ${targetNames}` + ).to.deep.equal(["ExecutableTarget", "LibraryTarget", "PluginTarget", "TargetsTests"]); + }); + }); + + suite("Tasks", () => { + test("Includes tasks", async () => { + const tasks = await getHeaderChildren("Tasks"); + const dep = tasks.find(n => n.name === "Build All (targets)") as PackageNode; + expect( + dep, + `Expected to find dependencies target, but instead items were ${tasks.map(n => n.name)}` + ).to.not.be.undefined; + }); + + test("Executes a task", async () => { + const tasks = await getHeaderChildren("Tasks"); + const task = tasks.find(n => n.name === "Build All (targets)"); + expect(task).to.not.be.undefined; + const treeItem = task?.toTreeItem(); + expect(treeItem?.command).to.not.be.undefined; + expect(treeItem?.command?.arguments).to.not.be.undefined; + if (treeItem && treeItem.command && treeItem.command.arguments) { + const command = treeItem.command.command; + const args = treeItem.command.arguments; + console.log("Executing", command, args); + const result = await vscode.commands.executeCommand(command, ...args); + expect(result).to.be.true; + } + }); + }); + + suite("Snippets", () => { + test("Includes snippets", async () => { + const snippets = await getHeaderChildren("Snippets"); + const snippetNames = snippets.map(n => n.name); + expect(snippetNames).to.deep.equal(["AnotherSnippet", "Snippet"]); + + for (const snippet of snippets) { + const snippetTasks = await snippet.getChildren(); + expect(snippetTasks.map(n => n.name)).to.deep.equal(["Run", "Debug"]); + } + }); + + test("Executes a snippet", async () => { + const snippets = await getHeaderChildren("Snippets"); + const snippet = snippets.find(n => n.name === "Snippet"); + expect(snippet).to.not.be.undefined; + const tasks = await snippet?.getChildren(); + const runTask = tasks?.find(n => n.name === "Run"); + expect(runTask).to.not.be.undefined; + expect(runTask).to.not.be.undefined; + const treeItem = runTask?.toTreeItem(); + expect(treeItem?.command).to.not.be.undefined; + expect(treeItem?.command?.arguments).to.not.be.undefined; + if (treeItem && treeItem.command && treeItem.command.arguments) { + const command = treeItem.command.command; + const args = treeItem.command.arguments; + const result = await vscode.commands.executeCommand(command, ...args); + expect(result).to.be.true; + } + }); + }); + + suite("Commands", () => { + test("Includes commands", async () => { + const commands = await getHeaderChildren("Commands"); + const commandNames = commands.map(n => n.name); + expect(commandNames).to.deep.equal(["PluginTarget"]); + }); + + test("Executes a command", async () => { + const commands = await getHeaderChildren("Commands"); + const command = commands.find(n => n.name === "PluginTarget"); + expect(command).to.not.be.undefined; + const treeItem = command?.toTreeItem(); + expect(treeItem?.command).to.not.be.undefined; + expect(treeItem?.command?.arguments).to.not.be.undefined; + if (treeItem && treeItem.command && treeItem.command.arguments) { + const command = treeItem.command.command; + const args = treeItem.command.arguments; + const result = await vscode.commands.executeCommand(command, ...args); + expect(result).to.be.true; + } + }); + }); + + suite("Dependencies", () => { + test("Includes remote dependency", async () => { + contextKeys.flatDependenciesList = false; + const items = await getHeaderChildren("Dependencies"); + const dep = items.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined; + expect(dep?.location).to.equal("https://github.com/swiftlang/swift-markdown.git"); + assertPathsEqual( + dep?.path, + path.join(testAssetPath("targets"), ".build/checkouts/swift-markdown") + ); + }); + + test("Includes local dependency", async () => { + const items = await getHeaderChildren("Dependencies"); + const dep = items.find(n => n.name === "defaultpackage") as PackageNode; + expect( + dep, + `Expected to find defaultPackage, but instead items were ${items.map(n => n.name)}` + ).to.not.be.undefined; + assertPathsEqual(dep?.location, testAssetPath("defaultPackage")); + assertPathsEqual(dep?.path, testAssetPath("defaultPackage")); + }); + + test("Lists local dependency file structure", async () => { + contextKeys.flatDependenciesList = false; + const children = await getHeaderChildren("Dependencies"); + const dep = children.find(n => n.name === "defaultpackage") as PackageNode; + expect( + dep, + `Expected to find defaultPackage, but instead items were ${children.map(n => n.name)}` + ).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const folder = folders.find(n => n.name === "Sources") as FileNode; + expect(folder).to.not.be.undefined; + + assertPathsEqual(folder?.path, path.join(testAssetPath("defaultPackage"), "Sources")); + + const childFolders = await treeProvider.getChildren(folder); + const childFolder = childFolders.find(n => n.name === "PackageExe") as FileNode; + expect(childFolder).to.not.be.undefined; + + assertPathsEqual( + childFolder?.path, + path.join(testAssetPath("defaultPackage"), "Sources/PackageExe") + ); + + const files = await treeProvider.getChildren(childFolder); + const file = files.find(n => n.name === "main.swift") as FileNode; + expect(file).to.not.be.undefined; + + assertPathsEqual( + file?.path, + path.join(testAssetPath("defaultPackage"), "Sources/PackageExe/main.swift") + ); + }); + + test("Lists remote dependency file structure", async () => { + contextKeys.flatDependenciesList = false; + const children = await getHeaderChildren("Dependencies"); + const dep = children.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep, `${JSON.stringify(children, null, 2)}`).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const folder = folders.find(n => n.name === "Sources") as FileNode; + expect(folder).to.not.be.undefined; + + const depPath = path.join(testAssetPath("targets"), ".build/checkouts/swift-markdown"); + assertPathsEqual(folder?.path, path.join(depPath, "Sources")); + + const childFolders = await treeProvider.getChildren(folder); + const childFolder = childFolders.find(n => n.name === "CAtomic") as FileNode; + expect(childFolder).to.not.be.undefined; + + assertPathsEqual(childFolder?.path, path.join(depPath, "Sources/CAtomic")); + + const files = await treeProvider.getChildren(childFolder); + const file = files.find(n => n.name === "CAtomic.c") as FileNode; + expect(file).to.not.be.undefined; + + assertPathsEqual(file?.path, path.join(depPath, "Sources/CAtomic/CAtomic.c")); + }); + + test("Shows a flat dependency list", async () => { + contextKeys.flatDependenciesList = true; + const items = await getHeaderChildren("Dependencies"); + expect(items.length).to.equal(3); + expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; + expect(items.find(n => n.name === "swift-cmark")).to.not.be.undefined; + expect(items.find(n => n.name === "defaultpackage")).to.not.be.undefined; + }); + + test("Shows a nested dependency list", async () => { + contextKeys.flatDependenciesList = false; + const items = await getHeaderChildren("Dependencies"); + expect(items.length).to.equal(2); + expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; + expect(items.find(n => n.name === "defaultpackage")).to.not.be.undefined; + }); + }); + + async function getHeaderChildren(headerName: string) { + const headers = await treeProvider.getChildren(); + const header = headers.find(n => n.name === headerName) as PackageNode; + expect(header).to.not.be.undefined; + return await header.getChildren(); + } + + function assertPathsEqual(path1: string | undefined, path2: string | undefined) { + expect(path1).to.not.be.undefined; + expect(path2).to.not.be.undefined; + // Convert to vscode.Uri to normalize paths, including drive letter capitalization on Windows. + expect(vscode.Uri.file(path1!).fsPath).to.equal(vscode.Uri.file(path2!).fsPath); + } +}); diff --git a/test/unit-tests/ui/PackageDependencyProvider.test.ts b/test/unit-tests/ui/PackageDependencyProvider.test.ts index d7d14b994..b7cca7b3a 100644 --- a/test/unit-tests/ui/PackageDependencyProvider.test.ts +++ b/test/unit-tests/ui/PackageDependencyProvider.test.ts @@ -15,7 +15,7 @@ import { expect } from "chai"; import * as vscode from "vscode"; import * as fs from "fs/promises"; -import { FileNode, PackageNode } from "../../../src/ui/PackageDependencyProvider"; +import { FileNode, PackageNode } from "../../../src/ui/ProjectPanelProvider"; import { mockGlobalModule } from "../../MockUtils"; suite("PackageDependencyProvider Unit Test Suite", function () { From 80fa84f88253bd26b804479c1b0e560c9402c4b2 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 14 Feb 2025 15:28:11 -0500 Subject: [PATCH 2/9] Switch test target to use XCTest to support testing older toolchains --- assets/test/targets/Tests/TargetsTests/TargetsTests.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/assets/test/targets/Tests/TargetsTests/TargetsTests.swift b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift index 4a2d903e8..1175ab3e5 100644 --- a/assets/test/targets/Tests/TargetsTests/TargetsTests.swift +++ b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift @@ -1,4 +1,6 @@ -import Testing +import XCTest -@Test func example() async throws { -} +class TargetsTests: XCTestCase { + func testExample() { + } +} \ No newline at end of file From 1bf93d22462a83eaa7aebe9b8c611714e1f44c56 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 20 Feb 2025 10:19:44 -0500 Subject: [PATCH 3/9] Set debugger in project panel tests --- .../ui/ProjectPanelProvider.test.ts | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index 336f974c2..5ca8fe161 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -13,14 +13,21 @@ //===----------------------------------------------------------------------===// import { expect } from "chai"; +import { beforeEach, afterEach } from "mocha"; import * as vscode from "vscode"; import * as path from "path"; import { ProjectPanelProvider, PackageNode, FileNode } from "../../../src/ui/ProjectPanelProvider"; import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities/tasks"; import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; import { testAssetPath } from "../../fixtures"; -import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "../utilities/testutilities"; import contextKeys from "../../../src/contextKeys"; +import { SwiftToolchain } from "../../../src/toolchain/toolchain"; +import { getLLDBLibPath } from "../../../src/debugger/lldb"; suite("ProjectPanelProvider Test Suite", function () { let treeProvider: ProjectPanelProvider; @@ -30,8 +37,10 @@ suite("ProjectPanelProvider Test Suite", function () { async setup(ctx) { const workspaceContext = ctx; await waitForNoRunningTasks(); - await folderInRootWorkspace("defaultPackage", workspaceContext); const folderContext = await folderInRootWorkspace("targets", workspaceContext); + await vscode.workspace.openTextDocument( + path.join(folderContext.folder.fsPath, "Package.swift") + ); await executeTaskAndWaitForResult((await getBuildAllTask(folderContext)) as SwiftTask); await folderContext.loadSwiftPlugins(); treeProvider = new ProjectPanelProvider(workspaceContext); @@ -44,6 +53,36 @@ suite("ProjectPanelProvider Test Suite", function () { testAssets: ["targets"], }); + async function getLLDBDebugAdapterPath() { + switch (process.platform) { + case "linux": + return "/usr/lib/liblldb.so"; + case "win32": + return await (await SwiftToolchain.create()).getLLDBDebugAdapter(); + default: + return getLLDBLibPath(await SwiftToolchain.create()); + } + } + + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + const lldbPath = { + "lldb.library": await getLLDBDebugAdapterPath(), + "lldb.launch.expressions": "native", + }; + + resetSettings = await updateSettings({ + "swift.debugger.useDebugAdapterFromToolchain": false, + ...lldbPath, + }); + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + } + }); + test("Includes top level nodes", async () => { const commands = await treeProvider.getChildren(); const commandNames = commands.map(n => n.name); @@ -79,8 +118,12 @@ suite("ProjectPanelProvider Test Suite", function () { test("Executes a task", async () => { const tasks = await getHeaderChildren("Tasks"); - const task = tasks.find(n => n.name === "Build All (targets)"); - expect(task).to.not.be.undefined; + const taskName = "Build All (targets)"; + const task = tasks.find(n => n.name === taskName); + expect( + task, + `Expected to find task called ${taskName}, but instead items were ${tasks.map(n => n.name)}` + ).to.not.be.undefined; const treeItem = task?.toTreeItem(); expect(treeItem?.command).to.not.be.undefined; expect(treeItem?.command?.arguments).to.not.be.undefined; From 9ddd56b7c20e8277cbf53fc14caeada817a747bb Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 20 Feb 2025 14:29:06 -0500 Subject: [PATCH 4/9] Refactor to use inline action buttons --- assets/test/.vscode/settings.json | 6 +- assets/test/targets/Package.swift | 4 + .../Sources/LibraryTarget/Targets.swift | 5 +- .../Tests/AnotherTests/AnotherTests.swift | 8 + .../Tests/TargetsTests/TargetsTests.swift | 2 + package.json | 80 +++++++- src/TestExplorer/TestRunner.ts | 17 +- src/WorkspaceContext.ts | 9 +- src/commands.ts | 49 ++++- src/commands/runAllTests.ts | 14 +- src/debugger/launch.ts | 1 + src/ui/ProjectPanelProvider.ts | 178 +++++++----------- src/ui/StatusItem.ts | 4 +- .../ui/ProjectPanelProvider.test.ts | 32 ++-- .../utilities/testutilities.ts | 4 +- 15 files changed, 252 insertions(+), 161 deletions(-) create mode 100644 assets/test/targets/Tests/AnotherTests/AnotherTests.swift diff --git a/assets/test/.vscode/settings.json b/assets/test/.vscode/settings.json index db1acbde4..8d72543e9 100644 --- a/assets/test/.vscode/settings.json +++ b/assets/test/.vscode/settings.json @@ -8,5 +8,9 @@ "-DTEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING" ], "lldb.verboseLogging": true, - "swift.backgroundCompilation": false + "swift.backgroundCompilation": false, + "swift.debugger.useDebugAdapterFromToolchain": false, + "lldb.library": "/usr/lib/liblldb.so", + "lldb.launch.expressions": "native", + "swift.pluginPermissions": {} } \ No newline at end of file diff --git a/assets/test/targets/Package.swift b/assets/test/targets/Package.swift index 7b79bf9c8..35cda10ab 100644 --- a/assets/test/targets/Package.swift +++ b/assets/test/targets/Package.swift @@ -40,5 +40,9 @@ let package = Package( name: "TargetsTests", dependencies: ["LibraryTarget"] ), + .testTarget( + name: "AnotherTests", + dependencies: ["LibraryTarget"] + ), ] ) diff --git a/assets/test/targets/Sources/LibraryTarget/Targets.swift b/assets/test/targets/Sources/LibraryTarget/Targets.swift index 1cddee56f..37c4f8832 100644 --- a/assets/test/targets/Sources/LibraryTarget/Targets.swift +++ b/assets/test/targets/Sources/LibraryTarget/Targets.swift @@ -1,6 +1,9 @@ // The Swift Programming Language // https://docs.swift.org/swift-book -func foo() { +public func foo() { print("foo") +} +public func bar() { + print("bar") } \ No newline at end of file diff --git a/assets/test/targets/Tests/AnotherTests/AnotherTests.swift b/assets/test/targets/Tests/AnotherTests/AnotherTests.swift new file mode 100644 index 000000000..8aa96db8b --- /dev/null +++ b/assets/test/targets/Tests/AnotherTests/AnotherTests.swift @@ -0,0 +1,8 @@ +import LibraryTarget +import XCTest + +class AnotherTests: XCTestCase { + func testExample() { + bar() + } +} \ No newline at end of file diff --git a/assets/test/targets/Tests/TargetsTests/TargetsTests.swift b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift index 1175ab3e5..089304193 100644 --- a/assets/test/targets/Tests/TargetsTests/TargetsTests.swift +++ b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift @@ -1,6 +1,8 @@ +import LibraryTarget import XCTest class TargetsTests: XCTestCase { func testExample() { + foo() } } \ No newline at end of file diff --git a/package.json b/package.json index 12143c7f0..74163686a 100644 --- a/package.json +++ b/package.json @@ -222,12 +222,14 @@ { "command": "swift.runSnippet", "title": "Run Swift Snippet", - "category": "Swift" + "category": "Swift", + "icon": "$(play)" }, { "command": "swift.debugSnippet", "title": "Debug Swift Snippet", - "category": "Swift" + "category": "Swift", + "icon": "$(debug)" }, { "command": "swift.runPluginTask", @@ -266,13 +268,27 @@ }, { "command": "swift.runAllTestsParallel", - "title": "Run All Tests in Parallel", - "category": "Test" + "title": "Run Tests in Parallel", + "category": "Test", + "icon": "$(testing-run-all-icon)" }, { "command": "swift.runAllTests", - "title": "Run All Tests", - "category": "Test" + "title": "Run Tests", + "category": "Test", + "icon": "$(testing-run-icon)" + }, + { + "command": "swift.debugAllTests", + "title": "Debug Tests", + "category": "Test", + "icon": "$(testing-debug-icon)" + }, + { + "command": "swift.coverAllTests", + "title": "Run Tests with Coverage", + "category": "Test", + "icon": "$(debug-coverage)" } ], "configuration": [ @@ -915,6 +931,18 @@ { "command": "swift.runAllTestsParallel", "when": "swift.isActivated" + }, + { + "command": "swift.runAllTests", + "when": "false" + }, + { + "command": "swift.debugAllTests", + "when": "false" + }, + { + "command": "swift.coverAllTests", + "when": "false" } ], "editor/context": [ @@ -1020,6 +1048,46 @@ { "command": "swift.openExternal", "when": "view == projectPanel && (viewItem == 'editing' || viewItem == 'remote')" + }, + { + "command": "swift.run", + "when": "view == projectPanel && viewItem == 'runnable'", + "group": "inline@0" + }, + { + "command": "swift.debug", + "when": "view == projectPanel && viewItem == 'runnable'", + "group": "inline@1" + }, + { + "command": "swift.runSnippet", + "when": "view == projectPanel && viewItem == 'snippet_runnable'", + "group": "inline@0" + }, + { + "command": "swift.debugSnippet", + "when": "view == projectPanel && viewItem == 'snippet_runnable'", + "group": "inline@1" + }, + { + "command": "swift.runAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@0" + }, + { + "command": "swift.debugAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@1" + }, + { + "command": "swift.runAllTestsParallel", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@2" + }, + { + "command": "swift.coverAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@3" } ] }, diff --git a/src/TestExplorer/TestRunner.ts b/src/TestExplorer/TestRunner.ts index d78f4462c..fd9346210 100644 --- a/src/TestExplorer/TestRunner.ts +++ b/src/TestExplorer/TestRunner.ts @@ -520,6 +520,18 @@ export class TestRunner { ]; } + /** + * Extracts a list of unique test Targets from the list of test items. + */ + private testTargets(items: vscode.TestItem[]): string[] { + const targets = new Set(); + for (const item of items) { + const target = item.id.split(".")[0]; + targets.add(target); + } + return Array.from(targets); + } + /** * Test run handler. Run a series of tests and extracts the results from the output * @param shouldDebug Should we run the debugger @@ -527,7 +539,8 @@ export class TestRunner { * @returns When complete */ async runHandler() { - this.workspaceContext.testsStarted(this.folderContext, this.testKind); + const testTargets = this.testTargets(this.testArgs.testItems); + this.workspaceContext.testsStarted(this.folderContext, this.testKind, testTargets); const runState = new TestRunnerTestRunState(this.testRun); @@ -554,7 +567,7 @@ export class TestRunner { cancellationDisposable.dispose(); await this.testRun.end(); - this.workspaceContext.testsFinished(this.folderContext, this.testKind); + this.workspaceContext.testsFinished(this.folderContext, this.testKind, testTargets); } /** Run test session without attaching to a debugger */ diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index eed2d6683..648b6776d 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -348,12 +348,12 @@ export class WorkspaceContext implements vscode.Disposable { await this.fireEvent(folderContext, FolderOperation.focus); } - public testsFinished(folder: FolderContext, kind: TestKind) { - this.testFinishEmitter.fire({ kind, folder }); + public testsFinished(folder: FolderContext, kind: TestKind, targets: string[]) { + this.testFinishEmitter.fire({ kind, folder, targets }); } - public testsStarted(folder: FolderContext, kind: TestKind) { - this.testStartEmitter.fire({ kind, folder }); + public testsStarted(folder: FolderContext, kind: TestKind, targets: string[]) { + this.testStartEmitter.fire({ kind, folder, targets }); } public buildStarted( @@ -634,6 +634,7 @@ export class WorkspaceContext implements vscode.Disposable { interface TestEvent { kind: TestKind; folder: FolderContext; + targets: string[]; } /** Build events for build + run start/stop */ diff --git a/src/commands.ts b/src/commands.ts index a883210d7..a28979167 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -38,7 +38,7 @@ import { updateDependencies } from "./commands/dependencies/update"; import { runPluginTask } from "./commands/runPluginTask"; import { runTestMultipleTimes } from "./commands/testMultipleTimes"; import { newSwiftFile } from "./commands/newFile"; -import { runAllTests, runAllTestsParallel } from "./commands/runAllTests"; +import { runAllTests } from "./commands/runAllTests"; import { updateDependenciesViewList } from "./commands/dependencies/updateDepViewList"; import { runTask } from "./commands/runTask"; import { TestKind } from "./TestExplorer/TestKind"; @@ -84,6 +84,10 @@ export enum Commands { RUN_SNIPPET = "swift.runSnippet", DEBUG_SNIPPET = "swift.debugSnippet", PREVIEW_DOCUMENTATION = "swift.previewDocumentation", + RUN_ALL_TESTS = "swift.runAllTests", + RUN_ALL_TESTS_PARALLEL = "swift.runAllTestsParallel", + DEBUG_ALL_TESTS = "swift.debugAllTests", + COVER_ALL_TESTS = "swift.coverAllTests", } /** @@ -98,8 +102,12 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { vscode.commands.registerCommand(Commands.UPDATE_DEPENDENCIES, () => updateDependencies(ctx) ), - vscode.commands.registerCommand(Commands.RUN, target => runBuild(ctx, target)), - vscode.commands.registerCommand(Commands.DEBUG, target => debugBuild(ctx, target)), + vscode.commands.registerCommand(Commands.RUN, target => + runBuild(ctx, ...unwrapTreeItem(target)) + ), + vscode.commands.registerCommand(Commands.DEBUG, target => + debugBuild(ctx, ...unwrapTreeItem(target)) + ), vscode.commands.registerCommand(Commands.CLEAN_BUILD, () => cleanBuild(ctx)), vscode.commands.registerCommand(Commands.RUN_TESTS_MULTIPLE_TIMES, item => { if (ctx.currentFolder) { @@ -120,9 +128,11 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { return openPackage(ctx.toolchain.swiftVersion, ctx.currentFolder.folder); } }), - vscode.commands.registerCommand(Commands.RUN_SNIPPET, target => runSnippet(ctx, target)), + vscode.commands.registerCommand(Commands.RUN_SNIPPET, target => + runSnippet(ctx, ...unwrapTreeItem(target)) + ), vscode.commands.registerCommand(Commands.DEBUG_SNIPPET, target => - debugSnippet(ctx, target) + debugSnippet(ctx, ...unwrapTreeItem(target)) ), vscode.commands.registerCommand(Commands.RUN_PLUGIN_TASK, () => runPluginTask()), vscode.commands.registerCommand(Commands.RUN_TASK, name => runTask(ctx, name)), @@ -164,12 +174,20 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { ), vscode.commands.registerCommand("swift.captureDiagnostics", () => captureDiagnostics(ctx)), vscode.commands.registerCommand( - "swift.runAllTestsParallel", - async () => await runAllTestsParallel(ctx) + Commands.RUN_ALL_TESTS_PARALLEL, + async item => await runAllTests(ctx, TestKind.parallel, ...unwrapTreeItem(item)) ), vscode.commands.registerCommand( - "swift.runAllTests", - async (testKind: TestKind) => await runAllTests(ctx, testKind) + Commands.RUN_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.standard, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.DEBUG_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.debug, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.COVER_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.coverage, ...unwrapTreeItem(item)) ), vscode.commands.registerCommand( Commands.PREVIEW_DOCUMENTATION, @@ -183,3 +201,16 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { ), ]; } + +/** + * Certain commands can be called via a vscode TreeView, which will pass a {@link CommandNode} object. + * If the command is called via a command palette or other means, the target will be a string. + */ +function unwrapTreeItem(target?: string | { args: string[] }): string[] { + if (typeof target === "object" && target !== null && "args" in target) { + return target.args ?? []; + } else if (typeof target === "string") { + return [target]; + } + return []; +} diff --git a/src/commands/runAllTests.ts b/src/commands/runAllTests.ts index acf0c847e..f629417ad 100644 --- a/src/commands/runAllTests.ts +++ b/src/commands/runAllTests.ts @@ -17,7 +17,7 @@ import { TestKind } from "../TestExplorer/TestKind"; import { WorkspaceContext } from "../WorkspaceContext"; import { flattenTestItemCollection } from "../TestExplorer/TestUtils"; -export async function runAllTests(ctx: WorkspaceContext, testKind: TestKind) { +export async function runAllTests(ctx: WorkspaceContext, testKind: TestKind, target?: string) { const testExplorer = ctx.currentFolder?.testExplorer; if (testExplorer === undefined) { return; @@ -28,7 +28,13 @@ export async function runAllTests(ctx: WorkspaceContext, testKind: TestKind) { return; } - const tests = flattenTestItemCollection(testExplorer.controller.items); + let tests = flattenTestItemCollection(testExplorer.controller.items); + + // If a target is specified, filter the tests to only run those that match the target. + if (target) { + const targetRegex = new RegExp(`^${target}(\\.|$)`); + tests = tests.filter(test => targetRegex.test(test.id)); + } const tokenSource = new vscode.CancellationTokenSource(); await profile.runHandler( new vscode.TestRunRequest(tests, undefined, profile), @@ -37,7 +43,3 @@ export async function runAllTests(ctx: WorkspaceContext, testKind: TestKind) { await vscode.commands.executeCommand("testing.showMostRecentOutput"); } - -export async function runAllTestsParallel(ctx: WorkspaceContext) { - await runAllTests(ctx, TestKind.parallel); -} diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index 2b0d2a08b..b91836828 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -181,6 +181,7 @@ export function createSnippetConfiguration( args: [], cwd: folder, env: swiftRuntimeEnv(true), + runType: "snippet", ...CI_DISABLE_ASLR, }; } diff --git a/src/ui/ProjectPanelProvider.ts b/src/ui/ProjectPanelProvider.ts index 457aa1551..793bebe1f 100644 --- a/src/ui/ProjectPanelProvider.ts +++ b/src/ui/ProjectPanelProvider.ts @@ -21,8 +21,8 @@ import { FolderOperation } from "../WorkspaceContext"; import contextKeys from "../contextKeys"; import { Dependency, ResolvedDependency, Target } from "../SwiftPackage"; import { SwiftPluginTaskProvider } from "../tasks/SwiftPluginTaskProvider"; -import { TestKind } from "../TestExplorer/TestKind"; +const LOADING_ICON = "loading~spin"; /** * References: * @@ -97,16 +97,23 @@ export class PackageNode { const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); item.id = this.id; item.description = this.dependency.version; - item.iconPath = - this.dependency.type === "editing" - ? new vscode.ThemeIcon("edit") - : new vscode.ThemeIcon("package"); + item.iconPath = new vscode.ThemeIcon(this.icon()); item.contextValue = this.dependency.type; item.accessibilityInformation = { label: `Package ${this.name}` }; item.tooltip = this.path; return item; } + icon() { + if (this.dependency.type === "editing") { + return "edit"; + } + if (this.dependency.type === "local") { + return "notebook-render-output"; + } + return "package"; + } + async getChildren(): Promise { const [childDeps, files] = await Promise.all([ this.childDependencies(this.dependency), @@ -174,7 +181,7 @@ class TaskNode { toTreeItem(): vscode.TreeItem { const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.None); item.id = `${this.type}-${this.name}`; - item.iconPath = new vscode.ThemeIcon(this.active ? "sync~spin" : "play"); + item.iconPath = new vscode.ThemeIcon(this.active ? LOADING_ICON : "play"); item.contextValue = "task"; item.accessibilityInformation = { label: this.name }; item.command = { @@ -190,31 +197,17 @@ class TaskNode { } } -class CommandNode { - constructor( - public command: string, - public name: string, - public args: unknown[] | undefined, - private active: boolean - ) {} - - toTreeItem(): vscode.TreeItem { - const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.None); - item.id = `${this.name}-${this.command}-${(this.args ?? []).join("-")}`; - item.iconPath = new vscode.ThemeIcon(this.active ? "sync~spin" : "play"); - item.contextValue = "command"; - item.accessibilityInformation = { label: this.name }; - item.command = { - command: this.command, - arguments: this.args, - title: this.name, - }; - return item; - } +/* + * Prefix a unique string on the test target name to avoid confusing it + * with another target that may share the same name. Targets can't start with % + * so this is guarenteed to be unique. + */ +function testTaskName(name: string): string { + return `%test-${name}`; +} - getChildren(): TreeNode[] { - return []; - } +function snippetTaskName(name: string): string { + return `%snippet-${name}`; } class TargetNode { @@ -227,6 +220,10 @@ class TargetNode { return this.target.name; } + get args(): string[] { + return [this.name]; + } + toTreeItem(): vscode.TreeItem { const name = this.target.name; const hasChildren = this.getChildren().length > 0; @@ -236,78 +233,54 @@ class TargetNode { ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None ); - item.id = name; + item.id = `${this.target.type}:${name}`; item.iconPath = new vscode.ThemeIcon(this.icon()); - item.contextValue = "target"; + item.contextValue = this.contextValue(); item.accessibilityInformation = { label: name }; return item; } private icon(): string { + if (this.activeTasks.has(this.name)) { + return LOADING_ICON; + } + switch (this.target.type) { case "executable": return "output"; case "library": return "library"; case "test": + if (this.activeTasks.has(testTaskName(this.name))) { + return LOADING_ICON; + } return "test-view-icon"; case "snippet": + if (this.activeTasks.has(snippetTaskName(this.name))) { + return LOADING_ICON; + } return "notebook"; case "plugin": return "plug"; } } - getChildren(): TreeNode[] { + private contextValue(): string | undefined { switch (this.target.type) { case "executable": - return [ - new CommandNode( - "swift.run", - "Run", - [this.target.name], - this.activeTasks.has(`${this.target.name} noDebug`) - ), - new CommandNode( - "swift.debug", - "Debug", - [this.target.name], - this.activeTasks.has(this.target.name) - ), - ]; - case "test": - return [ - new CommandNode( - "swift.runAllTests", - TestKind.standard, - [TestKind.standard], - this.activeTasks.has(TestKind.standard) - ), - new CommandNode( - "swift.runAllTests", - TestKind.debug, - [TestKind.debug], - this.activeTasks.has(TestKind.debug) - ), - new CommandNode( - "swift.runAllTests", - TestKind.parallel, - [TestKind.parallel], - this.activeTasks.has(TestKind.parallel) - ), - new CommandNode( - "swift.runAllTests", - TestKind.coverage, - [TestKind.coverage], - this.activeTasks.has(TestKind.coverage) - ), - ]; + return "runnable"; case "snippet": - case "library": - case "plugin": - return []; + return "snippet_runnable"; + case "test": + return "test_runnable"; + default: + return undefined; } } + + getChildren(): TreeNode[] { + return []; + } } class HeaderNode { @@ -341,10 +314,10 @@ class HeaderNode { * * Can be either a {@link PackageNode}, {@link FileNode}, {@link TargetNode}, {@link TaskNode} or {@link HeaderNode}. */ -type TreeNode = PackageNode | FileNode | HeaderNode | TaskNode | TargetNode | CommandNode; +type TreeNode = PackageNode | FileNode | HeaderNode | TaskNode | TargetNode; /** - * A {@link vscode.TreeDataProvider TreeDataProvider} for project dependencies, tasks and commands {@link vscode.TreeView TreeView}. + * A {@link vscode.TreeDataProvider TreeDataProvider} for project dependencies, tasks and commands {@link vscode.TreeView TreeView}. */ export class ProjectPanelProvider implements vscode.TreeDataProvider { private didChangeTreeDataEmitter = new vscode.EventEmitter< @@ -381,23 +354,31 @@ export class ProjectPanelProvider implements vscode.TreeDataProvider { this.didChangeTreeDataEmitter.fire(); }), ctx.onDidStartBuild(e => { - const taskId = e.options.noDebug ? `${e.targetName} noDebug` : e.targetName; - this.activeTasks.add(taskId); + if (e.launchConfig.runType === "snippet") { + this.activeTasks.add(snippetTaskName(e.targetName)); + } else { + this.activeTasks.add(e.targetName); + } this.didChangeTreeDataEmitter.fire(); }), ctx.onDidFinishBuild(e => { - const taskId = e.options.noDebug ? `${e.targetName} noDebug` : e.targetName; - this.activeTasks.delete(taskId); + if (e.launchConfig.runType === "snippet") { + this.activeTasks.delete(snippetTaskName(e.targetName)); + } else { + this.activeTasks.delete(e.targetName); + } this.didChangeTreeDataEmitter.fire(); }), ctx.onDidStartTests(e => { - const taskId = e.kind; - this.activeTasks.add(taskId); + for (const target of e.targets) { + this.activeTasks.add(testTaskName(target)); + } this.didChangeTreeDataEmitter.fire(); }), ctx.onDidFinishTests(e => { - const taskId = e.kind; - this.activeTasks.delete(taskId); + for (const target of e.targets) { + this.activeTasks.delete(testTaskName(target)); + } this.didChangeTreeDataEmitter.fire(); }) ); @@ -517,12 +498,13 @@ export class ProjectPanelProvider implements vscode.TreeDataProvider { if (!folderContext) { return []; } + const targetSort = (node: TargetNode) => `${node.target.type}-${node.name}`; return ( folderContext.swiftPackage.targets // Snipepts are shown under the Snippets header .filter(target => target.type !== "snippet") .map(target => new TargetNode(target, this.activeTasks)) - .sort((a, b) => a.name.localeCompare(b.name)) + .sort((a, b) => targetSort(a).localeCompare(targetSort(b))) ); } @@ -566,25 +548,7 @@ export class ProjectPanelProvider implements vscode.TreeDataProvider { } return folderContext.swiftPackage.targets .filter(target => target.type === "snippet") - .flatMap( - target => - new HeaderNode(`snippet-${target.name}`, target.name, "symbol-snippet", () => - Promise.resolve([ - new CommandNode( - "swift.runSnippet", - "Run", - [target.name], - this.activeTasks.has(`${target.name} noDebug`) - ), - new CommandNode( - "swift.debugSnippet", - "Debug", - [target.name], - this.activeTasks.has(target.name) - ), - ]) - ) - ) + .flatMap(target => new TargetNode(target, this.activeTasks)) .sort((a, b) => a.name.localeCompare(b.name)); } diff --git a/src/ui/StatusItem.ts b/src/ui/StatusItem.ts index 107429845..3d78ec29a 100644 --- a/src/ui/StatusItem.ts +++ b/src/ui/StatusItem.ts @@ -116,9 +116,9 @@ export class StatusItem { private showTask(task: RunningTask, message?: string) { message = message ?? task.name; if (typeof task.task !== "string") { - this.show(`$(sync~spin) ${message}`, message, "workbench.action.tasks.showTasks"); + this.show(`$(loading~spin) ${message}`, message, "workbench.action.tasks.showTasks"); } else { - this.show(`$(sync~spin) ${message}`, message); + this.show(`$(loading~spin) ${message}`, message); } } diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index 5ca8fe161..ab43a0ed0 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -60,7 +60,7 @@ suite("ProjectPanelProvider Test Suite", function () { case "win32": return await (await SwiftToolchain.create()).getLLDBDebugAdapter(); default: - return getLLDBLibPath(await SwiftToolchain.create()); + return (await getLLDBLibPath(await SwiftToolchain.create())).success; } } @@ -102,7 +102,13 @@ suite("ProjectPanelProvider Test Suite", function () { expect( targetNames, `Expected to find dependencies target, but instead items were ${targetNames}` - ).to.deep.equal(["ExecutableTarget", "LibraryTarget", "PluginTarget", "TargetsTests"]); + ).to.deep.equal([ + "ExecutableTarget", + "LibraryTarget", + "PluginTarget", + "AnotherTests", + "TargetsTests", + ]); }); }); @@ -130,7 +136,6 @@ suite("ProjectPanelProvider Test Suite", function () { if (treeItem && treeItem.command && treeItem.command.arguments) { const command = treeItem.command.command; const args = treeItem.command.arguments; - console.log("Executing", command, args); const result = await vscode.commands.executeCommand(command, ...args); expect(result).to.be.true; } @@ -142,30 +147,15 @@ suite("ProjectPanelProvider Test Suite", function () { const snippets = await getHeaderChildren("Snippets"); const snippetNames = snippets.map(n => n.name); expect(snippetNames).to.deep.equal(["AnotherSnippet", "Snippet"]); - - for (const snippet of snippets) { - const snippetTasks = await snippet.getChildren(); - expect(snippetTasks.map(n => n.name)).to.deep.equal(["Run", "Debug"]); - } }); test("Executes a snippet", async () => { const snippets = await getHeaderChildren("Snippets"); const snippet = snippets.find(n => n.name === "Snippet"); expect(snippet).to.not.be.undefined; - const tasks = await snippet?.getChildren(); - const runTask = tasks?.find(n => n.name === "Run"); - expect(runTask).to.not.be.undefined; - expect(runTask).to.not.be.undefined; - const treeItem = runTask?.toTreeItem(); - expect(treeItem?.command).to.not.be.undefined; - expect(treeItem?.command?.arguments).to.not.be.undefined; - if (treeItem && treeItem.command && treeItem.command.arguments) { - const command = treeItem.command.command; - const args = treeItem.command.arguments; - const result = await vscode.commands.executeCommand(command, ...args); - expect(result).to.be.true; - } + + const result = await vscode.commands.executeCommand("swift.runSnippet", snippet?.name); + expect(result).to.be.true; }); }); diff --git a/test/integration-tests/utilities/testutilities.ts b/test/integration-tests/utilities/testutilities.ts index 6393e39a5..321b8ccf3 100644 --- a/test/integration-tests/utilities/testutilities.ts +++ b/test/integration-tests/utilities/testutilities.ts @@ -311,7 +311,7 @@ export type SettingsMap = { [key: string]: unknown }; export async function updateSettings(settings: SettingsMap): Promise<() => Promise> { const applySettings = async (settings: SettingsMap) => { const savedOriginalSettings: SettingsMap = {}; - Object.keys(settings).forEach(async setting => { + for (const setting of Object.keys(settings)) { const { section, name } = decomposeSettingName(setting); const config = vscode.workspace.getConfiguration(section, { languageId: "swift" }); savedOriginalSettings[setting] = config.get(name); @@ -320,7 +320,7 @@ export async function updateSettings(settings: SettingsMap): Promise<() => Promi settings[setting] === "" ? undefined : settings[setting], vscode.ConfigurationTarget.Workspace ); - }); + } // There is actually a delay between when the config.update promise resolves and when // the setting is actually written. If we exit this function right away the test might From ffb584092ec79c175ac222c4e7ce9e24fcfb264d Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 20 Feb 2025 14:45:44 -0500 Subject: [PATCH 5/9] Refresh snippets on add/remove --- src/PackageWatcher.ts | 12 ++++++++++++ .../ui/ProjectPanelProvider.test.ts | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/PackageWatcher.ts b/src/PackageWatcher.ts index 946fe3b5e..86b4840e2 100644 --- a/src/PackageWatcher.ts +++ b/src/PackageWatcher.ts @@ -27,6 +27,7 @@ export class PackageWatcher { private packageFileWatcher?: vscode.FileSystemWatcher; private resolvedFileWatcher?: vscode.FileSystemWatcher; private workspaceStateFileWatcher?: vscode.FileSystemWatcher; + private snippetWatcher?: vscode.FileSystemWatcher; constructor( private folderContext: FolderContext, @@ -41,6 +42,7 @@ export class PackageWatcher { this.packageFileWatcher = this.createPackageFileWatcher(); this.resolvedFileWatcher = this.createResolvedFileWatcher(); this.workspaceStateFileWatcher = this.createWorkspaceStateFileWatcher(); + this.snippetWatcher = this.createSnippetFileWatcher(); } /** @@ -51,6 +53,7 @@ export class PackageWatcher { this.packageFileWatcher?.dispose(); this.resolvedFileWatcher?.dispose(); this.workspaceStateFileWatcher?.dispose(); + this.snippetWatcher?.dispose(); } private createPackageFileWatcher(): vscode.FileSystemWatcher { @@ -87,6 +90,15 @@ export class PackageWatcher { return watcher; } + private createSnippetFileWatcher(): vscode.FileSystemWatcher { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(this.folderContext.folder, "Snippets/*.swift") + ); + watcher.onDidCreate(async () => await this.handlePackageSwiftChange()); + watcher.onDidDelete(async () => await this.handlePackageSwiftChange()); + return watcher; + } + /** * Handles a create or change event for **Package.swift**. * diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index ab43a0ed0..1b447c864 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -75,6 +75,8 @@ suite("ProjectPanelProvider Test Suite", function () { "swift.debugger.useDebugAdapterFromToolchain": false, ...lldbPath, }); + + await waitForNoRunningTasks(); }); afterEach(async () => { From 301cc6717838f846756a12cce42a940b0018d987 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 20 Feb 2025 15:14:47 -0500 Subject: [PATCH 6/9] Fixup debugging in project panel tests --- assets/test/.vscode/settings.json | 6 +----- src/SwiftSnippets.ts | 3 ++- .../ui/ProjectPanelProvider.test.ts | 21 +------------------ 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/assets/test/.vscode/settings.json b/assets/test/.vscode/settings.json index 8d72543e9..db1acbde4 100644 --- a/assets/test/.vscode/settings.json +++ b/assets/test/.vscode/settings.json @@ -8,9 +8,5 @@ "-DTEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING" ], "lldb.verboseLogging": true, - "swift.backgroundCompilation": false, - "swift.debugger.useDebugAdapterFromToolchain": false, - "lldb.library": "/usr/lib/liblldb.so", - "lldb.launch.expressions": "native", - "swift.pluginPermissions": {} + "swift.backgroundCompilation": false } \ No newline at end of file diff --git a/src/SwiftSnippets.ts b/src/SwiftSnippets.ts index 0c0cb6f88..22898b94a 100644 --- a/src/SwiftSnippets.ts +++ b/src/SwiftSnippets.ts @@ -119,7 +119,8 @@ export async function debugSnippetWithOptions( ctx.buildFinished(snippetName, snippetDebugConfig, options); return result; }); - } catch { + } catch (error) { + ctx.outputChannel.appendLine(`Failed to debug snippet: ${error}`); // ignore error if task failed to run return false; } diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index 1b447c864..f8a50bc1a 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -26,8 +26,6 @@ import { updateSettings, } from "../utilities/testutilities"; import contextKeys from "../../../src/contextKeys"; -import { SwiftToolchain } from "../../../src/toolchain/toolchain"; -import { getLLDBLibPath } from "../../../src/debugger/lldb"; suite("ProjectPanelProvider Test Suite", function () { let treeProvider: ProjectPanelProvider; @@ -53,27 +51,10 @@ suite("ProjectPanelProvider Test Suite", function () { testAssets: ["targets"], }); - async function getLLDBDebugAdapterPath() { - switch (process.platform) { - case "linux": - return "/usr/lib/liblldb.so"; - case "win32": - return await (await SwiftToolchain.create()).getLLDBDebugAdapter(); - default: - return (await getLLDBLibPath(await SwiftToolchain.create())).success; - } - } - let resetSettings: (() => Promise) | undefined; beforeEach(async function () { - const lldbPath = { - "lldb.library": await getLLDBDebugAdapterPath(), - "lldb.launch.expressions": "native", - }; - resetSettings = await updateSettings({ - "swift.debugger.useDebugAdapterFromToolchain": false, - ...lldbPath, + "swift.debugger.debugAdapter": "CodeLLDB", }); await waitForNoRunningTasks(); From 1e1da9ea338c1cbcc2cf3cba69ea98feb24a66c5 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 20 Feb 2025 15:17:46 -0500 Subject: [PATCH 7/9] Fixup task test --- test/integration-tests/ui/ProjectPanelProvider.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index f8a50bc1a..98b14bc1a 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -39,7 +39,9 @@ suite("ProjectPanelProvider Test Suite", function () { await vscode.workspace.openTextDocument( path.join(folderContext.folder.fsPath, "Package.swift") ); - await executeTaskAndWaitForResult((await getBuildAllTask(folderContext)) as SwiftTask); + const buildAllTask = await getBuildAllTask(folderContext); + buildAllTask.definition.dontTriggerTestDiscovery = true; + await executeTaskAndWaitForResult(buildAllTask as SwiftTask); await folderContext.loadSwiftPlugins(); treeProvider = new ProjectPanelProvider(workspaceContext); await workspaceContext.focusFolder(folderContext); @@ -56,8 +58,6 @@ suite("ProjectPanelProvider Test Suite", function () { resetSettings = await updateSettings({ "swift.debugger.debugAdapter": "CodeLLDB", }); - - await waitForNoRunningTasks(); }); afterEach(async () => { @@ -96,6 +96,9 @@ suite("ProjectPanelProvider Test Suite", function () { }); suite("Tasks", () => { + beforeEach(async () => { + await waitForNoRunningTasks(); + }); test("Includes tasks", async () => { const tasks = await getHeaderChildren("Tasks"); const dep = tasks.find(n => n.name === "Build All (targets)") as PackageNode; From 52f93b1e1f88dc78ba2ddca5615905c9bf3fbdb2 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 21 Feb 2025 11:16:09 -0500 Subject: [PATCH 8/9] More robust project panel tasks tests on 5.10 and below --- .../commands/dependency.test.ts | 4 +- .../ui/ProjectPanelProvider.test.ts | 42 ++++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/test/integration-tests/commands/dependency.test.ts b/test/integration-tests/commands/dependency.test.ts index c12407041..771d3c8a4 100644 --- a/test/integration-tests/commands/dependency.test.ts +++ b/test/integration-tests/commands/dependency.test.ts @@ -25,8 +25,8 @@ import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider suite("Dependency Commmands Test Suite", function () { // full workflow's interaction with spm is longer than the default timeout - // 60 seconds for each test should be more than enough - this.timeout(60 * 1000); + // 120 seconds for each test should be more than enough + this.timeout(120 * 1000); let defaultContext: FolderContext; let depsContext: FolderContext; diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index 98b14bc1a..e1cb5c433 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -26,14 +26,17 @@ import { updateSettings, } from "../utilities/testutilities"; import contextKeys from "../../../src/contextKeys"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; +import { Version } from "../../../src/utilities/version"; suite("ProjectPanelProvider Test Suite", function () { + let workspaceContext: WorkspaceContext; let treeProvider: ProjectPanelProvider; this.timeout(2 * 60 * 1000); // Allow up to 2 minutes to build activateExtensionForSuite({ async setup(ctx) { - const workspaceContext = ctx; + workspaceContext = ctx; await waitForNoRunningTasks(); const folderContext = await folderInRootWorkspace("targets", workspaceContext); await vscode.workspace.openTextDocument( @@ -99,23 +102,34 @@ suite("ProjectPanelProvider Test Suite", function () { beforeEach(async () => { await waitForNoRunningTasks(); }); + + async function getBuildAllTask() { + // In Swift 5.10 and below the build tasks are disabled while other tasks that could modify .build are running. + // Typically because the extension has just started up in tests its `swift test list` that runs to gather tests + // for the test explorer. If we're running 5.10 or below, poll for the build all task for up to 60 seconds. + if (workspaceContext.toolchain.swiftVersion.isLessThan(new Version(6, 0, 0))) { + const startTime = Date.now(); + let task: PackageNode | undefined; + while (!task && Date.now() - startTime < 45 * 1000) { + const tasks = await getHeaderChildren("Tasks"); + task = tasks.find(n => n.name === "Build All (targets)") as PackageNode; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + return task; + } else { + const tasks = await getHeaderChildren("Tasks"); + return tasks.find(n => n.name === "Build All (targets)") as PackageNode; + } + } + test("Includes tasks", async () => { - const tasks = await getHeaderChildren("Tasks"); - const dep = tasks.find(n => n.name === "Build All (targets)") as PackageNode; - expect( - dep, - `Expected to find dependencies target, but instead items were ${tasks.map(n => n.name)}` - ).to.not.be.undefined; + const dep = await getBuildAllTask(); + expect(dep).to.not.be.undefined; }); test("Executes a task", async () => { - const tasks = await getHeaderChildren("Tasks"); - const taskName = "Build All (targets)"; - const task = tasks.find(n => n.name === taskName); - expect( - task, - `Expected to find task called ${taskName}, but instead items were ${tasks.map(n => n.name)}` - ).to.not.be.undefined; + const task = await getBuildAllTask(); + expect(task).to.not.be.undefined; const treeItem = task?.toTreeItem(); expect(treeItem?.command).to.not.be.undefined; expect(treeItem?.command?.arguments).to.not.be.undefined; From 12dccb464e9e1295a459979ac101ed0e846ee48e Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 21 Feb 2025 15:03:38 -0500 Subject: [PATCH 9/9] Expose testing commands to command palette --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 74163686a..97989137b 100644 --- a/package.json +++ b/package.json @@ -934,15 +934,15 @@ }, { "command": "swift.runAllTests", - "when": "false" + "when": "swift.isActivated" }, { "command": "swift.debugAllTests", - "when": "false" + "when": "swift.isActivated" }, { "command": "swift.coverAllTests", - "when": "false" + "when": "swift.isActivated" } ], "editor/context": [