Skip to content

Commit 2aea3df

Browse files
author
Akos Kitta
committed
fix: enforce valid sketch folder name on Save as
Closes #1599 Co-authored-by: Akos Kitta <[email protected]> Co-authored-by: per1234 <[email protected]> Signed-off-by: Akos Kitta <[email protected]>
1 parent 658f117 commit 2aea3df

File tree

13 files changed

+731
-82
lines changed

13 files changed

+731
-82
lines changed

Diff for: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
344344
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
345345
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
346346
import { ConfigServiceClient } from './config/config-service-client';
347+
import { ValidateSketch } from './contributions/validate-sketch';
347348

348349
export default new ContainerModule((bind, unbind, isBound, rebind) => {
349350
// Commands and toolbar items
@@ -729,6 +730,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
729730
Contribution.configure(bind, UpdateIndexes);
730731
Contribution.configure(bind, InterfaceScale);
731732
Contribution.configure(bind, NewCloudSketch);
733+
Contribution.configure(bind, ValidateSketch);
732734

733735
bindContributionProvider(bind, StartupTaskProvider);
734736
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window

Diff for: arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-ske
3030
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
3131
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
3232
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
33-
import { Command, CommandRegistry, Contribution, URI } from './contribution';
33+
import {
34+
Command,
35+
CommandRegistry,
36+
Contribution,
37+
Sketch,
38+
URI,
39+
} from './contribution';
3440

3541
@injectable()
3642
export class NewCloudSketch extends Contribution {
@@ -234,14 +240,7 @@ export class NewCloudSketch extends Contribution {
234240
input
235241
);
236242
}
237-
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
238-
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
239-
return '';
240-
}
241-
return nls.localize(
242-
'arduino/newCloudSketch/invalidSketchName',
243-
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
244-
);
243+
return Sketch.validateCloudSketchFolderName(input) ?? '';
245244
},
246245
},
247246
this.labelProvider,

Diff for: arduino-ide-extension/src/browser/contributions/save-as-sketch.ts

