• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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