Skip to content

Commit 9c4ace0

Browse files
delay WorkspaceContext creation until after extension activation
1 parent 7f53b13 commit 9c4ace0

35 files changed

+774
-725
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,9 +2046,9 @@
20462046
"check-package-json": "tsx ./scripts/check_package_json.ts",
20472047
"test": "vscode-test && npm run grammar-test",
20482048
"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",
2049-
"integration-test": "npm test -- --label integrationTests",
2050-
"unit-test": "npm test -- --label unitTests",
2051-
"coverage": "npm test -- --coverage",
2049+
"integration-test": "npm run pretest && vscode-test --label integrationTests",
2050+
"unit-test": "npm run pretest && vscode-test --label unitTests",
2051+
"coverage": "npm run pretest && vscode-test --coverage",
20522052
"compile-tests": "del-cli ./assets/test/**/.build && del-cli ./assets/test/**/.spm-cache && npm run compile",
20532053
"package": "tsx ./scripts/package.ts",
20542054
"dev-package": "tsx ./scripts/dev_package.ts",

src/SwiftExtensionApi.ts

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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 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+
| {
42+
type: "initializing";
43+
promise: Promise<WorkspaceContext>;
44+
cancellation: vscode.CancellationTokenSource;
45+
}
46+
| { type: "active"; context: WorkspaceContext; subscriptions: vscode.Disposable[] }
47+
| { type: "failed"; error: Error }
48+
) & { activatedBy: Error };
49+
50+
export class SwiftExtensionApi implements Api {
51+
private state?: State;
52+
53+
get workspaceContext(): WorkspaceContext | undefined {
54+
if (this.state?.type !== "active") {
55+
return undefined;
56+
}
57+
return this.state.context;
58+
}
59+
60+
contextKeys: ContextKeys;
61+
62+
logger: SwiftLogger;
63+
64+
constructor(private readonly extensionContext: vscode.ExtensionContext) {
65+
this.contextKeys = createContextKeys();
66+
this.logger = configureLogging(extensionContext);
67+
}
68+
69+
async waitForWorkspaceContext(): Promise<WorkspaceContext> {
70+
if (!this.state) {
71+
throw new Error("The Swift extension has not been activated yet.");
72+
}
73+
if (this.state.type === "failed") {
74+
throw this.state.error;
75+
}
76+
if (this.state.type === "active") {
77+
return this.state.context;
78+
}
79+
return await this.state.promise;
80+
}
81+
82+
async withWorkspaceContext<T>(task: (ctx: WorkspaceContext) => T | Promise<T>): Promise<T> {
83+
const workspaceContext = await this.waitForWorkspaceContext();
84+
return await task(workspaceContext);
85+
}
86+
87+
activate(): void {
88+
if (this.state) {
89+
throw new Error("The Swift extension has already been activated.", {
90+
cause: this.state.activatedBy,
91+
});
92+
}
93+
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+
this.extensionContext.subscriptions.push(
103+
new SwiftEnvironmentVariablesManager(this.extensionContext),
104+
SwiftTerminalProfileProvider.register(),
105+
...registerCommands(this),
106+
registerDebugger(this),
107+
new SelectedXcodeWatcher(this.logger),
108+
getReadOnlyDocumentProvider()
109+
);
110+
111+
const activatedBy = Error("The extension was activated by:");
112+
activatedBy.name = "Activation Source";
113+
const tokenSource = new vscode.CancellationTokenSource();
114+
this.state = {
115+
type: "initializing",
116+
activatedBy: Error("The extension was activated by:"),
117+
cancellation: new vscode.CancellationTokenSource(),
118+
promise: this.initializeWorkspace(tokenSource.token).then(
119+
({ context, subscriptions }) => {
120+
this.state = { type: "active", activatedBy, context, subscriptions };
121+
return context;
122+
},
123+
error => {
124+
if (!tokenSource.token.isCancellationRequested) {
125+
this.state = { type: "failed", activatedBy, error };
126+
}
127+
throw error;
128+
}
129+
),
130+
};
131+
132+
// Mark the extension as activated.
133+
this.contextKeys.isActivated = true;
134+
} catch (error) {
135+
const errorMessage = getErrorDescription(error);
136+
// show this error message as the VS Code error message only shows when running
137+
// the extension through the debugger
138+
void vscode.window.showErrorMessage(
139+
`Activating Swift extension failed: ${errorMessage}`
140+
);
141+
throw error;
142+
}
143+
}
144+
145+
private async initializeWorkspace(
146+
token: vscode.CancellationToken
147+
): Promise<{ context: WorkspaceContext; subscriptions: vscode.Disposable[] }> {
148+
const globalToolchain = await createActiveToolchain(
149+
this.extensionContext,
150+
this.contextKeys,
151+
this.logger
152+
);
153+
const workspaceContext = new WorkspaceContext(
154+
this.extensionContext,
155+
this.contextKeys,
156+
this.logger,
157+
globalToolchain
158+
);
159+
// project panel provider
160+
const dependenciesView = vscode.window.createTreeView("projectPanel", {
161+
treeDataProvider: workspaceContext.projectPanel,
162+
showCollapseAll: true,
163+
});
164+
workspaceContext.projectPanel.observeFolders(dependenciesView);
165+
166+
if (token.isCancellationRequested) {
167+
throw new Error("WorkspaceContext initialization was cancelled.");
168+
}
169+
return {
170+
context: workspaceContext,
171+
subscriptions: [
172+
vscode.tasks.registerTaskProvider("swift", workspaceContext.taskProvider),
173+
vscode.tasks.registerTaskProvider("swift-plugin", workspaceContext.pluginProvider),
174+
new LanguageStatusItems(workspaceContext),
175+
workspaceContext.onDidChangeFolders(({ folder, operation }) => {
176+
this.logger.info(`${operation}: ${folder?.folder.fsPath}`, folder?.name);
177+
}),
178+
dependenciesView,
179+
workspaceContext.onDidChangeFolders(handleFolderEvent(this.logger)),
180+
TestExplorer.observeFolders(workspaceContext),
181+
registerSourceKitSchemaWatcher(workspaceContext),
182+
],
183+
};
184+
}
185+
186+
deactivate(): void {
187+
this.contextKeys.isActivated = false;
188+
if (this.state?.type === "initializing") {
189+
this.state.cancellation.cancel();
190+
}
191+
if (this.state?.type === "active") {
192+
this.state.context.dispose();
193+
this.state.subscriptions.forEach(s => s.dispose());
194+
}
195+
this.extensionContext.subscriptions.forEach(subscription => subscription.dispose());
196+
this.extensionContext.subscriptions.length = 0;
197+
this.state = undefined;
198+
}
199+
200+
dispose(): void {
201+
this.logger.dispose();
202+
}
203+
}
204+
205+
function configureLogging(context: vscode.ExtensionContext) {
206+
const logger = new SwiftLoggerFactory(context.logUri).create(
207+
"Swift",
208+
"swift-vscode-extension.log"
209+
);
210+
// Create log directory asynchronously but don't await it to avoid blocking activation
211+
void vscode.workspace.fs
212+
.createDirectory(context.logUri)
213+
.then(undefined, error => logger.warn(`Failed to create log directory: ${error}`));
214+
return logger;
215+
}
216+
217+
function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise<void> {
218+
// function called when a folder is added. I broke this out so we can trigger it
219+
// without having to await for it.
220+
async function folderAdded(folder: FolderContext, workspace: WorkspaceContext) {
221+
if (
222+
!configuration.folder(folder.workspaceFolder).disableAutoResolve ||
223+
configuration.backgroundCompilation.enabled
224+
) {
225+
// if background compilation is set then run compile at startup unless
226+
// this folder is a sub-folder of the workspace folder. This is to avoid
227+
// kicking off compile for multiple projects at the same time
228+
if (
229+
configuration.backgroundCompilation.enabled &&
230+
folder.workspaceFolder.uri === folder.folder
231+
) {
232+
await folder.backgroundCompilation.runTask();
233+
} else {
234+
await resolveFolderDependencies(folder, true);
235+
}
236+
237+
if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0))) {
238+
void workspace.statusItem.showStatusWhileRunning(
239+
`Loading Swift Plugins (${FolderContext.uriName(folder.workspaceFolder.uri)})`,
240+
async () => {
241+
await folder.loadSwiftPlugins(logger);
242+
workspace.updatePluginContextKey();
243+
await folder.fireEvent(FolderOperation.pluginsUpdated);
244+
}
245+
);
246+
}
247+
}
248+
}
249+
250+
return async ({ folder, operation, workspace }) => {
251+
if (!folder) {
252+
return;
253+
}
254+
255+
switch (operation) {
256+
case FolderOperation.add:
257+
// Create launch.json files based on package description.
258+
void makeDebugConfigurations(folder);
259+
if (await folder.swiftPackage.foundPackage) {
260+
// do not await for this, let packages resolve in parallel
261+
void folderAdded(folder, workspace);
262+
}
263+
break;
264+
265+
case FolderOperation.packageUpdated:
266+
// Create launch.json files based on package description.
267+
await makeDebugConfigurations(folder);
268+
if (
269+
(await folder.swiftPackage.foundPackage) &&
270+
!configuration.folder(folder.workspaceFolder).disableAutoResolve
271+
) {
272+
await resolveFolderDependencies(folder, true);
273+
}
274+
break;
275+
276+
case FolderOperation.resolvedUpdated:
277+
if (
278+
(await folder.swiftPackage.foundPackage) &&
279+
!configuration.folder(folder.workspaceFolder).disableAutoResolve
280+
) {
281+
await resolveFolderDependencies(folder, true);
282+
}
283+
}
284+
};
285+
}
286+
287+
async function createActiveToolchain(
288+
extension: vscode.ExtensionContext,
289+
contextKeys: ContextKeys,
290+
logger: SwiftLogger
291+
): Promise<SwiftToolchain> {
292+
try {
293+
const toolchain = await SwiftToolchain.create(extension.extensionPath, undefined, logger);
294+
toolchain.logDiagnostics(logger);
295+
contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion);
296+
return toolchain;
297+
} catch (error) {
298+
if (!(await showToolchainError())) {
299+
throw error;
300+
}
301+
return await createActiveToolchain(extension, contextKeys, logger);
302+
}
303+
}

