• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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