[atl-1217] sketchbook explorer local & remote

This commit is contained in:
Akos Kitta 2021-04-16 16:47:23 +02:00 committed by Francesco Stasi
parent e6cbefb880
commit 4c536ec8fc
75 changed files with 5559 additions and 430 deletions

29
.vscode/launch.json vendored
View File

@ -1,22 +1,6 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
{
"type": "node",
"request": "launch",
"name": "Electron Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
},
{
"type": "node",
"request": "launch",
@ -106,6 +90,19 @@
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
{
"type": "node",
"request": "launch",
"name": "Electron Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
}
]
}

4
.vscode/tasks.json vendored
View File

@ -1,6 +1,4 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
@ -35,7 +33,7 @@
"panel": "new",
"clear": false
}
}
},
{
"label": "Arduino IDE - Watch Browser App",
"type": "shell",

View File

@ -1,19 +0,0 @@
FROM gitpod/workspace-full-vnc
USER root
RUN apt-get update -q --fix-missing && \
apt-get install -y -q software-properties-common && \
apt-get install -y -q --no-install-recommends \
build-essential \
libssl-dev \
golang-go \
libxkbfile-dev \
libnss3-dev
RUN set -ex && \
tmpdir=$(mktemp -d) && \
curl -L -o $tmpdir/protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip && \
mkdir -p /usr/lib/protoc && cd /usr/lib/protoc && unzip $tmpdir/protoc.zip && \
chmod -R 755 /usr/lib/protoc/include/google && \
ln -s /usr/lib/protoc/bin/* /usr/bin && \
rm $tmpdir/protoc.zip

View File

@ -33,13 +33,18 @@
"@theia/search-in-workspace": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^5.0.35",
"@types/google-protobuf": "^3.7.2",
"@types/js-yaml": "^3.12.2",
"@types/keytar": "^4.4.0",
"@types/lodash.debounce": "^4.0.6",
"@types/ncp": "^2.0.4",
"@types/node-fetch": "^2.5.7",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
@ -48,15 +53,24 @@
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"css-element-queries": "^1.2.0",
"dateformat": "^3.0.3",
"deepmerge": "^4.2.2",
"deepmerge": "2.0.1",
"fuzzy": "^0.1.3",
"glob": "^7.1.6",
"google-protobuf": "^3.11.4",
"lodash.debounce": "^4.0.8",
"hash.js": "^1.1.7",
"is-valid-path": "^0.1.1",
"js-yaml": "^3.13.1",
"jwt-decode": "^3.1.2",
"keytar": "7.2.0",
"lodash.debounce": "^4.0.8",
"ncp": "^2.0.0",
"node-fetch": "^2.6.1",
"open": "^8.0.6",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"react-disable": "^0.1.0",

View File

@ -1,38 +1,18 @@
import { Mutex } from 'async-mutex';
import {
MAIN_MENU_BAR,
MenuContribution,
MenuModelRegistry,
SelectionService,
ILogger,
DisposableCollection,
} from '@theia/core';
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger, DisposableCollection } from '@theia/core';
import {
ContextMenuRenderer,
FrontendApplication,
FrontendApplicationContribution,
OpenerService,
StatusBar,
StatusBarAlignment,
FrontendApplication, FrontendApplicationContribution,
OpenerService, StatusBar, StatusBarAlignment
} from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import {
TabBarToolbarContribution,
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import {
CommandContribution,
CommandRegistry,
} from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import {
EditorMainMenu,
EditorManager,
EditorOpenerOptions,
} from '@theia/editor/lib/browser';
import { EditorMainMenu, EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
@ -46,14 +26,7 @@ import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import { remote } from 'electron';
import { MainMenuManager } from '../common/main-menu-manager';
import {
BoardsService,
CoreService,
Port,
SketchesService,
ExecutableService,
Sketch,
} from '../common/protocol';
import { BoardsService, CoreService, Port, SketchesService, ExecutableService, Sketch } from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service';
import { FileSystemExt } from '../common/protocol/filesystem-ext';
@ -77,16 +50,12 @@ import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-c
import { SaveAsSketch } from './contributions/save-as-sketch';
import { FileChangeType } from '@theia/filesystem/lib/browser';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
@injectable()
export class ArduinoFrontendContribution
implements
FrontendApplicationContribution,
TabBarToolbarContribution,
CommandContribution,
MenuContribution,
ColorContribution
{
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution {
@inject(ILogger)
protected logger: ILogger;
@ -156,6 +125,9 @@ export class ArduinoFrontendContribution
@inject(SearchInWorkspaceFrontendContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(SketchbookWidgetContribution)
protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@ -195,74 +167,48 @@ export class ArduinoFrontendContribution
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
protected invalidConfigPopup:
| Promise<void | 'No' | 'Yes' | undefined>
| undefined;
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
protected toDisposeOnStop = new DisposableCollection();
@postConstruct()
protected async init(): Promise<void> {
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn(
'You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.'
);
this.messageService.warn('You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.');
}
const updateStatusBar = ({
selectedBoard,
selectedPort,
}: BoardsConfig.Config) => {
const updateStatusBar = ({ selectedBoard, selectedPort }: BoardsConfig.Config) => {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: selectedBoard
? `$(microchip) ${selectedBoard.name}`
: '$(close) no board selected',
className: 'arduino-selected-board',
text: selectedBoard ? `$(microchip) ${selectedBoard.name}` : '$(close) no board selected',
className: 'arduino-selected-board'
});
if (selectedBoard) {
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort
? `on ${Port.toString(selectedPort)}`
: '[not connected]',
className: 'arduino-selected-port',
text: selectedPort ? `on ${Port.toString(selectedPort)}` : '[not connected]',
className: 'arduino-selected-port'
});
}
};
}
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.appStateService.reachedState('ready').then(async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && !(await this.sketchService.isTemp(sketch))) {
this.toDisposeOnStop.push(
this.fileService.watch(new URI(sketch.uri))
);
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {
if (
type === FileChangeType.ADDED &&
resource.parent.toString() === sketch.uri
) {
const reloadedSketch =
await this.sketchService.loadSketch(
sketch.uri
);
if (
Sketch.isInSketch(resource, reloadedSketch)
) {
this.ensureOpened(
resource.toString(),
true,
{ mode: 'open' }
);
}
if (sketch && (!await this.sketchService.isTemp(sketch))) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(this.fileService.onDidFilesChange(async event => {
for (const { type, resource } of event.changes) {
if (type === FileChangeType.ADDED && resource.parent.toString() === sketch.uri) {
const reloadedSketch = await this.sketchService.loadSketch(sketch.uri)
if (Sketch.isInSketch(resource, reloadedSketch)) {
this.ensureOpened(resource.toString(), true, { mode: 'open' });
}
}
})
);
}
}));
}
});
}
onStart(app: FrontendApplication): void {
@ -274,7 +220,7 @@ export class ArduinoFrontendContribution
this.problemContribution,
this.scmContribution,
this.siwContribution,
] as Array<FrontendApplicationContribution>) {
this.sketchbookWidgetContribution] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(app);
}
@ -288,27 +234,18 @@ export class ArduinoFrontendContribution
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.language.log' &&
event.newValue !== event.oldValue
) {
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) {
start(this.boardsServiceClientImpl.boardsConfig);
}
});
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get(
'arduino.window.zoomLevel'
);
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.window.zoomLevel' &&
typeof event.newValue === 'number' &&
event.newValue !== event.oldValue
) {
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.window.zoomLevel' && typeof event.newValue === 'number' && event.newValue !== event.oldValue) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
@ -322,33 +259,21 @@ export class ArduinoFrontendContribution
protected languageServerFqbn?: string;
protected languageServerStartMutex = new Mutex();
protected async startLanguageServer(
fqbn: string,
name: string | undefined
): Promise<void> {
protected async startLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginSupport.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
console.info(
`Could not start language server for ${fqbn}. The core is not installed for the board.`
);
console.info(`Could not start language server for ${fqbn}. The core is not installed for the board.`);
if (this.languageServerFqbn) {
try {
await this.commandRegistry.executeCommand(
'arduino.languageserver.stop'
);
console.info(
`Stopped language server process for ${this.languageServerFqbn}.`
);
await this.commandRegistry.executeCommand('arduino.languageserver.stop');
console.info(`Stopped language server process for ${this.languageServerFqbn}.`);
this.languageServerFqbn = undefined;
} catch (e) {
console.error(
`Failed to start language server process for ${this.languageServerFqbn}`,
e
);
console.error(`Failed to start language server process for ${this.languageServerFqbn}`, e);
throw e;
}
}
@ -362,46 +287,31 @@ export class ArduinoFrontendContribution
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch =
await this.sketchServiceClient.currentSketch();
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileService.fsPath(
new URI(currentSketch.uri)
);
currentSketchPath = await this.fileService.fsPath(new URI(currentSketch.uri));
}
}
const { clangdUri, cliUri, lsUri } =
await this.executableService.list();
const [clangdPath, cliPath, lsPath, cliConfigPath] =
await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(cliUri)),
this.fileService.fsPath(new URI(lsUri)),
this.fileService.fsPath(
new URI(await this.configService.getCliConfigFileUri())
),
]);
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(cliUri)),
this.fileService.fsPath(new URI(lsUri)),
this.fileService.fsPath(new URI(await this.configService.getCliConfigFileUri()))
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
)
),
this.commandRegistry.executeCommand<string>(
'arduino.languageserver.start',
{
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliConfigPath,
board: {
fqbn,
name: name ? `"${name}"` : undefined,
},
new Promise<undefined>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${20_000} ms.`)), 20_000)),
this.commandRegistry.executeCommand<string>('arduino.languageserver.start', {
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliConfigPath,
board: {
fqbn,
name: name ? `"${name}"` : undefined
}
),
})
]);
} catch (e) {
console.log(`Failed to start language server for ${fqbn}`, e);
@ -414,33 +324,29 @@ export class ArduinoFrontendContribution
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => (
<BoardsToolBarItem
key="boardsToolbarItem"
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClientImpl}
/>
),
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 7,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClientImpl} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 7
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Serial Monitor',
tooltip: 'Serial Monitor'
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
execute: () => this.editorMode.toggleCompileForDebug(),
isToggled: () => this.editorMode.compileForDebug,
isToggled: () => this.editorMode.compileForDebug
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
execute: async (uri: URI) => {
this.openSketchFiles(uri);
},
}
});
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
execute: async (query?: string | undefined) => {
@ -448,7 +354,7 @@ export class ArduinoFrontendContribution
if (boardsConfig) {
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
},
}
});
}
@ -457,14 +363,10 @@ export class ArduinoFrontendContribution
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
};
registry
.getMenu(MAIN_MENU_BAR)
.removeNode(menuId(MonacoMenus.SELECTION));
}
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry
.getMenu(MAIN_MENU_BAR)
.removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
@ -472,7 +374,7 @@ export class ArduinoFrontendContribution
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '4',
order: '4'
});
}
@ -486,20 +388,11 @@ export class ArduinoFrontendContribution
await this.ensureOpened(mainFileUri, true);
if (mainFileUri.endsWith('.pde')) {
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
this.messageService
.info(message, 'Later', 'Yes')
.then(async (answer) => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
}
);
}
});
this.messageService.info(message, 'Later', 'Yes').then(async answer => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false });
}
});
}
} catch (e) {
console.error(e);
@ -508,14 +401,8 @@ export class ArduinoFrontendContribution
}
}
protected async ensureOpened(
uri: string,
forceOpen = false,
options?: EditorOpenerOptions | undefined
): Promise<any> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
protected async ensureOpened(uri: string, forceOpen = false, options?: EditorOpenerOptions | undefined): Promise<any> {
const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri), options);
}
@ -527,78 +414,73 @@ export class ArduinoFrontendContribution
id: 'arduino.branding.primary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background',
light: 'statusBar.background'
},
description:
'The primary branding color, such as dialog titles, library, and board manager list labels.',
description: 'The primary branding color, such as dialog titles, library, and board manager list labels.'
},
{
id: 'arduino.branding.secondary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background',
light: 'statusBar.background'
},
description:
'Secondary branding color for list selections, dropdowns, and widget borders.',
description: 'Secondary branding color for list selections, dropdowns, and widget borders.'
},
{
id: 'arduino.foreground',
defaults: {
dark: 'editorWidget.background',
light: 'editorWidget.background',
hc: 'editorWidget.background',
hc: 'editorWidget.background'
},
description:
'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.',
description: 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.'
},
{
id: 'arduino.toolbar.background',
defaults: {
dark: 'button.background',
light: 'button.background',
hc: 'activityBar.inactiveForeground',
hc: 'activityBar.inactiveForeground'
},
description:
'Background color of the toolbar items. Such as Upload, Verify, etc.',
description: 'Background color of the toolbar items. Such as Upload, Verify, etc.'
},
{
id: 'arduino.toolbar.hoverBackground',
defaults: {
dark: 'button.hoverBackground',
light: 'button.foreground',
hc: 'textLink.foreground',
hc: 'textLink.foreground'
},
description:
'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.',
description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.'
},
{
id: 'arduino.toolbar.toggleBackground',
defaults: {
dark: 'editor.selectionBackground',
light: 'editor.selectionBackground',
hc: 'textPreformat.foreground',
hc: 'textPreformat.foreground'
},
description:
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
description: 'Toggle color of the toolbar items when they are currently toggled (the command is in progress)'
},
{
id: 'arduino.output.foreground',
defaults: {
dark: 'editor.foreground',
light: 'editor.foreground',
hc: 'editor.foreground',
hc: 'editor.foreground'
},
description: 'Color of the text in the Output view.',
description: 'Color of the text in the Output view.'
},
{
id: 'arduino.output.background',
defaults: {
dark: 'editor.background',
light: 'editor.background',
hc: 'editor.background',
hc: 'editor.background'
},
description: 'Background color of the Output view.',
description: 'Background color of the Output view.'
}
);
}
}

View File

@ -217,6 +217,25 @@ import { NotificationManager } from './theia/messages/notifications-manager';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
import { NotificationsRenderer } from './theia/messages/notifications-renderer';
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
import { LocalCacheFsProvider } from './local-cache/local-cache-fs-provider';
import { CloudSketchbookWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-widget';
import { CloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { createCloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-container';
import { CreateApi } from './create/create-api';
import { ShareSketchDialog } from './dialogs.ts/cloud-share-sketch-dialog';
import { AuthenticationClientService } from './auth/authentication-client-service';
import {
AuthenticationService,
AuthenticationServicePath,
} from '../common/protocol/authentication-service';
import { CreateFsProvider } from './create/create-fs-provider';
import { FileServiceContribution } from '@theia/filesystem/lib/browser/file-service';
import { CloudSketchbookContribution } from './widgets/cloud-sketchbook/cloud-sketchbook-contributions';
import { CloudSketchbookCompositeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-composite-widget';
import { SketchbookWidget } from './widgets/sketchbook/sketchbook-widget';
import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget';
import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container';
const ElementQueries = require('css-element-queries/src/ElementQueries');
@ -653,4 +672,51 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaNotificationManager).toService(NotificationManager);
bind(NotificationsRenderer).toSelf().inSingletonScope();
rebind(TheiaNotificationsRenderer).toService(NotificationsRenderer);
// UI for the Sketchbook
bind(SketchbookWidget).toSelf();
bind(SketchbookTreeWidget).toDynamicValue(({ container }) =>
createSketchbookTreeWidget(container)
);
bindViewContribution(bind, SketchbookWidgetContribution);
bind(FrontendApplicationContribution).toService(
SketchbookWidgetContribution
);
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: 'arduino-sketchbook-widget',
createWidget: () => container.get(SketchbookWidget),
}));
bind(CloudSketchbookWidget).toSelf();
rebind(SketchbookWidget).toService(CloudSketchbookWidget);
bind(CloudSketchbookTreeWidget).toDynamicValue(({ container }) =>
createCloudSketchbookTreeWidget(container)
);
bind(CreateApi).toSelf().inSingletonScope();
bind(ShareSketchDialog).toSelf().inSingletonScope();
bind(AuthenticationClientService).toSelf().inSingletonScope();
bind(CommandContribution).toService(AuthenticationClientService);
bind(FrontendApplicationContribution).toService(
AuthenticationClientService
);
bind(AuthenticationService)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
context.container,
AuthenticationServicePath
)
)
.inSingletonScope();
bind(CreateFsProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CreateFsProvider);
bind(FileServiceContribution).toService(CreateFsProvider);
bind(CloudSketchbookContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(CloudSketchbookContribution);
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
bind(FileServiceContribution).toService(LocalCacheFsProvider);
bind(CloudSketchbookCompositeWidget).toSelf();
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
id: 'cloud-sketchbook-composite-widget',
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
}));
});

View File

@ -55,6 +55,62 @@ export const ArduinoConfigSchema: PreferenceSchema = {
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
default: true,
},
'arduino.sketchbook.showAllFiles': {
type: 'boolean',
description:
'True to show all sketch files inside the sketch. It is false by default.',
default: false,
},
'arduino.cloud.enabled': {
type: 'boolean',
description:
'True if the sketch sync functions are enabled. Defaults to true.',
default: true,
},
'arduino.cloud.pull.warn': {
type: 'boolean',
description:
'True if users should be warned before pulling a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.push.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.pushpublic.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.',
default: true,
},
'arduino.cloud.sketchSyncEnpoint': {
type: 'string',
description:
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.',
default: 'https://api2.arduino.cc/create',
},
'arduino.auth.clientID': {
type: 'string',
description: 'The OAuth2 client ID.',
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
},
'arduino.auth.domain': {
type: 'string',
description: 'The OAuth2 domain.',
default: 'login.arduino.cc',
},
'arduino.auth.audience': {
type: 'string',
description: 'The 0Auth2 audience.',
default: 'https://api.arduino.cc',
},
'arduino.auth.registerUri': {
type: 'string',
description: 'The URI used to register a new user.',
default: 'https://auth.arduino.cc/login#/register',
},
},
};
@ -67,6 +123,16 @@ export interface ArduinoConfiguration {
'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.autoUpdate': boolean;
'arduino.sketchbook.showAllFiles': boolean;
'arduino.cloud.enabled': boolean;
'arduino.cloud.pull.warn': boolean;
'arduino.cloud.push.warn': boolean;
'arduino.cloud.pushpublic.warn': boolean;
'arduino.cloud.sketchSyncEnpoint': string;
'arduino.auth.clientID': string;
'arduino.auth.domain': string;
'arduino.auth.audience': string;
'arduino.auth.registerUri': string;
}
export const ArduinoPreferences = Symbol('ArduinoPreferences');

View File

@ -0,0 +1,93 @@
import { inject, injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
CommandRegistry,
CommandContribution,
} from '@theia/core/lib/common/command';
import {
AuthenticationService,
AuthenticationServiceClient,
AuthenticationSession,
} from '../../common/protocol/authentication-service';
import { CloudUserCommands } from './cloud-user-commands';
import { serverPort } from '../../node/auth/authentication-server';
import { AuthOptions } from '../../node/auth/types';
import { ArduinoPreferences } from '../arduino-preferences';
@injectable()
export class AuthenticationClientService
implements
FrontendApplicationContribution,
CommandContribution,
AuthenticationServiceClient
{
@inject(AuthenticationService)
protected readonly service: JsonRpcProxy<AuthenticationService>;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
protected authOptions: AuthOptions;
protected _session: AuthenticationSession | undefined;
protected readonly toDispose = new DisposableCollection();
protected readonly onSessionDidChangeEmitter = new Emitter<
AuthenticationSession | undefined
>();
readonly onSessionDidChange = this.onSessionDidChangeEmitter.event;
onStart(): void {
this.toDispose.push(this.onSessionDidChangeEmitter);
this.service.setClient(this);
this.service
.session()
.then((session) => this.notifySessionDidChange(session));
this.setOptions();
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.preferenceName.startsWith('arduino.auth.')) {
this.setOptions();
}
});
}
setOptions(): void {
this.service.setOptions({
redirectUri: `http://localhost:${serverPort}/callback`,
responseType: 'code',
clientID: this.arduinoPreferences['arduino.auth.clientID'],
domain: this.arduinoPreferences['arduino.auth.domain'],
audience: this.arduinoPreferences['arduino.auth.audience'],
registerUri: this.arduinoPreferences['arduino.auth.registerUri'],
scopes: ['openid', 'profile', 'email', 'offline_access'],
});
}
protected updateSession(session?: AuthenticationSession | undefined) {
this._session = session;
this.onSessionDidChangeEmitter.fire(this._session);
}
get session(): AuthenticationSession | undefined {
return this._session;
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloudUserCommands.LOGIN, {
execute: () => this.service.login(),
});
registry.registerCommand(CloudUserCommands.LOGOUT, {
execute: () => this.service.logout(),
});
}
notifySessionDidChange(session: AuthenticationSession | undefined): void {
this.updateSession(session);
}
}

View File

@ -0,0 +1,18 @@
import { Command } from '@theia/core/lib/common/command';
export namespace CloudUserCommands {
export const LOGIN: Command = {
id: 'arduino-cloud--login',
label: 'Sign in',
};
export const LOGOUT: Command = {
id: 'arduino-cloud--logout',
label: 'Sign Out',
};
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu',
};
}

View File

@ -17,7 +17,10 @@ import {
TabBarToolbarRegistry,
open,
} from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
@injectable()
export class SketchControl extends SketchContribution {
@ -30,6 +33,15 @@ export class SketchControl extends SketchContribution {
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider;
protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection();
@ -61,8 +73,100 @@ export class SketchControl extends SketchContribution {
const { mainFileUri, rootFolderFileUris } =
await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...rootFolderFileUris];
const currentSketch =
await this.sketchesServiceClient.currentSketch();
const parentsketchUri = this.editorManager.currentEditor
?.getResourceUri()
?.toString();
const parentsketch =
await this.sketchService.getSketchFolder(
parentsketchUri || ''
);
// if the current file is in the current opened sketch, show extra menus
if (
currentSketch &&
parentsketch &&
parentsketch.uri === currentSketch.uri &&
(await this.allowRename(parentsketch.uri))
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_RENAME.id,
label: 'Rename',
order: '1',
}
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_RENAME
)
)
);
} else {
const renamePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
'Rename'
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
renamePlaceholder
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(
renamePlaceholder.id
)
)
);
}
if (
currentSketch &&
parentsketch &&
parentsketch.uri === currentSketch.uri &&
(await this.allowDelete(parentsketch.uri))
) {
this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: 'Delete',
order: '2',
}
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_DELETE
)
)
);
} else {
const deletePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
'Delete'
);
this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
deletePlaceholder
);
this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(
deletePlaceholder.id
)
)
);
}
for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]);
// focus on the opened sketch
const command = {
id: `arduino-focus-file--${uri.toString()}`,
};
@ -110,22 +214,6 @@ export class SketchControl extends SketchContribution {
order: '0',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_RENAME.id,
label: 'Rename',
order: '1',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: 'Delete',
order: '2',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
@ -166,6 +254,23 @@ export class SketchControl extends SketchContribution {
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
});
}
protected async isCloudSketch(uri: string) {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
}
return false;
}
protected async allowRename(uri: string) {
return !this.isCloudSketch(uri);
}
protected async allowDelete(uri: string) {
return !this.isCloudSketch(uri);
}
}
export namespace SketchControl {

View File

@ -0,0 +1,544 @@
import { injectable } from 'inversify';
import * as createPaths from './create-paths';
import { posix, splitSketchPath } from './create-paths';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { ArduinoPreferences } from '../arduino-preferences';
export interface ResponseResultProvider {
(response: Response): Promise<any>;
}
export namespace ResponseResultProvider {
export const NOOP: ResponseResultProvider = async () => undefined;
export const TEXT: ResponseResultProvider = (response) => response.text();
export const JSON: ResponseResultProvider = (response) => response.json();
}
type ResourceType = 'f' | 'd';
export let sketchCache: Create.Sketch[] = [];
@injectable()
export class CreateApi {
protected authenticationService: AuthenticationClientService;
protected arduinoPreferences: ArduinoPreferences;
public init(
authenticationService: AuthenticationClientService,
arduinoPreferences: ArduinoPreferences
): CreateApi {
this.authenticationService = authenticationService;
this.arduinoPreferences = arduinoPreferences;
return this;
}
async findSketchByPath(
path: string,
trustCache = true
): Promise<Create.Sketch | undefined> {
const skatches = sketchCache;
const sketch = skatches.find((sketch) => {
const [, spath] = splitSketchPath(sketch.path);
return path === spath;
});
if (trustCache) {
return Promise.resolve(sketch);
}
return await this.sketch({ id: sketch?.id });
}
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
return {
href: `${sketch.href}${posix.sep}${Create.arduino_secrets_file}`,
modified_at: sketch.modified_at,
name: `${Create.arduino_secrets_file}`,
path: `${sketch.path}${posix.sep}${Create.arduino_secrets_file}`,
mimetype: 'text/x-c++src; charset=utf-8',
type: 'file',
sketchId: sketch.id,
};
}
async sketch(opt: {
id?: string;
path?: string;
}): Promise<Create.Sketch | undefined> {
let url;
if (opt.id) {
url = new URL(`${this.domain()}/sketches/byID/${opt.id}`);
} else if (opt.path) {
url = new URL(`${this.domain()}/sketches/byPath${opt.path}`);
} else {
return;
}
url.searchParams.set('user_id', 'me');
const headers = await this.headers();
const result = await this.run<Create.Sketch>(url, {
method: 'GET',
headers,
});
return result;
}
async sketches(): Promise<Create.Sketch[]> {
const url = new URL(`${this.domain()}/sketches`);
url.searchParams.set('user_id', 'me');
const headers = await this.headers();
const result = await this.run<{ sketches: Create.Sketch[] }>(url, {
method: 'GET',
headers,
});
sketchCache = result.sketches;
return result.sketches;
}
async createSketch(
posixPath: string,
content: string = CreateApi.defaultInoContent
): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches`);
const headers = await this.headers();
const payload = {
ino: btoa(content),
path: posixPath,
user_id: 'me',
};
const init = {
method: 'PUT',
body: JSON.stringify(payload),
headers,
};
const result = await this.run<Create.Sketch>(url, init);
return result;
}
async readDirectory(
posixPath: string,
options: { recursive?: boolean; match?: string; secrets?: boolean } = {}
): Promise<Create.Resource[]> {
const url = new URL(
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
);
if (options.recursive) {
url.searchParams.set('deep', 'true');
}
if (options.match) {
url.searchParams.set('name_like', options.match);
}
const headers = await this.headers();
const sketchProm = options.secrets
? this.sketches()
: Promise.resolve(sketchCache);
return Promise.all([
this.run<Create.RawResource[]>(url, {
method: 'GET',
headers,
}),
sketchProm,
])
.then(async ([result, sketches]) => {
if (options.secrets) {
// for every sketch with secrets, create a fake arduino_secrets.h
result.forEach(async (res) => {
if (res.type !== 'sketch') {
return;
}
const [, spath] = createPaths.splitSketchPath(res.path);
const sketch = await this.findSketchByPath(spath);
if (
sketch &&
sketch.secrets &&
sketch.secrets.length > 0
) {
result.push(this.getSketchSecretStat(sketch));
}
});
if (posixPath !== posix.sep) {
const sketch = await this.findSketchByPath(posixPath);
if (
sketch &&
sketch.secrets &&
sketch.secrets.length > 0
) {
result.push(this.getSketchSecretStat(sketch));
}
}
}
const sketchesMap: Record<string, Create.Sketch> =
sketches.reduce((prev, curr) => {
return { ...prev, [curr.path]: curr };
}, {});
// add the sketch id and isPublic to the resource
return result.map((resource) => {
return {
...resource,
sketchId: sketchesMap[resource.path]?.id || '',
isPublic:
sketchesMap[resource.path]?.is_public || false,
};
});
})
.catch((reason) => {
if (reason?.status === 404) return [] as Create.Resource[];
else throw reason;
});
}
async createDirectory(posixPath: string): Promise<void> {
const url = new URL(
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
);
const headers = await this.headers();
await this.run(url, {
method: 'POST',
headers,
});
}
async stat(posixPath: string): Promise<Create.Resource> {
// The root is a directory read.
if (posixPath === '/') {
throw new Error('Stating the root is not supported');
}
// The RESTful API has different endpoints for files and directories.
// The RESTful API does not provide specific error codes, only HTP 500.
// We query the parent directory and look for the file with the last segment.
const parentPosixPath = createPaths.parentPosix(posixPath);
const basename = createPaths.basename(posixPath);
let resources;
if (basename === Create.arduino_secrets_file) {
const sketch = await this.findSketchByPath(parentPosixPath);
resources = sketch ? [this.getSketchSecretStat(sketch)] : [];
} else {
resources = await this.readDirectory(parentPosixPath, {
match: basename,
});
}
resources.sort((left, right) => left.path.length - right.path.length);
const resource = resources.find(({ name }) => name === basename);
if (!resource) {
throw new CreateError(`Not found: ${posixPath}.`, 404);
}
return resource;
}
async readFile(posixPath: string): Promise<string> {
const basename = createPaths.basename(posixPath);
if (basename === Create.arduino_secrets_file) {
const parentPosixPath = createPaths.parentPosix(posixPath);
const sketch = await this.findSketchByPath(parentPosixPath, false);
let file = '';
if (sketch && sketch.secrets) {
for (const item of sketch?.secrets) {
file += `#define ${item.name} "${item.value}"\r\n`;
}
}
return file;
}
const url = new URL(
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}`
);
const headers = await this.headers();
const result = await this.run<{ data: string }>(url, {
method: 'GET',
headers,
});
const { data } = result;
return atob(data);
}
async writeFile(
posixPath: string,
content: string | Uint8Array
): Promise<void> {
const basename = createPaths.basename(posixPath);
if (basename === Create.arduino_secrets_file) {
const parentPosixPath = createPaths.parentPosix(posixPath);
const sketch = await this.findSketchByPath(parentPosixPath);
if (sketch) {
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
const headers = await this.headers();
// parse the secret file
const secrets = (
typeof content === 'string'
? content
: new TextDecoder().decode(content)
)
.split(/\r?\n/)
.reduce((prev, curr) => {
// check if the line contains a secret
const secret = curr.split('SECRET_')[1] || null;
if (!secret) {
return prev;
}
const regexp = /(\S*)\s+([\S\s]*)/g;
const tokens = regexp.exec(secret) || [];
const name =
tokens[1].length > 0 ? `SECRET_${tokens[1]}` : '';
let value = '';
if (tokens[2].length > 0) {
value = JSON.parse(
JSON.stringify(
tokens[2]
.replace(/^['"]?/g, '')
.replace(/['"]?$/g, '')
)
);
}
if (name.length === 0 || value.length === 0) {
return prev;
}
return [...prev, { name, value }];
}, []);
const payload = {
id: sketch.id,
libraries: sketch.libraries,
secrets: { data: secrets },
};
// replace the sketch in the cache, so other calls will not overwrite each other
sketchCache = sketchCache.filter((skt) => skt.id !== sketch.id);
sketchCache.push({ ...sketch, secrets });
const init = {
method: 'POST',
body: JSON.stringify(payload),
headers,
};
await this.run(url, init);
}
return;
}
const url = new URL(
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}`
);
const headers = await this.headers();
const data = btoa(
typeof content === 'string'
? content
: new TextDecoder().decode(content)
);
const payload = { data };
const init = {
method: 'POST',
body: JSON.stringify(payload),
headers,
};
await this.run(url, init);
}
async deleteFile(posixPath: string): Promise<void> {
await this.delete(posixPath, 'f');
}
async deleteDirectory(posixPath: string): Promise<void> {
await this.delete(posixPath, 'd');
}
private async delete(posixPath: string, type: ResourceType): Promise<void> {
const url = new URL(
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
);
const headers = await this.headers();
await this.run(url, {
method: 'DELETE',
headers,
});
}
async rename(fromPosixPath: string, toPosixPath: string): Promise<void> {
const url = new URL(`${this.domain('v3')}/files/mv`);
const headers = await this.headers();
const payload = {
from: `$HOME/sketches_v2${fromPosixPath}`,
to: `$HOME/sketches_v2${toPosixPath}`,
};
const init = {
method: 'POST',
body: JSON.stringify(payload),
headers,
};
await this.run(url, init, ResponseResultProvider.NOOP);
}
async editSketch({
id,
params,
}: {
id: string;
params: Record<string, unknown>;
}): Promise<Create.Sketch> {
const url = new URL(`${this.domain()}/sketches/${id}`);
const headers = await this.headers();
const result = await this.run<Create.Sketch>(url, {
method: 'POST',
body: JSON.stringify({ id, ...params }),
headers,
});
return result;
}
async copy(fromPosixPath: string, toPosixPath: string): Promise<void> {
const payload = {
from: `$HOME/sketches_v2${fromPosixPath}`,
to: `$HOME/sketches_v2${toPosixPath}`,
};
const url = new URL(`${this.domain('v3')}/files/cp`);
const headers = await this.headers();
const init = {
method: 'POST',
body: JSON.stringify(payload),
headers,
};
await this.run(url, init, ResponseResultProvider.NOOP);
}
private async run<T>(
requestInfo: RequestInfo | URL,
init: RequestInit | undefined,
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
): Promise<T> {
const response = await fetch(
requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
init
);
if (!response.ok) {
let details: string | undefined = undefined;
try {
details = await response.json();
} catch (e) {
console.error('Cloud not get the error details.', e);
}
const { statusText, status } = response;
throw new CreateError(statusText, status, details);
}
const result = await resultProvider(response);
return result;
}
private async headers(): Promise<Record<string, string>> {
const token = await this.token();
return {
'content-type': 'application/json',
accept: 'application/json',
authorization: `Bearer ${token}`,
};
}
private domain(apiVersion = 'v2'): string {
const endpoint =
this.arduinoPreferences['arduino.cloud.sketchSyncEnpoint'];
return `${endpoint}/${apiVersion}`;
}
private async token(): Promise<string> {
return this.authenticationService.session?.accessToken || '';
}
}
export namespace CreateApi {
export const defaultInoContent = `/*
*/
void setup() {
}
void loop() {
}
`;
}
export namespace Create {
export interface Sketch {
readonly name: string;
readonly path: string;
readonly modified_at: string;
readonly created_at: string;
readonly secrets?: { name: string; value: string }[];
readonly id: string;
readonly is_public: boolean;
// readonly board_fqbn: '',
// readonly board_name: '',
// readonly board_type: 'serial' | 'network' | 'cloud' | '',
readonly href?: string;
readonly libraries: string[];
// readonly tutorials: string[] | null;
// readonly types: string[] | null;
// readonly user_id: string;
}
export type ResourceType = 'sketch' | 'folder' | 'file';
export const arduino_secrets_file = 'arduino_secrets.h';
export interface Resource {
readonly name: string;
/**
* Note: this path is **not** the POSIX path we use. It has the leading segments with the `user_id`.
*/
readonly path: string;
readonly type: ResourceType;
readonly sketchId: string;
readonly modified_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
readonly children?: number; // For 'sketch' and 'folder' types.
readonly size?: number; // For 'sketch' type only.
readonly isPublic?: boolean; // For 'sketch' type only.
readonly mimetype?: string; // For 'file' type.
readonly href?: string;
}
export namespace Resource {
export function is(arg: any): arg is Resource {
return (
!!arg &&
'name' in arg &&
typeof arg['name'] === 'string' &&
'path' in arg &&
typeof arg['path'] === 'string' &&
'type' in arg &&
typeof arg['type'] === 'string' &&
'modified_at' in arg &&
typeof arg['modified_at'] === 'string' &&
(arg['type'] === 'sketch' ||
arg['type'] === 'folder' ||
arg['type'] === 'file')
);
}
}
export type RawResource = Omit<Resource, 'sketchId' | 'isPublic'>;
}
export class CreateError extends Error {
constructor(
message: string,
readonly status: number,
readonly details?: string
) {
super(message);
Object.setPrototypeOf(this, CreateError.prototype);
}
}

View File

@ -0,0 +1,212 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { Event } from '@theia/core/lib/common/event';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
Stat,
FileType,
FileChange,
FileWriteOptions,
FileDeleteOptions,
FileOverwriteOptions,
FileSystemProvider,
FileSystemProviderError,
FileSystemProviderErrorCode,
FileSystemProviderCapabilities,
WatchOptions,
} from '@theia/filesystem/lib/common/files';
import {
FileService,
FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { Create, CreateApi } from './create-api';
import { CreateUri } from './create-uri';
import { SketchesService } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences';
export const REMOTE_ONLY_FILES = [
'sketch.json',
'thingsProperties.h',
'thingProperties.h',
];
@injectable()
export class CreateFsProvider
implements
FileSystemProvider,
FrontendApplicationContribution,
FileServiceContribution
{
@inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService;
@inject(CreateApi)
protected readonly createApi: CreateApi;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
protected readonly toDispose = new DisposableCollection();
readonly onFileWatchError: Event<void> = Event.None;
readonly onDidChangeFile: Event<readonly FileChange[]> = Event.None;
readonly onDidChangeCapabilities: Event<void> = Event.None;
readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.PathCaseSensitive |
FileSystemProviderCapabilities.Access;
onStop(): void {
this.toDispose.dispose();
}
registerFileSystemProviders(service: FileService): void {
service.onWillActivateFileSystemProvider((event) => {
if (event.scheme === CreateUri.scheme) {
event.waitUntil(
(async () => {
service.registerProvider(CreateUri.scheme, this);
})()
);
}
});
}
watch(uri: URI, opts: WatchOptions): Disposable {
return Disposable.NULL;
}
async stat(uri: URI): Promise<Stat> {
if (CreateUri.equals(CreateUri.root, uri)) {
this.getCreateApi; // This will throw when not logged in.
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0,
};
}
const resource = await this.getCreateApi.stat(uri.path.toString());
const mtime = Date.parse(resource.modified_at);
return {
type: this.toFileType(resource.type),
ctime: mtime,
mtime,
size: 0,
};
}
async mkdir(uri: URI): Promise<void> {
await this.getCreateApi.createDirectory(uri.path.toString());
}
async readdir(uri: URI): Promise<[string, FileType][]> {
const resources = await this.getCreateApi.readDirectory(
uri.path.toString(),
{
secrets: true,
}
);
return resources
.filter((res) => !REMOTE_ONLY_FILES.includes(res.name))
.map(({ name, type }) => [name, this.toFileType(type)]);
}
async delete(uri: URI, opts: FileDeleteOptions): Promise<void> {
return;
if (!opts.recursive) {
throw new Error(
'Arduino Create file-system provider does not support non-recursive deletion.'
);
}
const stat = await this.stat(uri);
if (!stat) {
throw new FileSystemProviderError(
'File not found.',
FileSystemProviderErrorCode.FileNotFound
);
}
switch (stat.type) {
case FileType.Directory: {
await this.getCreateApi.deleteDirectory(uri.path.toString());
break;
}
case FileType.File: {
await this.getCreateApi.deleteFile(uri.path.toString());
break;
}
default: {
throw new FileSystemProviderError(
`Unexpected file type '${
stat.type
}' for resource: ${uri.toString()}`,
FileSystemProviderErrorCode.Unknown
);
}
}
}
async rename(
oldUri: URI,
newUri: URI,
options: FileOverwriteOptions
): Promise<void> {
await this.getCreateApi.rename(
oldUri.path.toString(),
newUri.path.toString()
);
}
async readFile(uri: URI): Promise<Uint8Array> {
const content = await this.getCreateApi.readFile(uri.path.toString());
return new TextEncoder().encode(content);
}
async writeFile(
uri: URI,
content: Uint8Array,
options: FileWriteOptions
): Promise<void> {
await this.getCreateApi.writeFile(uri.path.toString(), content);
}
async access(uri: URI, mode?: number): Promise<void> {
this.getCreateApi; // Will throw if not logged in.
}
public toFileType(type: Create.ResourceType): FileType {
switch (type) {
case 'file':
return FileType.File;
case 'sketch':
case 'folder':
return FileType.Directory;
default:
return FileType.Unknown;
}
}
private get getCreateApi(): CreateApi {
const { session } = this.authenticationService;
if (!session) {
throw new FileSystemProviderError(
'Not logged in.',
FileSystemProviderErrorCode.NoPermissions
);
}
return this.createApi.init(
this.authenticationService,
this.arduinoPreferences
);
}
}

