Skip to content

Commit 31398ec

Browse files
author
Akos Kitta
committed
fix: sketchbook container building
Closes #1185 Signed-off-by: Akos Kitta <[email protected]>
1 parent 76f9f63 commit 31398ec

File tree

16 files changed

+343
-112
lines changed

16 files changed

+343
-112
lines changed

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ scripts/themes/tokens
2121
.env
2222
# content trace files for electron
2323
electron-app/traces
24+
# any Arduino LS generated log files
25+
inols*.log

Diff for: .vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"files.exclude": {
33
"**/lib": false
44
},
5+
"search.exclude": {
6+
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
7+
},
58
"typescript.tsdk": "node_modules/typescript/lib",
69
"editor.codeActionsOnSave": {
710
"source.fixAll.eslint": true

Diff for: arduino-ide-extension/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"dateformat": "^3.0.3",
7070
"deepmerge": "2.0.1",
7171
"electron-updater": "^4.6.5",
72+
"fast-json-stable-stringify": "^2.1.0",
7273
"fast-safe-stringify": "^2.1.1",
7374
"glob": "^7.1.6",
7475
"google-protobuf": "^3.20.1",

Diff for: arduino-ide-extension/src/node/sketches-service-impl.ts

+164-111
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as glob from 'glob';
77
import * as crypto from 'crypto';
88
import * as PQueue from 'p-queue';
99
import { ncp } from 'ncp';
10+
import { Mutable } from '@theia/core/lib/common/types';
1011
import URI from '@theia/core/lib/common/uri';
1112
import { ILogger } from '@theia/core/lib/common/logger';
1213
import { 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+
}

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/a_sketch/a_sketch.ino

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/bar++/bar++.ino

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/empty/.gitkeep

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/mismatchingName/MismatchingName.ino

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/nested_4/nested_3/nested_2/nested_1/nested_1.ino

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/nested_4/nested_3/nested_2/nested_2.ino

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/project1/CodeA/version1A/version1A.ino

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/project1/CodeA/version2A/version2A.ino

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/project1/CodeB/version1B/version1B.ino

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/__test_sketchbook__/project1/CodeB/version2B/version2B.pde

Whitespace-only changes.

Diff for: arduino-ide-extension/src/test/node/monitor-settings-utils.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describe('reconcileSettings', () => {
137137

138138
expect(reconciledSettings).not.to.have.property('setting4');
139139
});
140-
it('should reset non-value fields to those defiend in the default settings', async () => {
140+
it('should reset non-value fields to those defined in the default settings', async () => {
141141
const newSettings: DeepWriteable<PluggableMonitorSettings> = JSON.parse(
142142
JSON.stringify(defaultSettings)
143143
);

0 commit comments

Comments
 (0)