xref: /aosp_15_r20/external/pigweed/pw_ide/ts/pigweed-vscode/src/extensionManagement.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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