View File

@ -0,0 +1,59 @@
export const posix = { sep: '/' };
// TODO: poor man's `path.join(path, '..')` in the browser.
export function parentPosix(path: string): string {
const segments = path.split(posix.sep) || [];
segments.pop();
let modified = segments.join(posix.sep);
if (path.charAt(path.length - 1) === posix.sep) {
modified += posix.sep;
}
return modified;
}
export function basename(path: string): string {
const segments = path.split(posix.sep) || [];
return segments.pop()!;
}
export function posixSegments(posixPath: string): string[] {
return posixPath.split(posix.sep).filter((segment) => !!segment);
}
/**
* Splits the `raw` path into two segments, a root that contains user information and the relevant POSIX path. \
* For examples:
* ```
* `29ad0829759028dde9b877343fa3b0e1:testrest/sketches_v2/xxx_folder/xxx_sub_folder/sketch_in_folder/sketch_in_folder.ino`
* ```
* will be:
* ```
* ['29ad0829759028dde9b877343fa3b0e1:testrest/sketches_v2', '/xxx_folder/xxx_sub_folder/sketch_in_folder/sketch_in_folder.ino']
* ```
*/
export function splitSketchPath(
raw: string,
sep = '/sketches_v2/'
): [string, string] {
if (!sep) {
throw new Error('Invalid separator. Cannot be zero length.');
}
const index = raw.indexOf(sep);
if (index === -1) {
throw new Error(`Invalid path pattern. Raw path was '${raw}'.`);
}
const createRoot = raw.substring(0, index + sep.length - 1); // TODO: validate the `createRoot` format.
const posixPath = raw.substr(index + sep.length - 1);
if (!posixPath) {
throw new Error(`Could not extract POSIX path from '${raw}'.`);
}
return [createRoot, posixPath];
}
export function toPosixPath(raw: string): string {
if (raw === posix.sep) {
return posix.sep; // Handles the root resource case.
}
const [, posixPath] = splitSketchPath(raw);
return posixPath;
}

View File

@ -0,0 +1,39 @@
import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { Create } from './create-api';
import { toPosixPath, parentPosix, posix } from './create-paths';
export namespace CreateUri {
export const scheme = 'arduino-create';
export const root = toUri(posix.sep);
export function toUri(posixPathOrResource: string | Create.Resource): URI {
const posixPath =
typeof posixPathOrResource === 'string'
? posixPathOrResource
: toPosixPath(posixPathOrResource.path);
return new URI(
Uri.parse(posixPath).with({ scheme, authority: 'create' })
);
}
export function is(uri: URI): boolean {
return uri.scheme === scheme;
}
export function equals(left: URI, right: URI): boolean {
return is(left) && is(right) && left.toString() === right.toString();
}
export function parent(uri: URI): URI {
if (!is(uri)) {
throw new Error(
`Invalid URI scheme. Expected '${scheme}' got '${uri.scheme}' instead.`
);
}
if (equals(uri, root)) {
return uri;
}
return toUri(parentPosix(uri.path.toString()));
}
}

View File

