Skip to content

Commit 2c48d05

Browse files
delay WorkspaceContext creation until after extension activation
1 parent 608182d commit 2c48d05

37 files changed

+900
-724
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,6 +2042,7 @@
20422042
"test": "vscode-test && npm run grammar-test",
20432043
"grammar-test": "vscode-tmgrammar-test test/unit-tests/**/*.test.swift.gyb -g test/unit-tests/syntaxes/swift.tmLanguage.json -g test/unit-tests/syntaxes/MagicPython.tmLanguage.json",
20442044
"integration-test": "npm run pretest && vscode-test --label integrationTests",
2045+
"code-workspace-test": "npm run pretest && vscode-test --label codeWorkspaceTests",
20452046
"unit-test": "npm run pretest && vscode-test --label unitTests",
20462047
"coverage": "npm run pretest && vscode-test --coverage",
20472048
"compile-tests": "del-cli ./assets/test/**/.build && del-cli ./assets/test/**/.spm-cache && npm run compile",

src/SwiftExtensionApi.ts

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
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+
16+
import { FolderContext } from "./FolderContext";
17+
import { TestExplorer } from "./TestExplorer/TestExplorer";
18+
import { FolderEvent, FolderOperation, WorkspaceContext } from "./WorkspaceContext";
19+
import { registerCommands } from "./commands";
20+
import { resolveFolderDependencies } from "./commands/dependencies/resolve";
21+
import { registerSourceKitSchemaWatcher } from "./commands/generateSourcekitConfiguration";
22+
import configuration, { handleConfigurationChangeEvent } from "./configuration";
23+
import { ContextKeys, createContextKeys } from "./contextKeys";
24+
import { registerDebugger } from "./debugger/debugAdapterFactory";
25+
import { makeDebugConfigurations } from "./debugger/launch";
26+
import { Api } from "./extension";
27+
import { SwiftLogger } from "./logging/SwiftLogger";
28+
import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory";
29+
import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "./terminal";
30+
import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher";
31+
import { checkForSwiftlyInstallation } from "./toolchain/swiftly";
32+
import { SwiftToolchain } from "./toolchain/toolchain";
33+
import { LanguageStatusItems } from "./ui/LanguageStatusItems";
34+
import { getReadOnlyDocumentProvider } from "./ui/ReadOnlyDocumentProvider";
35+
import { showToolchainError } from "./ui/ToolchainSelection";
36+
import { checkAndWarnAboutWindowsSymlinks } from "./ui/win32";
37+
import { getErrorDescription } from "./utilities/utilities";
38+
import { Version } from "./utilities/version";
39+
40+
type State = (
41+
| { type: "initializing"; promise: Promise<WorkspaceContext>; cancel(): void }
42+
| { type: "active"; context: WorkspaceContext; subscriptions: vscode.Disposable[] }
43+
| { type: "failed"; error: Error }
44+
) & { activatedBy: Error };
45+
46+
export class SwiftExtensionApi implements Api {
47+
private state?: State;
48+
49+
get workspaceContext(): WorkspaceContext | undefined {
50+
if (this.state?.type !== "active") {
51+
return undefined;
52+
}
53+
return this.state.context;
54+
}
55+
56+
contextKeys: ContextKeys;
57+
58+
logger: SwiftLogger;
59+
60+
constructor(private readonly extensionContext: vscode.ExtensionContext) {
61+
this.contextKeys = createContextKeys();
62+
const logSetupStartTime = Date.now();
63+
this.logger = configureLogging(this.extensionContext);
64+
const logSetupElapsed = Date.now() - logSetupStartTime;
65+
this.logger.info(`Log setup completed in ${logSetupElapsed}ms`);
66+
}
67+
68+
async waitForWorkspaceContext(): Promise<WorkspaceContext> {
69+
if (!this.state) {
70+
throw new Error("The Swift extension has not been activated yet.");
71+
}
72+
if (this.state.type === "failed") {
73+
throw this.state.error;
74+
}
75+
if (this.state.type === "active") {
76+
return this.state.context;
77+
}
78+
return await this.state.promise;
79+
}
80+
81+
async withWorkspaceContext<T>(task: (ctx: WorkspaceContext) => T | Promise<T>): Promise<T> {
82+
const workspaceContext = await this.waitForWorkspaceContext();
83+
return await task(workspaceContext);
84+
}
85+
86+
activate(callSite?: Error): void {
87+
if (this.state) {
88+
throw new Error("The Swift extension has already been activated.", {
89+
cause: this.state.activatedBy,
90+
});
91+
}
92+
93+
const activationStartTime = Date.now();
94+
try {
95+
this.logger.info(
96+
`Activating Swift for Visual Studio Code ${this.extensionContext.extension.packageJSON.version}...`
97+
);
98+
99+
checkAndWarnAboutWindowsSymlinks(this.logger);
100+
checkForSwiftlyInstallation(this.contextKeys, this.logger);
101+
102+
const subscriptionsStartTime = Date.now();
103+
this.extensionContext.subscriptions.push(
104+
new SwiftEnvironmentVariablesManager(this.extensionContext)
105+
);
106+
this.extensionContext.subscriptions.push(SwiftTerminalProfileProvider.register());
107+
108+
this.extensionContext.subscriptions.push(...registerCommands(this));
109+
this.extensionContext.subscriptions.push(registerDebugger(this));
110+
this.extensionContext.subscriptions.push(new SelectedXcodeWatcher(this.logger));
111+
112+
// swift module document provider
113+
this.extensionContext.subscriptions.push(getReadOnlyDocumentProvider());
114+
115+
const subscriptionsElapsed = Date.now() - subscriptionsStartTime;
116+
117+
const finalStepsStartTime = Date.now();
118+
const activatedBy = callSite ?? Error("Extension was activated by:");
119+
activatedBy.name = "ActivatedBy";
120+
const cancellationSource = new vscode.CancellationTokenSource();
121+
this.state = {
122+
type: "initializing",
123+
activatedBy,
124+
promise: this.initializeWorkspace(cancellationSource.token)
125+
.then(({ workspaceContext, subscriptions }) => {
126+
if (cancellationSource.token.isCancellationRequested) {
127+
throw new vscode.CancellationError();
128+
}
129+
130+
this.state = {
131+
type: "active",
132+
activatedBy,
133+
context: workspaceContext,
134+
subscriptions,
135+
};
136+
return workspaceContext;
137+
})
138+
.catch(error => {
139+
if (!cancellationSource.token.isCancellationRequested) {
140+
this.state = { type: "failed", activatedBy, error };
141+
}
142+
throw error;
143+
}),
144+
cancel() {
145+
cancellationSource.cancel();
146+
},
147+
};
148+
// Mark the extension as activated.
149+
this.contextKeys.isActivated = true;
150+
const finalStepsElapsed = Date.now() - finalStepsStartTime;
151+
152+
const totalActivationTime = Date.now() - activationStartTime;
153+
this.logger.info(
154+
`Extension activation completed in ${totalActivationTime}ms (subscriptions: ${subscriptionsElapsed}ms, final-steps: ${finalStepsElapsed}ms)`
155+
);
156+
} catch (error) {
157+
const errorMessage = getErrorDescription(error);
158+
// show this error message as the VS Code error message only shows when running
159+
// the extension through the debugger
160+
void vscode.window.showErrorMessage(
161+
`Activating Swift extension failed: ${errorMessage}`
162+
);
163+
throw error;
164+
}
165+
}
166+
167+
private async initializeWorkspace(token: vscode.CancellationToken): Promise<{
168+
workspaceContext: WorkspaceContext;
169+
subscriptions: vscode.Disposable[];
170+
}> {
171+
if (token.isCancellationRequested) {
172+
throw new vscode.CancellationError();
173+
}
174+
175+
const activationStartTime = Date.now();
176+
const toolchainStartTime = Date.now();
177+
const toolchain = await createActiveToolchain(
178+
this.extensionContext,
179+
this.contextKeys,
180+
this.logger
181+
);
182+
const toolchainElapsed = Date.now() - toolchainStartTime;
183+
184+
if (token.isCancellationRequested) {
185+
throw new vscode.CancellationError();
186+
}
187+
188+
const workspaceContextStartTime = Date.now();
189+
const workspaceContext = new WorkspaceContext(
190+
this.extensionContext,
191+
this.contextKeys,
192+
this.logger,
193+
toolchain
194+
);
195+
this.extensionContext.subscriptions.push(workspaceContext);
196+
const workspaceContextElapsed = Date.now() - workspaceContextStartTime;
197+
198+
const subscriptionsStartTime = Date.now();
199+
const subscriptions: vscode.Disposable[] = [];
200+
201+
// Watch for configuration changes the trigger a reload of the extension if necessary.
202+
subscriptions.push(
203+
vscode.workspace.onDidChangeConfiguration(
204+
handleConfigurationChangeEvent(workspaceContext)
205+
)
206+
);
207+
208+
// Register task provider.
209+
subscriptions.push(
210+
vscode.tasks.registerTaskProvider("swift", workspaceContext.taskProvider)
211+
);
212+
213+
// Register swift plugin task provider.
214+
subscriptions.push(
215+
vscode.tasks.registerTaskProvider("swift-plugin", workspaceContext.pluginProvider)
216+
);
217+
218+
// Register the language status bar items.
219+
subscriptions.push(new LanguageStatusItems(workspaceContext));
220+
221+
// project panel provider
222+
const dependenciesView = vscode.window.createTreeView("projectPanel", {
223+
treeDataProvider: workspaceContext.projectPanel,
224+
showCollapseAll: true,
225+
});
226+
workspaceContext.projectPanel.observeFolders(dependenciesView);
227+
subscriptions.push(dependenciesView);
228+
229+
// observer that will resolve package and build launch configurations
230+
subscriptions.push(workspaceContext.onDidChangeFolders(handleFolderEvent(this.logger)));
231+
subscriptions.push(TestExplorer.observeFolders(workspaceContext));
232+
233+
subscriptions.push(registerSourceKitSchemaWatcher(workspaceContext));
234+
235+
// observer for logging workspace folder addition/removal
236+
subscriptions.push(
237+
workspaceContext.onDidChangeFolders(({ folder, operation }) => {
238+
this.logger.info(`${operation}: ${folder?.folder.fsPath}`, folder?.name);
239+
})
240+
);
241+
242+
const subscriptionsElapsed = Date.now() - subscriptionsStartTime;
243+
244+
// setup workspace context with initial workspace folders
245+
const workspaceFoldersStartTime = Date.now();
246+
await workspaceContext.addWorkspaceFolders();
247+
const workspaceFoldersElapsed = Date.now() - workspaceFoldersStartTime;
248+
249+
if (token.isCancellationRequested) {
250+
throw new vscode.CancellationError();
251+
}
252+
253+
const totalActivationTime = Date.now() - activationStartTime;
254+
this.logger.info(
255+
`Workspace initialization completed in ${totalActivationTime}ms (toolchain: ${toolchainElapsed}ms, workspace-context: ${workspaceContextElapsed}ms, subscriptions: ${subscriptionsElapsed}ms, workspace-folders: ${workspaceFoldersElapsed}ms)`
256+
);
257+
258+
return { workspaceContext, subscriptions };
259+
}
260+
261+
deactivate(): void {
262+
this.contextKeys.isActivated = false;
263+
if (this.state?.type === "initializing") {
264+
this.state.cancel();
265+
}
266+
if (this.state?.type === "active") {
267+
this.state.context.dispose();
268+
this.state.subscriptions.forEach(s => s.dispose());
269+
}
270+
this.extensionContext.subscriptions.forEach(subscription => subscription.dispose());
271+
this.extensionContext.subscriptions.length = 0;
272+
this.state = undefined;
273+
}
274+
275+
dispose(): void {
276+
this.logger.dispose();
277+
}
278+
}
279+
280+
function configureLogging(context: vscode.ExtensionContext) {
281+
const logger = new SwiftLoggerFactory(context.logUri).create(
282+
"Swift",
283+
"swift-vscode-extension.log"
284+
);
285+
// Create log directory asynchronously but don't await it to avoid blocking activation
286+
void vscode.workspace.fs
287+
.createDirectory(context.logUri)
288+
.then(undefined, error => logger.warn(`Failed to create log directory: ${error}`));
289+
return logger;
290+
}
291+
292+
function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise<void> {
293+
// function called when a folder is added. I broke this out so we can trigger it
294+
// without having to await for it.
295+
async function folderAdded(folder: FolderContext, workspace: WorkspaceContext) {
296+
if (
297+
!configuration.folder(folder.workspaceFolder).disableAutoResolve ||
298+
configuration.backgroundCompilation.enabled
299+
) {
300+
// if background compilation is set then run compile at startup unless
301+
// this folder is a sub-folder of the workspace folder. This is to avoid
302+
// kicking off compile for multiple projects at the same time
303+
if (
304+
configuration.backgroundCompilation.enabled &&
305+
folder.workspaceFolder.uri === folder.folder
306+
) {
307+
await folder.backgroundCompilation.runTask();
308+
} else {
309+
await resolveFolderDependencies(folder, true);
310+
}
311+
312+
if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0))) {
313+
void workspace.statusItem.showStatusWhileRunning(
314+
`Loading Swift Plugins (${FolderContext.uriName(folder.workspaceFolder.uri)})`,
315+
async () => {
316+
await folder.loadSwiftPlugins(logger);
317+
workspace.updatePluginContextKey();
318+
await folder.fireEvent(FolderOperation.pluginsUpdated);
319+
}
320+
);
321+
}
322+
}
323+
}
324+
325+
return async ({ folder, operation, workspace }) => {
326+
if (!folder) {
327+
return;
328+
}
329+
330+
switch (operation) {
331+
case FolderOperation.add:
332+
// Create launch.json files based on package description.
333+
void makeDebugConfigurations(folder);
334+
if (await folder.swiftPackage.foundPackage) {
335+
// do not await for this, let packages resolve in parallel
336+
void folderAdded(folder, workspace);
337+
}
338+
break;
339+
340+
case FolderOperation.packageUpdated:
341+
// Create launch.json files based on package description.
342+
await makeDebugConfigurations(folder);
343+
if (
344+
(await folder.swiftPackage.foundPackage) &&
345+
!configuration.folder(folder.workspaceFolder).disableAutoResolve
346+
) {
347+
await resolveFolderDependencies(folder, true);
348+
}
349+
break;
350+
351+
case FolderOperation.resolvedUpdated:
352+
if (
353+
(await folder.swiftPackage.foundPackage) &&
354+
!configuration.folder(folder.workspaceFolder).disableAutoResolve
355+
) {
356+
await resolveFolderDependencies(folder, true);
357+
}
358+
}
359+
};
360+
}
361+
362+
async function createActiveToolchain(
363+
extension: vscode.ExtensionContext,
364+
contextKeys: ContextKeys,
365+
logger: SwiftLogger
366+
): Promise<SwiftToolchain> {
367+
try {
368+
const toolchain = await SwiftToolchain.create(extension.extensionPath, undefined, logger);
369+
toolchain.logDiagnostics(logger);
370+
contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion);
371+
return toolchain;
372+
} catch (error) {
373+
if (!(await showToolchainError())) {
374+
throw error;
375+
}
376+
return await createActiveToolchain(extension, contextKeys, logger);
377+
}
378+
}

0 commit comments

Comments
 (0)