Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/TestExplorer/TestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,8 @@ export class TestRunner {
testBuildConfig: vscode.DebugConfiguration,
runState: TestRunnerTestRunState
) {
await this.workspaceContext.tempFolder.withTemporaryFile("xml", async filename => {
const tempFolder = await TemporaryFolder.create();
await tempFolder.withTemporaryFile("xml", async filename => {
const args = [...(testBuildConfig.args ?? []), "--xunit-output", filename];

try {
Expand Down
14 changes: 1 addition & 13 deletions src/WorkspaceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { StatusItem } from "./ui/StatusItem";
import { swiftLibraryPathKey } from "./utilities/utilities";
import { isExcluded, isPathInsidePath } from "./utilities/filesystem";
import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClientToolchainCoordinator";
import { TemporaryFolder } from "./utilities/tempFolder";
import { TaskManager } from "./tasks/TaskManager";
import { makeDebugConfigurations } from "./debugger/launch";
import configuration from "./configuration";
Expand Down Expand Up @@ -76,9 +75,8 @@ export class WorkspaceContext implements vscode.Disposable {

public loggerFactory: SwiftLoggerFactory;

private constructor(
constructor(
extensionContext: vscode.ExtensionContext,
public tempFolder: TemporaryFolder,
public logger: SwiftLogger,
public globalToolchain: SwiftToolchain
) {
Expand Down Expand Up @@ -230,16 +228,6 @@ export class WorkspaceContext implements vscode.Disposable {
return this.globalToolchain.swiftVersion;
}

/** Get swift version and create WorkspaceContext */
static async create(
extensionContext: vscode.ExtensionContext,
logger: SwiftLogger,
toolchain: SwiftToolchain
): Promise<WorkspaceContext> {
const tempFolder = await TemporaryFolder.create();
return new WorkspaceContext(extensionContext, tempFolder, logger, toolchain);
}

/**
* Update context keys based on package contents
*/
Expand Down
10 changes: 9 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,15 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
Commands.RESET_PACKAGE,
async (_ /* Ignore context */, folder) => await resetPackage(ctx, folder)
),
vscode.commands.registerCommand("swift.runScript", async () => await runSwiftScript(ctx)),
vscode.commands.registerCommand("swift.runScript", async () => {
if (ctx.currentFolder && vscode.window.activeTextEditor?.document) {
await runSwiftScript(
vscode.window.activeTextEditor.document,
ctx.tasks,
ctx.currentFolder.toolchain
);
}
}),
vscode.commands.registerCommand("swift.openPackage", async () => {
if (ctx.currentFolder) {
return await openPackage(ctx.currentFolder.swiftVersion, ctx.currentFolder.folder);
Expand Down
121 changes: 68 additions & 53 deletions src/commands/runSwiftScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,58 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs/promises";
import { createSwiftTask } from "../tasks/SwiftTaskProvider";
import { WorkspaceContext } from "../WorkspaceContext";
import { Version } from "../utilities/version";
import configuration from "../configuration";
import { createSwiftTask } from "../tasks/SwiftTaskProvider";
import { TemporaryFolder } from "../utilities/tempFolder";
import { TaskManager } from "../tasks/TaskManager";
import { SwiftToolchain } from "../toolchain/toolchain";

/**
* Run the active document through the Swift REPL
* Runs the Swift code in the supplied document.
*
* This function checks for a valid document and Swift version, then creates and executes
* a Swift task to run the script file. The task is configured to always reveal its output
* and clear previous output. The working directory is set to the script's location.
*
* @param document - The text document containing the Swift script to run. If undefined, the function returns early.
* @param tasks - The TaskManager instance used to execute and manage the Swift task.
* @param toolchain - The SwiftToolchain to use for running the script.
* @returns A promise that resolves when the script has finished running, or returns early if the user is prompted
* for which swift version to use and they exit the dialog without choosing one.
*/
export async function runSwiftScript(ctx: WorkspaceContext) {
const document = vscode.window.activeTextEditor?.document;
if (!document) {
return;
}

if (!ctx.currentFolder) {
export async function runSwiftScript(
document: vscode.TextDocument,
tasks: TaskManager,
toolchain: SwiftToolchain
) {
const targetVersion = await targetSwiftVersion();
if (!targetVersion) {
return;
}

// Swift scripts require new swift driver to work on Windows. Swift driver is available
// from v5.7 of Windows Swift
if (
process.platform === "win32" &&
ctx.currentFolder.swiftVersion.isLessThan(new Version(5, 7, 0))
) {
void vscode.window.showErrorMessage(
"Run Swift Script is unavailable with the legacy driver on Windows."
await withDocumentFile(document, async filename => {
const runTask = createSwiftTask(
["-swift-version", targetVersion, filename],
`Run ${filename}`,
{
scope: vscode.TaskScope.Global,
cwd: vscode.Uri.file(path.dirname(filename)),
presentationOptions: { reveal: vscode.TaskRevealKind.Always, clear: true },
},
toolchain
);
return;
}

let target: string;
await tasks.executeTaskAndWait(runTask);
});
}

/**
* Determines the target Swift language version to use for script execution.
* If the configuration is set to "Ask Every Run", prompts the user to select a version.
* Otherwise, returns the default version from the user's settings.
*
* @returns {Promise<string | undefined>} The selected Swift version, or undefined if no selection was made.
*/
async function targetSwiftVersion() {
const defaultVersion = configuration.scriptSwiftLanguageVersion;
if (defaultVersion === "Ask Every Run") {
const picked = await vscode.window.showQuickPick(
Expand All @@ -59,41 +79,36 @@ export async function runSwiftScript(ctx: WorkspaceContext) {
placeHolder: "Select a target Swift version",
}
);

if (!picked) {
return;
}
target = picked.value;
return picked?.value;
} else {
target = defaultVersion;
return defaultVersion;
}
}

let filename = document.fileName;
let isTempFile = false;
/**
* Executes a callback with the filename of the given `vscode.TextDocument`.
* If the document is untitled (not yet saved to disk), it creates a temporary file,
* writes the document's content to it, and passes its filename to the callback.
* Otherwise, it ensures the document is saved and passes its actual filename.
*
* The temporary file is automatically deleted when the callback completes.
*
* @param document - The VSCode text document to operate on.
* @param callback - An async function that receives the filename of the document or temporary file.
* @returns A promise that resolves when the callback has completed.
*/
async function withDocumentFile(
document: vscode.TextDocument,
callback: (filename: string) => Promise<void>
) {
if (document.isUntitled) {
// if document hasn't been saved, save it to a temporary file
isTempFile = true;
filename = ctx.tempFolder.filename(document.fileName, "swift");
const text = document.getText();
await fs.writeFile(filename, text);
const tmpFolder = await TemporaryFolder.create();
await tmpFolder.withTemporaryFile("swift", async filename => {
await fs.writeFile(filename, document.getText());
await callback(filename);
});
} else {
// otherwise save document
await document.save();
}
const runTask = createSwiftTask(
["-swift-version", target, filename],
`Run ${filename}`,
{
scope: vscode.TaskScope.Global,
cwd: vscode.Uri.file(path.dirname(filename)),
presentationOptions: { reveal: vscode.TaskRevealKind.Always, clear: true },
},
ctx.currentFolder.toolchain
);
await ctx.tasks.executeTaskAndWait(runTask);

// delete file after running swift
if (isTempFile) {
await fs.rm(filename);
await callback(document.fileName);
}
}
37 changes: 28 additions & 9 deletions src/coverage/LcovResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { FolderContext } from "../FolderContext";
import { execFileStreamOutput } from "../utilities/utilities";
import { BuildFlags } from "../toolchain/BuildFlags";
import { TestLibrary } from "../TestExplorer/TestRunner";
import { DisposableFileCollection } from "../utilities/tempFolder";
import { DisposableFileCollection, TemporaryFolder } from "../utilities/tempFolder";
import { TargetType } from "../SwiftPackage";
import { TestingConfigurationFactory } from "../debugger/buildConfig";
import { TestKind } from "../TestExplorer/TestKind";
Expand All @@ -35,13 +35,11 @@ interface CodeCovFile {

export class TestCoverage {
private lcovFiles: CodeCovFile[] = [];
private lcovTmpFiles: DisposableFileCollection;
private _lcovTmpFiles?: DisposableFileCollection;
private _lcovTmpFilesInit?: Promise<DisposableFileCollection>;
private coverageDetails = new Map<vscode.Uri, vscode.FileCoverageDetail[]>();

constructor(private folderContext: FolderContext) {
const tmpFolder = folderContext.workspaceContext.tempFolder;
this.lcovTmpFiles = tmpFolder.createDisposableFileCollection();
}
constructor(private folderContext: FolderContext) {}

/**
* Returns coverage information for the suppplied URI.
Expand All @@ -60,7 +58,7 @@ export class TestCoverage {
true
);
const result = await asyncfs.readFile(`${buildDirectory}/debug/codecov/default.profdata`);
const filename = this.lcovTmpFiles.file(testLibrary, "profdata");
const filename = (await this.lcovTmpFiles()).file(testLibrary, "profdata");
await asyncfs.writeFile(filename, result);
this.lcovFiles.push({ testLibrary, path: filename });
}
Expand Down Expand Up @@ -88,14 +86,14 @@ export class TestCoverage {
this.coverageDetails.set(uri, detailedCoverage);
}
}
await this.lcovTmpFiles.dispose();
await this._lcovTmpFiles?.dispose();
}

/**
* Merges multiple `.profdata` files into a single `.profdata` file.
*/
private async mergeProfdata(profDataFiles: string[]) {
const filename = this.lcovTmpFiles.file("merged", "profdata");
const filename = (await this.lcovTmpFiles()).file("merged", "profdata");
const toolchain = this.folderContext.toolchain;
const llvmProfdata = toolchain.getToolchainExecutable("llvm-profdata");
await execFileStreamOutput(
Expand Down Expand Up @@ -196,6 +194,27 @@ export class TestCoverage {
return buffer;
}

/**
* Lazily creates (once) and returns the disposable file collection used for LCOV processing.
* Safe against concurrent callers.
*/
private async lcovTmpFiles(): Promise<DisposableFileCollection> {
if (this._lcovTmpFiles) {
return this._lcovTmpFiles;
}

// Use an internal promise to avoid duplicate folder creation in concurrent calls.
if (!this._lcovTmpFilesInit) {
this._lcovTmpFilesInit = (async () => {
const tempFolder = await TemporaryFolder.create();
this._lcovTmpFiles = tempFolder.createDisposableFileCollection();
return this._lcovTmpFiles;
})();
}

return (await this._lcovTmpFilesInit)!;
}

/**
* Constructs a string containing all the paths to exclude from the code coverage report.
* This should exclude everything in the `.build` folder as well as all the test targets.
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
};
}

const workspaceContext = await WorkspaceContext.create(context, logger, toolchain);
const workspaceContext = new WorkspaceContext(context, logger, toolchain);
context.subscriptions.push(workspaceContext);

context.subscriptions.push(new SwiftEnvironmentVariablesManager(context));
Expand Down
Loading