@ -85,7 +85,11 @@
],
"colors": {
"list.highlightForeground": "#005c5f",
"list.activeSelectionBackground": "#005c5f",
"list.activeSelectionForeground": "#424242",
"list.activeSelectionBackground": "#DAE3E3",
"list.inactiveSelectionForeground": "#424242",
"list.inactiveSelectionBackground": "#DAE3E3",
"list.hoverBackground": "#ECF1F1",
"progressBar.background": "#005c5f",
"editor.background": "#ffffff",
"editorCursor.foreground": "#434f54",
@ -110,6 +114,7 @@
"activityBar.foreground": "#616161",
"statusBar.background": "#005c5f",
"secondaryButton.background": "#b5c8c9",
"secondaryButton.foreground": "#ececec",
"secondaryButton.hoverBackground": "#dae3e3",
"arduino.branding.primary": "#00979d",
"arduino.branding.secondary": "#b5c8c9",

View File

@ -0,0 +1,175 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { clipboard } from 'electron';
import {
AbstractDialog,
ReactWidget,
DialogProps,
} from '@theia/core/lib/browser';
import { CreateApi } from '../create/create-api';
const RadioButton = (props: {
id: string;
changed: (evt: React.BaseSyntheticEvent) => void;
value: string;
isSelected: boolean;
isDisabled: boolean;
label: string;
}) => {
return (
<p className="RadioButton">
<input
id={props.id}
onChange={props.changed}
value={props.value}
type="radio"
checked={props.isSelected}
disabled={props.isDisabled}
/>
<label htmlFor={props.id}>{props.label}</label>
</p>
);
};
export const ShareSketchComponent = ({
treeNode,
createApi,
domain = 'https://create.arduino.cc',
}: {
treeNode: any;
createApi: CreateApi;
domain?: string;
}): React.ReactElement => {
// const [publicVisibility, setPublicVisibility] = React.useState<boolean>(
// treeNode.isPublic
// );
const [loading, setloading] = React.useState<boolean>(false);
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
setloading(true);
const sketch = await createApi.editSketch({
id: treeNode.sketchId,
params: {
is_public: event.target.value === 'private' ? false : true,
},
});
// setPublicVisibility(sketch.is_public);
treeNode.isPublic = sketch.is_public;
setloading(false);
};
const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`;
const embedLink = `<iframe src="${sketchLink}?embed" style="height:510px;width:100%;margin:10px 0" frameborder=0></iframe>`;
return (
<div id="widget-container arduino-sharesketch-dialog">
<p>Choose visibility of your Sketch:</p>
<RadioButton
changed={radioChangeHandler}
id="1"
isSelected={treeNode.isPublic === false}
label="Private. Only you can view the Sketch."
value="private"
isDisabled={loading}
/>
<RadioButton
changed={radioChangeHandler}
id="2"
isSelected={treeNode.isPublic === true}
label="Public. Anyone with the link can view the Sketch."
value="public"
isDisabled={loading}
/>
{treeNode.isPublic && (
<div>
<p>Link:</p>
<div className="sketch-link">
<input
type="text"
readOnly
value={sketchLink}
className="theia-input"
/>
<button
onClick={() => clipboard.writeText(sketchLink)}
value="copy"
className="theia-button secondary"
>
Copy
</button>
</div>
<p>Embed:</p>
<div className="sketch-link-embed">
<textarea
readOnly
value={embedLink}
className="theia-input stretch"
/>
</div>
</div>
)}
</div>
);
};
@injectable()
export class ShareSketchWidget extends ReactWidget {
constructor(private treeNode: any, private createApi: CreateApi) {
super();
}
protected render(): React.ReactNode {
return (
<ShareSketchComponent
treeNode={this.treeNode}
createApi={this.createApi}
/>
);
}
}
@injectable()
export class ShareSketchDialogProps extends DialogProps {
readonly node: any;
readonly createApi: CreateApi;
}
@injectable()
export class ShareSketchDialog extends AbstractDialog<void> {
protected widget: ShareSketchWidget;
constructor(
@inject(ShareSketchDialogProps)
protected readonly props: ShareSketchDialogProps
) {
super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog');
this.widget = new ShareSketchWidget(props.node, props.createApi);
}
get value(): void {
return;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
}

View File

@ -0,0 +1,69 @@
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
ConfirmDialog,
ConfirmDialogProps,
DialogError,
} from '@theia/core/lib/browser/dialogs';
@injectable()
export class DoNotAskAgainConfirmDialogProps extends ConfirmDialogProps {
readonly onAccept: () => Promise<void>;
}
@injectable()
export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
protected readonly doNotAskAgainCheckbox: HTMLInputElement;
constructor(
@inject(DoNotAskAgainConfirmDialogProps)
protected readonly props: DoNotAskAgainConfirmDialogProps
) {
super(props);
this.controlPanel.removeChild(this.errorMessageNode);
const doNotAskAgainNode = document.createElement('div');
doNotAskAgainNode.setAttribute('style', 'flex: 2');
this.controlPanel.insertBefore(
doNotAskAgainNode,
this.controlPanel.firstChild
);
const doNotAskAgainLabel = document.createElement('label');
doNotAskAgainLabel.classList.add('flex-line');
doNotAskAgainNode.appendChild(doNotAskAgainLabel);
doNotAskAgainLabel.textContent = "Don't ask again";
this.doNotAskAgainCheckbox = document.createElement('input');
this.doNotAskAgainCheckbox.setAttribute('align-self', 'center');
doNotAskAgainLabel.appendChild(this.doNotAskAgainCheckbox);
this.doNotAskAgainCheckbox.type = 'checkbox';
}
protected async accept(): Promise<void> {
if (!this.resolve) {
return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
if (this.doNotAskAgainCheckbox.checked) {
await this.props.onAccept();
}
this.resolve(value);
Widget.detach(this);
}
}
protected setErrorMessage(error: DialogError): void {
if (this.acceptButton) {
this.acceptButton.disabled = !DialogError.getResult(error);
}
}
}

View File

@ -0,0 +1,167 @@
import { inject, injectable } from 'inversify';
import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util';
import {
FileSystemProvider,
FileSystemProviderError,
FileSystemProviderErrorCode,
} from '@theia/filesystem/lib/common/files';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider';
import {
FileService,
FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { AuthenticationSession } from '../../common/protocol/authentication-service';
import { ConfigService } from '../../common/protocol';
export namespace LocalCacheUri {
export const scheme = 'arduino-local-cache';
export const root = new URI(
Uri.parse('/').with({ scheme, authority: 'create' })
);
}
@injectable()
export class LocalCacheFsProvider
implements
FileServiceContribution,
DelegatingFileSystemProvider.URIConverter
{
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService;
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
readonly ready = new Deferred<void>();
private _localCacheRoot: URI;
registerFileSystemProviders(fileService: FileService): void {
fileService.onWillActivateFileSystemProvider(async (event) => {
if (event.scheme === LocalCacheUri.scheme) {
event.waitUntil(
(async () => {
this.init(fileService);
const provider = await this.createProvider(fileService);
fileService.registerProvider(
LocalCacheUri.scheme,
provider
);
})()
);
}
});
}
to(resource: URI): URI | undefined {
const relativePath = LocalCacheUri.root.relative(resource);
if (relativePath) {
return this.currentUserUri.resolve(relativePath).normalizePath();
}
return undefined;
}
from(resource: URI): URI | undefined {
const relativePath = this.currentUserUri.relative(resource);
if (relativePath) {
return LocalCacheUri.root.resolve(relativePath);
}
return undefined;
}
protected async createProvider(
fileService: FileService
): Promise<FileSystemProvider> {
const delegate = await fileService.activateProvider('file');
await this.ready.promise;
return new DelegatingFileSystemProvider(
delegate,
{
uriConverter: this,
},
new DisposableCollection(
delegate.watch(this.localCacheRoot, {
excludes: [],
recursive: true,
})
)
);
}
protected async init(fileService: FileService): Promise<void> {
const config = await this.configService.getConfiguration();
this._localCacheRoot = new URI(config.dataDirUri);
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
this._localCacheRoot = this._localCacheRoot.resolve(segment);
await fileService.createFolder(this._localCacheRoot);
}
this.session(fileService).then(() => this.ready.resolve());
this.authenticationService.onSessionDidChange(async (session) => {
if (session) {
await this.ensureExists(session, fileService);
}
});
}
private get currentUserUri(): URI {
const { session } = this.authenticationService;
if (!session) {
throw new FileSystemProviderError(
'Not logged in.',
FileSystemProviderErrorCode.NoPermissions
);
}
return this.toUri(session);
}
private get localCacheRoot(): URI {
return this._localCacheRoot;
}
private async session(
fileService: FileService
): Promise<AuthenticationSession> {
return new Promise<AuthenticationSession>(async (resolve) => {
const { session } = this.authenticationService;
if (session) {
await this.ensureExists(session, fileService);
resolve(session);
return;
}
const toDispose = new DisposableCollection();
toDispose.push(
this.authenticationService.onSessionDidChange(
async (session) => {
if (session) {
await this.ensureExists(session, fileService);
toDispose.dispose();
resolve(session);
}
}
)
);
});
}
private async ensureExists(
session: AuthenticationSession,
fileService: FileService
): Promise<URI> {
const uri = this.toUri(session);
const exists = await fileService.exists(uri);
if (!exists) {
await fileService.createFolder(uri);
}
return uri;
}
private toUri(session: AuthenticationSession): URI {
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
return this._localCacheRoot.resolve(session.id.split('|')[1]);
}
}

View File

@ -48,6 +48,7 @@ export interface Settings extends Index {
verboseOnUpload: boolean; // `arduino.upload.verbose`
verifyAfterUpload: boolean; // `arduino.upload.verify`
enableLsLogs: boolean; // `arduino.language.log`
sketchbookShowAllFiles: boolean; // `arduino.sketchbook.showAllFiles`
sketchbookPath: string; // CLI
additionalUrls: string[]; // CLI
@ -105,6 +106,7 @@ export class SettingsService {
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
sketchbookShowAllFiles,
cliConfig,
] = await Promise.all([
this.preferenceService.get<number>('editor.fontSize', 12),
@ -135,6 +137,10 @@ export class SettingsService {
this.preferenceService.get<boolean>('arduino.upload.verbose', true),
this.preferenceService.get<boolean>('arduino.upload.verify', true),
this.preferenceService.get<boolean>('arduino.language.log', true),
this.preferenceService.get<boolean>(
'arduino.sketchbook.showAllFiles',
false
),
this.configService.getConfiguration(),
]);
const { additionalUrls, sketchDirUri, network } = cliConfig;
@ -154,6 +160,7 @@ export class SettingsService {
verboseOnUpload,
verifyAfterUpload,
enableLsLogs,
sketchbookShowAllFiles,
additionalUrls,
sketchbookPath,
network,
@ -228,6 +235,7 @@ export class SettingsService {
sketchbookPath,
additionalUrls,
network,
sketchbookShowAllFiles,
} = this._settings;
const [config, sketchDirUri] = await Promise.all([
this.configService.getConfiguration(),
@ -294,6 +302,11 @@ export class SettingsService {
enableLsLogs,
PreferenceScope.User
),
this.preferenceService.set(
'arduino.sketchbook.showAllFiles',
sketchbookShowAllFiles,
PreferenceScope.User
),
this.configService.setConfiguration(config),
]);
this.onDidChangeEmitter.fire(this._settings);
@ -373,6 +386,14 @@ export class SettingsComponent extends React.Component<
Browse
</button>
</div>
<label className="flex-line">
<input
type="checkbox"
checked={this.state.sketchbookShowAllFiles === true}
onChange={this.sketchbookShowAllFilesDidChange}
/>
Show files inside Sketches
</label>
<div className="flex-line">
<div className="column">
<div className="flex-line">Editor font size:</div>
@ -762,6 +783,12 @@ export class SettingsComponent extends React.Component<
this.setState({ checkForUpdates: event.target.checked });
};
protected sketchbookShowAllFilesDidChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ sketchbookShowAllFiles: event.target.checked });
};
protected autoSaveDidChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="#626262"><path d="M16 7.992C16 3.58 12.416 0 8 0S0 3.58 0 7.992c0 2.43 1.104 4.62 2.832 6.09c.016.016.032.016.032.032c.144.112.288.224.448.336c.08.048.144.111.224.175A7.98 7.98 0 0 0 8.016 16a7.98 7.98 0 0 0 4.48-1.375c.08-.048.144-.111.224-.16c.144-.111.304-.223.448-.335c.016-.016.032-.016.032-.032c1.696-1.487 2.8-3.676 2.8-6.106zm-8 7.001c-1.504 0-2.88-.48-4.016-1.279c.016-.128.048-.255.08-.383a4.17 4.17 0 0 1 .416-.991c.176-.304.384-.576.64-.816c.24-.24.528-.463.816-.639c.304-.176.624-.304.976-.4A4.15 4.15 0 0 1 8 10.342a4.185 4.185 0 0 1 2.928 1.166c.368.368.656.8.864 1.295c.112.288.192.592.24.911A7.03 7.03 0 0 1 8 14.993zm-2.448-7.4a2.49 2.49 0 0 1-.208-1.024c0-.351.064-.703.208-1.023c.144-.32.336-.607.576-.847c.24-.24.528-.431.848-.575c.32-.144.672-.208 1.024-.208c.368 0 .704.064 1.024.208c.32.144.608.336.848.575c.24.24.432.528.576.847c.144.32.208.672.208 1.023c0 .368-.064.704-.208 1.023a2.84 2.84 0 0 1-.576.848a2.84 2.84 0 0 1-.848.575a2.715 2.715 0 0 1-2.064 0a2.84 2.84 0 0 1-.848-.575a2.526 2.526 0 0 1-.56-.848zm7.424 5.306c0-.032-.016-.048-.016-.08a5.22 5.22 0 0 0-.688-1.406a4.883 4.883 0 0 0-1.088-1.135a5.207 5.207 0 0 0-1.04-.608a2.82 2.82 0 0 0 .464-.383a4.2 4.2 0 0 0 .624-.784a3.624 3.624 0 0 0 .528-1.934a3.71 3.71 0 0 0-.288-1.47a3.799 3.799 0 0 0-.816-1.199a3.845 3.845 0 0 0-1.2-.8a3.72 3.72 0 0 0-1.472-.287a3.72 3.72 0 0 0-1.472.288a3.631 3.631 0 0 0-1.2.815a3.84 3.84 0 0 0-.8 1.199a3.71 3.71 0 0 0-.288 1.47c0 .352.048.688.144 1.007c.096.336.224.64.4.927c.16.288.384.544.624.784c.144.144.304.271.48.383a5.12 5.12 0 0 0-1.04.624c-.416.32-.784.703-1.088 1.119a4.999 4.999 0 0 0-.688 1.406c-.016.032-.016.064-.016.08C1.776 11.636.992 9.91.992 7.992C.992 4.14 4.144.991 8 .991s7.008 3.149 7.008 7.001a6.96 6.96 0 0 1-2.032 4.907z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,14 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.54383 11.5H0.612583C0.129139 9.86839 0.129139 8.13161 0.612583 6.5H3.54383C3.42911 7.32835 3.37272 8.16374 3.37508 9C3.37272 9.83626 3.42911 10.6716 3.54383 11.5Z" fill="#616161"/>
<path d="M3.775 12.75C4.08097 14.3119 4.72054 15.7893 5.65 17.0812C3.6488 16.2492 2.02597 14.7065 1.09375 12.75H3.775Z" fill="#616161"/>
<path d="M3.775 5.25001H1.09375C2.02597 3.29347 3.6488 1.75079 5.65 0.918762C4.72054 2.21073 4.08097 3.68814 3.775 5.25001Z" fill="#616161"/>
<path d="M8.375 0.34375V5.25H5.0625C5.69375 2.6875 6.93125 0.775 8.375 0.34375Z" fill="#616161"/>
<path d="M4.81248 6.5H8.37498V11.5H4.81248C4.56251 9.84271 4.56251 8.15729 4.81248 6.5Z" fill="#616161"/>
<path d="M5.0625 12.75H8.375V17.6562C6.93125 17.225 5.69375 15.3125 5.0625 12.75Z" fill="#616161"/>
<path d="M12.9375 12.75C12.3125 15.3125 11.0625 17.225 9.625 17.6562V12.75H12.9375Z" fill="#616161"/>
<path d="M12.9375 5.25H9.625V0.34375C11.0688 0.775 12.3063 2.6875 12.9375 5.25Z" fill="#616161"/>
<path d="M13.1875 11.5H9.625V6.5H13.1875C13.3115 7.32757 13.3742 8.16318 13.375 9C13.3742 9.83682 13.3115 10.6724 13.1875 11.5Z" fill="#616161"/>
<path d="M14.2251 12.75H16.9063C15.9741 14.7065 14.3513 16.2492 12.3501 17.0812C13.2796 15.7893 13.9191 14.3119 14.2251 12.75Z" fill="#616161"/>
<path d="M14.2251 5.25001C13.9191 3.68814 13.2796 2.21073 12.3501 0.918762C14.3513 1.75079 15.9741 3.29347 16.9063 5.25001H14.2251Z" fill="#616161"/>
<path d="M17.7503 9C17.7517 9.84653 17.6296 10.6887 17.3878 11.5H14.4565C14.5713 10.6716 14.6277 9.83626 14.6253 9C14.6277 8.16374 14.5713 7.32835 14.4565 6.5H17.3878C17.6296 7.31126 17.7517 8.15347 17.7503 9Z" fill="#616161"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,184 @@
.sign-in-title {
margin: 20px 0;
font-weight: bold;
}
.sign-in-desc {
margin: 20px;
line-height: 150%;
}
.sign-in-cta {
margin-left: 0 !important;
}
.sign-in-learnmore {
margin-bottom: 20px;
}
.cloud-sketchbook-tree-icon {
background: url("./cloud-sketchbook-tree-icon.svg") center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
background-size: auto 90%;
}
.sketchbook-trees-container
.p-TabBar[data-orientation="horizontal"]
> .p-TabBar-content {
justify-content: center;
border-bottom: 1px solid var(--theia-tree-indentGuidesStroke);
}
.sketchbook-trees-container
.p-Widget.p-TabBar.p-DockPanel-tabBar
> ul
> li.p-TabBar-tab
> div.p-TabBar-tabLabel {
display: none;
width: 0px;
max-width: 0px;
}
.sketchbook-trees-container
.p-Widget.p-TabBar.p-DockPanel-tabBar
> ul
> li.p-TabBar-tab
> div.p-TabBar-tabCloseIcon {
display: none;
width: 0px;
max-width: 0px;
}
.sketchbook-trees-container
.p-TabBar[data-orientation="horizontal"]
.p-TabBar-tab {
min-width: 55px;
}
.sketchbook-trees-container
.p-Widget.p-TabBar.p-DockPanel-tabBar
> ul
> li.p-TabBar-tab {
padding-left: 20px;
}
.sketchbook-trees-container
.p-Widget.p-TabBar.p-DockPanel-tabBar
> ul
> li.p-TabBar-tab.p-mod-current {
border-bottom: 2px solid var(--theia-statusBar-background);
}
.sketchbook-trees-container .center {
height: 100%;
display: flex;
align-items: center;
}
.cloud-sketchbook-welcome {
flex-direction: column;
text-align: center;
}
.cloud-sketchbook-welcome > .item {
align-items: flex-end;
}
.cloud-sketchbook-welcome > .item .link {
cursor: pointer;
color: var(--theia-arduino-branding-primary);
}
.pull-sketch-icon {
background: url("./pull-sketch-icon.svg") center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.push-sketch-icon {
background: url("./push-sketch-icon.svg") center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.account-icon {
background: url("./account-icon.svg") center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
border-radius: 50%;
overflow: hidden;
}
.account-icon > img {
max-width: 100%;
max-height: 100%;
}
.connected-status-icon {
background: url("./connected-status-icon.svg") center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.offline-status-icon {
background: url("./offline-status-icon.svg") center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.refresh-icon {
background: url("./refresh-icon.svg") center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.rotating {
animation: rotating 1s ease-in-out infinite;
}
.cloud-connection-status {
border-top: 1px solid var(--theia-tree-indentGuidesStroke);
border-bottom: none;
}
.cloud-connection-status .item {
padding: 8px;
}
.cloud-connection-status .status {
flex-grow: 2;
}
.cloud-connection-status .actions {
border-left: 1px solid var(--theia-tree-indentGuidesStroke);
border-right: 1px solid var(--theia-tree-indentGuidesStroke);
}
.composite-node {
height: 100%;
display: flex;
flex-direction: column-reverse;
}
.composite-node .tree-container {
height: 100%;
}
.arduino-share-sketch-dialog .sketch-link {
display: flex;
}
.arduino-share-sketch-dialog .sketch-link input {
flex: 1;
}
.arduino-share-sketch-dialog .sketch-link-embed textarea {
width: 100%;
}

View File

@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="4" r="4" fill="#1DA086"/>
</svg>

After

Width:  |  Height:  |  Size: 144 B

View File

@ -8,6 +8,8 @@
@import './editor.css';
@import './settings-dialog.css';
@import './debug.css';
@import './sketchbook.css';
@import './cloud-sketchbook.css';
.theia-input.warning:focus {
outline-width: 1px;
@ -61,7 +63,7 @@ button.theia-button {
button.theia-button.secondary {
background-color: var(--theia-secondaryButton-background);
color: var(--theia-foreground);
color: var(--theia-secondaryButton-foreground);
}
button.theia-button.main {
@ -84,3 +86,9 @@ button.theia-button.main {
height: 4px;
width: 66%;
}
.flex-line {
display: flex;
align-items: center;
white-space: nowrap;
}

View File

@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="4" r="4" fill="#95A5A6"/>
</svg>

After

Width:  |  Height:  |  Size: 144 B

View File

@ -0,0 +1,8 @@
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M9.35506 9.855L7.35506 11.855C7.26028 11.948 7.13283 12 7.00008 12C6.86733 12 6.73988 11.948 6.6451 11.855L4.6451 9.855C4.59848 9.80838 4.5615 9.75304 4.53627 9.69213C4.51104 9.63122 4.49805 9.56594 4.49805 9.50001C4.49804 9.36687 4.55093 9.23917 4.64508 9.14502C4.73923 9.05087 4.86692 8.99797 5.00006 8.99797C5.06599 8.99797 5.13127 9.01095 5.19218 9.03618C5.25309 9.06141 5.30844 9.09838 5.35506 9.145L6.50006 10.2949V4.5C6.50006 4.36739 6.55274 4.24021 6.64651 4.14645C6.74027 4.05268 6.86745 4 7.00006 4C7.13267 4 7.25984 4.05268 7.35361 4.14645C7.44738 4.24021 7.50006 4.36739 7.50006 4.5V10.2949L8.64506 9.145C8.73921 9.05085 8.86691 8.99795 9.00006 8.99795C9.13321 8.99795 9.26091 9.05085 9.35506 9.145C9.44921 9.23915 9.5021 9.36685 9.5021 9.5C9.5021 9.63315 9.44921 9.76085 9.35506 9.855Z"
fill="#008184" />
<path
d="M10.75 7.99999H9C8.86739 7.99999 8.74022 7.94732 8.64645 7.85355C8.55268 7.75978 8.5 7.6326 8.5 7.49999C8.5 7.36738 8.55268 7.24021 8.64645 7.14644C8.74022 7.05267 8.86739 6.99999 9 6.99999H10.75C11.198 6.99957 11.6289 6.82748 11.9539 6.51912C12.279 6.21076 12.4735 5.78957 12.4975 5.34218C12.5215 4.89478 12.3732 4.45519 12.083 4.11381C11.7929 3.77243 11.3829 3.55521 10.9375 3.50683C10.8238 3.49477 10.7176 3.44408 10.6368 3.36321C10.5559 3.28235 10.5052 3.17621 10.4932 3.06249C10.4474 2.63252 10.2439 2.23477 9.92194 1.94616C9.59996 1.65755 9.1824 1.49858 8.75 1.49999C8.42739 1.49788 8.11079 1.58727 7.83692 1.7578C7.74186 1.81745 7.62955 1.84351 7.51793 1.83182C7.40632 1.82014 7.30184 1.77139 7.22119 1.69335C6.76046 1.24566 6.14241 0.996689 5.5 0.999993C4.89213 1.00014 4.30514 1.22176 3.84885 1.6234C3.39257 2.02505 3.09826 2.57918 3.021 3.18212C3.0093 3.27543 2.97153 3.36354 2.91202 3.43635C2.85252 3.50915 2.77368 3.56371 2.68458 3.59374C2.29236 3.72766 1.9604 3.99644 1.7478 4.35221C1.5352 4.70798 1.45576 5.12765 1.52363 5.53651C1.5915 5.94537 1.80227 6.31687 2.11841 6.58487C2.43455 6.85287 2.83555 6.99998 3.25 6.99999H5C5.13261 6.99999 5.25979 7.05267 5.35356 7.14644C5.44732 7.24021 5.5 7.36738 5.5 7.49999C5.5 7.6326 5.44732 7.75978 5.35356 7.85355C5.25979 7.94732 5.13261 7.99999 5 7.99999H3.25C2.62484 7.99986 2.01838 7.78676 1.53053 7.39582C1.04269 7.00488 0.702574 6.45942 0.566223 5.84931C0.429872 5.2392 0.505423 4.60084 0.780426 4.03942C1.05543 3.47799 1.51348 3.02699 2.0791 2.76073C2.24864 1.97926 2.68041 1.27929 3.3027 0.777092C3.92499 0.274892 4.70035 0.000678213 5.5 -6.73109e-06C6.27505 -0.00288261 7.02853 0.255097 7.63916 0.732413C8.00591 0.57151 8.40329 0.492395 8.8037 0.500566C9.20411 0.508738 9.59793 0.604 9.95781 0.779733C10.3177 0.955465 10.635 1.20745 10.8876 1.51818C11.1403 1.82892 11.3223 2.19094 11.4209 2.57909C12.0715 2.74321 12.6398 3.13936 13.0188 3.69306C13.3979 4.24676 13.5616 4.91986 13.4792 5.58579C13.3967 6.25172 13.0739 6.8646 12.5713 7.30919C12.0687 7.75378 11.421 7.99945 10.75 7.99999Z"
fill="#008184" />
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,8 @@
<svg width="14" height="11" viewBox="0 0 14 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M9.35509 5.85499C9.26032 5.94794 9.13286 6.00001 9.00011 6.00001C8.86736 6.00001 8.73991 5.94794 8.64513 5.85499L7.50009 4.70494V10.5C7.50009 10.6326 7.44741 10.7598 7.35365 10.8535C7.25988 10.9473 7.1327 11 7.00009 11C6.86748 11 6.74031 10.9473 6.64654 10.8535C6.55277 10.7598 6.50009 10.6326 6.50009 10.5V4.70494L5.35509 5.85499C5.26094 5.94914 5.13324 6.00204 5.00009 6.00204C4.86694 6.00204 4.73924 5.94914 4.64509 5.85499C4.55094 5.76084 4.49805 5.63314 4.49805 5.49999C4.49805 5.36684 4.55094 5.23914 4.64509 5.14499L6.64509 3.14499C6.73947 3.05128 6.86707 2.99869 7.00007 2.99869C7.13307 2.99869 7.26068 3.05128 7.35505 3.14499L9.35505 5.14499C9.44875 5.23938 9.50134 5.36698 9.50135 5.49998C9.50136 5.63298 9.44878 5.76059 9.35509 5.85499Z"
fill="#008184" />
<path
d="M10.75 7.99999H9C8.86739 7.99999 8.74022 7.94732 8.64645 7.85355C8.55268 7.75978 8.5 7.6326 8.5 7.49999C8.5 7.36738 8.55268 7.24021 8.64645 7.14644C8.74022 7.05267 8.86739 6.99999 9 6.99999H10.75C11.198 6.99957 11.6289 6.82748 11.9539 6.51912C12.279 6.21076 12.4735 5.78957 12.4975 5.34218C12.5215 4.89478 12.3732 4.45519 12.083 4.11381C11.7929 3.77243 11.3829 3.55521 10.9375 3.50683C10.8238 3.49477 10.7176 3.44408 10.6368 3.36321C10.5559 3.28235 10.5052 3.17621 10.4932 3.06249C10.4474 2.63252 10.2439 2.23477 9.92194 1.94616C9.59996 1.65755 9.1824 1.49858 8.75 1.49999C8.42739 1.49788 8.11079 1.58727 7.83692 1.7578C7.74185 1.81743 7.62955 1.84349 7.51794 1.8318C7.40633 1.82012 7.30185 1.77138 7.22119 1.69335C6.76046 1.24566 6.14241 0.996689 5.5 0.999993C4.89213 1.00014 4.30514 1.22176 3.84885 1.6234C3.39257 2.02505 3.09826 2.57918 3.021 3.18212C3.0093 3.27543 2.97153 3.36354 2.91202 3.43635C2.85252 3.50915 2.77368 3.56371 2.68458 3.59374C2.29236 3.72766 1.9604 3.99644 1.7478 4.35221C1.5352 4.70798 1.45576 5.12765 1.52363 5.53651C1.5915 5.94537 1.80227 6.31687 2.11841 6.58487C2.43455 6.85287 2.83555 6.99998 3.25 6.99999H5C5.13261 6.99999 5.25979 7.05267 5.35356 7.14644C5.44732 7.24021 5.5 7.36738 5.5 7.49999C5.5 7.6326 5.44732 7.75978 5.35356 7.85355C5.25979 7.94732 5.13261 7.99999 5 7.99999H3.25C2.62484 7.99986 2.01838 7.78676 1.53053 7.39582C1.04269 7.00488 0.702574 6.45942 0.566223 5.84931C0.429872 5.2392 0.505423 4.60084 0.780426 4.03942C1.05543 3.47799 1.51348 3.02699 2.0791 2.76073C2.24864 1.97926 2.68041 1.27929 3.3027 0.777092C3.92499 0.274892 4.70035 0.000678213 5.5 -6.73109e-06C6.27505 -0.00288261 7.02853 0.255097 7.63916 0.732413C8.00591 0.57151 8.40329 0.492395 8.8037 0.500566C9.20411 0.508738 9.59793 0.604 9.95781 0.779733C10.3177 0.955465 10.635 1.20745 10.8876 1.51818C11.1403 1.82892 11.3223 2.19094 11.4209 2.57909C12.0715 2.74321 12.6398 3.13936 13.0188 3.69306C13.3979 4.24676 13.5616 4.91986 13.4792 5.58579C13.3967 6.25172 13.0739 6.8646 12.5713 7.30919C12.0687 7.75378 11.421 7.99945 10.75 7.99999Z"
fill="#008184" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00129 14.9989C8.85781 14.9989 10.6383 14.2614 11.951 12.9486C13.2638 11.6359 14.0013 9.85539 14.0013 7.99887C14.0013 7.86626 13.9486 7.73909 13.8548 7.64532C13.7611 7.55155 13.6339 7.49887 13.5013 7.49887C13.3687 7.49887 13.2415 7.55155 13.1477 7.64532C13.054 7.73909 13.0013 7.86626 13.0013 7.99887C13.001 9.3709 12.5305 10.7014 11.6682 11.7686C10.8059 12.8358 9.60391 13.5752 8.26254 13.8636C6.92117 14.152 5.52147 13.972 4.29674 13.3535C3.072 12.7351 2.09622 11.7156 1.53204 10.4649C0.967851 9.21424 0.849341 7.808 1.19626 6.48056C1.54318 5.15311 2.33457 3.98466 3.43852 3.16996C4.54248 2.35526 5.8923 1.94352 7.26302 2.00337C8.63374 2.06322 9.94255 2.59105 10.9713 3.49887L8.50129 3.49887C8.36868 3.49887 8.24151 3.55155 8.14774 3.64532C8.05397 3.73909 8.00129 3.86626 8.00129 3.99887C8.00129 4.13148 8.05397 4.25866 8.14774 4.35242C8.24151 4.44619 8.36868 4.49887 8.50129 4.49887L12.0013 4.49887C12.1339 4.49887 12.2611 4.44619 12.3548 4.35243C12.4486 4.25866 12.5013 4.13148 12.5013 3.99887L12.5013 0.498871C12.5013 0.366263 12.4486 0.239086 12.3548 0.145318C12.2611 0.0515498 12.1339 -0.00112862 12.0013 -0.00112863C11.8687 -0.00112863 11.7415 0.0515498 11.6477 0.145318C11.554 0.239086 11.5013 0.366263 11.5013 0.498871L11.5013 2.63887C10.6394 1.91492 9.61569 1.40949 8.5169 1.16542C7.4181 0.921352 6.27666 0.945851 5.18935 1.23684C4.10204 1.52783 3.10093 2.07673 2.27092 2.83699C1.44091 3.59725 0.806473 4.54645 0.421403 5.60411C0.0363342 6.66176 -0.0880083 7.79668 0.0589193 8.91262C0.205847 10.0286 0.619712 11.0926 1.26542 12.0146C1.91113 12.9365 2.76964 13.6891 3.76814 14.2087C4.76665 14.7282 5.87572 14.9992 7.00129 14.9989Z" fill="#424242"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,4 @@
<svg width="8" height="7" viewBox="0 0 8 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 6.18999C2.415 6.18999 2.33 6.16999 2.25 6.12499C1.17 5.49999 0.5 4.33999 0.5 3.09499C0.5 1.84999 1.17 0.689992 2.25 0.0649925C2.49 -0.0700075 2.795 0.00999246 2.935 0.249992C3.07 0.489992 2.99 0.794992 2.75 0.934992C1.98 1.37499 1.5 2.20499 1.5 3.09499C1.5 3.98499 1.98 4.81499 2.75 5.25499C2.99 5.39499 3.07 5.69999 2.935 5.93999C2.84 6.09999 2.675 6.18999 2.5 6.18999Z" fill="#008184"/>
<path d="M5.49993 6.18999C5.32493 6.18999 5.15993 6.09999 5.06493 5.93999C4.92493 5.69999 5.00993 5.39499 5.24993 5.25499C6.01993 4.81499 6.49993 3.98499 6.49993 3.09499C6.49993 2.20499 6.01993 1.37499 5.24993 0.934992C5.00993 0.794992 4.92993 0.489992 5.06493 0.249992C5.20493 0.00999246 5.50993 -0.0700075 5.74993 0.0649925C6.82993 0.689992 7.49993 1.84999 7.49993 3.09499C7.49993 4.33999 6.82993 5.49999 5.74993 6.12499C5.66993 6.16999 5.58493 6.18999 5.49993 6.18999Z" fill="#008184"/>
</svg>

After

Width:  |  Height:  |  Size: 992 B

View File

@ -0,0 +1,10 @@
<svg width="12" height="2" viewBox="0 0 12 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2C6.55228 2 7 1.55228 7 1C7 0.447715 6.55228 0 6 0C5.44772 0 5 0.447715 5 1C5 1.55228 5.44772 2 6 2Z"
fill="#008184" />
<path
d="M10.5 2C11.0523 2 11.5 1.55228 11.5 1C11.5 0.447715 11.0523 0 10.5 0C9.94772 0 9.5 0.447715 9.5 1C9.5 1.55228 9.94772 2 10.5 2Z"
fill="#008184" />
<path
d="M1.5 2C2.05228 2 2.5 1.55228 2.5 1C2.5 0.447715 2.05228 0 1.5 0C0.947715 0 0.5 0.447715 0.5 1C0.5 1.55228 0.947715 2 1.5 2Z"
fill="#008184" />
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@ -0,0 +1,11 @@
<svg width="20" height="14" viewBox="0 0 20 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M19.2492 12.2509L17.4992 9.9197V1.37595C17.4987 1.21033 17.4327 1.05164 17.3156 0.934533C17.1985 0.817426 17.0398 0.751424 16.8742 0.750946H3.12422C2.95861 0.751441 2.79992 0.817448 2.68282 0.934551C2.56572 1.05165 2.49971 1.21034 2.49922 1.37595V9.9197L0.749217 12.2509C0.679462 12.3442 0.637219 12.455 0.627276 12.571C0.617334 12.687 0.640091 12.8035 0.692967 12.9072C0.745302 13.0099 0.824858 13.0964 0.922952 13.157C1.02105 13.2176 1.13391 13.2501 1.24922 13.2509H18.7492C18.8645 13.2501 18.9774 13.2176 19.0755 13.157C19.1736 13.0964 19.2531 13.0099 19.3055 12.9072C19.3583 12.8035 19.3811 12.687 19.3712 12.571C19.3612 12.455 19.319 12.3442 19.2492 12.2509ZM3.74922 2.00095H16.2492V9.50095H3.74922V2.00095ZM2.49922 12.0009L3.43672 10.7509H16.5617L17.4992 12.0009H2.49922Z"
fill="#616161" />
<path
d="M7.49987 7.62501C7.37627 7.62498 7.25546 7.58831 7.15271 7.51964C7.04995 7.45096 6.96986 7.35336 6.92257 7.23917C6.87527 7.12498 6.8629 6.99934 6.887 6.87812C6.91111 6.7569 6.97061 6.64554 7.05799 6.55814L9.55799 4.05814C9.67587 3.94429 9.83374 3.88129 9.99762 3.88271C10.1615 3.88414 10.3182 3.94987 10.4341 4.06575C10.55 4.18163 10.6157 4.33839 10.6172 4.50226C10.6186 4.66613 10.5556 4.82401 10.4417 4.94189L7.94174 7.44189C7.88379 7.50002 7.81491 7.54612 7.73908 7.57755C7.66325 7.60898 7.58195 7.62511 7.49987 7.62501Z"
fill="#616161" />
<path
d="M6.24987 5.12501C6.12627 5.12498 6.00546 5.08831 5.90271 5.01964C5.79995 4.95096 5.71986 4.85336 5.67257 4.73917C5.62527 4.62498 5.6129 4.49934 5.637 4.37812C5.66111 4.2569 5.72061 4.14554 5.80799 4.05814L6.43299 3.43314C6.55087 3.31929 6.70874 3.25629 6.87262 3.25771C7.03649 3.25914 7.19325 3.32487 7.30913 3.44075C7.42501 3.55663 7.49074 3.71339 7.49216 3.87726C7.49359 4.04113 7.43059 4.19901 7.31674 4.31688L6.69174 4.94189C6.63379 5.00002 6.56491 5.04612 6.48908 5.07755C6.41325 5.10898 6.33195 5.12511 6.24987 5.12501Z"
fill="#616161" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,38 @@
.sketchbook-tab-icon {
-webkit-mask: url('./sketchbook.svg');
mask: url('./sketchbook.svg');
}
.sketch-folder-icon {
background: url('./sketch-folder-icon.svg') center center no-repeat;
background-position-x: 1px;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.sketchbook-tree-icon {
background: url('./sketchbook-tree-icon.svg') center center no-repeat;
width: 19px !important;
height: var(--theia-icon-size);
}
.sketchbook-trees-container {
height: 100%;
}
.sketchbook-tree__opts {
background: url('./sketchbook-opts-icon.svg') center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.active-sketch {
font-weight: 500;
background-color: var(--theia-button-disabledBackground) !important;
color: var(--theia-list-inactiveSelectionForeground) !important;
}
#arduino-sketchbook-tree-widget .theia-TreeNodeSegmentGrow {
flex: 1;
}

View File

@ -0,0 +1,3 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.625 3.75001H11.3587L8.12125 0.503765C8.03949 0.422668 7.94253 0.358509 7.83592 0.314965C7.72931 0.271421 7.61516 0.249349 7.5 0.250015H1.375C1.14294 0.250015 0.920376 0.342202 0.756282 0.506296C0.592187 0.670391 0.5 0.89295 0.5 1.12501V16.875C0.5 17.1071 0.592187 17.3296 0.756282 17.4937C0.920376 17.6578 1.14294 17.75 1.375 17.75H20.625C20.8571 17.75 21.0796 17.6578 21.2437 17.4937C21.4078 17.3296 21.5 17.1071 21.5 16.875V4.62501C21.5 4.39295 21.4078 4.17039 21.2437 4.0063C21.0796 3.8422 20.8571 3.75001 20.625 3.75001ZM2.25 2.00001H7.14125L8.89125 3.75001H2.25V2.00001ZM19.75 16H2.25V5.50001H19.75V16Z" fill="#4E5B61"/>
</svg>

After

Width:  |  Height:  |  Size: 742 B

View File

@ -1,12 +1,10 @@
import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
CommonFrontendContribution as TheiaCommonFrontendContribution,
CommonCommands,
} from '@theia/core/lib/browser/common-frontend-contribution';
import { CommonFrontendContribution as TheiaCommonFrontendContribution, CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
@injectable()
export class CommonFrontendContribution extends TheiaCommonFrontendContribution {
registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
for (const command of [
@ -23,9 +21,14 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
CommonCommands.SELECT_ICON_THEME,
CommonCommands.SELECT_COLOR_THEME,
CommonCommands.ABOUT_COMMAND,
CommonCommands.SAVE_WITHOUT_FORMATTING, // Patched for https://github.com/eclipse-theia/theia/pull/8877
CommonCommands.CLOSE_TAB,
CommonCommands.CLOSE_OTHER_TABS,
CommonCommands.CLOSE_ALL_TABS,
CommonCommands.COLLAPSE_PANEL,
CommonCommands.SAVE_WITHOUT_FORMATTING // Patched for https://github.com/eclipse-theia/theia/pull/8877
]) {
registry.unregisterMenuAction(command);
}
}
}

View File

@ -2,15 +2,9 @@ import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { open } from '@theia/core/lib/browser/opener-service';
import { FileStat } from '@theia/filesystem/lib/common/files';
import {
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import {
WorkspaceCommandContribution as TheiaWorkspaceCommandContribution,
WorkspaceCommands,
} from '@theia/workspace/lib/browser/workspace-commands';
import { Sketch } from '../../../common/protocol';
import { CommandRegistry, CommandService } from '@theia/core/lib/common/command';
import { WorkspaceCommandContribution as TheiaWorkspaceCommandContribution, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { Sketch, SketchesService } from '../../../common/protocol';
import { WorkspaceInputDialog } from './workspace-input-dialog';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
@ -18,28 +12,26 @@ import { SingleTextInputDialog } from '@theia/core/lib/browser';
@injectable()
export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribution {
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.unregisterCommand(WorkspaceCommands.NEW_FILE);
registry.registerCommand(
WorkspaceCommands.NEW_FILE,
this.newWorkspaceRootUriAwareCommandHandler({
execute: (uri) => this.newFile(uri),
})
);
registry.registerCommand(WorkspaceCommands.NEW_FILE, this.newWorkspaceRootUriAwareCommandHandler({
execute: uri => this.newFile(uri)
}));
registry.unregisterCommand(WorkspaceCommands.FILE_RENAME);
registry.registerCommand(
WorkspaceCommands.FILE_RENAME,
this.newUriAwareCommandHandler({
execute: (uri) => this.renameFile(uri),
})
);
registry.registerCommand(WorkspaceCommands.FILE_RENAME, this.newUriAwareCommandHandler({
execute: uri => this.renameFile(uri)
}));
}
protected async newFile(uri: URI | undefined): Promise<void> {
@ -52,14 +44,11 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
}
const parentUri = parent.resource;
const dialog = new WorkspaceInputDialog(
{
title: 'Name for new file',
parentUri,
validate: (name) => this.validateFileName(name, parent, true),
},
this.labelProvider
);
const dialog = new WorkspaceInputDialog({
title: 'Name for new file',
parentUri,
validate: name => this.validateFileName(name, parent, true)
}, this.labelProvider);
const name = await dialog.open();
const nameWithExt = this.maybeAppendInoExt(name);
@ -71,20 +60,12 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
}
}
protected async validateFileName(
name: string,
parent: FileStat,
recursive = false
): Promise<string> {
protected async validateFileName(name: string, parent: FileStat, recursive = false): Promise<string> {
// In the Java IDE the followings are the rules:
// - `name` without an extension should default to `name.ino`.
// - `name` with a single trailing `.` also defaults to `name.ino`.
const nameWithExt = this.maybeAppendInoExt(name);
const errorMessage = await super.validateFileName(
nameWithExt,
parent,
recursive
);
const errorMessage = await super.validateFileName(nameWithExt, parent, recursive);
if (errorMessage) {
return errorMessage;
}
@ -104,10 +85,10 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
}
if (name.trim().length) {
if (name.indexOf('.') === -1) {
return `${name}.ino`;
return `${name}.ino`
}
if (name.lastIndexOf('.') === name.length - 1) {
return `${name.slice(0, -1)}.ino`;
return `${name.slice(0, -1)}.ino`
}
}
return name;
@ -121,16 +102,20 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
if (!sketch) {
return;
}
// file belongs to another sketch, do not allow rename
const parentsketch = await this.sketchService.getSketchFolder(uri.toString());
if (parentsketch && parentsketch.uri !== sketch.uri) {
return;
}
if (uri.toString() === sketch.mainFileUri) {
const options = {
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: true,
wipeOriginal: true
};
await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
options
);
await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, options);
return;
}
const parent = await this.getParent(uri);
@ -143,14 +128,14 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
initialValue,
initialSelectionRange: {
start: 0,
end: uri.path.name.length,
end: uri.path.name.length
},
validate: (name, mode) => {
if (initialValue === name && mode === 'preview') {
return false;
}
return this.validateFileName(name, parent, false);
},
}
});
const newName = await dialog.open();
const newNameWithExt = this.maybeAppendInoExt(newName);
@ -160,4 +145,5 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
this.fileService.move(oldUri, newUri);
}
}
}

View File

@ -0,0 +1,65 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets';
import { Message, MessageLoop } from '@phosphor/messaging';
import { Disposable } from '@theia/core/lib/common/disposable';
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
import { UserStatus } from './cloud-user-status';
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
@injectable()
export class CloudSketchbookCompositeWidget extends BaseWidget {
@inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService;
@inject(CloudSketchbookTreeWidget)
protected readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
private compositeNode: HTMLElement;
private cloudUserStatusNode: HTMLElement;
constructor() {
super();
this.compositeNode = document.createElement('div');
this.compositeNode.classList.add('composite-node');
this.cloudUserStatusNode = document.createElement('div');
this.cloudUserStatusNode.classList.add('cloud-status-node');
this.compositeNode.appendChild(this.cloudUserStatusNode);
this.node.appendChild(this.compositeNode);
this.title.caption = 'Cloud Sketchbook';
this.title.iconClass = 'cloud-sketchbook-tree-icon';
this.title.closable = false;
this.id = 'cloud-sketchbook-composite-widget';
}
protected onAfterAttach(message: Message): void {
super.onAfterAttach(message);
Widget.attach(this.cloudSketchbookTreeWidget, this.compositeNode);
ReactDOM.render(
<UserStatus
model={
this.cloudSketchbookTreeWidget
.model as CloudSketchbookTreeModel
}
authenticationService={this.authenticationService}
/>,
this.cloudUserStatusNode
);
this.toDisposeOnDetach.push(
Disposable.create(() =>
Widget.detach(this.cloudSketchbookTreeWidget)
)
);
}
protected onResize(message: Widget.ResizeMessage): void {
super.onResize(message);
MessageLoop.sendMessage(
this.cloudSketchbookTreeWidget,
Widget.ResizeMessage.UnknownSize
);
}
}

View File

@ -0,0 +1,393 @@
import { inject, injectable } from 'inversify';
import { TreeNode } from '@theia/core/lib/browser/tree';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
import {
ContextMenuRenderer,
RenderContextMenuOptions,
} from '@theia/core/lib/browser';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { CloudUserCommands } from '../../auth/cloud-user-commands';
import { ShareSketchDialog } from '../../dialogs.ts/cloud-share-sketch-dialog';
import { CreateApi } from '../../create/create-api';
import {
PreferenceService,
PreferenceScope,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { Contribution } from '../../contributions/contribution';
import { ArduinoPreferences } from '../../arduino-preferences';
import { MainMenuManager } from '../../../common/main-menu-manager';
export const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context'];
// `Open Folder`, `Open in New Window`
export const SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP = [
...SKETCHBOOKSYNC__CONTEXT,
'0_main',
];
export const CLOUD_USER__CONTEXT = ['arduino-cloud-user--context'];
export const CLOUD_USER__CONTEXT__USERNAME = [
...CLOUD_USER__CONTEXT,
'0_username',
];
export const CLOUD_USER__CONTEXT__MAIN_GROUP = [
...CLOUD_USER__CONTEXT,
'1_main',
];
export namespace CloudSketchbookCommands {
export interface Arg {
model: CloudSketchbookTreeModel;
node: TreeNode;
event?: MouseEvent;
}
export namespace Arg {
export function is(arg: Partial<Arg> | undefined): arg is Arg {
return (
!!arg &&
!!arg.node &&
arg.model instanceof CloudSketchbookTreeModel
);
}
}
export const TOGGLE_CLOUD_SKETCHBOOK: Command = {
id: 'arduino-cloud-sketchbook--disable',
label: 'Show/Hide Remote Sketchbook',
};
export const PULL_SKETCH: Command = {
id: 'arduino-cloud-sketchbook--pull-sketch',
label: 'Pull Sketch',
iconClass: 'pull-sketch-icon',
};
export const PUSH_SKETCH: Command = {
id: 'arduino-cloud-sketchbook--push-sketch',
label: 'Push Sketch',
iconClass: 'push-sketch-icon',
};
export const OPEN_IN_CLOUD_EDITOR: Command = {
id: 'arduino-cloud-sketchbook--open-in-cloud-editor',
label: 'Open in Cloud Editor',
};
export const OPEN_SKETCHBOOKSYNC_CONTEXT_MENU: Command = {
id: 'arduino-sketchbook-sync--open-sketch-context-menu',
label: 'Options...',
iconClass: 'sketchbook-tree__opts',
};
export const OPEN_SKETCH_SHARE_DIALOG: Command = {
id: 'arduino-cloud-sketchbook--share-modal',
label: 'Share...',
};
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu',
};
}
@injectable()
export class CloudSketchbookContribution extends Contribution {
@inject(FileService)
protected readonly fileService: FileService;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(CreateApi)
protected readonly createApi: CreateApi;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
protected readonly toDisposeBeforeNewContextMenu =
new DisposableCollection();
registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(ArduinoMenus.FILE__ADVANCED_SUBMENU, {
commandId: CloudSketchbookCommands.TOGGLE_CLOUD_SKETCHBOOK.id,
label: CloudSketchbookCommands.TOGGLE_CLOUD_SKETCHBOOK.label,
order: '2',
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(
CloudSketchbookCommands.TOGGLE_CLOUD_SKETCHBOOK,
{
execute: () => {
this.preferenceService.set(
'arduino.cloud.enabled',
!this.arduinoPreferences['arduino.cloud.enabled'],
PreferenceScope.User
);
},
isEnabled: () => true,
isVisible: () => true,
}
);
registry.registerCommand(CloudSketchbookCommands.PULL_SKETCH, {
execute: (arg) => arg.model.sketchbookTree().pull(arg),
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
});
registry.registerCommand(CloudSketchbookCommands.PUSH_SKETCH, {
execute: (arg) => arg.model.sketchbookTree().push(arg.node),
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
!!arg.node.synced,
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
!!arg.node.synced,
});
registry.registerCommand(CloudSketchbookCommands.OPEN_IN_CLOUD_EDITOR, {
execute: (arg) => {
this.windowService.openNewWindow(
`https://create.arduino.cc/editor/${arg.node.sketchId}`,
{ external: true }
);
},
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
});
registry.registerCommand(
CloudSketchbookCommands.OPEN_SKETCH_SHARE_DIALOG,
{
execute: (arg) => {
new ShareSketchDialog({
node: arg.node,
title: 'Share Sketch',
createApi: this.createApi,
}).open();
},
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
}
);
registry.registerCommand(
CloudSketchbookCommands.OPEN_SKETCHBOOKSYNC_CONTEXT_MENU,
{
isEnabled: (arg) =>
!!arg &&
'node' in arg &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isVisible: (arg) =>
!!arg &&
'node' in arg &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
execute: async (arg) => {
// cleanup previous context menu entries
this.toDisposeBeforeNewContextMenu.dispose();
const container = arg.event.target;
if (!container) {
return;
}
this.menuRegistry.registerMenuAction(
SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP,
{
commandId:
CloudSketchbookCommands.OPEN_IN_CLOUD_EDITOR.id,
label: CloudSketchbookCommands.OPEN_IN_CLOUD_EDITOR
.label,
order: '0',
}
);
this.toDisposeBeforeNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
CloudSketchbookCommands.OPEN_IN_CLOUD_EDITOR
)
)
);
this.menuRegistry.registerMenuAction(
SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP,
{
commandId:
CloudSketchbookCommands.OPEN_SKETCH_SHARE_DIALOG
.id,
label: CloudSketchbookCommands
.OPEN_SKETCH_SHARE_DIALOG.label,
order: '1',
}
);
this.toDisposeBeforeNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
CloudSketchbookCommands.OPEN_SKETCH_SHARE_DIALOG
)
)
);
const currentSketch =
await this.sketchServiceClient.currentSketch();
const localUri =
await arg.model.cloudSketchbookTree.localUri(arg.node);
let underlying = null;
if (arg.node && localUri) {
underlying =
await this.fileService.toUnderlyingResource(
localUri
);
}
// disable the "open sketch" command for the current sketch and for those not in sync
if (
!underlying ||
(currentSketch &&
currentSketch.uri === underlying.toString())
) {
const placeholder = new PlaceholderMenuNode(
SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP,
SketchbookCommands.OPEN_NEW_WINDOW.label!
);
this.menuRegistry.registerMenuNode(
SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP,
placeholder
);
this.toDisposeBeforeNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(
placeholder.id
)
)
);
} else {
arg.node.uri = localUri;
this.menuRegistry.registerMenuAction(
SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP,
{
commandId:
SketchbookCommands.OPEN_NEW_WINDOW.id,
label: SketchbookCommands.OPEN_NEW_WINDOW.label,
}
);
this.toDisposeBeforeNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
SketchbookCommands.OPEN_NEW_WINDOW
)
)
);
}
const options: RenderContextMenuOptions = {
menuPath: SKETCHBOOKSYNC__CONTEXT,
anchor: {
x: container.getBoundingClientRect().left,
y:
container.getBoundingClientRect().top +
container.offsetHeight,
},
args: arg,
};
this.contextMenuRenderer.render(options);
},
}
);
registry.registerCommand(CloudUserCommands.OPEN_PROFILE_CONTEXT_MENU, {
execute: async (arg) => {
this.toDisposeBeforeNewContextMenu.dispose();
const container = arg.event.target;
if (!container) {
return;
}
this.menuRegistry.registerMenuAction(
CLOUD_USER__CONTEXT__MAIN_GROUP,
{
commandId: CloudUserCommands.LOGOUT.id,
label: CloudUserCommands.LOGOUT.label,
}
);
this.toDisposeBeforeNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
CloudUserCommands.LOGOUT
)
)
);
const placeholder = new PlaceholderMenuNode(
CLOUD_USER__CONTEXT__USERNAME,
arg.username
);
this.menuRegistry.registerMenuNode(
CLOUD_USER__CONTEXT__USERNAME,
placeholder
);
this.toDisposeBeforeNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(placeholder.id)
)
);
const options: RenderContextMenuOptions = {
menuPath: CLOUD_USER__CONTEXT,
anchor: {
x: container.getBoundingClientRect().left,
y:
container.getBoundingClientRect().top -
3.5 * container.offsetHeight,
},
args: arg,
};
this.contextMenuRenderer.render(options);
},
});
this.registerMenus(this.menuRegistry);
}
}

