blob: 30a8639fd1712d64168186846f0cad5d995c11ff [file] [log] [blame]
// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
import * as vscode from 'vscode';
import { getExtensionsJson } from './config';
import { launchBootstrapTerminal, launchTerminal } from './terminal';
const bugUrl =
'https://issues.pigweed.dev/issues/new?component=1194524&template=1911548';
/**
* Open the bug report template in the user's browser.
*/
function fileBug() {
vscode.env.openExternal(vscode.Uri.parse(bugUrl));
}
/**
* Open the extensions sidebar and show the provided extensions.
* @param extensions - A list of extension IDs
*/
function showExtensions(extensions: string[]) {
vscode.commands.executeCommand(
'workbench.extensions.search',
'@id:' + extensions.join(', @id:'),
);
}
/**
* Given a list of extensions, return the subset that are not installed or are
* disabled.
* @param extensions - A list of extension IDs
* @returns A list of extension IDs
*/
function getUnavailableExtensions(extensions: string[]): string[] {
const unavailableExtensions: string[] = [];
const available = vscode.extensions.all;
// TODO(chadnorvell): Verify that this includes disabled extensions
extensions.map(async (extId) => {
const ext = available.find((ext) => ext.id == extId);
if (!ext) {
unavailableExtensions.push(extId);
}
});
return unavailableExtensions;
}
/**
* If there are recommended extensions that are not installed or enabled in the
* current workspace, prompt the user to install them. This is "sticky" in the
* sense that it will keep bugging the user to enable those extensions until
* they enable them all, or until they explicitly cancel.
* @param recs - A list of extension IDs
*/
async function installRecommendedExtensions(recs: string[]): Promise<void> {
let unavailableRecs = getUnavailableExtensions(recs);
const totalNumUnavailableRecs = unavailableRecs.length;
let numUnavailableRecs = totalNumUnavailableRecs;
const update = () => {
unavailableRecs = getUnavailableExtensions(recs);
numUnavailableRecs = unavailableRecs.length;
};
const wait = async () => new Promise((resolve) => setTimeout(resolve, 2500));
const progressIncrement = (num: number) =>
1 - (num / totalNumUnavailableRecs) * 100;
// All recommendations are installed; we're done.
if (totalNumUnavailableRecs == 0) {
console.log('User has all recommended extensions');
return;
}
showExtensions(unavailableRecs);
vscode.window.showInformationMessage(
`This Pigweed project needs you to install ${totalNumUnavailableRecs} ` +
'required extensions. ' +
'Install the extensions shown in the extensions tab.',
{ modal: true },
'Ok',
);
vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title:
'Install these extensions! This Pigweed project needs these recommended extensions to be installed.',
cancellable: true,
},
async (progress, token) => {
while (numUnavailableRecs > 0) {
// TODO(chadnorvell): Wait for vscode.extensions.onDidChange
await wait();
update();
progress.report({
increment: progressIncrement(numUnavailableRecs),
});
if (numUnavailableRecs > 0) {
console.log(
`User lacks ${numUnavailableRecs} recommended extensions`,
);
showExtensions(unavailableRecs);
}
if (token.isCancellationRequested) {
console.log('User cancelled recommended extensions check');
break;
}
}
console.log('All recommended extensions are enabled');
vscode.commands.executeCommand(
'workbench.action.toggleSidebarVisibility',
);
progress.report({ increment: 100 });
},
);
}
/**
* Given a list of extensions, return the subset that are enabled.
* @param extensions - A list of extension IDs
* @returns A list of extension IDs
*/
function getEnabledExtensions(extensions: string[]): string[] {
const enabledExtensions: string[] = [];
const available = vscode.extensions.all;
// TODO(chadnorvell): Verify that this excludes disabled extensions
extensions.map(async (extId) => {
const ext = available.find((ext) => ext.id == extId);
if (ext) {
enabledExtensions.push(extId);
}
});
return enabledExtensions;
}
/**
* If there are unwanted extensions that are enabled in the current workspace,
* prompt the user to disable them. This is "sticky" in the sense that it will
* keep bugging the user to disable those extensions until they disable them
* all, or until they explicitly cancel.
* @param recs - A list of extension IDs
*/
async function disableUnwantedExtensions(unwanted: string[]) {
let enabledUnwanted = getEnabledExtensions(unwanted);
const totalNumEnabledUnwanted = enabledUnwanted.length;
let numEnabledUnwanted = totalNumEnabledUnwanted;
const update = () => {
enabledUnwanted = getEnabledExtensions(unwanted);
numEnabledUnwanted = enabledUnwanted.length;
};
const wait = async () => new Promise((resolve) => setTimeout(resolve, 2500));
const progressIncrement = (num: number) =>
1 - (num / totalNumEnabledUnwanted) * 100;
// All unwanted are disabled; we're done.
if (totalNumEnabledUnwanted == 0) {
console.log('User has no unwanted extensions enabled');
return;
}
showExtensions(enabledUnwanted);
vscode.window.showInformationMessage(
`This Pigweed project needs you to disable ${totalNumEnabledUnwanted} ` +
'incompatible extensions. ' +
'Disable the extensions shown the extensions tab.',
{ modal: true },
'Ok',
);
vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title:
'Disable these extensions! This Pigweed project needs these extensions to be disabled.',
cancellable: true,
},
async (progress, token) => {
while (numEnabledUnwanted > 0) {
// TODO(chadnorvell): Wait for vscode.extensions.onDidChange
await wait();
update();
progress.report({
increment: progressIncrement(numEnabledUnwanted),
});
if (numEnabledUnwanted > 0) {
console.log(
`User has ${numEnabledUnwanted} unwanted extensions enabled`,
);
showExtensions(enabledUnwanted);
}
if (token.isCancellationRequested) {
console.log('User cancelled unwanted extensions check');
break;
}
}
console.log('All unwanted extensions are disabled');
vscode.commands.executeCommand(
'workbench.action.toggleSidebarVisibility',
);
progress.report({ increment: 100 });
},
);
}
async function checkExtensions() {
const extensions = await getExtensionsJson();
const num_recommendations = extensions?.recommendations?.length ?? 0;
const num_unwanted = extensions?.unwantedRecommendations?.length ?? 0;
if (extensions && num_recommendations > 0) {
await installRecommendedExtensions(extensions.recommendations as string[]);
}
if (extensions && num_unwanted > 0) {
await disableUnwantedExtensions(
extensions.unwantedRecommendations as string[],
);
}
}
function registerCommands(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('pigweed.file-bug', () => fileBug()),
);
context.subscriptions.push(
vscode.commands.registerCommand('pigweed.check-extensions', () =>
checkExtensions(),
),
);
context.subscriptions.push(
vscode.commands.registerCommand('pigweed.launch-terminal', () =>
launchTerminal(),
),
);
context.subscriptions.push(
vscode.commands.registerCommand('pigweed.bootstrap-terminal', () =>
launchBootstrapTerminal(),
),
);
}
export async function activate(context: vscode.ExtensionContext) {
registerCommands(context);
const shouldEnforce = vscode.workspace
.getConfiguration('pigweed')
.get('enforceExtensionRecommendations') as string;
if (shouldEnforce === 'true') {
console.log('pigweed.enforceExtensionRecommendations: true');
await checkExtensions();
}
}
export function deactivate() {
// Do nothing.
}