diff --git a/.vscode/settings.json b/.vscode/settings.json index 7070a53d..9c8aaaa3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,8 +2,10 @@ "files.exclude": { // set them to true to hide them in vscode "out": false, - "dist": false, - "**/*.vsix": true + "dist": true, + "**/*.vsix": true, + ".yarn": true, + ".pnp.*": true }, "search.exclude": { "out": true, diff --git a/README.md b/README.md index 60868c3c..d94718d3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Raspberry Pi Pico Visual Studio Code extension -> NOTE: The extension is currently under development. +> **Note: The extension is currently under development.** This is the official Visual Studio Code extension for Raspberry Pi Pico development. This extension equips you with a suite of tools designed to streamline your Pico projects using Visual Studio Code and the official [Pico SDK](https://github.com/raspberrypi/pico-sdk). @@ -10,18 +10,34 @@ For comprehensive setup instructions, refer to the [Getting Started guide](https ## Features -- Project Generator: Easily create new projects targeting the Ninja build system. -- Automatic CMake Configuration: Automatically configures CMake when loading a project. -- Version Switching: Seamlessly switch between different versions of the Pico SDK and tools. -- No Manual Setup Required: The extension handles environment variables, toolchain, SDK, and tool management for you. -- One-Click Compilation: Compile projects directly from the status bar with your selected SDK and tools. -- Offline Documentation: Access Pico SDK documentation offline. -- Quick Project Setup: Quickly create new Pico projects from the Explorer view when no workspace is open. -- Includes an Uninstaller: Easily remove the extension along with all automatically installed tools and SDKs. +### Project Setup and Management + +- **Project Generator**: Easily create and configure new projects with support for advanced Pico features like I2C and PIO. The generator targets the Ninja build system and allows customization during project creation. +- **Quick Project Setup**: Initiate new Pico projects directly from the Explorer view, when no workspace is open. +- **MicroPython Support**: Create and develop MicroPython-based Pico projects with support provided through the [MicroPico](https://github.com/paulober/MicroPico) extension. + +### Configuration and Tool Management + +- **Automatic CMake Configuration**: Automatically configures CMake when loading a project. +- **Version Switching**: Seamlessly switch between different versions of the Pico SDK and tools. +- **No Manual Setup Required**: Automatically handles environment variables, toolchain, SDK, and tool management. +- **Includes an Uninstaller**: Easily remove the extension along with all automatically installed tools and SDKs. + +### Build, Debug, and Documentation + +- **One-Click Compilation and Debugging**: Automatically configure OpenOCD, Ninja, and CMake, allowing you to compile and debug with a single click. +- **Offline Documentation**: Conveniently access Pico SDK documentation directly within the editor, even when offline. + +- **Version Switching**: Seamlessly switch between different versions of the Pico SDK and tools. +- **No Manual Setup Required**: The extension handles environment variables, toolchain, SDK, and tool management for you. +- **One-Click Compilation**: Compile projects directly from the status bar with your selected SDK and tools. +- **Offline Documentation**: Access Pico SDK documentation offline. +- **Quick Project Setup**: Quickly create new Pico projects from the Explorer view when no workspace is open. +- **MicroPython Support**: Create MicroPython-based Pico projects with support provided through the MicroPico extension. ## Requirements by OS -> Supported Platforms: Raspberry Pi OS (64-bit), Windows 10/11 (x86_64), macOS Sonoma (14.0) and newer, Linux x64 and arm64 +> **Supported Platforms: Raspberry Pi OS (64-bit), Windows 10/11 (x86_64), macOS Sonoma (14.0) and newer, Linux x64 and arm64** - Visual Studio Code v1.87.0 or later @@ -38,19 +54,19 @@ To meet the requirements for macOS, run the following command in Terminal to ins xcode-select --install ``` This command installs all of the necessary tools, including but not limited to: -- Git 2.28 or later (ensure it's in your PATH) -- Tar (ensure it's in your PATH) +- **Git 2.28 or later** (ensure it's in your PATH) +- **Tar** (ensure it's in your PATH) ### Windows No additional requirements are needed for Windows. ### Linux -- Python 3.9 or later (ensure it’s in your PATH or set in settings) -- Git 2.28 or later (ensure it’s in your PATH) -- Tar (ensure it’s in your PATH) -- \[Optional\] gdb-multiarch for debugging (x86_64 only) -- For \[Ubuntu 22.04\], install `libftdi1-2` and `libhidapi-hidraw0` packages to use OpenOCD +- **Python 3.9 or later** (ensure it’s in your PATH or set in settings) +- **Git 2.28 or later** (ensure it’s in your PATH) +- **Tar** (ensure it’s in your PATH) +- **\[Optional\]** gdb-multiarch for debugging (x86_64 only) +- For **\[Ubuntu 22.04\]**, install `libftdi1-2` and `libhidapi-hidraw0` packages to use OpenOCD ## Extension Settings diff --git a/eslint.config.mjs b/eslint.config.mjs index a6a184e6..97977f0e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import tseslint from "typescript-eslint"; import js from "@eslint/js"; import eslintConfigPrettier from "eslint-config-prettier"; -export default [ +export default tseslint.config( js.configs.recommended, ...tseslint.configs.recommendedTypeChecked, eslintConfigPrettier, @@ -19,7 +19,7 @@ export default [ ...globals.commonjs }, parserOptions: { - project: true, + projectService: true, tsconfigRootDir: import.meta.dirname } }, @@ -55,4 +55,4 @@ export default [ "web/**/*.js", ] } -]; +); diff --git a/package.json b/package.json index bfbe05a8..9b4a28ef 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "url": "https://github.com/raspberrypi/pico-vscode/" }, "engines": { - "vscode": "^1.87.0", - "node": ">=18.17.1" + "vscode": "^1.92.1", + "node": ">=20.14.0" }, "os": [ "win32", @@ -46,11 +46,13 @@ "ms-vscode.cpptools", "ms-vscode.cpptools-extension-pack", "ms-vscode.vscode-serial-monitor", - "marus25.cortex-debug" + "marus25.cortex-debug", + "paulober.pico-w-go", + "ms-python.python" ], "main": "./dist/extension.cjs", "markdown": "github", - "minimumNodeVersion": 18, + "minimumNodeVersion": 20, "capabilities": { "virtualWorkspaces": { "supported": false, @@ -63,7 +65,8 @@ }, "activationEvents": [ "workspaceContains:./pico_sdk_import.cmake", - "onWebviewPanel:newPicoProject" + "onWebviewPanel:newPicoProject", + "onWebviewPanel:newPicoMicroPythonProject" ], "contributes": { "commands": [ @@ -279,9 +282,9 @@ "@rollup/plugin-typescript": "^11.1.6", "@types/adm-zip": "^0.5.5", "@types/ini": "^4.1.1", - "@types/node": "18.17.x", + "@types/node": "20.14.0", "@types/uuid": "^10.0.0", - "@types/vscode": "^1.87.0", + "@types/vscode": "^1.92.0", "@types/which": "^3.0.4", "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", @@ -292,6 +295,7 @@ "typescript-eslint": "^8.1.0" }, "dependencies": { + "@vscode/python-extension": "^1.0.5", "adm-zip": "^0.5.14 <0.5.15", "got": "^14.4.2", "ini": "^4.1.3", diff --git a/src/commands/newProject.mts b/src/commands/newProject.mts index 398e5a29..45c01a74 100644 --- a/src/commands/newProject.mts +++ b/src/commands/newProject.mts @@ -1,11 +1,25 @@ -import { Command } from "./command.mjs"; +import { CommandWithArgs } from "./command.mjs"; import Logger from "../logger.mjs"; -import { type Uri } from "vscode"; +import { window, type Uri } from "vscode"; import { NewProjectPanel } from "../webview/newProjectPanel.mjs"; +// eslint-disable-next-line max-len +import { NewMicroPythonProjectPanel } from "../webview/newMicroPythonProjectPanel.mjs"; -export default class NewProjectCommand extends Command { +/** + * Enum for the language of the project. + * Can be used to specify the language of the project + * in the `NewProjectCommand` class. + */ +export enum ProjectLang { + cCpp = 1, + micropython = 2, +} + +export default class NewProjectCommand extends CommandWithArgs { private readonly _logger: Logger = new Logger("NewProjectCommand"); private readonly _extensionUri: Uri; + private static readonly micropythonOption = "MicroPython"; + private static readonly cCppOption = "C/C++"; public static readonly id = "newProject"; @@ -15,8 +29,38 @@ export default class NewProjectCommand extends Command { this._extensionUri = extensionUri; } - execute(): void { - // show webview where the process of creating a new project is continued - NewProjectPanel.createOrShow(this._extensionUri); + private preSelectedTypeToStr(preSelectedType?: number): string | undefined { + return preSelectedType === ProjectLang.cCpp + ? NewProjectCommand.cCppOption + : preSelectedType === ProjectLang.micropython + ? NewProjectCommand.micropythonOption + : undefined; + } + + async execute(preSelectedType?: number): Promise { + // ask the user what language to use + const lang = + this.preSelectedTypeToStr(preSelectedType) ?? + (await window.showQuickPick( + [NewProjectCommand.cCppOption, NewProjectCommand.micropythonOption], + { + placeHolder: "Select which language to use for your new project", + canPickMany: false, + ignoreFocusOut: false, + title: "New Pico Project", + } + )); + + if (lang === undefined) { + return; + } + + if (lang === NewProjectCommand.micropythonOption) { + // create a new project with MicroPython + NewMicroPythonProjectPanel.createOrShow(this._extensionUri); + } else { + // show webview where the process of creating a new project is continued + NewProjectPanel.createOrShow(this._extensionUri); + } } } diff --git a/src/extension.mts b/src/extension.mts index 0cf6dd58..aa9a95d5 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -74,6 +74,8 @@ import NewExampleProjectCommand from "./commands/newExampleProject.mjs"; import SwitchBoardCommand from "./commands/switchBoard.mjs"; import UninstallPicoSDKCommand from "./commands/uninstallPicoSDK.mjs"; import FlashProjectSWDCommand from "./commands/flashProjectSwd.mjs"; +// eslint-disable-next-line max-len +import { NewMicroPythonProjectPanel } from "./webview/newMicroPythonProjectPanel.mjs"; export async function activate(context: ExtensionContext): Promise { Logger.info(LoggerSource.extension, "Extension activation triggered"); @@ -144,6 +146,17 @@ export async function activate(context: ExtensionContext): Promise { }) ); + context.subscriptions.push( + window.registerWebviewPanelSerializer(NewMicroPythonProjectPanel.viewType, { + // eslint-disable-next-line @typescript-eslint/require-await + async deserializeWebviewPanel(webviewPanel: WebviewPanel): Promise { + // Reset the webview options so we use latest uri for `localResourceRoots`. + webviewPanel.webview.options = getWebviewOptions(context.extensionUri); + NewMicroPythonProjectPanel.revive(webviewPanel, context.extensionUri); + }, + }) + ); + context.subscriptions.push( window.registerTreeDataProvider( PicoProjectActivityBar.viewType, diff --git a/src/utils/githubREST.mts b/src/utils/githubREST.mts index 1b628dba..bc86c6f7 100644 --- a/src/utils/githubREST.mts +++ b/src/utils/githubREST.mts @@ -61,15 +61,13 @@ export const PYENV_REPOSITORY_URL = "https://github.com/pyenv/pyenv.git"; export function ownerOfRepository(repository: GithubRepository): string { switch (repository) { case GithubRepository.picoSDK: + case GithubRepository.tools: + case GithubRepository.picotool: return "raspberrypi"; case GithubRepository.cmake: return "Kitware"; case GithubRepository.ninja: return "ninja-build"; - case GithubRepository.tools: - return "raspberrypi"; - case GithubRepository.picotool: - return "raspberrypi"; } } diff --git a/src/webview/activityBar.mts b/src/webview/activityBar.mts index c2c330b0..bf607649 100644 --- a/src/webview/activityBar.mts +++ b/src/webview/activityBar.mts @@ -9,7 +9,7 @@ import { } from "vscode"; import Logger from "../logger.mjs"; import { extensionName } from "../commands/command.mjs"; -import NewProjectCommand from "../commands/newProject.mjs"; +import NewProjectCommand, { ProjectLang } from "../commands/newProject.mjs"; import CompileProjectCommand from "../commands/compileProject.mjs"; import RunProjectCommand from "../commands/runProject.mjs"; import SwitchSDKCommand from "../commands/switchSDK.mjs"; @@ -39,7 +39,8 @@ const COMMON_COMMANDS_PARENT_LABEL = "General"; const PROJECT_COMMANDS_PARENT_LABEL = "Project"; const DOCUMENTATION_COMMANDS_PARENT_LABEL = "Documentation"; -const NEW_PROJECT_LABEL = "New Project"; +const NEW_C_CPP_PROJECT_LABEL = "New C/C++ Project"; +const NEW_MICROPYTHON_PROJECT_LABEL = "New MicroPython Project"; const IMPORT_PROJECT_LABEL = "Import Project"; const EXAMPLE_PROJECT_LABEL = "New Project From Example"; const SWITCH_SDK_LABEL = "Switch SDK"; @@ -78,10 +79,13 @@ export class PicoProjectActivityBar element: QuickAccessCommand ): TreeItem | Thenable { switch (element.label) { - case NEW_PROJECT_LABEL: + case NEW_C_CPP_PROJECT_LABEL: // alt. "new-folder" element.iconPath = new ThemeIcon("file-directory-create"); break; + case NEW_MICROPYTHON_PROJECT_LABEL: + element.iconPath = new ThemeIcon("file-directory-create"); + break; case IMPORT_PROJECT_LABEL: // alt. "repo-pull" element.iconPath = new ThemeIcon("repo-clone"); @@ -158,11 +162,21 @@ export class PicoProjectActivityBar } else if (element.label === COMMON_COMMANDS_PARENT_LABEL) { return [ new QuickAccessCommand( - NEW_PROJECT_LABEL, + NEW_C_CPP_PROJECT_LABEL, + TreeItemCollapsibleState.None, + { + command: `${extensionName}.${NewProjectCommand.id}`, + title: NEW_C_CPP_PROJECT_LABEL, + arguments: [ProjectLang.cCpp], + } + ), + new QuickAccessCommand( + NEW_MICROPYTHON_PROJECT_LABEL, TreeItemCollapsibleState.None, { command: `${extensionName}.${NewProjectCommand.id}`, - title: NEW_PROJECT_LABEL, + title: NEW_MICROPYTHON_PROJECT_LABEL, + arguments: [ProjectLang.micropython], } ), new QuickAccessCommand( diff --git a/src/webview/newMicroPythonProjectPanel.mts b/src/webview/newMicroPythonProjectPanel.mts new file mode 100644 index 00000000..1c296b56 --- /dev/null +++ b/src/webview/newMicroPythonProjectPanel.mts @@ -0,0 +1,645 @@ +/* eslint-disable max-len */ +import type { Webview, Progress } from "vscode"; +import { + Uri, + ViewColumn, + window, + type WebviewPanel, + type Disposable, + ColorThemeKind, + workspace, + ProgressLocation, + commands, +} from "vscode"; +import Settings from "../settings.mjs"; +import Logger from "../logger.mjs"; +import type { WebviewMessage } from "./newProjectPanel.mjs"; +import { + getNonce, + getProjectFolderDialogOptions, + getWebviewOptions, +} from "./newProjectPanel.mjs"; +import which from "which"; +import { existsSync } from "fs"; +import { join } from "path"; +import { PythonExtension } from "@vscode/python-extension"; + +interface SubmitMessageValue { + projectName: string; + pythonMode: number; + pythonPath: string; +} + +export class NewMicroPythonProjectPanel { + public static currentPanel: NewMicroPythonProjectPanel | undefined; + + public static readonly viewType = "newPicoMicroPythonProject"; + + private readonly _panel: WebviewPanel; + private readonly _extensionUri: Uri; + private readonly _settings: Settings; + private readonly _logger: Logger = new Logger("NewMicroPythonProjectPanel"); + private _disposables: Disposable[] = []; + + private _projectRoot?: Uri; + private _pythonExtensionApi?: PythonExtension; + + public static createOrShow(extensionUri: Uri, projectUri?: Uri): void { + const column = window.activeTextEditor + ? window.activeTextEditor.viewColumn + : undefined; + + if (NewMicroPythonProjectPanel.currentPanel) { + NewMicroPythonProjectPanel.currentPanel._panel.reveal(column); + // update already exiting panel with new project root + if (projectUri) { + NewMicroPythonProjectPanel.currentPanel._projectRoot = projectUri; + // update webview + void NewMicroPythonProjectPanel.currentPanel._panel.webview.postMessage( + { + command: "changeLocation", + value: projectUri?.fsPath, + } + ); + } + + return; + } + + const panel = window.createWebviewPanel( + NewMicroPythonProjectPanel.viewType, + "New MicroPython Pico Project", + column || ViewColumn.One, + getWebviewOptions(extensionUri) + ); + + const settings = Settings.getInstance(); + if (!settings) { + panel.dispose(); + + // TODO: maybe add restart button + void window.showErrorMessage( + "Failed to load settings. Please restart VSCode." + ); + + return; + } + + NewMicroPythonProjectPanel.currentPanel = new NewMicroPythonProjectPanel( + panel, + settings, + extensionUri, + projectUri + ); + } + + public static revive(panel: WebviewPanel, extensionUri: Uri): void { + const settings = Settings.getInstance(); + if (settings === undefined) { + // TODO: maybe add restart button + void window.showErrorMessage( + "Failed to load settings. Please restart VSCode." + ); + + return; + } + + // TODO: reload if it was import panel maybe in state + NewMicroPythonProjectPanel.currentPanel = new NewMicroPythonProjectPanel( + panel, + settings, + extensionUri + ); + } + + private constructor( + panel: WebviewPanel, + settings: Settings, + extensionUri: Uri, + projectUri?: Uri + ) { + this._panel = panel; + this._extensionUri = extensionUri; + this._settings = settings; + + this._projectRoot = projectUri ?? this._settings.getLastProjectRoot(); + + void this._update(); + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + // Update the content based on view changes + this._panel.onDidChangeViewState( + async () => { + if (this._panel.visible) { + await this._update(); + } + }, + null, + this._disposables + ); + + workspace.onDidChangeConfiguration( + async () => { + await this._updateTheme(); + }, + null, + this._disposables + ); + + this._panel.webview.onDidReceiveMessage( + async (message: WebviewMessage) => { + switch (message.command) { + case "changeLocation": + { + const newLoc = await window.showOpenDialog( + getProjectFolderDialogOptions(this._projectRoot, false) + ); + + if (newLoc && newLoc[0]) { + // overwrite preview folderUri + this._projectRoot = newLoc[0]; + await this._settings.setLastProjectRoot(newLoc[0]); + + // update webview + await this._panel.webview.postMessage({ + command: "changeLocation", + value: newLoc[0].fsPath, + }); + } + } + break; + case "cancel": + this.dispose(); + break; + case "error": + void window.showErrorMessage(message.value as string); + break; + case "submit": + { + const data = message.value as SubmitMessageValue; + + if ( + this._projectRoot === undefined || + this._projectRoot.fsPath === "" + ) { + void window.showErrorMessage( + "No project root selected. Please select a project root." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + if ( + data.projectName === undefined || + data.projectName.length === 0 + ) { + void window.showWarningMessage( + "The project name is empty. Please enter a project name." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + // check if projectRoot/projectName folder already exists + if ( + existsSync(join(this._projectRoot.fsPath, data.projectName)) + ) { + void window.showErrorMessage( + "Project already exists. " + + "Please select a different project name or root." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + // close panel before generating project + this.dispose(); + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Generating MicroPico project ${ + data.projectName ?? "undefined" + } in ${this._projectRoot?.fsPath}...`, + }, + async progress => + this._generateProjectOperation(progress, data, message) + ); + } + break; + } + }, + null, + this._disposables + ); + + if (projectUri !== undefined) { + // update webview + void this._panel.webview.postMessage({ + command: "changeLocation", + value: projectUri.fsPath, + }); + } + } + + private async _generateProjectOperation( + progress: Progress<{ message?: string; increment?: number }>, + data: SubmitMessageValue, + message: WebviewMessage + ): Promise { + const projectPath = this._projectRoot?.fsPath ?? ""; + + if ( + typeof message.value !== "object" || + message.value === null || + projectPath.length === 0 + ) { + void window.showErrorMessage( + "Failed to generate MicroPython project. " + + "Please try again and check your settings." + ); + + return; + } + + // install python (if necessary) + let python3Path: string | undefined; + if (process.platform === "darwin" || process.platform === "win32") { + switch (data.pythonMode) { + case 0: + python3Path = data.pythonPath; + break; + case 1: + python3Path = process.platform === "win32" ? "python" : "python3"; + break; + case 2: + python3Path = data.pythonPath; + break; + } + + if (python3Path === undefined) { + progress.report({ + message: "Failed", + increment: 100, + }); + await window.showErrorMessage("Failed to find python3 executable."); + + return; + } + } else { + python3Path = "python3"; + } + + // create the folder with project name in project root + // open the folder in vscode and call micropico.initialise + + // create the project folder + const projectFolder = join(projectPath, data.projectName); + progress.report({ + message: `Creating project folder ${projectFolder}`, + increment: 10, + }); + + try { + await workspace.fs.createDirectory(Uri.file(projectFolder)); + // also create a blink.py in it with a import machine + const blinkPyCode = `from machine import Pin +from utime import sleep + +pin = Pin("LED", Pin.OUT) + +print("LED starts flashing...") +while True: + try: + pin.toggle() + sleep(1) # sleep 1sec + except KeyboardInterrupt: + break +pin.off() +print("Finished.")\r\n`; + const filePath = join(projectFolder, "blink.py"); + await workspace.fs.writeFile( + Uri.file(filePath), + new TextEncoder().encode(blinkPyCode) + ); + } catch { + progress.report({ + message: "Failed", + increment: 100, + }); + await window.showErrorMessage( + `Failed to create project folder ${projectFolder}` + ); + + return; + } + + commands.executeCommand("micropico.initialise", projectFolder, python3Path); + progress.report({ + message: "Project initialized", + increment: 90, + }); + + // wait 2 seconds to give user option to read notifications + await new Promise(resolve => setTimeout(resolve, 2000)); + + // open and call initialise + commands.executeCommand("vscode.openFolder", Uri.file(projectFolder), { + forceReuseWindow: true, + }); + } + + private async _update(): Promise { + this._panel.title = "New MicroPython Pico Project"; + + this._panel.iconPath = Uri.joinPath( + this._extensionUri, + "web", + "raspberry-128.png" + ); + if (!this._pythonExtensionApi) { + this._pythonExtensionApi = await PythonExtension.api(); + } + const html = await this._getHtmlForWebview(this._panel.webview); + + if (html !== "") { + this._panel.webview.html = html; + await this._updateTheme(); + } else { + void window.showErrorMessage( + "Failed to load webview for new MicroPython project" + ); + this.dispose(); + } + } + + private async _updateTheme(): Promise { + await this._panel.webview.postMessage({ + command: "setTheme", + theme: + window.activeColorTheme.kind === ColorThemeKind.Dark || + window.activeColorTheme.kind === ColorThemeKind.HighContrast + ? "dark" + : "light", + }); + } + + public dispose(): void { + NewMicroPythonProjectPanel.currentPanel = undefined; + + this._panel.dispose(); + + while (this._disposables.length) { + const x = this._disposables.pop(); + + if (x) { + x.dispose(); + } + } + } + + private async _getHtmlForWebview(webview: Webview): Promise { + const mainScriptUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "mpy", "main.js") + ); + + const mainStyleUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "main.css") + ); + + const tailwindcssScriptUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "tailwindcss-3_3_5.js") + ); + + // images + const navHeaderSvgUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "raspberrypi-nav-header.svg") + ); + + const navHeaderDarkSvgUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "raspberrypi-nav-header-dark.svg") + ); + + // TODO: add support for onDidChangeActiveEnvironment and filter envs that don't directly point + // to an executable + const environments = this._pythonExtensionApi?.environments; + const knownEnvironments = environments?.known; + const activeEnv = environments?.getActiveEnvironmentPath(); + + // TODO: check python version, workaround, only allow python3 commands on unix + const isPythonSystemAvailable = + (await which("python3", { nothrow: true })) !== null || + (await which("python", { nothrow: true })) !== null; + + // Restrict the webview to only load specific scripts + const nonce = getNonce(); + + return ` + + + + + + + + + + New Pico MicroPython Project + + + + + +
+
+ + +
+
+

Basic Settings

+
+
+
+ +
+
+ + +
+
+ + +
+ +
+
+ + + ${ + knownEnvironments && knownEnvironments.length > 0 + ? ` +
+ + + + +
+ ` + : "" + } + + ${ + process.platform === "darwin" || + process.platform === "win32" + ? ` + ${ + isPythonSystemAvailable + ? `
+ + +
` + : "" + } + +
+ + + +
` + : "" + } +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ + +
+
+ + + + `; + } +} diff --git a/src/webview/newProjectPanel.mts b/src/webview/newProjectPanel.mts index 85d9bb82..c6702710 100644 --- a/src/webview/newProjectPanel.mts +++ b/src/webview/newProjectPanel.mts @@ -119,7 +119,7 @@ interface SubmitMessageValue extends ImportProjectMessageValue { cppExceptions: boolean; } -interface WebviewMessage { +export interface WebviewMessage { command: string; value: object | string | SubmitMessageValue; } @@ -382,6 +382,8 @@ export class NewProjectPanel { const settings = Settings.getInstance(); if (settings === undefined) { + panel.dispose(); + // TODO: maybe add restart button void window.showErrorMessage( "Failed to load settings. Please restart VSCode." @@ -736,7 +738,7 @@ export class NewProjectPanel { // update webview void this._panel.webview.postMessage({ command: "changeLocation", - value: projectUri?.fsPath, + value: projectUri.fsPath, }); } } diff --git a/web/main.js b/web/main.js index 3d0d4699..ae9d334c 100644 --- a/web/main.js +++ b/web/main.js @@ -210,7 +210,7 @@ var exampleSupportedBoards = []; } } - // selected cmake version + // selected python version const pythonVersionRadio = document.getElementsByName('python-version-radio'); let pythonMode = null; let pythonPath = null; @@ -225,7 +225,7 @@ var exampleSupportedBoards = []; pythonMode = 1; } - // if cmake version is null or not a number, smaller than 0 or bigger than 3, set it to 0 + // if python version is null or not a number, smaller than 0 or bigger than 3, set it to 0 if (pythonMode === null || isNaN(pythonMode) || pythonMode < 0 || pythonMode > 3) { // TODO: first check if defaul is supported pythonMode = 0; diff --git a/web/mpy/main.js b/web/mpy/main.js new file mode 100644 index 00000000..56331315 --- /dev/null +++ b/web/mpy/main.js @@ -0,0 +1,183 @@ +"use strict"; + +const CMD_CHANGE_LOCATION = 'changeLocation'; +const CMD_SUBMIT = 'submit'; +const CMD_CANCEL = 'cancel'; +const CMD_SET_THEME = 'setTheme'; +const CMD_ERROR = 'error'; +const CMD_SUBMIT_DENIED = 'submitDenied'; + +var submitted = false; + +(function () { + const vscode = acquireVsCodeApi(); + + // needed so a element isn't hidden behind the navbar on scroll + const navbarOffsetHeight = document.getElementById('top-navbar').offsetHeight; + + // returns true if project name input is valid + function projectNameFormValidation(projectNameElement) { + if (typeof examples !== 'undefined') { + return true; + } + + const projectNameError = document.getElementById('inp-project-name-error'); + const projectName = projectNameElement.value; + + var invalidChars = /[\/:*?"<>| ]/; + // check for reserved names in Windows + var reservedNames = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i; + if (projectName.trim().length == 0 || invalidChars.test(projectName) || reservedNames.test(projectName)) { + projectNameError.hidden = false; + //projectNameElement.scrollIntoView({ behavior: "smooth" }); + window.scrollTo({ + top: projectNameElement.offsetTop - navbarOffsetHeight, + behavior: 'smooth' + }); + + return false; + } + + projectNameError.hidden = true; + return true; + } + + window.changeLocation = () => { + // Send a message back to the extension + vscode.postMessage({ + command: CMD_CHANGE_LOCATION, + value: null + }); + } + + window.cancelBtnClick = () => { + // close webview + vscode.postMessage({ + command: CMD_CANCEL, + value: null + }); + } + + window.submitBtnClick = () => { + /* Catch silly users who spam the submit button */ + if (submitted) { + console.error("already submitted"); + return; + } + submitted = true; + + // get all values of inputs + const projectNameElement = document.getElementById('inp-project-name'); + // if is project import then the project name element will not be rendered and does not exist in the DOM + const projectName = projectNameElement.value; + if (projectName !== undefined && !projectNameFormValidation(projectNameElement)) { + submitted = false; + return; + } + + // selected python version + const pythonVersionRadio = document.getElementsByName('python-version-radio'); + let pythonMode = null; + let pythonPath = null; + for (let i = 0; i < pythonVersionRadio.length; i++) { + if (pythonVersionRadio[i].checked) { + pythonMode = Number(pythonVersionRadio[i].value); + break; + } + } + if (pythonVersionRadio.length == 0) { + // default to python mode 0 == python ext version + pythonMode = 0; + } + + // if python version is null or not a number, smaller than 0 or bigger than 3, set it to 0 + if (pythonMode === null || isNaN(pythonMode) || pythonMode < 0 || pythonMode > 3) { + pythonMode = 0; + console.debug('Invalid python version value: ' + pythonMode.toString()); + vscode.postMessage({ + command: CMD_ERROR, + value: "Please select a valid python version." + }); + submitted = false; + + return; + } + if (pythonMode === 0) { + const pyenvKnownSel = document.getElementById("sel-pyenv-known"); + pythonPath = pyenvKnownSel.value; + } else if (pythonMode === 2) { + const files = document.getElementById('python-path-executable').files; + + if (files.length == 1) { + pythonPath = files[0].name; + } else { + console.debug("Please select a valid python executable file"); + vscode.postMessage({ + command: CMD_ERROR, + value: "Please select a valid python executable file." + }); + submitted = false; + + return; + } + } + + //post all data values to the extension + vscode.postMessage({ + command: CMD_SUBMIT, + value: { + projectName: projectName, + pythonMode: Number(pythonMode), + pythonPath: pythonPath + } + }); + } + + function _onMessage(event) { + // JSON data sent from the extension + const message = event.data; + + switch (message.command) { + case CMD_CHANGE_LOCATION: + // update UI + document.getElementById('inp-project-location').value = message.value; + break; + case CMD_SET_THEME: + console.log("set theme", message.theme); + // update UI + if (message.theme == "dark") { + // explicitly choose dark mode + localStorage.theme = 'dark' + document.body.classList.add('dark') + } else if (message.theme == "light") { + document.body.classList.remove('dark') + // explicitly choose light mode + localStorage.theme = 'light' + } + break; + case CMD_SUBMIT_DENIED: + submitted = false; + break; + default: + console.error('Unknown command: ' + message.command); + break; + } + } + + window.addEventListener("message", _onMessage); + + // add onclick event handlers to avoid inline handlers + document.getElementById('btn-change-project-location').addEventListener('click', changeLocation); + document.getElementById('btn-cancel').addEventListener('click', cancelBtnClick); + document.getElementById('btn-create').addEventListener('click', submitBtnClick); + + document.getElementById('inp-project-name').addEventListener('input', function () { + const projName = document.getElementById('inp-project-name').value; + console.log(`${projName} is now`); + // TODO: future examples stuff (maybe) + }); + + const pythonVersionRadio = document.getElementsByName('python-version-radio'); + if (pythonVersionRadio.length > 0) + pythonVersionRadio[0].checked = true; +}()); diff --git a/yarn.lock b/yarn.lock index 0ae4b2fc..1679c7e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -500,10 +500,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.17.x": - version: 18.17.19 - resolution: "@types/node@npm:18.17.19" - checksum: 10/7d83ee58e0c402d8dc8efa59151215cb8b233e5fa06630399779d46ae8a0c7cb458b36c0352fb8b06328dd446b4f02e28b9a81ceceb509ad938feec3d3c54a3d +"@types/node@npm:20.14.0": + version: 20.14.0 + resolution: "@types/node@npm:20.14.0" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10/49b332fbf8aee4dc4f61cc1f1f6e130632510f795dd7b274e55894516feaf4bec8a3d13ea764e2443e340a64ce9bbeb006d14513bf6ccdd4f21161eccc7f311e languageName: node linkType: hard @@ -521,10 +523,10 @@ __metadata: languageName: node linkType: hard -"@types/vscode@npm:^1.87.0": - version: 1.87.0 - resolution: "@types/vscode@npm:1.87.0" - checksum: 10/6f10df10d9fbe305ccd69ad5432357f05de267c72100a5f5aa54341c454b4b3505a98580d0308a5dcd9562268a22701c46e70b744080c50955d44ac415f60cf1 +"@types/vscode@npm:^1.92.0": + version: 1.92.0 + resolution: "@types/vscode@npm:1.92.0" + checksum: 10/395f3eeec345a9e2f85f82d4f7082433480a791ccd936a16d07946fa8bddfe0a399fd7341e4e124556abbb1f31920bf5974d1500444b3d5c5428510a8a4f9d87 languageName: node linkType: hard @@ -651,6 +653,13 @@ __metadata: languageName: node linkType: hard +"@vscode/python-extension@npm:^1.0.5": + version: 1.0.5 + resolution: "@vscode/python-extension@npm:1.0.5" + checksum: 10/aeaa1ee9275f8679aa745c3096a68e1952328b8c7de90e1665f369e321dafb3e1271d42c32e4f8d6a3c92f6cdbb4aaef22cc45101764a623900efc4f3dfd3270 + languageName: node + linkType: hard + "abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -2446,10 +2455,11 @@ __metadata: "@rollup/plugin-typescript": "npm:^11.1.6" "@types/adm-zip": "npm:^0.5.5" "@types/ini": "npm:^4.1.1" - "@types/node": "npm:18.17.x" + "@types/node": "npm:20.14.0" "@types/uuid": "npm:^10.0.0" - "@types/vscode": "npm:^1.87.0" + "@types/vscode": "npm:^1.92.0" "@types/which": "npm:^3.0.4" + "@vscode/python-extension": "npm:^1.0.5" adm-zip: "npm:^0.5.14 <0.5.15" eslint: "npm:^9.9.0" eslint-config-prettier: "npm:^9.1.0" @@ -2965,6 +2975,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10/0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd + languageName: node + linkType: hard + "undici@npm:^6.19.7": version: 6.19.7 resolution: "undici@npm:6.19.7"