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