View File

@ -0,0 +1,29 @@
import { interfaces, Container } from 'inversify';
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { createSketchbookTreeContainer } from '../sketchbook/sketchbook-tree-container';
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
import { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model';
import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget';
function createCloudSketchbookTreeContainer(
parent: interfaces.Container
): Container {
const child = createSketchbookTreeContainer(parent);
child.bind(CloudSketchbookTree).toSelf();
child.rebind(SketchbookTree).toService(CloudSketchbookTree);
child.bind(CloudSketchbookTreeModel).toSelf();
child.rebind(SketchbookTreeModel).toService(CloudSketchbookTreeModel);
child.bind(CloudSketchbookTreeWidget).toSelf();
child.rebind(SketchbookTreeWidget).toService(CloudSketchbookTreeWidget);
return child;
}
export function createCloudSketchbookTreeWidget(
parent: interfaces.Container
): CloudSketchbookTreeWidget {
return createCloudSketchbookTreeContainer(parent).get(
CloudSketchbookTreeWidget
);
}

View File

@ -0,0 +1,166 @@
import { inject, injectable, postConstruct } from 'inversify';
import { TreeNode } from '@theia/core/lib/browser/tree';
import { toPosixPath, posixSegments, posix } from '../../create/create-paths';
import { CreateApi, Create } from '../../create/create-api';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import {
LocalCacheFsProvider,
LocalCacheUri,
} from '../../local-cache/local-cache-fs-provider';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model';
import { ArduinoPreferences } from '../../arduino-preferences';
import { ConfigService } from '../../../common/protocol';
export type CreateCache = Record<string, Create.Resource>;
export namespace CreateCache {
export function build(resources: Create.Resource[]): CreateCache {
const treeData: CreateCache = {};
treeData[posix.sep] = CloudSketchbookTree.rootResource;
for (const resource of resources) {
const { path } = resource;
const posixPath = toPosixPath(path);
if (treeData[posixPath] !== undefined) {
throw new Error(
`Already visited resource for path: ${posixPath}.\nData: ${JSON.stringify(
treeData,
null,
2
)}`
);
}
treeData[posixPath] = resource;
}
return treeData;
}
export function childrenOf(
resource: Create.Resource,
cache: CreateCache
): Create.Resource[] | undefined {
if (resource.type === 'file') {
return undefined;
}
const posixPath = toPosixPath(resource.path);
const childSegmentCount = posixSegments(posixPath).length + 1;
return Object.keys(cache)
.filter(
(key) =>
key.startsWith(posixPath) &&
posixSegments(key).length === childSegmentCount
)
.map((childPosixPath) => cache[childPosixPath]);
}
}
@injectable()
export class CloudSketchbookTreeModel extends SketchbookTreeModel {
@inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(CreateApi)
protected readonly createApi: CreateApi;
@inject(CloudSketchbookTree)
protected readonly cloudSketchbookTree: CloudSketchbookTree;
@inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider;
@inject(CommandRegistry)
public readonly commandRegistry: CommandRegistry;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@postConstruct()
protected init(): void {
super.init();
this.toDispose.push(
this.authenticationService.onSessionDidChange(() =>
this.updateRoot()
)
);
}
async updateRoot(): Promise<void> {
const { session } = this.authenticationService;
if (!session) {
this.tree.root = undefined;
return;
}
this.createApi.init(
this.authenticationService,
this.arduinoPreferences
);
const resources = await this.createApi.readDirectory(posix.sep, {
recursive: true,
secrets: true,
});
const cache = CreateCache.build(resources);
// also read local files
for await (const path of Object.keys(cache)) {
if (cache[path].type === 'sketch') {
const localUri = LocalCacheUri.root.resolve(path);
const exists = await this.fileService.exists(localUri);
if (exists) {
const fileStat = await this.fileService.resolve(localUri);
// add every missing file
fileStat.children
?.filter(
(child) =>
!Object.keys(cache).includes(
path + posix.sep + child.name
)
)
.forEach((child) => {
const localChild: Create.Resource = {
modified_at: '',
href: cache[path].href + posix.sep + child.name,
mimetype: '',
name: child.name,
path: cache[path].path + posix.sep + child.name,
sketchId: '',
type: child.isFile ? 'file' : 'folder',
};
cache[path + posix.sep + child.name] = localChild;
});
}
}
}
const showAllFiles =
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
this.tree.root = CloudSketchbookTree.CloudRootNode.create(
cache,
showAllFiles
);
}
sketchbookTree(): CloudSketchbookTree {
return this.tree as CloudSketchbookTree;
}
protected recursivelyFindSketchRoot(node: TreeNode): any {
if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) {
if (node.hasOwnProperty('underlying')) {
return { ...node, uri: node.underlying };
}
return node;
}
if (node && node.parent) {
return this.recursivelyFindSketchRoot(node.parent);
}
// can't find a root, return false
return false;
}
}

