Skip to content

Commit fa06b4e

Browse files
author
Akos Kitta
committed
feat: Create remote sketch
Closes #1580 Signed-off-by: Akos Kitta <[email protected]>
1 parent 0d05509 commit fa06b4e

12 files changed

+413
-60
lines changed

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

Diff for: 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

Diff for: 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-

Diff for: arduino-ide-extension/src/browser/style/sketchbook.css

+18
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);
@@ -87,3 +103,5 @@
87103
.hc-black.hc-theia.theia-hc .theia-Tree .ReactVirtualized__List:focus .theia-TreeNode.theia-mod-selected {
88104
outline: 1px solid var(--theia-focusBorder);
89105
}
106+
107+

0 commit comments

Comments
 (0)