Skip to content

Commit 94ff8f0

Browse files
authored
Support different toolchains per folder (#1478)
* 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 4d3313e commit 94ff8f0

Some content is hidden

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

54 files changed

+1264
-631
lines changed

src/FolderContext.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { BackgroundCompilation } from "./BackgroundCompilation";
2323
import { TaskQueue } from "./tasks/TaskQueue";
2424
import { isPathInsidePath } from "./utilities/filesystem";
2525
import { SwiftOutputChannel } from "./ui/SwiftOutputChannel";
26+
import { SwiftToolchain } from "./toolchain/toolchain";
2627

2728
export class FolderContext implements vscode.Disposable {
2829
private packageWatcher: PackageWatcher;
@@ -39,13 +40,13 @@ export class FolderContext implements vscode.Disposable {
3940
*/
4041
private constructor(
4142
public folder: vscode.Uri,
43+
public toolchain: SwiftToolchain,
4244
public linuxMain: LinuxMain,
4345
public swiftPackage: SwiftPackage,
4446
public workspaceFolder: vscode.WorkspaceFolder,
4547
public workspaceContext: WorkspaceContext
4648
) {
4749
this.packageWatcher = new PackageWatcher(this, workspaceContext);
48-
this.packageWatcher.install();
4950
this.backgroundCompilation = new BackgroundCompilation(this);
5051
this.taskQueue = new TaskQueue(this);
5152
}
@@ -71,16 +72,19 @@ export class FolderContext implements vscode.Disposable {
7172
): Promise<FolderContext> {
7273
const statusItemText = `Loading Package (${FolderContext.uriName(folder)})`;
7374
workspaceContext.statusItem.start(statusItemText);
75+
76+
const toolchain = await SwiftToolchain.create(folder);
7477
const { linuxMain, swiftPackage } =
7578
await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => {
7679
const linuxMain = await LinuxMain.create(folder);
77-
const swiftPackage = await SwiftPackage.create(folder, workspaceContext.toolchain);
80+
const swiftPackage = await SwiftPackage.create(folder, toolchain);
7881
return { linuxMain, swiftPackage };
7982
});
8083
workspaceContext.statusItem.end(statusItemText);
8184

8285
const folderContext = new FolderContext(
8386
folder,
87+
toolchain,
8488
linuxMain,
8589
swiftPackage,
8690
workspaceFolder,
@@ -97,6 +101,10 @@ export class FolderContext implements vscode.Disposable {
97101
folderContext.name
98102
);
99103
}
104+
105+
// Start watching for changes to Package.swift, Package.resolved and .swift-version
106+
await folderContext.packageWatcher.install();
107+
100108
return folderContext;
101109
}
102110

@@ -117,9 +125,13 @@ export class FolderContext implements vscode.Disposable {
117125
return this.workspaceFolder.uri === this.folder;
118126
}
119127

128+
get swiftVersion() {
129+
return this.toolchain.swiftVersion;
130+
}
131+
120132
/** reload swift package for this folder */
121133
async reload() {
122-
await this.swiftPackage.reload(this.workspaceContext.toolchain);
134+
await this.swiftPackage.reload(this.toolchain);
123135
}
124136

125137
/** reload Package.resolved for this folder */
@@ -134,7 +146,7 @@ export class FolderContext implements vscode.Disposable {
134146

135147
/** Load Swift Plugins and store in Package */
136148
async loadSwiftPlugins(outputChannel: SwiftOutputChannel) {
137-
await this.swiftPackage.loadSwiftPlugins(this.workspaceContext.toolchain, outputChannel);
149+
await this.swiftPackage.loadSwiftPlugins(this.toolchain, outputChannel);
138150
}
139151

140152
/**

src/PackageWatcher.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@
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";
22+
import { fileExists } from "./utilities/filesystem";
23+
import { showReloadExtensionNotification } from "./ui/ReloadExtension";
1924

2025
/**
2126
* Watches for changes to **Package.swift** and **Package.resolved**.
@@ -28,6 +33,8 @@ export class PackageWatcher {
2833
private resolvedFileWatcher?: vscode.FileSystemWatcher;
2934
private workspaceStateFileWatcher?: vscode.FileSystemWatcher;
3035
private snippetWatcher?: vscode.FileSystemWatcher;
36+
private swiftVersionFileWatcher?: vscode.FileSystemWatcher;
37+
private currentVersion?: Version;
3138

3239
constructor(
3340
private folderContext: FolderContext,
@@ -38,11 +45,12 @@ export class PackageWatcher {
3845
* Creates and installs {@link vscode.FileSystemWatcher file system watchers} for
3946
* **Package.swift** and **Package.resolved**.
4047
*/
41-
install() {
48+
async install() {
4249
this.packageFileWatcher = this.createPackageFileWatcher();
4350
this.resolvedFileWatcher = this.createResolvedFileWatcher();
44-
this.workspaceStateFileWatcher = this.createWorkspaceStateFileWatcher();
51+
this.workspaceStateFileWatcher = await this.createWorkspaceStateFileWatcher();
4552
this.snippetWatcher = this.createSnippetFileWatcher();
53+
this.swiftVersionFileWatcher = await this.createSwiftVersionFileWatcher();
4654
}
4755

4856
/**
@@ -54,6 +62,7 @@ export class PackageWatcher {
5462
this.resolvedFileWatcher?.dispose();
5563
this.workspaceStateFileWatcher?.dispose();
5664
this.snippetWatcher?.dispose();
65+
this.swiftVersionFileWatcher?.dispose();
5766
}
5867

5968
private createPackageFileWatcher(): vscode.FileSystemWatcher {
@@ -76,7 +85,7 @@ export class PackageWatcher {
7685
return watcher;
7786
}
7887

79-
private createWorkspaceStateFileWatcher(): vscode.FileSystemWatcher {
88+
private async createWorkspaceStateFileWatcher(): Promise<vscode.FileSystemWatcher> {
8089
const uri = vscode.Uri.joinPath(
8190
vscode.Uri.file(
8291
BuildFlags.buildDirectoryFromWorkspacePath(this.folderContext.folder.fsPath, true)
@@ -87,6 +96,11 @@ export class PackageWatcher {
8796
watcher.onDidCreate(async () => await this.handleWorkspaceStateChange());
8897
watcher.onDidChange(async () => await this.handleWorkspaceStateChange());
8998
watcher.onDidDelete(async () => await this.handleWorkspaceStateChange());
99+
100+
if (await fileExists(uri.fsPath)) {
101+
await this.handleWorkspaceStateChange();
102+
}
103+
90104
return watcher;
91105
}
92106

@@ -99,6 +113,45 @@ export class PackageWatcher {
99113
return watcher;
100114
}
101115

116+
private async createSwiftVersionFileWatcher(): Promise<vscode.FileSystemWatcher> {
117+
const watcher = vscode.workspace.createFileSystemWatcher(
118+
new vscode.RelativePattern(this.folderContext.folder, ".swift-version")
119+
);
120+
watcher.onDidCreate(async () => await this.handleSwiftVersionFileChange());
121+
watcher.onDidChange(async () => await this.handleSwiftVersionFileChange());
122+
watcher.onDidDelete(async () => await this.handleSwiftVersionFileChange());
123+
this.currentVersion =
124+
(await this.readSwiftVersionFile()) ?? this.folderContext.toolchain.swiftVersion;
125+
return watcher;
126+
}
127+
128+
async handleSwiftVersionFileChange() {
129+
const version = await this.readSwiftVersionFile();
130+
if (version && version.toString() !== this.currentVersion?.toString()) {
131+
this.workspaceContext.fireEvent(
132+
this.folderContext,
133+
FolderOperation.swiftVersionUpdated
134+
);
135+
await showReloadExtensionNotification(
136+
"Changing the swift toolchain version requires the extension to be reloaded"
137+
);
138+
}
139+
this.currentVersion = version ?? this.folderContext.toolchain.swiftVersion;
140+
}
141+
142+
private async readSwiftVersionFile() {
143+
const versionFile = path.join(this.folderContext.folder.fsPath, ".swift-version");
144+
try {
145+
const contents = await fs.readFile(versionFile);
146+
return Version.fromString(contents.toString().trim());
147+
} catch (error) {
148+
this.workspaceContext.outputChannel.appendLine(
149+
`Failed to read .swift-version file at ${versionFile}: ${error}`
150+
);
151+
return undefined;
152+
}
153+
}
154+
102155
/**
103156
* Handles a create or change event for **Package.swift**.
104157
*

src/SwiftSnippets.ts

Lines changed: 3 additions & 3 deletions
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

Lines changed: 8 additions & 8 deletions
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
@@ -182,10 +182,10 @@ export class TestExplorer {
182182
break;
183183
case FolderOperation.focus:
184184
if (folder) {
185-
workspace.languageClientManager.documentSymbolWatcher = (
186-
document,
187-
symbols
188-
) => TestExplorer.onDocumentSymbols(folder, document, symbols);
185+
const languageClientManager =
186+
workspace.languageClientManager.get(folder);
187+
languageClientManager.documentSymbolWatcher = (document, symbols) =>
188+
TestExplorer.onDocumentSymbols(folder, document, symbols);
189189
}
190190
}
191191
}
@@ -307,7 +307,7 @@ export class TestExplorer {
307307
}
308308
});
309309
}
310-
const toolchain = explorer.folderContext.workspaceContext.toolchain;
310+
const toolchain = explorer.folderContext.toolchain;
311311
// get build options before build is run so we can be sure they aren't changed
312312
// mid-build
313313
const testBuildOptions = buildOptions(toolchain);

src/TestExplorer/TestRunner.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ export class TestRunner {
396396
this.xcTestOutputParser =
397397
testKind === TestKind.parallel
398398
? new ParallelXCTestOutputParser(
399-
this.folderContext.workspaceContext.toolchain.hasMultiLineParallelTestOutput
399+
this.folderContext.toolchain.hasMultiLineParallelTestOutput
400400
)
401401
: new XCTestOutputParser();
402402
this.swiftTestOutputParser = new SwiftTestingOutputParser(
@@ -774,7 +774,7 @@ export class TestRunner {
774774
prefix: this.folderContext.name,
775775
presentationOptions: { reveal: vscode.TaskRevealKind.Never },
776776
},
777-
this.folderContext.workspaceContext.toolchain,
777+
this.folderContext.toolchain,
778778
{ ...process.env, ...testBuildConfig.env },
779779
{ readOnlyTerminal: process.platform !== "win32" }
780780
);
@@ -859,7 +859,7 @@ export class TestRunner {
859859

860860
const buffer = await asyncfs.readFile(filename, "utf8");
861861
const xUnitParser = new TestXUnitParser(
862-
this.folderContext.workspaceContext.toolchain.hasMultiLineParallelTestOutput
862+
this.folderContext.toolchain.hasMultiLineParallelTestOutput
863863
);
864864
const results = await xUnitParser.parse(
865865
buffer,

src/WorkspaceContext.ts

Lines changed: 23 additions & 19 deletions
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);
@@ -202,8 +202,8 @@ export class WorkspaceContext implements vscode.Disposable {
202202
this.subscriptions.length = 0;
203203
}
204204

205-
get swiftVersion() {
206-
return this.toolchain.swiftVersion;
205+
get globalToolchainSwiftVersion() {
206+
return this.globalToolchain.swiftVersion;
207207
}
208208

209209
/** Get swift version and create WorkspaceContext */
@@ -248,19 +248,21 @@ export class WorkspaceContext implements vscode.Disposable {
248248
contextKeys.currentTargetType = undefined;
249249
}
250250

251-
// Set context keys that depend on features from SourceKit-LSP
252-
this.languageClientManager.useLanguageClient(async client => {
253-
const experimentalCaps = client.initializeResult?.capabilities.experimental;
254-
if (!experimentalCaps) {
255-
contextKeys.supportsReindexing = false;
256-
contextKeys.supportsDocumentationLivePreview = false;
257-
return;
258-
}
259-
contextKeys.supportsReindexing =
260-
experimentalCaps[ReIndexProjectRequest.method] !== undefined;
261-
contextKeys.supportsDocumentationLivePreview =
262-
experimentalCaps[DocCDocumentationRequest.method] !== undefined;
263-
});
251+
if (this.currentFolder) {
252+
const languageClient = this.languageClientManager.get(this.currentFolder);
253+
languageClient.useLanguageClient(async client => {
254+
const experimentalCaps = client.initializeResult?.capabilities.experimental;
255+
if (!experimentalCaps) {
256+
contextKeys.supportsReindexing = false;
257+
contextKeys.supportsDocumentationLivePreview = false;
258+
return;
259+
}
260+
contextKeys.supportsReindexing =
261+
experimentalCaps[ReIndexProjectRequest.method] !== undefined;
262+
contextKeys.supportsDocumentationLivePreview =
263+
experimentalCaps[DocCDocumentationRequest.method] !== undefined;
264+
});
265+
}
264266

265267
setSnippetContextKey(this);
266268
}
@@ -645,6 +647,8 @@ export enum FolderOperation {
645647
packageViewUpdated = "packageViewUpdated",
646648
// Package plugins list has been updated
647649
pluginsUpdated = "pluginsUpdated",
650+
// The folder's swift toolchain version has been updated
651+
swiftVersionUpdated = "swiftVersionUpdated",
648652
}
649653

650654
/** Workspace Folder Event */

0 commit comments

Comments
 (0)