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 05c554f

Browse files
author
Akos Kitta
committed
feat: Create remote sketch
Closes #1580 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 0773c39 commit 05c554f

File tree

7 files changed

+362
-11
lines changed

7 files changed

+362
-11
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ 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';
338339

339340
const registerArduinoThemes = () => {
340341
const themes: MonacoThemeJson[] = [
@@ -751,6 +752,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
751752
Contribution.configure(bind, DeleteSketch);
752753
Contribution.configure(bind, UpdateIndexes);
753754
Contribution.configure(bind, InterfaceScale);
755+
Contribution.configure(bind, NewCloudSketch);
754756

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

‎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/style/dialogs.css‎

Lines changed: 4 additions & 2 deletions
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/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export class CloudSketchbookTree extends SketchbookTree {
136136
return;
137137
}
138138
}
139-
this.runWithState(node, 'pulling', async (node) => {
139+
returnthis.runWithState(node, 'pulling', async (node) => {
140140
const commandsCopy = node.commands;
141141
node.commands = [];
142142

@@ -196,7 +196,7 @@ export class CloudSketchbookTree extends SketchbookTree {
196196
return;
197197
}
198198
}
199-
this.runWithState(node, 'pushing', async (node) => {
199+
returnthis.runWithState(node, 'pushing', async (node) => {
200200
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
201201
throw new Error(
202202
nls.localize(

0 commit comments

Comments
(0)

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