xref: /aosp_15_r20/external/pigweed/pw_ide/ts/pigweed-vscode/src/extension.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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 { configureBazelisk, configureBazelSettings } from './bazel';
18import { BazelRefreshCompileCommandsWatcher } from './bazelWatcher';
19import {
20  ClangdActiveFilesCache,
21  initClangdPath,
22  setCompileCommandsTargetOnSettingsChange,
23  setTarget,
24} from './clangd';
25import { registerBazelProjectCommands } from './commands/bazel';
26import { getSettingsData, syncSettingsSharedToProject } from './configParsing';
27import { Disposer } from './disposables';
28import { didInit, linkRefreshManagerToEvents } from './events';
29import { checkExtensions } from './extensionManagement';
30import { InactiveFileDecorationProvider } from './inactiveFileDecoration';
31import logger, { output } from './logging';
32import { fileBug, launchTroubleshootingLink } from './links';
33
34import {
35  getPigweedProjectRoot,
36  isBazelWorkspaceProject,
37  isBootstrapProject,
38} from './project';
39
40import { RefreshManager } from './refreshManager';
41import { settings, workingDir } from './settings';
42import { ClangdFileWatcher, SettingsFileWatcher } from './settingsWatcher';
43
44import {
45  InactiveVisibilityStatusBarItem,
46  TargetStatusBarItem,
47} from './statusBar';
48
49import {
50  launchBootstrapTerminal,
51  launchTerminal,
52  patchBazeliskIntoTerminalPath,
53} from './terminal';
54
55const disposer = new Disposer();
56
57function registerUniversalCommands(context: vscode.ExtensionContext) {
58  context.subscriptions.push(
59    vscode.commands.registerCommand('pigweed.open-output-panel', output.show),
60  );
61
62  context.subscriptions.push(
63    vscode.commands.registerCommand('pigweed.file-bug', fileBug),
64  );
65
66  context.subscriptions.push(
67    vscode.commands.registerCommand(
68      'pigweed.check-extensions',
69      checkExtensions,
70    ),
71  );
72
73  context.subscriptions.push(
74    vscode.commands.registerCommand('pigweed.sync-settings', async () =>
75      syncSettingsSharedToProject(await getSettingsData(), true),
76    ),
77  );
78}
79
80function registerBootstrapCommands(context: vscode.ExtensionContext) {
81  context.subscriptions.push(
82    vscode.commands.registerCommand('pigweed.launch-terminal', launchTerminal),
83  );
84
85  context.subscriptions.push(
86    vscode.commands.registerCommand(
87      'pigweed.bootstrap-terminal',
88      launchBootstrapTerminal,
89    ),
90  );
91
92  context.subscriptions.push(
93    vscode.commands.registerCommand(
94      'pigweed.disable-inactive-file-code-intelligence',
95      () =>
96        vscode.window.showWarningMessage(
97          'This command is currently not supported with Bootstrap projects',
98        ),
99    ),
100  );
101
102  context.subscriptions.push(
103    vscode.commands.registerCommand(
104      'pigweed.enable-inactive-file-code-intelligence',
105      () =>
106        vscode.window.showWarningMessage(
107          'This command is currently not supported with Bootstrap projects',
108        ),
109    ),
110  );
111
112  context.subscriptions.push(
113    vscode.commands.registerCommand('pigweed.refresh-compile-commands', () =>
114      vscode.window.showWarningMessage(
115        'This command is currently not supported with Bootstrap projects',
116      ),
117    ),
118  );
119
120  context.subscriptions.push(
121    vscode.commands.registerCommand(
122      'pigweed.refresh-compile-commands-and-set-target',
123      () =>
124        vscode.window.showWarningMessage(
125          'This command is currently not supported with Bootstrap projects',
126        ),
127    ),
128  );
129
130  context.subscriptions.push(
131    vscode.commands.registerCommand('pigweed.set-bazelisk-path', () =>
132      vscode.window.showWarningMessage(
133        'This command is currently not supported with Bootstrap projects',
134      ),
135    ),
136  );
137
138  context.subscriptions.push(
139    vscode.commands.registerCommand(
140      'pigweed.set-bazel-recommended-settings',
141      () =>
142        vscode.window.showWarningMessage(
143          'This command is currently not supported with Bootstrap projects',
144        ),
145    ),
146  );
147
148  context.subscriptions.push(
149    vscode.commands.registerCommand('pigweed.select-target', () =>
150      vscode.window.showWarningMessage(
151        'This command is currently not supported with Bootstrap projects',
152      ),
153    ),
154  );
155}
156
157async function initAsBazelProject(context: vscode.ExtensionContext) {
158  // Do stuff that we want to do on load.
159  await initClangdPath();
160  await configureBazelSettings();
161  await configureBazelisk();
162
163  if (settings.activateBazeliskInNewTerminals()) {
164    vscode.window.onDidOpenTerminal(
165      patchBazeliskIntoTerminalPath,
166      disposer.disposables,
167    );
168  }
169
170  // Marshall all of our components and dependencies.
171  const refreshManager = disposer.add(RefreshManager.create());
172  linkRefreshManagerToEvents(refreshManager);
173
174  const { clangdActiveFilesCache, compileCommandsWatcher } = disposer.addMany({
175    clangdActiveFilesCache: new ClangdActiveFilesCache(refreshManager),
176    compileCommandsWatcher: new BazelRefreshCompileCommandsWatcher(
177      refreshManager,
178      settings.disableCompileCommandsFileWatcher(),
179    ),
180    inactiveVisibilityStatusBarItem: new InactiveVisibilityStatusBarItem(),
181    settingsFileWatcher: new SettingsFileWatcher(),
182    targetStatusBarItem: new TargetStatusBarItem(),
183  });
184
185  // If the current target is changed directly via a settings file change (in
186  // other words, not by running a command), detect that and do all the other
187  // stuff that the command would otherwise have done.
188  vscode.workspace.onDidChangeConfiguration(
189    setCompileCommandsTargetOnSettingsChange(clangdActiveFilesCache),
190    disposer.disposables,
191  );
192
193  disposer.add(new ClangdFileWatcher(clangdActiveFilesCache));
194  disposer.add(new InactiveFileDecorationProvider(clangdActiveFilesCache));
195
196  registerBazelProjectCommands(
197    context,
198    refreshManager,
199    compileCommandsWatcher,
200    clangdActiveFilesCache,
201  );
202
203  if (!settings.disableCompileCommandsFileWatcher()) {
204    compileCommandsWatcher.refresh();
205  }
206
207  didInit.fire();
208}
209
210/**
211 * Get the project type and configuration details on startup.
212 *
213 * This is a little long and over-complex, but essentially it does just a few
214 * things:
215 *   - Do I know where the Pigweed project root is?
216 *   - Do I know if this is a Bazel or bootstrap project?
217 *   - If I don't know any of that, ask the user to tell me and save the
218 *     selection to settings.
219 *   - If the user needs help, route them to the right place.
220 */
221async function configureProject(context: vscode.ExtensionContext) {
222  // If we're missing a piece of information, we can ask the user to manually
223  // provide it. If they do, we should re-run this flow, and that intent is
224  // signaled by setting this var.
225  let tryAgain = false;
226
227  const projectRoot = await getPigweedProjectRoot(settings, workingDir);
228
229  if (projectRoot) {
230    output.appendLine(`The Pigweed project root is ${projectRoot}`);
231
232    if (
233      settings.projectType() === 'bootstrap' ||
234      isBootstrapProject(projectRoot)
235    ) {
236      output.appendLine('This is a bootstrap project');
237      registerBootstrapCommands(context);
238    } else if (
239      settings.projectType() === 'bazel' ||
240      isBazelWorkspaceProject(projectRoot)
241    ) {
242      output.appendLine('This is a Bazel project');
243      await initAsBazelProject(context);
244    } else {
245      vscode.window
246        .showErrorMessage(
247          "I couldn't automatically determine what type of Pigweed project " +
248            'this is. Choose one of these project types, or get more help.',
249          'Bazel',
250          'Bootstrap',
251          'Get Help',
252        )
253        .then((selection) => {
254          switch (selection) {
255            case 'Bazel': {
256              settings.projectType('bazel');
257              vscode.window.showInformationMessage(
258                'Configured as a Pigweed Bazel project',
259              );
260              tryAgain = true;
261              break;
262            }
263            case 'Bootstrap': {
264              settings.projectType('bootstrap');
265              vscode.window.showInformationMessage(
266                'Configured as a Pigweed Bootstrap project',
267              );
268              tryAgain = true;
269              break;
270            }
271            case 'Get Help': {
272              launchTroubleshootingLink('project-type');
273              break;
274            }
275          }
276        });
277    }
278  } else {
279    vscode.window
280      .showErrorMessage(
281        "I couldn't automatically determine the location of the  Pigweed " +
282          'root directory. Enter it manually, or get more help.',
283        'Browse',
284        'Get Help',
285      )
286      .then((selection) => {
287        switch (selection) {
288          case 'Browse': {
289            vscode.window
290              .showOpenDialog({
291                canSelectFiles: false,
292                canSelectFolders: true,
293                canSelectMany: false,
294              })
295              .then((result) => {
296                // The user can cancel, making result undefined
297                if (result) {
298                  const [uri] = result;
299                  settings.projectRoot(uri.fsPath);
300                  vscode.window.showInformationMessage(
301                    `Set the Pigweed project root to: ${uri.fsPath}`,
302                  );
303                  tryAgain = true;
304                }
305              });
306            break;
307          }
308          case 'Get Help': {
309            launchTroubleshootingLink('project-root');
310            break;
311          }
312        }
313      });
314  }
315
316  // This should only re-run if something has materially changed, e.g., the user
317  // provided a piece of information we needed.
318  if (tryAgain) {
319    await configureProject(context);
320  }
321}
322
323export async function activate(context: vscode.ExtensionContext) {
324  // Register commands that apply to all project types
325  registerUniversalCommands(context);
326  logger.info('Extension loaded');
327
328  // Determine the project type and configuration parameters. This also
329  // registers the commands specific to each project type.
330  await configureProject(context);
331
332  if (settings.enforceExtensionRecommendations()) {
333    logger.info('Project is configured to enforce extension recommendations');
334    await checkExtensions();
335  }
336}
337
338export function deactivate() {
339  disposer.dispose();
340}
341