Skip to content

Commit f4b13ce

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 f4b13ce

File tree

6 files changed

+910
-394
lines changed

6 files changed

+910
-394
lines changed

src/ContextKeyManager.ts

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
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+
/** Update context keys based on package contents. Call this when folder focus changes. */
106+
updateForFolder(folderContext: FolderContext | null): void;
107+
/** Update context keys based on current file. Call this when the active file changes. */
108+
updateForFile(
109+
currentDocument: vscode.Uri | null,
110+
currentFolder: FolderContext | null,
111+
languageClientManager: { get(folder: FolderContext): LanguageClientManager }
112+
): Promise<void>;
113+
/** Update hasPlugins context key by checking all folders. Call this when packages are added/removed or plugins change. */
114+
updateForPlugins(folders: FolderContext[]): void;
115+
}
116+
117+
/**
118+
* Manages VS Code context keys for the Swift extension.
119+
* Encapsulates context key state and provides both low-level property access
120+
* and high-level update methods for common patterns.
121+
*/
122+
export class ContextKeyManager implements ContextKeys {
123+
private _isActivated = false;
124+
private _hasPackage = false;
125+
private _hasExecutableProduct = false;
126+
private _flatDependenciesList = false;
127+
private _packageHasDependencies = false;
128+
private _packageHasPlugins = false;
129+
private _currentTargetType: string | undefined = undefined;
130+
private _fileIsSnippet = false;
131+
private _lldbVSCodeAvailable = false;
132+
private _createNewProjectAvailable = false;
133+
private _supportsReindexing = false;
134+
private _supportsDocumentationLivePreview = false;
135+
private _supportsSwiftlyInstall = false;
136+
private _switchPlatformAvailable = false;
137+
138+
get isActivated(): boolean {
139+
return this._isActivated;
140+
}
141+
set isActivated(value: boolean) {
142+
this._isActivated = value;
143+
void vscode.commands.executeCommand("setContext", "swift.isActivated", value);
144+
}
145+
146+
get hasPackage(): boolean {
147+
return this._hasPackage;
148+
}
149+
set hasPackage(value: boolean) {
150+
this._hasPackage = value;
151+
void vscode.commands.executeCommand("setContext", "swift.hasPackage", value);
152+
}
153+
154+
get hasExecutableProduct(): boolean {
155+
return this._hasExecutableProduct;
156+
}
157+
set hasExecutableProduct(value: boolean) {
158+
this._hasExecutableProduct = value;
159+
void vscode.commands.executeCommand("setContext", "swift.hasExecutableProduct", value);
160+
}
161+
162+
get packageHasDependencies(): boolean {
163+
return this._packageHasDependencies;
164+
}
165+
set packageHasDependencies(value: boolean) {
166+
this._packageHasDependencies = value;
167+
void vscode.commands.executeCommand("setContext", "swift.packageHasDependencies", value);
168+
}
169+
170+
get flatDependenciesList(): boolean {
171+
return this._flatDependenciesList;
172+
}
173+
set flatDependenciesList(value: boolean) {
174+
this._flatDependenciesList = value;
175+
void vscode.commands.executeCommand("setContext", "swift.flatDependenciesList", value);
176+
}
177+
178+
get packageHasPlugins(): boolean {
179+
return this._packageHasPlugins;
180+
}
181+
set packageHasPlugins(value: boolean) {
182+
this._packageHasPlugins = value;
183+
void vscode.commands.executeCommand("setContext", "swift.packageHasPlugins", value);
184+
}
185+
186+
get currentTargetType(): string | undefined {
187+
return this._currentTargetType;
188+
}
189+
set currentTargetType(value: string | undefined) {
190+
this._currentTargetType = value;
191+
void vscode.commands.executeCommand(
192+
"setContext",
193+
"swift.currentTargetType",
194+
value ?? "none"
195+
);
196+
}
197+
198+
get fileIsSnippet(): boolean {
199+
return this._fileIsSnippet;
200+
}
201+
set fileIsSnippet(value: boolean) {
202+
this._fileIsSnippet = value;
203+
void vscode.commands.executeCommand("setContext", "swift.fileIsSnippet", value);
204+
}
205+
206+
get lldbVSCodeAvailable(): boolean {
207+
return this._lldbVSCodeAvailable;
208+
}
209+
set lldbVSCodeAvailable(value: boolean) {
210+
this._lldbVSCodeAvailable = value;
211+
void vscode.commands.executeCommand("setContext", "swift.lldbVSCodeAvailable", value);
212+
}
213+
214+
get createNewProjectAvailable(): boolean {
215+
return this._createNewProjectAvailable;
216+
}
217+
set createNewProjectAvailable(value: boolean) {
218+
this._createNewProjectAvailable = value;
219+
void vscode.commands.executeCommand("setContext", "swift.createNewProjectAvailable", value);
220+
}
221+
222+
get supportsReindexing(): boolean {
223+
return this._supportsReindexing;
224+
}
225+
set supportsReindexing(value: boolean) {
226+
this._supportsReindexing = value;
227+
void vscode.commands.executeCommand("setContext", "swift.supportsReindexing", value);
228+
}
229+
230+
get supportsDocumentationLivePreview(): boolean {
231+
return this._supportsDocumentationLivePreview;
232+
}
233+
set supportsDocumentationLivePreview(value: boolean) {
234+
this._supportsDocumentationLivePreview = value;
235+
void vscode.commands.executeCommand(
236+
"setContext",
237+
"swift.supportsDocumentationLivePreview",
238+
value
239+
);
240+
}
241+
242+
get supportsSwiftlyInstall(): boolean {
243+
return this._supportsSwiftlyInstall;
244+
}
245+
set supportsSwiftlyInstall(value: boolean) {
246+
this._supportsSwiftlyInstall = value;
247+
void vscode.commands.executeCommand("setContext", "swift.supportsSwiftlyInstall", value);
248+
}
249+
250+
get switchPlatformAvailable(): boolean {
251+
return this._switchPlatformAvailable;
252+
}
253+
set switchPlatformAvailable(value: boolean) {
254+
this._switchPlatformAvailable = value;
255+
void vscode.commands.executeCommand("setContext", "swift.switchPlatformAvailable", value);
256+
}
257+
258+
/**
259+
* Update context keys based on package contents.
260+
* Call this when folder focus changes.
261+
*/
262+
updateForFolder(folderContext: FolderContext | null): void {
263+
if (!folderContext) {
264+
this.hasPackage = false;
265+
this.hasExecutableProduct = false;
266+
this.packageHasDependencies = false;
267+
return;
268+
}
269+
270+
void Promise.all([
271+
folderContext.swiftPackage.foundPackage,
272+
folderContext.swiftPackage.executableProducts,
273+
folderContext.swiftPackage.dependencies,
274+
]).then(([foundPackage, executableProducts, dependencies]) => {
275+
this.hasPackage = foundPackage;
276+
this.hasExecutableProduct = executableProducts.length > 0;
277+
this.packageHasDependencies = dependencies.length > 0;
278+
});
279+
}
280+
281+
/**
282+
* Update context keys based on current file.
283+
* Call this when the active file changes.
284+
*/
285+
async updateForFile(
286+
currentDocument: vscode.Uri | null,
287+
currentFolder: FolderContext | null,
288+
languageClientManager: { get(folder: FolderContext): LanguageClientManager }
289+
): Promise<void> {
290+
if (currentDocument && currentFolder) {
291+
const target = await currentFolder.swiftPackage.getTarget(currentDocument.fsPath);
292+
this.currentTargetType = target?.type;
293+
} else {
294+
this.currentTargetType = undefined;
295+
}
296+
297+
if (currentFolder) {
298+
const languageClient = languageClientManager.get(currentFolder);
299+
await languageClient.useLanguageClient(async client => {
300+
const experimentalCaps = client.initializeResult?.capabilities.experimental;
301+
if (!experimentalCaps) {
302+
this.supportsReindexing = false;
303+
this.supportsDocumentationLivePreview = false;
304+
return;
305+
}
306+
this.supportsReindexing =
307+
experimentalCaps[ReIndexProjectRequest.method] !== undefined;
308+
this.supportsDocumentationLivePreview =
309+
experimentalCaps[DocCDocumentationRequest.method] !== undefined;
310+
});
311+
}
312+
313+
this.updateSnippetContextKey(currentDocument, currentFolder);
314+
}
315+
316+
/**
317+
* Update hasPlugins context key by checking all folders.
318+
* Call this when packages are added/removed or plugins change.
319+
*/
320+
updateForPlugins(folders: FolderContext[]): void {
321+
let hasPlugins = false;
322+
for (const folder of folders) {
323+
if (folder.swiftPackage.plugins.length > 0) {
324+
hasPlugins = true;
325+
break;
326+
}
327+
}
328+
this.packageHasPlugins = hasPlugins;
329+
}
330+
331+
/**
332+
* Update fileIsSnippet context key based on current file location.
333+
* Private helper called from updateForFile.
334+
*/
335+
private updateSnippetContextKey(
336+
currentDocument: vscode.Uri | null,
337+
currentFolder: FolderContext | null
338+
): void {
339+
if (
340+
!currentFolder ||
341+
!currentDocument ||
342+
currentFolder.swiftVersion.isLessThan({ major: 5, minor: 7, patch: 0 })
343+
) {
344+
this.fileIsSnippet = false;
345+
return;
346+
}
347+
348+
const filename = currentDocument.fsPath;
349+
const snippetsFolder = path.join(currentFolder.folder.fsPath, "Snippets");
350+
this.fileIsSnippet = filename.startsWith(snippetsFolder);
351+
}
352+
353+
/**
354+
* Sets values for context keys that are enabled/disabled based on the toolchain version in use.
355+
*/
356+
updateKeysBasedOnActiveVersion(toolchainVersion: Version): void {
357+
this.createNewProjectAvailable = toolchainVersion.isGreaterThanOrEqual(
358+
new Version(5, 8, 0)
359+
);
360+
this.switchPlatformAvailable =
361+
process.platform === "darwin"
362+
? toolchainVersion.isGreaterThanOrEqual(new Version(6, 1, 0))
363+
: false;
364+
}
365+
}

0 commit comments

Comments
 (0)