View File

@ -0,0 +1,184 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { TreeModel } from '@theia/core/lib/browser/tree/tree-model';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CloudUserCommands } from '../../auth/cloud-user-commands';
import { NodeProps } from '@theia/core/lib/browser/tree/tree-widget';
import { TreeNode } from '@theia/core/lib/browser/tree';
import { CompositeTreeNode } from '@theia/core/lib/browser';
import { shell } from 'electron';
import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget';
const LEARN_MORE_URL =
'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync';
@injectable()
export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
@inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(CloudSketchbookTree)
protected readonly cloudSketchbookTree: CloudSketchbookTree;
@postConstruct()
protected async init(): Promise<void> {
await super.init();
this.addClass('tree-container'); // Adds `height: 100%` to the tree. Otherwise you cannot see it.
}
protected renderTree(model: TreeModel): React.ReactNode {
if (this.shouldShowWelcomeView()) return this.renderViewWelcome();
if (this.shouldShowEmptyView()) return this.renderEmptyView();
return super.renderTree(model);
}
protected renderEmptyView() {
return (
<div className="cloud-sketchbook-welcome center">
<div className="center item">
<div>
<p>
<b>Your Sketchbook is empty</b>
</p>
<p>Visit Arduino Cloud to create Cloud Sketches.</p>
</div>
</div>
<button
className="theia-button"
onClick={() =>
shell.openExternal('https://create.arduino.cc/editor')
}
>
GO TO CLOUD
</button>
<div className="center item"></div>
</div>
);
}
protected shouldShowWelcomeView(): boolean {
if (!this.model || this.model instanceof CloudSketchbookTreeModel) {
return !this.authenticationService.session;
}
return super.shouldShowWelcomeView();
}
protected shouldShowEmptyView(): boolean {
const node = this.cloudSketchbookTree.root as TreeNode;
return CompositeTreeNode.is(node) && node.children.length === 0;
}
protected createNodeClassNames(node: any, props: NodeProps): string[] {
const classNames = super.createNodeClassNames(node, props);
if (
node &&
node.hasOwnProperty('underlying') &&
this.currentSketchUri === node.underlying.toString()
) {
classNames.push('active-sketch');
}
return classNames;
}
protected renderInlineCommands(
node: any,
props: NodeProps
): React.ReactNode {
if (
CloudSketchbookTree.CloudSketchDirNode.is(node) &&
node.commands &&
(node.id === this.hoveredNodeId ||
this.currentSketchUri === node.underlying?.toString())
) {
return Array.from(new Set(node.commands)).map((command) =>
this.renderInlineCommand(command.id, node)
);
}
return undefined;
}
protected renderViewWelcome(): React.ReactNode {
return (
<div className="cloud-sketchbook-welcome center">
<div className="center item">
<div>
<p className="sign-in-title">
Sign in to Arduino Cloud
</p>
<p className="sign-in-desc">
Sync and edit your Arduino Cloud Sketches
</p>
</div>
</div>
<button
className="theia-button sign-in-cta"
onClick={() =>
this.commandRegistry.executeCommand(
CloudUserCommands.LOGIN.id
)
}
>
SIGN IN
</button>
<div className="center item">
<div
className="link sign-in-learnmore"
onClick={() =>
this.windowService.openNewWindow(LEARN_MORE_URL, {
external: true,
})
}
>
Learn more
</div>
</div>
</div>
);
}
protected async handleClickEvent(
node: any,
event: React.MouseEvent<HTMLElement>
) {
event.persist();
let uri = node.uri;
// overwrite the uri using the local-cache
const localUri = await this.cloudSketchbookTree.localUri(node);
if (node && localUri) {
const underlying = await this.fileService.toUnderlyingResource(
localUri
);
uri = underlying;
}
super.handleClickEvent({ ...node, uri }, event);
}
protected async handleDblClickEvent(
node: any,
event: React.MouseEvent<HTMLElement>
) {
event.persist();
let uri = node.uri;
// overwrite the uri using the local-cache
// if the localURI does not exists, ignore the double click, so that the sketch is not opened
const localUri = await this.cloudSketchbookTree.localUri(node);
if (node && localUri) {
const underlying = await this.fileService.toUnderlyingResource(
localUri
);
uri = underlying;
super.handleDblClickEvent({ ...node, uri }, event);
}
}
}

View File

@ -0,0 +1,523 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { MaybePromise } from '@theia/core/lib/common/types';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStatNode } from '@theia/filesystem/lib/browser/file-tree';
import { Command } from '@theia/core/lib/common/command';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { DecoratedTreeNode } from '@theia/core/lib/browser/tree/tree-decorator';
import {
FileNode,
DirNode,
} from '@theia/filesystem/lib/browser/file-tree/file-tree';
import { TreeNode, CompositeTreeNode } from '@theia/core/lib/browser/tree';
import {
PreferenceService,
PreferenceScope,
} from '@theia/core/lib/browser/preferences/preference-service';
import { MessageService } from '@theia/core/lib/common/message-service';
import { REMOTE_ONLY_FILES } from './../../create/create-fs-provider';
import { posix } from '../../create/create-paths';
import { Create, CreateApi } from '../../create/create-api';
import { CreateUri } from '../../create/create-uri';
import {
CloudSketchbookTreeModel,
CreateCache,
} from './cloud-sketchbook-tree-model';
import { LocalCacheUri } from '../../local-cache/local-cache-fs-provider';
import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
import { DoNotAskAgainConfirmDialog } from '../../dialogs.ts/dialogs';
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
import { firstToUpperCase } from '../../../common/utils';
import { ArduinoPreferences } from '../../arduino-preferences';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
const MESSAGE_TIMEOUT = 5 * 1000;
const deepmerge = require('deepmerge').default;
@injectable()
export class CloudSketchbookTree extends SketchbookTree {
@inject(FileService)
protected readonly fileService: FileService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(CreateApi)
protected readonly createApi: CreateApi;
async pushPublicWarn(
node: CloudSketchbookTree.CloudSketchDirNode
): Promise<boolean> {
const warn =
node.isPublic &&
this.arduinoPreferences['arduino.cloud.pushpublic.warn'];
if (warn) {
const ok = await new DoNotAskAgainConfirmDialog({
ok: 'Continue',
cancel: 'Cancel',
title: 'Push Sketch',
msg: 'This is a Public Sketch. Before pushing, make sure any sensitive information is defined in arduino_secrets.h files. You can make a Sketch private from the Share panel.',
maxWidth: 400,
onAccept: () =>
this.preferenceService.set(
'arduino.cloud.pushpublic.warn',
false,
PreferenceScope.User
),
}).open();
if (!ok) {
return false;
}
return true;
} else {
return true;
}
}
async pull(arg: any): Promise<void> {
const {
model,
node,
}: {
model: CloudSketchbookTreeModel;
node: CloudSketchbookTree.CloudSketchDirNode;
} = arg;
const warn =
node.synced && this.arduinoPreferences['arduino.cloud.pull.warn'];
if (warn) {
const ok = await new DoNotAskAgainConfirmDialog({
ok: 'Pull',
cancel: 'Cancel',
title: 'Pull Sketch',
msg: 'Pulling this Sketch from the Cloud will overwrite its local version. Are you sure you want to continue?',
maxWidth: 400,
onAccept: () =>
this.preferenceService.set(
'arduino.cloud.pull.warn',
false,
PreferenceScope.User
),
}).open();
if (!ok) {
return;
}
}
this.runWithState(node, 'pulling', async (node) => {
const commandsCopy = node.commands;
node.commands = [];
// check if the sketch dir already exist
if (node.synced) {
const filesToPull = (
await this.createApi.readDirectory(
node.uri.path.toString(),
{
secrets: true,
}
)
).filter((file: any) => !REMOTE_ONLY_FILES.includes(file.name));
await Promise.all(
filesToPull.map((file: any) => {
const uri = CreateUri.toUri(file);
this.fileService.copy(
uri,
LocalCacheUri.root.resolve(uri.path),
{ overwrite: true }
);
})
);
// open the pulled files in the current workspace
const currentSketch =
await this.sketchServiceClient.currentSketch();
if (
currentSketch &&
node.underlying &&
currentSketch.uri === node.underlying.toString()
) {
filesToPull.forEach(async (file) => {
const localUri = LocalCacheUri.root.resolve(
CreateUri.toUri(file).path
);
const underlying =
await this.fileService.toUnderlyingResource(
localUri
);
model.open(underlying);
});
}
} else {
await this.fileService.copy(
node.uri,
LocalCacheUri.root.resolve(node.uri.path),
{ overwrite: true }
);
}
node.commands = commandsCopy;
this.messageService.info(`Done pulling ${node.fileStat.name}.`, {
timeout: MESSAGE_TIMEOUT,
});
});
}
async push(node: CloudSketchbookTree.CloudSketchDirNode): Promise<void> {
if (!node.synced) {
throw new Error('Cannot push to Cloud. It is not yet pulled.');
}
const pushPublic = await this.pushPublicWarn(node);
if (!pushPublic) {
return;
}
const warn = this.arduinoPreferences['arduino.cloud.push.warn'];
if (warn) {
const ok = await new DoNotAskAgainConfirmDialog({
ok: 'Push',
cancel: 'Cancel',
title: 'Push Sketch',
msg: 'Pushing this Sketch will overwrite its Cloud version. Are you sure you want to continue?',
maxWidth: 400,
onAccept: () =>
this.preferenceService.set(
'arduino.cloud.push.warn',
false,
PreferenceScope.User
),
}).open();
if (!ok) {
return;
}
}
this.runWithState(node, 'pushing', async (node) => {
if (!node.synced) {
throw new Error(
'You have to pull first to be able to push to the Cloud.'
);
}
const commandsCopy = node.commands;
node.commands = [];
// delete every first level file, then push everything
const result = await this.fileService.copy(
LocalCacheUri.root.resolve(node.uri.path),
node.uri,
{ overwrite: true }
);
node.commands = commandsCopy;
this.messageService.info(`Done pushing ${result.name}.`, {
timeout: MESSAGE_TIMEOUT,
});
});
}
async refresh(
node?: CompositeTreeNode
): Promise<CompositeTreeNode | undefined> {
if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) {
const localUri = await this.localUri(node);
if (localUri) {
node.synced = true;
if (
node.commands?.indexOf(
CloudSketchbookCommands.PUSH_SKETCH
) === -1
) {
node.commands.splice(
1,
0,
CloudSketchbookCommands.PUSH_SKETCH
);
}
// remove italic from synced nodes
if (
'decorationData' in node &&
'fontData' in (node as any).decorationData
) {
delete (node as any).decorationData.fontData;
}
}
}
return super.refresh(node);
}
private async runWithState<T>(
node: CloudSketchbookTree.CloudSketchDirNode &
Partial<DecoratedTreeNode>,
state: CloudSketchbookTree.CloudSketchDirNode.State,
task: (node: CloudSketchbookTree.CloudSketchDirNode) => MaybePromise<T>
): Promise<T> {
const decoration: WidgetDecoration.TailDecoration = {
data: `${firstToUpperCase(state)}...`,
fontData: {
color: 'var(--theia-list-highlightForeground)',
},
};
try {
node.state = state;
this.mergeDecoration(node, { tailDecorations: [decoration] });
await this.refresh(node);
const result = await task(node);
return result;
} finally {
delete node.state;
// TODO: find a better way to attach and detach decorators. Do we need a proper `TreeDecorator` instead?
const index = node.decorationData?.tailDecorations?.findIndex(
(candidate) =>
JSON.stringify(decoration) === JSON.stringify(candidate)
);
if (typeof index === 'number' && index !== -1) {
node.decorationData?.tailDecorations?.splice(index, 1);
}
await this.refresh(node);
}
}
protected async resolveFileStat(
node: FileStatNode
): Promise<FileStat | undefined> {
if (
CreateUri.is(node.uri) &&
CloudSketchbookTree.CloudRootNode.is(this.root)
) {
const resource = this.root.cache[node.uri.path.toString()];
if (!resource) {
return undefined;
}
return CloudSketchbookTree.toFileStat(resource, this.root.cache, 1);
}
return super.resolveFileStat(node);
}
protected readonly notInSyncDecoration: WidgetDecoration.Data = {
fontData: {
color: 'var(--theia-activityBar-inactiveForeground)',
},
};
protected async toNodes(
fileStat: FileStat,
parent: CompositeTreeNode
): Promise<CloudSketchbookTree.CloudSketchTreeNode[]> {
const children = await super.toNodes(fileStat, parent);
for (const child of children.filter(FileStatNode.is)) {
if (!CreateFileStat.is(child.fileStat)) {
continue;
}
const localUri = await this.localUri(child);
let underlying = null;
if (localUri) {
underlying = await this.fileService.toUnderlyingResource(
localUri
);
Object.assign(child, { underlying });
}
if (CloudSketchbookTree.CloudSketchDirNode.is(child)) {
if (child.fileStat.sketchId) {
child.sketchId = child.fileStat.sketchId;
child.isPublic = child.fileStat.isPublic;
}
const commands = [CloudSketchbookCommands.PULL_SKETCH];
if (underlying) {
child.synced = true;
commands.push(CloudSketchbookCommands.PUSH_SKETCH);
} else {
this.mergeDecoration(child, this.notInSyncDecoration);
}
commands.push(
CloudSketchbookCommands.OPEN_SKETCHBOOKSYNC_CONTEXT_MENU
);
Object.assign(child, { commands });
if (!this.showAllFiles) {
delete (child as any).expanded;
}
} else if (CloudSketchbookTree.CloudSketchDirNode.is(parent)) {
if (!parent.synced) {
this.mergeDecoration(child, this.notInSyncDecoration);
} else {
this.setDecoration(
child,
underlying ? undefined : this.notInSyncDecoration
);
}
}
}
if (
CloudSketchbookTree.SketchDirNode.is(parent) &&
!this.showAllFiles
) {
return [];
}
return children;
}
protected toNode(
fileStat: FileStat,
parent: CompositeTreeNode
): FileNode | DirNode {
const node = super.toNode(fileStat, parent);
if (CreateFileStat.is(fileStat)) {
Object.assign(node, {
type: fileStat.type,
isPublic: fileStat.isPublic,
sketchId: fileStat.sketchId,
});
}
return node;
}
private mergeDecoration(
node: TreeNode,
decorationData: WidgetDecoration.Data
): void {
Object.assign(node, {
decorationData: deepmerge(
DecoratedTreeNode.is(node) ? node.decorationData : {},
decorationData
),
});
}
private setDecoration(
node: TreeNode,
decorationData: WidgetDecoration.Data | undefined
): void {
if (!decorationData) {
delete (node as any).decorationData;
} else {
Object.assign(node, { decorationData });
}
}
public async localUri(node: FileStatNode): Promise<URI | undefined> {
const localUri = LocalCacheUri.root.resolve(node.uri.path);
const exists = await this.fileService.exists(localUri);
if (exists) {
return localUri;
}
return undefined;
}
private get showAllFiles(): boolean {
return this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
}
}
export interface CreateFileStat extends FileStat {
type: Create.ResourceType;
sketchId?: string;
isPublic?: boolean;
}
export namespace CreateFileStat {
export function is(
stat: FileStat & { type?: Create.ResourceType }
): stat is CreateFileStat {
return !!stat.type;
}
}
export namespace CloudSketchbookTree {
export const rootResource: Create.Resource = Object.freeze({
modified_at: '',
name: '',
path: posix.sep,
type: 'folder',
children: Number.MIN_SAFE_INTEGER,
size: Number.MIN_SAFE_INTEGER,
sketchId: '',
});
export interface CloudRootNode extends SketchbookTree.RootNode {
readonly cache: CreateCache;
}
export namespace CloudRootNode {
export function create(
cache: CreateCache,
showAllFiles: boolean
): CloudRootNode {
return Object.assign(
SketchbookTree.RootNode.create(
toFileStat(rootResource, cache, 1),
showAllFiles
),
{ cache }
);
}
export function is(
node: (TreeNode & Partial<CloudRootNode>) | undefined
): node is CloudRootNode {
return !!node && !!node.cache && SketchbookTree.RootNode.is(node);
}
}
export interface CloudSketchDirNode extends SketchbookTree.SketchDirNode {
state?: CloudSketchDirNode.State;
synced?: true;
sketchId?: string;
isPublic?: boolean;
commands?: Command[];
underlying?: URI;
}
export interface CloudSketchTreeNode extends TreeNode {
underlying?: URI;
}
export namespace CloudSketchDirNode {
export function is(node: TreeNode): node is CloudSketchDirNode {
return SketchbookTree.SketchDirNode.is(node);
}
export type State = 'syncing' | 'pulling' | 'pushing';
}
export function toFileStat(
resource: Create.Resource,
cache: CreateCache,
depth = 0
): CreateFileStat {
return {
isDirectory: resource.type !== 'file',
isFile: resource.type === 'file',
isPublic: resource.isPublic,
isSymbolicLink: false,
name: resource.name,
resource: CreateUri.toUri(resource),
size: resource.size,
mtime: Date.parse(resource.modified_at),
sketchId: resource.sketchId || undefined,
type: resource.type,
...(!!depth && {
children: CreateCache.childrenOf(resource, cache)?.map(
(childResource) =>
toFileStat(childResource, cache, depth - 1)
),
}),
};
}
}

