Skip to content

Commit bf2f81e

Browse files
committed
Support different toolchains per folder
With the introduction of swiftly's `.swift-version` file the active toolchain can now be per-folder instead of global. If the user has multiple different packages open they may each be using a different toolchain as defined by their `.swift-version` file. In order to support this new paradigm the `toolchain` has been moved from `WorkspaceContext` to `FolderContext`. Each time a folder (package) is added to the workspace a new toolchain is created, as it might be different from folder to folder. The toolchain created respects the `.swift-version` file. If the toolchain specified in the `.swift-version` file is not installed an error message is shown prompting the user to install the version with swiftly. There is still a `globalToolchain` on the `WorkspaceContext` which refers to the globally available toolchain. This would be the toolchain used when you run `swift` outside of a workspace folder. This is mainly used as a fallback toolchain for when there are no workspace folders. It is generally advisable to use the toolchain provided on the `FolderContext` to ensure you don't end up using mismatched versions. This PR also refactors the `LanguageClientManager` so that one instance of sourcekit-lsp is started per-toolchain, coordinating startup so that the server from a given toolchain starts up when a folder using that toolchain is added to the workspace. While this PR adds support for .swift-version files, there is still quite a bit of work to do to make using swiftly with the VS Code Swift extension a nicer experience including: Installing swiftly directly from the extension, downloading missing toolchains automatically, listing/picking/downloading toolchains via `swiftly list`, a smoother toolchain switching experience that would optionally write the `.swift-version` file, and more.
1 parent 1a60d97 commit bf2f81e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1054
-625
lines changed

src/FolderContext.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { WorkspaceContext, FolderOperation } from "./WorkspaceContext";
2222
import { BackgroundCompilation } from "./BackgroundCompilation";
2323
import { TaskQueue } from "./tasks/TaskQueue";
2424
import { isPathInsidePath } from "./utilities/filesystem";
25+
import { SwiftToolchain } from "./toolchain/toolchain";
2526

