Skip to content

Commit 83885b1

Browse files
authored
Support "swift.play" CodeLens (#1924)
* Support "swift.play" CodeLens - Advertise to the LSP we support "swift.play" - Add a "swift.play" command, hiding it from the command palette - Fix order of env variables passed to task so variables like DYLD_LIBRARY_PATH are not overwritten accidentally Issue: #1782 * Add copyright header * Add tests * Restructure to make more testable * Fix review comment
1 parent aafc500 commit 83885b1

File tree

7 files changed

+214
-4
lines changed

7 files changed

+214
-4
lines changed

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@
289289
"category": "Swift",
290290
"icon": "$(play)"
291291
},
292+
{
293+
"command": "swift.play",
294+
"title": "Run Swift playground",
295+
"category": "Swift",
296+
"icon": "$(play)"
297+
},
292298
{
293299
"command": "swift.debug",
294300
"title": "Debug Swift executable",
@@ -1372,6 +1378,10 @@
13721378
{
13731379
"command": "swift.openEducationalNote",
13741380
"when": "false"
1381+
},
1382+
{
1383+
"command": "swift.play",
1384+
"when": "false"
13751385
}
13761386
],
13771387
"editor/context": [

src/commands.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { reindexProject } from "./commands/reindexProject";
4242
import { resetPackage } from "./commands/resetPackage";
4343
import restartLSPServer from "./commands/restartLSPServer";
4444
import { runAllTests } from "./commands/runAllTests";
45+
import { runPlayground } from "./commands/runPlayground";
4546
import { runPluginTask } from "./commands/runPluginTask";
4647
import { runSwiftScript } from "./commands/runSwiftScript";
4748
import { runTask } from "./commands/runTask";
@@ -88,6 +89,7 @@ export function registerToolchainCommands(
8889
export enum Commands {
8990
RUN = "swift.run",
9091
DEBUG = "swift.debug",
92+
PLAY = "swift.play",
9193
CLEAN_BUILD = "swift.cleanBuild",
9294
RESOLVE_DEPENDENCIES = "swift.resolveDependencies",
9395
SHOW_FLAT_DEPENDENCIES_LIST = "swift.flatDependenciesList",
@@ -146,6 +148,13 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
146148
Commands.DEBUG,
147149
async target => await debugBuild(ctx, ...unwrapTreeItem(target))
148150
),
151+
vscode.commands.registerCommand(Commands.PLAY, async target => {
152+
const folder = ctx.currentFolder;
153+
if (!folder || !target) {
154+
return false;
155+
}
156+
return await runPlayground(folder, ctx.tasks, target);
157+
}),
149158
vscode.commands.registerCommand(Commands.CLEAN_BUILD, async () => await cleanBuild(ctx)),
150159
vscode.commands.registerCommand(
151160
Commands.RUN_TESTS_MULTIPLE_TIMES,

src/commands/runPlayground.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 * as vscode from "vscode";
15+
import { Location, Range } from "vscode-languageclient";
16+
17+
import { FolderContext } from "../FolderContext";
18+
import { createSwiftTask } from "../tasks/SwiftTaskProvider";
19+
import { TaskManager } from "../tasks/TaskManager";
20+
import { packageName } from "../utilities/tasks";
21+
22+
export interface PlaygroundItem {
23+
id: string;
24+
label?: string;
25+
}
26+
27+
export interface DocumentPlaygroundItem extends PlaygroundItem {
28+
range: Range;
29+
}
30+
31+
export interface WorkspacePlaygroundItem extends PlaygroundItem {
32+
location: Location;
33+
}
34+
35+
/**
36+
* Executes a {@link vscode.Task task} to run swift playground.
37+
*/
38+
export async function runPlayground(
39+
folderContext: FolderContext,
40+
tasks: TaskManager,
41+
item: PlaygroundItem
42+
) {
43+
const id = item.label ?? item.id;
44+
const task = createSwiftTask(
45+
["play", id],
46+
`Play "${id}"`,
47+
{
48+
cwd: folderContext.folder,
49+
scope: folderContext.workspaceFolder,
50+
packageName: packageName(folderContext),
51+
presentationOptions: { reveal: vscode.TaskRevealKind.Always },
52+
},
53+
folderContext.toolchain
54+
);
55+
56+
await tasks.executeTaskAndWait(task);
57+
return true;
58+
}

src/sourcekit-lsp/LanguageClientConfiguration.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function initializationOptions(swiftVersion: Version): any {
3636
supportedCommands: {
3737
"swift.run": "swift.run",
3838
"swift.debug": "swift.debug",
39+
"swift.play": "swift.play",
3940
},
4041
},
4142
};
@@ -254,6 +255,9 @@ export function lspClientOptions(
254255
case "swift.debug":
255256
codelens.command.title = `$(debug)\u00A0${codelens.command.title}`;
256257
break;
258+
case "swift.play":
259+
codelens.command.title = `$(play)\u00A0${codelens.command.title}`;
260+
break;
257261
}
258262
return codelens;
259263
});

