Skip to content

Commit acf5bff

Browse files
authored
Fix Swift Scripts without a containing folder (#1832)
* Fix Swift Scripts without a containing folder If the user tried to run a Swift file without having opened a folder first, the script wouldn't run. Allow the script to run without a focused folder.
1 parent 2a3ba9f commit acf5bff

File tree

9 files changed

+98
-23
lines changed

9 files changed

+98
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Don't start debugging XCTest cases if the swift-testing debug session was stopped ([#1797](https://github.com/swiftlang/vscode-swift/pull/1797))
1515
- Improve error handling when the swift path is misconfigured ([#1801](https://github.com/swiftlang/vscode-swift/pull/1801))
1616
- Fix an error when performing "Run/Debug Tests Multiple Times" on Linux ([#1824](https://github.com/swiftlang/vscode-swift/pull/1824))
17+
- Fix the `> Swift: Run Swift Script` command not running unless a Swift Package folder is open ([#1832](https://github.com/swiftlang/vscode-swift/pull/1832))
1718

1819
## 2.11.20250806 - 2025-08-06
1920

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Hello World")

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,6 @@
495495
"Use Swift 5 when running Swift scripts.",
496496
"Prompt to select the Swift version each time a script is run."
497497
],
498-
"default": "6",
499498
"markdownDescription": "The default Swift version to use when running Swift scripts.",
500499
"scope": "machine-overridable"
501500
},

src/commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,11 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
220220
async (_ /* Ignore context */, folder) => await resetPackage(ctx, folder)
221221
),
222222
vscode.commands.registerCommand("swift.runScript", async () => {
223-
if (ctx.currentFolder && vscode.window.activeTextEditor?.document) {
223+
if (ctx && vscode.window.activeTextEditor?.document) {
224224
await runSwiftScript(
225225
vscode.window.activeTextEditor.document,
226226
ctx.tasks,
227-
ctx.currentFolder.toolchain
227+
ctx.currentFolder?.toolchain ?? ctx.globalToolchain
228228
);
229229
}
230230
}),

src/commands/runSwiftScript.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ import { TemporaryFolder } from "../utilities/tempFolder";
3737
export async function runSwiftScript(
3838
document: vscode.TextDocument,
3939
tasks: TaskManager,
40-
toolchain: SwiftToolchain
41-
) {
42-
const targetVersion = await targetSwiftVersion();
40+
toolchain: SwiftToolchain,
41+
logger?: (data: string) => void
42+
): Promise<number | undefined> {
43+
const targetVersion = await targetSwiftVersion(toolchain);
4344
if (!targetVersion) {
4445
return;
4546
}
4647

47-
await withDocumentFile(document, async filename => {
48+
return await withDocumentFile(document, async filename => {
4849
const runTask = createSwiftTask(
4950
["-swift-version", targetVersion, filename],
5051
`Run ${filename}`,
@@ -55,7 +56,8 @@ export async function runSwiftScript(
5556
},
5657
toolchain
5758
);
58-
await tasks.executeTaskAndWait(runTask);
59+
runTask.execution.onDidWrite(data => logger?.(data));
60+
return await tasks.executeTaskAndWait(runTask);
5961
});
6062
}
6163

@@ -66,8 +68,8 @@ export async function runSwiftScript(
6668
*
6769
* @returns {Promise<string | undefined>} The selected Swift version, or undefined if no selection was made.
6870
*/
69-
async function targetSwiftVersion() {
70-
const defaultVersion = configuration.scriptSwiftLanguageVersion;
71+
async function targetSwiftVersion(toolchain: SwiftToolchain) {
72+
const defaultVersion = configuration.scriptSwiftLanguageVersion(toolchain);
7173
if (defaultVersion === "Ask Every Run") {
7274
const picked = await vscode.window.showQuickPick(
7375
[
@@ -97,18 +99,18 @@ async function targetSwiftVersion() {
9799
* @param callback - An async function that receives the filename of the document or temporary file.
98100
* @returns A promise that resolves when the callback has completed.
99101
*/
100-
async function withDocumentFile(
102+
async function withDocumentFile<T>(
101103
document: vscode.TextDocument,
102-
callback: (filename: string) => Promise<void>
103-
) {
104+
callback: (filename: string) => Promise<T>
105+
): Promise<T> {
104106
if (document.isUntitled) {
105107
const tmpFolder = await TemporaryFolder.create();
106-
await tmpFolder.withTemporaryFile("swift", async filename => {
108+
return await tmpFolder.withTemporaryFile("swift", async filename => {
107109
await fs.writeFile(filename, document.getText());
108-
await callback(filename);
110+
return await callback(filename);
109111
});
110112
} else {
111113
await document.save();
112-
await callback(document.fileName);
114+
return await callback(document.fileName);
113115
}
114116
}

src/configuration.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as path from "path";
1616
import * as vscode from "vscode";
1717

1818
import { WorkspaceContext } from "./WorkspaceContext";
19+
import { SwiftToolchain } from "./toolchain/toolchain";
1920
import { showReloadExtensionNotification } from "./ui/ReloadExtension";
2021

2122
export type DebugAdapters = "auto" | "lldb-dap" | "CodeLLDB";
@@ -352,10 +353,14 @@ const configuration = {
352353
.get<string[]>("buildArguments", [])
353354
.map(substituteVariablesInString);
354355
},
355-
get scriptSwiftLanguageVersion(): string {
356-
return vscode.workspace
356+
scriptSwiftLanguageVersion(toolchain: SwiftToolchain): string {
357+
const version = vscode.workspace
357358
.getConfiguration("swift")
358-
.get<string>("scriptSwiftLanguageVersion", "6");
359+
.get<string>("scriptSwiftLanguageVersion", toolchain.swiftVersion.major.toString());
360+
if (version.length === 0) {
361+
return toolchain.swiftVersion.major.toString();
362+
}
363+
return version;
359364
},
360365
/** swift package arguments */
361366
get packageArguments(): string[] {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import { expect } from "chai";
15+
import * as path from "path";
16+
import * as vscode from "vscode";
17+
18+
import { runSwiftScript } from "@src/commands/runSwiftScript";
19+
import { TaskManager } from "@src/tasks/TaskManager";
20+
import { SwiftToolchain } from "@src/toolchain/toolchain";
21+
22+
import { activateExtensionForSuite, findWorkspaceFolder } from "../utilities/testutilities";
23+
24+
suite("Swift Scripts Suite", () => {
25+
let document: vscode.TextDocument;
26+
let tasks: TaskManager;
27+
let toolchain: SwiftToolchain;
28+
29+
activateExtensionForSuite({
30+
async setup(ctx) {
31+
if (process.platform === "win32") {
32+
// Swift Scripts on Windows give a JIT error in CI.
33+
this.skip();
34+
}
35+
36+
tasks = ctx.tasks;
37+
toolchain = ctx.globalToolchain;
38+
39+
const folder = findWorkspaceFolder("scripts", ctx);
40+
if (!folder) {
41+
throw new Error("Could not find 'scripts' workspace folder");
42+
}
43+
const scriptPath = path.join(folder.folder.fsPath, "SwiftScript.swift");
44+
const editor = await vscode.window.showTextDocument(vscode.Uri.file(scriptPath));
45+
document = editor.document;
46+
},
47+
testAssets: ["scripts"],
48+
});
49+
50+
test("Successfully runs a swift script", async () => {
51+
let output = "";
52+
const exitCode = await runSwiftScript(document, tasks, toolchain, data => (output += data));
53+
expect(output).to.contain("Hello World");
54+
expect(exitCode).to.be.equal(0);
55+
});
56+
});

test/integration-tests/utilities/testutilities.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414
import * as assert from "assert";
1515
import * as mocha from "mocha";
16+
import * as path from "path";
1617
import { isDeepStrictEqual } from "util";
1718
import * as vscode from "vscode";
1819

@@ -23,6 +24,7 @@ import { Api } from "@src/extension";
2324
import { SwiftLogger } from "@src/logging/SwiftLogger";
2425
import { buildAllTaskName, resetBuildAllTaskCache } from "@src/tasks/SwiftTaskProvider";
2526
import { Extension } from "@src/utilities/extensions";
27+
import { fileExists } from "@src/utilities/filesystem";
2628
import { Version } from "@src/utilities/version";
2729

2830
import { testAssetPath, testAssetUri } from "../../fixtures";
@@ -368,10 +370,17 @@ export const folderInRootWorkspace = async (
368370
if (!folder) {
369371
folder = await workspaceContext.addPackageFolder(testAssetUri(name), workspaceFolder);
370372
}
373+
374+
// Folders that aren't packages (i.e. assets/tests/scripts) wont generate build tasks.
375+
if (!(await fileExists(path.join(testAssetUri(name).fsPath, "Package.swift")))) {
376+
return folder;
377+
}
378+
371379
let i = 0;
372380
while (i++ < 5) {
373381
const tasks = await vscode.tasks.fetchTasks({ type: "swift" });
374-
if (tasks.find(t => t.name === buildAllTaskName(folder, false))) {
382+
const buildAllName = buildAllTaskName(folder, false);
383+
if (tasks.find(t => t.name === buildAllName)) {
375384
break;
376385
}
377386
await new Promise(r => setTimeout(r, 5000));

test/unit-tests/commands/runSwiftScript.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ import configuration from "@src/configuration";
2121
import { TaskManager } from "@src/tasks/TaskManager";
2222
import { BuildFlags } from "@src/toolchain/BuildFlags";
2323
import { SwiftToolchain } from "@src/toolchain/toolchain";
24+
import { Version } from "@src/utilities/version";
2425

2526
import { instance, mockFn, mockGlobalObject, mockGlobalValue, mockObject } from "../../MockUtils";
2627

2728
suite("runSwiftScript Test Suite", () => {
2829
const mockTaskManager = mockObject<TaskManager>({ executeTaskAndWait: stub().resolves() });
2930
const mockToolchain = mockObject<SwiftToolchain>({
3031
getToolchainExecutable: () => "/usr/bin/swift",
32+
swiftVersion: new Version(6, 0, 0),
3133
buildFlags: instance(
3234
mockObject<BuildFlags>({
3335
withAdditionalFlags: mockFn(s => s.callsFake(args => args)),
@@ -93,7 +95,7 @@ suite("runSwiftScript Test Suite", () => {
9395
const mockWindow = mockGlobalObject(vscode, "window");
9496

9597
test("Executes run task with the users chosen swift version", async () => {
96-
config.setValue("5");
98+
config.setValue(() => "5");
9799

98100
await runSwiftScript(
99101
instance(createMockTextDocument()),
@@ -107,7 +109,7 @@ suite("runSwiftScript Test Suite", () => {
107109
});
108110

109111
test("Prompts for the users desired swift version", async () => {
110-
config.setValue("Ask Every Run");
112+
config.setValue(() => "Ask Every Run");
111113
const selectedItem = { value: "6", label: "Swift 6" };
112114
mockWindow.showQuickPick.resolves(selectedItem);
113115

@@ -123,7 +125,7 @@ suite("runSwiftScript Test Suite", () => {
123125
});
124126

125127
test("Exists when the user cancels the prompt", async () => {
126-
config.setValue("Ask Every Run");
128+
config.setValue(() => "Ask Every Run");
127129
mockWindow.showQuickPick.resolves(undefined);
128130

129131
await runSwiftScript(

0 commit comments

Comments
 (0)