2627
export class FolderContext implements vscode.Disposable {
2728
private packageWatcher: PackageWatcher;
@@ -38,13 +39,13 @@ export class FolderContext implements vscode.Disposable {
3839
*/
3940
private constructor(
4041
public folder: vscode.Uri,
42+
public toolchain: SwiftToolchain,
4143
public linuxMain: LinuxMain,
4244
public swiftPackage: SwiftPackage,
4345
public workspaceFolder: vscode.WorkspaceFolder,
4446
public workspaceContext: WorkspaceContext
4547
) {
4648
this.packageWatcher = new PackageWatcher(this, workspaceContext);
47-
this.packageWatcher.install();
4849
this.backgroundCompilation = new BackgroundCompilation(this);
4950
this.taskQueue = new TaskQueue(this);
5051
}
@@ -71,17 +72,19 @@ export class FolderContext implements vscode.Disposable {
7172
const statusItemText = `Loading Package (${FolderContext.uriName(folder)})`;
7273
workspaceContext.statusItem.start(statusItemText);
7374

75+
const toolchain = await SwiftToolchain.create(folder);
7476
const { linuxMain, swiftPackage } =
7577
await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => {
7678
const linuxMain = await LinuxMain.create(folder);
77-
const swiftPackage = await SwiftPackage.create(folder, workspaceContext.toolchain);
79+
const swiftPackage = await SwiftPackage.create(folder, toolchain);
7880
return { linuxMain, swiftPackage };
7981
});
8082

8183
workspaceContext.statusItem.end(statusItemText);
8284

8385
const folderContext = new FolderContext(
8486
folder,
87+
toolchain,
8588
linuxMain,
8689
swiftPackage,
8790
workspaceFolder,
@@ -99,6 +102,9 @@ export class FolderContext implements vscode.Disposable {
99102
);
100103
}
101104

105+
// Start watching for changes to Package.swift, Package.resolved and .swift-version
106+
await folderContext.packageWatcher.install();
107+
102108
return folderContext;
103109
}
104110

@@ -119,9 +125,13 @@ export class FolderContext implements vscode.Disposable {
119125
return this.workspaceFolder.uri === this.folder;
120126
}
121127

128+
get swiftVersion() {
129+
return this.toolchain.swiftVersion;
130+
}
131+
122132
/** reload swift package for this folder */
123133
async reload() {
124-
await this.swiftPackage.reload(this.workspaceContext.toolchain);
134+
await this.swiftPackage.reload(this.toolchain);
125135
}
126136

127137
/** reload Package.resolved for this folder */
@@ -136,7 +146,7 @@ export class FolderContext implements vscode.Disposable {
136146

137147
/** Load Swift Plugins and store in Package */
138148
async loadSwiftPlugins() {
139-
await this.swiftPackage.loadSwiftPlugins(this.workspaceContext.toolchain);
149+
await this.swiftPackage.loadSwiftPlugins(this.toolchain);
140150
}
141151

142152
/**

src/PackageWatcher.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import * as path from "path";
16+
import * as fs from "fs/promises";
1517
import * as vscode from "vscode";
1618
import { FolderContext } from "./FolderContext";
1719
import { FolderOperation, WorkspaceContext } from "./WorkspaceContext";
1820
import { BuildFlags } from "./toolchain/BuildFlags";
21+
import { Version } from "./utilities/version";
1922

2023
/**
2124
* Watches for changes to **Package.swift** and **Package.resolved**.
@@ -28,6 +31,8 @@ export class PackageWatcher {
2831
private resolvedFileWatcher?: vscode.FileSystemWatcher;
2932
private workspaceStateFileWatcher?: vscode.FileSystemWatcher;
3033
private snippetWatcher?: vscode.FileSystemWatcher;
34+
private swiftVersionFileWatcher?: vscode.FileSystemWatcher;
35+
private currentVersion?: Version;
3136

3237
constructor(
3338
private folderContext: FolderContext,
@@ -38,11 +43,12 @@ export class PackageWatcher {
3843
* Creates and installs {@link vscode.FileSystemWatcher file system watchers} for
3944
* **Package.swift** and **Package.resolved**.
4045
*/
41-
install() {
46+
async install() {
4247
this.packageFileWatcher = this.createPackageFileWatcher();
4348
this.resolvedFileWatcher = this.createResolvedFileWatcher();
4449
this.workspaceStateFileWatcher = this.createWorkspaceStateFileWatcher();
4550
this.snippetWatcher = this.createSnippetFileWatcher();
51+
this.swiftVersionFileWatcher = await this.createSwiftVersionFileWatcher();
4652
}
4753

4854
/**
@@ -54,6 +60,7 @@ export class PackageWatcher {
5460
this.resolvedFileWatcher?.dispose();
5561
this.workspaceStateFileWatcher?.dispose();
5662
this.snippetWatcher?.dispose();
63+
this.swiftVersionFileWatcher?.dispose();
5764
}
5865

5966
private createPackageFileWatcher(): vscode.FileSystemWatcher {
@@ -99,6 +106,43 @@ export class PackageWatcher {
99106
return watcher;
100107
}
101108

109+
private async createSwiftVersionFileWatcher(): Promise<vscode.FileSystemWatcher> {
110+
const watcher = vscode.workspace.createFileSystemWatcher(
111+
new vscode.RelativePattern(this.folderContext.folder, ".swift-version")
112+
);
113+
watcher.onDidCreate(async () => await this.handleSwiftVersionFileChange());
114+
watcher.onDidChange(async () => await this.handleSwiftVersionFileChange());
115+
watcher.onDidDelete(async () => await this.handleSwiftVersionFileChange());
116+
this.currentVersion =
117+
(await this.readSwiftVersionFile()) ?? this.folderContext.toolchain.swiftVersion;
118+
return watcher;
119+
}
120+
121+
async handleSwiftVersionFileChange() {
122+
try {
123+
const version = await this.readSwiftVersionFile();
124+
if (version && version.toString() !== this.currentVersion?.toString()) {
125+
this.workspaceContext.fireEvent(
126+
this.folderContext,
127+
FolderOperation.swiftVersionUpdated
128+
);
129+
}
130+
this.currentVersion = version ?? this.folderContext.toolchain.swiftVersion;
131+
} catch {
132+
// do nothing
133+
}
134+
}
135+
136+
private async readSwiftVersionFile() {
137+
const versionFile = path.join(this.folderContext.folder.fsPath, ".swift-version");
138+
try {
139+
const contents = await fs.readFile(versionFile);
140+
return Version.fromString(contents.toString().trim());
141+
} catch {
142+
return undefined;
143+
}
144+
}
145+
102146
/**
103147
* Handles a create or change event for **Package.swift**.
104148
*

src/SwiftSnippets.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ import { TaskOperation } from "./tasks/TaskQueue";
2626
*/
2727
export function setSnippetContextKey(ctx: WorkspaceContext) {
2828
if (
29-
ctx.swiftVersion.isLessThan({ major: 5, minor: 7, patch: 0 }) ||
3029
!ctx.currentFolder ||
31-
!ctx.currentDocument
30+
!ctx.currentDocument ||
31+
ctx.currentFolder.swiftVersion.isLessThan({ major: 5, minor: 7, patch: 0 })
3232
) {
3333
contextKeys.fileIsSnippet = false;
3434
return;
@@ -97,7 +97,7 @@ export async function debugSnippetWithOptions(
9797
reveal: vscode.TaskRevealKind.Always,
9898
},
9999
},
100-
ctx.toolchain
100+
folderContext.toolchain
101101
);
102102
const snippetDebugConfig = createSnippetConfiguration(snippetName, folderContext);
103103
try {

src/TestExplorer/TestExplorer.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ export class TestExplorer {
6767
this.onDidCreateTestRunEmitter
6868
);
6969

70-
this.lspTestDiscovery = new LSPTestDiscovery(
71-
folderContext.workspaceContext.languageClientManager
72-
);
70+
const workspaceContext = folderContext.workspaceContext;
71+
const languageClientManager = workspaceContext.languageClientManager.get(folderContext);
72+
this.lspTestDiscovery = new LSPTestDiscovery(languageClientManager);
7373

7474
// add end of task handler to be called whenever a build task has finished. If
7575
// it is the build task for this folder then update the tests
@@ -169,10 +169,10 @@ export class TestExplorer {
169169
break;
170170
case FolderOperation.focus:
171171
if (folder) {
172-
workspace.languageClientManager.documentSymbolWatcher = (
173-
document,
174-
symbols
175-
) => TestExplorer.onDocumentSymbols(folder, document, symbols);
172+
const languageClientManager =
173+
workspace.languageClientManager.get(folder);
174+
languageClientManager.documentSymbolWatcher = (document, symbols) =>
175+
TestExplorer.onDocumentSymbols(folder, document, symbols);
176176
}
177177
}
178178
}
@@ -289,7 +289,7 @@ export class TestExplorer {
289289
}
290290
});
291291
}
292-
const toolchain = explorer.folderContext.workspaceContext.toolchain;
292+
const toolchain = explorer.folderContext.toolchain;
293293
// get build options before build is run so we can be sure they aren't changed
294294
// mid-build
295295
const testBuildOptions = buildOptions(toolchain);

