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 | undefined; 21 (value: T | undefined): Thenable<void>; 22} 23 24type ProjectType = 'bootstrap' | 'bazel'; 25type TerminalShell = 'bash' | 'zsh'; 26 27export interface Settings { 28 activateBazeliskInNewTerminals: Setting<boolean>; 29 codeAnalysisTarget: Setting<string>; 30 disableBazelSettingsRecommendations: Setting<boolean>; 31 disableBazeliskCheck: Setting<boolean>; 32 disableCompileCommandsFileWatcher: Setting<boolean>; 33 disableInactiveFileNotice: Setting<boolean>; 34 disableInactiveFileCodeIntelligence: Setting<boolean>; 35 enforceExtensionRecommendations: Setting<boolean>; 36 hideInactiveFileIndicators: Setting<boolean>; 37 preserveBazelPath: Setting<boolean>; 38 projectRoot: Setting<string>; 39 projectType: Setting<ProjectType>; 40 refreshCompileCommandsTarget: Setting<string>; 41 terminalShell: Setting<TerminalShell>; 42} 43 44export type ConfigAccessor<T> = { 45 get(): T | undefined; 46 update(value: T | undefined): Thenable<void>; 47}; 48 49/** Wrap the verbose ceremony of accessing/updating a particular setting. */ 50export function settingFor<T>(section: string, category = 'pigweed') { 51 return { 52 get: () => 53 vscode.workspace.getConfiguration(category).get(section) as T | undefined, 54 update: (value: T | undefined) => 55 vscode.workspace.getConfiguration(category).update(section, value), 56 }; 57} 58 59/** 60 * Wrap the verbose ceremony of accessing/updating a particular setting. 61 * 62 * This variation handles some edge cases of string settings, and allows you 63 * to constrain the type of the string, e.g., to a union of literals. 64 */ 65export function stringSettingFor<T extends string = string>( 66 section: string, 67 category = 'pigweed', 68) { 69 return { 70 get: (): T | undefined => { 71 const current = vscode.workspace 72 .getConfiguration(category) 73 .get(section) as T | undefined; 74 75 // Undefined settings can manifest as empty strings. 76 if (current === undefined || current.length === 0) { 77 return undefined; 78 } 79 80 return current; 81 }, 82 update: (value: T | undefined): Thenable<void> => 83 vscode.workspace.getConfiguration(category).update(section, value), 84 }; 85} 86 87/** 88 * Wrap the verbose ceremony of accessing/updating a particular setting. 89 * 90 * This variation handles some edge cases of boolean settings. 91 */ 92export function boolSettingFor(section: string, category = 'pigweed') { 93 return { 94 get: (): boolean | undefined => { 95 const current = vscode.workspace 96 .getConfiguration(category) 97 .get(section) as boolean | string | undefined; 98 99 // This seems obvious, but thanks to the edge cases handled below, we 100 // need to compare actual values, not just truthiness. 101 if (current === true) return true; 102 if (current === false) return false; 103 104 // Undefined settings can manifest as empty strings. 105 if (current === undefined || current.length === 0) { 106 return undefined; 107 } 108 109 // In some cases, booleans are returned as strings. 110 if (current === 'true') return true; 111 if (current === 'false') return false; 112 }, 113 114 update: (value: boolean | undefined): Thenable<void> => 115 vscode.workspace.getConfiguration(category).update(section, value), 116 }; 117} 118 119function activateBazeliskInNewTerminals(): boolean; 120function activateBazeliskInNewTerminals( 121 value: boolean | undefined, 122): Thenable<void>; 123function activateBazeliskInNewTerminals( 124 value?: boolean, 125): boolean | undefined | Thenable<void> { 126 const { get, update } = boolSettingFor('activateBazeliskInNewTerminals'); 127 if (value === undefined) return get() ?? false; 128 return update(value); 129} 130 131function codeAnalysisTarget(): string | undefined; 132function codeAnalysisTarget(value: string | undefined): Thenable<void>; 133function codeAnalysisTarget( 134 value?: string, 135): string | undefined | Thenable<void> { 136 const { get, update } = stringSettingFor('codeAnalysisTarget'); 137 if (value === undefined) return get(); 138 return update(value); 139} 140 141function disableBazelSettingsRecommendations(): boolean; 142function disableBazelSettingsRecommendations( 143 value: boolean | undefined, 144): Thenable<void>; 145function disableBazelSettingsRecommendations( 146 value?: boolean, 147): boolean | undefined | Thenable<void> { 148 const { get, update } = boolSettingFor('disableBazelSettingsRecommendations'); 149 if (value === undefined) return get() ?? false; 150 return update(value); 151} 152 153function disableBazeliskCheck(): boolean; 154function disableBazeliskCheck(value: boolean | undefined): Thenable<void>; 155function disableBazeliskCheck( 156 value?: boolean, 157): boolean | undefined | Thenable<void> { 158 const { get, update } = boolSettingFor('disableBazeliskCheck'); 159 if (value === undefined) return get() ?? false; 160 return update(value); 161} 162 163function disableInactiveFileNotice(): boolean; 164function disableInactiveFileNotice(value: boolean | undefined): Thenable<void>; 165function disableInactiveFileNotice( 166 value?: boolean, 167): boolean | undefined | Thenable<void> { 168 const { get, update } = boolSettingFor('disableInactiveFileNotice'); 169 if (value === undefined) return get() ?? false; 170 return update(value); 171} 172 173function disableInactiveFileCodeIntelligence(): boolean; 174function disableInactiveFileCodeIntelligence( 175 value: boolean | undefined, 176): Thenable<void>; 177function disableInactiveFileCodeIntelligence( 178 value?: boolean, 179): boolean | undefined | Thenable<void> { 180 const { get, update } = boolSettingFor('disableInactiveFileCodeIntelligence'); 181 if (value === undefined) return get() ?? true; 182 return update(value); 183} 184 185function disableCompileCommandsFileWatcher(): boolean; 186function disableCompileCommandsFileWatcher( 187 value: boolean | undefined, 188): Thenable<void>; 189function disableCompileCommandsFileWatcher( 190 value?: boolean, 191): boolean | undefined | Thenable<void> { 192 const { get, update } = boolSettingFor('disableCompileCommandsFileWatcher'); 193 if (value === undefined) return get() ?? false; 194 return update(value); 195} 196 197function enforceExtensionRecommendations(): boolean; 198function enforceExtensionRecommendations( 199 value: boolean | undefined, 200): Thenable<void>; 201function enforceExtensionRecommendations( 202 value?: boolean, 203): boolean | undefined | Thenable<void> { 204 const { get, update } = boolSettingFor('enforceExtensionRecommendations'); 205 if (value === undefined) return get() ?? false; 206 return update(value); 207} 208 209function hideInactiveFileIndicators(): boolean; 210function hideInactiveFileIndicators(value: boolean | undefined): Thenable<void>; 211function hideInactiveFileIndicators( 212 value?: boolean, 213): boolean | undefined | Thenable<void> { 214 const { get, update } = boolSettingFor('hideInactiveFileIndicators'); 215 if (value === undefined) return get() ?? false; 216 update(value); 217} 218 219function preserveBazelPath(): boolean; 220function preserveBazelPath(value: boolean | undefined): Thenable<void>; 221function preserveBazelPath( 222 value?: boolean, 223): boolean | undefined | Thenable<void> { 224 const { get, update } = boolSettingFor('preserveBazelPath'); 225 if (value === undefined) return get() ?? false; 226 update(value); 227} 228 229function projectRoot(): string | undefined; 230function projectRoot(value: string | undefined): Thenable<void>; 231function projectRoot(value?: string): string | undefined | Thenable<void> { 232 const { get, update } = stringSettingFor('projectRoot'); 233 if (value === undefined) return get(); 234 return update(value); 235} 236 237function projectType(): ProjectType | undefined; 238function projectType(value: ProjectType | undefined): Thenable<void>; 239function projectType( 240 value?: ProjectType | undefined, 241): ProjectType | undefined | Thenable<void> { 242 const { get, update } = stringSettingFor<ProjectType>('projectType'); 243 if (value === undefined) return get(); 244 return update(value); 245} 246 247function refreshCompileCommandsTarget(): string; 248function refreshCompileCommandsTarget( 249 value: string | undefined, 250): Thenable<void>; 251function refreshCompileCommandsTarget( 252 value?: string, 253): string | undefined | Thenable<void> { 254 const { get, update } = stringSettingFor('refreshCompileCommandsTarget'); 255 if (value === undefined) return get() ?? '//:refresh_compile_commands'; 256 return update(value); 257} 258 259function terminalShell(): TerminalShell; 260function terminalShell(value: TerminalShell | undefined): Thenable<void>; 261function terminalShell( 262 value?: TerminalShell | undefined, 263): TerminalShell | undefined | Thenable<void> { 264 const { get, update } = stringSettingFor<TerminalShell>('terminalShell'); 265 if (value === undefined) return get() ?? 'bash'; 266 return update(value); 267} 268 269/** Entry point for accessing settings. */ 270export const settings: Settings = { 271 activateBazeliskInNewTerminals, 272 codeAnalysisTarget, 273 disableBazelSettingsRecommendations, 274 disableBazeliskCheck, 275 disableCompileCommandsFileWatcher, 276 disableInactiveFileNotice, 277 disableInactiveFileCodeIntelligence, 278 enforceExtensionRecommendations, 279 hideInactiveFileIndicators, 280 preserveBazelPath, 281 projectRoot, 282 projectType, 283 refreshCompileCommandsTarget, 284 terminalShell, 285}; 286 287// Config accessors for Bazel extension settings. 288export const bazel_codelens = boolSettingFor('enableCodeLens', 'bazel'); 289export const bazel_executable = stringSettingFor('executable', 'bazel'); 290export const buildifier_executable = stringSettingFor( 291 'buildifierExecutable', 292 'bazel', 293); 294 295/** Find the root directory of the project open in the editor. */ 296function editorRootDir(): vscode.WorkspaceFolder { 297 const dirs = vscode.workspace.workspaceFolders; 298 299 if (!dirs || dirs.length === 0) { 300 logger.error( 301 "Couldn't get editor root dir. There's no directory open in the editor!", 302 ); 303 304 throw new Error("There's no directory open in the editor!"); 305 } 306 307 if (dirs.length > 1) { 308 logger.error( 309 "Couldn't get editor root dir. " + 310 "This is a multiroot workspace, which isn't currently supported.", 311 ); 312 313 throw new Error( 314 "This is a multiroot workspace, which isn't currently supported.", 315 ); 316 } 317 318 return dirs[0]; 319} 320 321/** This should be used in place of, e.g., process.cwd(). */ 322const defaultWorkingDir = () => editorRootDir().uri.fsPath; 323 324export interface WorkingDirStore { 325 get(): string; 326 set(path: string): void; 327} 328 329let workingDirStore: WorkingDirStoreImpl; 330 331/** 332 * A singleton for storing the project working directory. 333 * 334 * The location of this path could vary depending on project structure, and it 335 * could be stored in settings, or it may need to be inferred by traversing the 336 * project structure. The latter could be slow and shouldn't be repeated every 337 * time we need something as basic as the project root. 338 * 339 * So compute the working dir path once, store it here, then fetch it whenever 340 * you want without worrying about performance. The only downside is that you 341 * need to make sure you set a value early in your execution path. 342 * 343 * This also serves as a platform-independent interface for the working dir 344 * (for example, in Jest tests we don't have access to `vscode` so most of our 345 * directory traversal strategies are unavailable). 346 */ 347class WorkingDirStoreImpl implements WorkingDirStore { 348 constructor(path?: string) { 349 if (workingDirStore) { 350 throw new Error("This is a singleton. You can't create it!"); 351 } 352 353 if (path) { 354 this._path = path; 355 } 356 357 // eslint-disable-next-line @typescript-eslint/no-this-alias 358 workingDirStore = this; 359 } 360 361 _path: string | undefined = undefined; 362 363 set(path: string) { 364 this._path = path; 365 } 366 367 get(): string { 368 if (!this._path) { 369 throw new Error( 370 'Yikes! You tried to get this value without setting it first.', 371 ); 372 } 373 374 return this._path; 375 } 376} 377 378export const workingDir = new WorkingDirStoreImpl(defaultWorkingDir()); 379