From a0438e6ea00d7d7d7128408f8730979640144bf5 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 15 Jul 2026 12:28:23 -0700 Subject: [PATCH] Add Python formatter button --- index.html | 2 ++ js/script.js | 79 +++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 7 +++++ package.json | 1 + 4 files changed, 89 insertions(+) diff --git a/index.html b/index.html index 781dabc..181d434 100644 --- a/index.html +++ b/index.html @@ -64,6 +64,7 @@
+
@@ -85,6 +86,7 @@ diff --git a/js/script.js b/js/script.js index 64906cf..272e591 100644 --- a/js/script.js +++ b/js/script.js @@ -11,6 +11,7 @@ import { xml } from "@codemirror/lang-xml"; import { markdown } from "@codemirror/lang-markdown"; import { syntaxHighlighting, indentUnit } from "@codemirror/language"; import { classHighlighter } from "@lezer/highlight"; +import initRuffFormatter, { PositionEncoding, Workspace } from "@astral-sh/ruff-wasm-web"; import { circuitpythonHighlight } from "./common/circuitpython_highlight.js"; import { getFileIcon } from "./common/file_dialog.js"; @@ -40,6 +41,9 @@ let workflow = null; let unchanged = 0; let connectionPromise = null; let debugMessageAnsi = null; +let formatterInitPromise = null; +let formatterWorkspace = null; +let currentEditorPath = null; const btnRestart = document.querySelector('.btn-restart'); const btnHalt = document.querySelector('.btn-halt'); @@ -48,6 +52,7 @@ const btnClear = document.querySelector('.btn-clear'); const btnConnect = document.querySelectorAll('.btn-connect'); const btnNew = document.querySelectorAll('.btn-new'); const btnOpen = document.querySelectorAll('.btn-open'); +const btnFormat = document.querySelectorAll('.btn-format'); const btnSave = document.querySelectorAll('.btn-save'); const btnSaveAs = document.querySelectorAll('.btn-save-as'); const btnSaveRun = document.querySelectorAll('.btn-save-run'); @@ -112,6 +117,21 @@ function getFileExtensionFromPath(path) { return base.split(".").pop().toLowerCase(); } +function isPythonFilePath(path) { + if (path === null || path === undefined) { + return true; + } + return getFileExtensionFromPath(path) === "py"; +} + +function updateFormatterButtonState(path) { + const enabled = isPythonFilePath(path); + btnFormat.forEach((element) => { + element.disabled = !enabled; + element.setAttribute("aria-disabled", enabled ? "false" : "true"); + }); +} + // Pick the CodeMirror language extensions to use for a given file path. // Returns an array so callers can spread it directly into the editor's // extension list. New (untitled) docs default to Python so the editor @@ -211,6 +231,15 @@ btnOpen.forEach((element) => { }); }); +// Format Link/Button (Mobile and Desktop Layout) +btnFormat.forEach((element) => { + element.addEventListener('click', async function(e) { + e.preventDefault(); + e.stopPropagation(); + await formatCurrentFile(); + }); +}); + // Save Link/Button (Mobile and Desktop Layout) btnSave.forEach((element) => { element.addEventListener('click', async function(e) { @@ -301,6 +330,53 @@ async function openFile() { } } +async function initFormatter() { + if (!formatterInitPromise) { + formatterInitPromise = initRuffFormatter().then(() => { + formatterWorkspace = new Workspace( + { + "line-length": 88, + "indent-width": 4, + format: { + "indent-style": "space", + "quote-style": "double", + }, + }, + PositionEncoding.Utf16, + ); + }); + } + return formatterInitPromise; +} + +async function formatCurrentFile() { + if (!isPythonFilePath(currentEditorPath)) { + await showMessage("Format is only available for Python files."); + return false; + } + + try { + await initFormatter(); + const contents = editor.state.doc.sliceString(0); + const formatted = formatterWorkspace.format(contents); + + if (formatted !== contents) { + editor.dispatch({ + changes: { + from: 0, + to: editor.state.doc.length, + insert: formatted, + }, + }); + } + return true; + } catch (e) { + console.error("Could not format Python file", e); + await showMessage(`Could not format Python file. ${e.message || e}`); + return false; + } +} + async function saveFile() { if (await checkConnected()) { await workflow.saveFile(); @@ -436,6 +512,9 @@ async function checkReadOnly() { /* Update the filename and update the UI */ function setFilename(path) { + currentEditorPath = path; + updateFormatterButtonState(path); + // Refresh the CodeMirror language plugin whenever the active file // changes — this is the single chokepoint that all filename // changes route through (Open File, New File, Save As, backend diff --git a/package-lock.json b/package-lock.json index 0c4b853..fdd68eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@adafruit/ble-file-transfer-js": "adafruit/ble-file-transfer-js#1.0.5", "@adafruit/circuitpython-repl-js": "adafruit/circuitpython-repl-js#3.3.0", + "@astral-sh/ruff-wasm-web": "^0.15.21", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-javascript": "^6.2.5", @@ -49,6 +50,12 @@ "integrity": "sha512-/pWFRIAwwwYXbHfNPw0v6YjX09EjC2yEXAZ98RnVhiQIGh6+N6W9hNKRyCPOBYJvIVXftlyLDHzHc2paV+Oefg==", "license": "MIT" }, + "node_modules/@astral-sh/ruff-wasm-web": { + "version": "0.15.21", + "resolved": "https://registry.npmjs.org/@astral-sh/ruff-wasm-web/-/ruff-wasm-web-0.15.21.tgz", + "integrity": "sha512-EcMPUkDZGAJ0vUAe0hgBWYojGtQAJcz92Nyrsj2P60KB8Bc5eNDuGEGtFbhrRXO/I9pFGG5NNPAJL/uVmyMNig==", + "license": "MIT" + }, "node_modules/@codemirror/autocomplete": { "version": "6.20.1", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", diff --git a/package.json b/package.json index dbc76b0..cb6a9be 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@adafruit/ble-file-transfer-js": "adafruit/ble-file-transfer-js#1.0.5", "@adafruit/circuitpython-repl-js": "adafruit/circuitpython-repl-js#3.3.0", + "@astral-sh/ruff-wasm-web": "^0.15.21", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-javascript": "^6.2.5",