diff --git a/package.json b/package.json index 55e4a6c79..53b1dad19 100644 --- a/package.json +++ b/package.json @@ -2046,9 +2046,10 @@ "check-package-json": "tsx ./scripts/check_package_json.ts", "test": "vscode-test && npm run grammar-test", "grammar-test": "vscode-tmgrammar-test test/unit-tests/**/*.test.swift.gyb -g test/unit-tests/syntaxes/swift.tmLanguage.json -g test/unit-tests/syntaxes/MagicPython.tmLanguage.json", - "integration-test": "npm test -- --label integrationTests", - "unit-test": "npm test -- --label unitTests", - "coverage": "npm test -- --coverage", + "integration-test": "npm run pretest && vscode-test --label integrationTests", + "code-workspace-test": "npm run pretest && vscode-test --label codeWorkspaceTests", + "unit-test": "npm run pretest && vscode-test --label unitTests", + "coverage": "npm run pretest && vscode-test --coverage", "compile-tests": "del-cli ./assets/test/**/.build && del-cli ./assets/test/**/.spm-cache && npm run compile", "package": "tsx ./scripts/package.ts", "dev-package": "tsx ./scripts/dev_package.ts", diff --git a/src/SwiftExtensionApi.ts b/src/SwiftExtensionApi.ts new file mode 100644 index 000000000..077c3d9f9 --- /dev/null +++ b/src/SwiftExtensionApi.ts @@ -0,0 +1,304 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; + +import { FolderContext } from "./FolderContext"; +import { TestExplorer } from "./TestExplorer/TestExplorer"; +import { FolderEvent, FolderOperation, WorkspaceContext } from "./WorkspaceContext"; +import { registerCommands } from "./commands"; +import { resolveFolderDependencies } from "./commands/dependencies/resolve"; +import { registerSourceKitSchemaWatcher } from "./commands/generateSourcekitConfiguration"; +import configuration from "./configuration"; +import { ContextKeys, createContextKeys } from "./contextKeys"; +import { registerDebugger } from "./debugger/debugAdapterFactory"; +import { makeDebugConfigurations } from "./debugger/launch"; +import { Api } from "./extension"; +import { SwiftLogger } from "./logging/SwiftLogger"; +import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory"; +import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "./terminal"; +import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher"; +import { checkForSwiftlyInstallation } from "./toolchain/swiftly"; +import { SwiftToolchain } from "./toolchain/toolchain"; +import { LanguageStatusItems } from "./ui/LanguageStatusItems"; +import { getReadOnlyDocumentProvider } from "./ui/ReadOnlyDocumentProvider"; +import { showToolchainError } from "./ui/ToolchainSelection"; +import { checkAndWarnAboutWindowsSymlinks } from "./ui/win32"; +import { getErrorDescription } from "./utilities/utilities"; +import { Version } from "./utilities/version"; + +type State = ( + | { + type: "initializing"; + promise: Promise; + cancellation: vscode.CancellationTokenSource; + } + | { type: "active"; context: WorkspaceContext; subscriptions: vscode.Disposable[] } + | { type: "failed"; error: Error } +) & { activatedBy: Error }; + +export class SwiftExtensionApi implements Api { + private state?: State; + + get workspaceContext(): WorkspaceContext | undefined { + if (this.state?.type !== "active") { + return undefined; + } + return this.state.context; + } + + contextKeys: ContextKeys; + + logger: SwiftLogger; + + constructor(private readonly extensionContext: vscode.ExtensionContext) { + this.contextKeys = createContextKeys(); + this.logger = configureLogging(extensionContext); + } + + async waitForWorkspaceContext(): Promise { + if (!this.state) { + throw new Error("The Swift extension has not been activated yet."); + } + if (this.state.type === "failed") { + throw this.state.error; + } + if (this.state.type === "active") { + return this.state.context; + } + return await this.state.promise; + } + + async withWorkspaceContext(task: (ctx: WorkspaceContext) => T | Promise): Promise { + const workspaceContext = await this.waitForWorkspaceContext(); + return await task(workspaceContext); + } + + activate(callSite?: Error): void { + if (this.state) { + throw new Error("The Swift extension has already been activated.", { + cause: this.state.activatedBy, + }); + } + + try { + this.logger.info( + `Activating Swift for Visual Studio Code ${this.extensionContext.extension.packageJSON.version}...` + ); + + checkAndWarnAboutWindowsSymlinks(this.logger); + checkForSwiftlyInstallation(this.contextKeys, this.logger); + + this.extensionContext.subscriptions.push( + new SwiftEnvironmentVariablesManager(this.extensionContext), + SwiftTerminalProfileProvider.register(), + ...registerCommands(this), + registerDebugger(this), + new SelectedXcodeWatcher(this.logger), + getReadOnlyDocumentProvider() + ); + + const activatedBy = callSite ?? Error("The extension was activated by:"); + activatedBy.name = "Activation Source"; + const tokenSource = new vscode.CancellationTokenSource(); + this.state = { + type: "initializing", + activatedBy, + cancellation: new vscode.CancellationTokenSource(), + promise: this.initializeWorkspace(tokenSource.token).then( + ({ context, subscriptions }) => { + this.state = { type: "active", activatedBy, context, subscriptions }; + return context; + }, + error => { + if (!tokenSource.token.isCancellationRequested) { + this.state = { type: "failed", activatedBy, error }; + } + throw error; + } + ), + }; + + // Mark the extension as activated. + this.contextKeys.isActivated = true; + } catch (error) { + const errorMessage = getErrorDescription(error); + // show this error message as the VS Code error message only shows when running + // the extension through the debugger + void vscode.window.showErrorMessage( + `Activating Swift extension failed: ${errorMessage}` + ); + throw error; + } + } + + private async initializeWorkspace( + token: vscode.CancellationToken + ): Promise<{ context: WorkspaceContext; subscriptions: vscode.Disposable[] }> { + const globalToolchain = await createActiveToolchain( + this.extensionContext, + this.contextKeys, + this.logger + ); + const workspaceContext = new WorkspaceContext( + this.extensionContext, + this.contextKeys, + this.logger, + globalToolchain + ); + await workspaceContext.addWorkspaceFolders(); + // project panel provider + const dependenciesView = vscode.window.createTreeView("projectPanel", { + treeDataProvider: workspaceContext.projectPanel, + showCollapseAll: true, + }); + workspaceContext.projectPanel.observeFolders(dependenciesView); + + if (token.isCancellationRequested) { + throw new Error("WorkspaceContext initialization was cancelled."); + } + return { + context: workspaceContext, + subscriptions: [ + vscode.tasks.registerTaskProvider("swift", workspaceContext.taskProvider), + vscode.tasks.registerTaskProvider("swift-plugin", workspaceContext.pluginProvider), + new LanguageStatusItems(workspaceContext), + workspaceContext.onDidChangeFolders(({ folder, operation }) => { + this.logger.info(`${operation}: ${folder?.folder.fsPath}`, folder?.name); + }), + dependenciesView, + workspaceContext.onDidChangeFolders(handleFolderEvent(this.logger)), + TestExplorer.observeFolders(workspaceContext), + registerSourceKitSchemaWatcher(workspaceContext), + ], + }; + } + + deactivate(): void { + this.contextKeys.isActivated = false; + if (this.state?.type === "initializing") { + this.state.cancellation.cancel(); + } + if (this.state?.type === "active") { + this.state.context.dispose(); + this.state.subscriptions.forEach(s => s.dispose()); + } + this.extensionContext.subscriptions.forEach(subscription => subscription.dispose()); + this.extensionContext.subscriptions.length = 0; + this.state = undefined; + } + + dispose(): void { + this.logger.dispose(); + } +} + +function configureLogging(context: vscode.ExtensionContext) { + const logger = new SwiftLoggerFactory(context.logUri).create( + "Swift", + "swift-vscode-extension.log" + ); + // Create log directory asynchronously but don't await it to avoid blocking activation + void vscode.workspace.fs + .createDirectory(context.logUri) + .then(undefined, error => logger.warn(`Failed to create log directory: ${error}`)); + return logger; +} + +function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise { + // function called when a folder is added. I broke this out so we can trigger it + // without having to await for it. + async function folderAdded(folder: FolderContext, workspace: WorkspaceContext) { + if ( + !configuration.folder(folder.workspaceFolder).disableAutoResolve || + configuration.backgroundCompilation.enabled + ) { + // if background compilation is set then run compile at startup unless + // this folder is a sub-folder of the workspace folder. This is to avoid + // kicking off compile for multiple projects at the same time + if ( + configuration.backgroundCompilation.enabled && + folder.workspaceFolder.uri === folder.folder + ) { + await folder.backgroundCompilation.runTask(); + } else { + await resolveFolderDependencies(folder, true); + } + + if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0))) { + void workspace.statusItem.showStatusWhileRunning( + `Loading Swift Plugins (${FolderContext.uriName(folder.workspaceFolder.uri)})`, + async () => { + await folder.loadSwiftPlugins(logger); + workspace.updatePluginContextKey(); + await folder.fireEvent(FolderOperation.pluginsUpdated); + } + ); + } + } + } + + return async ({ folder, operation, workspace }) => { + if (!folder) { + return; + } + + switch (operation) { + case FolderOperation.add: + // Create launch.json files based on package description. + void makeDebugConfigurations(folder); + if (await folder.swiftPackage.foundPackage) { + // do not await for this, let packages resolve in parallel + void folderAdded(folder, workspace); + } + break; + + case FolderOperation.packageUpdated: + // Create launch.json files based on package description. + await makeDebugConfigurations(folder); + if ( + (await folder.swiftPackage.foundPackage) && + !configuration.folder(folder.workspaceFolder).disableAutoResolve + ) { + await resolveFolderDependencies(folder, true); + } + break; + + case FolderOperation.resolvedUpdated: + if ( + (await folder.swiftPackage.foundPackage) && + !configuration.folder(folder.workspaceFolder).disableAutoResolve + ) { + await resolveFolderDependencies(folder, true); + } + } + }; +} + +async function createActiveToolchain( + extension: vscode.ExtensionContext, + contextKeys: ContextKeys, + logger: SwiftLogger +): Promise { + try { + const toolchain = await SwiftToolchain.create(extension.extensionPath, undefined, logger); + toolchain.logDiagnostics(logger); + contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion); + return toolchain; + } catch (error) { + if (!(await showToolchainError())) { + throw error; + } + return await createActiveToolchain(extension, contextKeys, logger); + } +} diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 30f013674..efc5a0e72 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -21,7 +21,6 @@ import { TestKind } from "./TestExplorer/TestKind"; import { TestRunManager } from "./TestExplorer/TestRunManager"; import configuration from "./configuration"; import { ContextKeys } from "./contextKeys"; -import { LLDBDebugConfigurationProvider } from "./debugger/debugAdapterFactory"; import { makeDebugConfigurations } from "./debugger/launch"; import { DocumentationManager } from "./documentation/DocumentationManager"; import { CommentCompletionProviders } from "./editor/CommentCompletion"; @@ -56,7 +55,6 @@ export class WorkspaceContext implements vscode.Disposable { public diagnostics: DiagnosticsManager; public taskProvider: SwiftTaskProvider; public pluginProvider: SwiftPluginTaskProvider; - public launchProvider: LLDBDebugConfigurationProvider; public subscriptions: vscode.Disposable[]; public commentCompletionProvider: CommentCompletionProviders; public documentation: DocumentationManager; @@ -100,7 +98,6 @@ export class WorkspaceContext implements vscode.Disposable { this.diagnostics = new DiagnosticsManager(this); this.taskProvider = new SwiftTaskProvider(this); this.pluginProvider = new SwiftPluginTaskProvider(this); - this.launchProvider = new LLDBDebugConfigurationProvider(process.platform, this, logger); this.documentation = new DocumentationManager(extensionContext, this); this.currentDocument = null; this.commentCompletionProvider = new CommentCompletionProviders(); @@ -225,7 +222,6 @@ export class WorkspaceContext implements vscode.Disposable { this.diagnostics, this.documentation, this.languageClientManager, - this.logger, this.statusItem, this.buildStatus, this.projectPanel, diff --git a/src/commands.ts b/src/commands.ts index 581d635a0..59f57b024 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -14,6 +14,7 @@ import * as path from "path"; import * as vscode from "vscode"; +import { SwiftExtensionApi } from "./SwiftExtensionApi"; import { debugSnippet, runSnippet } from "./SwiftSnippets"; import { TestKind } from "./TestExplorer/TestKind"; import { WorkspaceContext } from "./WorkspaceContext"; @@ -48,7 +49,6 @@ import { runTask } from "./commands/runTask"; import { runTest } from "./commands/runTest"; import { switchPlatform } from "./commands/switchPlatform"; import { extractTestItemsAndCount, runTestMultipleTimes } from "./commands/testMultipleTimes"; -import { SwiftLogger } from "./logging/SwiftLogger"; import { SwiftToolchain } from "./toolchain/toolchain"; import { PackageNode } from "./ui/ProjectPanelProvider"; import { showToolchainSelectionQuickPick } from "./ui/ToolchainSelection"; @@ -64,27 +64,6 @@ import { showToolchainSelectionQuickPick } from "./ui/ToolchainSelection"; export type WorkspaceContextWithToolchain = WorkspaceContext & { toolchain: SwiftToolchain }; -export function registerToolchainCommands( - ctx: WorkspaceContext | undefined, - logger: SwiftLogger -): vscode.Disposable[] { - return [ - vscode.commands.registerCommand("swift.createNewProject", () => - createNewProject(ctx?.globalToolchain) - ), - vscode.commands.registerCommand("swift.selectToolchain", () => - showToolchainSelectionQuickPick( - ctx?.currentFolder?.toolchain ?? ctx?.globalToolchain, - logger, - ctx?.currentFolder?.folder - ) - ), - vscode.commands.registerCommand("swift.pickProcess", configuration => - pickProcess(configuration) - ), - ]; -} - export enum Commands { RUN = "swift.run", DEBUG = "swift.debug", @@ -123,35 +102,50 @@ export enum Commands { /** * Registers this extension's commands in the given {@link vscode.ExtensionContext context}. */ -export function register(ctx: WorkspaceContext): vscode.Disposable[] { +export function registerCommands(api: SwiftExtensionApi): vscode.Disposable[] { return [ - vscode.commands.registerCommand( - "swift.generateLaunchConfigurations", - async () => await generateLaunchConfigurations(ctx) + vscode.commands.registerCommand("swift.createNewProject", () => + api.withWorkspaceContext(ctx => createNewProject(ctx.globalToolchain)) ), - vscode.commands.registerCommand("swift.newFile", async uri => await newSwiftFile(uri)), - vscode.commands.registerCommand( - Commands.RESOLVE_DEPENDENCIES, - async () => await resolveDependencies(ctx) + vscode.commands.registerCommand("swift.selectToolchain", () => + api.withWorkspaceContext(ctx => + showToolchainSelectionQuickPick( + ctx.currentFolder?.toolchain ?? ctx.globalToolchain, + api.logger, + ctx.currentFolder?.folder + ) + ) ), - vscode.commands.registerCommand( - Commands.UPDATE_DEPENDENCIES, - async () => await updateDependencies(ctx) + vscode.commands.registerCommand("swift.pickProcess", configuration => + pickProcess(configuration) ), - vscode.commands.registerCommand( - Commands.RUN, - async target => await runBuild(ctx, ...unwrapTreeItem(target)) + vscode.commands.registerCommand("swift.generateLaunchConfigurations", () => + api.withWorkspaceContext(ctx => generateLaunchConfigurations(ctx)) ), - vscode.commands.registerCommand( - Commands.DEBUG, - async target => await debugBuild(ctx, ...unwrapTreeItem(target)) + vscode.commands.registerCommand("swift.newFile", uri => newSwiftFile(uri)), + vscode.commands.registerCommand(Commands.RESOLVE_DEPENDENCIES, () => + api.withWorkspaceContext(ctx => resolveDependencies(ctx)) + ), + vscode.commands.registerCommand(Commands.UPDATE_DEPENDENCIES, () => + api.withWorkspaceContext(ctx => updateDependencies(ctx)) + ), + vscode.commands.registerCommand(Commands.RUN, target => + api.withWorkspaceContext(ctx => runBuild(ctx, ...unwrapTreeItem(target))) + ), + vscode.commands.registerCommand(Commands.DEBUG, target => + api.withWorkspaceContext(ctx => debugBuild(ctx, ...unwrapTreeItem(target))) + ), + vscode.commands.registerCommand(Commands.CLEAN_BUILD, () => + api.withWorkspaceContext(ctx => cleanBuild(ctx)) ), - vscode.commands.registerCommand(Commands.CLEAN_BUILD, async () => await cleanBuild(ctx)), vscode.commands.registerCommand( Commands.RUN_TESTS_MULTIPLE_TIMES, - async (...args: (vscode.TestItem | number)[]) => { + (...args: (vscode.TestItem | number)[]) => { const { testItems, count } = extractTestItemsAndCount(...args); - if (ctx.currentFolder) { + return api.withWorkspaceContext(async ctx => { + if (!ctx.currentFolder) { + return undefined; + } return await runTestMultipleTimes( ctx.currentFolder, testItems, @@ -159,14 +153,17 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { TestKind.standard, count ); - } + }); } ), vscode.commands.registerCommand( Commands.RUN_TESTS_UNTIL_FAILURE, async (...args: (vscode.TestItem | number)[]) => { const { testItems, count } = extractTestItemsAndCount(...args); - if (ctx.currentFolder) { + return api.withWorkspaceContext(async ctx => { + if (!ctx.currentFolder) { + return undefined; + } return await runTestMultipleTimes( ctx.currentFolder, testItems, @@ -174,7 +171,7 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { TestKind.standard, count ); - } + }); } ), @@ -182,7 +179,10 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { Commands.DEBUG_TESTS_MULTIPLE_TIMES, async (...args: (vscode.TestItem | number)[]) => { const { testItems, count } = extractTestItemsAndCount(...args); - if (ctx.currentFolder) { + return api.withWorkspaceContext(async ctx => { + if (!ctx.currentFolder) { + return undefined; + } return await runTestMultipleTimes( ctx.currentFolder, testItems, @@ -190,14 +190,17 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { TestKind.debug, count ); - } + }); } ), vscode.commands.registerCommand( Commands.DEBUG_TESTS_UNTIL_FAILURE, async (...args: (vscode.TestItem | number)[]) => { const { testItems, count } = extractTestItemsAndCount(...args); - if (ctx.currentFolder) { + return api.withWorkspaceContext(async ctx => { + if (!ctx.currentFolder) { + return undefined; + } return await runTestMultipleTimes( ctx.currentFolder, testItems, @@ -205,77 +208,81 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { TestKind.debug, count ); - } + }); } ), // Note: switchPlatform is only available on macOS and Swift 6.1 or later // (gated in `package.json`) because it's the only OS and toolchain combination that // has Darwin SDKs available and supports code editing with SourceKit-LSP - vscode.commands.registerCommand( - "swift.switchPlatform", - async () => await switchPlatform(ctx) + vscode.commands.registerCommand("swift.switchPlatform", () => + api.withWorkspaceContext(ctx => switchPlatform(ctx)) ), - vscode.commands.registerCommand( - Commands.RESET_PACKAGE, - async (_ /* Ignore context */, folder) => await resetPackage(ctx, folder) + vscode.commands.registerCommand(Commands.RESET_PACKAGE, (_ /* Ignore context */, folder) => + api.withWorkspaceContext(ctx => resetPackage(ctx, folder)) ), - vscode.commands.registerCommand("swift.runScript", async () => { - if (ctx && vscode.window.activeTextEditor?.document) { - await runSwiftScript( + vscode.commands.registerCommand("swift.runScript", () => + api.withWorkspaceContext(async ctx => { + if (!ctx || !vscode.window.activeTextEditor?.document) { + return undefined; + } + return await runSwiftScript( vscode.window.activeTextEditor.document, ctx.tasks, ctx.currentFolder?.toolchain ?? ctx.globalToolchain ); - } - }), - vscode.commands.registerCommand("swift.openPackage", async () => { - if (ctx.currentFolder) { - return await openPackage(ctx.currentFolder.swiftVersion, ctx.currentFolder.folder); - } - }), - vscode.commands.registerCommand( - Commands.RUN_SNIPPET, - async target => await runSnippet(ctx, ...unwrapTreeItem(target)) + }) ), - vscode.commands.registerCommand( - Commands.DEBUG_SNIPPET, - async target => await debugSnippet(ctx, ...unwrapTreeItem(target)) + vscode.commands.registerCommand("swift.openPackage", () => + api.withWorkspaceContext(async ctx => { + if (ctx.currentFolder) { + return await openPackage( + ctx.currentFolder.swiftVersion, + ctx.currentFolder.folder + ); + } + }) ), - vscode.commands.registerCommand( - Commands.RUN_PLUGIN_TASK, - async () => await runPluginTask() + vscode.commands.registerCommand(Commands.RUN_SNIPPET, target => + api.withWorkspaceContext(ctx => runSnippet(ctx, ...unwrapTreeItem(target))) ), - vscode.commands.registerCommand(Commands.RUN_TASK, async name => await runTask(ctx, name)), - vscode.commands.registerCommand( - Commands.RESTART_LSP, - async () => await restartLSPServer(ctx) + vscode.commands.registerCommand(Commands.DEBUG_SNIPPET, target => + api.withWorkspaceContext(ctx => debugSnippet(ctx, ...unwrapTreeItem(target))) ), - vscode.commands.registerCommand( - "swift.reindexProject", - async () => await reindexProject(ctx) + vscode.commands.registerCommand(Commands.RUN_PLUGIN_TASK, () => () => runPluginTask()), + vscode.commands.registerCommand(Commands.RUN_TASK, name => runTask(api, name)), + vscode.commands.registerCommand(Commands.RESTART_LSP, () => + api.withWorkspaceContext(ctx => restartLSPServer(ctx)) ), - vscode.commands.registerCommand( - "swift.insertFunctionComment", - async () => await insertFunctionComment(ctx) + vscode.commands.registerCommand("swift.reindexProject", () => + api.withWorkspaceContext(ctx => reindexProject(ctx)) ), - vscode.commands.registerCommand(Commands.USE_LOCAL_DEPENDENCY, async (item, dep) => { - if (PackageNode.isPackageNode(item)) { - return await useLocalDependency(item.name, ctx, dep); - } - }), - vscode.commands.registerCommand("swift.editDependency", async (item, folder) => { - if (PackageNode.isPackageNode(item)) { - return await editDependency(item.name, ctx, folder); - } - }), - vscode.commands.registerCommand(Commands.UNEDIT_DEPENDENCY, async (item, folder) => { - if (PackageNode.isPackageNode(item)) { - return await uneditDependency(item.name, ctx, folder); - } - }), - vscode.commands.registerCommand("swift.openInWorkspace", async item => { + vscode.commands.registerCommand("swift.insertFunctionComment", () => + api.withWorkspaceContext(ctx => insertFunctionComment(ctx)) + ), + vscode.commands.registerCommand(Commands.USE_LOCAL_DEPENDENCY, (item, dep) => + api.withWorkspaceContext(async ctx => { + if (PackageNode.isPackageNode(item)) { + return await useLocalDependency(item.name, ctx, dep); + } + }) + ), + vscode.commands.registerCommand("swift.editDependency", (item, folder) => + api.withWorkspaceContext(async ctx => { + if (PackageNode.isPackageNode(item)) { + return await editDependency(item.name, ctx, folder); + } + }) + ), + vscode.commands.registerCommand(Commands.UNEDIT_DEPENDENCY, (item, folder) => + api.withWorkspaceContext(async ctx => { + if (PackageNode.isPackageNode(item)) { + return await uneditDependency(item.name, ctx, folder); + } + }) + ), + vscode.commands.registerCommand("swift.openInWorkspace", item => { if (PackageNode.isPackageNode(item)) { - return await openInWorkspace(item); + return openInWorkspace(item); } }), vscode.commands.registerCommand("swift.openExternal", item => { @@ -285,49 +292,48 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { }), vscode.commands.registerCommand("swift.attachDebugger", attachDebugger), vscode.commands.registerCommand("swift.clearDiagnosticsCollection", () => - ctx.diagnostics.clear() + api.withWorkspaceContext(ctx => ctx.diagnostics.clear()) ), - vscode.commands.registerCommand( - "swift.captureDiagnostics", - async () => await captureDiagnostics(ctx) + vscode.commands.registerCommand("swift.captureDiagnostics", () => + api.withWorkspaceContext(ctx => captureDiagnostics(ctx)) ), - vscode.commands.registerCommand( - Commands.RUN_ALL_TESTS_PARALLEL, - async item => await runAllTests(ctx, TestKind.parallel, ...unwrapTreeItem(item)) + vscode.commands.registerCommand(Commands.RUN_ALL_TESTS_PARALLEL, item => + api.withWorkspaceContext(ctx => + runAllTests(ctx, TestKind.parallel, ...unwrapTreeItem(item)) + ) ), - vscode.commands.registerCommand( - Commands.RUN_ALL_TESTS, - async item => await runAllTests(ctx, TestKind.standard, ...unwrapTreeItem(item)) + vscode.commands.registerCommand(Commands.RUN_ALL_TESTS, item => + api.withWorkspaceContext(ctx => + 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.DEBUG_ALL_TESTS, item => + api.withWorkspaceContext(ctx => + 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.COVER_ALL_TESTS, item => + api.withWorkspaceContext(ctx => + runAllTests(ctx, TestKind.coverage, ...unwrapTreeItem(item)) + ) ), - vscode.commands.registerCommand( - Commands.RUN_TEST, - async item => await runTest(ctx, TestKind.standard, item) + vscode.commands.registerCommand(Commands.RUN_TEST, item => + api.withWorkspaceContext(ctx => runTest(ctx, TestKind.standard, item)) ), - vscode.commands.registerCommand( - Commands.DEBUG_TEST, - async item => await runTest(ctx, TestKind.debug, item) + vscode.commands.registerCommand(Commands.DEBUG_TEST, item => + api.withWorkspaceContext(ctx => runTest(ctx, TestKind.debug, item)) ), - vscode.commands.registerCommand( - Commands.RUN_TEST_WITH_COVERAGE, - async item => await runTest(ctx, TestKind.coverage, item) + vscode.commands.registerCommand(Commands.RUN_TEST_WITH_COVERAGE, item => + api.withWorkspaceContext(ctx => runTest(ctx, TestKind.coverage, item)) ), - vscode.commands.registerCommand( - Commands.PREVIEW_DOCUMENTATION, - async () => await ctx.documentation.launchDocumentationPreview() + vscode.commands.registerCommand(Commands.PREVIEW_DOCUMENTATION, () => + api.withWorkspaceContext(ctx => ctx.documentation.launchDocumentationPreview()) ), vscode.commands.registerCommand(Commands.SHOW_FLAT_DEPENDENCIES_LIST, () => - updateDependenciesViewList(ctx, true) + api.withWorkspaceContext(ctx => updateDependenciesViewList(ctx, true)) ), vscode.commands.registerCommand(Commands.SHOW_NESTED_DEPENDENCIES_LIST, () => - updateDependenciesViewList(ctx, false) + api.withWorkspaceContext(ctx => updateDependenciesViewList(ctx, false)) ), vscode.commands.registerCommand("swift.openEducationalNote", uri => openEducationalNote(uri) @@ -337,9 +343,8 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(packagePath)); }), vscode.commands.registerCommand("swift.openDocumentation", () => openDocumentation()), - vscode.commands.registerCommand( - Commands.GENERATE_SOURCEKIT_CONFIG, - async () => await generateSourcekitConfiguration(ctx) + vscode.commands.registerCommand(Commands.GENERATE_SOURCEKIT_CONFIG, () => + api.withWorkspaceContext(ctx => generateSourcekitConfiguration(ctx)) ), vscode.commands.registerCommand( "swift.showCommands", @@ -354,13 +359,11 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { "@ext:swiftlang.swift-vscode " ) ), - vscode.commands.registerCommand( - Commands.INSTALL_SWIFTLY_TOOLCHAIN, - async () => await promptToInstallSwiftlyToolchain(ctx, "stable") + vscode.commands.registerCommand(Commands.INSTALL_SWIFTLY_TOOLCHAIN, () => + api.withWorkspaceContext(ctx => promptToInstallSwiftlyToolchain(ctx, "stable")) ), - vscode.commands.registerCommand( - Commands.INSTALL_SWIFTLY_SNAPSHOT_TOOLCHAIN, - async () => await promptToInstallSwiftlyToolchain(ctx, "snapshot") + vscode.commands.registerCommand(Commands.INSTALL_SWIFTLY_SNAPSHOT_TOOLCHAIN, () => + api.withWorkspaceContext(ctx => promptToInstallSwiftlyToolchain(ctx, "snapshot")) ), ]; } diff --git a/src/commands/runTask.ts b/src/commands/runTask.ts index e74e5bd73..6ed1d5813 100644 --- a/src/commands/runTask.ts +++ b/src/commands/runTask.ts @@ -13,30 +13,32 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { WorkspaceContext } from "../WorkspaceContext"; +import { Api } from "../extension"; import { TaskOperation } from "../tasks/TaskQueue"; -export const runTask = async (ctx: WorkspaceContext, name: string) => { - if (!ctx.currentFolder) { - return; - } +export function runTask(api: Api, name: string): Promise { + return api.withWorkspaceContext(async ctx => { + if (!ctx.currentFolder) { + return false; + } - const tasks = await vscode.tasks.fetchTasks(); - let task = tasks.find(task => task.name === name); - if (!task) { - const pluginTaskProvider = ctx.pluginProvider; - const pluginTasks = await pluginTaskProvider.provideTasks( - new vscode.CancellationTokenSource().token - ); - task = pluginTasks.find(task => task.name === name); - } + const tasks = await vscode.tasks.fetchTasks(); + let task = tasks.find(task => task.name === name); + if (!task) { + const pluginTaskProvider = ctx.pluginProvider; + const pluginTasks = await pluginTaskProvider.provideTasks( + new vscode.CancellationTokenSource().token + ); + task = pluginTasks.find(task => task.name === name); + } - if (!task) { - void vscode.window.showErrorMessage(`Task "${name}" not found`); - return; - } + if (!task) { + void vscode.window.showErrorMessage(`Task "${name}" not found`); + return false; + } - return ctx.currentFolder.taskQueue - .queueOperation(new TaskOperation(task)) - .then(result => result === 0); -}; + return ctx.currentFolder.taskQueue + .queueOperation(new TaskOperation(task)) + .then(result => result === 0); + }); +} diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index eca0a3f83..bbc3986a1 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -14,9 +14,8 @@ import * as path from "path"; import * as vscode from "vscode"; -import { WorkspaceContext } from "../WorkspaceContext"; import configuration from "../configuration"; -import { SwiftLogger } from "../logging/SwiftLogger"; +import { Api } from "../extension"; import { SwiftToolchain } from "../toolchain/toolchain"; import { fileExists } from "../utilities/filesystem"; import { getErrorDescription, swiftRuntimeEnv } from "../utilities/utilities"; @@ -28,10 +27,10 @@ import { registerLoggingDebugAdapterTracker } from "./logTracker"; /** * Registers the active debugger with the extension, and reregisters it * when the debugger settings change. - * @param workspaceContext The workspace context + * @param api The Swift extension API * @returns A disposable to be disposed when the extension is deactivated */ -export function registerDebugger(workspaceContext: WorkspaceContext): vscode.Disposable { +export function registerDebugger(api: Api): vscode.Disposable { let subscriptions: vscode.Disposable[] = []; // Monitor the swift.debugger.disable setting and register automatically @@ -48,7 +47,7 @@ export function registerDebugger(workspaceContext: WorkspaceContext): vscode.Dis function register() { subscriptions.push(registerLoggingDebugAdapterTracker()); - subscriptions.push(registerLLDBDebugAdapter(workspaceContext)); + subscriptions.push(registerLLDBDebugAdapter(api)); } if (!configuration.debugger.disable) { @@ -65,13 +64,13 @@ export function registerDebugger(workspaceContext: WorkspaceContext): vscode.Dis /** * Registers the LLDB debug adapter with the VS Code debug adapter descriptor factory. - * @param workspaceContext The workspace context + * @param api The Swift extension API * @returns A disposable to be disposed when the extension is deactivated */ -function registerLLDBDebugAdapter(workspaceContext: WorkspaceContext): vscode.Disposable { +function registerLLDBDebugAdapter(api: Api): vscode.Disposable { return vscode.debug.registerDebugConfigurationProvider( SWIFT_LAUNCH_CONFIG_TYPE, - workspaceContext.launchProvider + new LLDBDebugConfigurationProvider(process.platform, api) ); } @@ -87,18 +86,19 @@ function registerLLDBDebugAdapter(workspaceContext: WorkspaceContext): vscode.Di export class LLDBDebugConfigurationProvider implements vscode.DebugConfigurationProvider { constructor( private platform: NodeJS.Platform, - private workspaceContext: WorkspaceContext, - private logger: SwiftLogger + private api: Api ) {} async resolveDebugConfigurationWithSubstitutedVariables( folder: vscode.WorkspaceFolder | undefined, launchConfig: vscode.DebugConfiguration ): Promise { - const folderContext = this.workspaceContext.folders.find( + const promise = this.api.waitForWorkspaceContext(); + const workspaceContext = await promise; + const folderContext = workspaceContext.folders.find( f => f.workspaceFolder.uri.fsPath === folder?.uri.fsPath ); - const toolchain = folderContext?.toolchain ?? this.workspaceContext.globalToolchain; + const toolchain = folderContext?.toolchain ?? workspaceContext.globalToolchain; // "launch" requests must have either a "target" or "program" property if ( @@ -250,7 +250,7 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration void vscode.window.showWarningMessage( `Failed to setup CodeLLDB for debugging of Swift code. Debugging may produce unexpected results. ${errorMessage}` ); - this.logger.error(`Failed to setup CodeLLDB: ${errorMessage}`); + this.api.logger.error(`Failed to setup CodeLLDB: ${errorMessage}`); return; } const libLldbPath = libLldbPathResult.success; diff --git a/src/extension.ts b/src/extension.ts index cd2727ca4..60000fdc3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,299 +16,91 @@ import "source-map-support/register"; import * as vscode from "vscode"; -import { FolderContext } from "./FolderContext"; -import { TestExplorer } from "./TestExplorer/TestExplorer"; -import { FolderEvent, FolderOperation, WorkspaceContext } from "./WorkspaceContext"; -import * as commands from "./commands"; -import { resolveFolderDependencies } from "./commands/dependencies/resolve"; -import { registerSourceKitSchemaWatcher } from "./commands/generateSourcekitConfiguration"; -import configuration, { handleConfigurationChangeEvent } from "./configuration"; -import { ContextKeys, createContextKeys } from "./contextKeys"; -import { registerDebugger } from "./debugger/debugAdapterFactory"; -import * as debug from "./debugger/launch"; +import { SwiftExtensionApi } from "./SwiftExtensionApi"; +import { WorkspaceContext } from "./WorkspaceContext"; +import { ContextKeys } from "./contextKeys"; import { SwiftLogger } from "./logging/SwiftLogger"; -import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory"; -import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "./terminal"; -import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher"; -import { checkForSwiftlyInstallation } from "./toolchain/swiftly"; -import { SwiftToolchain } from "./toolchain/toolchain"; -import { LanguageStatusItems } from "./ui/LanguageStatusItems"; -import { getReadOnlyDocumentProvider } from "./ui/ReadOnlyDocumentProvider"; -import { showToolchainError } from "./ui/ToolchainSelection"; -import { checkAndWarnAboutWindowsSymlinks } from "./ui/win32"; -import { getErrorDescription } from "./utilities/utilities"; -import { Version } from "./utilities/version"; /** * External API as exposed by the extension. Can be queried by other extensions * or by the integration test runner for VS Code extensions. */ export interface Api { + /** + * The {@link WorkspaceContext} if it is currently available. + * + * The Swift extension starting in 2.16.0 delays workspace initialization in order to + * speed up activation. Use {@link waitForWorkspaceContext} or {@link withWorkspaceContext} + * to wait for the workspace to be initialized. + */ workspaceContext?: WorkspaceContext; + + /** + * Can be used to query for the Swift extension's [context keys](https://code.visualstudio.com/api/references/when-clause-contexts#add-a-custom-when-clause-context). + * + * **DO NOT** edit these context keys outside of the Swift extension. This will cause + * the extension to not behave correctly. + */ + contextKeys: ContextKeys; + + /** + * The {@link SwiftLogger} used by the extension to log behavior. + */ logger: SwiftLogger; - activate(): Promise; - deactivate(): Promise; + + /** + * Waits for workspace initialization to complete and returns the {@link WorkspaceContext}. + */ + waitForWorkspaceContext(): Promise; + + /** + * Waits for workspace initialization to complete and executes the provided task, passing + * in the {@link WorkspaceContext}. + * + * @param task The task to execute after the workspace has finished initialization. + * @param token An optional cancellation token used to cancel the task. + */ + withWorkspaceContext( + task: (ctx: WorkspaceContext) => T | Promise, + token?: vscode.CancellationToken + ): Promise; + + /** + * Activate the extension. + * + * **DO NOT** use this method directly. It is exposed for testing purposes only. + * + * @param callSite An optional call site used to determine where the extension was activated from. + */ + activate(callSite?: Error): void; + + /** + * Deactivate the extension. + * + * **DO NOT** use this method directly. It is exposed for testing purposes only. + */ + deactivate(): void; + + /** + * Dispose of the API. + * + * **DO NOT** use this method directly. It is exposed for testing purposes only. + */ + dispose(): void; } +let extensionApi: Api | undefined = undefined; + /** * Activate the extension. This is the main entry point. */ export async function activate(context: vscode.ExtensionContext): Promise { - const activationStartTime = Date.now(); - try { - const logSetupStartTime = Date.now(); - const logger = configureLogging(context); - const logSetupElapsed = Date.now() - logSetupStartTime; - logger.info( - `Activating Swift for Visual Studio Code ${context.extension.packageJSON.version}...` - ); - logger.info(`Log setup completed in ${logSetupElapsed}ms`); - - const preToolchainStartTime = Date.now(); - checkAndWarnAboutWindowsSymlinks(logger); - - const contextKeys = createContextKeys(); - const preToolchainElapsed = Date.now() - preToolchainStartTime; - const toolchainStartTime = Date.now(); - const toolchain = await createActiveToolchain(context, contextKeys, logger); - const toolchainElapsed = Date.now() - toolchainStartTime; - - const swiftlyCheckStartTime = Date.now(); - checkForSwiftlyInstallation(contextKeys, logger); - const swiftlyCheckElapsed = Date.now() - swiftlyCheckStartTime; - - // If we don't have a toolchain, show an error and stop initializing the extension. - // This can happen if the user has not installed Swift or if the toolchain is not - // properly configured. - if (!toolchain) { - // In order to select a toolchain we need to register the command first. - const subscriptions = commands.registerToolchainCommands(undefined, logger); - const chosenRemediation = await showToolchainError(); - subscriptions.forEach(sub => sub.dispose()); - - // If they tried to fix the improperly configured toolchain, re-initialize the extension. - if (chosenRemediation) { - return activate(context); - } else { - return { - workspaceContext: undefined, - logger, - activate: () => activate(context), - deactivate: async () => { - await deactivate(context); - }, - }; - } - } - - const workspaceContextStartTime = Date.now(); - const workspaceContext = new WorkspaceContext(context, contextKeys, logger, toolchain); - context.subscriptions.push(workspaceContext); - const workspaceContextElapsed = Date.now() - workspaceContextStartTime; - - const subscriptionsStartTime = Date.now(); - context.subscriptions.push(new SwiftEnvironmentVariablesManager(context)); - context.subscriptions.push(SwiftTerminalProfileProvider.register()); - context.subscriptions.push( - ...commands.registerToolchainCommands(workspaceContext, workspaceContext.logger) - ); - - // Watch for configuration changes the trigger a reload of the extension if necessary. - context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration( - handleConfigurationChangeEvent(workspaceContext) - ) - ); - - context.subscriptions.push(...commands.register(workspaceContext)); - context.subscriptions.push(registerDebugger(workspaceContext)); - context.subscriptions.push(new SelectedXcodeWatcher(logger)); - - // Register task provider. - context.subscriptions.push( - vscode.tasks.registerTaskProvider("swift", workspaceContext.taskProvider) - ); - - // Register swift plugin task provider. - context.subscriptions.push( - vscode.tasks.registerTaskProvider("swift-plugin", workspaceContext.pluginProvider) - ); - - // Register the language status bar items. - context.subscriptions.push(new LanguageStatusItems(workspaceContext)); - - // swift module document provider - context.subscriptions.push(getReadOnlyDocumentProvider()); - - // observer for logging workspace folder addition/removal - context.subscriptions.push( - workspaceContext.onDidChangeFolders(({ folder, operation }) => { - logger.info(`${operation}: ${folder?.folder.fsPath}`, folder?.name); - }) - ); - - // project panel provider - const dependenciesView = vscode.window.createTreeView("projectPanel", { - treeDataProvider: workspaceContext.projectPanel, - showCollapseAll: true, - }); - workspaceContext.projectPanel.observeFolders(dependenciesView); - - context.subscriptions.push(dependenciesView); - - // observer that will resolve package and build launch configurations - context.subscriptions.push(workspaceContext.onDidChangeFolders(handleFolderEvent(logger))); - context.subscriptions.push(TestExplorer.observeFolders(workspaceContext)); - - context.subscriptions.push(registerSourceKitSchemaWatcher(workspaceContext)); - const subscriptionsElapsed = Date.now() - subscriptionsStartTime; - - // setup workspace context with initial workspace folders - const workspaceFoldersStartTime = Date.now(); - await workspaceContext.addWorkspaceFolders(); - const workspaceFoldersElapsed = Date.now() - workspaceFoldersStartTime; - - const finalStepsStartTime = Date.now(); - // Mark the extension as activated. - contextKeys.isActivated = true; - const finalStepsElapsed = Date.now() - finalStepsStartTime; - - const totalActivationTime = Date.now() - activationStartTime; - logger.info( - `Extension activation completed in ${totalActivationTime}ms (log-setup: ${logSetupElapsed}ms, pre-toolchain: ${preToolchainElapsed}ms, toolchain: ${toolchainElapsed}ms, swiftly-check: ${swiftlyCheckElapsed}ms, workspace-context: ${workspaceContextElapsed}ms, subscriptions: ${subscriptionsElapsed}ms, workspace-folders: ${workspaceFoldersElapsed}ms, final-steps: ${finalStepsElapsed}ms)` - ); - - return { - workspaceContext, - logger, - activate: () => activate(context), - deactivate: async () => { - await workspaceContext.stop(); - await deactivate(context); - }, - }; - } catch (error) { - const errorMessage = getErrorDescription(error); - // show this error message as the VS Code error message only shows when running - // the extension through the debugger - void vscode.window.showErrorMessage(`Activating Swift extension failed: ${errorMessage}`); - throw error; - } -} - -function configureLogging(context: vscode.ExtensionContext) { - // Create log directory asynchronously but don't await it to avoid blocking activation - const logDirPromise = vscode.workspace.fs.createDirectory(context.logUri); - - const logger = new SwiftLoggerFactory(context.logUri).create( - "Swift", - "swift-vscode-extension.log" - ); - context.subscriptions.push(logger); - - void Promise.resolve(logDirPromise) - .then(() => { - // File transport will be added when directory is ready - }) - .catch((error: unknown) => { - logger.warn(`Failed to create log directory: ${error}`); - }); - return logger; -} - -function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise { - // function called when a folder is added. I broke this out so we can trigger it - // without having to await for it. - async function folderAdded(folder: FolderContext, workspace: WorkspaceContext) { - if ( - !configuration.folder(folder.workspaceFolder).disableAutoResolve || - configuration.backgroundCompilation.enabled - ) { - // if background compilation is set then run compile at startup unless - // this folder is a sub-folder of the workspace folder. This is to avoid - // kicking off compile for multiple projects at the same time - if ( - configuration.backgroundCompilation.enabled && - folder.workspaceFolder.uri === folder.folder - ) { - await folder.backgroundCompilation.runTask(); - } else { - await resolveFolderDependencies(folder, true); - } - - if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0))) { - void workspace.statusItem.showStatusWhileRunning( - `Loading Swift Plugins (${FolderContext.uriName(folder.workspaceFolder.uri)})`, - async () => { - await folder.loadSwiftPlugins(logger); - workspace.updatePluginContextKey(); - await folder.fireEvent(FolderOperation.pluginsUpdated); - } - ); - } - } - } - - return async ({ folder, operation, workspace }) => { - if (!folder) { - return; - } - - switch (operation) { - case FolderOperation.add: - // Create launch.json files based on package description, don't block execution. - void debug.makeDebugConfigurations(folder); - - if (await folder.swiftPackage.foundPackage) { - // do not await for this, let packages resolve in parallel - void folderAdded(folder, workspace); - } - break; - - case FolderOperation.packageUpdated: - // Create launch.json files based on package description, don't block execution. - void debug.makeDebugConfigurations(folder); - - if ( - (await folder.swiftPackage.foundPackage) && - !configuration.folder(folder.workspaceFolder).disableAutoResolve - ) { - await resolveFolderDependencies(folder, true); - } - break; - - case FolderOperation.resolvedUpdated: - if ( - (await folder.swiftPackage.foundPackage) && - !configuration.folder(folder.workspaceFolder).disableAutoResolve - ) { - await resolveFolderDependencies(folder, true); - } - } - }; -} - -async function createActiveToolchain( - extension: vscode.ExtensionContext, - contextKeys: ContextKeys, - logger: SwiftLogger -): Promise { - try { - const toolchain = await SwiftToolchain.create(extension.extensionPath, undefined, logger); - toolchain.logDiagnostics(logger); - contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion); - return toolchain; - } catch (error) { - logger.error(`Failed to discover Swift toolchain: ${error}`); - return undefined; - } + extensionApi = new SwiftExtensionApi(context); + extensionApi.activate(); + return extensionApi; } -async function deactivate(context: vscode.ExtensionContext): Promise { - const workspaceContext = (context.extension.exports as Api).workspaceContext; - if (workspaceContext) { - workspaceContext.contextKeys.isActivated = false; - } - context.subscriptions.forEach(subscription => subscription.dispose()); - context.subscriptions.length = 0; +export function deactivate(): void { + extensionApi?.deactivate(); + extensionApi?.dispose(); } diff --git a/src/logging/SwiftLoggerFactory.ts b/src/logging/SwiftLoggerFactory.ts index a4f3fa2e8..7b13e20eb 100644 --- a/src/logging/SwiftLoggerFactory.ts +++ b/src/logging/SwiftLoggerFactory.ts @@ -34,7 +34,7 @@ export class SwiftLoggerFactory { } /** - * This is mainly only intended for testing purposes + * This is only intended for testing purposes */ async temp(name: string): Promise { const folder = await TemporaryFolder.create(); diff --git a/test/MockUtils.ts b/test/MockUtils.ts index 66c44d568..e65fd9a63 100644 --- a/test/MockUtils.ts +++ b/test/MockUtils.ts @@ -147,6 +147,11 @@ export function mockObject(overrides: Partial): MockedObject { const clonedObject = replaceWithMocks(overrides); function checkAndAcquireValueFromTarget(target: any, property: string | symbol): any { if (!Object.prototype.hasOwnProperty.call(target, property)) { + if (property === "then") { + // Utilities that check for promise-like objects expect that accessing a + // property won't result in an error. + return undefined; + } throw new Error( `Attempted to access property '${String(property)}', but it was not mocked.` ); diff --git a/test/integration-tests/BackgroundCompilation.test.ts b/test/integration-tests/BackgroundCompilation.test.ts index 3237158cc..a94aa4597 100644 --- a/test/integration-tests/BackgroundCompilation.test.ts +++ b/test/integration-tests/BackgroundCompilation.test.ts @@ -35,27 +35,25 @@ tag("large").suite("BackgroundCompilation Test Suite", () => { let folderContext: FolderContext; let buildAllTask: vscode.Task; - async function setupFolder(ctx: WorkspaceContext) { - workspaceContext = ctx; - folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext); - buildAllTask = await getBuildAllTask(folderContext); - } - - suite("build all on save", () => { - let subscriptions: vscode.Disposable[]; - - activateExtensionForSuite({ - async setup(ctx) { - subscriptions = []; - await setupFolder(ctx); + activateExtensionForSuite({ + async setup(api) { + return await api.withWorkspaceContext(async ctx => { + workspaceContext = ctx; + folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext); + buildAllTask = await getBuildAllTask(folderContext); return await updateSettings({ "swift.backgroundCompilation": true, }); - }, - }); + }); + }, + }); + + suite("build all on save", () => { + let subscriptions: vscode.Disposable[] = []; - suiteTeardown(async () => { + teardown(async () => { subscriptions.forEach(s => s.dispose()); + subscriptions = []; await closeAllEditors(); }); @@ -88,44 +86,42 @@ tag("large").suite("BackgroundCompilation Test Suite", () => { let backgroundConfiguration: BackgroundCompilation; suite("useDefaultTask", () => { - activateExtensionForSuite({ - async setup(ctx) { - await setupFolder(ctx); - nonSwiftTask = new vscode.Task( - { - type: "shell", - command: ["swift"], - args: ["build"], - group: { - id: "build", - isDefault: true, - }, - label: "shell build", - }, - folderContext.workspaceFolder, - "shell build", - "Workspace", - new vscode.ShellExecution("", { - cwd: testAssetUri("defaultPackage").fsPath, - }) - ); - swiftTask = createSwiftTask( - ["build"], - "swift build", - { - cwd: testAssetUri("defaultPackage"), - scope: folderContext.workspaceFolder, + suiteSetup(async () => { + nonSwiftTask = new vscode.Task( + { + type: "shell", + command: ["swift"], + args: ["build"], + group: { + id: "build", + isDefault: true, }, - folderContext.toolchain - ); - swiftTask.source = "Workspace"; - return await updateSettings({ - "swift.backgroundCompilation": { - enabled: true, - useDefaultTask: true, - }, - }); - }, + label: "shell build", + }, + folderContext.workspaceFolder, + "shell build", + "Workspace", + new vscode.ShellExecution("", { + cwd: testAssetUri("defaultPackage").fsPath, + }) + ); + swiftTask = createSwiftTask( + ["build"], + "swift build", + { + cwd: testAssetUri("defaultPackage"), + scope: folderContext.workspaceFolder, + }, + folderContext.toolchain + ); + swiftTask.source = "Workspace"; + // Restoring settings will be handled by the top level suite's setup() function. + await updateSettings({ + "swift.backgroundCompilation": { + enabled: true, + useDefaultTask: true, + }, + }); }); setup(() => { diff --git a/test/integration-tests/DiagnosticsManager.test.ts b/test/integration-tests/DiagnosticsManager.test.ts index fc541f8c0..106d23223 100644 --- a/test/integration-tests/DiagnosticsManager.test.ts +++ b/test/integration-tests/DiagnosticsManager.test.ts @@ -127,7 +127,8 @@ tag("medium").suite("DiagnosticsManager Test Suite", function () { }; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; toolchain = workspaceContext.globalToolchain; folderContext = await folderInRootWorkspace("diagnostics", workspaceContext); diff --git a/test/integration-tests/ExtensionActivation.test.ts b/test/integration-tests/ExtensionActivation.test.ts index 14ccfc21f..4f48319a4 100644 --- a/test/integration-tests/ExtensionActivation.test.ts +++ b/test/integration-tests/ExtensionActivation.test.ts @@ -33,21 +33,21 @@ tag("medium").suite("Extension Activation/Deactivation Tests", () => { await deactivateExtension(); }); - async function activate(currentTest?: Mocha.Test) { - assert.ok(await activateExtension(currentTest), "Extension did not return its API"); + async function activate() { + assert.ok(await activateExtension(), "Extension did not return its API"); const ext = vscode.extensions.getExtension("swiftlang.swift-vscode"); assert.ok(ext, "Extension is not found"); assert.strictEqual(ext.isActive, true); } test("Activation", async function () { - await activate(this.test as Mocha.Test); + await activate(); }); test("Duplicate Activation", async function () { - await activate(this.test as Mocha.Test); + await activate(); // eslint-disable-next-line @typescript-eslint/no-floating-promises - assert.rejects(activateExtension(this.test as Mocha.Test), err => { + assert.rejects(activateExtension(), err => { const msg = (err as unknown as any).message; return ( msg.includes("Extension is already activated") && @@ -58,18 +58,19 @@ tag("medium").suite("Extension Activation/Deactivation Tests", () => { }); test("Deactivation", async function () { - const workspaceContext = await activateExtension(this.test as Mocha.Test); + const api = await activateExtension(); await deactivateExtension(); const ext = vscode.extensions.getExtension("swiftlang.swift-vscode"); assert(ext); - assert.equal(workspaceContext.subscriptions.length, 0); + assert.equal(api.workspaceContext, undefined); }); suite("Extension Activation per suite", () => { let workspaceContext: WorkspaceContext | undefined; let capturedWorkspaceContext: WorkspaceContext | undefined; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; }, }); @@ -88,7 +89,8 @@ tag("medium").suite("Extension Activation/Deactivation Tests", () => { let workspaceContext: WorkspaceContext | undefined; let capturedWorkspaceContext: WorkspaceContext | undefined; activateExtensionForTest({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; }, }); @@ -107,7 +109,8 @@ tag("medium").suite("Extension Activation/Deactivation Tests", () => { let workspaceContext: WorkspaceContext; activateExtensionForTest({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; }, testAssets: ["cmake", "cmake-compile-flags"], diff --git a/test/integration-tests/FolderContext.test.ts b/test/integration-tests/FolderContext.test.ts index c1e624486..5a5482db5 100644 --- a/test/integration-tests/FolderContext.test.ts +++ b/test/integration-tests/FolderContext.test.ts @@ -31,15 +31,15 @@ suite("FolderContext Error Handling Test Suite", () => { const showToolchainError = mockGlobalValue(toolchain, "showToolchainError"); activateExtensionForSuite({ - async setup(ctx) { - workspaceContext = ctx; - this.timeout(60000); + async setup(api) { + workspaceContext = await api.waitForWorkspaceContext(); }, testAssets: ["defaultPackage"], }); afterEach(() => { folderContext?.dispose(); + workspaceContext.logger.clear(); restore(); }); diff --git a/test/integration-tests/SwiftSnippet.test.ts b/test/integration-tests/SwiftSnippet.test.ts index d644126f8..ffe99a807 100644 --- a/test/integration-tests/SwiftSnippet.test.ts +++ b/test/integration-tests/SwiftSnippet.test.ts @@ -42,7 +42,8 @@ tag("large").suite("SwiftSnippet Test Suite", function () { let resetSettings: (() => Promise) | undefined; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; const folder = await folderInRootWorkspace("defaultPackage", workspaceContext); diff --git a/test/integration-tests/WorkspaceContext.test.ts b/test/integration-tests/WorkspaceContext.test.ts index 6dfe8dc6d..68e00a3e9 100644 --- a/test/integration-tests/WorkspaceContext.test.ts +++ b/test/integration-tests/WorkspaceContext.test.ts @@ -48,7 +48,8 @@ tag("medium").suite("WorkspaceContext Test Suite", () => { suite("Folder Events", () => { activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; }, // No default assets as we want to verify against a clean workspace. @@ -95,7 +96,8 @@ tag("medium").suite("WorkspaceContext Test Suite", () => { suite("Tasks", function () { activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; }, }); @@ -193,7 +195,8 @@ tag("medium").suite("WorkspaceContext Test Suite", () => { suite("Toolchain", function () { activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; }, }); diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts index 0c25958f1..ccb10d418 100644 --- a/test/integration-tests/commands/build.test.ts +++ b/test/integration-tests/commands/build.test.ts @@ -36,7 +36,8 @@ tag("large").suite("Build Commands", function () { ]; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); // The description of this package is crashing on Windows with Swift 5.9.x and below if ( process.platform === "win32" && diff --git a/test/integration-tests/commands/captureDiagnostics.test.ts b/test/integration-tests/commands/captureDiagnostics.test.ts index dbc1e7408..5adc1415e 100644 --- a/test/integration-tests/commands/captureDiagnostics.test.ts +++ b/test/integration-tests/commands/captureDiagnostics.test.ts @@ -36,8 +36,8 @@ tag("medium").suite("captureDiagnostics Test Suite", () => { suite("Minimal", () => { activateExtensionForSuite({ - async setup(ctx) { - workspaceContext = ctx; + async setup(api) { + workspaceContext = await api.waitForWorkspaceContext(); }, testAssets: ["defaultPackage"], }); @@ -92,8 +92,8 @@ tag("medium").suite("captureDiagnostics Test Suite", () => { tag("large").suite("Full", function () { activateExtensionForSuite({ - async setup(ctx) { - workspaceContext = ctx; + async setup(api) { + workspaceContext = await api.waitForWorkspaceContext(); }, testAssets: ["defaultPackage"], }); diff --git a/test/integration-tests/commands/dependency.test.ts b/test/integration-tests/commands/dependency.test.ts index b2067506b..42522af6a 100644 --- a/test/integration-tests/commands/dependency.test.ts +++ b/test/integration-tests/commands/dependency.test.ts @@ -32,7 +32,8 @@ tag("large").suite("Dependency Commands Test Suite", function () { let workspaceContext: WorkspaceContext; activateExtensionForTest({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; depsContext = findWorkspaceFolder("dependencies", workspaceContext)!; }, diff --git a/test/integration-tests/commands/generateSourcekitConfiguration.test.ts b/test/integration-tests/commands/generateSourcekitConfiguration.test.ts index e581e8bfe..433750317 100644 --- a/test/integration-tests/commands/generateSourcekitConfiguration.test.ts +++ b/test/integration-tests/commands/generateSourcekitConfiguration.test.ts @@ -49,7 +49,8 @@ suite("Generate SourceKit-LSP configuration Command", function () { } activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext); configFileUri = vscode.Uri.file(sourcekitConfigFilePath(folderContext)); diff --git a/test/integration-tests/commands/runSwiftScript.test.ts b/test/integration-tests/commands/runSwiftScript.test.ts index 0928b3046..12a2f17b7 100644 --- a/test/integration-tests/commands/runSwiftScript.test.ts +++ b/test/integration-tests/commands/runSwiftScript.test.ts @@ -27,7 +27,8 @@ suite("Swift Scripts Suite", () => { let toolchain: SwiftToolchain; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); if (process.platform === "win32") { // Swift Scripts on Windows give a JIT error in CI. this.skip(); diff --git a/test/integration-tests/commands/runTestMultipleTimes.test.ts b/test/integration-tests/commands/runTestMultipleTimes.test.ts index 8d592a4ce..a3f8605fe 100644 --- a/test/integration-tests/commands/runTestMultipleTimes.test.ts +++ b/test/integration-tests/commands/runTestMultipleTimes.test.ts @@ -26,7 +26,8 @@ suite("Test Multiple Times Command Test Suite", () => { let testItem: vscode.TestItem; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); folderContext = await folderInRootWorkspace("defaultPackage", ctx); const testExplorer = await folderContext.resolvedTestExplorer; diff --git a/test/integration-tests/configuration.test.ts b/test/integration-tests/configuration.test.ts index ed3651184..0b9bc1649 100644 --- a/test/integration-tests/configuration.test.ts +++ b/test/integration-tests/configuration.test.ts @@ -29,7 +29,8 @@ suite("Configuration Test Suite", function () { let workspaceContext: WorkspaceContext; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; }, }); diff --git a/test/integration-tests/debugger/lldb.test.ts b/test/integration-tests/debugger/lldb.test.ts index b0a4c1c2c..3aca6604a 100644 --- a/test/integration-tests/debugger/lldb.test.ts +++ b/test/integration-tests/debugger/lldb.test.ts @@ -24,7 +24,8 @@ suite("lldb contract test suite", () => { let workspaceContext: WorkspaceContext; activateExtensionForTest({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); // lldb.exe on Windows is not launching correctly, but only in Docker. if ( IS_RUNNING_UNDER_DOCKER && diff --git a/test/integration-tests/documentation/DocumentationLivePreview.test.ts b/test/integration-tests/documentation/DocumentationLivePreview.test.ts index 9f7b60735..790ad84f0 100644 --- a/test/integration-tests/documentation/DocumentationLivePreview.test.ts +++ b/test/integration-tests/documentation/DocumentationLivePreview.test.ts @@ -32,7 +32,8 @@ tag("medium").suite("Documentation Live Preview", function () { let workspaceContext: WorkspaceContext; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; await waitForNoRunningTasks(); folderContext = await folderInRootWorkspace("documentation-live-preview", ctx); diff --git a/test/integration-tests/extension.test.ts b/test/integration-tests/extension.test.ts index c64e478fd..689b7fb8e 100644 --- a/test/integration-tests/extension.test.ts +++ b/test/integration-tests/extension.test.ts @@ -25,7 +25,8 @@ suite("Extension Test Suite", function () { let workspaceContext: WorkspaceContext; activateExtensionForTest({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; }, }); diff --git a/test/integration-tests/language/LanguageClientIntegration.test.ts b/test/integration-tests/language/LanguageClientIntegration.test.ts index 5e8e0ded4..6cbb48bc4 100644 --- a/test/integration-tests/language/LanguageClientIntegration.test.ts +++ b/test/integration-tests/language/LanguageClientIntegration.test.ts @@ -41,7 +41,8 @@ tag("large").suite("Language Client Integration Suite", function () { let folderContext: FolderContext; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); if (process.platform === "win32") { this.skip(); return; diff --git a/test/integration-tests/tasks/SwiftExecution.test.ts b/test/integration-tests/tasks/SwiftExecution.test.ts index 3efb75310..d9a8d5af3 100644 --- a/test/integration-tests/tasks/SwiftExecution.test.ts +++ b/test/integration-tests/tasks/SwiftExecution.test.ts @@ -27,7 +27,8 @@ suite("SwiftExecution Tests Suite", () => { let workspaceFolder: vscode.WorkspaceFolder; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; toolchain = await SwiftToolchain.create( workspaceContext.extensionContext.extensionPath diff --git a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts index 9371e6eb7..31a4956d5 100644 --- a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts +++ b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -40,7 +40,8 @@ tag("medium").suite("SwiftPluginTaskProvider Test Suite", function () { let folderContext: FolderContext; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; ctx.logger.info("Locating command-plugin folder in root workspace"); folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); diff --git a/test/integration-tests/tasks/SwiftTaskProvider.test.ts b/test/integration-tests/tasks/SwiftTaskProvider.test.ts index 9307a2c51..2fb105eef 100644 --- a/test/integration-tests/tasks/SwiftTaskProvider.test.ts +++ b/test/integration-tests/tasks/SwiftTaskProvider.test.ts @@ -37,7 +37,8 @@ suite("SwiftTaskProvider Test Suite", () => { let folderContext: FolderContext; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; expect(workspaceContext.folders).to.not.have.lengthOf(0); workspaceFolder = workspaceContext.folders[0].workspaceFolder; diff --git a/test/integration-tests/tasks/TaskManager.test.ts b/test/integration-tests/tasks/TaskManager.test.ts index 34624a0fb..3227e425a 100644 --- a/test/integration-tests/tasks/TaskManager.test.ts +++ b/test/integration-tests/tasks/TaskManager.test.ts @@ -25,7 +25,8 @@ tag("medium").suite("TaskManager Test Suite", () => { let taskManager: TaskManager; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; taskManager = workspaceContext.tasks; assert.notEqual(workspaceContext.folders.length, 0); diff --git a/test/integration-tests/tasks/TaskQueue.test.ts b/test/integration-tests/tasks/TaskQueue.test.ts index 1f26b9376..debb1245d 100644 --- a/test/integration-tests/tasks/TaskQueue.test.ts +++ b/test/integration-tests/tasks/TaskQueue.test.ts @@ -26,7 +26,8 @@ tag("medium").suite("TaskQueue Test Suite", () => { let taskQueue: TaskQueue; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; assert.notEqual(workspaceContext.folders.length, 0); taskQueue = workspaceContext.folders[0].taskQueue; diff --git a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts index 70a15e837..be9ce0fae 100644 --- a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts +++ b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts @@ -65,7 +65,8 @@ tag("large").suite("Test Explorer Suite", function () { ) => Promise; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); // It can take a very long time for sourcekit-lsp to index tests on Windows, // especially w/ Swift 6.0. Wait for up to 25 minutes for the indexing to complete. if (process.platform === "win32") { diff --git a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts index 1af395eb9..0bc799ba0 100644 --- a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts +++ b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts @@ -75,7 +75,8 @@ ${tests.map( let hasMultiLineParallelTestOutput: boolean; let workspaceContext: WorkspaceContext; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; hasMultiLineParallelTestOutput = ctx.globalToolchain.hasMultiLineParallelTestOutput; }, diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index 247a1d3b1..38e49dcb5 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -43,7 +43,8 @@ tag("medium").suite("ProjectPanelProvider Test Suite", function () { let treeProvider: ProjectPanelProvider; activateExtensionForSuite({ - async setup(ctx) { + async setup(api) { + const ctx = await api.waitForWorkspaceContext(); workspaceContext = ctx; const folderContext = await folderInRootWorkspace("targets", workspaceContext); diff --git a/test/integration-tests/utilities/testutilities.ts b/test/integration-tests/utilities/testutilities.ts index 5fcb38595..0f5533ace 100644 --- a/test/integration-tests/utilities/testutilities.ts +++ b/test/integration-tests/utilities/testutilities.ts @@ -103,20 +103,15 @@ function configureLogDumpOnTimeout(timeout: number, logger: ExtensionActivationL } const extensionBootstrapper = (() => { - let activator: (() => Promise) | undefined = undefined; let activatedAPI: Api | undefined = undefined; - let lastTestName: string | undefined = undefined; const testTitle = (currentTest: Mocha.Test) => currentTest.titlePath().join(" → "); let activationLogger: ExtensionActivationLogger; - let asyncLogWrapper: (prefix: string, asyncWork: () => Thenable) => Promise; + let logOnError: (prefix: string, work: () => Thenable | T) => Promise; function testRunnerSetup( before: Mocha.HookFunction, setup: - | (( - this: Mocha.Context, - ctx: WorkspaceContext - ) => Promise<(() => Promise) | void>) + | ((this: Mocha.Context, api: Api) => Promise<(() => Promise) | void>) | undefined, after: Mocha.HookFunction, teardown: ((this: Mocha.Context) => Promise) | undefined, @@ -124,14 +119,18 @@ const extensionBootstrapper = (() => { requiresLSP: boolean = false, requiresDebugger: boolean = false ) { - let workspaceContext: WorkspaceContext | undefined; let autoTeardown: void | (() => Promise); let restoreSettings: (() => Promise) | undefined; activationLogger = new ExtensionActivationLogger(); - asyncLogWrapper = withLogging(activationLogger); + logOnError = withLogging(activationLogger); const SETUP_TIMEOUT_MS = 300_000; const TEARDOWN_TIMEOUT_MS = 60_000; + // Extension activation happens asynchronously which means that we need to store the + // call site of this function to use as the activation site for the extension. This is + // used so that we know which test didn't clean up its extension activation. + const callSite = Error("Extension was activated by:"); + before("Activate Swift Extension", async function () { // Allow enough time for the extension to activate this.timeout(SETUP_TIMEOUT_MS); @@ -144,7 +143,7 @@ const extensionBootstrapper = (() => { // Make sure that CodeLLDB is installed for debugging related tests if (!vscode.extensions.getExtension("vadimcn.vscode-lldb")) { - await asyncLogWrapper( + await logOnError( "vadimcn.vscode-lldb is not installed, installing CodeLLDB extension for the debugging tests.", () => vscode.commands.executeCommand( @@ -155,47 +154,54 @@ const extensionBootstrapper = (() => { } // Always activate the extension. If no test assets are provided, // default to adding `defaultPackage` to the workspace. - workspaceContext = await extensionBootstrapper.activateExtension( - this.currentTest ?? this.test, - testAssets ?? ["defaultPackage"] + const api = await extensionBootstrapper.activateExtension( + testAssets ?? ["defaultPackage"], + callSite ); - activationLogger.setLogger(workspaceContext.logger); + activationLogger.setLogger(api.logger); activationLogger.info(`Extension activated successfully.`); - // Need the `disableSandbox` configuration which is only in 6.1 - // https://github.com/swiftlang/sourcekit-lsp/commit/7e2d12a7a0d184cc820ae6af5ddbb8aa18b1501c - if ( - process.platform === "darwin" && - workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(6, 1, 0)) && - requiresLSP - ) { - activationLogger.info(`Skipping test, LSP is required but not available.`); - this.skip(); - } - if (requiresDebugger && configuration.debugger.disable) { - activationLogger.info( - `Skipping test, Debugger is required but disabled in the configuration.` - ); - this.skip(); - } - // CodeLLDB does not work with libllbd in Swift toolchains prior to 5.10 - if (workspaceContext.globalToolchainSwiftVersion.isLessThan(new Version(5, 10, 0))) { - await asyncLogWrapper('Setting swift.debugger.setupCodeLLDB: "never"', () => - updateSettings({ - "swift.debugger.setupCodeLLDB": "never", - }) - ); - } else if (requiresDebugger) { - const lldbLibPath = await asyncLogWrapper("Getting LLDB library path", async () => - getLLDBLibPath(workspaceContext!.globalToolchain) - ); - activationLogger.info( - `LLDB library path is: ${lldbLibPath.success ?? "not found"}` - ); - } + await api.withWorkspaceContext(async workspaceContext => { + // Need the `disableSandbox` configuration which is only in 6.1 + // https://github.com/swiftlang/sourcekit-lsp/commit/7e2d12a7a0d184cc820ae6af5ddbb8aa18b1501c + if ( + process.platform === "darwin" && + workspaceContext.globalToolchain.swiftVersion.isLessThan( + new Version(6, 1, 0) + ) && + requiresLSP + ) { + activationLogger.info(`Skipping test, LSP is required but not available.`); + this.skip(); + } + if (requiresDebugger && configuration.debugger.disable) { + activationLogger.info( + `Skipping test, Debugger is required but disabled in the configuration.` + ); + this.skip(); + } + // CodeLLDB does not work with libllbd in Swift toolchains prior to 5.10 + if ( + workspaceContext.globalToolchainSwiftVersion.isLessThan(new Version(5, 10, 0)) + ) { + await logOnError('Setting swift.debugger.setupCodeLLDB: "never"', () => + updateSettings({ + "swift.debugger.setupCodeLLDB": "never", + }) + ); + } else if (requiresDebugger) { + const lldbLibPath = await logOnError("Getting LLDB library path", async () => + getLLDBLibPath(workspaceContext!.globalToolchain) + ); + activationLogger.info( + `LLDB library path is: ${lldbLibPath.success ?? "not found"}` + ); + activationLogger.info(`LLDB library path is: ${lldbLibPath}`); + } + }); // Make sure no running tasks before setting up - await asyncLogWrapper("Waiting for no running tasks before starting test/suite", () => + await logOnError("Waiting for no running tasks before starting test/suite", () => waitForNoRunningTasks({ timeout: 10000 }) ); @@ -211,9 +217,9 @@ const extensionBootstrapper = (() => { // If the setup returns a promise it is used to undo whatever setup it did. // Typically this is the promise returned from `updateSettings`, which will // undo any settings changed during setup. - autoTeardown = await asyncLogWrapper( + autoTeardown = await logOnError( "Calling user defined setup method to configure test/suite specifics", - () => setup.call(this, workspaceContext!) + () => setup.call(this, activatedAPI!) ); } catch (error: any) { // Mocha will throw an error to break out of a test if `.skip` is used. @@ -254,20 +260,17 @@ const extensionBootstrapper = (() => { try { // First run the users supplied teardown, then await the autoTeardown if it exists. if (teardown) { - await asyncLogWrapper("Running user teardown function...", () => + await logOnError("Running user teardown function...", () => teardown.call(this) ); } if (autoTeardown) { - await asyncLogWrapper( + await logOnError( "Running auto teardown function (function returned from setup)...", () => autoTeardown!() ); } } catch (error) { - if (workspaceContext) { - printLogs(activationLogger, "Error during test/suite teardown"); - } // We always want to restore settings and deactivate the extension even if the // user supplied teardown fails. That way we have the best chance at not causing // issues with the next test. @@ -277,9 +280,7 @@ const extensionBootstrapper = (() => { } if (restoreSettings) { - await asyncLogWrapper("Running restore settings function...", () => - restoreSettings!() - ); + await logOnError("Running restore settings function...", () => restoreSettings!()); } activationLogger.info("Deactivation complete, calling deactivateExtension()"); await extensionBootstrapper.deactivateExtension(); @@ -300,25 +301,17 @@ const extensionBootstrapper = (() => { // test run, so after it is called once we switch over to calling activate on // the returned API object which behaves like the extension is being launched for // the first time _as long as everything is disposed of properly in `deactivate()`_. - activateExtension: async function (currentTest?: Mocha.Runnable, testAssets?: string[]) { - if (activatedAPI) { - throw new Error( - `Extension is already activated. Last test that activated the extension: ${lastTestName}` - ); - } - + async activateExtension(testAssets?: string[], callSite?: Error): Promise { const extensionId = "swiftlang.swift-vscode"; const ext = vscode.extensions.getExtension(extensionId); if (!ext) { throw new Error(`Unable to find extension "${extensionId}"`); } - let workspaceContext: WorkspaceContext | undefined; - // We can only _really_ call activate through // `vscode.extensions.getExtension("swiftlang.swift-vscode")` once. // Subsequent activations must be done through the returned API object. - if (!activator) { + if (!activatedAPI) { activationLogger.info( "Performing the one and only extension activation for this test run." ); @@ -327,31 +320,23 @@ const extensionBootstrapper = (() => { if (!dep) { throw new Error(`Unable to find extension "${depId}"`); } - await asyncLogWrapper(`Activating dependency extension "${depId}".`, () => + await logOnError(`Activating dependency extension "${depId}".`, () => dep.activate() ); } - activatedAPI = await asyncLogWrapper( + activatedAPI = await logOnError( "Activating Swift extension (true activation)...", () => ext.activate() ); - - // Save the test name so if the test doesn't clean up by deactivating properly the next - // test that tries to activate can throw an error with the name of the test that needs to clean up. - lastTestName = currentTest?.titlePath().join(" → "); - activator = activatedAPI.activate; - workspaceContext = activatedAPI.workspaceContext; } else { - activatedAPI = await asyncLogWrapper( + await logOnError( "Activating Swift extension by re-calling the extension's activation method...", - () => activator!() + () => activatedAPI!.activate(callSite) ); - lastTestName = currentTest?.titlePath().join(" → "); - workspaceContext = activatedAPI.workspaceContext; } - if (!workspaceContext) { + if (!activatedAPI) { printLogs( activationLogger, "Error during test/suite setup, workspace context could not be created" @@ -360,65 +345,67 @@ const extensionBootstrapper = (() => { } // Add assets required for the suite/test to the workspace. - const expectedAssets = testAssets ?? ["defaultPackage"]; - if (!vscode.workspace.workspaceFile) { - activationLogger.info(`No workspace file found, adding assets directly.`); - for (const asset of expectedAssets) { - await asyncLogWrapper(`Adding ${asset} to workspace...`, () => - folderInRootWorkspace(asset, workspaceContext) - ); - } - activationLogger.info(`All assets added to workspace.`); - } else if (expectedAssets.length > 0) { - await new Promise(res => { - const found: string[] = []; - for (const f of workspaceContext.folders) { - if (found.includes(f.name) || !expectedAssets.includes(f.name)) { - continue; - } - activationLogger.info(`Added ${f.name} to workspace`); - found.push(f.name); - } - if (expectedAssets.length === found.length) { - res(); - return; + await activatedAPI.withWorkspaceContext(async workspaceContext => { + const expectedAssets = testAssets ?? ["defaultPackage"]; + if (!vscode.workspace.workspaceFile) { + activationLogger.info(`No workspace file found, adding assets directly.`); + for (const asset of expectedAssets) { + await logOnError(`Adding ${asset} to workspace...`, () => + folderInRootWorkspace(asset, workspaceContext) + ); } - const disposable = workspaceContext.onDidChangeFolders(e => { - if ( - e.operation !== FolderOperation.add || - found.includes(e.folder!.name) || - !expectedAssets.includes(e.folder!.name) - ) { - return; + activationLogger.info(`All assets added to workspace.`); + } else if (expectedAssets.length > 0) { + await new Promise(res => { + const found: string[] = []; + for (const f of workspaceContext.folders) { + if (found.includes(f.name) || !expectedAssets.includes(f.name)) { + continue; + } + activationLogger.info(`Added ${f.name} to workspace`); + found.push(f.name); } - activationLogger.info(`Added ${e.folder!.name} to workspace`); - found.push(e.folder!.name); if (expectedAssets.length === found.length) { res(); - disposable.dispose(); + return; } + const disposable = workspaceContext.onDidChangeFolders(e => { + if ( + e.operation !== FolderOperation.add || + found.includes(e.folder!.name) || + !expectedAssets.includes(e.folder!.name) + ) { + return; + } + activationLogger.info(`Added ${e.folder!.name} to workspace`); + found.push(e.folder!.name); + if (expectedAssets.length === found.length) { + res(); + disposable.dispose(); + } + }); }); - }); - activationLogger.info(`All assets added to workspace.`); - } + activationLogger.info(`All assets added to workspace.`); + } + }); - return workspaceContext; + return activatedAPI; }, - deactivateExtension: async () => { + async deactivateExtension(): Promise { if (!activatedAPI) { throw new Error("Extension is not activated. Call activateExtension() first."); } // Wait for up to 10 seconds for all tasks to complete before deactivating. // Long running tasks should be avoided in tests, but this is a safety net. - await asyncLogWrapper(`Deactivating extension, waiting for no running tasks.`, () => + await logOnError(`Deactivating extension, waiting for no running tasks.`, () => waitForNoRunningTasks({ timeout: 10000 }) ); // Close all editors before deactivating the extension. - await asyncLogWrapper(`Closing all editors.`, () => closeAllEditors()); + await logOnError(`Closing all editors.`, () => closeAllEditors()); - await asyncLogWrapper( + await logOnError( `Removing root workspace folder.`, () => activatedAPI!.workspaceContext?.removeWorkspaceFolder( @@ -426,17 +413,12 @@ const extensionBootstrapper = (() => { ) ?? Promise.resolve() ); activationLogger.info(`Running extension deactivation function.`); - await activatedAPI.deactivate(); + activatedAPI.deactivate(); activationLogger.reset(); - activatedAPI = undefined; - lastTestName = undefined; }, activateExtensionForSuite: function (config?: { - setup?: ( - this: Mocha.Context, - ctx: WorkspaceContext - ) => Promise<(() => Promise) | void>; + setup?: (this: Mocha.Context, api: Api) => Promise<(() => Promise) | void>; teardown?: (this: Mocha.Context) => Promise; testAssets?: string[]; requiresLSP?: boolean; @@ -454,10 +436,7 @@ const extensionBootstrapper = (() => { }, activateExtensionForTest: function (config?: { - setup?: ( - this: Mocha.Context, - ctx: WorkspaceContext - ) => Promise<(() => Promise) | void>; + setup?: (this: Mocha.Context, api: Api) => Promise<(() => Promise) | void>; teardown?: (this: Mocha.Context) => Promise; testAssets?: string[]; requiresLSP?: boolean; @@ -698,10 +677,10 @@ export function isConfigurationSuperset(configValue: unknown, expected: unknown) * @returns A wrapper function that takes a prefix and async work function, returning a promise that resolves to the result of the async work */ export function withLogging(logger: { info: (message: string) => void }) { - return async function (prefix: string, asyncWork: () => Thenable): Promise { + return async function (prefix: string, work: () => Thenable | T): Promise { logger.info(`${prefix} - starting`); try { - const result = await asyncWork(); + const result = await work(); logger.info(`${prefix} - completed`); return result; } catch (error) { diff --git a/test/unit-tests/MockUtils.test.ts b/test/unit-tests/MockUtils.test.ts index 02fe92d1d..9d8faf92d 100644 --- a/test/unit-tests/MockUtils.test.ts +++ b/test/unit-tests/MockUtils.test.ts @@ -162,6 +162,14 @@ suite("MockUtils Test Suite", () => { expect(() => sut.a).to.throw("Cannot access this property"); }); + + test("can be used as an argument to Promise.resolve()", async () => { + interface TestInterface { + readonly a: number; + } + const test = mockObject({ a: 4 }); + await expect(Promise.resolve(test)).to.eventually.have.property("a", 4); + }); }); suite("mockFn()", () => { diff --git a/test/unit-tests/debugger/debugAdapterFactory.test.ts b/test/unit-tests/debugger/debugAdapterFactory.test.ts index 2fa04a136..47f0c0187 100644 --- a/test/unit-tests/debugger/debugAdapterFactory.test.ts +++ b/test/unit-tests/debugger/debugAdapterFactory.test.ts @@ -23,6 +23,7 @@ import { LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "@src/debugger/debugA import * as debugAdapter from "@src/debugger/debugAdapter"; import { LLDBDebugConfigurationProvider } from "@src/debugger/debugAdapterFactory"; import * as lldb from "@src/debugger/lldb"; +import { Api } from "@src/extension"; import { SwiftLogger } from "@src/logging/SwiftLogger"; import { BuildFlags } from "@src/toolchain/BuildFlags"; import { SwiftToolchain } from "@src/toolchain/toolchain"; @@ -39,6 +40,7 @@ import { } from "../../MockUtils"; suite("LLDBDebugConfigurationProvider Tests", () => { + let mockExtensionApi: MockedObject; let mockWorkspaceContext: MockedObject; let mockToolchain: MockedObject; let mockBuildFlags: MockedObject; @@ -62,13 +64,20 @@ suite("LLDBDebugConfigurationProvider Tests", () => { subscriptions: [], folders: [], }); + mockExtensionApi = mockObject({ + workspaceContext: instance(mockWorkspaceContext), + logger: instance(mockLogger), + withWorkspaceContext: mockFn(s => + s.callsFake(async task => task(instance(mockWorkspaceContext))) + ), + waitForWorkspaceContext: mockFn(s => s.resolves(instance(mockWorkspaceContext))), + }); }); test("allows specifying a 'pid' in the launch configuration", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( undefined, @@ -85,8 +94,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("converts 'pid' property from a string to a number", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( undefined, @@ -106,8 +114,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( undefined, @@ -127,8 +134,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( undefined, @@ -161,8 +167,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockWorkspaceContext.folders = [instance(mockedFolderCtx)]; const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( folder, @@ -206,8 +211,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("returns a launch configuration that uses CodeLLDB as the debug adapter", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -224,8 +228,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockWindow.showErrorMessage.resolves("Install CodeLLDB" as any); const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); await expect( configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -246,8 +249,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockWindow.showInformationMessage.resolves("Global" as any); const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); await expect( configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -269,8 +271,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockLldbConfiguration.get.withArgs("library").returns(undefined); const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); await expect( configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -304,8 +305,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("returns a launch configuration that uses lldb-dap as the debug adapter", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -324,8 +324,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockFS({}); // Reset mockFS so that no files exist const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); await expect( configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -341,8 +340,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("modifies program to add file extension on Windows", async () => { const configProvider = new LLDBDebugConfigurationProvider( "win32", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -359,8 +357,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("does not modify program on Windows if file extension is already present", async () => { const configProvider = new LLDBDebugConfigurationProvider( "win32", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -377,8 +374,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("does not modify program on macOS", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -395,8 +391,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("does not modify program on Linux", async () => { const configProvider = new LLDBDebugConfigurationProvider( "linux", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -413,8 +408,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("should convert environment variables to string[] format when using lldb-dap", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -435,8 +429,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("should leave env undefined when environment variables are undefined and using lldb-dap", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -451,8 +444,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("should convert empty environment variables when using lldb-dap", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -474,8 +466,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { } const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { @@ -514,8 +505,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockWorkspaceContext.folders.push(instance(mockFolder)); const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockWorkspaceContext), - instance(mockLogger) + instance(mockExtensionApi) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( {