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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## {{releaseVersion}} - {{releaseDate}}

### Added

- Show progress when describing/listing dependencies on package load ([#2028](https://github.com/swiftlang/vscode-swift/pull/2028))

### Fixed

- Fix the wrong toolchain being shown as selected when using swiftly v1.0.1 ([#2014](https://github.com/swiftlang/vscode-swift/pull/2014))
Expand Down
35 changes: 19 additions & 16 deletions src/FolderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class FolderContext implements vscode.Disposable {
/** dispose of any thing FolderContext holds */
dispose() {
this.linuxMain?.dispose();
this.swiftPackage.dispose();
this.packageWatcher.dispose();
this.testExplorer?.dispose();
this.backgroundCompilation.dispose();
Expand Down Expand Up @@ -138,11 +139,7 @@ export class FolderContext implements vscode.Disposable {
const { linuxMain, swiftPackage } =
await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => {
const linuxMain = await LinuxMain.create(folder);
const swiftPackage = await SwiftPackage.create(
folder,
toolchain,
configuration.disableSwiftPMIntegration
);
const swiftPackage = await SwiftPackage.create(folder);
return { linuxMain, swiftPackage };
});
workspaceContext.statusItem.end(statusItemText);
Expand All @@ -156,16 +153,22 @@ export class FolderContext implements vscode.Disposable {
workspaceContext
);

const error = await swiftPackage.error;
if (error) {
void vscode.window.showErrorMessage(
`Failed to load ${folderContext.name}/Package.swift: ${error.message}`
);
workspaceContext.logger.info(
`Failed to load Package.swift: ${error.message}`,
folderContext.name
);
}
// List the package's dependencies without blocking folder creation
void swiftPackage
.loadPackageState(folderContext)
.then(async () => await swiftPackage.error)
.catch(error => error)
.then(async error => {
if (error) {
void vscode.window.showErrorMessage(
`Failed to load ${folderContext.name}/Package.swift: ${error.message}`
);
workspaceContext.logger.info(
`Failed to load Package.swift: ${error.message}`,
folderContext.name
);
}
});

// Start watching for changes to Package.swift, Package.resolved and .swift-version
await folderContext.packageWatcher.install();
Expand Down Expand Up @@ -200,7 +203,7 @@ export class FolderContext implements vscode.Disposable {

/** reload swift package for this folder */
async reload() {
await this.swiftPackage.reload(this.toolchain, configuration.disableSwiftPMIntegration);
await this.swiftPackage.reload(this);
}

/** reload Package.resolved for this folder */
Expand Down
102 changes: 59 additions & 43 deletions src/SwiftPackage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ import * as fs from "fs/promises";
import * as path from "path";
import * as vscode from "vscode";

import { FolderContext } from "./FolderContext";
import { describePackage } from "./commands/dependencies/describe";
import { showPackageDependencies } from "./commands/dependencies/show";
import { SwiftLogger } from "./logging/SwiftLogger";
import { BuildFlags } from "./toolchain/BuildFlags";
import { SwiftToolchain } from "./toolchain/toolchain";
import { isPathInsidePath } from "./utilities/filesystem";
import { lineBreakRegex } from "./utilities/tasks";
import { execSwift, getErrorDescription, hashString } from "./utilities/utilities";
import { execSwift, getErrorDescription, hashString, unwrapPromise } from "./utilities/utilities";

/** Swift Package Manager contents */
interface PackageContents {
export interface PackageContents {
name: string;
products: Product[];
dependencies: Dependency[];
Expand Down Expand Up @@ -196,9 +199,12 @@ function isError(state: SwiftPackageState): state is Error {
/**
* Class holding Swift Package Manager Package
*/
export class SwiftPackage {
export class SwiftPackage implements vscode.Disposable {
public plugins: PackagePlugin[] = [];
private _contents: SwiftPackageState | undefined;
private contentsPromise: Promise<SwiftPackageState>;
private contentsResolve: (value: SwiftPackageState | PromiseLike<SwiftPackageState>) => void;
private tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource();

/**
* SwiftPackage Constructor
Expand All @@ -208,34 +214,26 @@ export class SwiftPackage {
*/
private constructor(
readonly folder: vscode.Uri,
private contentsPromise: Promise<SwiftPackageState>,
public resolved: PackageResolved | undefined,
// TODO: Make private again
public workspaceState: WorkspaceState | undefined
) {}
) {
const { promise, resolve } = unwrapPromise<SwiftPackageState>();
this.contentsPromise = promise;
this.contentsResolve = resolve;
}

/**
* Create a SwiftPackage from a folder
* @param folder folder package is in
* @param toolchain Swift toolchain to use
* @param disableSwiftPMIntegration Whether to disable SwiftPM integration
* @returns new SwiftPackage
*/
public static async create(
folder: vscode.Uri,
toolchain: SwiftToolchain,
disableSwiftPMIntegration: boolean = false
): Promise<SwiftPackage> {
public static async create(folder: vscode.Uri): Promise<SwiftPackage> {
const [resolved, workspaceState] = await Promise.all([
SwiftPackage.loadPackageResolved(folder),
SwiftPackage.loadWorkspaceState(folder),
]);
return new SwiftPackage(
folder,
SwiftPackage.loadPackage(folder, toolchain, disableSwiftPMIntegration),
resolved,
workspaceState
);
return new SwiftPackage(folder, resolved, workspaceState);
}

/**
Expand All @@ -259,45 +257,54 @@ export class SwiftPackage {
/**
* Run `swift package describe` and return results
* @param folder folder package is in
* @param toolchain Swift toolchain to use
* @param disableSwiftPMIntegration Whether to disable SwiftPM integration
* @returns results of `swift package describe`
*/
static async loadPackage(
folder: vscode.Uri,
toolchain: SwiftToolchain,
public async loadPackageState(
folderContext: FolderContext,
disableSwiftPMIntegration: boolean = false
): Promise<SwiftPackageState> {
const resolve = this.contentsResolve;
const result = await this.performLoadPackageState(folderContext, disableSwiftPMIntegration);
resolve(result);
return result;
}

private async performLoadPackageState(
folderContext: FolderContext,
disableSwiftPMIntegration: boolean = false
): Promise<SwiftPackageState> {
// When SwiftPM integration is disabled, return undefined to disable all features
if (disableSwiftPMIntegration) {
return undefined;
}

// If there is an existing package load, cancel any running tasks first before loading a new one.
this.tokenSource.cancel();
this.tokenSource.dispose();
this.tokenSource = new vscode.CancellationTokenSource();

try {
// Use swift package describe to describe the package targets, products, and platforms
// Use swift package show-dependencies to get the dependencies in a tree format
const [describe, dependencies] = await Promise.all([
execSwift(["package", "describe", "--type", "json"], toolchain, {
cwd: folder.fsPath,
}),
execSwift(["package", "show-dependencies", "--format", "json"], toolchain, {
cwd: folder.fsPath,
}),
]);
const describe = await describePackage(folderContext, this.tokenSource.token);
const dependencies = await showPackageDependencies(
folderContext,
this.tokenSource.token
);

const packageState = {
...(JSON.parse(SwiftPackage.trimStdout(describe.stdout)) as PackageContents),
dependencies: JSON.parse(SwiftPackage.trimStdout(dependencies.stdout)).dependencies,
...(describe as PackageContents),
dependencies: dependencies,
};

return packageState;
} catch (error) {
const execError = error as { stderr: string };
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
// if caught error and it begins with "error: root manifest" then there is no Package.swift
if (
execError.stderr !== undefined &&
(execError.stderr.startsWith("error: root manifest") ||
execError.stderr.startsWith("error: Could not find Package.swift"))
errorMessage.startsWith("error: root manifest") ||
errorMessage.startsWith("error: Could not find Package.swift")
) {
return undefined;
} else {
Expand Down Expand Up @@ -378,14 +385,18 @@ export class SwiftPackage {
}

/** Reload swift package */
public async reload(toolchain: SwiftToolchain, disableSwiftPMIntegration: boolean = false) {
const loadedContents = await SwiftPackage.loadPackage(
this.folder,
toolchain,
public async reload(folderContext: FolderContext, disableSwiftPMIntegration: boolean = false) {
const { promise, resolve } = unwrapPromise<SwiftPackageState>();
this.contentsPromise = promise;
this.contentsResolve = resolve;

const loadedContents = await this.performLoadPackageState(
folderContext,
disableSwiftPMIntegration
);

this._contents = loadedContents;
this.contentsPromise = Promise.resolve(loadedContents);
resolve(loadedContents);
}

/** Reload Package.resolved file */
Expand Down Expand Up @@ -573,14 +584,19 @@ export class SwiftPackage {
);
}

private static trimStdout(stdout: string): string {
static trimStdout(stdout: string): string {
// remove lines from `swift package describe` until we find a "{"
while (!stdout.startsWith("{")) {
const firstNewLine = stdout.indexOf("\n");
stdout = stdout.slice(firstNewLine + 1);
}
return stdout;
}

dispose() {
this.tokenSource.cancel();
this.tokenSource.dispose();
}
}

export enum TargetType {
Expand Down
125 changes: 125 additions & 0 deletions src/commands/dependencies/describe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2021-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 { PackageContents, SwiftPackage } from "../../SwiftPackage";
import { SwiftTaskProvider, createSwiftTask } from "../../tasks/SwiftTaskProvider";
import { packageName } from "../../utilities/tasks";
import { executeTaskWithUI, updateAfterError } from "../utilities";

/**
* Configuration for executing a Swift package command
*/
export interface SwiftPackageCommandConfig {
/** The Swift command arguments (e.g., ["package", "show-dependencies", "--format", "json"]) */
args: string[];
/** The task name for the SwiftTaskProvider */
taskName: string;
/** The UI message to display during execution */
uiMessage: string;
/** The command name for error messages */
commandName: string;
}

/**
* Execute a Swift package command and return the parsed JSON output
* @param folderContext folder to run the command in
* @param config command configuration
* @returns parsed JSON output from the command
*/
export async function executeSwiftPackageCommand<T>(
folderContext: FolderContext,
config: SwiftPackageCommandConfig,
token?: vscode.CancellationToken
): Promise<T> {
const task = createSwiftTask(
config.args,
config.taskName,
{
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
packageName: packageName(folderContext),
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
dontTriggerTestDiscovery: true,
group: vscode.TaskGroup.Build,
},
folderContext.toolchain,
undefined,
{ readOnlyTerminal: true }
);

const outputChunks: string[] = [];
task.execution.onDidWrite((data: string) => {
outputChunks.push(data);
});

const success = await executeTaskWithUI(
task,
config.uiMessage,
folderContext,
false,
false,
token
);
updateAfterError(success, folderContext);

const output = outputChunks.join("");

if (!success) {
throw new Error(output);
}

if (!output.trim()) {
throw new Error(`No output received from swift ${config.commandName} command`);
}

try {
const trimmedOutput = SwiftPackage.trimStdout(output);
const parsedOutput = JSON.parse(trimmedOutput);

// Validate the parsed output is an object
if (!parsedOutput || typeof parsedOutput !== "object") {
throw new Error(`Invalid format received from swift ${config.commandName} command`);
}

return parsedOutput as T;
} catch (parseError) {
throw new Error(
`Failed to parse ${config.commandName} output: ${parseError instanceof Error ? parseError.message : "Unknown error"}`
);
}
}

/**
* Run `swift package describe` inside a folder
* @param folderContext folder to run describe for
*/
export async function describePackage(
folderContext: FolderContext,
token?: vscode.CancellationToken
): Promise<PackageContents> {
const result = await executeSwiftPackageCommand<PackageContents>(
folderContext,
{
args: ["package", "describe", "--type", "json"],
taskName: SwiftTaskProvider.describePackageName,
uiMessage: "Describing Package",
commandName: "package describe",
},
token
);

return result;
}
Loading