View File

@ -0,0 +1,48 @@
import { inject, injectable, postConstruct } from 'inversify';
import { CloudSketchbookCompositeWidget } from './cloud-sketchbook-composite-widget';
import { SketchbookWidget } from '../sketchbook/sketchbook-widget';
import { ArduinoPreferences } from '../../arduino-preferences';
@injectable()
export class CloudSketchbookWidget extends SketchbookWidget {
@inject(CloudSketchbookCompositeWidget)
protected readonly widget: CloudSketchbookCompositeWidget;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@postConstruct()
protected init(): void {
super.init();
}
checkCloudEnabled() {
if (this.arduinoPreferences['arduino.cloud.enabled']) {
this.sketchbookTreesContainer.activateWidget(this.widget);
} else {
this.sketchbookTreesContainer.activateWidget(
this.localSketchbookTreeWidget
);
}
this.setDocumentMode();
}
setDocumentMode() {
if (this.arduinoPreferences['arduino.cloud.enabled']) {
this.sketchbookTreesContainer.mode = 'multiple-document';
} else {
this.sketchbookTreesContainer.mode = 'single-document';
}
}
protected onAfterAttach(msg: any) {
this.sketchbookTreesContainer.addWidget(this.widget);
this.setDocumentMode();
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.preferenceName === 'arduino.cloud.enabled') {
this.checkCloudEnabled();
}
});
super.onAfterAttach(msg);
}
}

View File

@ -0,0 +1,124 @@
import * as React from 'react';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { CloudUserCommands } from '../../auth/cloud-user-commands';
import { firstToUpperCase } from '../../../common/utils';
import { AuthenticationSessionAccountInformation } from '../../../common/protocol/authentication-service';
export class UserStatus extends React.Component<
UserStatus.Props,
UserStatus.State
> {
protected readonly toDispose = new DisposableCollection();
constructor(props: UserStatus.Props) {
super(props);
this.state = {
status: this.status,
accountInfo: props.authenticationService.session?.account,
refreshing: false,
};
}
componentDidMount(): void {
const statusListener = () => this.setState({ status: this.status });
window.addEventListener('online', statusListener);
window.addEventListener('offline', statusListener);
this.toDispose.pushAll([
this.props.authenticationService.onSessionDidChange((session) =>
this.setState({ accountInfo: session?.account })
),
Disposable.create(() =>
window.removeEventListener('online', statusListener)
),
Disposable.create(() =>
window.removeEventListener('offline', statusListener)
),
]);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
render(): React.ReactNode {
if (!this.props.authenticationService.session) {
return null;
}
return (
<div className="cloud-connection-status flex-line">
<div className="status item flex-line">
<div
className={`${
this.state.status === 'connected'
? 'connected-status-icon'
: 'offline-status-icon'
}`}
/>
{firstToUpperCase(this.state.status)}
</div>
<div className="actions item flex-line">
<div
className={`refresh-icon ${
(this.state.refreshing && 'rotating') || ''
}`}
style={{ cursor: 'pointer' }}
onClick={this.onDidClickRefresh}
/>
</div>
<div className="account item flex-line">
<div
className="account-icon"
style={{ cursor: 'pointer' }}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
this.props.model.commandRegistry.executeCommand(
CloudUserCommands.OPEN_PROFILE_CONTEXT_MENU.id,
{
event: event.nativeEvent,
username: this.state.accountInfo?.label,
}
);
}}
>
{this.state.accountInfo?.picture && (
<img
src={this.state.accountInfo?.picture}
alt="Profile picture"
/>
)}
</div>
</div>
</div>
);
}
private onDidClickRefresh = () => {
this.setState({ refreshing: true });
this.props.model.updateRoot().then(() => {
this.props.model.sketchbookTree().refresh();
this.setState({ refreshing: false });
});
};
private get status(): 'connected' | 'offline' {
return window.navigator.onLine ? 'connected' : 'offline';
}
}
export namespace UserStatus {
export interface Props {
readonly model: CloudSketchbookTreeModel;
readonly authenticationService: AuthenticationClientService;
}
export interface State {
status: 'connected' | 'offline';
accountInfo?: AuthenticationSessionAccountInformation;
refreshing?: boolean;
}
}

View File

@ -0,0 +1,33 @@
import { Command } from '@theia/core/lib/common/command';
export namespace SketchbookCommands {
export const OPEN_NEW_WINDOW: Command = {
id: 'arduino-sketchbook--open-sketch-new-window',
label: 'Open Sketch in New Window',
};
export const REVEAL_IN_FINDER: Command = {
id: 'arduino-sketchbook--reveal-in-finder',
label: 'Open Folder',
};
export const OPEN_SKETCHBOOK_CONTEXT_MENU: Command = {
id: 'arduino-sketchbook--open-sketch-context-menu',
label: 'Contextual menu',
iconClass: 'sketchbook-tree__opts'
};
export const SKETCHBOOK_HIDE_FILES: Command = {
id: 'arduino-sketchbook--hide-files',
label: 'Contextual menu',
};
export const SKETCHBOOK_SHOW_FILES: Command = {
id: 'arduino-sketchbook--show-files',
label: 'Contextual menu',
};
}

View File

@ -0,0 +1,26 @@
import { interfaces, Container } from 'inversify';
import { createTreeContainer, Tree, TreeImpl, TreeModel, TreeModelImpl, TreeWidget } from '@theia/core/lib/browser/tree';
import { SketchbookTree } from './sketchbook-tree';
import { SketchbookTreeModel } from './sketchbook-tree-model';
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
export function createSketchbookTreeContainer(parent: interfaces.Container): Container {
const child = createTreeContainer(parent);
child.unbind(TreeImpl);
child.bind(SketchbookTree).toSelf();
child.rebind(Tree).toService(SketchbookTree);
child.unbind(TreeModelImpl);
child.bind(SketchbookTreeModel).toSelf();
child.rebind(TreeModel).toService(SketchbookTreeModel);
child.bind(SketchbookTreeWidget).toSelf();
child.rebind(TreeWidget).toService(SketchbookTreeWidget);
return child;
}
export function createSketchbookTreeWidget(parent: interfaces.Container): SketchbookTreeWidget {
return createSketchbookTreeContainer(parent).get(SketchbookTreeWidget);
}

View File

@ -0,0 +1,105 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ConfigService } from '../../../common/protocol';
import { SketchbookTree } from './sketchbook-tree';
import { ArduinoPreferences } from '../../arduino-preferences';
import { SelectableTreeNode, TreeNode } from '@theia/core/lib/browser/tree';
import { SketchbookCommands } from './sketchbook-commands';
import { OpenerService, open } from '@theia/core/lib/browser';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { CommandRegistry } from '@theia/core/lib/common/command';
@injectable()
export class SketchbookTreeModel extends FileTreeModel {
@inject(FileService)
protected readonly fileService: FileService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
async updateRoot(): Promise<void> {
const config = await this.configService.getConfiguration();
const fileStat = await this.fileService.resolve(
new URI(config.sketchDirUri)
);
const showAllFiles =
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
this.tree.root = SketchbookTree.RootNode.create(fileStat, showAllFiles);
}
// selectNode gets called when the user single-clicks on an item
// when this happens, we want to open the file if it belongs to the currently open sketch
async selectNode(node: Readonly<SelectableTreeNode>): Promise<void> {
super.selectNode(node);
if (FileNode.is(node) && (await this.isFileInsideCurrentSketch(node))) {
this.open(node.uri);
}
}
public open(uri: URI): void {
open(this.openerService, uri);
}
protected async doOpenNode(node: TreeNode): Promise<void> {
// if it's a sketch dir, or a file from another sketch, open in new window
if (!(await this.isFileInsideCurrentSketch(node))) {
const sketchRoot = this.recursivelyFindSketchRoot(node);
if (sketchRoot) {
this.commandRegistry.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node: sketchRoot }
);
}
return;
}
if (node.visible === false) {
return;
} else if (FileNode.is(node)) {
this.open(node.uri);
} else {
super.doOpenNode(node);
}
}
private async isFileInsideCurrentSketch(node: TreeNode): Promise<boolean> {
// it's a directory, not a file
if (!FileNode.is(node)) {
return false;
}
// check if the node is a file that belongs to another sketch
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && node.uri.toString().indexOf(sketch.uri) !== 0) {
return false;
}
return true;
}
protected recursivelyFindSketchRoot(node: TreeNode): TreeNode | false {
if (node && SketchbookTree.SketchDirNode.is(node)) {
return node;
}
if (node && node.parent) {
return this.recursivelyFindSketchRoot(node.parent);
}
// can't find a root, return false
return false;
}
}

View File

@ -0,0 +1,168 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { TreeNode } from '@theia/core/lib/browser/tree/tree';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { NodeProps, TreeProps, TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS } from '@theia/core/lib/browser/tree/tree-widget';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { FileTreeWidget } from '@theia/filesystem/lib/browser';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import { SketchbookTree } from './sketchbook-tree';
import { SketchbookTreeModel } from './sketchbook-tree-model';
import { ArduinoPreferences } from '../../arduino-preferences';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection';
import { Sketch } from '../../contributions/contribution';
@injectable()
export class SketchbookTreeWidget extends FileTreeWidget {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
protected currentSketchUri = '';
constructor(
@inject(TreeProps) readonly props: TreeProps,
@inject(SketchbookTreeModel) readonly model: SketchbookTreeModel,
@inject(ContextMenuRenderer) readonly contextMenuRenderer: ContextMenuRenderer,
@inject(EditorManager) readonly editorManager: EditorManager
) {
super(props, model, contextMenuRenderer);
this.id = 'arduino-sketchbook-tree-widget';
this.title.iconClass = 'sketchbook-tree-icon';
this.title.caption = 'Local Sketchbook';
this.title.closable = false;
}
@postConstruct()
protected async init(): Promise<void> {
super.init();
this.toDispose.push(this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
this.updateModel();
}
}));
this.updateModel();
// cache the current open sketch uri
const currentSketch = await this.sketchServiceClient.currentSketch();
this.currentSketchUri = currentSketch && currentSketch.uri || '';
}
async updateModel(): Promise<void> {
return this.model.updateRoot();
}
protected createNodeClassNames(node: TreeNode, props: NodeProps): string[] {
const classNames = super.createNodeClassNames(node, props);
if (SketchbookTree.SketchDirNode.is(node) && this.currentSketchUri === node?.uri.toString()) {
classNames.push('active-sketch');
}
return classNames;
}
protected renderIcon(node: TreeNode, props: NodeProps): React.ReactNode {
if (SketchbookTree.SketchDirNode.is(node) || Sketch.isSketchFile(node.id)) {
return <div className='sketch-folder-icon file-icon'></div>;
}
const icon = this.toNodeIcon(node);
if (icon) {
return <div className={icon + ' file-icon'}></div>;
}
return undefined;
}
protected renderTailDecorations(node: TreeNode, props: NodeProps): React.ReactNode {
return <React.Fragment>
{super.renderTailDecorations(node, props)}
{this.renderInlineCommands(node, props)}
</React.Fragment>
}
protected hoveredNodeId: string | undefined;
protected setHoverNodeId(id: string | undefined): void {
this.hoveredNodeId = id;
this.update();
}
protected createNodeAttributes(node: TreeNode, props: NodeProps): React.Attributes & React.HTMLAttributes<HTMLElement> {
return {
...super.createNodeAttributes(node, props),
draggable: false,
onMouseOver: () => this.setHoverNodeId(node.id),
onMouseOut: () => this.setHoverNodeId(undefined)
};
}
protected renderInlineCommands(node: TreeNode, props: NodeProps): React.ReactNode {
if (SketchbookTree.SketchDirNode.is(node) && (node.commands && node.id === this.hoveredNodeId || this.currentSketchUri === node?.uri.toString())) {
return Array.from(new Set(node.commands)).map(command => this.renderInlineCommand(command.id, node));
}
return undefined;
}
protected renderInlineCommand(commandId: string, node: SketchbookTree.SketchDirNode): React.ReactNode {
const command = this.commandRegistry.getCommand(commandId);
const icon = command?.iconClass;
const args = { model: this.model, node: node };
if (command && icon && this.commandRegistry.isEnabled(commandId, args) && this.commandRegistry.isVisible(commandId, args)) {
const className = [TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, icon, 'theia-tree-view-inline-action'].join(' ');
return <div
key={`${commandId}--${node.id}`}
className={className}
title={command?.label || command.id}
onClick={event => {
event.preventDefault();
event.stopPropagation();
this.commandRegistry.executeCommand(commandId, Object.assign(args, { event: event.nativeEvent }));
}}
/>;
}
return undefined;
}
protected handleClickEvent(node: TreeNode | undefined, event: React.MouseEvent<HTMLElement>): void {
if (node) {
if (!!this.props.multiSelect) {
const shiftMask = this.hasShiftMask(event);
const ctrlCmdMask = this.hasCtrlCmdMask(event);
if (SelectableTreeNode.is(node)) {
if (shiftMask) {
this.model.selectRange(node);
} else if (ctrlCmdMask) {
this.model.toggleNode(node);
} else {
this.model.selectNode(node);
}
}
} else {
if (SelectableTreeNode.is(node)) {
this.model.selectNode(node);
}
}
event.stopPropagation();
}
}
protected doToggle(event: React.MouseEvent<HTMLElement>): void {
const nodeId = event.currentTarget.getAttribute('data-node-id');
if (nodeId) {
const node = this.model.getNode(nodeId);
if (node && this.isExpandable(node)) {
this.model.toggleNodeExpansion(node);
}
}
event.stopPropagation();
}
}

View File

@ -0,0 +1,92 @@
import { inject, injectable } from 'inversify';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { Command } from '@theia/core/lib/common/command';
import { TreeNode, CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { DirNode, FileStatNode, FileTree } from '@theia/filesystem/lib/browser/file-tree';
import { SketchesService } from '../../../common/protocol';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { SketchbookCommands } from './sketchbook-commands';
@injectable()
export class SketchbookTree extends FileTree {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
if (!FileStatNode.is(parent)) {
return super.resolveChildren(parent);
}
const { root } = this;
if (!root) {
return [];
}
if (!SketchbookTree.RootNode.is(root)) {
return [];
}
const children = (await Promise.all((await super.resolveChildren(parent)).map(node => this.maybeDecorateNode(node, root.showAllFiles)))).filter(node => {
// filter out hidden nodes
if (DirNode.is(node) || FileStatNode.is(node)) {
return node.fileStat.name.indexOf('.') !== 0
}
return true;
});
if (SketchbookTree.RootNode.is(parent)) {
return children.filter(DirNode.is).filter(node => ['libraries', 'hardware'].indexOf(this.labelProvider.getName(node)) === -1);
}
if (SketchbookTree.SketchDirNode.is(parent)) {
return children.filter(FileStatNode.is);
}
return children;
}
protected async maybeDecorateNode(node: TreeNode, showAllFiles: boolean): Promise<TreeNode> {
if (DirNode.is(node)) {
const sketch = await this.sketchesService.maybeLoadSketch(node.uri.toString());
if (sketch) {
Object.assign(node, { type: 'sketch', commands: [SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU] });
if (!showAllFiles) {
delete (node as any).expanded;
}
return node;
}
}
return node;
}
}
export namespace SketchbookTree {
export interface RootNode extends DirNode {
readonly showAllFiles: boolean;
}
export namespace RootNode {
export function is(node: TreeNode & Partial<RootNode>): node is RootNode {
return typeof node.showAllFiles === 'boolean';
}
export function create(fileStat: FileStat, showAllFiles: boolean): RootNode {
return Object.assign(DirNode.createRoot(fileStat), { showAllFiles, visible: false });
}
}
export interface SketchDirNode extends DirNode {
readonly type: 'sketch';
readonly commands?: Command[];
}
export namespace SketchDirNode {
export function is(node: TreeNode & Partial<SketchDirNode> | undefined): node is SketchDirNode {
return !!node && node.type === 'sketch' && DirNode.is(node);
}
}
}

View File

@ -0,0 +1,156 @@
import { shell } from 'electron';
import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { MainMenuManager } from '../../../common/main-menu-manager';
import { ArduinoPreferences } from '../../arduino-preferences';
import { SketchbookWidget } from './sketchbook-widget';
import { PlaceholderMenuNode } from '../../menu/arduino-menus';
import { SketchbookTree } from './sketchbook-tree';
import { SketchbookCommands } from './sketchbook-commands';
import { WorkspaceService } from '../../theia/workspace/workspace-service';
import { ContextMenuRenderer, RenderContextMenuOptions } from '@theia/core/lib/browser';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
export const SKETCHBOOK__CONTEXT = ['arduino-sketchbook--context'];
// `Open Folder`, `Open in New Window`
export const SKETCHBOOK__CONTEXT__MAIN_GROUP = [...SKETCHBOOK__CONTEXT, '0_main'];
@injectable()
export class SketchbookWidgetContribution extends AbstractViewContribution<SketchbookWidget> implements FrontendApplicationContribution {
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(FileService)
protected readonly fileService: FileService;
protected readonly toDisposeBeforeNewContextMenu = new DisposableCollection();
constructor() {
super({
widgetId: 'arduino-sketchbook-widget',
widgetName: 'Sketchbook',
defaultWidgetOptions: {
area: 'left',
rank: 1
},
toggleCommandId: 'arduino-sketchbook-widget:toggle',
toggleKeybinding: 'CtrlCmd+Shift+B'
});
}
onStart(): void {
this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === 'arduino.sketchbook.showAllFiles') {
this.mainMenuManager.update();
}
});
}
async initializeLayout(): Promise<void> {
return this.openView() as Promise<any>;
}
registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
execute: async arg => {
const underlying = await this.fileService.toUnderlyingResource(arg.node.uri);
return this.workspaceService.open(underlying)
},
isEnabled: arg => !!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
isVisible: arg => !!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node)
});
registry.registerCommand(SketchbookCommands.REVEAL_IN_FINDER, {
execute: (arg) => {
shell.openPath(arg.node.id);
},
isEnabled: (arg) => !!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
isVisible: (arg) => !!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
});
registry.registerCommand(SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU, {
isEnabled: (arg) => !!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
isVisible: (arg) => !!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
execute: async (arg) => {
// cleanup previous context menu entries
this.toDisposeBeforeNewContextMenu.dispose();
const container = arg.event.target;
if (!container) {
return;
}
// disable the "open sketch" command for the current sketch.
// otherwise make the command clickable
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch && currentSketch.uri === arg.node.uri.toString()) {
const placeholder = new PlaceholderMenuNode(SKETCHBOOK__CONTEXT__MAIN_GROUP, SketchbookCommands.OPEN_NEW_WINDOW.label!);
this.menuRegistry.registerMenuNode(SKETCHBOOK__CONTEXT__MAIN_GROUP, placeholder);
this.toDisposeBeforeNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
} else {
this.menuRegistry.registerMenuAction(SKETCHBOOK__CONTEXT__MAIN_GROUP, {
commandId: SketchbookCommands.OPEN_NEW_WINDOW.id,
label: SketchbookCommands.OPEN_NEW_WINDOW.label,
});
this.toDisposeBeforeNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(SketchbookCommands.OPEN_NEW_WINDOW)));
}
const options: RenderContextMenuOptions = {
menuPath: SKETCHBOOK__CONTEXT,
anchor: {
x: container.getBoundingClientRect().left,
y: container.getBoundingClientRect().top + container.offsetHeight
},
args: arg
}
this.contextMenuRenderer.render(options);
}
});
}
registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
// unregister main menu action
registry.unregisterMenuAction({
commandId: 'arduino-sketchbook-widget:toggle',
});
registry.registerMenuAction(SKETCHBOOK__CONTEXT__MAIN_GROUP, {
commandId: SketchbookCommands.REVEAL_IN_FINDER.id,
label: SketchbookCommands.REVEAL_IN_FINDER.label,
order: '0'
});
}
}

View File

@ -0,0 +1,85 @@
import { inject, injectable, postConstruct } from 'inversify';
import { toArray } from '@phosphor/algorithm';
import { IDragEvent } from '@phosphor/dragdrop';
import { DockPanel, Widget } from '@phosphor/widgets';
import { Message, MessageLoop } from '@phosphor/messaging';
import { Disposable } from '@theia/core/lib/common/disposable';
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
@injectable()
export class SketchbookWidget extends BaseWidget {
@inject(SketchbookTreeWidget)
protected readonly localSketchbookTreeWidget: SketchbookTreeWidget;
protected readonly sketchbookTreesContainer: DockPanel;
constructor() {
super();
this.id = 'arduino-sketchbook-widget';
this.title.caption = 'Sketchbook';
this.title.label = 'Sketchbook';
this.title.iconClass = 'sketchbook-tab-icon';
this.title.closable = true;
this.node.tabIndex = 0;
this.sketchbookTreesContainer = this.createTreesContainer();
}
@postConstruct()
protected init(): void {
this.sketchbookTreesContainer.addWidget(this.localSketchbookTreeWidget);
}
protected onAfterAttach(message: Message): void {
super.onAfterAttach(message);
Widget.attach(this.sketchbookTreesContainer, this.node);
this.toDisposeOnDetach.push(Disposable.create(() => Widget.detach(this.sketchbookTreesContainer)));
}
protected onActivateRequest(message: Message): void {
super.onActivateRequest(message);
// TODO: focus the active sketchbook
// if (this.editor) {
// this.editor.focus();
// } else {
// }
this.node.focus();
}
protected onResize(message: Widget.ResizeMessage): void {
super.onResize(message);
MessageLoop.sendMessage(this.sketchbookTreesContainer, Widget.ResizeMessage.UnknownSize);
for (const widget of toArray(this.sketchbookTreesContainer.widgets())) {
MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
}
}
protected onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.onResize(Widget.ResizeMessage.UnknownSize);
}
protected createTreesContainer(): DockPanel {
const panel = new NoopDragOverDockPanel({ spacing: 0, mode: 'single-document' });
panel.addClass('sketchbook-trees-container');
panel.node.tabIndex = -1;
return panel;
}
}
export class NoopDragOverDockPanel extends DockPanel {
constructor(options?: DockPanel.IOptions) {
super(options);
NoopDragOverDockPanel.prototype['_evtDragOver'] = (event: IDragEvent) => {
event.preventDefault();
event.stopPropagation();
event.dropAction = 'none';
};
}
}

View File

@ -0,0 +1,30 @@
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { AuthOptions } from '../../node/auth/types';
export interface AuthenticationSession {
readonly id: string;
readonly accessToken: string;
readonly account: AuthenticationSessionAccountInformation;
readonly scopes: ReadonlyArray<string>;
}
export interface AuthenticationSessionAccountInformation {
readonly id: string;
readonly email: string;
readonly label: string;
readonly picture: string;
}
export const AuthenticationServicePath = '/services/authentication-service';
export const AuthenticationService = Symbol('AuthenticationService');
export interface AuthenticationService
extends JsonRpcServer<AuthenticationServiceClient> {
login(): Promise<AuthenticationSession>;
logout(): Promise<void>;
session(): Promise<AuthenticationSession | undefined>;
disposeClient(client: AuthenticationServiceClient): void;
setOptions(authOptions: AuthOptions): void;
}
export interface AuthenticationServiceClient {
notifySessionDidChange(session?: AuthenticationSession | undefined): void;
}

View File

