xref: /aosp_15_r20/external/pigweed/pw_ide/ts/pigweed-vscode/src/configParsing.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1// Copyright 2023 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';
16import { Uri } from 'vscode';
17
18import * as hjson from 'hjson';
19
20import logger from './logging';
21
22/**
23 * Schema for settings.json
24 */
25export type SettingsJson = Record<string, any>;
26
27/**
28 * Schema for extensions.json
29 */
30export interface ExtensionsJson {
31  recommendations?: string[];
32  unwantedRecommendations?: string[];
33}
34
35/**
36 * Partial schema for the workspace config file
37 */
38interface WorkspaceConfig {
39  extensions?: ExtensionsJson;
40  settings?: SettingsJson;
41}
42
43// When the project is opened directly (i.e., by opening the repo directory),
44// we have direct access to extensions.json. But if the project is part of a
45// workspace (https://code.visualstudio.com/docs/editor/workspaces), we'll get
46// a combined config that includes the equivalent of extensions.json associated
47// with the "extensions" key. This is taken into consideration only for the sake
48// of completeness; Pigweed doesn't currently support the use of workspaces.
49type LoadableExtensionsConfig = ExtensionsJson & WorkspaceConfig;
50
51/**
52 * Load a config file that contains extensions.json data. This could be
53 * extensions.json itself, or a workspace file that contains the equivalent.
54 * @param uri - A file path to load
55 * @returns - The extensions.json file data
56 */
57export async function loadExtensionsJson(
58  uri: vscode.Uri,
59): Promise<ExtensionsJson> {
60  const buffer = await vscode.workspace.fs.readFile(uri);
61  const config: LoadableExtensionsConfig = hjson.parse(buffer.toString());
62
63  if (config.extensions) {
64    return config.extensions;
65  }
66
67  return config as ExtensionsJson;
68}
69
70/**
71 * Find and return the extensions.json data for the project.
72 * @param includeWorkspace - Also search workspace files
73 * @returns The extensions.json file data
74 */
75export async function getExtensionsJson(
76  includeWorkspace = false,
77): Promise<ExtensionsJson | null> {
78  const files = await vscode.workspace.findFiles(
79    '.vscode/extensions.json',
80    '**/node_modules/**',
81  );
82
83  if (includeWorkspace) {
84    const workspaceFile = vscode.workspace.workspaceFile;
85
86    if (workspaceFile) {
87      files.push(workspaceFile);
88    }
89  }
90
91  if (files.length == 0) {
92    return null;
93  } else {
94    if (files.length > 1) {
95      vscode.window.showWarningMessage(
96        'Found multiple extensions.json! Will only use the first.',
97      );
98    }
99
100    return await loadExtensionsJson(files[0]);
101  }
102}
103
104export async function loadSettingsJson(
105  uri: vscode.Uri,
106): Promise<SettingsJson | undefined> {
107  const buffer = await vscode.workspace.fs.readFile(uri);
108  return hjson.parse(buffer.toString()) as SettingsJson;
109}
110
111interface SettingsData {
112  shared?: SettingsJson;
113  project?: SettingsJson;
114  workspace?: SettingsJson;
115}
116
117/** Get VSC settings from all potential sources. */
118export async function getSettingsData(): Promise<SettingsData> {
119  let shared: SettingsJson | undefined;
120  let project: SettingsJson | undefined;
121  let workspace: SettingsJson | undefined;
122
123  const workspaceFolders = vscode.workspace.workspaceFolders;
124  if (!workspaceFolders) return {};
125  const workspaceFolder = workspaceFolders[0];
126
127  const sharedSettingsFilePath = Uri.joinPath(
128    workspaceFolder.uri,
129    '.vscode/settings.shared.json',
130  );
131
132  try {
133    const buffer = await vscode.workspace.fs.readFile(sharedSettingsFilePath);
134    shared = hjson.parse(buffer.toString());
135  } catch (err: unknown) {
136    // do nothing, shared remains undefined if the file doesn't exist
137  }
138
139  const projectSettingsFilePath = Uri.joinPath(
140    workspaceFolder.uri,
141    '.vscode/settings.json',
142  );
143
144  try {
145    const buffer = await vscode.workspace.fs.readFile(projectSettingsFilePath);
146    project = hjson.parse(buffer.toString());
147  } catch (err: unknown) {
148    // do nothing, project remains undefined if the file doesn't exist
149  }
150
151  const workspaceFile = vscode.workspace.workspaceFile;
152
153  if (workspaceFile) {
154    const buffer = await vscode.workspace.fs.readFile(workspaceFile);
155    const workspaceData = hjson.parse(buffer.toString()) as WorkspaceConfig;
156    workspace = workspaceData.settings;
157  }
158
159  return { shared, project, workspace };
160}
161
162export async function syncSettingsSharedToProject(
163  settingsData: SettingsData,
164  overwrite = false,
165): Promise<void> {
166  const { shared, project } = settingsData;
167
168  // If there are no shared settings, there's nothing to sync.
169  if (!shared) return;
170
171  logger.info('Syncing shared settings');
172  let diff: SettingsJson = {};
173
174  if (!project) {
175    // If there are no project settings, just sync all of the shared settings.
176    diff = shared;
177  } else {
178    // Otherwise, sync the differences.
179    for (const key of Object.keys(shared)) {
180      // If this key isn't in the project settings, copy it over
181      if (project[key] === undefined) {
182        diff[key] = shared[key];
183      }
184
185      // If the setting exists in both places but conflicts, the action we take
186      // depends on whether we're *overwriting* (letting the shared setting
187      // value take precedence) or not (let the project setting value remain).
188      // Letting the project setting remain means doing nothing.
189      if (project[key] !== shared[key] && overwrite) {
190        diff[key] = shared[key];
191      }
192    }
193  }
194
195  // Apply the different settings.
196  for (const [key, value] of Object.entries(diff)) {
197    const [category, section] = key.split(/\.(.*)/s, 2);
198
199    try {
200      await vscode.workspace.getConfiguration(category).update(section, value);
201      logger.info(`==> ${key}: ${value}`);
202    } catch (err: unknown) {
203      // An error will be thrown if the setting isn't registered (e.g., if
204      // it's not a real setting or the extension it pertains to isn't
205      // installed). That's fine, just ignore it.
206    }
207  }
208
209  logger.info('Finished syncing shared settings');
210}
211