Skip to content

Commit fd1c075

Browse files
authored
Avoid blocking folder addition on package loading (#1422)
When a folder is added to a VSCode workspace we perform a `swift package describe` to load information about the package. This operation must complete before the folder is added to the `workspaceContext`, which prevents the user from taking many actions in the extension that could be made available right away. A `swift package describe` operation can potentially take a good amount of time if package resolution has not been performed. Work around this limitation by wrapping the data pulled from this `package describe` behind promises, allowing clients to wait for package resolution to complete only if they need information that is gated by it. Issue: #1250
1 parent abc76d5 commit fd1c075

30 files changed

+419
-327
lines changed

src/FolderContext.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,12 @@ export class FolderContext implements vscode.Disposable {
7171
): Promise<FolderContext> {
7272
const statusItemText = `Loading Package (${FolderContext.uriName(folder)})`;
7373
workspaceContext.statusItem.start(statusItemText);
74-
7574
const { linuxMain, swiftPackage } =
7675
await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => {
7776
const linuxMain = await LinuxMain.create(folder);
7877
const swiftPackage = await SwiftPackage.create(folder, workspaceContext.toolchain);
7978
return { linuxMain, swiftPackage };
8079
});
81-
8280
workspaceContext.statusItem.end(statusItemText);
8381

8482
const folderContext = new FolderContext(
@@ -89,7 +87,7 @@ export class FolderContext implements vscode.Disposable {
8987
workspaceContext
9088
);
9189

92-
const error = swiftPackage.error;
90+
const error = await swiftPackage.error;
9391
if (error) {
9492
vscode.window.showErrorMessage(
9593
`Failed to load ${folderContext.name}/Package.swift: ${error.message}`
@@ -99,7 +97,6 @@ export class FolderContext implements vscode.Disposable {
9997
folderContext.name
10098
);
10199
}
102-
103100
return folderContext;
104101
}
105102

@@ -188,11 +185,11 @@ export class FolderContext implements vscode.Disposable {
188185
* @param uri URI to find target for
189186
* @returns Target
190187
*/
191-
getTestTarget(uri: vscode.Uri, type?: TargetType): Target | undefined {
188+
async getTestTarget(uri: vscode.Uri, type?: TargetType): Promise<Target | undefined> {
192189
if (!isPathInsidePath(uri.fsPath, this.folder.fsPath)) {
193190
return undefined;
194191
}
195-
const testTargets = this.swiftPackage.getTargets(type);
192+
const testTargets = await this.swiftPackage.getTargets(type);
196193
const target = testTargets.find(element => {
197194
const relativeUri = path.relative(
198195
path.join(this.folder.fsPath, element.path),

src/SwiftPackage.ts

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { BuildFlags } from "./toolchain/BuildFlags";
2222
import { SwiftOutputChannel } from "./ui/SwiftOutputChannel";
2323

2424
/** Swift Package Manager contents */
25-
export interface PackageContents {
25+
interface PackageContents {
2626
name: string;
2727
products: Product[];
2828
dependencies: Dependency[];
@@ -185,8 +185,10 @@ function isError(state: SwiftPackageState): state is Error {
185185
/**
186186
* Class holding Swift Package Manager Package
187187
*/
188-
export class SwiftPackage implements PackageContents {
188+
export class SwiftPackage {
189189
public plugins: PackagePlugin[] = [];
190+
private _contents: SwiftPackageState | undefined;
191+
190192
/**
191193
* SwiftPackage Constructor
192194
* @param folder folder package is in
@@ -195,7 +197,7 @@ export class SwiftPackage implements PackageContents {
195197
*/
196198
private constructor(
197199
readonly folder: vscode.Uri,
198-
private contents: SwiftPackageState,
200+
private contentsPromise: Promise<SwiftPackageState>,
199201
public resolved: PackageResolved | undefined,
200202
private workspaceState: WorkspaceState | undefined
201203
) {}
@@ -209,10 +211,34 @@ export class SwiftPackage implements PackageContents {
209211
folder: vscode.Uri,
210212
toolchain: SwiftToolchain
211213
): Promise<SwiftPackage> {
212-
const contents = await SwiftPackage.loadPackage(folder, toolchain);
213-
const resolved = await SwiftPackage.loadPackageResolved(folder);
214-
const workspaceState = await SwiftPackage.loadWorkspaceState(folder);
215-
return new SwiftPackage(folder, contents, resolved, workspaceState);
214+
const [resolved, workspaceState] = await Promise.all([
215+
SwiftPackage.loadPackageResolved(folder),
216+
SwiftPackage.loadWorkspaceState(folder),
217+
]);
218+
return new SwiftPackage(
219+
folder,
220+
SwiftPackage.loadPackage(folder, toolchain),
221+
resolved,
222+
workspaceState
223+
);
224+
}
225+
226+
/**
227+
* Returns the package state once it has loaded.
228+
* A snapshot of the state is stored in `_contents` after initial resolution.
229+
*/
230+
private get contents(): Promise<SwiftPackageState> {
231+
return this.contentsPromise.then(contents => {
232+
// If `reload` is called immediately its possible for it to resolve
233+
// before the initial contentsPromise resolution. In that case return
234+
// the newer loaded `_contents`.
235+
if (this._contents === undefined) {
236+
this._contents = contents;
237+
return contents;
238+
} else {
239+
return this._contents;
240+
}
241+
});
216242
}
217243

218244
/**
@@ -329,7 +355,9 @@ export class SwiftPackage implements PackageContents {
329355

330356
/** Reload swift package */
331357
public async reload(toolchain: SwiftToolchain) {
332-
this.contents = await SwiftPackage.loadPackage(this.folder, toolchain);
358+
const loadedContents = await SwiftPackage.loadPackage(this.folder, toolchain);
359+
this._contents = loadedContents;
360+
this.contentsPromise = Promise.resolve(loadedContents);
333361
}
334362

335363
/** Reload Package.resolved file */
@@ -346,31 +374,26 @@ export class SwiftPackage implements PackageContents {
346374
}
347375

348376
/** Return if has valid contents */
349-
public get isValid(): boolean {
350-
return isPackage(this.contents);
377+
public get isValid(): Promise<boolean> {
378+
return this.contents.then(contents => isPackage(contents));
351379
}
352380

353381
/** Load error */
354-
public get error(): Error | undefined {
355-
if (isError(this.contents)) {
356-
return this.contents;
357-
} else {
358-
return undefined;
359-
}
382+
public get error(): Promise<Error | undefined> {
383+
return this.contents.then(contents => (isError(contents) ? contents : undefined));
360384
}
361385

362386
/** Did we find a Package.swift */
363-
public get foundPackage(): boolean {
364-
return this.contents !== undefined;
387+
public get foundPackage(): Promise<boolean> {
388+
return this.contents.then(contents => contents !== undefined);
365389
}
366390

367-
public rootDependencies(): ResolvedDependency[] {
391+
public get rootDependencies(): Promise<ResolvedDependency[]> {
368392
// Correlate the root dependencies found in the Package.swift with their
369393
// checked out versions in the workspace-state.json.
370-
const result = this.dependencies.map(dependency =>
371-
this.resolveDependencyAgainstWorkspaceState(dependency)
394+
return this.dependencies.then(dependencies =>
395+
dependencies.map(dependency => this.resolveDependencyAgainstWorkspaceState(dependency))
372396
);
373-
return result;
374397
}
375398

376399
private resolveDependencyAgainstWorkspaceState(dependency: Dependency): ResolvedDependency {
@@ -446,55 +469,70 @@ export class SwiftPackage implements PackageContents {
446469
}
447470
}
448471

449-
/** name of Swift Package */
450-
get name(): string {
451-
return (this.contents as PackageContents)?.name ?? "";
472+
/** getName of Swift Package */
473+
get name(): Promise<string> {
474+
return this.contents.then(contents => (contents as PackageContents)?.name ?? "");
452475
}
453476

454477
/** array of products in Swift Package */
455-
get products(): Product[] {
456-
return (this.contents as PackageContents)?.products ?? [];
478+
private get products(): Promise<Product[]> {
479+
return this.contents.then(contents => (contents as PackageContents)?.products ?? []);
457480
}
458481

459482
/** array of dependencies in Swift Package */
460-
get dependencies(): Dependency[] {
461-
return (this.contents as PackageContents)?.dependencies ?? [];
483+
get dependencies(): Promise<Dependency[]> {
484+
return this.contents.then(contents => (contents as PackageContents)?.dependencies ?? []);
462485
}
463486

464487
/** array of targets in Swift Package */
465-
get targets(): Target[] {
466-
return (this.contents as PackageContents)?.targets ?? [];
488+
get targets(): Promise<Target[]> {
489+
return this.contents.then(contents => (contents as PackageContents)?.targets ?? []);
467490
}
468491

469492
/** array of executable products in Swift Package */
470-
get executableProducts(): Product[] {
471-
return this.products.filter(product => product.type.executable !== undefined);
493+
get executableProducts(): Promise<Product[]> {
494+
return this.products.then(products =>
495+
products.filter(product => product.type.executable !== undefined)
496+
);
472497
}
473498

474499
/** array of library products in Swift Package */
475-
get libraryProducts(): Product[] {
476-
return this.products.filter(product => product.type.library !== undefined);
500+
get libraryProducts(): Promise<Product[]> {
501+
return this.products.then(products =>
502+
products.filter(product => product.type.library !== undefined)
503+
);
504+
}
505+
506+
/**
507+
* Array of targets in Swift Package. The targets may not be loaded yet.
508+
* It is preferable to use the `targets` property that returns a promise that
509+
* returns the targets when they're guarenteed to be resolved.
510+
**/
511+
get currentTargets(): Target[] {
512+
return (this._contents as unknown as { targets: Target[] })?.targets ?? [];
477513
}
478514

479515
/**
480516
* Return array of targets of a certain type
481517
* @param type Type of target
482518
* @returns Array of targets
483519
*/
484-
getTargets(type?: TargetType): Target[] {
520+
async getTargets(type?: TargetType): Promise<Target[]> {
485521
if (type === undefined) {
486522
return this.targets;
487523
} else {
488-
return this.targets.filter(target => target.type === type);
524+
return this.targets.then(targets => targets.filter(target => target.type === type));
489525
}
490526
}
491527

492528
/**
493529
* Get target for file
494530
*/
495-
getTarget(file: string): Target | undefined {
531+
async getTarget(file: string): Promise<Target | undefined> {
496532
const filePath = path.relative(this.folder.fsPath, file);
497-
return this.targets.find(target => isPathInsidePath(filePath, target.path));
533+
return this.targets.then(targets =>
534+
targets.find(target => isPathInsidePath(filePath, target.path))
535+
);
498536
}
499537

500538
private static trimStdout(stdout: string): string {

src/TestExplorer/LSPTestDiscovery.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@ import {
1919
TextDocumentTestsRequest,
2020
WorkspaceTestsRequest,
2121
} from "../sourcekit-lsp/extensions";
22-
import { SwiftPackage, TargetType } from "../SwiftPackage";
23-
import {
24-
checkExperimentalCapability,
25-
LanguageClientManager,
26-
} from "../sourcekit-lsp/LanguageClientManager";
22+
import { checkExperimentalCapability } from "../sourcekit-lsp/LanguageClientManager";
23+
import { SwiftPackage } from "../SwiftPackage";
24+
import { LanguageClientManager } from "../sourcekit-lsp/LanguageClientManager";
2725
import { LanguageClient } from "vscode-languageclient/node";
2826

2927
/**
@@ -71,7 +69,7 @@ export class LSPTestDiscovery {
7169
// workspace/tests method, and is at least version 2.
7270
if (checkExperimentalCapability(client, WorkspaceTestsRequest.method, 2)) {
7371
const tests = await client.sendRequest(WorkspaceTestsRequest.type, token);
74-
return this.transformToTestClass(client, swiftPackage, tests);
72+
return await this.transformToTestClass(client, swiftPackage, tests);
7573
} else {
7674
throw new Error(`${WorkspaceTestsRequest.method} requests not supported`);
7775
}
@@ -82,38 +80,41 @@ export class LSPTestDiscovery {
8280
* Convert from `LSPTestItem[]` to `TestDiscovery.TestClass[]`,
8381
* updating the format of the location.
8482
*/
85-
private transformToTestClass(
83+
private async transformToTestClass(
8684
client: LanguageClient,
8785
swiftPackage: SwiftPackage,
8886
input: LSPTestItem[]
89-
): TestDiscovery.TestClass[] {
90-
return input.map(item => {
87+
): Promise<TestDiscovery.TestClass[]> {
88+
let result: TestDiscovery.TestClass[] = [];
89+
for (const item of input) {
9190
const location = client.protocol2CodeConverter.asLocation(item.location);
92-
return {
93-
...item,
94-
id: this.transformId(item, location, swiftPackage),
95-
children: this.transformToTestClass(client, swiftPackage, item.children),
96-
location,
97-
};
98-
});
91+
result = [
92+
...result,
93+
{
94+
...item,
95+
id: await this.transformId(item, location, swiftPackage),
96+
children: await this.transformToTestClass(client, swiftPackage, item.children),
97+
location,
98+
},
99+
];
100+
}
101+
return result;
99102
}
100103

101104
/**
102105
* If the test is an XCTest, transform the ID provided by the LSP from a
103106
* swift-testing style ID to one that XCTest can use. This allows the ID to
104107
* be used to tell to the test runner (xctest or swift-testing) which tests to run.
105108
*/
106-
private transformId(
109+
private async transformId(
107110
item: LSPTestItem,
108111
location: vscode.Location,
109112
swiftPackage: SwiftPackage
110-
): string {
113+
): Promise<string> {
111114
// XCTest: Target.TestClass/testCase
112115
// swift-testing: TestClass/testCase()
113116
// TestClassOrStruct/NestedTestSuite/testCase()
114-
const target = swiftPackage
115-
.getTargets(TargetType.test)
116-
.find(target => swiftPackage.getTarget(location.uri.fsPath) === target);
117+
const target = await swiftPackage.getTarget(location.uri.fsPath);
117118

118119
// If we're using an older sourcekit-lsp it doesn't prepend the target name
119120
// to the test item id.

src/TestExplorer/TestDiscovery.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,34 @@ const defaultTags = [runnableTag.id, "test-target", "XCTest", "swift-testing"];
4343
* @param swiftPackage A swift package containing test targets
4444
* @param testClasses Array of test classes
4545
*/
46-
export function updateTestsFromClasses(
46+
export async function updateTestsFromClasses(
4747
testController: vscode.TestController,
4848
swiftPackage: SwiftPackage,
4949
testItems: TestClass[]
5050
) {
51-
const targets = swiftPackage.getTargets(TargetType.test).map(target => {
52-
const filteredItems = testItems.filter(
53-
testItem =>
54-
testItem.location && swiftPackage.getTarget(testItem.location.uri.fsPath) === target
55-
);
56-
return {
51+
const targets = await swiftPackage.getTargets(TargetType.test);
52+
const results: TestClass[] = [];
53+
for (const target of targets) {
54+
const filteredItems: TestClass[] = [];
55+
for (const testItem of testItems) {
56+
if (
57+
testItem.location &&
58+
(await swiftPackage.getTarget(testItem.location.uri.fsPath)) === target
59+
) {
60+
filteredItems.push(testItem);
61+
}
62+
}
63+
results.push({
5764
id: target.c99name,
5865
label: target.name,
5966
children: filteredItems,
6067
location: undefined,
6168
disabled: false,
6269
style: "test-target",
6370
tags: [],
64-
} as TestClass;
65-
});
66-
updateTests(testController, targets);
71+
});
72+
}
73+
updateTests(testController, results);
6774
}
6875

6976
export function updateTestsForTarget(

0 commit comments

Comments
 (0)