src/tasks/SwiftTaskProvider.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,11 @@ export function createSwiftTask(
312312
} else {
313313
cwd = config.cwd.fsPath;
314314
}*/
315-
const env = { ...configuration.swiftEnvironmentVariables, ...swiftRuntimeEnv(), ...cmdEnv };
315+
const env = {
316+
...swiftRuntimeEnv(), // From process.env first
317+
...configuration.swiftEnvironmentVariables, // Then swiftEnvironmentVariables settings
318+
...cmdEnv, // Task configuration takes highest precedence
319+
};
316320
const presentation = config?.presentationOptions ?? {};
317321
if (config?.packageName) {
318322
name += ` (${config?.packageName})`;
@@ -469,9 +473,9 @@ export class SwiftTaskProvider implements vscode.TaskProvider {
469473
const env = platform?.env ?? task.definition.env;
470474
const fullCwd = resolveTaskCwd(task, platform?.cwd ?? task.definition.cwd);
471475
const fullEnv = {
472-
...configuration.swiftEnvironmentVariables,
473-
...swiftRuntimeEnv(),
474-
...env,
476+
...swiftRuntimeEnv(), // From process.env first
477+
...configuration.swiftEnvironmentVariables, // Then swiftEnvironmentVariables settings
478+
...env, // Task configuration takes highest precedence
475479
};
476480

477481
const presentation = task.definition.presentation ?? task.presentationOptions ?? {};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 { stub } from "sinon";
16+
import * as vscode from "vscode";
17+
18+
import { FolderContext } from "@src/FolderContext";
19+
import { WorkspaceContext } from "@src/WorkspaceContext";
20+
import { Commands } from "@src/commands";
21+
import { runPlayground } from "@src/commands/runPlayground";
22+
import { SwiftTask } from "@src/tasks/SwiftTaskProvider";
23+
import { TaskManager } from "@src/tasks/TaskManager";
24+
25+
import { MockedObject, instance, mockObject } from "../../MockUtils";
26+
import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities";
27+
28+
suite("Run Playground Command", function () {
29+
let folderContext: FolderContext;
30+
let workspaceContext: WorkspaceContext;
31+
let mockTaskManager: MockedObject<TaskManager>;
32+
33+
activateExtensionForSuite({
34+
async setup(ctx) {
35+
workspaceContext = ctx;
36+
folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext);
37+
},
38+
});
39+
40+
setup(async () => {
41+
await workspaceContext.focusFolder(folderContext);
42+
mockTaskManager = mockObject<TaskManager>({ executeTaskAndWait: stub().resolves() });
43+
});
44+
45+
suite("Command", () => {
46+
test("Succeeds", async () => {
47+
expect(
48+
await vscode.commands.executeCommand(Commands.PLAY, {
49+
id: "PackageLib/PackageLib.swift:3",
50+
})
51+
).to.be.true;
52+
});
53+
54+
test("No playground item provided", async () => {
55+
expect(await vscode.commands.executeCommand(Commands.PLAY), undefined).to.be.false;
56+
});
57+
58+
test("No folder focussed", async () => {
59+
await workspaceContext.focusFolder(null);
60+
expect(
61+
await vscode.commands.executeCommand(Commands.PLAY, {
62+
id: "PackageLib/PackageLib.swift:3",
63+
})
64+
).to.be.false;
65+
});
66+
});
67+
68+
suite("Arguments", () => {
69+
test('Runs "swift play" on "id"', async () => {
70+
expect(
71+
await runPlayground(folderContext, instance(mockTaskManager), {
72+
id: "PackageLib/PackageLib.swift:3",
73+
})
74+
).to.be.true;
75+
expect(mockTaskManager.executeTaskAndWait).to.have.been.calledOnce;
76+
77+
const task = mockTaskManager.executeTaskAndWait.args[0][0] as SwiftTask;
78+
expect(task.execution.args).to.deep.equal(["play", "PackageLib/PackageLib.swift:3"]);
79+
expect(task.execution.options.cwd).to.equal(folderContext.folder.fsPath);
80+
});
81+
82+
test('Runs "swift play" on "id" with space in path', async () => {
83+
expect(
84+
await runPlayground(folderContext, instance(mockTaskManager), {
85+
id: "PackageLib/Package Lib.swift:3",
86+
})
87+
).to.be.true;
88+
expect(mockTaskManager.executeTaskAndWait).to.have.been.calledOnce;
89+
90+
const task = mockTaskManager.executeTaskAndWait.args[0][0] as SwiftTask;
91+
expect(task.execution.args).to.deep.equal(["play", "PackageLib/Package Lib.swift:3"]);
92+
expect(task.execution.options.cwd).to.equal(folderContext.folder.fsPath);
93+
});
94+
95+
test('Runs "swift play" on "label"', async () => {
96+
expect(
97+
await runPlayground(folderContext, instance(mockTaskManager), {
98+
id: "PackageLib/PackageLib.swift:3",
99+
label: "bar",
100+
})
101+
).to.be.true;
102+
expect(mockTaskManager.executeTaskAndWait).to.have.been.calledOnce;
103+
104+
const task = mockTaskManager.executeTaskAndWait.args[0][0] as SwiftTask;
105+
expect(task.execution.args).to.deep.equal(["play", "bar"]);
106+
expect(task.execution.options.cwd).to.equal(folderContext.folder.fsPath);
107+
});
108+
});
109+
});

test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,14 @@ suite("LanguageClientManager Suite", () => {
547547
},
548548
isResolved: true,
549549
},
550+
{
551+
range: new vscode.Range(0, 0, 0, 0),
552+
command: {
553+
title: 'Play "bar"',
554+
command: "swift.play",
555+
},
556+
isResolved: true,
557+
},
550558
{
551559
range: new vscode.Range(0, 0, 0, 0),
552560
command: {
@@ -588,6 +596,14 @@ suite("LanguageClientManager Suite", () => {
588596
},
589597
isResolved: true,
590598
},
599+
{
600+
range: new vscode.Range(0, 0, 0, 0),
601+
command: {
602+
title: '$(play)\u00A0Play "bar"',
603+
command: "swift.play",
604+
},
605+
isResolved: true,
606+
},
591607
{
592608
range: new vscode.Range(0, 0, 0, 0),
593609
command: {

0 commit comments

Comments
 (0)