Skip to content

Commit 3b9ced8

Browse files
committed
Extract context key management into ContextKeyManager
Attempt to start to reduce the WorkspaceContext's responsibilities by moving context key related logic into a dedicated ContextKeyManager service. Adds many unit tests to support this.
1 parent 1b136a0 commit 3b9ced8

File tree

6 files changed

+917
-394
lines changed

6 files changed

+917
-394
lines changed

src/ContextKeyManager.ts

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
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 path from "path";
15+
import * as vscode from "vscode";
16+
17+
import { FolderContext } from "./FolderContext";
18+
import { LanguageClientManager } from "./sourcekit-lsp/LanguageClientManager";
19+
import { DocCDocumentationRequest, ReIndexProjectRequest } from "./sourcekit-lsp/extensions";
20+
import { Version } from "./utilities/version";
21+
22+
/**
23+
* References:
24+
*
25+
* - `when` clause contexts:
26+
* https://code.visualstudio.com/api/references/when-clause-contexts
27+
*/
28+
29+
/** Interface for getting and setting the VS Code Swift extension's context keys */
30+
export interface ContextKeys {
31+
/**
32+
* Whether or not the swift extension is activated.
33+
*/
34+
isActivated: boolean;
35+
36+
/**
37+
* Whether the workspace folder contains a Swift package.
38+
*/
39+
hasPackage: boolean;
40+
41+
/**
42+
* Whether the workspace folder contains a Swift package with at least one executable product.
43+
*/
44+
hasExecutableProduct: boolean;
45+
46+
/**
47+
* Whether the Swift package has any dependencies to display in the Package Dependencies view.
48+
*/
49+
packageHasDependencies: boolean;
50+
51+
/**
52+
* Whether the dependencies list is displayed in a nested or flat view.
53+
*/
54+
flatDependenciesList: boolean;
55+
56+
/**
57+
* Whether the Swift package has any plugins.
58+
*/
59+
packageHasPlugins: boolean;
60+
61+
/**
62+
* Whether current active file is in a SwiftPM source target folder
63+
*/
64+
currentTargetType: string | undefined;
65+
66+
/**
67+
* Whether current active file is a Snippet
68+
*/
69+
fileIsSnippet: boolean;
70+
71+
/**
72+
* Whether current active file is a Snippet
73+
*/
74+
lldbVSCodeAvailable: boolean;
75+
76+
/**
77+
* Whether the swift.createNewProject command is available.
78+
*/
79+
createNewProjectAvailable: boolean;
80+
81+
/**
82+
* Whether the SourceKit-LSP server supports reindexing the workspace.
83+
*/
84+
supportsReindexing: boolean;
85+
86+
/**
87+
* Whether the SourceKit-LSP server supports documentation live preview.
88+
*/
89+
supportsDocumentationLivePreview: boolean;
90+
91+
/**
92+
* Whether the installed version of Swiftly can be used to install toolchains from within VS Code.
93+
*/
94+
supportsSwiftlyInstall: boolean;
95+
96+
/**
97+
* Whether the swift.switchPlatform command is available.
98+
*/
99+
switchPlatformAvailable: boolean;
100+
101+
/**
102+
* Sets values for context keys that are enabled/disabled based on the toolchain version in use.
103+
*/
104+
updateKeysBasedOnActiveVersion(toolchainVersion: Version): void;
105+
106+
/**
107+
* Update context keys based on package contents. Call this when folder focus changes.
108+
*/
109+
updateForFolder(folderContext: FolderContext | null): void;
110+
111+
/**
112+
* Update context keys based on current file. Call this when the active file changes.
113+
*/
114+
updateForFile(
115+
currentDocument: vscode.Uri | null,
116+
currentFolder: FolderContext | null,
117+
languageClientManager: { get(folder: FolderContext): LanguageClientManager }
118+
): Promise<void>;
119+
120+
/**
121+
* Update hasPlugins context key by checking all folders. Call this when packages are added/removed or plugins change.
122+
*/
123+
updateForPlugins(folders: FolderContext[]): void;
124+
}
125+
126+
/**
127+
* Manages the extension's context key values.
128+
*/
129+
export class ContextKeyManager implements ContextKeys {
130+
private _isActivated = false;
131+
private _hasPackage = false;
132+
private _hasExecutableProduct = false;
133+
private _flatDependenciesList = false;
134+
private _packageHasDependencies = false;
135+
private _packageHasPlugins = false;
136+
private _currentTargetType: string | undefined = undefined;
137+
private _fileIsSnippet = false;
138+
private _lldbVSCodeAvailable = false;
139+
private _createNewProjectAvailable = false;
140+
private _supportsReindexing = false;
141+
private _supportsDocumentationLivePreview = false;
142+
private _supportsSwiftlyInstall = false;
143+
private _switchPlatformAvailable = false;
144+
145+
get isActivated(): boolean {
146+
return this._isActivated;
147+
}
148+
set isActivated(value: boolean) {
149+
this._isActivated = value;
150+
void vscode.commands.executeCommand("setContext", "swift.isActivated", value);
151+
}
152+
153+
get hasPackage(): boolean {
154+
return this._hasPackage;
155+
}
156+
set hasPackage(value: boolean) {
157+
this._hasPackage = value;
158+
void vscode.commands.executeCommand("setContext", "swift.hasPackage", value);
159+
}
160+
161+
get hasExecutableProduct(): boolean {
162+
return this._hasExecutableProduct;
163+
}
164+
set hasExecutableProduct(value: boolean) {
165+
this._hasExecutableProduct = value;
166+
void vscode.commands.executeCommand("setContext", "swift.hasExecutableProduct", value);
167+
}
168+
169+
get packageHasDependencies(): boolean {
170+
return this._packageHasDependencies;
171+
}
172+
set packageHasDependencies(value: boolean) {
173+
this._packageHasDependencies = value;
174+
void vscode.commands.executeCommand("setContext", "swift.packageHasDependencies", value);
175+
}
176+
177+
get flatDependenciesList(): boolean {
178+
return this._flatDependenciesList;
179+
}
180+
set flatDependenciesList(value: boolean) {
181+
this._flatDependenciesList = value;
182+
void vscode.commands.executeCommand("setContext", "swift.flatDependenciesList", value);
183+
}
184+
185+
get packageHasPlugins(): boolean {
186+
return this._packageHasPlugins;
187+
}
188+
set packageHasPlugins(value: boolean) {
189+
this._packageHasPlugins = value;
190+
void vscode.commands.executeCommand("setContext", "swift.packageHasPlugins", value);
191+
}
192+
193+
get currentTargetType(): string | undefined {
194+
return this._currentTargetType;
195+
}
196+
set currentTargetType(value: string | undefined) {
197+
this._currentTargetType = value;
198+
void vscode.commands.executeCommand(
199+
"setContext",
200+
"swift.currentTargetType",
201+
value ?? "none"
202+
);
203+
}
204+
205+
get fileIsSnippet(): boolean {
206+
return this._fileIsSnippet;
207+
}
208+
set fileIsSnippet(value: boolean) {
209+
this._fileIsSnippet = value;
210+
void vscode.commands.executeCommand("setContext", "swift.fileIsSnippet", value);
211+
}
212+
213+
get lldbVSCodeAvailable(): boolean {
214+
return this._lldbVSCodeAvailable;
215+
}
216+
set lldbVSCodeAvailable(value: boolean) {
217+
this._lldbVSCodeAvailable = value;
218+
void vscode.commands.executeCommand("setContext", "swift.lldbVSCodeAvailable", value);
219+
}
220+
221+
get createNewProjectAvailable(): boolean {
222+
return this._createNewProjectAvailable;
223+
}
224+
set createNewProjectAvailable(value: boolean) {
225+
this._createNewProjectAvailable = value;
226+
void vscode.commands.executeCommand("setContext", "swift.createNewProjectAvailable", value);
227+
}
228+
229+
get supportsReindexing(): boolean {
230+
return this._supportsReindexing;
231+
}
232+
set supportsReindexing(value: boolean) {
233+
this._supportsReindexing = value;
234+
void vscode.commands.executeCommand("setContext", "swift.supportsReindexing", value);
235+
}
236+
237+
get supportsDocumentationLivePreview(): boolean {
238+
return this._supportsDocumentationLivePreview;
239+
}
240+
set supportsDocumentationLivePreview(value: boolean) {
241+
this._supportsDocumentationLivePreview = value;
242+
void vscode.commands.executeCommand(
243+
"setContext",
244+
"swift.supportsDocumentationLivePreview",
245+
value
246+
);
247+
}
248+
249+
get supportsSwiftlyInstall(): boolean {
250+
return this._supportsSwiftlyInstall;
251+
}
252+
set supportsSwiftlyInstall(value: boolean) {
253+
this._supportsSwiftlyInstall = value;
254+
void vscode.commands.executeCommand("setContext", "swift.supportsSwiftlyInstall", value);
255+
}
256+
257+
get switchPlatformAvailable(): boolean {
258+
return this._switchPlatformAvailable;
259+
}
260+
set switchPlatformAvailable(value: boolean) {
261+
this._switchPlatformAvailable = value;
262+
void vscode.commands.executeCommand("setContext", "swift.switchPlatformAvailable", value);
263+
}
264+
265+
/**
266+
* Update context keys based on package contents.
267+
* Call this when folder focus changes.
268+
*/
269+
updateForFolder(folderContext: FolderContext | null): void {
270+
if (!folderContext) {
271+
this.hasPackage = false;
272+
this.hasExecutableProduct = false;
273+
this.packageHasDependencies = false;
274+
return;
275+
}
276+
277+
void Promise.all([
278+
folderContext.swiftPackage.foundPackage,
279+
folderContext.swiftPackage.executableProducts,
280+
folderContext.swiftPackage.dependencies,
281+
]).then(([foundPackage, executableProducts, dependencies]) => {
282+
this.hasPackage = foundPackage;
283+
this.hasExecutableProduct = executableProducts.length > 0;
284+
this.packageHasDependencies = dependencies.length > 0;
285+
});
286+
}
287+
288+
/**
289+
* Update context keys based on current file.
290+
* Call this when the active file changes.
291+
*/
292+
async updateForFile(
293+
currentDocument: vscode.Uri | null,
294+
currentFolder: FolderContext | null,
295+
languageClientManager: { get(folder: FolderContext): LanguageClientManager }
296+
): Promise<void> {
297+
if (currentDocument && currentFolder) {
298+
const target = await currentFolder.swiftPackage.getTarget(currentDocument.fsPath);
299+
this.currentTargetType = target?.type;
300+
} else {
301+
this.currentTargetType = undefined;
302+
}
303+
304+
if (currentFolder) {
305+
const languageClient = languageClientManager.get(currentFolder);
306+
await languageClient.useLanguageClient(async client => {
307+
const experimentalCaps = client.initializeResult?.capabilities.experimental;
308+
if (!experimentalCaps) {
309+
this.supportsReindexing = false;
310+
this.supportsDocumentationLivePreview = false;
311+
return;
312+
}
313+
this.supportsReindexing =
314+
experimentalCaps[ReIndexProjectRequest.method] !== undefined;
315+
this.supportsDocumentationLivePreview =
316+
experimentalCaps[DocCDocumentationRequest.method] !== undefined;
317+
});
318+
}
319+
320+
this.updateSnippetContextKey(currentDocument, currentFolder);
321+
}
322+
323+
/**
324+
* Update hasPlugins context key by checking all folders.
325+
* Call this when packages are added/removed or plugins change.
326+
*/
327+
updateForPlugins(folders: FolderContext[]): void {
328+
let hasPlugins = false;
329+
for (const folder of folders) {
330+
if (folder.swiftPackage.plugins.length > 0) {
331+
hasPlugins = true;
332+
break;
333+
}
334+
}
335+
this.packageHasPlugins = hasPlugins;
336+
}
337+
338+
/**
339+
* Update fileIsSnippet context key based on current file location.
340+
* Private helper called from updateForFile.
341+
*/
342+
private updateSnippetContextKey(
343+
currentDocument: vscode.Uri | null,
344+
currentFolder: FolderContext | null
345+
): void {
346+
if (
347+
!currentFolder ||
348+
!currentDocument ||
349+
currentFolder.swiftVersion.isLessThan({ major: 5, minor: 7, patch: 0 })
350+
) {
351+
this.fileIsSnippet = false;
352+
return;
353+
}
354+
355+
const filename = currentDocument.fsPath;
356+
const snippetsFolder = path.join(currentFolder.folder.fsPath, "Snippets");
357+
this.fileIsSnippet = filename.startsWith(snippetsFolder);
358+
}
359+
360+
/**
361+
* Sets values for context keys that are enabled/disabled based on the toolchain version in use.
362+
*/
363+
updateKeysBasedOnActiveVersion(toolchainVersion: Version): void {
364+
this.createNewProjectAvailable = toolchainVersion.isGreaterThanOrEqual(
365+
new Version(5, 8, 0)
366+
);
367+
this.switchPlatformAvailable =
368+
process.platform === "darwin"
369+
? toolchainVersion.isGreaterThanOrEqual(new Version(6, 1, 0))
370+
: false;
371+
}
372+
}

0 commit comments

Comments
 (0)