Skip to content

Commit 413a1fd

Browse files
author
Akos Kitta
committed
feat: Create remote sketch
Closes #1580 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 0d05509 commit 413a1fd

16 files changed

+572
-65
lines changed

arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+8
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ import { UserFields } from './contributions/user-fields';
335335
import { UpdateIndexes } from './contributions/update-indexes';
336336
import { InterfaceScale } from './contributions/interface-scale';
337337
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
338+
import { NewCloudSketch } from './contributions/new-cloud-sketch';
339+
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
338340

339341
const registerArduinoThemes = () => {
340342
const themes: MonacoThemeJson[] = [
@@ -751,6 +753,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
751753
Contribution.configure(bind, DeleteSketch);
752754
Contribution.configure(bind, UpdateIndexes);
753755
Contribution.configure(bind, InterfaceScale);
756+
Contribution.configure(bind, NewCloudSketch);
754757

755758
bindContributionProvider(bind, StartupTaskProvider);
756759
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@@ -905,6 +908,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
905908
id: 'arduino-sketchbook-widget',
906909
createWidget: () => container.get(SketchbookWidget),
907910
}));
911+
bind(SketchbookCompositeWidget).toSelf();
912+
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
913+
id: 'sketchbook-composite-widget',
914+
createWidget: () => ctx.container.get(SketchbookCompositeWidget),
915+
}));
908916

909917
bind(CloudSketchbookWidget).toSelf();
910918
rebind(SketchbookWidget).toService(CloudSketchbookWidget);

arduino-ide-extension/src/browser/contributions/close.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class Close extends SketchContribution {
6565
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
6666
commandId: Close.Commands.CLOSE.id,
6767
label: nls.localize('vscode/editor.contribution/close', 'Close'),
68-
order: '5',
68+
order: '6',
6969
});
7070
}
7171

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { MenuModelRegistry } from '@theia/core';
2+
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
3+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
4+
import { nls } from '@theia/core/lib/common/nls';
5+
import { inject, injectable } from '@theia/core/shared/inversify';
6+
import { MainMenuManager } from '../../common/main-menu-manager';
7+
import type { AuthenticationSession } from '../../node/auth/types';
8+
import { AuthenticationClientService } from '../auth/authentication-client-service';
9+
import { CreateApi } from '../create/create-api';
10+
import { CreateUri } from '../create/create-uri';
11+
import { Create } from '../create/typings';
12+
import { ArduinoMenus } from '../menu/arduino-menus';
13+
import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog';
14+
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
15+
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
16+
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
17+
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
18+
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
19+
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
20+
import { Command, CommandRegistry, Contribution, URI } from './contribution';
21+
22+
@injectable()
23+
export class NewCloudSketch extends Contribution {
24+
@inject(CreateApi)
25+
private readonly createApi: CreateApi;
26+
@inject(SketchbookWidgetContribution)
27+
private readonly widgetContribution: SketchbookWidgetContribution;
28+
@inject(AuthenticationClientService)
29+
private readonly authenticationService: AuthenticationClientService;
30+
@inject(MainMenuManager)
31+
private readonly mainMenuManager: MainMenuManager;
32+
33+
private _session: AuthenticationSession | undefined;
34+
private readonly toDispose = new DisposableCollection();
35+
36+
override onReady(): void {
37+
this.toDispose.push(
38+
this.authenticationService.onSessionDidChange((session) => {
39+
const oldSession = this._session;
40+
this._session = session;
41+
if (!!oldSession !== !!this._session) {
42+
this.mainMenuManager.update();
43+
}
44+
})
45+
);
46+
this._session = this.authenticationService.session;
47+
if (this._session) {
48+
this.mainMenuManager.update();
49+
}
50+
}
51+
52+
onStop(): void {
53+
this.toDispose.dispose();
54+
}
55+
56+
override registerCommands(registry: CommandRegistry): void {
57+
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
58+
execute: () => this.createNewSketch(),
59+
isEnabled: () => !!this._session,
60+
});
61+
}
62+
63+
override registerMenus(registry: MenuModelRegistry): void {
64+
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
65+
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
66+
label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
67+
order: '1',
68+
});
69+
}
70+
71+
private async createNewSketch(
72+
initialValue?: string | undefined
73+
): Promise<URI | undefined> {
74+
const widget = await this.widgetContribution.widget;
75+
const treeModel = this.treeModelFrom(widget);
76+
if (!treeModel) {
77+
return undefined;
78+
}
79+
const rootNode = CompositeTreeNode.is(treeModel.root)
80+
? treeModel.root
81+
: undefined;
82+
if (!rootNode) {
83+
return undefined;
84+
}
85+
86+
const newSketchName = await this.newSketchName(rootNode, initialValue);
87+
if (!newSketchName) {
88+
return undefined;
89+
}
90+
let result: Create.Sketch | undefined | 'conflict';
91+
try {
92+
result = await this.createApi.createSketch(newSketchName);
93+
} catch (err) {
94+
if (isConflict(err)) {
95+
result = 'conflict';
96+
} else {
97+
throw err;
98+
}
99+
} finally {
100+
if (result) {
101+
await treeModel.updateRoot();
102+
await treeModel.refresh();
103+
}
104+
}
105+
106+
if (result === 'conflict') {
107+
return this.createNewSketch(newSketchName);
108+
}
109+
110+
if (result) {
111+
const newSketch = result;
112+
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
113+
this.messageService
114+
.info(
115+
nls.localize(
116+
'arduino/newCloudSketch/openNewSketch',
117+
"Do you want to pull the new remote sketch '{0}' and open it in a new window?",
118+
newSketchName
119+
),
120+
yes
121+
)
122+
.then(async (answer) => {
123+
if (answer === yes) {
124+
const node = treeModel.getNode(
125+
CreateUri.toUri(newSketch).path.toString()
126+
);
127+
if (!node) {
128+
return;
129+
}
130+
if (CloudSketchbookTree.CloudSketchDirNode.is(node)) {
131+
try {
132+
await treeModel.sketchbookTree().pull({ node });
133+
} catch (err) {
134+
if (isNotFound(err)) {
135+
await treeModel.updateRoot();
136+
await treeModel.refresh();
137+
this.messageService.error(
138+
nls.localize(
139+
'arduino/newCloudSketch/notFound',
140+
"Could not pull the remote sketch '{0}'. It does not exist.",
141+
newSketchName
142+
)
143+
);
144+
return;
145+
}
146+
throw err;
147+
}
148+
return this.commandService.executeCommand(
149+
SketchbookCommands.OPEN_NEW_WINDOW.id,
150+
{ node }
151+
);
152+
}
153+
}
154+
});
155+
}
156+
return undefined;
157+
}
158+
159+
private treeModelFrom(
160+
widget: SketchbookWidget
161+
): CloudSketchbookTreeModel | undefined {
162+
const treeWidget = widget.getTreeWidget();
163+
if (treeWidget instanceof CloudSketchbookTreeWidget) {
164+
const model = treeWidget.model;
165+
if (model instanceof CloudSketchbookTreeModel) {
166+
return model;
167+
}
168+
}
169+
return undefined;
170+
}
171+
172+
private async newSketchName(
173+
rootNode: CompositeTreeNode,
174+
initialValue?: string | undefined
175+
): Promise<string | undefined> {
176+
const existingNames = rootNode.children
177+
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
178+
.map(({ fileStat }) => fileStat.name);
179+
return new WorkspaceInputDialog(
180+
{
181+
title: nls.localize(
182+
'arduino/newCloudSketch/newSketchTitle',
183+
'Name of a new Remote Sketch'
184+
),
185+
parentUri: CreateUri.root,
186+
initialValue,
187+
validate: (input) => {
188+
if (existingNames.includes(input)) {
189+
return nls.localize(
190+
'arduino/newCloudSketch/sketchAlreadyExists',
191+
"Remote sketch '{0}' already exists.",
192+
input
193+
);
194+
}
195+
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
196+
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
197+
return '';
198+
}
199+
return nls.localize(
200+
'arduino/newCloudSketch/invalidSketchName',
201+
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
202+
);
203+
},
204+
},
205+
this.labelProvider
206+
).open();
207+
}
208+
}
209+
export namespace NewCloudSketch {
210+
export namespace Commands {
211+
export const NEW_CLOUD_SKETCH: Command = {
212+
id: 'arduino-new-cloud-sketch',
213+
};
214+
}
215+
}
216+
217+
function isConflict(err: unknown): boolean {
218+
return isErrorWithStatusOf(err, 409);
219+
}
220+
function isNotFound(err: unknown): boolean {
221+
return isErrorWithStatusOf(err, 404);
222+
}
223+
function isErrorWithStatusOf(
224+
err: unknown,
225+
status: number
226+
): err is Error & { status: number } {
227+
if (err instanceof Error) {
228+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
229+
const object = err as any;
230+
return 'status' in object && object.status === status;
231+
}
232+
return false;
233+
}

