Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 91dcdb0

Browse files
author
Akos Kitta
committed
feat: Create remote sketch
Closes #1580 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 8a85b5c commit 91dcdb0

21 files changed

+683
-111
lines changed

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

Lines changed: 8 additions & 0 deletions
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‎

Lines changed: 1 addition & 1 deletion
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

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

‎arduino-ide-extension/src/browser/contributions/new-sketch.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class NewSketch extends SketchContribution {
2121
override registerMenus(registry: MenuModelRegistry): void {
2222
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
2323
commandId: NewSketch.Commands.NEW_SKETCH.id,
24-
label: nls.localize('arduino/sketch/new', 'New'),
24+
label: nls.localize('arduino/sketch/new', 'New Sketch'),
2525
order: '0',
2626
});
2727
}

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

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 3 additions & 1 deletion
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/local-cache/local-cache-fs-provider.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export class LocalCacheFsProvider
3434
@inject(AuthenticationClientService)
3535
protected readonly authenticationService: AuthenticationClientService;
3636

37-
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
3837
readonly ready = new Deferred<void>();
3938

4039
private _localCacheRoot: URI;
@@ -153,7 +152,7 @@ export class LocalCacheFsProvider
153152
return uri;
154153
}
155154

156-
privatetoUri(session: AuthenticationSession): URI {
155+
toUri(session: AuthenticationSession): URI {
157156
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
158157
return this._localCacheRoot.resolve(session.id.split('|')[1]);
159158
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,8 @@
8080
opacity: .4;
8181
}
8282

83-
8483
@media only screen and (max-height: 560px) {
8584
.p-Widget.dialogOverlay .dialogBlock {
8685
max-height: 400px;
8786
}
8887
}
89-

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

Lines changed: 16 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /