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