@ -23,6 +23,11 @@ export interface SketchesService {
*/
loadSketch(uri: string): Promise<Sketch>;
/**
* Unlike `loadSketch`, this method gracefully resolves to `undefined` instead or rejecting if the `uri` is not a sketch folder.
*/
maybeLoadSketch(uri: string): Promise<Sketch | undefined>;
/**
* Creates a new sketch folder in the temp location.
*/

View File

@ -74,6 +74,12 @@ import { BackendApplication } from './theia/core/backend-application';
import { BoardDiscovery } from './board-discovery';
import { DefaultGitInit } from './theia/git/git-init';
import { GitInit } from '@theia/git/lib/node/init/git-init';
import { AuthenticationServiceImpl } from './auth/authentication-service-impl';
import {
AuthenticationService,
AuthenticationServiceClient,
AuthenticationServicePath,
} from '../common/protocol/authentication-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@ -280,4 +286,28 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DefaultGitInit).toSelf();
rebind(GitInit).toService(DefaultGitInit);
// Remote sketchbook bindings
bind(AuthenticationServiceImpl).toSelf().inSingletonScope();
bind(AuthenticationService).toService(AuthenticationServiceImpl);
bind(BackendApplicationContribution).toService(AuthenticationServiceImpl);
bind(ConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler<AuthenticationServiceClient>(
AuthenticationServicePath,
(client) => {
const server =
context.container.get<AuthenticationServiceImpl>(
AuthenticationServiceImpl
);
server.setClient(client);
client.onDidCloseConnection(() =>
server.disposeClient(client)
);
return server;
}
)
)
.inSingletonScope();
});

View File

@ -0,0 +1,344 @@
import fetch from 'node-fetch';
import { injectable } from 'inversify';
import { createServer, startServer } from './authentication-server';
import { Keychain } from './keychain';
import {
generateProofKeyPair,
Token,
IToken,
IToken2Session,
token2IToken,
RefreshToken,
} from './utils';
import { Authentication } from 'auth0-js';
import {
AuthenticationProviderAuthenticationSessionsChangeEvent,
AuthenticationSession,
AuthenticationProvider,
AuthOptions,
} from './types';
import { Event, Emitter } from '@theia/core/lib/common/event';
import * as open from 'open';
const LOGIN_TIMEOUT = 30 * 1000;
const REFRESH_INTERVAL = 10 * 60 * 1000;
@injectable()
export class ArduinoAuthenticationProvider implements AuthenticationProvider {
protected authOptions: AuthOptions;
public readonly id: string = 'arduino-account-auth';
public readonly label = 'Arduino';
// create a keychain holding the keys
private keychain = new Keychain({
credentialsSection: this.id,
account: this.label,
});
private _tokens: IToken[] = [];
private _refreshTimeouts: Map<string, NodeJS.Timeout> = new Map<
string,
NodeJS.Timeout
>();
private _onDidChangeSessions =
new Emitter<AuthenticationProviderAuthenticationSessionsChangeEvent>();
public get onDidChangeSessions(): Event<AuthenticationProviderAuthenticationSessionsChangeEvent> {
return this._onDidChangeSessions.event;
}
private get sessions(): Promise<AuthenticationSession[]> {
return Promise.resolve(
this._tokens.map((token) => IToken2Session(token))
);
}
public getSessions(): Promise<AuthenticationSession[]> {
return Promise.resolve(this.sessions);
}
public async init(): Promise<void> {
// restore previously stored sessions
const stringTokens = await this.keychain.getStoredCredentials();
// no valid token, nothing to do
if (!stringTokens) {
return;
}
const checkToken = async () => {
// tokens exist, parse and refresh them
try {
const tokens: IToken[] = JSON.parse(stringTokens);
// refresh the tokens when needed
await Promise.all(
tokens.map(async (token) => {
// if refresh not needed, add the existing token
if (!IToken.requiresRefresh(token, REFRESH_INTERVAL)) {
return this.addToken(token);
}
const refreshedToken = await this.refreshToken(token);
return this.addToken(refreshedToken);
})
);
} catch {
return;
}
};
checkToken();
setInterval(checkToken, REFRESH_INTERVAL);
}
public setOptions(authOptions: AuthOptions) {
this.authOptions = authOptions;
}
public dispose(): void {}
public async refreshToken(token: IToken): Promise<IToken> {
if (!token.refreshToken) {
throw new Error('Unable to refresh a token without a refreshToken');
}
console.log(`Refreshing token ${token.sessionId}`);
const response = await fetch(
`https://${this.authOptions.domain}/oauth/token`,
{
method: 'POST',
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: this.authOptions.clientID,
refresh_token: token.refreshToken,
}),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
if (response.ok) {
const result: RefreshToken = await response.json();
// add the refresh_token from the old token
return token2IToken({
...result,
refresh_token: token.refreshToken,
});
}
throw new Error(`Failed to refresh a token: ${response.statusText}`);
}
private async exchangeCodeForToken(
authCode: string,
verifier: string
): Promise<Token> {
const response = await fetch(
`https://${this.authOptions.domain}/oauth/token`,
{
method: 'POST',
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: this.authOptions.clientID,
code_verifier: verifier,
code: authCode,
redirect_uri: this.authOptions.redirectUri,
}),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
if (response.ok) {
return await response.json();
}
throw new Error(`Failed to fetch a token: ${response.statusText}`);
}
public async createSession(): Promise<AuthenticationSession> {
const token = await this.login();
this.addToken(token);
return IToken2Session(token);
}
private async login(): Promise<IToken> {
return new Promise<IToken>(async (resolve, reject) => {
const pkp = generateProofKeyPair();
const server = createServer(async (req, res) => {
const { url } = req;
if (url && url.startsWith('/callback?code=')) {
const code = url.slice('/callback?code='.length);
const token = await this.exchangeCodeForToken(
code,
pkp.verifier
);
resolve(token2IToken(token));
}
// schedule server shutdown after 10 seconds
setTimeout(() => {
server.close();
}, LOGIN_TIMEOUT);
});
try {
const port = await startServer(server);
console.log(`server listening on http://localhost:${port}`);
const auth0 = new Authentication({
clientID: this.authOptions.clientID,
domain: this.authOptions.domain,
audience: this.authOptions.audience,
redirectUri: `http://localhost:${port}/callback`,
scope: this.authOptions.scopes.join(' '),
responseType: this.authOptions.responseType,
code_challenge_method: 'S256',
code_challenge: pkp.challenge,
} as any);
const authorizeUrl = auth0.buildAuthorizeUrl({
redirectUri: `http://localhost:${port}/callback`,
responseType: this.authOptions.responseType,
});
await open(authorizeUrl);
// set a timeout if the authentication takes too long
setTimeout(() => {
server.close();
reject(new Error('Login timeout.'));
}, 30000);
} finally {
// server is usually closed by the callback or the timeout, this is to handle corner cases
setTimeout(() => {
server.close();
}, 50000);
}
});
}
public async signUp(): Promise<void> {
await open(this.authOptions.registerUri);
}
/**
* Returns extended account info for the given (and logged-in) sessionId.
*
* @param sessionId the sessionId to get info about. If not provided, all account info are returned
* @returns an array of IToken, containing extended info for the accounts
*/
public accountInfo(sessionId?: string) {
return this._tokens.filter((token) =>
sessionId ? token.sessionId === sessionId : true
);
}
/**
* Removes any logged-in sessions
*/
public logout() {
this._tokens.forEach((token) => this.removeSession(token.sessionId));
// remove any dangling credential in the keychain
this.keychain.deleteCredentials();
}
public async removeSession(sessionId: string): Promise<void> {
// remove token from memory, if successful fire the event
const token = this.removeInMemoryToken(sessionId);
if (token) {
this._onDidChangeSessions.fire({
added: [],
removed: [IToken2Session(token)],
changed: [],
});
}
// update the tokens in the keychain
this.keychain.storeCredentials(JSON.stringify(this._tokens));
}
/**
* Clears the refresh timeout associated to a session and removes the key from the set
*/
private clearSessionTimeout(sessionId: string): void {
const timeout = this._refreshTimeouts.get(sessionId);
if (timeout) {
clearTimeout(timeout);
this._refreshTimeouts.delete(sessionId);
}
}
/**
* Remove the given token from memory and clears the associated refresh timeout
* @param token
* @returns the removed token
*/
private removeInMemoryToken(sessionId: string): IToken | undefined {
const tokenIndex = this._tokens.findIndex(
(token) => token.sessionId === sessionId
);
let token: IToken | undefined;
if (tokenIndex > -1) {
token = this._tokens[tokenIndex];
this._tokens.splice(tokenIndex, 1);
}
this.clearSessionTimeout(sessionId);
return token;
}
/**
* Add the given token to memory storage and keychain. Prepares Timeout for token refresh
* NOTE: we currently support 1 token (logged user) at a time
* @param token
* @returns
*/
public async addToken(token: IToken): Promise<IToken | undefined> {
if (!token) {
return;
}
this._tokens = [token];
// update the tokens in the keychain
this.keychain.storeCredentials(JSON.stringify(this._tokens));
// notify subscribers about the newly added/changed session
const session = IToken2Session(token);
const changedToken = this._tokens.find(
(itoken) => itoken.sessionId === session.id
);
const changes = {
added: (!changedToken && [session]) || [],
removed: [],
changed: (!!changedToken && [session]) || [],
};
this._onDidChangeSessions.fire(changes);
// setup token refresh
this.clearSessionTimeout(token.sessionId);
if (token.expiresAt) {
// refresh the token 30sec before expiration
const expiration = token.expiresAt - Date.now() - 30 * 1000;
this._refreshTimeouts.set(
token.sessionId,
setTimeout(
async () => {
try {
const refreshedToken = await this.refreshToken(
token
);
this.addToken(refreshedToken);
} catch (e) {
await this.removeSession(token.sessionId);
}
},
expiration > 0 ? expiration : 0
)
);
}
return token;
}
}

View File

@ -0,0 +1,67 @@
import * as http from 'http';
import * as url from 'url';
import { body } from './body';
export const authCallbackPath = 'callback';
export const serverPort = 9876;
export function createServer(
authCallback: (req: http.IncomingMessage, res: http.ServerResponse) => void
) {
const server = http.createServer(function (req, res) {
const reqUrl = url.parse(req.url!, /* parseQueryString */ true);
switch (reqUrl.pathname) {
case `/${authCallbackPath}`:
authCallback(req, res);
res.writeHead(200, {
'Content-Length': body.length,
'Content-Type': 'text/html; charset=utf-8',
});
res.end(body);
break;
default:
res.writeHead(404);
res.end();
break;
}
});
return server;
}
export async function startServer(server: http.Server): Promise<string> {
let portTimer: NodeJS.Timer;
function cancelPortTimer() {
clearTimeout(portTimer);
}
const port = new Promise<string>((resolve, reject) => {
portTimer = setTimeout(() => {
reject(new Error('Timeout waiting for port'));
}, 5000);
server.on('listening', () => {
const address = server.address();
if (typeof address === 'undefined' || address === null) {
reject(new Error('address is null or undefined'));
} else if (typeof address === 'string') {
resolve(address);
} else {
resolve(address.port.toString());
}
});
server.on('error', (_) => {
reject(new Error('Error listening to server'));
});
server.on('close', () => {
reject(new Error('Closed'));
});
server.listen(serverPort);
});
port.then(cancelPortTimer, cancelPortTimer);
return port;
}

View File

@ -0,0 +1,87 @@
import { injectable } from 'inversify';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import {
AuthenticationService,
AuthenticationServiceClient,
AuthenticationSession,
} from '../../common/protocol/authentication-service';
import { ArduinoAuthenticationProvider } from './arduino-auth-provider';
import { AuthOptions } from './types';
@injectable()
export class AuthenticationServiceImpl
implements AuthenticationService, BackendApplicationContribution
{
protected readonly delegate = new ArduinoAuthenticationProvider();
protected readonly clients: AuthenticationServiceClient[] = [];
protected readonly toDispose = new DisposableCollection();
async onStart(): Promise<void> {
this.toDispose.pushAll([
this.delegate,
this.delegate.onDidChangeSessions(({ added, removed, changed }) => {
added?.forEach((session) =>
this.clients.forEach((client) =>
client.notifySessionDidChange(session)
)
);
changed?.forEach((session) =>
this.clients.forEach((client) =>
client.notifySessionDidChange(session)
)
);
removed?.forEach(() =>
this.clients.forEach((client) =>
client.notifySessionDidChange()
)
);
}),
Disposable.create(() =>
this.clients.forEach((client) => this.disposeClient(client))
),
]);
await this.delegate.init();
}
setOptions(authOptions: AuthOptions) {
this.delegate.setOptions(authOptions);
}
async login(): Promise<AuthenticationSession> {
return this.delegate.createSession();
}
async logout(): Promise<void> {
this.delegate.logout();
}
async session(): Promise<AuthenticationSession | undefined> {
const sessions = await this.delegate.getSessions();
return sessions[0];
}
dispose(): void {
this.toDispose.dispose();
}
setClient(client: AuthenticationServiceClient | undefined): void {
if (client) {
this.clients.push(client);
}
}
disposeClient(client: AuthenticationServiceClient) {
const index = this.clients.indexOf(client);
if (index === -1) {
console.warn(
'Could not dispose authentications service client. It was not registered. Skipping.'
);
return;
}
this.clients.splice(index, 1);
}
}

View File

