xref: /aosp_15_r20/external/pigweed/pw_ide/ts/pigweed-vscode/src/settings.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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