1// Copyright 2024 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15import * as vscode from 'vscode'; 16 17import logger from '../logging'; 18 19interface Setting<T> { 20 (): T; 21 (value: T): Thenable<void>; 22} 23 24interface CompDbSearchPath { 25 pathGlob: string; 26 targetInferencePattern: string; 27} 28 29type TerminalShell = 'bash' | 'zsh'; 30 31export interface Settings { 32 activateBazeliskInNewTerminals: Setting<boolean>; 33 codeAnalysisTarget: Setting<string | undefined>; 34 codeAnalysisTargetDir: Setting<string | undefined>; 35 compDbSearchPaths: Setting<CompDbSearchPath[]>; 36 disableBazelSettingsRecommendations: Setting<boolean>; 37 disableBazeliskCheck: Setting<boolean>; 38 disableCompileCommandsFileWatcher: Setting<boolean>; 39 disableInactiveFileNotice: Setting<boolean>; 40 disableInactiveFileCodeIntelligence: Setting<boolean>; 41 enforceExtensionRecommendations: Setting<boolean>; 42 hideInactiveFileIndicators: Setting<boolean>; 43 preserveBazelPath: Setting<boolean>; 44 projectRoot: Setting<string | undefined>; 45 refreshCompileCommandsTarget: Setting<string>; 46 supportBazelTargets: Setting<boolean | 'auto'>; 47 supportCmakeTargets: Setting<boolean | 'auto'>; 48 supportGnTargets: Setting<boolean | 'auto'>; 49 terminalShell: Setting<TerminalShell>; 50} 51 52export type ConfigAccessor<T> = { 53 get(): T | undefined; 54 update(value: T | undefined): Thenable<void>; 55}; 56 57/** Wrap the verbose ceremony of accessing/updating a particular setting. */ 58export function settingFor<T>(section: string, category = 'pigweed') { 59 return { 60 get: () => 61 vscode.workspace.getConfiguration(category).get(section) as T | undefined, 62 update: (value: T | undefined) => 63 vscode.workspace.getConfiguration(category).update(section, value), 64 }; 65} 66 67/** 68 * Wrap the verbose ceremony of accessing/updating a particular setting. 69 * 70 * This variation handles some edge cases of string settings, and allows you 71 * to constrain the type of the string, e.g., to a union of literals. 72 */ 73export function stringSettingFor<T extends string = string>( 74 section: string, 75 category = 'pigweed', 76) { 77 return { 78 get: (): T | undefined => { 79 const current = vscode.workspace 80 .getConfiguration(category) 81 .get(section) as T | undefined; 82 83 // Undefined settings can manifest as empty strings. 84 if (current === undefined || current.length === 0) { 85 return undefined; 86 } 87 88 return current; 89 }, 90 update: (value: T | undefined): Thenable<void> => 91 vscode.workspace.getConfiguration(category).update(section, value), 92 }; 93} 94 95/** 96 * Wrap the verbose ceremony of accessing/updating a particular setting. 97 * 98 * This variation handles some edge cases of boolean settings. 99 */ 100export function boolSettingFor(section: string, category = 'pigweed') { 101 return { 102 get: (): boolean | undefined => { 103 const current = vscode.workspace 104 .getConfiguration(category) 105 .get(section) as boolean | string | undefined; 106 107 // This seems obvious, but thanks to the edge cases handled below, we 108 // need to compare actual values, not just truthiness. 109 if (current === true) return true; 110 if (current === false) return false; 111 112 // Undefined settings can manifest as empty strings. 113 if (current === undefined || current.length === 0) { 114 return undefined; 115 } 116 117 // In some cases, booleans are returned as strings. 118 if (current === 'true') return true; 119 if (current === 'false') return false; 120 }, 121 122 update: (value: boolean | undefined): Thenable<void> => 123 vscode.workspace.getConfiguration(category).update(section, value), 124 }; 125} 126 127/** 128 * Wrap the verbose ceremony of accessing/updating a particular setting. 129 * 130 * This variation handles some edge cases of boolean settings that also accept 131 * one or more other string literal values. 132 */ 133export function boolWithExtraSettingFor<T extends string>( 134 section: string, 135 category = 'pigweed', 136) { 137 return { 138 get: (): boolean | T | undefined => { 139 const current = vscode.workspace 140 .getConfiguration(category) 141 .get(section) as boolean | T | string | undefined; 142 143 // This seems obvious, but thanks to the edge cases handled below, we 144 // need to compare actual values, not just truthiness. 145 if (current === true) return true; 146 if (current === false) return false; 147 148 // Undefined settings can manifest as empty strings. 149 if (current === undefined || current.length === 0) { 150 return undefined; 151 } 152 153 // In some cases, booleans are returned as strings. 154 if (current === 'true') return true; 155 if (current === 'false') return false; 156 157 return current as T; 158 }, 159 160 update: (value: boolean | T | undefined): Thenable<void> => 161 vscode.workspace.getConfiguration(category).update(section, value), 162 }; 163} 164 165function activateBazeliskInNewTerminals(): boolean; 166function activateBazeliskInNewTerminals( 167 value: boolean | undefined, 168): Thenable<void>; 169function activateBazeliskInNewTerminals( 170 value?: boolean, 171): boolean | undefined | Thenable<void> { 172 const { get, update } = boolSettingFor('activateBazeliskInNewTerminals'); 173 if (value === undefined) return get() ?? false; 174 return update(value); 175} 176 177function codeAnalysisTarget(): string | undefined; 178function codeAnalysisTarget(value: string | undefined): Thenable<void>; 179function codeAnalysisTarget( 180 value?: string, 181): string | undefined | Thenable<void> { 182 const { get, update } = stringSettingFor('codeAnalysisTarget'); 183 if (value === undefined) return get(); 184 return update(value); 185} 186 187function codeAnalysisTargetDir(): string | undefined; 188function codeAnalysisTargetDir(value: string | undefined): Thenable<void>; 189function codeAnalysisTargetDir( 190 value?: string, 191): string | undefined | Thenable<void> { 192 const { get, update } = stringSettingFor('codeAnalysisTargetDir'); 193 if (value === undefined) return get(); 194 return update(value); 195} 196 197function compDbSearchPaths(): CompDbSearchPath[]; 198function compDbSearchPaths(value: CompDbSearchPath[]): Thenable<void>; 199function compDbSearchPaths( 200 value?: CompDbSearchPath[], 201): CompDbSearchPath[] | Thenable<void> { 202 const get = () => 203 vscode.workspace 204 .getConfiguration('pigweed') 205 .get('compDbSearchPaths') as CompDbSearchPath[]; 206 207 const update = (value: CompDbSearchPath[]) => 208 vscode.workspace 209 .getConfiguration('pigweed') 210 .update('compDbSearchPaths', value); 211 212 if (value === undefined) return get(); 213 return update(value); 214} 215 216function disableBazelSettingsRecommendations(): boolean; 217function disableBazelSettingsRecommendations( 218 value: boolean | undefined, 219): Thenable<void>; 220function disableBazelSettingsRecommendations( 221 value?: boolean, 222): boolean | undefined | Thenable<void> { 223 const { get, update } = boolSettingFor('disableBazelSettingsRecommendations'); 224 if (value === undefined) return get() ?? false; 225 return update(value); 226} 227 228function disableBazeliskCheck(): boolean; 229function disableBazeliskCheck(value: boolean | undefined): Thenable<void>; 230function disableBazeliskCheck( 231 value?: boolean, 232): boolean | undefined | Thenable<void> { 233 const { get, update } = boolSettingFor('disableBazeliskCheck'); 234 if (value === undefined) return get() ?? false; 235 return update(value); 236} 237 238function disableInactiveFileNotice(): boolean; 239function disableInactiveFileNotice(value: boolean | undefined): Thenable<void>; 240function disableInactiveFileNotice( 241 value?: boolean, 242): boolean | undefined | Thenable<void> { 243 const { get, update } = boolSettingFor('disableInactiveFileNotice'); 244 if (value === undefined) return get() ?? false; 245 return update(value); 246} 247 248function disableInactiveFileCodeIntelligence(): boolean; 249function disableInactiveFileCodeIntelligence( 250 value: boolean | undefined, 251): Thenable<void>; 252function disableInactiveFileCodeIntelligence( 253 value?: boolean, 254): boolean | undefined | Thenable<void> { 255 const { get, update } = boolSettingFor('disableInactiveFileCodeIntelligence'); 256 if (value === undefined) return get() ?? true; 257 return update(value); 258} 259 260function disableCompileCommandsFileWatcher(): boolean; 261function disableCompileCommandsFileWatcher( 262 value: boolean | undefined, 263): Thenable<void>; 264function disableCompileCommandsFileWatcher( 265 value?: boolean, 266): boolean | undefined | Thenable<void> { 267 const { get, update } = boolSettingFor('disableCompileCommandsFileWatcher'); 268 if (value === undefined) return get() ?? false; 269 return update(value); 270} 271 272function enforceExtensionRecommendations(): boolean; 273function enforceExtensionRecommendations( 274 value: boolean | undefined, 275): Thenable<void>; 276function enforceExtensionRecommendations( 277 value?: boolean, 278): boolean | undefined | Thenable<void> { 279 const { get, update } = boolSettingFor('enforceExtensionRecommendations'); 280 if (value === undefined) return get() ?? false; 281 return update(value); 282} 283 284function hideInactiveFileIndicators(): boolean; 285function hideInactiveFileIndicators(value: boolean | undefined): Thenable<void>; 286function hideInactiveFileIndicators( 287 value?: boolean, 288): boolean | undefined | Thenable<void> { 289 const { get, update } = boolSettingFor('hideInactiveFileIndicators'); 290 if (value === undefined) return get() ?? false; 291 update(value); 292} 293 294function preserveBazelPath(): boolean; 295function preserveBazelPath(value: boolean | undefined): Thenable<void>; 296function preserveBazelPath( 297 value?: boolean, 298): boolean | undefined | Thenable<void> { 299 const { get, update } = boolSettingFor('preserveBazelPath'); 300 if (value === undefined) return get() ?? false; 301 update(value); 302} 303 304function projectRoot(): string | undefined; 305function projectRoot(value: string | undefined): Thenable<void>; 306function projectRoot(value?: string): string | undefined | Thenable<void> { 307 const { get, update } = stringSettingFor('projectRoot'); 308 if (value === undefined) return get(); 309 return update(value); 310} 311 312function refreshCompileCommandsTarget(): string; 313function refreshCompileCommandsTarget( 314 value: string | undefined, 315): Thenable<void>; 316function refreshCompileCommandsTarget( 317 value?: string, 318): string | undefined | Thenable<void> { 319 const { get, update } = stringSettingFor('refreshCompileCommandsTarget'); 320 if (value === undefined) return get() ?? '//:refresh_compile_commands'; 321 return update(value); 322} 323 324function supportBazelTargets(): boolean | 'auto'; 325function supportBazelTargets( 326 value: boolean | 'auto' | undefined, 327): Thenable<void>; 328function supportBazelTargets( 329 value?: boolean | 'auto', 330): boolean | 'auto' | undefined | Thenable<void> { 331 const { get, update } = boolWithExtraSettingFor<'auto'>( 332 'supportBazelTargets', 333 ); 334 if (value === undefined) return get() ?? 'auto'; 335 update(value); 336} 337 338function supportCmakeTargets(): boolean | 'auto'; 339function supportCmakeTargets( 340 value: boolean | 'auto' | undefined, 341): Thenable<void>; 342function supportCmakeTargets( 343 value?: boolean | 'auto', 344): boolean | 'auto' | undefined | Thenable<void> { 345 const { get, update } = boolWithExtraSettingFor<'auto'>( 346 'supportCmakeTargets', 347 ); 348 if (value === undefined) return get() ?? 'auto'; 349 update(value); 350} 351 352function supportGnTargets(): boolean | 'auto'; 353function supportGnTargets(value: boolean | 'auto' | undefined): Thenable<void>; 354function supportGnTargets( 355 value?: boolean | 'auto', 356): boolean | 'auto' | undefined | Thenable<void> { 357 const { get, update } = boolWithExtraSettingFor<'auto'>('supportGnTargets'); 358 if (value === undefined) return get() ?? 'auto'; 359 update(value); 360} 361 362function terminalShell(): TerminalShell; 363function terminalShell(value: TerminalShell | undefined): Thenable<void>; 364function terminalShell( 365 value?: TerminalShell | undefined, 366): TerminalShell | undefined | Thenable<void> { 367 const { get, update } = stringSettingFor<TerminalShell>('terminalShell'); 368 if (value === undefined) return get() ?? 'bash'; 369 return update(value); 370} 371 372/** Entry point for accessing settings. */ 373export const settings: Settings = { 374 activateBazeliskInNewTerminals, 375 codeAnalysisTarget, 376 codeAnalysisTargetDir, 377 compDbSearchPaths, 378 disableBazelSettingsRecommendations, 379 disableBazeliskCheck, 380 disableCompileCommandsFileWatcher, 381 disableInactiveFileNotice, 382 disableInactiveFileCodeIntelligence, 383 enforceExtensionRecommendations, 384 hideInactiveFileIndicators, 385 preserveBazelPath, 386 projectRoot, 387 refreshCompileCommandsTarget, 388 supportBazelTargets, 389 supportCmakeTargets, 390 supportGnTargets, 391 terminalShell, 392}; 393 394// Config accessors for Bazel extension settings. 395export const bazel_codelens = boolSettingFor('enableCodeLens', 'bazel'); 396export const bazel_executable = stringSettingFor('executable', 'bazel'); 397export const buildifier_executable = stringSettingFor( 398 'buildifierExecutable', 399 'bazel', 400); 401 402/** Find the root directory of the project open in the editor. */ 403function editorRootDir(): vscode.WorkspaceFolder { 404 const dirs = vscode.workspace.workspaceFolders; 405 406 if (!dirs || dirs.length === 0) { 407 logger.error( 408 "Couldn't get editor root dir. There's no directory open in the editor!", 409 ); 410 411 throw new Error("There's no directory open in the editor!"); 412 } 413 414 if (dirs.length > 1) { 415 logger.error( 416 "Couldn't get editor root dir. " + 417 "This is a multiroot workspace, which isn't currently supported.", 418 ); 419 420 throw new Error( 421 "This is a multiroot workspace, which isn't currently supported.", 422 ); 423 } 424 425 return dirs[0]; 426} 427 428/** This should be used in place of, e.g., process.cwd(). */ 429const defaultWorkingDir = () => editorRootDir().uri.fsPath; 430 431export interface WorkingDirStore { 432 get(): string; 433 set(path: string): void; 434} 435 436let workingDirStore: WorkingDirStoreImpl; 437 438/** 439 * A singleton for storing the project working directory. 440 * 441 * The location of this path could vary depending on project structure, and it 442 * could be stored in settings, or it may need to be inferred by traversing the 443 * project structure. The latter could be slow and shouldn't be repeated every 444 * time we need something as basic as the project root. 445 * 446 * So compute the working dir path once, store it here, then fetch it whenever 447 * you want without worrying about performance. The only downside is that you 448 * need to make sure you set a value early in your execution path. 449 * 450 * This also serves as a platform-independent interface for the working dir 451 * (for example, in Jest tests we don't have access to `vscode` so most of our 452 * directory traversal strategies are unavailable). 453 */ 454class WorkingDirStoreImpl implements WorkingDirStore { 455 constructor(path?: string) { 456 if (workingDirStore) { 457 throw new Error("This is a singleton. You can't create it!"); 458 } 459 460 if (path) { 461 this._path = path; 462 } 463 464 // eslint-disable-next-line @typescript-eslint/no-this-alias 465 workingDirStore = this; 466 } 467 468 _path: string | undefined = undefined; 469 470 set(path: string) { 471 this._path = path; 472 } 473 474 get(): string { 475 if (!this._path) { 476 throw new Error( 477 'Yikes! You tried to get this value without setting it first.', 478 ); 479 } 480 481 return this._path; 482 } 483} 484 485export const workingDir = new WorkingDirStoreImpl(defaultWorkingDir()); 486