@@ -11,6 +11,7 @@ import { xml } from "@codemirror/lang-xml";
1111import { markdown } from "@codemirror/lang-markdown" ;
1212import { syntaxHighlighting , indentUnit } from "@codemirror/language" ;
1313import { classHighlighter } from "@lezer/highlight" ;
14+ import initRuffFormatter , { PositionEncoding , Workspace } from "@astral-sh/ruff-wasm-web" ;
1415import { circuitpythonHighlight } from "./common/circuitpython_highlight.js" ;
1516import { getFileIcon } from "./common/file_dialog.js" ;
1617
@@ -40,6 +41,9 @@ let workflow = null;
4041let unchanged = 0 ;
4142let connectionPromise = null ;
4243let debugMessageAnsi = null ;
44+ let formatterInitPromise = null ;
45+ let formatterWorkspace = null ;
46+ let currentEditorPath = null ;
4347
4448const btnRestart = document . querySelector ( '.btn-restart' ) ;
4549const btnHalt = document . querySelector ( '.btn-halt' ) ;
@@ -48,6 +52,7 @@ const btnClear = document.querySelector('.btn-clear');
4852const btnConnect = document . querySelectorAll ( '.btn-connect' ) ;
4953const btnNew = document . querySelectorAll ( '.btn-new' ) ;
5054const btnOpen = document . querySelectorAll ( '.btn-open' ) ;
55+ const btnFormat = document . querySelectorAll ( '.btn-format' ) ;
5156const btnSave = document . querySelectorAll ( '.btn-save' ) ;
5257const btnSaveAs = document . querySelectorAll ( '.btn-save-as' ) ;
5358const btnSaveRun = document . querySelectorAll ( '.btn-save-run' ) ;
@@ -112,6 +117,21 @@ function getFileExtensionFromPath(path) {
112117 return base . split ( "." ) . pop ( ) . toLowerCase ( ) ;
113118}
114119
120+ function isPythonFilePath ( path ) {
121+ if ( path === null || path === undefined ) {
122+ return true ;
123+ }
124+ return getFileExtensionFromPath ( path ) === "py" ;
125+ }
126+
127+ function updateFormatterButtonState ( path ) {
128+ const enabled = isPythonFilePath ( path ) ;
129+ btnFormat . forEach ( ( element ) => {
130+ element . disabled = ! enabled ;
131+ element . setAttribute ( "aria-disabled" , enabled ? "false" : "true" ) ;
132+ } ) ;
133+ }
134+
115135// Pick the CodeMirror language extensions to use for a given file path.
116136// Returns an array so callers can spread it directly into the editor's
117137// extension list. New (untitled) docs default to Python so the editor
@@ -211,6 +231,15 @@ btnOpen.forEach((element) => {
211231 } ) ;
212232} ) ;
213233
234+ // Format Link/Button (Mobile and Desktop Layout)
235+ btnFormat . forEach ( ( element ) => {
236+ element . addEventListener ( 'click' , async function ( e ) {
237+ e . preventDefault ( ) ;
238+ e . stopPropagation ( ) ;
239+ await formatCurrentFile ( ) ;
240+ } ) ;
241+ } ) ;
242+
214243// Save Link/Button (Mobile and Desktop Layout)
215244btnSave . forEach ( ( element ) => {
216245 element . addEventListener ( 'click' , async function ( e ) {
@@ -301,6 +330,53 @@ async function openFile() {
301330 }
302331}
303332
333+ async function initFormatter ( ) {
334+ if ( ! formatterInitPromise ) {
335+ formatterInitPromise = initRuffFormatter ( ) . then ( ( ) => {
336+ formatterWorkspace = new Workspace (
337+ {
338+ "line-length" : 88 ,
339+ "indent-width" : 4 ,
340+ format : {
341+ "indent-style" : "space" ,
342+ "quote-style" : "double" ,
343+ } ,
344+ } ,
345+ PositionEncoding . Utf16 ,
346+ ) ;
347+ } ) ;
348+ }
349+ return formatterInitPromise ;
350+ }
351+
352+ async function formatCurrentFile ( ) {
353+ if ( ! isPythonFilePath ( currentEditorPath ) ) {
354+ await showMessage ( "Format is only available for Python files." ) ;
355+ return false ;
356+ }
357+
358+ try {
359+ await initFormatter ( ) ;
360+ const contents = editor . state . doc . sliceString ( 0 ) ;
361+ const formatted = formatterWorkspace . format ( contents ) ;
362+
363+ if ( formatted !== contents ) {
364+ editor . dispatch ( {
365+ changes : {
366+ from : 0 ,
367+ to : editor . state . doc . length ,
368+ insert : formatted ,
369+ } ,
370+ } ) ;
371+ }
372+ return true ;
373+ } catch ( e ) {
374+ console . error ( "Could not format Python file" , e ) ;
375+ await showMessage ( `Could not format Python file. ${ e . message || e } ` ) ;
376+ return false ;
377+ }
378+ }
379+
304380async function saveFile ( ) {
305381 if ( await checkConnected ( ) ) {
306382 await workflow . saveFile ( ) ;
@@ -436,6 +512,9 @@ async function checkReadOnly() {
436512
437513/* Update the filename and update the UI */
438514function setFilename ( path ) {
515+ currentEditorPath = path ;
516+ updateFormatterButtonState ( path ) ;
517+
439518 // Refresh the CodeMirror language plugin whenever the active file
440519 // changes — this is the single chokepoint that all filename
441520 // changes route through (Open File, New File, Save As, backend
0 commit comments