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 fb544f9

Browse files
author
Akos Kitta
committed
feat: Create remote sketch
Closes #1580
1 parent 0d05509 commit fb544f9

21 files changed

+699
-82
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/arduino-preferences.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,35 @@ export namespace ErrorRevealStrategy {
4040
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
4141
}
4242

43+
export const CloudSketchOpenStrategyLiterals = [
44+
'Ask',
45+
'Never',
46+
'Always',
47+
] as const;
48+
export type CloudSketchOpenStrategy =
49+
typeof CloudSketchOpenStrategyLiterals[number];
50+
export namespace CloudSketchOpenStrategy {
51+
export const Default: CloudSketchOpenStrategy = 'Ask';
52+
export function labelOf(strategy: CloudSketchOpenStrategy): string {
53+
return CloudSketchOpenStrategyLabels[strategy];
54+
}
55+
const CloudSketchOpenStrategyLabels: Record<CloudSketchOpenStrategy, string> =
56+
{
57+
Ask: nls.localize(
58+
'arduino/preferences/cloud.sketchOpenStrategy.ask',
59+
'Ask'
60+
),
61+
Never: nls.localize(
62+
'arduino/preferences/cloud.sketchOpenStrategy.never',
63+
'Never'
64+
),
65+
Always: nls.localize(
66+
'arduino/preferences/cloud.sketchOpenStrategy.always',
67+
'Always'
68+
),
69+
};
70+
}
71+
4372
export const ArduinoConfigSchema: PreferenceSchema = {
4473
type: 'object',
4574
properties: {
@@ -161,6 +190,29 @@ export const ArduinoConfigSchema: PreferenceSchema = {
161190
),
162191
default: true,
163192
},
193+
'arduino.cloud.sketchOpenStrategy': {
194+
enum: [...CloudSketchOpenStrategyLiterals],
195+
enumDescriptions: [
196+
nls.localize(
197+
'arduino/preferences/cloud.sketchOpenStrategy.ask.description',
198+
'IDE asks users whether to pull the remote sketch and open it in a new window.'
199+
),
200+
nls.localize(
201+
'arduino/preferences/cloud.sketchOpenStrategy.never.description',
202+
'IDE neither pulls nor opens the remote sketch after creating it. Users can manually pull the remote sketch.'
203+
),
204+
nls.localize(
205+
'arduino/preferences/cloud.sketchOpenStrategy.always.description',
206+
'IDE automatically pulls and opens the remote sketch in a new window.'
207+
),
208+
],
209+
markdownDescription: nls.localize(
210+
'arduino/preferences/cloud.sketchOpenStrategy',
211+
'Configures what IDE does after creating a new remote sketch. The default value is `"{0}"`.',
212+
CloudSketchOpenStrategy.labelOf('Ask')
213+
),
214+
default: CloudSketchOpenStrategy.Default,
215+
},
164216
'arduino.cloud.pull.warn': {
165217
type: 'boolean',
166218
description: nls.localize(
@@ -276,6 +328,7 @@ export interface ArduinoConfiguration {
276328
'arduino.board.certificates': string;
277329
'arduino.sketchbook.showAllFiles': boolean;
278330
'arduino.cloud.enabled': boolean;
331+
'arduino.cloud.sketchOpenStrategy': CloudSketchOpenStrategy;
279332
'arduino.cloud.pull.warn': boolean;
280333
'arduino.cloud.push.warn': boolean;
281334
'arduino.cloud.pushpublic.warn': boolean;

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

0 commit comments

Comments
(0)

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