@@ -7,6 +7,7 @@ import * as glob from 'glob';
77import * as crypto from 'crypto' ;
88import * as PQueue from 'p-queue' ;
99import { ncp } from 'ncp' ;
10+ import { Mutable } from '@theia/core/lib/common/types' ;
1011import URI from '@theia/core/lib/common/uri' ;
1112import { ILogger } from '@theia/core/lib/common/logger' ;
1213import { FileUri } from '@theia/core/lib/node/file-uri' ;
@@ -84,108 +85,15 @@ export class SketchesServiceImpl
8485 this . logger . warn ( `Could not derive sketchbook root from ${ uri } .` ) ;
8586 return SketchContainer . create ( '' ) ;
8687 }
87- const exists = await this . exists ( root ) ;
88- if ( ! exists ) {
88+ const rootExists = await exists ( root ) ;
89+ if ( ! rootExists ) {
8990 this . logger . warn ( `Sketchbook root ${ root } does not exist.` ) ;
9091 return SketchContainer . create ( '' ) ;
9192 }
92- const pathToAllSketchFiles = await new Promise < string [ ] > (
93- ( resolve , reject ) => {
94- glob (
95- '/!(libraries|hardware)/**/*.{ino,pde}' ,
96- { root } ,
97- ( error , results ) => {
98- if ( error ) {
99- reject ( error ) ;
100- } else {
101- resolve ( results ) ;
102- }
103- }
104- ) ;
105- }
93+ const container = < Mutable < SketchContainer > > (
94+ SketchContainer . create ( uri ? path . basename ( root ) : 'Sketchbook' )
10695 ) ;
107- // Sort by path length to filter out nested sketches, such as the `Nested_folder` inside the `Folder` sketch.
108- //
109- // `directories#user`
110- // |
111- // +--Folder
112- // |
113- // +--Folder.ino
114- // |
115- // +--Nested_folder
116- // |
117- // +--Nested_folder.ino
118- pathToAllSketchFiles . sort ( ( left , right ) => left . length - right . length ) ;
119- const container = SketchContainer . create (
120- uri ? path . basename ( root ) : 'Sketchbook'
121- ) ;
122- const getOrCreateChildContainer = (
123- parent : SketchContainer ,
124- segments : string [ ]
125- ) => {
126- if ( segments . length === 1 ) {
127- throw new Error (
128- `Expected at least two segments relative path: ['ExampleSketchName', 'ExampleSketchName.{ino,pde}]. Was: ${ segments } `
129- ) ;
130- }
131- if ( segments . length === 2 ) {
132- return parent ;
133- }
134- const label = segments [ 0 ] ;
135- const existingSketch = parent . sketches . find (
136- ( sketch ) => sketch . name === label
137- ) ;
138- if ( existingSketch ) {
139- // If the container has a sketch with the same label, it cannot have a child container.
140- // See above example about how to ignore nested sketches.
141- return undefined ;
142- }
143- let child = parent . children . find ( ( child ) => child . label === label ) ;
144- if ( ! child ) {
145- child = SketchContainer . create ( label ) ;
146- parent . children . push ( child ) ;
147- }
148- return child ;
149- } ;
150- for ( const pathToSketchFile of pathToAllSketchFiles ) {
151- const relative = path . relative ( root , pathToSketchFile ) ;
152- if ( ! relative ) {
153- this . logger . warn (
154- `Could not determine relative sketch path from the root <${ root } > to the sketch <${ pathToSketchFile } >. Skipping. Relative path was: ${ relative } `
155- ) ;
156- continue ;
157- }
158- const segments = relative . split ( path . sep ) ;
159- if ( segments . length < 2 ) {
160- // folder name, and sketch name.
161- this . logger . warn (
162- `Expected at least one segment relative path from the root <${ root } > to the sketch <${ pathToSketchFile } >. Skipping. Segments were: ${ segments } .`
163- ) ;
164- continue ;
165- }
166- // the folder name and the sketch name must match. For example, `Foo/foo.ino` is invalid.
167- // drop the folder name from the sketch name, if `.ino` or `.pde` remains, it's valid
168- const sketchName = segments [ segments . length - 2 ] ;
169- const sketchFilename = segments [ segments . length - 1 ] ;
170- const sketchFileExtension = segments [ segments . length - 1 ] . replace (
171- new RegExp ( escapeRegExpCharacters ( sketchName ) ) ,
172- ''
173- ) ;
174- if ( sketchFileExtension !== '.ino' && sketchFileExtension !== '.pde' ) {
175- this . logger . warn (
176- `Mismatching sketch file <${ sketchFilename } > and sketch folder name <${ sketchName } >. Skipping`
177- ) ;
178- continue ;
179- }
180- const child = getOrCreateChildContainer ( container , segments ) ;
181- if ( child ) {
182- child . sketches . push ( {
183- name : sketchName ,
184- uri : FileUri . create ( path . dirname ( pathToSketchFile ) ) . toString ( ) ,
185- } ) ;
186- }
187- }
188- return container ;
96+ return discoverSketches ( root , container , this . logger ) ;
18997 }
19098
19199 private async root ( uri ?: string | undefined ) : Promise < string | undefined > {
@@ -488,7 +396,7 @@ export class SketchesServiceImpl
488396 this . sketchSuffixIndex ++
489397 ) } `;
490398 // Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
491- const sketchExists = await this . exists (
399+ const sketchExists = await exists (
492400 path . join ( sketchbookPath , sketchNameCandidate )
493401 ) ;
494402 if ( ! sketchExists ) {
@@ -579,8 +487,8 @@ export class SketchesServiceImpl
579487 { destinationUri } : { destinationUri : string }
580488 ) : Promise < string > {
581489 const source = FileUri . fsPath ( sketch . uri ) ;
582- const exists = await this . exists ( source ) ;
583- if ( ! exists ) {
490+ const sketchExists = await exists ( source ) ;
491+ if ( ! sketchExists ) {
584492 throw new Error ( `Sketch does not exist: ${ sketch } ` ) ;
585493 }
586494 // Nothing to do when source and destination are the same.
@@ -635,7 +543,7 @@ export class SketchesServiceImpl
635543 const { client } = await this . coreClient ;
636544 const archivePath = FileUri . fsPath ( destinationUri ) ;
637545 // The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
638- if ( await this . exists ( archivePath ) ) {
546+ if ( await exists ( archivePath ) ) {
639547 await fs . unlink ( archivePath ) ;
640548 }
641549 const req = new ArchiveSketchRequest ( ) ;
@@ -680,15 +588,6 @@ export class SketchesServiceImpl
680588 } ) ;
681589 }
682590
683- private async exists ( pathLike : string ) : Promise < boolean > {
684- try {
685- await fs . access ( pathLike , constants . R_OK ) ;
686- return true ;
687- } catch {
688- return false ;
689- }
690- }
691- 692591 // Returns the default.ino from the settings or from default folder.
693592 private async readSettings ( ) : Promise < Record < string , unknown > | undefined > {
694593 const configDirUri = await this . envVariableServer . getConfigDirUri ( ) ;
@@ -837,3 +736,157 @@ function sketchIndexToLetters(num: number): string {
837736 } while ( pow > 0 ) ;
838737 return out ;
839738}
739+ 740+ async function exists ( pathLike : string ) : Promise < boolean > {
741+ try {
742+ await fs . access ( pathLike , constants . R_OK ) ;
743+ return true ;
744+ } catch {
745+ return false ;
746+ }
747+ }
748+ 749+ /**
750+ * Recursively discovers sketches in the `root` folder give by the filesystem path.
751+ * Missing `root` must be handled by callers. This function expects an accessible `root` directory.
752+ */
753+ export async function discoverSketches (
754+ root : string ,
755+ container : Mutable < SketchContainer > ,
756+ logger ?: ILogger
757+ ) : Promise < SketchContainer > {
758+ const pathToAllSketchFiles = await globSketches (
759+ '/!(libraries|hardware)/**/*.{ino,pde}' ,
760+ root
761+ ) ;
762+ // if no match try to glob the sketchbook as a sketch folder
763+ if ( ! pathToAllSketchFiles . length ) {
764+ pathToAllSketchFiles . push ( ...( await globSketches ( '/*.{ino,pde}' , root ) ) ) ;
765+ }
766+ 767+ // Sort by path length to filter out nested sketches, such as the `Nested_folder` inside the `Folder` sketch.
768+ //
769+ // `directories#user`
770+ // |
771+ // +--Folder
772+ // |
773+ // +--Folder.ino
774+ // |
775+ // +--Nested_folder
776+ // |
777+ // +--Nested_folder.ino
778+ pathToAllSketchFiles . sort ( ( left , right ) => left . length - right . length ) ;
779+ const getOrCreateChildContainer = (
780+ container : SketchContainer ,
781+ segments : string [ ]
782+ ) : SketchContainer => {
783+ // the sketchbook is a sketch folder
784+ if ( segments . length === 1 ) {
785+ return container ;
786+ }
787+ const segmentsCopy = segments . slice ( ) ;
788+ let currentContainer = container ;
789+ while ( segmentsCopy . length > 2 ) {
790+ const currentSegment = segmentsCopy . shift ( ) ;
791+ if ( ! currentSegment ) {
792+ throw new Error (
793+ `'currentSegment' was not set when processing sketch container: ${ JSON . stringify (
794+ container
795+ ) } , original segments: ${ JSON . stringify (
796+ segments
797+ ) } , current container: ${ JSON . stringify (
798+ currentContainer
799+ ) } , current working segments: ${ JSON . stringify ( segmentsCopy ) } `
800+ ) ;
801+ }
802+ let childContainer = currentContainer . children . find (
803+ ( childContainer ) => childContainer . label === currentSegment
804+ ) ;
805+ if ( ! childContainer ) {
806+ childContainer = SketchContainer . create ( currentSegment ) ;
807+ currentContainer . children . push ( childContainer ) ;
808+ }
809+ currentContainer = childContainer ;
810+ }
811+ if ( segmentsCopy . length !== 2 ) {
812+ throw new Error (
813+ `Expected exactly two segments. A sketch folder name and the main sketch file name. For example, ['ExampleSketchName', 'ExampleSketchName.{ino,pde}]. Was: ${ segmentsCopy } `
814+ ) ;
815+ }
816+ return currentContainer ;
817+ } ;
818+ 819+ // If the container has a sketch with the same name, it cannot have a child container.
820+ // See above example about how to ignore nested sketches.
821+ const prune = (
822+ container : Mutable < SketchContainer >
823+ ) : Mutable < SketchContainer > => {
824+ for ( const sketch of container . sketches ) {
825+ const childContainerIndex = container . children . findIndex (
826+ ( childContainer ) => childContainer . label === sketch . name
827+ ) ;
828+ if ( childContainerIndex >= 0 ) {
829+ container . children . splice ( childContainerIndex , 1 ) ;
830+ }
831+ }
832+ container . children . forEach ( prune ) ;
833+ return container ;
834+ } ;
835+ 836+ for ( const pathToSketchFile of pathToAllSketchFiles ) {
837+ const relative = path . relative ( root , pathToSketchFile ) ;
838+ if ( ! relative ) {
839+ logger ?. warn (
840+ `Could not determine relative sketch path from the root <${ root } > to the sketch <${ pathToSketchFile } >. Skipping. Relative path was: ${ relative } `
841+ ) ;
842+ continue ;
843+ }
844+ const segments = relative . split ( path . sep ) ;
845+ let sketchName : string ;
846+ let sketchFilename : string ;
847+ if ( ! segments . length ) {
848+ // no segments.
849+ logger ?. warn (
850+ `Expected at least one segment relative path ${ relative } from the root <${ root } > to the sketch <${ pathToSketchFile } >. Skipping.`
851+ ) ;
852+ continue ;
853+ } else if ( segments . length === 1 ) {
854+ // The sketchbook root is a sketch folder
855+ sketchName = path . basename ( root ) ;
856+ sketchFilename = segments [ 0 ] ;
857+ } else {
858+ // the folder name and the sketch name must match. For example, `Foo/foo.ino` is invalid.
859+ // drop the folder name from the sketch name, if `.ino` or `.pde` remains, it's valid
860+ sketchName = segments [ segments . length - 2 ] ;
861+ sketchFilename = segments [ segments . length - 1 ] ;
862+ }
863+ const sketchFileExtension = segments [ segments . length - 1 ] . replace (
864+ new RegExp ( escapeRegExpCharacters ( sketchName ) ) ,
865+ ''
866+ ) ;
867+ if ( sketchFileExtension !== '.ino' && sketchFileExtension !== '.pde' ) {
868+ logger ?. warn (
869+ `Mismatching sketch file <${ sketchFilename } > and sketch folder name <${ sketchName } >. Skipping`
870+ ) ;
871+ continue ;
872+ }
873+ const child = getOrCreateChildContainer ( container , segments ) ;
874+ child . sketches . push ( {
875+ name : sketchName ,
876+ uri : FileUri . create ( path . dirname ( pathToSketchFile ) ) . toString ( ) ,
877+ } ) ;
878+ }
879+ return prune ( container ) ;
880+ }
881+ 882+ async function globSketches ( pattern : string , root : string ) : Promise < string [ ] > {
883+ return new Promise < string [ ] > ( ( resolve , reject ) => {
884+ glob ( pattern , { root } , ( error , results ) => {
885+ if ( error ) {
886+ reject ( error ) ;
887+ } else {
888+ resolve ( results ) ;
889+ }
890+ } ) ;
891+ } ) ;
892+ }
0 commit comments