Skip to content

Support for multi-root .code-workspace workspaces #1566

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 16, 2025
Merged
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
36 changes: 36 additions & 0 deletions .vscode-test.js
Original file line number Diff line number Diff line change
@@ -89,6 +89,42 @@ module.exports = defineConfig({
reuseMachineInstall: !isCIBuild,
installExtensions,
},
{
label: "codeWorkspaceTests",
files: [
"dist/test/common.js",
"dist/test/integration-tests/extension.test.js",
"dist/test/integration-tests/WorkspaceContext.test.js",
"dist/test/integration-tests/tasks/**/*.test.js",
"dist/test/integration-tests/commands/build.test.js",
"dist/test/integration-tests/testexplorer/TestExplorerIntegration.test.js",
"dist/test/integration-tests/commands/dependency.test.js",
],
version: process.env["VSCODE_VERSION"] ?? "stable",
workspaceFolder: "./assets/test.code-workspace",
launchArgs,
extensionDevelopmentPath: vsixPath
? [`${__dirname}/.vscode-test/extensions/${publisher}.${name}-${version}`]
: undefined,
mocha: {
ui: "tdd",
color: true,
timeout,
forbidOnly: isCIBuild,
grep: isFastTestRun ? "@slow" : undefined,
invert: isFastTestRun,
slow: 10000,
retries: 1,
reporter: path.join(__dirname, ".mocha-reporter.js"),
reporterOptions: {
jsonReporterOptions: {
output: path.join(__dirname, "test-results", "code-workspace-tests.json"),
},
},
},
reuseMachineInstall: !isCIBuild,
installExtensions,
},
{
label: "unitTests",
files: ["dist/test/common.js", "dist/test/unit-tests/**/*.test.js"],
13 changes: 13 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -29,6 +29,19 @@
},
"preLaunchTask": "compile-tests"
},
{
"name": "Code Workspace Tests",
"type": "extensionHost",
"request": "launch",
"testConfiguration": "${workspaceFolder}/.vscode-test.js",
"testConfigurationLabel": "codeWorkspaceTests",
"args": ["--profile=testing-debug"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"env": {
"VSCODE_DEBUG": "1"
},
"preLaunchTask": "compile-tests"
},
{
"name": "Unit Tests",
"type": "extensionHost",
144 changes: 144 additions & 0 deletions assets/test.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
{
"folders": [
{
"name": "diagnostics",
"path": "./test/diagnostics"
},
{
"name": "dependencies",
"path": "./test/dependencies"
},
{
"name": "command-plugin",
"path": "./test/command-plugin"
},
{
"name": "defaultPackage",
"path": "./test/defaultPackage"
}
],
"settings": {
"swift.disableAutoResolve": true,
"swift.autoGenerateLaunchConfigurations": false,
"swift.debugger.debugAdapter": "lldb-dap",
"swift.debugger.setupCodeLLDB": "alwaysUpdateGlobal",
"swift.additionalTestArguments": [
"-Xswiftc",
"-DTEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING"
],
"lldb.verboseLogging": true,
"lldb.launch.terminal": "external",
"lldb-dap.detachOnError": true,
"swift.sourcekit-lsp.backgroundIndexing": "off"
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"type": "swift",
"args": [
"build",
"--build-tests",
"--verbose",
"-Xswiftc",
"-DBAR"
],
"cwd": "${workspaceFolder:defaultPackage}",
"group": {
"kind": "build",
"isDefault": true
},
"label": "swift: Build All (defaultPackage) (workspace)",
"detail": "swift build --build-tests --verbose -Xswiftc -DBAR"
},
{
"type": "swift",
"args": [
"build",
"--show-bin-path"
],
"cwd": "${workspaceFolder:defaultPackage}",
"group": "build",
"label": "swift: Build All from code workspace",
"detail": "swift build --show-bin-path"
},
{
"type": "swift-plugin",
"command": "command_plugin",
"args": [
"--foo"
],
"cwd": "${workspaceFolder:command-plugin}",
"disableSandbox": true,
"label": "swift: command-plugin from code workspace",
"detail": "swift package command_plugin --foo"
},
{
"type": "swift",
"args": [
"build",
"--product",
"PackageExe",
"-Xswiftc",
"-diagnostic-style=llvm",
"-Xswiftc",
"-DBAR"
],
"cwd": "${workspaceFolder:defaultPackage}",
"group": "build",
"label": "swift: Build Debug PackageExe (defaultPackage) (workspace)",
"detail": "swift build --product PackageExe -Xswiftc -diagnostic-style=llvm -Xswiftc -DBAR"
},
{
"type": "swift",
"args": [
"build",
"-c",
"release",
"--product",
"PackageExe",
"-Xswiftc",
"-diagnostic-style=llvm",
"-Xswiftc",
"-DBAR"
],
"cwd": "${workspaceFolder:defaultPackage}",
"group": "build",
"label": "swift: Build Release PackageExe (defaultPackage) (workspace)",
"detail": "swift build -c release --product PackageExe -Xswiftc -diagnostic-style=llvm -Xswiftc -DBAR"
}
]
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"type": "swift",
"request": "launch",
"name": "Debug PackageExe (defaultPackage) (workspace)",
"program": "${workspaceFolder:defaultPackage}/.build/debug/PackageExe",
"args": [],
"cwd": "${workspaceFolder:defaultPackage}",
"preLaunchTask": "swift: Build Debug PackageExe (defaultPackage) (workspace)",
"disableASLR": false,
"initCommands": [
"settings set target.disable-aslr false"
]
},
{
"type": "swift",
"request": "launch",
"name": "Release PackageExe (defaultPackage) (workspace)",
"program": "${workspaceFolder:defaultPackage}/.build/release/PackageExe",
"args": [],
"cwd": "${workspaceFolder:defaultPackage}",
"preLaunchTask": "swift: Build Release PackageExe (defaultPackage) (workspace)",
"disableASLR": false,
"initCommands": [
"settings set target.disable-aslr false"
]
}
],
"compounds": []
}
}
10 changes: 7 additions & 3 deletions assets/test/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -6,15 +6,17 @@
"args": [
"build",
"--build-tests",
"--verbose"
"--verbose",
"-Xswiftc",
"-DFOO"
],
"cwd": "defaultPackage",
"problemMatcher": [
"$swiftc"
],
"group": "build",
"label": "swift: Build All (defaultPackage)",
"detail": "swift build --build-tests --verbose"
"detail": "swift build --build-tests --verbose -Xswiftc -DFOO"
},
{
"type": "swift",
@@ -33,7 +35,9 @@
{
"type": "swift-plugin",
"command": "command_plugin",
"args": ["--foo"],
"args": [
"--foo"
],
"cwd": "command-plugin",
"disableSandbox": true,
"problemMatcher": [
62 changes: 62 additions & 0 deletions assets/test/defaultPackage/.vscode/launch copy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"configurations": [
{
"type": "swift",
"request": "launch",
"name": "Attach to Swift Executable",
"program": "${workspaceFolder}/.build/aarch64-unknown-linux-gnu/debug/PackageExe",
// "attachCommands": [
// // "gdb-remote 1234",
// // "process launch"
// "platform select remote-linux",
// "platform connect connect://127.0.0.1:1234"
// ],
// "initCommands": [
// "target create .build/aarch64-unknown-linux-gnu/debug/PackageExe",
// "b main.swift:7",
// ]
"initCommands": [
"platform select remote-linux",
// "platform connect connect://127.0.0.1:1234",
"gdb-remote 1234",
"settings set target.inherit-env false"
]
},
{
"type": "swift",
"request": "launch",
"name": "Debug package1",
"program": "${workspaceFolder:defaultPackage}/.build/debug/package1",
"args": [],
"cwd": "${workspaceFolder:defaultPackage}",
"preLaunchTask": "swift: Build Debug package1"
},
{
"type": "swift",
"request": "launch",
"name": "Release package1",
"program": "${workspaceFolder:defaultPackage}/.build/release/package1",
"args": [],
"cwd": "${workspaceFolder:defaultPackage}",
"preLaunchTask": "swift: Build Release package1"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:defaultPackage}",
"name": "Debug PackageExe",
"program": "${workspaceFolder:defaultPackage}/.build/debug/PackageExe",
"preLaunchTask": "swift: Build Debug PackageExe"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:defaultPackage}",
"name": "Release PackageExe",
"program": "${workspaceFolder:defaultPackage}/.build/release/PackageExe",
"preLaunchTask": "swift: Build Release PackageExe"
}
]
}
19 changes: 19 additions & 0 deletions scripts/test_windows.ps1
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
function Update-SwiftBuildAndPackageArguments {
param (
[string]$jsonFilePath = "./assets/test/.vscode/settings.json",
[string]$codeWorkspaceFilePath = "./assets/test.code-workspace",
[string]$windowsSdkVersion = "10.0.22000.0",
[string]$vcToolsVersion = "14.43.34808"
)
@@ -28,9 +29,17 @@ function Update-SwiftBuildAndPackageArguments {
exit 1
}

try {
$codeWorkspaceContent = Get-Content -Raw -Path $codeWorkspaceFilePath | ConvertFrom-Json
} catch {
Write-Host "Invalid JSON content in $codeWorkspaceFilePath"
exit 1
}

if ($jsonContent.PSObject.Properties['swift.buildArguments']) {
$jsonContent.PSObject.Properties.Remove('swift.buildArguments')
}


$jsonContent | Add-Member -MemberType NoteProperty -Name "swift.buildArguments" -Value @(
"-Xbuild-tools-swiftc", "-windows-sdk-root", "-Xbuild-tools-swiftc", $windowsSdkRoot,
@@ -54,10 +63,20 @@ function Update-SwiftBuildAndPackageArguments {
"-Xswiftc", "-visualc-tools-version", "-Xswiftc", $vcToolsVersion
)


$codeWorkspaceContent.PSObject.Properties.Remove('settings')
$codeWorkspaceContent | Add-Member -MemberType NoteProperty -Name "settings" -Value $jsonContent

$jsonContent | ConvertTo-Json -Depth 32 | Set-Content -Path $jsonFilePath


$codeWorkspaceContent | ConvertTo-Json -Depth 32 | Set-Content -Path $codeWorkspaceFilePath

Write-Host "Contents of ${jsonFilePath}:"
Get-Content -Path $jsonFilePath

Write-Host "Contents of ${codeWorkspaceFilePath}:"
Get-Content -Path $codeWorkspaceFilePath
}

$swiftVersionOutput = & swift --version
5 changes: 3 additions & 2 deletions src/TestExplorer/TestRunner.ts
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ import { reduceTestItemChildren } from "./TestUtils";
import { CompositeCancellationToken } from "../utilities/cancellation";
// eslint-disable-next-line @typescript-eslint/no-require-imports
import stripAnsi = require("strip-ansi");
import { packageName, resolveScope } from "../utilities/tasks";

export enum TestLibrary {
xctest = "XCTest",
@@ -773,8 +774,8 @@ export class TestRunner {
`Building and Running Tests${kindLabel}`,
{
cwd: this.folderContext.folder,
scope: this.folderContext.workspaceFolder,
prefix: this.folderContext.name,
scope: resolveScope(this.folderContext.workspaceFolder),
packageName: packageName(this.folderContext),
presentationOptions: { reveal: vscode.TaskRevealKind.Never },
},
this.folderContext.toolchain,
9 changes: 7 additions & 2 deletions src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import { debugLaunchConfig, getLaunchConfiguration } from "../debugger/launch";
import { executeTaskWithUI } from "./utilities";
import { FolderContext } from "../FolderContext";
import { Target } from "../SwiftPackage";
import { packageName } from "../utilities/tasks";

/**
* Executes a {@link vscode.Task task} to run swift target.
@@ -56,7 +57,7 @@ export async function folderCleanBuild(folderContext: FolderContext) {
{
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
prefix: folderContext.name,
packageName: packageName(folderContext),
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
group: vscode.TaskGroup.Clean,
},
@@ -111,7 +112,11 @@ export async function debugBuildWithOptions(
const launchConfig = getLaunchConfiguration(target.name, current);
if (launchConfig) {
ctx.buildStarted(target.name, launchConfig, options);
const result = await debugLaunchConfig(current.workspaceFolder, launchConfig, options);
const result = await debugLaunchConfig(
vscode.workspace.workspaceFile ? undefined : current.workspaceFolder,
launchConfig,
options
);
ctx.buildFinished(target.name, launchConfig, options);
return result;
}
3 changes: 2 additions & 1 deletion src/commands/dependencies/edit.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import * as vscode from "vscode";
import { createSwiftTask } from "../../tasks/SwiftTaskProvider";
import { FolderOperation, WorkspaceContext } from "../../WorkspaceContext";
import { executeTaskWithUI } from "../utilities";
import { packageName } from "../../utilities/tasks";

/**
* Setup package dependency to be edited
@@ -34,7 +35,7 @@ export async function editDependency(identifier: string, ctx: WorkspaceContext)
{
scope: currentFolder.workspaceFolder,
cwd: currentFolder.folder,
prefix: currentFolder.name,
packageName: packageName(currentFolder),
},
currentFolder.toolchain
);
3 changes: 2 additions & 1 deletion src/commands/dependencies/resolve.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import { FolderContext } from "../../FolderContext";
import { createSwiftTask, SwiftTaskProvider } from "../../tasks/SwiftTaskProvider";
import { WorkspaceContext } from "../../WorkspaceContext";
import { executeTaskWithUI, updateAfterError } from "../utilities";
import { packageName } from "../../utilities/tasks";

/**
* Executes a {@link vscode.Task task} to resolve this package's dependencies.
@@ -43,7 +44,7 @@ export async function resolveFolderDependencies(
{
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
prefix: folderContext.name,
packageName: packageName(folderContext),
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
},
folderContext.toolchain
3 changes: 2 additions & 1 deletion src/commands/dependencies/update.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import { FolderContext } from "../../FolderContext";
import { WorkspaceContext } from "../../WorkspaceContext";
import { createSwiftTask, SwiftTaskProvider } from "../../tasks/SwiftTaskProvider";
import { executeTaskWithUI, updateAfterError } from "./../utilities";
import { packageName } from "../../utilities/tasks";

/**
* Executes a {@link vscode.Task task} to update this package's dependencies.
@@ -41,7 +42,7 @@ export async function updateFolderDependencies(folderContext: FolderContext) {
{
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
prefix: folderContext.name,
packageName: packageName(folderContext),
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
},
folderContext.toolchain
3 changes: 2 additions & 1 deletion src/commands/dependencies/useLocal.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import * as vscode from "vscode";
import { FolderOperation, WorkspaceContext } from "../../WorkspaceContext";
import { createSwiftTask } from "../../tasks/SwiftTaskProvider";
import { executeTaskWithUI } from "../utilities";
import { packageName } from "../../utilities/tasks";

/**
* Use local version of package dependency
@@ -61,7 +62,7 @@ export async function useLocalDependency(
{
scope: currentFolder.workspaceFolder,
cwd: currentFolder.folder,
prefix: currentFolder.name,
packageName: packageName(currentFolder),
},
currentFolder.toolchain
);
5 changes: 3 additions & 2 deletions src/commands/resetPackage.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import { FolderContext } from "../FolderContext";
import { createSwiftTask, SwiftTaskProvider } from "../tasks/SwiftTaskProvider";
import { WorkspaceContext } from "../WorkspaceContext";
import { executeTaskWithUI } from "./utilities";
import { packageName } from "../utilities/tasks";

/**
* Executes a {@link vscode.Task task} to reset the complete cache/build directory.
@@ -40,7 +41,7 @@ export async function folderResetPackage(folderContext: FolderContext) {
{
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
prefix: folderContext.name,
packageName: packageName(folderContext),
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
group: vscode.TaskGroup.Clean,
},
@@ -71,7 +72,7 @@ export async function folderResetPackage(folderContext: FolderContext) {
{
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
prefix: folderContext.name,
packageName: packageName(folderContext),
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
},
folderContext.toolchain
7 changes: 7 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
@@ -510,6 +510,13 @@ function computeVscodeVar(varName: string): string | null {

const file = () => vscode.window.activeTextEditor?.document?.uri?.fsPath || "";

const regex = /workspaceFolder:(.*)/gm;
const match = regex.exec(varName);
if (match) {
const name = match[1];
return vscode.workspace.workspaceFolders?.find(f => f.name === name)?.uri.fsPath ?? null;
}

// https://code.visualstudio.com/docs/editor/variables-reference
// Variables to be substituted should be added here.
const supportedVariables: { [k: string]: () => string } = {
12 changes: 7 additions & 5 deletions src/debugger/buildConfig.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import { TestLibrary } from "../TestExplorer/TestRunner";
import { TestKind, isDebugging, isRelease } from "../TestExplorer/TestKind";
import { buildOptions } from "../tasks/SwiftTaskProvider";
import { updateLaunchConfigForCI } from "./lldb";
import { packageName } from "../utilities/tasks";

export class BuildConfigurationFactory {
public static buildAll(
@@ -719,13 +720,14 @@ export function getFolderAndNameSuffix(
? ctx.workspaceFolder.uri.fsPath
: `\${workspaceFolder:${ctx.workspaceFolder.name}}`;
let folder: string;
let nameSuffix: string;
if (ctx.relativePath.length === 0) {
let nameSuffix;
const pkgName = packageName(ctx);
if (pkgName) {
folder = nodePath.join(workspaceFolder, ctx.relativePath);
nameSuffix = ` (${packageName(ctx)})`;
} else {
folder = workspaceFolder;
nameSuffix = "";
} else {
folder = nodePath.join(workspaceFolder, ctx.relativePath);
nameSuffix = ` (${ctx.relativePath})`;
}
return { folder: folder, nameSuffix: nameSuffix };
}
17 changes: 12 additions & 5 deletions src/debugger/launch.ts
Original file line number Diff line number Diff line change
@@ -50,7 +50,9 @@ export async function makeDebugConfigurations(
return false;
}

const wsLaunchSection = vscode.workspace.getConfiguration("launch", ctx.folder);
const wsLaunchSection = vscode.workspace.workspaceFile
? vscode.workspace.getConfiguration("launch")
: vscode.workspace.getConfiguration("launch", ctx.folder);
const launchConfigs = wsLaunchSection.get<vscode.DebugConfiguration[]>("configurations") || [];

// Determine which launch configurations need updating/creating
@@ -111,7 +113,9 @@ export async function makeDebugConfigurations(
await wsLaunchSection.update(
"configurations",
launchConfigs,
vscode.ConfigurationTarget.WorkspaceFolder
vscode.workspace.workspaceFile
? vscode.ConfigurationTarget.Workspace
: vscode.ConfigurationTarget.WorkspaceFolder
);
return true;
}
@@ -121,7 +125,9 @@ export function getLaunchConfiguration(
target: string,
folderCtx: FolderContext
): vscode.DebugConfiguration | undefined {
const wsLaunchSection = vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder);
const wsLaunchSection = vscode.workspace.workspaceFile
? vscode.workspace.getConfiguration("launch")
: vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder);
const launchConfigs = wsLaunchSection.get<vscode.DebugConfiguration[]>("configurations") || [];
const { folder } = getFolderAndNameSuffix(folderCtx);
const targetPath = path.join(
@@ -131,9 +137,10 @@ export function getLaunchConfiguration(
);
// Users could be on different platforms with different path annotations,
// so normalize before we compare.
return launchConfigs.find(
const launchConfig = launchConfigs.find(
config => path.normalize(config.program) === path.normalize(targetPath)
);
return launchConfig;
}

// Return array of DebugConfigurations for executables based on what is in Package.swift
@@ -201,7 +208,7 @@ export function createSnippetConfiguration(
* @param workspaceFolder Workspace to run debugger in
*/
export async function debugLaunchConfig(
workspaceFolder: vscode.WorkspaceFolder,
workspaceFolder: vscode.WorkspaceFolder | undefined,
config: vscode.DebugConfiguration,
options: vscode.DebugSessionOptions = {}
) {
13 changes: 4 additions & 9 deletions src/tasks/SwiftPluginTaskProvider.ts
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ import { WorkspaceContext } from "../WorkspaceContext";
import { PackagePlugin } from "../SwiftPackage";
import { swiftRuntimeEnv } from "../utilities/utilities";
import { SwiftExecution } from "../tasks/SwiftExecution";
import { resolveTaskCwd } from "../utilities/tasks";
import { packageName, resolveTaskCwd } from "../utilities/tasks";
import configuration, {
PluginPermissionConfiguration,
substituteVariablesInString,
@@ -31,7 +31,7 @@ interface TaskConfig {
cwd: vscode.Uri;
scope: vscode.WorkspaceFolder;
presentationOptions?: vscode.TaskPresentationOptions;
prefix?: string;
packageName?: string;
}

/**
@@ -62,6 +62,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider {
presentationOptions: {
reveal: vscode.TaskRevealKind.Always,
},
packageName: packageName(folderContext),
})
);
}
@@ -150,13 +151,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider {
}),
[]
);
let prefix: string;
if (config.prefix) {
prefix = `(${config.prefix}) `;
} else {
prefix = "";
}
task.detail = `${prefix}swift ${swiftArgs.join(" ")}`;
task.detail = `swift ${swiftArgs.join(" ")}`;
task.presentationOptions = presentation;
return task as SwiftTask;
}
48 changes: 24 additions & 24 deletions src/tasks/SwiftTaskProvider.ts
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ import { swiftRuntimeEnv } from "../utilities/utilities";
import { Version } from "../utilities/version";
import { SwiftToolchain } from "../toolchain/toolchain";
import { SwiftExecution } from "../tasks/SwiftExecution";
import { resolveTaskCwd } from "../utilities/tasks";
import { packageName, resolveScope, resolveTaskCwd } from "../utilities/tasks";
import { BuildConfigurationFactory } from "../debugger/buildConfig";

/**
@@ -44,7 +44,7 @@ interface TaskConfig {
scope: vscode.TaskScope | vscode.WorkspaceFolder;
group?: vscode.TaskGroup;
presentationOptions?: vscode.TaskPresentationOptions;
prefix?: string;
packageName?: string;
disableTaskQueue?: boolean;
dontTriggerTestDiscovery?: boolean;
showBuildStatus?: ShowBuildStatusOptions;
@@ -139,8 +139,9 @@ function buildAllTaskName(folderContext: FolderContext, release: boolean): strin
let buildTaskName = release
? `${SwiftTaskProvider.buildAllName} - Release`
: SwiftTaskProvider.buildAllName;
if (folderContext.relativePath.length > 0) {
buildTaskName += ` (${folderContext.relativePath})`;
const packageNamePostfix = packageName(folderContext);
if (packageNamePostfix) {
buildTaskName += ` (${packageNamePostfix})`;
}
return buildTaskName;
}
@@ -160,7 +161,7 @@ export async function createBuildAllTask(
{
group: vscode.TaskGroup.Build,
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
scope: resolveScope(folderContext.workspaceFolder),
presentationOptions: {
reveal: getBuildRevealOption(),
},
@@ -189,11 +190,17 @@ export async function getBuildAllTask(
// search for build all task in task.json first, that are valid for folder
const tasks = await vscode.tasks.fetchTasks();
const workspaceTasks = tasks.filter(task => {
if (task.source !== "Workspace" || task.scope !== folderContext.workspaceFolder) {
if (task.source !== "Workspace") {
return false;
}
const swiftExecutionOptions = (task.execution as SwiftExecution).options;
let cwd = swiftExecutionOptions?.cwd;
if (task.scope === vscode.TaskScope.Workspace) {
return cwd && substituteVariablesInString(cwd) === folderContext.folder.fsPath;
}
if (task.scope !== folderContext.workspaceFolder) {
return false;
}
if (cwd === "${workspaceFolder}" || cwd === undefined) {
cwd = folderWorkingDir;
}
@@ -232,40 +239,37 @@ export async function getBuildAllTask(
*/
function createBuildTasks(product: Product, folderContext: FolderContext): vscode.Task[] {
const toolchain = folderContext.toolchain;
let buildTaskNameSuffix = "";
if (folderContext.relativePath.length > 0) {
buildTaskNameSuffix = ` (${folderContext.relativePath})`;
}

const buildDebugName = `Build Debug ${product.name}${buildTaskNameSuffix}`;
const buildDebugName = `Build Debug ${product.name}`;
const buildDebugTask = createSwiftTask(
["build", "--product", product.name, ...buildOptions(toolchain)],
buildDebugName,
{
group: vscode.TaskGroup.Build,
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
scope: resolveScope(folderContext.workspaceFolder),
presentationOptions: {
reveal: getBuildRevealOption(),
},
packageName: packageName(folderContext),
disableTaskQueue: true,
dontTriggerTestDiscovery: true,
},
folderContext.toolchain
);
const buildDebug = buildAllTaskCache.get(buildDebugName, folderContext, buildDebugTask);

const buildReleaseName = `Build Release ${product.name}${buildTaskNameSuffix}`;
const buildReleaseName = `Build Release ${product.name}`;
const buildReleaseTask = createSwiftTask(
["build", "-c", "release", "--product", product.name, ...buildOptions(toolchain, false)],
`Build Release ${product.name}${buildTaskNameSuffix}`,
`Build Release ${product.name}`,
{
group: vscode.TaskGroup.Build,
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
presentationOptions: {
reveal: getBuildRevealOption(),
},
packageName: packageName(folderContext),
disableTaskQueue: true,
dontTriggerTestDiscovery: true,
},
@@ -306,6 +310,9 @@ export function createSwiftTask(
}*/
const env = { ...configuration.swiftEnvironmentVariables, ...swiftRuntimeEnv(), ...cmdEnv };
const presentation = config?.presentationOptions ?? {};
if (config?.packageName) {
name += ` (${config?.packageName})`;
}
const task = new vscode.Task(
{
type: "swift",
@@ -334,14 +341,7 @@ export function createSwiftTask(
);
// This doesn't include any quotes added by VS Code.
// See also: https://github.com/microsoft/vscode/issues/137895

let prefix: string;
if (config?.prefix) {
prefix = `(${config.prefix}) `;
} else {
prefix = "";
}
task.detail = `${prefix}swift ${args.join(" ")}`;
task.detail = `swift ${args.join(" ")}`;
task.group = config?.group;
task.presentationOptions = presentation;
return task as SwiftTask;
@@ -404,7 +404,7 @@ export class SwiftTaskProvider implements vscode.TaskProvider {
type: "swift",
args: [],
},
folderContext.workspaceFolder,
resolveScope(folderContext.workspaceFolder),
buildTaskName,
"swift",
new vscode.CustomExecution(() => {
8 changes: 1 addition & 7 deletions src/ui/StatusItem.ts
Original file line number Diff line number Diff line change
@@ -13,18 +13,12 @@
//===----------------------------------------------------------------------===//

import * as vscode from "vscode";
import * as path from "path";

export class RunningTask {
constructor(public task: vscode.Task | string) {}
get name(): string {
if (typeof this.task !== "string") {
const folder = this.task.definition.cwd as string;
if (folder) {
return `${this.task.name} (${path.basename(folder)})`;
} else {
return this.task.name;
}
return this.task.name;
} else {
return this.task;
}
21 changes: 21 additions & 0 deletions src/utilities/tasks.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@
//===----------------------------------------------------------------------===//
import * as path from "path";
import * as vscode from "vscode";
import { substituteVariablesInString } from "../configuration";
import { FolderContext } from "../FolderContext";

export const lineBreakRegex = /\r\n|\n|\r/gm;

@@ -22,6 +24,10 @@ export function resolveTaskCwd(task: vscode.Task, cwd?: string): string | undefi
return scopeWorkspaceFolder;
}

if (/\$\{.*\}/g.test(cwd)) {
return substituteVariablesInString(cwd);
}

if (path.isAbsolute(cwd)) {
return cwd;
} else if (scopeWorkspaceFolder) {
@@ -53,3 +59,18 @@ export function checkIfBuildComplete(line: string): boolean {
}
return false;
}

export function packageName(folderContext: FolderContext): string | undefined {
if (vscode.workspace.workspaceFile) {
return folderContext.name;
} else if (folderContext.relativePath.length > 0) {
return folderContext.relativePath;
}
}

export function resolveScope(scope: vscode.WorkspaceFolder | vscode.TaskScope) {
if (vscode.workspace.workspaceFile) {
return vscode.TaskScope.Workspace;
}
return scope;
}
21 changes: 13 additions & 8 deletions test/integration-tests/WorkspaceContext.test.ts
Original file line number Diff line number Diff line change
@@ -20,9 +20,14 @@ import { FolderOperation, WorkspaceContext } from "../../src/WorkspaceContext";
import { createBuildAllTask } from "../../src/tasks/SwiftTaskProvider";
import { Version } from "../../src/utilities/version";
import { SwiftExecution } from "../../src/tasks/SwiftExecution";
import { activateExtensionForSuite, updateSettings } from "./utilities/testutilities";
import {
activateExtensionForSuite,
getRootWorkspaceFolder,
updateSettings,
} from "./utilities/testutilities";
import { FolderContext } from "../../src/FolderContext";
import { assertContains } from "./testexplorer/utilities";
import { resolveScope } from "../../src/utilities/tasks";

function assertContainsArg(execution: SwiftExecution, arg: string) {
assert(execution?.args.find(a => a === arg));
@@ -45,7 +50,7 @@ suite("WorkspaceContext Test Suite", () => {
workspaceContext = ctx;
},
// No default assets as we want to verify against a clean workspace.
testAssets: [],
testAssets: ["defaultPackage"],
});

test("Add", async () => {
@@ -60,7 +65,7 @@ suite("WorkspaceContext Test Suite", () => {
recordedFolders.push(changedFolderRecord);
});

const workspaceFolder = vscode.workspace.workspaceFolders?.values().next().value;
const workspaceFolder = getRootWorkspaceFolder();

assert.ok(workspaceFolder, "No workspace folders found in workspace");

@@ -102,7 +107,7 @@ suite("WorkspaceContext Test Suite", () => {
});

// Was hitting a timeout in suiteSetup during CI build once in a while
this.timeout(5000);
this.timeout(15000);

test("Default Task values", async () => {
const folder = workspaceContext.folders.find(
@@ -120,7 +125,7 @@ suite("WorkspaceContext Test Suite", () => {
assertContainsArg(execution, "--build-tests");
assertContainsArg(execution, "-Xswiftc");
assertContainsArg(execution, "-diagnostic-style=llvm");
assert.strictEqual(buildAllTask.scope, folder.workspaceFolder);
assert.strictEqual(buildAllTask.scope, resolveScope(folder.workspaceFolder));
});

test('"default" diagnosticsStyle', async () => {
@@ -138,7 +143,7 @@ suite("WorkspaceContext Test Suite", () => {
assertContainsArg(execution, "build");
assertContainsArg(execution, "--build-tests");
assertNotContainsArg(execution, "-diagnostic-style");
assert.strictEqual(buildAllTask.scope, folder.workspaceFolder);
assert.strictEqual(buildAllTask.scope, resolveScope(folder.workspaceFolder));
});

test('"swift" diagnosticsStyle', async () => {
@@ -157,7 +162,7 @@ suite("WorkspaceContext Test Suite", () => {
assertContainsArg(execution, "--build-tests");
assertContainsArg(execution, "-Xswiftc");
assertContainsArg(execution, "-diagnostic-style=swift");
assert.strictEqual(buildAllTask.scope, folder.workspaceFolder);
assert.strictEqual(buildAllTask.scope, resolveScope(folder.workspaceFolder));
});

test("Build Settings", async () => {
@@ -244,4 +249,4 @@ suite("WorkspaceContext Test Suite", () => {
});
}).timeout(1000);
});
}).timeout(10000);
}).timeout(15000);
3 changes: 2 additions & 1 deletion test/integration-tests/commands/build.test.ts
Original file line number Diff line number Diff line change
@@ -68,7 +68,8 @@ suite("Build Commands @slow", function () {
// NB: "stopped" is the exact command when debuggee has stopped due to break point,
// but "stackTrace" is the deterministic sync point we will use to make sure we can execute continue
const bpPromise = waitForDebugAdapterRequest(
"Debug PackageExe (defaultPackage)",
"Debug PackageExe (defaultPackage)" +
(vscode.workspace.workspaceFile ? " (workspace)" : ""),
workspaceContext.globalToolchain.swiftVersion,
"stackTrace"
);
1 change: 1 addition & 0 deletions test/integration-tests/commands/dependency.test.ts
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ suite("Dependency Commmands Test Suite", function () {
workspaceContext = ctx;
depsContext = await folderInRootWorkspace("dependencies", workspaceContext);
},
testAssets: ["dependencies"],
});

setup(async () => {
11 changes: 7 additions & 4 deletions test/integration-tests/configuration.test.ts
Original file line number Diff line number Diff line change
@@ -12,9 +12,12 @@
//
//===----------------------------------------------------------------------===//

import * as vscode from "vscode";
import * as path from "path";
import { activateExtensionForSuite, updateSettings } from "./utilities/testutilities";
import {
activateExtensionForSuite,
getRootWorkspaceFolder,
updateSettings,
} from "./utilities/testutilities";
import { expect } from "chai";
import { afterEach } from "mocha";
import configuration from "../../src/configuration";
@@ -47,7 +50,7 @@ suite("Configuration Test Suite", function () {
expect(task.definition.args).to.not.be.undefined;
const index = task.definition.args.indexOf("--scratch-path");
expect(task.definition.args[index + 1]).to.equal(
vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath + "/somepath"
getRootWorkspaceFolder()?.uri.fsPath + "/somepath"
);
});

@@ -56,7 +59,7 @@ suite("Configuration Test Suite", function () {
"swift.buildPath": "${workspaceFolder}${pathSeparator}${workspaceFolderBasename}",
});

const basePath = vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath;
const basePath = getRootWorkspaceFolder()?.uri.fsPath;
const baseName = path.basename(basePath ?? "");
const sep = path.sep;
expect(configuration.buildPath).to.equal(`${basePath}${sep}${baseName}`);
14 changes: 10 additions & 4 deletions test/integration-tests/extension.test.ts
Original file line number Diff line number Diff line change
@@ -13,10 +13,11 @@
//===----------------------------------------------------------------------===//

import * as assert from "assert";
import * as vscode from "vscode";
import { WorkspaceContext } from "../../src/WorkspaceContext";
import { getBuildAllTask } from "../../src/tasks/SwiftTaskProvider";
import { SwiftExecution } from "../../src/tasks/SwiftExecution";
import { activateExtensionForTest } from "./utilities/testutilities";
import { activateExtensionForTest, findWorkspaceFolder } from "./utilities/testutilities";
import { expect } from "chai";

suite("Extension Test Suite", function () {
@@ -47,13 +48,18 @@ suite("Extension Test Suite", function () {
this.timeout(60000);
/** Verify tasks.json is being loaded */
test("Tasks.json", async () => {
const folder = workspaceContext.folders.find(f => f.name === "test/defaultPackage");
const folder = findWorkspaceFolder("defaultPackage", workspaceContext);
assert(folder);
const buildAllTask = await getBuildAllTask(folder);
const execution = buildAllTask.execution as SwiftExecution;
expect(buildAllTask.definition.type).to.equal("swift");
expect(buildAllTask.name).to.include("Build All (defaultPackage)");
for (const arg of ["build", "--build-tests", "--verbose"]) {
expect(buildAllTask.name).to.include(
"Build All (defaultPackage)" +
(vscode.workspace.workspaceFile ? " (workspace)" : "")
);
for (const arg of ["build", "--build-tests", "--verbose"].concat([
vscode.workspace.workspaceFile ? "-DBAR" : "-DFOO",
])) {
assert(execution?.args.find(item => item === arg));
}
}).timeout(60000);
18 changes: 15 additions & 3 deletions test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts
Original file line number Diff line number Diff line change
@@ -263,15 +263,27 @@ suite("SwiftPluginTaskProvider Test Suite", function () {
});

suite("includes command plugin provided by tasks.json", () => {
let task: vscode.Task | undefined;
let task: SwiftTask | undefined;

setup(async () => {
const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" });
task = tasks.find(t => t.name === "swift: command-plugin from tasks.json");
task = tasks.find(
t =>
t.name ===
"swift: command-plugin from " +
(vscode.workspace.workspaceFile ? "code workspace" : "tasks.json")
) as SwiftTask;
});

test("provides", () => {
expect(task?.detail).to.include("swift package command_plugin --foo");
expect(task?.execution.args).to.deep.equal(
folderContext.toolchain.buildFlags.withAdditionalFlags([
"package",
"--disable-sandbox",
"command_plugin",
"--foo",
])
);
});

test("executes", async () => {
7 changes: 6 additions & 1 deletion test/integration-tests/tasks/SwiftTaskProvider.test.ts
Original file line number Diff line number Diff line change
@@ -120,7 +120,12 @@ suite("SwiftTaskProvider Test Suite", () => {

setup(async () => {
const tasks = await vscode.tasks.fetchTasks({ type: "swift" });
task = tasks.find(t => t.name === "swift: Build All from tasks.json");
task = tasks.find(
t =>
t.name ===
"swift: Build All from " +
(vscode.workspace.workspaceFile ? "code workspace" : "tasks.json")
);
});

test("provided", async () => {
6 changes: 3 additions & 3 deletions test/integration-tests/tasks/TaskQueue.test.ts
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ import * as assert from "assert";
import { testAssetPath } from "../../fixtures";
import { WorkspaceContext } from "../../../src/WorkspaceContext";
import { SwiftExecOperation, TaskOperation, TaskQueue } from "../../../src/tasks/TaskQueue";
import { activateExtensionForSuite } from "../utilities/testutilities";
import { activateExtensionForSuite, findWorkspaceFolder } from "../utilities/testutilities";

suite("TaskQueue Test Suite", () => {
let workspaceContext: WorkspaceContext;
@@ -155,7 +155,7 @@ suite("TaskQueue Test Suite", () => {

// check queuing task will return expected value
test("swift exec", async () => {
const folder = workspaceContext.folders.find(f => f.name === "test/defaultPackage");
const folder = findWorkspaceFolder("defaultPackage", workspaceContext);
assert(folder);
const operation = new SwiftExecOperation(
["--version"],
@@ -172,7 +172,7 @@ suite("TaskQueue Test Suite", () => {

// check queuing swift exec operation will throw expected error
test("swift exec error", async () => {
const folder = workspaceContext.folders.find(f => f.name === "test/defaultPackage");
const folder = findWorkspaceFolder("defaultPackage", workspaceContext);
assert(folder);
const operation = new SwiftExecOperation(
["--version"],
50 changes: 43 additions & 7 deletions test/integration-tests/utilities/testutilities.ts
Original file line number Diff line number Diff line change
@@ -16,8 +16,8 @@ import * as vscode from "vscode";
import * as assert from "assert";
import * as mocha from "mocha";
import { Api } from "../../../src/extension";
import { testAssetUri } from "../../fixtures";
import { WorkspaceContext } from "../../../src/WorkspaceContext";
import { testAssetPath, testAssetUri } from "../../fixtures";
import { FolderOperation, WorkspaceContext } from "../../../src/WorkspaceContext";
import { FolderContext } from "../../../src/FolderContext";
import { waitForNoRunningTasks } from "../../utilities/tasks";
import { closeAllEditors } from "../../utilities/commands";
@@ -27,7 +27,7 @@ import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel";
import configuration from "../../../src/configuration";
import { resetBuildAllTaskCache } from "../../../src/tasks/SwiftTaskProvider";

function getRootWorkspaceFolder(): vscode.WorkspaceFolder {
export function getRootWorkspaceFolder(): vscode.WorkspaceFolder {
const result = vscode.workspace.workspaceFolders?.at(0);
assert(result, "No workspace folders were opened for the tests to use");
return result;
@@ -230,10 +230,39 @@ const extensionBootstrapper = (() => {
}

// Add assets required for the suite/test to the workspace.
const workspaceFolder = getRootWorkspaceFolder();
for (const asset of testAssets ?? []) {
const packageFolder = testAssetUri(asset);
await workspaceContext.addPackageFolder(packageFolder, workspaceFolder);
const expectedAssets = testAssets ?? ["defaultPackage"];
if (!vscode.workspace.workspaceFile) {
for (const asset of expectedAssets) {
await folderInRootWorkspace(asset, workspaceContext);
}
} else if (expectedAssets.length > 0) {
await new Promise<void>(res => {
const found: string[] = [];
for (const f of workspaceContext.folders) {
if (found.includes(f.name) || !expectedAssets.includes(f.name)) {
continue;
}
found.push(f.name);
}
if (expectedAssets.length === found.length) {
res();
return;
}
const disposable = workspaceContext.onDidChangeFolders(e => {
if (
e.operation !== FolderOperation.add ||
found.includes(e.folder!.name) ||
!expectedAssets.includes(e.folder!.name)
) {
return;
}
found.push(e.folder!.name);
if (expectedAssets.length === found.length) {
res();
disposable.dispose();
}
});
});
}

return workspaceContext;
@@ -339,6 +368,13 @@ export const folderInRootWorkspace = async (
return folder;
};

export function findWorkspaceFolder(
name: string,
workspaceContext: WorkspaceContext
): FolderContext | undefined {
return workspaceContext.folders.find(f => f.folder.fsPath === testAssetPath(name));
}

export type SettingsMap = { [key: string]: unknown };

/**