Skip to content

Commit 15609b1

Browse files
committed
Show progress for package description tasks on activation
The `swift package show-dependencies` and `swift package describe` commands run on extension activation, and several extension features are gated behind their results. These were run with `execSwift` calls which show no progress in the VS Code UI. The `swift package show-dependencies` command specifically could kick off a package resolution if the dependencies are missing, and for large packages this could take quite some time. As a result the extension could look like it was not activating. Move these two commands into VS Code tasks that report their status in the progress bar, and let the user see the actual commands being run in the terminal.
1 parent 75d1511 commit 15609b1

File tree

15 files changed

+343
-156
lines changed

15 files changed

+343
-156
lines changed

src/FolderContext.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,7 @@ export class FolderContext implements vscode.Disposable {
138138
const { linuxMain, swiftPackage } =
139139
await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => {
140140
const linuxMain = await LinuxMain.create(folder);
141-
const swiftPackage = await SwiftPackage.create(
142-
folder,
143-
toolchain,
144-
configuration.disableSwiftPMIntegration
145-
);
141+
const swiftPackage = await SwiftPackage.create(folder);
146142
return { linuxMain, swiftPackage };
147143
});
148144
workspaceContext.statusItem.end(statusItemText);
@@ -156,16 +152,19 @@ export class FolderContext implements vscode.Disposable {
156152
workspaceContext
157153
);
158154

159-
const error = await swiftPackage.error;
160-
if (error) {
161-
void vscode.window.showErrorMessage(
162-
`Failed to load ${folderContext.name}/Package.swift: ${error.message}`
163-
);
164-
workspaceContext.logger.info(
165-
`Failed to load Package.swift: ${error.message}`,
166-
folderContext.name
167-
);
168-
}
155+
// List the package's dependencies without blocking folder creation
156+
void swiftPackage.loadPackageState(folderContext).then(async () => {
157+
const error = await swiftPackage.error;
158+
if (error) {
159+
void vscode.window.showErrorMessage(
160+
`Failed to load ${folderContext.name}/Package.swift: ${error.message}`
161+
);
162+
workspaceContext.logger.info(
163+
`Failed to load Package.swift: ${error.message}`,
164+
folderContext.name
165+
);
166+
}
167+
});
169168

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

201200
/** reload swift package for this folder */
202201
async reload() {
203-
await this.swiftPackage.reload(this.toolchain, configuration.disableSwiftPMIntegration);
202+
await this.swiftPackage.reload(this, configuration.disableSwiftPMIntegration);
204203
}
205204

206205
/** reload Package.resolved for this folder */

src/PackageWatcher.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,6 @@ export class PackageWatcher {
195195
*/
196196
private async handleWorkspaceStateChange() {
197197
await this.folderContext.reloadWorkspaceState();
198-
// TODO: Remove this
199-
this.logger.info(
200-
`Package watcher state updated workspace-state.json: ${JSON.stringify(this.folderContext.swiftPackage.workspaceState, null, 2)}`
201-
);
202198
await this.folderContext.fireEvent(FolderOperation.workspaceStateUpdated);
203199
}
204200
}

src/SwiftPackage.ts

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import * as fs from "fs/promises";
1515
import * as path from "path";
1616
import * as vscode from "vscode";
1717

18+
import { FolderContext } from "./FolderContext";
19+
import { describePackage } from "./commands/dependencies/describe";
20+
import { showPackageDependencies } from "./commands/dependencies/show";
1821
import { SwiftLogger } from "./logging/SwiftLogger";
1922
import { BuildFlags } from "./toolchain/BuildFlags";
2023
import { SwiftToolchain } from "./toolchain/toolchain";
@@ -23,7 +26,7 @@ import { lineBreakRegex } from "./utilities/tasks";
2326
import { execSwift, getErrorDescription, hashString } from "./utilities/utilities";
2427