@ -0,0 +1,134 @@
export const body = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Arduino Account</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html {
height: 100%;
}
body {
box-sizing: border-box;
min-height: 100%;
margin: 0;
padding: 15px 30px;
display: flex;
flex-direction: column;
color: white;
font-family: "Segoe UI", "Helvetica Neue", "Helvetica", Arial, sans-serif;
background-color: #2C2C32;
}
.branding {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNDIiIGhlaWdodD0iMTg3Ljg5Ij48cGF0aCBmaWxsPSIjRkZGIiBkPSJNMzE0LjIgOTYuMmMwLTM3LjgtMzEuOS02OC41LTcxLTY4LjUtMy42IDAtNy4zLjItMTAuOS44LTMwLjQgNC4zLTUwLjggMjYuMi02Mi4yIDQzLTExLjQtMTYuOC0zMS44LTM4LjctNjIuMi00My0zLjYtLjUtNy4zLS44LTEwLjktLjgtMzkuMiAwLTcxIDMwLjctNzEgNjguNXMzMS45IDY4LjUgNzEgNjguNWMzLjYgMCA3LjMtLjIgMTEtLjggMzAuNC00LjQgNTAuOC0yNi4zIDYyLjItNDMuMSAxMS40IDE2LjggMzEuOCAzOC43IDYyLjIgNDMuMSAzLjYuNSA3LjMuOCAxMSAuOCAzOC45IDAgNzAuOC0zMC43IDcwLjgtNjguNW0tMjA5LjggNDMuN2MtMi41LjQtNSAuNS03LjUuNS0yNS44IDAtNDYuNy0xOS45LTQ2LjctNDQuMkM1MC4yIDcxLjggNzEuMiA1MiA5NyA1MmMyLjUgMCA1IC4yIDcuNS41IDI4LjcgNC4xIDQ2LjIgMzIuNCA1Mi4yIDQzLjctNi4xIDExLjQtMjMuNyAzOS42LTUyLjMgNDMuN203OS4xLTQzLjdjNS45LTExLjMgMjMuNS0zOS42IDUyLjItNDMuNyAyLjUtLjMgNS0uNSA3LjUtLjUgMjUuOCAwIDQ2LjcgMTkuOSA0Ni43IDQ0LjIgMCAyNC40LTIxIDQ0LjItNDYuNyA0NC4yLTIuNSAwLTUtLjItNy41LS41LTI4LjctNC4xLTQ2LjMtMzIuNC01Mi4yLTQzLjciLz48cGF0aCBmaWxsPSIjRkZGIiBkPSJNNzcuMyA4OS41SDEyMHYxMy43SDc3LjN6bTE3MC42IDEzLjhoMTQuNFY4OS41aC0xNC40Vjc1LjFoLTEzLjh2MTQuNGgtMTQuNHYxMy44aDE0LjR2MTQuNGgxMy44em01Mi44LTYzLjljMC0zLjUgMi44LTYuNCA2LjItNi40IDMuNSAwIDYuMyAyLjkgNi4zIDYuNCAwIDMuNy0yLjggNi40LTYuMyA2LjQtMy40IDAtNi4yLTIuOS02LjItNi40em0xMS40IDBjMC0yLjktMi4yLTUuNC01LjItNS40cy01LjEgMi4yLTUuMSA1LjRjMCAzLjMgMi40IDUuNCA1LjEgNS40IDMgMCA1LjItMi4xIDUuMi01LjR6bS04LjItNGgzLjFjMi40IDAgMy4zIDEgMy4zIDIuNyAwIDEuMS0uNSAxLjktMS41IDIuM2wxLjUgM2gtMi4ybC0xLjEtMi43aC0xLjF2Mi43aC0ydi04em0yLjkgMy45Yy45IDAgMS40LS4zIDEuNC0xLjMgMC0uOS0uMy0xLjItMS40LTEuMmgtMXYyLjZsMS0uMXoiLz48L3N2Zz4=');
background-size: 24px;
background-repeat: no-repeat;
background-position: left center;
padding-left: 36px;
font-size: 20px;
letter-spacing: -0.04rem;
font-weight: 400;
color: white;
text-decoration: none;
}
.message-container {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
margin: 0 30px;
}
.message {
font-weight: 300;
font-size: 1.4rem;
}
body.error .message {
display: none;
}
body.error .error-message {
display: block;
}
.error-message {
display: none;
font-weight: 300;
font-size: 1.3rem;
}
.error-text {
color: red;
font-size: 1rem;
}
@font-face {
font-family: 'Segoe UI';
src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.eot"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.eot?#iefix") format("embedded-opentype");
src: local("Segoe UI Light"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.woff2") format("woff2"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.woff") format("woff"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.ttf") format("truetype"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.svg#web") format("svg");
font-weight: 200
}
@font-face {
font-family: 'Segoe UI';
src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.eot"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.eot?#iefix") format("embedded-opentype");
src: local("Segoe UI Semilight"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.woff2") format("woff2"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.woff") format("woff"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.ttf") format("truetype"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.svg#web") format("svg");
font-weight: 300
}
@font-face {
font-family: 'Segoe UI';
src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.eot"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.eot?#iefix") format("embedded-opentype");
src: local("Segoe UI"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.woff2") format("woff"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.woff") format("woff"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.ttf") format("truetype"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.svg#web") format("svg");
font-weight: 400
}
@font-face {
font-family: 'Segoe UI';
src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.eot"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.eot?#iefix") format("embedded-opentype");
src: local("Segoe UI Semibold"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.woff2") format("woff"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.woff") format("woff"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.ttf") format("truetype"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.svg#web") format("svg");
font-weight: 600
}
@font-face {
font-family: 'Segoe UI';
src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.eot"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.eot?#iefix") format("embedded-opentype");
src: local("Segoe UI Bold"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.woff2") format("woff"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.woff") format("woff"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.ttf") format("truetype"), url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.svg#web") format("svg");
font-weight: 700
}
</style>
</head>
<body>
<a class="branding" href="https://create.arduino.cc/">
Arduino Account
</a>
<div class="message-container">
<div class="message">
You are signed-in. You can close this browser window.
</div>
<div class="error-message">
An error occurred while signing in:
<div class="error-text"></div>
</div>
</div>
<script>
var search = window.location.search;
var error = (/[?&^]error=([^&]+)/.exec(search) || [])[1];
if (error) {
document.querySelector('.error-text').textContent = decodeURIComponent(error);
document.querySelector('body').classList.add('error');
}
window
</script>
</body>
</html>
`;

View File

@ -0,0 +1,76 @@
import type * as keytarType from 'keytar';
export type KeychainConfig = {
credentialsSection: string;
account: string;
};
type Keytar = {
getPassword: typeof keytarType['getPassword'];
setPassword: typeof keytarType['setPassword'];
deletePassword: typeof keytarType['deletePassword'];
};
export class Keychain {
credentialsSection: string;
account: string;
constructor(config: KeychainConfig) {
this.credentialsSection = config.credentialsSection;
this.account = config.account;
}
getKeytar(): Keytar | undefined {
try {
return require('keytar');
} catch (err) {
console.log(err);
}
return undefined;
}
async getStoredCredentials(): Promise<string | undefined | null> {
const keytar = this.getKeytar();
if (!keytar) {
return undefined;
}
try {
return keytar.getPassword(this.credentialsSection, this.account);
} catch {
return undefined;
}
}
async storeCredentials(stringifiedToken: string): Promise<boolean> {
const keytar = this.getKeytar();
if (!keytar) {
return false;
}
try {
await keytar.setPassword(
this.credentialsSection,
this.account,
stringifiedToken
);
return true;
} catch {
return false;
}
}
async deleteCredentials(): Promise<boolean> {
const keytar = this.getKeytar();
if (!keytar) {
return false;
}
try {
const result = await keytar.deletePassword(
this.credentialsSection,
this.account
);
return result;
} catch {
return false;
}
}
}

View File

@ -0,0 +1,26 @@
import { AuthenticationSession } from '../../common/protocol/authentication-service';
export { AuthenticationSession };
export type AuthOptions = {
redirectUri: string;
responseType: string;
clientID: string;
domain: string;
audience: string;
registerUri: string;
scopes: string[];
};
export interface AuthenticationProviderAuthenticationSessionsChangeEvent {
readonly added?: ReadonlyArray<AuthenticationSession>;
readonly removed?: ReadonlyArray<AuthenticationSession>;
readonly changed?: ReadonlyArray<AuthenticationSession>;
}
export interface AuthenticationProvider {
readonly onDidChangeSessions: any; // Event<AuthenticationProviderAuthenticationSessionsChangeEvent>;
getSessions(): Promise<ReadonlyArray<AuthenticationSession>>;
createSession(): Promise<AuthenticationSession>;
removeSession(sessionId: string): Promise<void>;
setOptions(authOptions: AuthOptions): void;
}

View File

@ -0,0 +1,105 @@
import jwt_decode from 'jwt-decode';
import { sha256 } from 'hash.js';
import { randomBytes } from 'crypto';
import btoa = require('btoa'); // TODO: check why we cannot
import { AuthenticationSession } from './types';
export interface IToken {
accessToken: string; // When unable to refresh due to network problems, the access token becomes undefined
idToken?: string; // depending on the scopes can be either supplied or empty
expiresIn?: number; // How long access token is valid, in seconds
expiresAt?: number; // UNIX epoch time at which token will expire
refreshToken: string;
account: {
id: string;
email: string;
nickname: string;
picture: string;
};
scope: string;
sessionId: string;
}
export namespace IToken {
// check if the token is expired or will expired before the buffer
export function requiresRefresh(token: IToken, buffer: number): boolean {
return token.expiresAt ? token.expiresAt < Date.now() + buffer : false;
}
}
export interface Token {
access_token: string;
id_token?: string;
refresh_token: string;
scope: 'offline_access' | string; // `offline_access`
expires_in: number; // expires in seconds
token_type: string; // `Bearer`
}
export type RefreshToken = Omit<Token, 'refresh_token'>;
export function token2IToken(token: Token): IToken {
const parsedIdToken: any =
(token.id_token && jwt_decode(token.id_token)) || {};
return {
idToken: token.id_token,
expiresIn: token.expires_in,
expiresAt: token.expires_in
? Date.now() + token.expires_in * 1000
: undefined,
accessToken: token.access_token,
refreshToken: token.refresh_token,
sessionId: parsedIdToken.sub,
scope: token.scope,
account: {
id: parsedIdToken.sub || 'unknown',
email: parsedIdToken.email || 'unknown',
nickname: parsedIdToken.nickname || 'unknown',
picture: parsedIdToken.picture || 'unknown',
},
};
}
export function IToken2Session(token: IToken): AuthenticationSession {
return {
accessToken: token.accessToken,
account: {
id: token.account.id,
label: token.account.nickname,
picture: token.account.picture,
email: token.account.email,
},
id: token.account.id,
scopes: token.scope.split(' '),
};
}
export function getRandomValues(input: Uint8Array): Uint8Array {
const bytes = randomBytes(input.length);
for (let i = 0, n = bytes.length; i < n; ++i) {
input[i] = bytes[i];
}
return input;
}
export function generateProofKeyPair() {
const urlEncode = (str: string) =>
str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const decode = (buffer: Uint8Array | number[]) => {
let decodedString = '';
for (let i = 0; i < buffer.length; i++) {
decodedString += String.fromCharCode(buffer[i]);
}
return decodedString;
};
const buffer = getRandomValues(new Uint8Array(32));
const seed = btoa(decode(buffer));
const verifier = urlEncode(seed);
const challenge = urlEncode(
btoa(decode(sha256().update(verifier).digest()))
);
return { verifier, challenge };
}

View File

@ -4,7 +4,6 @@ import * as temp from 'temp';
import * as yaml from 'js-yaml';
import { promisify } from 'util';
import * as grpc from '@grpc/grpc-js';
import * as deepmerge from 'deepmerge';
import { injectable, inject, named } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
@ -30,6 +29,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { deepClone } from '@theia/core';
const deepmerge = require('deepmerge');
const track = temp.track();
@injectable()

View File

@ -175,6 +175,10 @@ export class SketchesServiceImpl
return sketch;
}
async maybeLoadSketch(uri: string): Promise<Sketch | undefined> {
return this._isSketchFolder(uri);
}
private get recentSketchesFsPath(): Promise<string> {
return this.envVariableServer
.getConfigDirUri()

43
docs/README.md Normal file
View File

@ -0,0 +1,43 @@
# Remote Sketchbook
Arduino IDE provides a Remote Sketchbook feature that can be used to upload sketches to Arduino Cloud.
![](static/remote.png)
In order to use this feature, a user must be registered on [Arduino Cloud](https://store.arduino.cc/digital/create) and logged in.
This feature is completely optional and can be disabled in the IDE via _"File > Advanced > Hide Remote Sketchbook"_ menu item.
## Developer guide
A developer could use the content of this repo to create a customized version of this feature and implement a different remote storage as follows:
### 1. Changing remote connection parameters in the Preferences panel (be careful while editing the Preferences panel!)
Here a screenshot of the Preferences panel
![](static/preferences.png)
- The settings under _Arduino > Auth_ should be edited to match the OAuth2 configuration of your custom remote sketchbook storage
- The setting under _Arduino > Sketch Sync Endpoint_ should be edited to point to your custom remote sketchbook storage service
### 2. Implementing the Arduino Cloud Store APIs for your custom remote sketchbook storage
Following the API Reference below:
| API Call | OpenAPI documentation |
| ------------- | ------------- |
| DELETE create/v2/files/d/$HOME/sketches_v2{posixPath} | https://api2.arduino.cc/create/docs#!/files95v2/files_v2_deletedir |
| DELETE create/v2/files/f/$HOME/sketches_v2{posixPath} | https://api2.arduino.cc/create/docs#!/files95v2/files_v2_deletefile |
| GET create/v2/files/d/$HOME/sketches_v2{posixPath} | https://api2.arduino.cc/create/docs#!/files95v2/files_v2_list |
| GET create/v2/files/f/$HOME/sketches_v2{posixPath} | https://api2.arduino.cc/create/docs#!/files95v2/files_v2_read |
| GET create/v2/sketches | https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_search |
| GET create/v2/sketches/byID/{id} | https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_byID |
| GET create/v2/sketches/byPath{path} | https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_byPath |
| POST create/v2/files/d/$HOME/sketches_v2{posixPath} | https://api2.arduino.cc/create/docs#!/files95v2/files_v2_mkdir |
| POST create/v2/files/f/$HOME/sketches_v2{posixPath} | https://api2.arduino.cc/create/docs#!/files95v2/files_v2_write |
| POST create/v2/sketches/{sketch.id} | https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_edit |
| POST create/v3/files/cp | https://api2.arduino.cc/create/docs#!/files95v3/files_v3_copy |
| POST create/v3/files/mv | https://api2.arduino.cc/create/docs#!/files95v3/files_v3_move |
| PUT create/v2/sketches | https://api2.arduino.cc/create/docs#!/sketches95v2/sketches_v2_create |
## Build the Arduino IDE with this extension
To build the Arduino IDE with this extension, run the following in a terminal. On Windows, use _Git Bash_.
```sh
./bootstrap.sh
```

BIN
docs/static/preferences.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
docs/static/remote.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -13,39 +13,3 @@ yarn --cwd ./electron/packager/ && yarn --cwd ./electron/packager/ package
```
The packaged application will be under the `./electron/build/dist` folder.
## CI
We always build an electron-based application for Windows. Create a PR, and the CI will automatically create the app for Windows. Do you need the builds for macOS and Linux? Start a build manually.
The electron packager runs when:
- the build is manually triggered by the user, or
- on scheduled (CRON) jobs.
## Creating a Release Draft
One can create a GitHub release draft, tag the source, and upload the artifacts to GitHub with Azure.
- Go to the Azure [build](https://dev.azure.com/typefox/Arduino/_build) page.
- Click on `Queue` in the top right corner.
- Set the branch to `master` or leave as is if it is already showing `master`.
- Add the `Release.Tag` pipeline variable and set the desired release version. Note, the version must start with `v` and we recommend naming tags that fit within [semantic versioning](https://semver.org).
![](static/azure-create-gh-release.jpg)
- Click on `Queue`.
- 🎈🎉
## Publishing the Release Draft
One has to manually publish the GitHub release.
- Go to the [release page](https://github.com/bcmi-labs/arduino-editor/releases) of the `arduino-editor` repository.
- Select your release draft.
- Click on `Edit`.
![](static/edit-gh-release-draft.jpg)
- Optionally, you can adjust the release draft if you want.
![](static/publish-gh-release.jpg)
- Select `Publish release`.
- ✨

View File

@ -0,0 +1,3 @@
[
"arduino-ide-extension"
]

View File

@ -42,11 +42,13 @@
// rm -rf ../working-copy
rm('-rf', path('..', workingCopy));
// Clean up the `./electron/build` folder.
shell.exec(`git -C ${path('..', 'build')} clean -ffxdq`, { async: false });
const resourcesToKeep = ['patch', 'resources', 'scripts', 'template-package.json'];
for (const filename of fs.readdirSync(path('..', 'build')).filter(filename => resourcesToKeep.indexOf(filename) === -1)) {
rm('-rf', path('..', 'build', filename));
}
const extensions = [
'arduino-ide-extension'
];
const extensions = require('./extensions.json');
echo(`Building the application with the following extensions:\n${extensions.map(ext => ` - ${ext}`).join(',\n')}`);
const allDependencies = [
...extensions,
'electron-app'

View File

@ -18,7 +18,7 @@
"7zip-min": "^1.1.1",
"chai": "^4.2.0",
"dateformat": "^3.0.3",
"deepmerge": "^4.2.2",
"deepmerge": "2.01",
"depcheck": "^0.9.2",
"file-type": "^14.1.4",
"glob": "^7.1.6",

View File

@ -473,10 +473,10 @@ deep-eql@^3.0.1:
dependencies:
type-detect "^4.0.0"
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
deepmerge@2.01:
version "2.1.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.1.tgz#e862b4e45ea0555072bf51e7fd0d9845170ae768"
integrity sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w==
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -2,7 +2,7 @@
"name": "arduino-ide",
"version": "2.0.0-beta.7",
"description": "Arduino IDE",
"repository": "https://github.com/bcmi-labs/arduino-editor.git",
"repository": "https://github.com/arduino/arduino-ide.git",
"author": "Arduino SA",
"license": "AGPL-3.0-or-later",
"private": true,

310
yarn.lock
View File

@ -2633,6 +2633,16 @@
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
"@types/atob@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@types/atob/-/atob-2.1.2.tgz#157eb0cc46264a8c55f2273a836c7a1a644fb820"
integrity sha512-8GAYQ1jDRUQkSpHzJUqXwAkYFOxuWAOGLhIR4aPd/Y/yL12Q/9m7LsKpHKlfKdNE/362Hc9wPI1Yh6opDfxVJg==
"@types/auth0-js@^9.14.0":
version "9.14.4"
resolved "https://registry.yarnpkg.com/@types/auth0-js/-/auth0-js-9.14.4.tgz#cd358fa2ab5377f37f3ca56fc769877f75fc5ba3"
integrity sha512-cnGo/1qzZTtdqirNKn+HHO2g45gtxLwzOwCHhimPQZoR8cNlYertVeHqk0Zm7YXdJBskTkuJuOFVg4hHs27ovA==
"@types/base64-arraybuffer@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@types/base64-arraybuffer/-/base64-arraybuffer-0.1.0.tgz#739eea0a974d13ae831f96d97d882ceb0b187543"
@ -2646,6 +2656,13 @@
"@types/connect" "*"
"@types/node" "*"
"@types/btoa@^1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/btoa/-/btoa-1.2.3.tgz#2c8e7093f902bf8f0e10992a731a4996aa1a5732"
integrity sha512-ANNCZICS/ofxhzUl8V1DniBJs+sFQ+Yg5am1ZwVEf/sxoKY/J2+h5Fuw3xUErlZ7eJLdgzukBjZwnsV6+/2Rmg==
dependencies:
"@types/node" "*"
"@types/caseless@*":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
@ -2762,6 +2779,13 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
"@types/keytar@^4.4.0":
version "4.4.2"
resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.2.tgz#49ef917d6cbb4f19241c0ab50cd35097b5729b32"
integrity sha512-xtQcDj9ruGnMwvSu1E2BH4SFa5Dv2PvSPd0CKEBLN5hEj/v5YpXJY+B6hAfuKIbvEomD7vJTc/P1s1xPNh2kRw==
dependencies:
keytar "*"
"@types/lodash.debounce@4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.3.tgz#d712aee9e6136be77f70523ed9f0fc049a6cf15a"
@ -3142,73 +3166,72 @@
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^4.27.0":
version "4.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.27.0.tgz#0b7fc974e8bc9b2b5eb98ed51427b0be529b4ad0"
integrity sha512-DsLqxeUfLVNp3AO7PC3JyaddmEHTtI9qTSAs+RB6ja27QvIM0TA8Cizn1qcS6vOu+WDLFJzkwkgweiyFhssDdQ==
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz#1a66f03b264844387beb7dc85e1f1d403bd1803f"
integrity sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==
dependencies:
"@typescript-eslint/experimental-utils" "4.27.0"
"@typescript-eslint/scope-manager" "4.27.0"
"@typescript-eslint/experimental-utils" "4.28.0"
"@typescript-eslint/scope-manager" "4.28.0"
debug "^4.3.1"
functional-red-black-tree "^1.0.1"
lodash "^4.17.21"
regexpp "^3.1.0"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/experimental-utils@4.27.0":
version "4.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.27.0.tgz#78192a616472d199f084eab8f10f962c0757cd1c"
integrity sha512-n5NlbnmzT2MXlyT+Y0Jf0gsmAQzCnQSWXKy4RGSXVStjDvS5we9IWbh7qRVKdGcxT0WYlgcCYUK/HRg7xFhvjQ==
"@typescript-eslint/experimental-utils@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz#13167ed991320684bdc23588135ae62115b30ee0"
integrity sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==
dependencies:
"@types/json-schema" "^7.0.7"
"@typescript-eslint/scope-manager" "4.27.0"
"@typescript-eslint/types" "4.27.0"
"@typescript-eslint/typescript-estree" "4.27.0"
"@typescript-eslint/scope-manager" "4.28.0"
"@typescript-eslint/types" "4.28.0"
"@typescript-eslint/typescript-estree" "4.28.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/parser@^4.27.0":
version "4.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.27.0.tgz#85447e573364bce4c46c7f64abaa4985aadf5a94"
integrity sha512-XpbxL+M+gClmJcJ5kHnUpBGmlGdgNvy6cehgR6ufyxkEJMGP25tZKCaKyC0W/JVpuhU3VU1RBn7SYUPKSMqQvQ==
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa"
integrity sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==
dependencies:
"@typescript-eslint/scope-manager" "4.27.0"
"@typescript-eslint/types" "4.27.0"
"@typescript-eslint/typescript-estree" "4.27.0"
"@typescript-eslint/scope-manager" "4.28.0"
"@typescript-eslint/types" "4.28.0"
"@typescript-eslint/typescript-estree" "4.28.0"
debug "^4.3.1"
"@typescript-eslint/scope-manager@4.27.0":
version "4.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.27.0.tgz#b0b1de2b35aaf7f532e89c8e81d0fa298cae327d"
integrity sha512-DY73jK6SEH6UDdzc6maF19AHQJBFVRf6fgAXHPXCGEmpqD4vYgPEzqpFz1lf/daSbOcMpPPj9tyXXDPW2XReAw==
"@typescript-eslint/scope-manager@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz#6a3009d2ab64a30fc8a1e257a1a320067f36a0ce"
integrity sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==
dependencies:
"@typescript-eslint/types" "4.27.0"
"@typescript-eslint/visitor-keys" "4.27.0"
"@typescript-eslint/types" "4.28.0"
"@typescript-eslint/visitor-keys" "4.28.0"
"@typescript-eslint/types@4.27.0":
version "4.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.27.0.tgz#712b408519ed699baff69086bc59cd2fc13df8d8"
integrity sha512-I4ps3SCPFCKclRcvnsVA/7sWzh7naaM/b4pBO2hVxnM3wrU51Lveybdw5WoIktU/V4KfXrTt94V9b065b/0+wA==
"@typescript-eslint/types@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0"
integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==
"@typescript-eslint/typescript-estree@4.27.0":
version "4.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.27.0.tgz#189a7b9f1d0717d5cccdcc17247692dedf7a09da"
integrity sha512-KH03GUsUj41sRLLEy2JHstnezgpS5VNhrJouRdmh6yNdQ+yl8w5LrSwBkExM+jWwCJa7Ct2c8yl8NdtNRyQO6g==
"@typescript-eslint/typescript-estree@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf"
integrity sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==
dependencies:
"@typescript-eslint/types" "4.27.0"
"@typescript-eslint/visitor-keys" "4.27.0"
"@typescript-eslint/types" "4.28.0"
"@typescript-eslint/visitor-keys" "4.28.0"
debug "^4.3.1"
globby "^11.0.3"
is-glob "^4.0.1"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@4.27.0":
version "4.27.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.27.0.tgz#f56138b993ec822793e7ebcfac6ffdce0a60cb81"
integrity sha512-es0GRYNZp0ieckZ938cEANfEhsfHrzuLrePukLKtY3/KPXcq1Xd555Mno9/GOgXhKzn0QfkDLVgqWO3dGY80bg==
"@typescript-eslint/visitor-keys@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434"
integrity sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==
dependencies:
"@typescript-eslint/types" "4.27.0"
"@typescript-eslint/types" "4.28.0"
eslint-visitor-keys "^2.0.0"
"@webassemblyjs/ast@1.9.0":
@ -4093,6 +4116,19 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
auth0-js@^9.14.0:
version "9.15.0"
resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-9.15.0.tgz#ebb88aa87e9aee313f084e3295a57eadaf2d4f8c"
integrity sha512-LM9gdOeN7yG+F7OWaq8LeJ21GR3ZyKV72+IAN8/MrxPRr7VAMzdvXEsLTx7r9QizHBKfNfXbRoy1AeLQVPJqWQ==
dependencies:
base64-js "^1.3.0"
idtoken-verifier "^2.0.3"
js-cookie "^2.2.0"
qs "^6.7.0"
superagent "^5.3.1"
url-join "^4.0.1"
winchan "^0.2.2"
autolinker@~0.28.0:
version "0.28.1"
resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.28.1.tgz#0652b491881879f0775dace0cdca3233942a4e47"
@ -5160,6 +5196,11 @@ btoa-lite@^1.0.0:
resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc=
btoa@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
@ -5944,7 +5985,7 @@ compare-func@^2.0.0:
array-ify "^1.0.0"
dot-prop "^5.1.0"
component-emitter@^1.2.1:
component-emitter@^1.2.1, component-emitter@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
@ -6142,6 +6183,11 @@ cookie@^0.4.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
cookiejar@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==
copy-anything@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.3.tgz#842407ba02466b0df844819bbe3baebbe5d45d87"
@ -6354,6 +6400,11 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
crypto-js@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.3.0.tgz#846dd1cce2f68aacfa156c8578f926a609b7976b"
integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==
css-color-names@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@ -6683,7 +6734,7 @@ deep-is@^0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
deepmerge@*, deepmerge@^4.2.2:
deepmerge@*:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
@ -6717,6 +6768,11 @@ defer-to-connect@^1.0.1:
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@ -7276,7 +7332,7 @@ es6-error@^4.1.1:
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
es6-promise@^4.0.3, es6-promise@^4.2.4:
es6-promise@^4.0.3, es6-promise@^4.2.4, es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
@ -7396,9 +7452,9 @@ eslint-visitor-keys@^2.0.0:
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint@^7.28.0:
version "7.28.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.28.0.tgz#435aa17a0b82c13bb2be9d51408b617e49c1e820"
integrity sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==
version "7.29.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0"
integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.2"
@ -7831,6 +7887,11 @@ fast-plist@^0.1.2:
resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8"
integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg=
fast-safe-stringify@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
fast-text-encoding@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
@ -8213,6 +8274,11 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formidable@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9"
integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -9116,7 +9182,7 @@ hash-base@^3.0.0:
readable-stream "^3.6.0"
safe-buffer "^5.2.0"
hash.js@^1.0.0, hash.js@^1.0.3:
hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
@ -9352,6 +9418,18 @@ idb@^4.0.5:
resolved "https://registry.yarnpkg.com/idb/-/idb-4.0.5.tgz#23b930fbb0abce391e939c35b7b31a669e74041f"
integrity sha512-P+Fk9HT2h1DhXoE1YNK183SY+CRh2GHNh28de94sGwhe0bUA75JJeVJWt3SenE5p0BXK7maflIq29dl6UZHrFw==
idtoken-verifier@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-2.1.0.tgz#e61ea083be596390012aff6d9f12c2599af4847b"
integrity sha512-X0423UM4Rc5bFb39Ai0YHr35rcexlu4oakKdYzSGZxtoPy84P86hhAbzlpgbgomcLOFRgzgKRvhY7YjO5g8OPA==
dependencies:
base64-js "^1.3.0"
crypto-js "^3.2.1"
es6-promise "^4.2.8"
jsbn "^1.1.0"
unfetch "^4.1.0"
url-join "^4.0.1"
ieee754@^1.1.13, ieee754@^1.1.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -9724,6 +9802,11 @@ is-directory@^0.3.1:
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
is-dotfile@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@ -9813,6 +9896,13 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
is-invalid-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/is-invalid-path/-/is-invalid-path-0.1.0.tgz#307a855b3cf1a938b44ea70d2c61106053714f34"
integrity sha1-MHqFWzzxqTi0TqcNLGEQYFNxTzQ=
dependencies:
is-glob "^2.0.0"
is-natural-number@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8"
@ -10026,6 +10116,13 @@ is-utf8@^0.2.0, is-utf8@^0.2.1:
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
is-valid-path@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df"
integrity sha1-EQ+f90w39mPh7HkV60UfLbk6yd8=
dependencies:
is-invalid-path "^0.1.0"
is-what@^3.12.0:
version "3.14.1"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
@ -10041,6 +10138,13 @@ is-wsl@^1.1.0:
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
dependencies:
is-docker "^2.0.0"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@ -10122,6 +10226,11 @@ js-base64@^2.1.9:
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
js-cookie@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -10156,6 +10265,11 @@ js-yaml@~3.7.0:
argparse "^1.0.7"
esprima "^2.6.0"
jsbn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA=
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -10351,6 +10465,27 @@ jws@^4.0.0:
jwa "^2.0.0"
safe-buffer "^5.0.1"
jwt-decode@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==
keytar@*:
version "7.6.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.6.0.tgz#498e796443cb543d31722099443f29d7b5c44100"
integrity sha512-H3cvrTzWb11+iv0NOAnoNAPgEapVZnYLVHZQyxmh7jdmVfR/c0jNNFEZ6AI38W/4DeTGTaY66ZX4Z1SbfKPvCQ==
dependencies:
node-addon-api "^3.0.0"
prebuild-install "^6.0.0"
keytar@7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.2.0.tgz#4db2bec4f9700743ffd9eda22eebb658965c8440"
integrity sha512-ECSaWvoLKI5SI0pGpZQeUV1/lpBYfkaxvoSp3zkiPOz05VavwSfLi8DdEaa9N2ekQZv3Chy+o7aP6n9mairBgw==
dependencies:
node-addon-api "^3.0.0"
prebuild-install "^6.0.0"
keyv@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
@ -10732,7 +10867,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0:
lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -11153,7 +11288,7 @@ merge2@^1.2.3, merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
methods@~1.1.2:
methods@^1.1.2, methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
@ -11229,7 +11364,7 @@ mime@1.6.0, mime@^1.4.1:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.0.3, mime@^2.4.4:
mime@^2.0.3, mime@^2.4.4, mime@^2.4.6:
version "2.5.2"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
@ -11631,6 +11766,18 @@ node-abi@^2.11.0, node-abi@^2.7.0:
dependencies:
semver "^5.4.1"
node-abi@^2.21.0:
version "2.26.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.26.0.tgz#355d5d4bc603e856f74197adbf3f5117a396ba40"
integrity sha512-ag/Vos/mXXpWLLAYWsAoQdgS+gW7IwvgMLOgqopm/DbzAjazLltzgzpVMsFlgmo9TzG5hGXeaBZx2AI731RIsQ==
dependencies:
semver "^5.4.1"
node-addon-api@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
node-dir@0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.8.tgz#55fb8deb699070707fb67f91a460f0448294c77d"
@ -12174,6 +12321,15 @@ oniguruma@^7.2.0:
dependencies:
nan "^2.14.0"
open@^8.0.6:
version "8.0.6"
resolved "https://registry.yarnpkg.com/open/-/open-8.0.6.tgz#bdf94a80b4ef5685d8c7b58fb0fbbe5729b37204"
integrity sha512-vDOC0KwGabMPFtIpCO2QOnQeOz0N2rEkbuCuxICwLMUCrpv+A7NHrrzJ2dQReJmVluHhO4pYRh/Pn6s8t7Op6Q==
dependencies:
define-lazy-prop "^2.0.0"
is-docker "^2.1.1"
is-wsl "^2.2.0"
optimist@~0.3.5:
version "0.3.7"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9"
@ -13054,6 +13210,26 @@ prebuild-install@^5.2.4:
tunnel-agent "^0.6.0"
which-pm-runs "^1.0.0"
prebuild-install@^6.0.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.1.tgz#6754fa6c0d55eced7f9e14408ff9e4cba6f097b4"
integrity sha512-M+cKwofFlHa5VpTWub7GLg5RLcunYIcLqtY5pKcls/u7xaAb8FrXZ520qY8rkpYy5xw90tYCyMO0MP5ggzR3Sw==
dependencies:
detect-libc "^1.0.3"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^2.21.0"
noop-logger "^0.1.1"
npmlog "^4.0.1"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^3.0.3"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -13327,7 +13503,7 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.9.4:
qs@^6.7.0, qs@^6.9.4:
version "6.10.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
@ -15138,6 +15314,23 @@ sumchecker@^3.0.1:
dependencies:
debug "^4.1.0"
superagent@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-5.3.1.tgz#d62f3234d76b8138c1320e90fa83dc1850ccabf1"
integrity sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==
dependencies:
component-emitter "^1.3.0"
cookiejar "^2.1.2"
debug "^4.1.1"
fast-safe-stringify "^2.0.7"
form-data "^3.0.0"
formidable "^1.2.2"
methods "^1.1.2"
mime "^2.4.6"
qs "^6.9.4"
readable-stream "^3.6.0"
semver "^7.3.2"
supports-color@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a"
@ -15764,6 +15957,11 @@ underscore@~1.6.0:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
integrity sha1-izixDKze9jM3uLJOT/htRa6lKag=
unfetch@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@ -15917,6 +16115,11 @@ urix@^0.1.0:
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-join@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
url-loader@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8"
@ -16326,6 +16529,11 @@ wide-align@1.1.3, wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
winchan@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.2.tgz#6766917b88e5e1cb75f455ffc7cc13f51e5c834e"
integrity sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ==
windows-release@^3.1.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999"