src/WorkspaceContext.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { TestKind } from "./TestExplorer/TestKind";
2121
import { TestRunManager } from "./TestExplorer/TestRunManager";
2222
import configuration from "./configuration";
2323
import { ContextKeys } from "./contextKeys";
24-
import { LLDBDebugConfigurationProvider } from "./debugger/debugAdapterFactory";
2524
import { makeDebugConfigurations } from "./debugger/launch";
2625
import { DocumentationManager } from "./documentation/DocumentationManager";
2726
import { CommentCompletionProviders } from "./editor/CommentCompletion";
@@ -56,7 +55,6 @@ export class WorkspaceContext implements vscode.Disposable {
5655
public diagnostics: DiagnosticsManager;
5756
public taskProvider: SwiftTaskProvider;
5857
public pluginProvider: SwiftPluginTaskProvider;
59-
public launchProvider: LLDBDebugConfigurationProvider;
6058
public subscriptions: vscode.Disposable[];
6159
public commentCompletionProvider: CommentCompletionProviders;
6260
public documentation: DocumentationManager;
@@ -100,7 +98,6 @@ export class WorkspaceContext implements vscode.Disposable {
10098
this.diagnostics = new DiagnosticsManager(this);
10199
this.taskProvider = new SwiftTaskProvider(this);
102100
this.pluginProvider = new SwiftPluginTaskProvider(this);
103-
this.launchProvider = new LLDBDebugConfigurationProvider(process.platform, this, logger);
104101
this.documentation = new DocumentationManager(extensionContext, this);
105102
this.currentDocument = null;
106103
this.commentCompletionProvider = new CommentCompletionProviders();
@@ -225,7 +222,6 @@ export class WorkspaceContext implements vscode.Disposable {
225222
this.diagnostics,
226223
this.documentation,
227224
this.languageClientManager,
228-
this.logger,
229225
this.statusItem,
230226
this.buildStatus,
231227
this.projectPanel,

0 commit comments

Comments
 (0)