2528
/** Swift Package Manager contents */
26-
interface PackageContents {
29+
export interface PackageContents {
2730
name: string;
2831
products: Product[];
2932
dependencies: Dependency[];
@@ -192,7 +195,11 @@ function isError(state: SwiftPackageState): state is Error {
192195
*/
193196
export class SwiftPackage {
194197
public plugins: PackagePlugin[] = [];
198+
195199
private _contents: SwiftPackageState | undefined;
200+
private contentsPromise: Promise<SwiftPackageState>;
201+
private contentsResolve: (value: SwiftPackageState | PromiseLike<SwiftPackageState>) => void;
202+
private contentsReject: (reason?: unknown) => void;
196203

197204
/**
198205
* SwiftPackage Constructor
@@ -202,11 +209,19 @@ export class SwiftPackage {
202209
*/
203210
private constructor(
204211
readonly folder: vscode.Uri,
205-
private contentsPromise: Promise<SwiftPackageState>,
206212
public resolved: PackageResolved | undefined,
207213
// TODO: Make private again
208214
public workspaceState: WorkspaceState | undefined
209-
) {}
215+
) {
216+
let res: (value: SwiftPackageState | PromiseLike<SwiftPackageState>) => void;
217+
let rej: (reason?: unknown) => void;
218+
this.contentsPromise = new Promise((resolve, reject) => {
219+
res = resolve;
220+
rej = reject;
221+
});
222+
this.contentsResolve = res!;
223+
this.contentsReject = rej!;
224+
}
210225

211226
/**
212227
* Create a SwiftPackage from a folder
@@ -215,21 +230,12 @@ export class SwiftPackage {
215230
* @param disableSwiftPMIntegration Whether to disable SwiftPM integration
216231
* @returns new SwiftPackage
217232
*/
218-
public static async create(
219-
folder: vscode.Uri,
220-
toolchain: SwiftToolchain,
221-
disableSwiftPMIntegration: boolean = false
222-
): Promise<SwiftPackage> {
233+
public static async create(folder: vscode.Uri): Promise<SwiftPackage> {
223234
const [resolved, workspaceState] = await Promise.all([
224235
SwiftPackage.loadPackageResolved(folder),
225236
SwiftPackage.loadWorkspaceState(folder),
226237
]);
227-
return new SwiftPackage(
228-
folder,
229-
SwiftPackage.loadPackage(folder, toolchain, disableSwiftPMIntegration),
230-
resolved,
231-
workspaceState
232-
);
238+
return new SwiftPackage(folder, resolved, workspaceState);
233239
}
234240

235241
/**
@@ -257,39 +263,32 @@ export class SwiftPackage {
257263
* @param disableSwiftPMIntegration Whether to disable SwiftPM integration
258264
* @returns results of `swift package describe`
259265
*/
260-
static async loadPackage(
261-
folder: vscode.Uri,
262-
toolchain: SwiftToolchain,
266+
public async loadPackageState(
267+
folderContext: FolderContext,
263268
disableSwiftPMIntegration: boolean = false
264-
): Promise<SwiftPackageState> {
269+
): Promise<void> {
265270
// When SwiftPM integration is disabled, return empty package structure
266271
if (disableSwiftPMIntegration) {
267-
return {
268-
name: path.basename(folder.fsPath),
272+
this.contentsResolve({
273+
name: path.basename(folderContext.folder.fsPath),
269274
products: [],
270275
dependencies: [],
271276
targets: [],
272-
};
277+
});
273278
}
274279

275280
try {
276281
// Use swift package describe to describe the package targets, products, and platforms
277282
// Use swift package show-dependencies to get the dependencies in a tree format
278-
const [describe, dependencies] = await Promise.all([
279-
execSwift(["package", "describe", "--type", "json"], toolchain, {
280-
cwd: folder.fsPath,
281-
}),
282-
execSwift(["package", "show-dependencies", "--format", "json"], toolchain, {
283-
cwd: folder.fsPath,
284-
}),
285-
]);
286-
283+
// Each of these locks the folder so we can't run them in parallel, so just serially run them.
284+
const describe = await describePackage(folderContext);
285+
const dependencies = await showPackageDependencies(folderContext);
287286
const packageState = {
288-
...(JSON.parse(SwiftPackage.trimStdout(describe.stdout)) as PackageContents),
289-
dependencies: JSON.parse(SwiftPackage.trimStdout(dependencies.stdout)).dependencies,
287+
...(describe as PackageContents),
288+
dependencies: dependencies,
290289
};
291290

292-
return packageState;
291+
this.contentsResolve(packageState);
293292
} catch (error) {
294293
const execError = error as { stderr: string };
295294
// if caught error and it begins with "error: root manifest" then there is no Package.swift
@@ -298,11 +297,18 @@ export class SwiftPackage {
298297
(execError.stderr.startsWith("error: root manifest") ||
299298
execError.stderr.startsWith("error: Could not find Package.swift"))
300299
) {
301-
return undefined;
300+
this.contentsResolve({
301+
name: path.basename(folderContext.folder.fsPath),
302+
products: [],
303+
dependencies: [],
304+
targets: [],
305+
});
306+
return;
302307
} else {
303308
// otherwise it is an error loading the Package.swift so return `null` indicating
304309
// we have a package but we failed to load it
305-
return Error(getErrorDescription(error));
310+
this.contentsReject(Error(getErrorDescription(error)));
311+
return;
306312
}
307313
}
308314
}
@@ -377,14 +383,16 @@ export class SwiftPackage {
377383
}
378384

379385
/** Reload swift package */
380-
public async reload(toolchain: SwiftToolchain, disableSwiftPMIntegration: boolean = false) {
381-
const loadedContents = await SwiftPackage.loadPackage(
382-
this.folder,
383-
toolchain,
384-
disableSwiftPMIntegration
385-
);
386-
this._contents = loadedContents;
387-
this.contentsPromise = Promise.resolve(loadedContents);
386+
public async reload(folderContext: FolderContext, disableSwiftPMIntegration: boolean = false) {
387+
let res: (value: SwiftPackageState | PromiseLike<SwiftPackageState>) => void;
388+
let rej: (reason?: unknown) => void;
389+
this.contentsPromise = new Promise((resolve, reject) => {
390+
res = resolve;
391+
rej = reject;
392+
});
393+
this.contentsResolve = res!;
394+
this.contentsReject = rej!;
395+
await this.loadPackageState(folderContext, disableSwiftPMIntegration);
388396
}
389397

390398
/** Reload Package.resolved file */
@@ -572,7 +580,7 @@ export class SwiftPackage {
572580
);
573581
}
574582

575-
private static trimStdout(stdout: string): string {
583+
static trimStdout(stdout: string): string {
576584
// remove lines from `swift package describe` until we find a "{"
577585
while (!stdout.startsWith("{")) {
578586
const firstNewLine = stdout.indexOf("\n");
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2021-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+
16+
import { FolderContext } from "../../FolderContext";
17+
import { SwiftPackage } from "../../SwiftPackage";
18+
import { createSwiftTask } from "../../tasks/SwiftTaskProvider";
19+
import { packageName } from "../../utilities/tasks";
20+
import { executeTaskWithUI, updateAfterError } from "../utilities";
21+
22+
/**
23+
* Configuration for executing a Swift package command
24+
*/
25+
export interface SwiftPackageCommandConfig {
26+
/** The Swift command arguments (e.g., ["package", "show-dependencies", "--format", "json"]) */
27+
args: string[];
28+
/** The task name for the SwiftTaskProvider */
29+
taskName: string;
30+
/** The UI message to display during execution */
31+
uiMessage: string;
32+
/** The command name for error messages */
33+
commandName: string;
34+
}
35+
36+
/**
37+
* Execute a Swift package command and return the parsed JSON output
38+
* @param folderContext folder to run the command in
39+
* @param config command configuration
40+
* @returns parsed JSON output from the command
41+
*/
42+
export async function executeSwiftPackageCommand<T>(
43+
folderContext: FolderContext,
44+
config: SwiftPackageCommandConfig
45+
): Promise<T> {
46+
const task = createSwiftTask(
47+
config.args,
48+
config.taskName,
49+
{
50+
cwd: folderContext.folder,
51+
scope: folderContext.workspaceFolder,
52+
packageName: packageName(folderContext),
53+
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
54+
dontTriggerTestDiscovery: true,
55+
group: vscode.TaskGroup.Build,
56+
},
57+
folderContext.toolchain,
58+
undefined,
59+
{ readOnlyTerminal: true }
60+
);
61+
62+
const outputChunks: string[] = [];
63+
task.execution.onDidWrite((data: string) => {
64+
outputChunks.push(data);
65+
});
66+
67+
const success = await executeTaskWithUI(task, config.uiMessage, folderContext, false);
68+
updateAfterError(success, folderContext);
69+
70+
const stdout = outputChunks.join("");
71+
if (!stdout.trim()) {
72+
throw new Error(`No output received from swift ${config.commandName} command`);
73+
}
74+
75+
try {
76+
const trimmedOutput = SwiftPackage.trimStdout(stdout);
77+
const parsedOutput = JSON.parse(trimmedOutput);
78+
79+
// Validate the parsed output is an object
80+
if (!parsedOutput || typeof parsedOutput !== "object") {
81+
throw new Error(`Invalid format received from swift ${config.commandName} command`);
82+
}
83+
84+
return parsedOutput as T;
85+
} catch (parseError) {
86+
throw new Error(
87+
`Failed to parse ${config.commandName} output: ${parseError instanceof Error ? parseError.message : "Unknown error"}`
88+
);
89+
}
90+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2021-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 { FolderContext } from "../../FolderContext";
15+
import { PackageContents } from "../../SwiftPackage";
16+
import { SwiftTaskProvider } from "../../tasks/SwiftTaskProvider";
17+
import { executeSwiftPackageCommand } from "./common";
18+
19+
/**
20+
* Run `swift package describe` inside a folder
21+
* @param folderContext folder to run describe for
22+
*/
23+
export async function describePackage(folderContext: FolderContext): Promise<PackageContents> {
24+
const result = await executeSwiftPackageCommand<PackageContents>(folderContext, {
25+
args: ["package", "describe", "--type", "json"],
26+
taskName: SwiftTaskProvider.describePackageName,
27+
uiMessage: "Describing Package",
28+
commandName: "package describe",
29+
});
30+
31+
return result;
32+
}

0 commit comments

Comments
 (0)