@@ -7,6 +7,7 @@ import * as glob from 'glob';
7
7
import * as crypto from 'crypto' ;
8
8
import * as PQueue from 'p-queue' ;
9
9
import { ncp } from 'ncp' ;
10
+ import { Mutable } from '@theia/core/lib/common/types' ;
10
11
import URI from '@theia/core/lib/common/uri' ;
11
12
import { ILogger } from '@theia/core/lib/common/logger' ;
12
13
import { FileUri } from '@theia/core/lib/node/file-uri' ;
@@ -84,108 +85,15 @@ export class SketchesServiceImpl
84
85
this . logger . warn ( `Could not derive sketchbook root from ${ uri } .` ) ;
85
86
return SketchContainer . create ( '' ) ;
86
87
}
87
- const exists = await this . exists ( root ) ;
88
- if ( ! exists ) {
88
+ const rootExists = await exists ( root ) ;
89
+ if ( ! rootExists ) {
89
90
this . logger . warn ( `Sketchbook root ${ root } does not exist.` ) ;
90
91
return SketchContainer . create ( '' ) ;
91
92
}
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' )
106
95
) ;
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 ) ;
189
97
}
190
98
191
99
private async root ( uri ?: string | undefined ) : Promise < string | undefined > {
@@ -488,7 +396,7 @@ export class SketchesServiceImpl
488
396
this . sketchSuffixIndex ++
489
397
) } `;
490
398
// 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 (
492
400
path . join ( sketchbookPath , sketchNameCandidate )
493
401
) ;
494
402
if ( ! sketchExists ) {
@@ -579,8 +487,8 @@ export class SketchesServiceImpl
579
487
{ destinationUri } : { destinationUri : string }
580
488
) : Promise < string > {
581
489
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 ) {
584
492
throw new Error ( `Sketch does not exist: ${ sketch } ` ) ;
585
493
}
586
494
// Nothing to do when source and destination are the same.
@@ -635,7 +543,7 @@ export class SketchesServiceImpl
635
543
const { client } = await this . coreClient ;
636
544
const archivePath = FileUri . fsPath ( destinationUri ) ;
637
545
// 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 ) ) {
639
547
await fs . unlink ( archivePath ) ;
640
548
}
641
549
const req = new ArchiveSketchRequest ( ) ;
@@ -680,15 +588,6 @@ export class SketchesServiceImpl
680
588
} ) ;
681
589
}
682
590
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
-
692
591
// Returns the default.ino from the settings or from default folder.
693
592
private async readSettings ( ) : Promise < Record < string , unknown > | undefined > {
694
593
const configDirUri = await this . envVariableServer . getConfigDirUri ( ) ;
@@ -837,3 +736,157 @@ function sketchIndexToLetters(num: number): string {
837
736
} while ( pow > 0 ) ;
838
737
return out ;
839
738
}
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