Skip to content

Commit b3ba786

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 87e6248 commit b3ba786

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

+1067
-630
lines changed

src/FolderContext.ts

+16-4
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

+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
@@ -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

+3-3
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

+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 */
@@ -246,19 +246,21 @@ export class WorkspaceContext implements vscode.Disposable {
246246
contextKeys.currentTargetType = undefined;
247247
}
248248

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

263265
setSnippetContextKey(this);
264266
}
@@ -643,6 +645,8 @@ export enum FolderOperation {
643645
packageViewUpdated = "packageViewUpdated",
644646
// Package plugins list has been updated
645647
pluginsUpdated = "pluginsUpdated",
648+
// The folder's swift toolchain version has been updated
649+
swiftVersionUpdated = "swiftVersionUpdated",
646650
}
647651

648652
/** Workspace Folder Event */

src/commands.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
135135
vscode.commands.registerCommand("swift.runScript", () => runSwiftScript(ctx)),
136136
vscode.commands.registerCommand("swift.openPackage", () => {
137137
if (ctx.currentFolder) {
138-
return openPackage(ctx.toolchain.swiftVersion, ctx.currentFolder.folder);
138+
return openPackage(ctx.currentFolder.swiftVersion, ctx.currentFolder.folder);
139139
}
140140
}),
141141
vscode.commands.registerCommand(Commands.RUN_SNIPPET, target =>
@@ -146,9 +146,13 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
146146
),
147147
vscode.commands.registerCommand(Commands.RUN_PLUGIN_TASK, () => runPluginTask()),
148148
vscode.commands.registerCommand(Commands.RUN_TASK, name => runTask(ctx, name)),
149-
vscode.commands.registerCommand("swift.restartLSPServer", () =>
150-
ctx.languageClientManager.restart()
151-
),
149+
vscode.commands.registerCommand("swift.restartLSPServer", async () => {
150+
if (!ctx.currentFolder) {
151+
return;
152+
}
153+
const languageClientManager = ctx.languageClientManager.get(ctx.currentFolder);
154+
await languageClientManager.restart();
155+
}),
152156
vscode.commands.registerCommand("swift.reindexProject", () => reindexProject(ctx)),
153157
vscode.commands.registerCommand("swift.insertFunctionComment", () =>
154158
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)