feat: Create remote sketch

Closes #1580

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2022-10-25 17:13:43 +02:00
committed by Akos Kitta
parent 6984c52b92
commit 7d6a2d5e33
21 changed files with 683 additions and 111 deletions

View File

@@ -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;
}