Skip to content

Commit 1ac4fb9

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

37 files changed

+828
-726
lines changed

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,9 +2046,10 @@
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+
"code-workspace-test": "npm run pretest && vscode-test --label codeWorkspaceTests",
2051+
"unit-test": "npm run pretest && vscode-test --label unitTests",
2052+
"coverage": "npm run pretest && vscode-test --coverage",
20522053
"compile-tests": "del-cli ./assets/test/**/.build && del-cli ./assets/test/**/.spm-cache && npm run compile",
20532054
"package": "tsx ./scripts/package.ts",
20542055
"dev-package": "tsx ./scripts/dev_package.ts",

src/SwiftExtensionApi.ts

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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(callSite?: Error): 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 = callSite ?? 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,
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+
await workspaceContext.addWorkspaceFolders();
160+
// project panel provider
161+
const dependenciesView = vscode.window.createTreeView("projectPanel", {
162+
treeDataProvider: workspaceContext.projectPanel,
163+
showCollapseAll: true,
164+
});
165+
workspaceContext.projectPanel.observeFolders(dependenciesView);
166+
167+
if (token.isCancellationRequested) {
168+
throw new Error("WorkspaceContext initialization was cancelled.");
169+
}
170+
return {
171+
context: workspaceContext,
172+
subscriptions: [
173+
vscode.tasks.registerTaskProvider("swift", workspaceContext.taskProvider),
174+
vscode.tasks.registerTaskProvider("swift-plugin", workspaceContext.pluginProvider),
175+
new LanguageStatusItems(workspaceContext),
176+
workspaceContext.onDidChangeFolders(({ folder, operation }) => {
177+
this.logger.info(`${operation}: ${folder?.folder.fsPath}`, folder?.name);
178+
}),
179+
dependenciesView,
180+
workspaceContext.onDidChangeFolders(handleFolderEvent(this.logger)),
181+
TestExplorer.observeFolders(workspaceContext),
182+
registerSourceKitSchemaWatcher(workspaceContext),
183+
],
184+
};
185+
}
186+
187+
deactivate(): void {
188+
this.contextKeys.isActivated = false;
189+
if (this.state?.type === "initializing") {
190+
this.state.cancellation.cancel();
191+
}
192+
if (this.state?.type === "active") {
193+
this.state.context.dispose();
194+
this.state.subscriptions.forEach(s => s.dispose());
195+
}
196+
this.extensionContext.subscriptions.forEach(subscription => subscription.dispose());
197+
this.extensionContext.subscriptions.length = 0;
198+
this.state = undefined;
199+
}
200+
201+
dispose(): void {
202+
this.logger.dispose();
203+
}
204+
}
205+
206+
function configureLogging(context: vscode.ExtensionContext) {
207+
const logger = new SwiftLoggerFactory(context.logUri).create(
208+
"Swift",
209+
"swift-vscode-extension.log"
210+
);
211+
// Create log directory asynchronously but don't await it to avoid blocking activation
212+
void vscode.workspace.fs
213+
.createDirectory(context.logUri)
214+
.then(undefined, error => logger.warn(`Failed to create log directory: ${error}`));
215+
return logger;
216+
}
217+
218+
function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise<void> {
219+
// function called when a folder is added. I broke this out so we can trigger it
220+
// without having to await for it.
221+
async function folderAdded(folder: FolderContext, workspace: WorkspaceContext) {
222+
if (
223+
!configuration.folder(folder.workspaceFolder).disableAutoResolve ||
224+
configuration.backgroundCompilation.enabled
225+
) {
226+
// if background compilation is set then run compile at startup unless
227+
// this folder is a sub-folder of the workspace folder. This is to avoid
228+
// kicking off compile for multiple projects at the same time
229+
if (
230+
configuration.backgroundCompilation.enabled &&
231+
folder.workspaceFolder.uri === folder.folder
232+
) {
233+
await folder.backgroundCompilation.runTask();
234+
} else {
235+
await resolveFolderDependencies(folder, true);
236+
}
237+
238+
if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0))) {
239+
void workspace.statusItem.showStatusWhileRunning(
240+
`Loading Swift Plugins (${FolderContext.uriName(folder.workspaceFolder.uri)})`,
241+
async () => {
242+
await folder.loadSwiftPlugins(logger);
243+
workspace.updatePluginContextKey();
244+
await folder.fireEvent(FolderOperation.pluginsUpdated);
245+
}
246+
);
247+
}
248+
}
249+
}
250+
251+
return async ({ folder, operation, workspace }) => {
252+
if (!folder) {
253+
return;
254+
}
255+
256+
switch (operation) {
257+
case FolderOperation.add:
258+
// Create launch.json files based on package description.
259+
void makeDebugConfigurations(folder);
260+
if (await folder.swiftPackage.foundPackage) {
261+
// do not await for this, let packages resolve in parallel
262+
void folderAdded(folder, workspace);
263+
}
264+
break;
265+
266+
case FolderOperation.packageUpdated:
267+
// Create launch.json files based on package description.
268+
await makeDebugConfigurations(folder);
269+
if (
270+
(await folder.swiftPackage.foundPackage) &&
271+
!configuration.folder(folder.workspaceFolder).disableAutoResolve
272+
) {
273+
await resolveFolderDependencies(folder, true);
274+
}
275+
break;
276+
277+
case FolderOperation.resolvedUpdated:
278+
if (
279+
(await folder.swiftPackage.foundPackage) &&
280+
!configuration.folder(folder.workspaceFolder).disableAutoResolve
281+
) {
282+
await resolveFolderDependencies(folder, true);
283+
}
284+
}
285+
};
286+
}
287+
288+
async function createActiveToolchain(
289+
extension: vscode.ExtensionContext,
290+
contextKeys: ContextKeys,
291+
logger: SwiftLogger
292+
): Promise<SwiftToolchain> {
293+
try {
294+
const toolchain = await SwiftToolchain.create(extension.extensionPath, undefined, logger);
295+
toolchain.logDiagnostics(logger);
296+
contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion);
297+
return toolchain;
298+
} catch (error) {
299+
if (!(await showToolchainError())) {
300+
throw error;
301+
}
302+
return await createActiveToolchain(extension, contextKeys, logger);
303+
}
304+
}

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)