arduino-ide-extension/src/browser/contributions/open-sketch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class OpenSketch extends SketchContribution {
5454
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
5555
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
5656
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
57-
order: '1',
57+
order: '2',
5858
});
5959
}
6060

arduino-ide-extension/src/browser/contributions/save-sketch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class SaveSketch extends SketchContribution {
2424
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
2525
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
2626
label: nls.localize('vscode/fileCommands/save', 'Save'),
27-
order: '6',
27+
order: '7',
2828
});
2929
}
3030

arduino-ide-extension/src/browser/create/create-uri.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export namespace CreateUri {
77
export const scheme = 'arduino-create';
88
export const root = toUri(posix.sep);
99

10-
export function toUri(posixPathOrResource: string | Create.Resource): URI {
10+
export function toUri(
11+
posixPathOrResource: string | Create.Resource | Create.Sketch
12+
): URI {
1113
const posixPath =
1214
typeof posixPathOrResource === 'string'
1315
? posixPathOrResource

arduino-ide-extension/src/browser/style/dialogs.css

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
min-height: 0;
3030
}
3131

32+
.p-Widget.dialogOverlay .dialogBlock .dialogControl .error {
33+
word-break: normal;
34+
}
35+
3236
.p-Widget.dialogOverlay .dialogBlock .dialogContent {
3337
padding: 0;
3438
overflow: auto;
@@ -80,10 +84,8 @@
8084
opacity: .4;
8185
}
8286

83-
8487
@media only screen and (max-height: 560px) {
8588
.p-Widget.dialogOverlay .dialogBlock {
8689
max-height: 400px;
8790
}
8891
}
89-

arduino-ide-extension/src/browser/style/sketchbook.css

+16
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@
3333
height: 100%;
3434
}
3535

36+
.sketchbook-trees-container .create-new {
37+
min-height: 58px;
38+
height: 58px;
39+
display: flex;
40+
align-items: center;
41+
justify-content: center;
42+
}
43+
/*
44+
By default, theia-button has a left-margin. IDE2 does not need the left margin
45+
for the _New Remote? Sketch_. Otherwise, the button does not fit the default
46+
widget width.
47+
*/
48+
.sketchbook-trees-container .create-new .theia-button {
49+
margin-left: unset;
50+
}
51+
3652
.sketchbook-tree__opts {
3753
background-color: var(--theia-foreground);
3854
-webkit-mask: url(./sketchbook-opts-icon.svg);

0 commit comments

Comments
 (0)