+64-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as remote from '@theia/core/electron-shared/@electron/remote';
33
import * as dateFormat from 'dateformat';
44
import { ArduinoMenus } from '../menu/arduino-menus';
55
import {
6+
Sketch,
67
SketchContribution,
78
URI,
89
Command,
@@ -90,20 +91,9 @@ export class SaveAsSketch extends SketchContribution {
9091
: sketch.name
9192
);
9293
const defaultPath = await this.fileService.fsPath(defaultUri);
93-
const { filePath, canceled } = await remote.dialog.showSaveDialog(
94-
remote.getCurrentWindow(),
95-
{
96-
title: nls.localize(
97-
'arduino/sketch/saveFolderAs',
98-
'Save sketch folder as...'
99-
),
100-
defaultPath,
101-
}
94+
const destinationUri = await this.promptSketchFolderDestination(
95+
defaultPath
10296
);
103-
if (!filePath || canceled) {
104-
return false;
105-
}
106-
const destinationUri = await this.fileSystemExt.getUri(filePath);
10797
if (!destinationUri) {
10898
return false;
10999
}
@@ -133,6 +123,67 @@ export class SaveAsSketch extends SketchContribution {
133123
return !!workspaceUri;
134124
}
135125

126+
/**
127+
* Prompts for the new sketch folder name until a valid one is give,
128+
* then resolves with the destination sketch folder URI string,
129+
* or `undefined` if the operation was canceled.
130+
*/
131+
private async promptSketchFolderDestination(
132+
defaultPath: string
133+
): Promise<string | undefined> {
134+
let sketchFolderDestinationUri: string | undefined;
135+
while (!sketchFolderDestinationUri) {
136+
const { filePath } = await remote.dialog.showSaveDialog(
137+
remote.getCurrentWindow(),
138+
{
139+
title: nls.localize(
140+
'arduino/sketch/saveFolderAs',
141+
'Save sketch folder as...'
142+
),
143+
defaultPath,
144+
}
145+
);
146+
if (!filePath) {
147+
return undefined;
148+
}
149+
const destinationUri = await this.fileSystemExt.getUri(filePath);
150+
const sketchFolderName = new URI(destinationUri).path.base;
151+
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
152+
if (errorMessage) {
153+
const message = `
154+
${nls.localize(
155+
'arduino/sketch/invalidSketchFolderNameTitle',
156+
"Invalid sketch folder name: '{0}'",
157+
sketchFolderName
158+
)}
159+
160+
${errorMessage}
161+
162+
${nls.localize(
163+
'arduino/sketch/editInvalidSketchFolderName',
164+
'Do you want to try to save the sketch folder with a different name?'
165+
)}`.trim();
166+
defaultPath = filePath;
167+
const { response } = await remote.dialog.showMessageBox(
168+
remote.getCurrentWindow(),
169+
{
170+
message,
171+
buttons: [
172+
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
173+
nls.localize('vscode/extensionsUtils/yes', 'Yes'),
174+
],
175+
});
176+
// cancel
177+
if (response === 0) {
178+
return undefined;
179+
}
180+
} else {
181+
sketchFolderDestinationUri = destinationUri;
182+
}
183+
}
184+
return sketchFolderDestinationUri;
185+
}
186+
136187
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
137188
const widgets = this.applicationShell.widgets;
138189
const snapshots = new Map<string, object>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import * as remote from '@theia/core/electron-shared/@electron/remote';
2+
import { Dialog } from '@theia/core/lib/browser/dialogs';
3+
import { nls } from '@theia/core/lib/common/nls';
4+
import { Deferred } from '@theia/core/lib/common/promise-util';
5+
import { injectable } from '@theia/core/shared/inversify';
6+
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
7+
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
8+
import { Sketch, SketchContribution, URI } from './contribution';
9+
import { SaveAsSketch } from './save-as-sketch';
10+
11+
@injectable()
12+
export class ValidateSketch extends SketchContribution {
13+
override onReady(): void {
14+
this.validate();
15+
}
16+
17+
private async validate(): Promise<void> {
18+
const result = await this.promptFixActions();
19+
if (!result) {
20+
const yes = await this.prompt(
21+
nls.localize('arduino/validateSketch/abortFixTitle', 'Invalid sketch'),
22+
nls.localize(
23+
'arduino/validateSketch/abortFixMessage',
24+
"The sketch is still invalid. Do you want to fix the remaining problems? By clicking '{0}', a new sketch will open.",
25+
Dialog.NO
26+
),
27+
[Dialog.NO, Dialog.YES]
28+
);
29+
if (yes) {
30+
return this.validate();
31+
}
32+
const sketch = await this.sketchService.createNewSketch();
33+
this.workspaceService.open(new URI(sketch.uri), {
34+
preserveWindow: true,
35+
});
36+
}
37+
}
38+
39+
/**
40+
* Returns with an array of actions the user has to perform to fix the invalid sketch.
41+
*/
42+
private validateSketch(sketch: Sketch): FixAction[] {
43+
// sketch folder + main sketch file (requires `Save as...` and window reload)
44+
const sketchFolderName = new URI(sketch.uri).path.base;
45+
const sketchFolderNameError =
46+
Sketch.validateSketchFolderName(sketchFolderName);
47+
if (sketchFolderNameError) {
48+
return [
49+
{
50+
execute: async () => {
51+
const unknown =
52+
(await this.promptRenameSketch(sketch)) &&
53+
(await this.commandService.executeCommand(
54+
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
55+
<SaveAsSketch.Options>{
56+
markAsRecentlyOpened: true,
57+
openAfterMove: true,
58+
wipeOriginal: true,
59+
}
60+
));
61+
return !!unknown;
62+
},
63+
},
64+
];
65+
}
66+
67+
// sketch code files (does not require window reload)
68+
return Sketch.uris(sketch)
69+
.filter((uri) => uri !== sketch.mainFileUri)
70+
.map((uri) => new URI(uri))
71+
.filter((uri) => Sketch.Extensions.CODE_FILES.includes(uri.path.ext))
72+
.map((uri) => ({
73+
uri,
74+
error: Sketch.validateSketchFolderName(uri.path.name),
75+
}))
76+
.filter(({ error }) => Boolean(error))
77+
.map(({ uri }) => ({
78+
execute: async () => {
79+
const unknown =
80+
(await this.promptRenameSketchFile(uri)) &&
81+
(await this.commandService.executeCommand(
82+
WorkspaceCommands.FILE_RENAME.id,
83+
uri
84+
));
85+
return !!unknown;
86+
},
87+
}));
88+
}
89+
90+
private async currentSketch(): Promise<Sketch> {
91+
const sketch = this.sketchServiceClient.tryGetCurrentSketch();
92+
if (CurrentSketch.isValid(sketch)) {
93+
return sketch;
94+
}
95+
const deferred = new Deferred<Sketch>();
96+
const disposable = this.sketchServiceClient.onCurrentSketchDidChange(
97+
(sketch) => {
98+
if (CurrentSketch.isValid(sketch)) {
99+
disposable.dispose();
100+
deferred.resolve(sketch);
101+
}
102+
}
103+
);
104+
return deferred.promise;
105+
}
106+
107+
private async promptFixActions(): Promise<boolean> {
108+
const sketch = await this.currentSketch();
109+
const fixActions = this.validateSketch(sketch);
110+
for (const fixAction of fixActions) {
111+
const result = await fixAction.execute();
112+
if (!result) {
113+
return false;
114+
}
115+
}
116+
return true;
117+
}
118+
119+
private async promptRenameSketch(sketch: Sketch): Promise<boolean> {
120+
return this.prompt(
121+
nls.localize(
122+
'arduino/validateSketch/renameSketchFolderTitle',
123+
'Invalid sketch name'
124+
),
125+
nls.localize(
126+
'arduino/validateSketch/renameSketchFolderMessage',
127+
"The sketch '{0}' cannot be used. Sketch names must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters. To get rid of this message, rename the sketch. Do you want to rename the sketch now?",
128+
sketch.name
129+
)
130+
);
131+
}
132+
133+
private async promptRenameSketchFile(uri: URI): Promise<boolean> {
134+
return this.prompt(
135+
nls.localize(
136+
'arduino/validateSketch/renameSketchFileTitle',
137+
'Invalid sketch filename'
138+
),
139+
nls.localize(
140+
'arduino/validateSketch/renameSketchFileMessage',
141+
"The sketch file '{0}' cannot be used. Sketch filenames must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters without the file extension. To get rid of this message, rename the sketch file. Do you want to rename the sketch file now?",
142+
uri.path.base
143+
)
144+
);
145+
}
146+
147+
private async prompt(
148+
title: string,
149+
message: string,
150+
buttons: string[] = [Dialog.CANCEL, Dialog.OK]
151+
): Promise<boolean> {
152+
const { response } = await remote.dialog.showMessageBox(
153+
remote.getCurrentWindow(),
154+
{
155+
title,
156+
message,
157+
type: 'warning',
158+
buttons,
159+
}
160+
);
161+
// cancel
162+
if (response === 0) {
163+
return false;
164+
}
165+
return true;
166+
}
167+
}
168+
169+
interface FixAction {
170+
execute(): Promise<boolean>;
171+
}

Diff for: arduino-ide-extension/src/browser/contributions/verify-sketch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface VerifySketchParams {
2727
}
2828

2929
/**
30-
* - `"idle"` when neither verify, not upload is running,
30+
* - `"idle"` when neither verify, nor upload is running,
3131
* - `"explicit-verify"` when only verify is running triggered by the user, and
3232
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
3333
*/

0 commit comments

Comments
 (0)