mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-07 18:38:32 +00:00
feat: Create remote sketch
Closes #1580 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import type { AuthenticationSession } from '../../node/auth/types';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { Create } from '../create/typings';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
|
||||
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
|
||||
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
|
||||
import { Command, CommandRegistry, Contribution, URI } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class NewCloudSketch extends Contribution {
|
||||
@inject(CreateApi)
|
||||
private readonly createApi: CreateApi;
|
||||
@inject(SketchbookWidgetContribution)
|
||||
private readonly widgetContribution: SketchbookWidgetContribution;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(MainMenuManager)
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private _session: AuthenticationSession | undefined;
|
||||
private _enabled: boolean;
|
||||
|
||||
override onReady(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.authenticationService.onSessionDidChange((session) => {
|
||||
const oldSession = this._session;
|
||||
this._session = session;
|
||||
if (!!oldSession !== !!this._session) {
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}),
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.cloud.enabled') {
|
||||
const oldEnabled = this._enabled;
|
||||
this._enabled = Boolean(newValue);
|
||||
if (this._enabled !== oldEnabled) {
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
this._enabled = this.preferences['arduino.cloud.enabled'];
|
||||
this._session = this.authenticationService.session;
|
||||
if (this._session) {
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
|
||||
execute: () => this.createNewSketch(),
|
||||
isEnabled: () => !!this._session,
|
||||
isVisible: () => this._enabled,
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+Alt+N',
|
||||
});
|
||||
}
|
||||
|
||||
private async createNewSketch(
|
||||
initialValue?: string | undefined
|
||||
): Promise<URI | undefined> {
|
||||
const widget = await this.widgetContribution.widget;
|
||||
const treeModel = this.treeModelFrom(widget);
|
||||
if (!treeModel) {
|
||||
return undefined;
|
||||
}
|
||||
const rootNode = CompositeTreeNode.is(treeModel.root)
|
||||
? treeModel.root
|
||||
: undefined;
|
||||
if (!rootNode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newSketchName = await this.newSketchName(rootNode, initialValue);
|
||||
if (!newSketchName) {
|
||||
return undefined;
|
||||
}
|
||||
let result: Create.Sketch | undefined | 'conflict';
|
||||
try {
|
||||
result = await this.createApi.createSketch(newSketchName);
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
result = 'conflict';
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (result) {
|
||||
await treeModel.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
if (result === 'conflict') {
|
||||
return this.createNewSketch(newSketchName);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return this.open(treeModel, result);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async open(
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
newSketch: Create.Sketch
|
||||
): Promise<URI | undefined> {
|
||||
const id = CreateUri.toUri(newSketch).path.toString();
|
||||
const node = treeModel.getNode(id);
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`Could not find remote sketchbook tree node with Tree node ID: ${id}.`
|
||||
);
|
||||
}
|
||||
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
throw new Error(
|
||||
`Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await treeModel.sketchbookTree().pull({ node });
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
await treeModel.refresh();
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/newCloudSketch/notFound',
|
||||
"Could not pull the remote sketch '{0}'. It does not exist.",
|
||||
newSketch.name
|
||||
)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return this.commandService.executeCommand(
|
||||
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
|
||||
private treeModelFrom(
|
||||
widget: SketchbookWidget
|
||||
): CloudSketchbookTreeModel | undefined {
|
||||
const treeWidget = widget.getTreeWidget();
|
||||
if (treeWidget instanceof CloudSketchbookTreeWidget) {
|
||||
const model = treeWidget.model;
|
||||
if (model instanceof CloudSketchbookTreeModel) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async newSketchName(
|
||||
rootNode: CompositeTreeNode,
|
||||
initialValue?: string | undefined
|
||||
): Promise<string | undefined> {
|
||||
const existingNames = rootNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
return new WorkspaceInputDialog(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/newCloudSketch/newSketchTitle',
|
||||
'Name of a new Remote Sketch'
|
||||
),
|
||||
parentUri: CreateUri.root,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return nls.localize(
|
||||
'arduino/newCloudSketch/sketchAlreadyExists',
|
||||
"Remote sketch '{0}' already exists.",
|
||||
input
|
||||
);
|
||||
}
|
||||
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
|
||||
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
|
||||
return '';
|
||||
}
|
||||
return nls.localize(
|
||||
'arduino/newCloudSketch/invalidSketchName',
|
||||
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
|
||||
);
|
||||
},
|
||||
},
|
||||
this.labelProvider
|
||||
).open();
|
||||
}
|
||||
}
|
||||
export namespace NewCloudSketch {
|
||||
export namespace Commands {
|
||||
export const NEW_CLOUD_SKETCH: Command = {
|
||||
id: 'arduino-new-cloud-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isConflict(err: unknown): boolean {
|
||||
return isErrorWithStatusOf(err, 409);
|
||||
}
|
||||
function isNotFound(err: unknown): boolean {
|
||||
return isErrorWithStatusOf(err, 404);
|
||||
}
|
||||
function isErrorWithStatusOf(
|
||||
err: unknown,
|
||||
status: number
|
||||
): err is Error & { status: number } {
|
||||
if (err instanceof Error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const object = err as any;
|
||||
return 'status' in object && object.status === status;
|
||||
}
|
||||
return false;
|
||||
}
|
Reference in New Issue
Block a user