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 { getExtensionsJson } from './configParsing'; 18import logger from './logging'; 19 20/** 21 * Open the extensions sidebar and show the provided extensions. 22 * @param extensions - A list of extension IDs 23 */ 24function showExtensions(extensions: string[]) { 25 vscode.commands.executeCommand( 26 'workbench.extensions.search', 27 '@id:' + extensions.join(', @id:'), 28 ); 29} 30 31/** 32 * Given a list of extensions, return the subset that are not installed or are 33 * disabled. 34 * @param extensions - A list of extension IDs 35 * @return A list of extension IDs 36 */ 37function getUnavailableExtensions(extensions: string[]): string[] { 38 const unavailableExtensions: string[] = []; 39 const available = vscode.extensions.all; 40 41 // TODO(chadnorvell): Verify that this includes disabled extensions 42 extensions.map(async (extId) => { 43 const ext = available.find((ext) => ext.id == extId); 44 45 if (!ext) { 46 unavailableExtensions.push(extId); 47 } 48 }); 49 50 return unavailableExtensions; 51} 52 53/** 54 * If there are recommended extensions that are not installed or enabled in the 55 * current workspace, prompt the user to install them. This is "sticky" in the 56 * sense that it will keep bugging the user to enable those extensions until 57 * they enable them all, or until they explicitly cancel. 58 * @param recs - A list of extension IDs 59 */ 60async function installRecommendedExtensions(recs: string[]): Promise<void> { 61 let unavailableRecs = getUnavailableExtensions(recs); 62 const totalNumUnavailableRecs = unavailableRecs.length; 63 let numUnavailableRecs = totalNumUnavailableRecs; 64 65 const update = () => { 66 unavailableRecs = getUnavailableExtensions(recs); 67 numUnavailableRecs = unavailableRecs.length; 68 }; 69 70 const wait = async () => new Promise((resolve) => setTimeout(resolve, 2500)); 71 72 const progressIncrement = (num: number) => 73 1 - (num / totalNumUnavailableRecs) * 100; 74 75 // All recommendations are installed; we're done. 76 if (totalNumUnavailableRecs == 0) { 77 logger.info('User has all recommended extensions'); 78 79 return; 80 } 81 82 showExtensions(unavailableRecs); 83 84 vscode.window.showInformationMessage( 85 `This Pigweed project needs you to install ${totalNumUnavailableRecs} ` + 86 'required extensions. ' + 87 'Install the extensions shown in the extensions tab.', 88 { modal: true }, 89 'Ok', 90 ); 91 92 vscode.window.withProgress( 93 { 94 location: vscode.ProgressLocation.Notification, 95 title: 96 'Install these extensions! This Pigweed project needs these recommended extensions to be installed.', 97 cancellable: true, 98 }, 99 async (progress, token) => { 100 while (numUnavailableRecs > 0) { 101 // TODO(chadnorvell): Wait for vscode.extensions.onDidChange 102 await wait(); 103 update(); 104 105 progress.report({ 106 increment: progressIncrement(numUnavailableRecs), 107 }); 108 109 if (numUnavailableRecs > 0) { 110 logger.info( 111 `User lacks ${numUnavailableRecs} recommended extensions`, 112 ); 113 114 showExtensions(unavailableRecs); 115 } 116 117 if (token.isCancellationRequested) { 118 logger.info('User cancelled recommended extensions check'); 119 120 break; 121 } 122 } 123 124 logger.info('All recommended extensions are enabled'); 125 vscode.commands.executeCommand( 126 'workbench.action.toggleSidebarVisibility', 127 ); 128 progress.report({ increment: 100 }); 129 }, 130 ); 131} 132 133/** 134 * Given a list of extensions, return the subset that are enabled. 135 * @param extensions - A list of extension IDs 136 * @return A list of extension IDs 137 */ 138function getEnabledExtensions(extensions: string[]): string[] { 139 const enabledExtensions: string[] = []; 140 const available = vscode.extensions.all; 141 142 // TODO(chadnorvell): Verify that this excludes disabled extensions 143 extensions.map(async (extId) => { 144 const ext = available.find((ext) => ext.id == extId); 145 146 if (ext) { 147 enabledExtensions.push(extId); 148 } 149 }); 150 151 return enabledExtensions; 152} 153 154/** 155 * If there are unwanted extensions that are enabled in the current workspace, 156 * prompt the user to disable them. This is "sticky" in the sense that it will 157 * keep bugging the user to disable those extensions until they disable them 158 * all, or until they explicitly cancel. 159 * @param recs - A list of extension IDs 160 */ 161async function disableUnwantedExtensions(unwanted: string[]) { 162 let enabledUnwanted = getEnabledExtensions(unwanted); 163 const totalNumEnabledUnwanted = enabledUnwanted.length; 164 let numEnabledUnwanted = totalNumEnabledUnwanted; 165 166 const update = () => { 167 enabledUnwanted = getEnabledExtensions(unwanted); 168 numEnabledUnwanted = enabledUnwanted.length; 169 }; 170 171 const wait = async () => new Promise((resolve) => setTimeout(resolve, 2500)); 172 173 const progressIncrement = (num: number) => 174 1 - (num / totalNumEnabledUnwanted) * 100; 175 176 // All unwanted are disabled; we're done. 177 if (totalNumEnabledUnwanted == 0) { 178 logger.info('User has no unwanted extensions enabled'); 179 180 return; 181 } 182 183 showExtensions(enabledUnwanted); 184 185 vscode.window.showInformationMessage( 186 `This Pigweed project needs you to disable ${totalNumEnabledUnwanted} ` + 187 'incompatible extensions. ' + 188 'Disable the extensions shown the extensions tab.', 189 { modal: true }, 190 'Ok', 191 ); 192 193 vscode.window.withProgress( 194 { 195 location: vscode.ProgressLocation.Notification, 196 title: 197 'Disable these extensions! This Pigweed project needs these extensions to be disabled.', 198 cancellable: true, 199 }, 200 async (progress, token) => { 201 while (numEnabledUnwanted > 0) { 202 // TODO(chadnorvell): Wait for vscode.extensions.onDidChange 203 await wait(); 204 update(); 205 206 progress.report({ 207 increment: progressIncrement(numEnabledUnwanted), 208 }); 209 210 if (numEnabledUnwanted > 0) { 211 logger.info( 212 `User has ${numEnabledUnwanted} unwanted extensions enabled`, 213 ); 214 215 showExtensions(enabledUnwanted); 216 } 217 218 if (token.isCancellationRequested) { 219 logger.info('User cancelled unwanted extensions check'); 220 221 break; 222 } 223 } 224 225 logger.info('All unwanted extensions are disabled'); 226 vscode.commands.executeCommand( 227 'workbench.action.toggleSidebarVisibility', 228 ); 229 progress.report({ increment: 100 }); 230 }, 231 ); 232} 233 234export async function checkExtensions() { 235 const extensions = await getExtensionsJson(); 236 237 const num_recommendations = extensions?.recommendations?.length ?? 0; 238 const num_unwanted = extensions?.unwantedRecommendations?.length ?? 0; 239 240 if (extensions && num_recommendations > 0) { 241 await installRecommendedExtensions(extensions.recommendations as string[]); 242 } 243 244 if (extensions && num_unwanted > 0) { 245 await disableUnwantedExtensions( 246 extensions.unwantedRecommendations as string[], 247 ); 248 } 249} 250