src/TestExplorer/TestRunner.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ export class TestRunner {
379379
this.xcTestOutputParser =
380380
testKind === TestKind.parallel
381381
? new ParallelXCTestOutputParser(
382-
this.folderContext.workspaceContext.toolchain.hasMultiLineParallelTestOutput
382+
this.folderContext.toolchain.hasMultiLineParallelTestOutput
383383
)
384384
: new XCTestOutputParser();
385385
this.swiftTestOutputParser = new SwiftTestingOutputParser(
@@ -757,7 +757,7 @@ export class TestRunner {
757757
prefix: this.folderContext.name,
758758
presentationOptions: { reveal: vscode.TaskRevealKind.Never },
759759
},
760-
this.folderContext.workspaceContext.toolchain,
760+
this.folderContext.toolchain,
761761
{ ...process.env, ...testBuildConfig.env },
762762
{ readOnlyTerminal: process.platform !== "win32" }
763763
);
@@ -842,7 +842,7 @@ export class TestRunner {
842842

843843
const buffer = await asyncfs.readFile(filename, "utf8");
844844
const xUnitParser = new TestXUnitParser(
845-
this.folderContext.workspaceContext.toolchain.hasMultiLineParallelTestOutput
845+
this.folderContext.toolchain.hasMultiLineParallelTestOutput
846846
);
847847
const results = await xUnitParser.parse(
848848
buffer,

src/WorkspaceContext.ts

+23-19
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { StatusItem } from "./ui/StatusItem";
1919
import { SwiftOutputChannel } from "./ui/SwiftOutputChannel";
2020
import { swiftLibraryPathKey } from "./utilities/utilities";
2121
import { isPathInsidePath } from "./utilities/filesystem";
22-
import { LanguageClientManager } from "./sourcekit-lsp/LanguageClientManager";
22+
import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClientToolchainCoordinator";
2323
import { TemporaryFolder } from "./utilities/tempFolder";
2424
import { TaskManager } from "./tasks/TaskManager";
2525
import { makeDebugConfigurations } from "./debugger/launch";
@@ -45,7 +45,7 @@ export class WorkspaceContext implements vscode.Disposable {
4545
public currentDocument: vscode.Uri | null;
4646
public statusItem: StatusItem;
4747
public buildStatus: SwiftBuildStatus;
48-
public languageClientManager: LanguageClientManager;
48+
public languageClientManager: LanguageClientToolchainCoordinator;
4949
public tasks: TaskManager;
5050
public diagnostics: DiagnosticsManager;
5151
public subscriptions: vscode.Disposable[];
@@ -69,11 +69,11 @@ export class WorkspaceContext implements vscode.Disposable {
6969
extensionContext: vscode.ExtensionContext,
7070
public tempFolder: TemporaryFolder,
7171
public outputChannel: SwiftOutputChannel,
72-
public toolchain: SwiftToolchain
72+
public globalToolchain: SwiftToolchain
7373
) {
7474
this.statusItem = new StatusItem();
7575
this.buildStatus = new SwiftBuildStatus(this.statusItem);
76-
this.languageClientManager = new LanguageClientManager(this);
76+
this.languageClientManager = new LanguageClientToolchainCoordinator(this);
7777
this.tasks = new TaskManager(this);
7878
this.diagnostics = new DiagnosticsManager(this);
7979
this.documentation = new DocumentationManager(extensionContext, this);
@@ -200,8 +200,8 @@ export class WorkspaceContext implements vscode.Disposable {
200200
this.subscriptions.length = 0;
201201
}
202202

203-
get swiftVersion() {
204-
return this.toolchain.swiftVersion;
203+
get globalToolchainSwiftVersion() {
204+
return this.globalToolchain.swiftVersion;
205205
}
206206

207207
/** Get swift version and create WorkspaceContext */
@@ -239,19 +239,21 @@ export class WorkspaceContext implements vscode.Disposable {
239239
contextKeys.currentTargetType = undefined;
240240
}
241241

242-
// Set context keys that depend on features from SourceKit-LSP
243-
this.languageClientManager.useLanguageClient(async client => {
244-
const experimentalCaps = client.initializeResult?.capabilities.experimental;
245-
if (!experimentalCaps) {
246-
contextKeys.supportsReindexing = false;
247-
contextKeys.supportsDocumentationLivePreview = false;
248-
return;
249-
}
250-
contextKeys.supportsReindexing =
251-
experimentalCaps[ReIndexProjectRequest.method] !== undefined;
252-
contextKeys.supportsDocumentationLivePreview =
253-
experimentalCaps[DocCDocumentationRequest.method] !== undefined;
254-
});
242+
if (this.currentFolder) {
243+
const languageClient = this.languageClientManager.get(this.currentFolder);
244+
languageClient.useLanguageClient(async client => {
245+
const experimentalCaps = client.initializeResult?.capabilities.experimental;
246+
if (!experimentalCaps) {
247+
contextKeys.supportsReindexing = false;
248+
contextKeys.supportsDocumentationLivePreview = false;
249+
return;
250+
}
251+
contextKeys.supportsReindexing =
252+
experimentalCaps[ReIndexProjectRequest.method] !== undefined;
253+
contextKeys.supportsDocumentationLivePreview =
254+
experimentalCaps[DocCDocumentationRequest.method] !== undefined;
255+
});
256+
}
255257

256258
setSnippetContextKey(this);
257259
}
@@ -637,6 +639,8 @@ export enum FolderOperation {
637639
packageViewUpdated = "packageViewUpdated",
638640
// Package plugins list has been updated
639641
pluginsUpdated = "pluginsUpdated",
642+
// The folder's swift toolchain version has been updated
643+
swiftVersionUpdated = "swiftVersionUpdated",
640644
}
641645

642646
/** Workspace Folder Event */

src/commands.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
134134
vscode.commands.registerCommand("swift.runScript", () => runSwiftScript(ctx)),
135135
vscode.commands.registerCommand("swift.openPackage", () => {
136136
if (ctx.currentFolder) {
137-
return openPackage(ctx.toolchain.swiftVersion, ctx.currentFolder.folder);
137+
return openPackage(ctx.currentFolder.swiftVersion, ctx.currentFolder.folder);
138138
}
139139
}),
140140
vscode.commands.registerCommand(Commands.RUN_SNIPPET, target =>
@@ -145,9 +145,13 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
145145
),
146146
vscode.commands.registerCommand(Commands.RUN_PLUGIN_TASK, () => runPluginTask()),
147147
vscode.commands.registerCommand(Commands.RUN_TASK, name => runTask(ctx, name)),
148-
vscode.commands.registerCommand("swift.restartLSPServer", () =>
149-
ctx.languageClientManager.restart()
150-
),
148+
vscode.commands.registerCommand("swift.restartLSPServer", async () => {
149+
if (!ctx.currentFolder) {
150+
return;
151+
}
152+
const languageClientManager = ctx.languageClientManager.get(ctx.currentFolder);
153+
await languageClientManager.restart();
154+
}),
151155
vscode.commands.registerCommand("swift.reindexProject", () => reindexProject(ctx)),
152156
vscode.commands.registerCommand("swift.insertFunctionComment", () =>
153157
insertFunctionComment(ctx)

src/commands/build.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export async function folderCleanBuild(folderContext: FolderContext) {
6060
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
6161
group: vscode.TaskGroup.Clean,
6262
},
63-
folderContext.workspaceContext.toolchain
63+
folderContext.toolchain
6464
);
6565

6666
return await executeTaskWithUI(task, "Clean Build", folderContext);

0 commit comments

Comments
 (0)