]> Git Repo - pico-vscode.git/blob - src/extension.mts
Merge branch 'main' into main
[pico-vscode.git] / src / extension.mts
1 import {
2   workspace,
3   type ExtensionContext,
4   window,
5   type WebviewPanel,
6   commands,
7   ProgressLocation,
8 } from "vscode";
9 import {
10   extensionName,
11   type Command,
12   type CommandWithArgs,
13   type CommandWithResult,
14 } from "./commands/command.mjs";
15 import NewProjectCommand from "./commands/newProject.mjs";
16 import Logger, { LoggerSource } from "./logger.mjs";
17 import {
18   CMAKE_DO_NOT_EDIT_HEADER_PREFIX,
19   cmakeGetSelectedBoard,
20   cmakeGetSelectedToolchainAndSDKVersions,
21   configureCmakeNinja,
22 } from "./utils/cmakeUtil.mjs";
23 import Settings, {
24   SettingsKey,
25   type PackageJSON,
26   HOME_VAR,
27 } from "./settings.mjs";
28 import UI from "./ui.mjs";
29 import SwitchSDKCommand from "./commands/switchSDK.mjs";
30 import { existsSync, readFileSync } from "fs";
31 import { basename, join } from "path";
32 import CompileProjectCommand from "./commands/compileProject.mjs";
33 import RunProjectCommand from "./commands/runProject.mjs";
34 import LaunchTargetPathCommand from "./commands/launchTargetPath.mjs";
35 import {
36   GetPythonPathCommand,
37   GetEnvPathCommand,
38   GetGDBPathCommand,
39   GetChipCommand,
40   GetTargetCommand,
41   GetChipUppercaseCommand,
42 } from "./commands/getPaths.mjs";
43 import {
44   downloadAndInstallCmake,
45   downloadAndInstallNinja,
46   downloadAndInstallSDK,
47   downloadAndInstallToolchain,
48   downloadAndInstallTools,
49   downloadAndInstallPicotool,
50   downloadAndInstallOpenOCD,
51   downloadEmbedPython,
52 } from "./utils/download.mjs";
53 import { SDK_REPOSITORY_URL } from "./utils/githubREST.mjs";
54 import { getSupportedToolchains } from "./utils/toolchainUtil.mjs";
55 import {
56   NewProjectPanel,
57   getWebviewOptions,
58   openOCDVersion,
59 } from "./webview/newProjectPanel.mjs";
60 import GithubApiCache from "./utils/githubApiCache.mjs";
61 import ClearGithubApiCacheCommand from "./commands/clearGithubApiCache.mjs";
62 import { ContextKeys } from "./contextKeys.mjs";
63 import { PicoProjectActivityBar } from "./webview/activityBar.mjs";
64 import ConditionalDebuggingCommand from "./commands/conditionalDebugging.mjs";
65 import DebugLayoutCommand from "./commands/debugLayout.mjs";
66 import OpenSdkDocumentationCommand from "./commands/openSdkDocumentation.mjs";
67 import ConfigureCmakeCommand from "./commands/configureCmake.mjs";
68 import ImportProjectCommand from "./commands/importProject.mjs";
69 import { homedir } from "os";
70 import VersionBundlesLoader from "./utils/versionBundles.mjs";
71 import { pyenvInstallPython, setupPyenv } from "./utils/pyenvUtil.mjs";
72 import NewExampleProjectCommand from "./commands/newExampleProject.mjs";
73 import SwitchBoardCommand from "./commands/switchBoard.mjs";
74 import UninstallPicoSDKCommand from "./commands/uninstallPicoSDK.mjs";
75 import FlashProjectSWDCommand from "./commands/flashProjectSwd.mjs";
76
77 export async function activate(context: ExtensionContext): Promise<void> {
78   Logger.info(LoggerSource.extension, "Extension activation triggered");
79
80   const settings = Settings.createInstance(
81     context.workspaceState,
82     context.globalState,
83     context.extension.packageJSON as PackageJSON
84   );
85   GithubApiCache.createInstance(context);
86
87   const picoProjectActivityBarProvider = new PicoProjectActivityBar();
88   const ui = new UI(picoProjectActivityBarProvider);
89   ui.init();
90
91   const COMMANDS: Array<
92     | Command
93     | CommandWithResult<string>
94     | CommandWithResult<boolean>
95     | CommandWithArgs
96   > = [
97     new NewProjectCommand(context.extensionUri),
98     new SwitchSDKCommand(ui, context.extensionUri),
99     new SwitchBoardCommand(ui, context.extensionUri),
100     new LaunchTargetPathCommand(),
101     new GetPythonPathCommand(),
102     new GetEnvPathCommand(),
103     new GetGDBPathCommand(),
104     new GetChipCommand(),
105     new GetChipUppercaseCommand(),
106     new GetTargetCommand(),
107     new CompileProjectCommand(),
108     new RunProjectCommand(),
109     new FlashProjectSWDCommand(),
110     new ClearGithubApiCacheCommand(),
111     new ConditionalDebuggingCommand(),
112     new DebugLayoutCommand(),
113     new OpenSdkDocumentationCommand(context.extensionUri),
114     new ConfigureCmakeCommand(),
115     new ImportProjectCommand(context.extensionUri),
116     new NewExampleProjectCommand(context.extensionUri),
117     new UninstallPicoSDKCommand(),
118   ];
119
120   // register all command handlers
121   COMMANDS.forEach(command => {
122     context.subscriptions.push(command.register());
123   });
124
125   context.subscriptions.push(
126     window.registerWebviewPanelSerializer(NewProjectPanel.viewType, {
127       // eslint-disable-next-line @typescript-eslint/require-await
128       async deserializeWebviewPanel(
129         webviewPanel: WebviewPanel,
130         state: { isImportProject: boolean; forceFromExample: boolean }
131       ): Promise<void> {
132         // Reset the webview options so we use latest uri for `localResourceRoots`.
133         webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
134         NewProjectPanel.revive(
135           webviewPanel,
136           context.extensionUri,
137           state && state.isImportProject,
138           state && state.forceFromExample
139         );
140       },
141     })
142   );
143
144   context.subscriptions.push(
145     window.registerTreeDataProvider(
146       PicoProjectActivityBar.viewType,
147       picoProjectActivityBarProvider
148     )
149   );
150
151   const workspaceFolder = workspace.workspaceFolders?.[0];
152
153   // check if is a pico project
154   if (
155     workspaceFolder === undefined ||
156     !existsSync(join(workspaceFolder.uri.fsPath, "pico_sdk_import.cmake"))
157   ) {
158     // finish activation
159     Logger.warn(
160       LoggerSource.extension,
161       "No workspace folder or Pico project found."
162     );
163     await commands.executeCommand(
164       "setContext",
165       ContextKeys.isPicoProject,
166       false
167     );
168
169     return;
170   }
171
172   const cmakeListsFilePath = join(workspaceFolder.uri.fsPath, "CMakeLists.txt");
173   if (!existsSync(cmakeListsFilePath)) {
174     Logger.warn(
175       LoggerSource.extension,
176       "No CMakeLists.txt in workspace folder has been found."
177     );
178     await commands.executeCommand(
179       "setContext",
180       ContextKeys.isPicoProject,
181       false
182     );
183
184     return;
185   }
186
187   // check if it has .vscode folder and cmake donotedit header in CMakelists.txt
188   if (
189     !existsSync(join(workspaceFolder.uri.fsPath, ".vscode")) ||
190     !readFileSync(cmakeListsFilePath)
191       .toString("utf-8")
192       .includes(CMAKE_DO_NOT_EDIT_HEADER_PREFIX)
193   ) {
194     Logger.warn(
195       LoggerSource.extension,
196       "No .vscode folder and/or cmake",
197       '"DO NOT EDIT"-header in CMakelists.txt found.'
198     );
199     await commands.executeCommand(
200       "setContext",
201       ContextKeys.isPicoProject,
202       false
203     );
204     const wantToImport = await window.showInformationMessage(
205       "Do you want to import this project as Raspberry Pi Pico project?",
206       "Yes",
207       "No"
208     );
209     if (wantToImport === "Yes") {
210       void commands.executeCommand(
211         `${extensionName}.${ImportProjectCommand.id}`,
212         workspaceFolder.uri
213       );
214     }
215
216     return;
217   }
218
219   await commands.executeCommand("setContext", ContextKeys.isPicoProject, true);
220
221   // get sdk selected in the project
222   const selectedToolchainAndSDKVersions =
223     await cmakeGetSelectedToolchainAndSDKVersions(workspaceFolder.uri);
224   if (selectedToolchainAndSDKVersions === null) {
225     return;
226   }
227
228   // get all available toolchains for download link of current selected one
229   const toolchains = await getSupportedToolchains();
230   const selectedToolchain = toolchains.find(
231     toolchain => toolchain.version === selectedToolchainAndSDKVersions[1]
232   );
233
234   // install if needed
235   if (
236     !(await downloadAndInstallSDK(
237       selectedToolchainAndSDKVersions[0],
238       SDK_REPOSITORY_URL
239     )) ||
240     !(await downloadAndInstallTools(selectedToolchainAndSDKVersions[0])) ||
241     !(await downloadAndInstallPicotool(selectedToolchainAndSDKVersions[2]))
242   ) {
243     Logger.error(
244       LoggerSource.extension,
245       "Failed to install project SDK",
246       `version: ${selectedToolchainAndSDKVersions[0]}.`,
247       "Make sure all requirements are met."
248     );
249
250     void window.showErrorMessage("Failed to install project SDK version.");
251
252     return;
253   } else {
254     Logger.info(
255       LoggerSource.extension,
256       "Found/installed project SDK",
257       `version: ${selectedToolchainAndSDKVersions[0]}`
258     );
259
260     if (!(await downloadAndInstallOpenOCD(openOCDVersion))) {
261       Logger.error(
262         LoggerSource.extension,
263         "Failed to download and install openocd."
264       );
265     } else {
266       Logger.debug(
267         LoggerSource.extension,
268         "Successfully downloaded and installed openocd."
269       );
270     }
271   }
272
273   // install if needed
274   if (
275     selectedToolchain === undefined ||
276     !(await downloadAndInstallToolchain(selectedToolchain))
277   ) {
278     Logger.error(
279       LoggerSource.extension,
280       "Failed to install project toolchain",
281       `version: ${selectedToolchainAndSDKVersions[1]}`
282     );
283
284     void window.showErrorMessage(
285       "Failed to install project toolchain version."
286     );
287
288     return;
289   } else {
290     Logger.info(
291       LoggerSource.extension,
292       "Found/installed project toolchain",
293       `version: ${selectedToolchainAndSDKVersions[1]}`
294     );
295   }
296
297   // TODO: move this ninja, cmake and python installs out of extension.mts
298   const ninjaPath = settings.getString(SettingsKey.ninjaPath);
299   if (ninjaPath && ninjaPath.includes("/.pico-sdk/ninja")) {
300     // check if ninja path exists
301     if (!existsSync(ninjaPath.replace(HOME_VAR, homedir()))) {
302       Logger.debug(
303         LoggerSource.extension,
304         "Ninja path in settings does not exist.",
305         "Installing ninja to default path."
306       );
307       const ninjaVersion = /\/\.pico-sdk\/ninja\/([v.0-9]+)\//.exec(
308         ninjaPath
309       )?.[1];
310       if (ninjaVersion === undefined) {
311         Logger.error(
312           LoggerSource.extension,
313           "Failed to get ninja version from path in the settings."
314         );
315         await commands.executeCommand(
316           "setContext",
317           ContextKeys.isPicoProject,
318           false
319         );
320
321         return;
322       }
323
324       let result = false;
325       await window.withProgress(
326         {
327           location: ProgressLocation.Notification,
328           title: "Downloading and installing Ninja. This may take a while...",
329           cancellable: false,
330         },
331         async progress => {
332           result = await downloadAndInstallNinja(ninjaVersion);
333           progress.report({
334             increment: 100,
335           });
336         }
337       );
338
339       if (!result) {
340         Logger.error(LoggerSource.extension, "Failed to install ninja.");
341         await commands.executeCommand(
342           "setContext",
343           ContextKeys.isPicoProject,
344           false
345         );
346
347         return;
348       }
349       Logger.debug(
350         LoggerSource.extension,
351         "Installed selected ninja for project."
352       );
353     }
354   }
355
356   const cmakePath = settings.getString(SettingsKey.cmakePath);
357   if (cmakePath && cmakePath.includes("/.pico-sdk/cmake")) {
358     // check if cmake path exists
359     if (!existsSync(cmakePath.replace(HOME_VAR, homedir()))) {
360       Logger.warn(
361         LoggerSource.extension,
362         "CMake path in settings does not exist.",
363         "Installing CMake to default path."
364       );
365
366       const cmakeVersion = /\/\.pico-sdk\/cmake\/([v.0-9A-Za-z-]+)\//.exec(
367         cmakePath
368       )?.[1];
369       if (cmakeVersion === undefined) {
370         Logger.error(
371           LoggerSource.extension,
372           "Failed to get CMake version from path in the settings."
373         );
374         await commands.executeCommand(
375           "setContext",
376           ContextKeys.isPicoProject,
377           false
378         );
379
380         return;
381       }
382
383       let result = false;
384       await window.withProgress(
385         {
386           location: ProgressLocation.Notification,
387           title: "Downloading and installing CMake. This may take a while...",
388           cancellable: false,
389         },
390         async progress => {
391           result = await downloadAndInstallCmake(cmakeVersion);
392           progress.report({
393             increment: 100,
394           });
395         }
396       );
397
398       if (!result) {
399         Logger.error(LoggerSource.extension, "Failed to install CMake.");
400         await commands.executeCommand(
401           "setContext",
402           ContextKeys.isPicoProject,
403           false
404         );
405
406         return;
407       }
408       Logger.debug(
409         LoggerSource.extension,
410         "Installed selected cmake for project."
411       );
412     }
413   }
414
415   const pythonPath = settings.getString(SettingsKey.python3Path);
416   if (pythonPath && pythonPath.includes("/.pico-sdk/python")) {
417     // check if python path exists
418     if (!existsSync(pythonPath.replace(HOME_VAR, homedir()))) {
419       Logger.warn(
420         LoggerSource.extension,
421         "Python path in settings does not exist.",
422         "Installing Python3 to default path."
423       );
424       const pythonVersion = /\/\.pico-sdk\/python\/([.0-9]+)\//.exec(
425         pythonPath
426       )?.[1];
427       if (pythonVersion === undefined) {
428         Logger.error(
429           LoggerSource.extension,
430           "Failed to get Python version from path."
431         );
432         await commands.executeCommand(
433           "setContext",
434           ContextKeys.isPicoProject,
435           false
436         );
437
438         return;
439       }
440
441       let result: string | undefined;
442       await window.withProgress(
443         {
444           location: ProgressLocation.Notification,
445           title:
446             "Downloading and installing Python. This may take a long while...",
447           cancellable: false,
448         },
449         async progress => {
450           if (process.platform === "win32") {
451             const versionBundle = await new VersionBundlesLoader(
452               context.extensionUri
453             ).getPythonWindowsAmd64Url(pythonVersion);
454
455             if (versionBundle === undefined) {
456               Logger.error(
457                 LoggerSource.extension,
458                 "Failed to get Python download url from version bundle."
459               );
460               await commands.executeCommand(
461                 "setContext",
462                 ContextKeys.isPicoProject,
463                 false
464               );
465
466               return;
467             }
468
469             // ! because data.pythonMode === 0 => versionBundle !== undefined
470             result = await downloadEmbedPython(versionBundle);
471           } else if (process.platform === "darwin") {
472             const result1 = await setupPyenv();
473             if (!result1) {
474               progress.report({
475                 increment: 100,
476               });
477
478               return;
479             }
480             const result2 = await pyenvInstallPython(pythonVersion);
481
482             if (result2 !== null) {
483               result = result2;
484             }
485           } else {
486             Logger.info(
487               LoggerSource.extension,
488               "Automatic Python installation is only",
489               "supported on Windows and macOS."
490             );
491
492             await window.showErrorMessage(
493               "Automatic Python installation is only " +
494                 "supported on Windows and macOS."
495             );
496           }
497           progress.report({
498             increment: 100,
499           });
500         }
501       );
502
503       if (result === undefined) {
504         Logger.error(LoggerSource.extension, "Failed to install Python3.");
505         await commands.executeCommand(
506           "setContext",
507           ContextKeys.isPicoProject,
508           false
509         );
510
511         return;
512       }
513     }
514   }
515
516   ui.showStatusBarItems();
517   ui.updateSDKVersion(selectedToolchainAndSDKVersions[0]);
518
519   const selectedBoard = cmakeGetSelectedBoard(workspaceFolder.uri);
520   if (selectedBoard !== null) {
521     ui.updateBoard(selectedBoard);
522   } else {
523     ui.updateBoard("unknown");
524   }
525
526   // auto project configuration with cmake
527   if (settings.getBoolean(SettingsKey.cmakeAutoConfigure)) {
528     //run `cmake -G Ninja -B ./build ` in the root folder
529     await configureCmakeNinja(workspaceFolder.uri);
530
531     workspace.onDidChangeTextDocument(event => {
532       // Check if the changed document is the file you are interested in
533       if (basename(event.document.fileName) === "CMakeLists.txt") {
534         // File has changed, do something here
535         // TODO: rerun configure project
536         // TODO: maybe conflicts with cmake extension which also does this
537         console.log("File changed:", event.document.fileName);
538       }
539     });
540   } else {
541     Logger.info(
542       LoggerSource.extension,
543       "No workspace folder for configuration found",
544       "or cmakeAutoConfigure disabled."
545     );
546   }
547 }
548
549 export function deactivate(): void {
550   // TODO: maybe await
551   void commands.executeCommand("setContext", ContextKeys.isPicoProject, true);
552
553   Logger.info(LoggerSource.extension, "Extension deactivated.");
554 }
This page took 0.054915 seconds and 4 git commands to generate.