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