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 942d974

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 942d974

File tree

7 files changed

+361
-11
lines changed

7 files changed

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

‎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 によって変換されたページ (->オリジナル) /