feat: introduced cloud state in sketchbook view

Closes #1879
Closes #1876
Closes #1899
Closes #1878

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2023-02-15 18:03:37 +01:00 committed by Akos Kitta
parent b09ae48536
commit 0ab28266df
53 changed files with 1971 additions and 659 deletions

File diff suppressed because one or more lines are too long

View File

@ -53,7 +53,6 @@
"@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/p-queue": "^2.3.1",
"@types/ps-tree": "^1.1.0",
@ -66,6 +65,7 @@
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"classnames": "^2.3.1",
"cpy": "^8.1.2",
"cross-fetch": "^3.1.5",
"dateformat": "^3.0.3",
"deepmerge": "2.0.1",
@ -76,6 +76,7 @@
"glob": "^7.1.6",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-online": "^9.0.1",
"js-yaml": "^3.13.1",
"jsonc-parser": "^2.2.0",
"just-diff": "^5.1.1",
@ -83,7 +84,6 @@
"keytar": "7.2.0",
"lodash.debounce": "^4.0.8",
"minimatch": "^3.1.2",
"ncp": "^2.0.0",
"node-fetch": "^2.6.1",
"open": "^8.0.6",
"p-debounce": "^2.1.0",
@ -120,6 +120,7 @@
"mocha": "^7.0.0",
"mockdate": "^3.0.5",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"uuid": "^3.2.1",

View File

@ -93,6 +93,8 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th
import {
FrontendConnectionStatusService,
ApplicationConnectionStatusContribution,
DaemonPort,
IsOnline,
} from './theia/core/connection-status-service';
import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
@ -353,6 +355,7 @@ import { CreateFeatures } from './create/create-features';
import { Account } from './contributions/account';
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
import { CreateCloudCopy } from './contributions/create-cloud-copy';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
@ -741,6 +744,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, ValidateSketch);
Contribution.configure(bind, RenameCloudSketch);
Contribution.configure(bind, Account);
Contribution.configure(bind, CloudSketchbookContribution);
Contribution.configure(bind, CreateCloudCopy);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@ -919,8 +924,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
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();
@ -1026,4 +1029,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
bind(ArduinoComponentContextMenuRenderer).toSelf().inSingletonScope();
bind(DaemonPort).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(DaemonPort);
bind(IsOnline).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(IsOnline);
});

View File

@ -8,6 +8,7 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands';
import { CreateFeatures } from '../create/create-features';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import {
Command,
CommandRegistry,
@ -29,6 +30,8 @@ export class Account extends Contribution {
private readonly windowService: WindowService;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private readonly toDispose = new DisposableCollection();
private app: FrontendApplication;
@ -50,21 +53,28 @@ export class Account extends Contribution {
override registerCommands(registry: CommandRegistry): void {
const openExternal = (url: string) =>
this.windowService.openNewWindow(url, { external: true });
const loggedIn = () => Boolean(this.createFeatures.session);
const loggedInWithInternetConnection = () =>
loggedIn() && this.connectionStatus.offlineStatus !== 'internet';
registry.registerCommand(Account.Commands.LEARN_MORE, {
execute: () => openExternal(LEARN_MORE_URL),
isEnabled: () => !Boolean(this.createFeatures.session),
isEnabled: () => !loggedIn(),
isVisible: () => !loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_PROFILE, {
execute: () => openExternal('https://id.arduino.cc/'),
isEnabled: () => Boolean(this.createFeatures.session),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, {
execute: () => openExternal('https://create.arduino.cc/editor'),
isEnabled: () => Boolean(this.createFeatures.session),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, {
execute: () => openExternal('https://create.arduino.cc/iot/'),
isEnabled: () => Boolean(this.createFeatures.session),
isEnabled: () => loggedInWithInternetConnection(),
isVisible: () => loggedIn(),
});
}

View File

@ -93,7 +93,7 @@ export abstract class CloudSketchContribution extends SketchContribution {
);
}
try {
await treeModel.sketchbookTree().pull({ node });
await treeModel.sketchbookTree().pull({ node }, true);
return node;
} catch (err) {
if (isNotFound(err)) {

View File

@ -14,7 +14,6 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import {
MenuModelRegistry,
MenuContribution,
@ -58,7 +57,7 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { ExecuteWithProgress } from '../../common/protocol/progressible';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '../theia/messages/notifications-manager';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service';
import { MainMenuManager } from '../../common/main-menu-manager';
@ -295,7 +294,7 @@ export abstract class CoreServiceContribution extends SketchContribution {
}
private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager.getMessageId({
return this.notificationManager['getMessageId']({
text: message,
actions,
type: MessageType.Error,

View File

@ -0,0 +1,118 @@
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ApplicationShell } from '@theia/core/lib/browser/shell';
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Create } from '../create/typings';
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree';
import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model';
import { CloudSketchContribution, pushingSketch } from './cloud-contribution';
import {
CreateNewCloudSketchCallback,
NewCloudSketch,
NewCloudSketchParams,
} from './new-cloud-sketch';
import { saveOntoCopiedSketch } from './save-as-sketch';
interface CreateCloudCopyParams {
readonly model: SketchbookTreeModel;
readonly node: SketchbookTree.SketchDirNode;
}
function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams {
return (
typeof arg === 'object' &&
(<CreateCloudCopyParams>arg).model !== undefined &&
(<CreateCloudCopyParams>arg).model instanceof SketchbookTreeModel &&
(<CreateCloudCopyParams>arg).node !== undefined &&
SketchbookTree.SketchDirNode.is((<CreateCloudCopyParams>arg).node)
);
}
@injectable()
export class CreateCloudCopy extends CloudSketchContribution {
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private shell: ApplicationShell;
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, {
execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args),
isEnabled: (args: unknown) =>
Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args),
isVisible: (args: unknown) =>
Boolean(this.createFeatures.enabled) &&
Boolean(this.createFeatures.session) &&
this.connectionStatus.offlineStatus !== 'internet' &&
isCreateCloudCopyParams(args),
});
}
/**
* - creates new cloud sketch with the name of the params sketch,
* - pulls the cloud sketch,
* - copies files from params sketch to pulled cloud sketch in the cache folder,
* - pushes the cloud sketch, and
* - opens in new window.
*/
private async createCloudCopy(params: CreateCloudCopyParams): Promise<void> {
const sketch = await this.sketchesService.loadSketch(
params.node.fileStat.resource.toString()
);
const callback: CreateNewCloudSketchCallback = async (
newSketch: Create.Sketch,
newNode: CloudSketchbookTree.CloudSketchDirNode,
progress: Progress
) => {
const treeModel = await this.treeModel();
if (!treeModel) {
throw new Error('Could not retrieve the cloud sketchbook tree model.');
}
progress.report({
message: nls.localize(
'arduino/createCloudCopy/copyingSketchFilesMessage',
'Copying local sketch files...'
),
});
const localCacheFolderUri = newNode.uri.toString();
await this.sketchesService.copy(sketch, {
destinationUri: localCacheFolderUri,
onlySketchFiles: true,
});
await saveOntoCopiedSketch(
sketch,
localCacheFolderUri,
this.shell,
this.editorManager
);
progress.report({ message: pushingSketch(newSketch.name) });
await treeModel.sketchbookTree().push(newNode, true, true);
};
return this.commandService.executeCommand(
NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
<NewCloudSketchParams>{
initialValue: params.node.fileStat.name,
callback,
skipShowErrorMessageOnOpen: false,
}
);
}
}
export namespace CreateCloudCopy {
export namespace Commands {
export const CREATE_CLOUD_COPY: Command = {
id: 'arduino-create-cloud-copy',
iconClass: 'fa fa-arduino-cloud-upload',
};
}
}

View File

@ -6,7 +6,7 @@ import { Progress } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { CreateUri } from '../create/create-uri';
import { isConflict } from '../create/typings';
import { Create, isConflict } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
TaskFactoryImpl,
@ -15,13 +15,36 @@ import {
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import { Command, CommandRegistry, Sketch } from './contribution';
import {
CloudSketchContribution,
pullingSketch,
sketchAlreadyExists,
synchronizingSketchbook,
} from './cloud-contribution';
import { Command, CommandRegistry, Sketch } from './contribution';
export interface CreateNewCloudSketchCallback {
(
newSketch: Create.Sketch,
newNode: CloudSketchbookTree.CloudSketchDirNode,
progress: Progress
): Promise<void>;
}
export interface NewCloudSketchParams {
/**
* Value to populate the dialog `<input>` when it opens.
*/
readonly initialValue?: string | undefined;
/**
* Additional callback to call when the new cloud sketch has been created.
*/
readonly callback?: CreateNewCloudSketchCallback;
/**
* If `true`, the validation error message will not be visible in the input dialog, but the `OK` button will be disabled. Defaults to `true`.
*/
readonly skipShowErrorMessageOnOpen?: boolean;
}
@injectable()
export class NewCloudSketch extends CloudSketchContribution {
@ -43,7 +66,12 @@ export class NewCloudSketch extends CloudSketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
execute: () => this.createNewSketch(true),
execute: (params: NewCloudSketchParams) =>
this.createNewSketch(
params?.skipShowErrorMessageOnOpen === false ? false : true,
params?.initialValue,
params?.callback
),
isEnabled: () => Boolean(this.createFeatures.session),
isVisible: () => this.createFeatures.enabled,
});
@ -66,7 +94,8 @@ export class NewCloudSketch extends CloudSketchContribution {
private async createNewSketch(
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
initialValue?: string | undefined,
callback?: CreateNewCloudSketchCallback
): Promise<void> {
const treeModel = await this.treeModel();
if (treeModel) {
@ -75,7 +104,8 @@ export class NewCloudSketch extends CloudSketchContribution {
rootNode,
treeModel,
skipShowErrorMessageOnOpen,
initialValue
initialValue,
callback
);
}
}
@ -84,13 +114,14 @@ export class NewCloudSketch extends CloudSketchContribution {
rootNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
skipShowErrorMessageOnOpen: boolean,
initialValue?: string | undefined
initialValue?: string | undefined,
callback?: CreateNewCloudSketchCallback
): Promise<void> {
const existingNames = rootNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
const taskFactory = new TaskFactoryImpl((value) =>
this.createNewSketchWithProgress(treeModel, value)
this.createNewSketchWithProgress(treeModel, value, callback)
);
try {
const dialog = new WorkspaceInputDialogWithProgress(
@ -118,7 +149,11 @@ export class NewCloudSketch extends CloudSketchContribution {
} catch (err) {
if (isConflict(err)) {
await treeModel.refresh();
return this.createNewSketch(false, taskFactory.value ?? initialValue);
return this.createNewSketch(
false,
taskFactory.value ?? initialValue,
callback
);
}
throw err;
}
@ -126,7 +161,8 @@ export class NewCloudSketch extends CloudSketchContribution {
private createNewSketchWithProgress(
treeModel: CloudSketchbookTreeModel,
value: string
value: string,
callback?: CreateNewCloudSketchCallback
): (
progress: Progress
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
@ -143,6 +179,9 @@ export class NewCloudSketch extends CloudSketchContribution {
await treeModel.refresh();
progress.report({ message: pullingSketch(sketch.name) });
const node = await this.pull(sketch);
if (callback && node) {
await callback(sketch, node, progress);
}
return node;
};
}
@ -152,7 +191,7 @@ export class NewCloudSketch extends CloudSketchContribution {
): Promise<void> {
return this.commandService.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node }
{ node, treeWidgetId: 'cloud-sketchbook-composite-widget' }
);
}
}

View File

@ -123,7 +123,7 @@ export class RenameCloudSketch extends CloudSketchContribution {
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
// push
progress.report({ message: pushingSketch(params.sketch.name) });
await treeModel.sketchbookTree().push(node);
await treeModel.sketchbookTree().push(node, true);
// rename
progress.report({

View File

@ -6,6 +6,7 @@ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shel
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
import { StartupTask } from '../../electron-common/startup-task';
import { ArduinoMenus } from '../menu/arduino-menus';
@ -28,7 +29,7 @@ import {
@injectable()
export class SaveAsSketch extends CloudSketchContribution {
@inject(ApplicationShell)
private readonly applicationShell: ApplicationShell;
private readonly shell: ApplicationShell;
@inject(WindowService)
private readonly windowService: WindowService;
@ -80,14 +81,17 @@ export class SaveAsSketch extends CloudSketchContribution {
return false;
}
const newWorkspaceUri = await this.sketchesService.copy(sketch, {
const copiedSketch = await this.sketchesService.copy(sketch, {
destinationUri,
});
if (!newWorkspaceUri) {
return false;
}
const newWorkspaceUri = copiedSketch.uri;
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
await saveOntoCopiedSketch(
sketch,
newWorkspaceUri,
this.shell,
this.editorManager
);
if (markAsRecentlyOpened) {
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
}
@ -238,53 +242,6 @@ ${dialogContent.question}`.trim();
}
return sketchFolderDestinationUri;
}
private async saveOntoCopiedSketch(
sketch: Sketch,
newSketchFolderUri: string
): Promise<void> {
const widgets = this.applicationShell.widgets;
const snapshots = new Map<string, Saveable.Snapshot>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
if (!uri) {
continue;
}
const uriString = uri.toString();
let relativePath: string;
if (
uriString.includes(sketch.uri) &&
saveable &&
saveable.createSnapshot
) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (sketch.mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketch.uri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchFolderUri + path);
try {
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
})
);
}
}
interface InvalidSketchFolderDialogContent {
@ -317,3 +274,48 @@ export namespace SaveAsSketch {
};
}
}
export async function saveOntoCopiedSketch(
sketch: Sketch,
newSketchFolderUri: string,
shell: ApplicationShell,
editorManager: EditorManager
): Promise<void> {
const widgets = shell.widgets;
const snapshots = new Map<string, Saveable.Snapshot>();
for (const widget of widgets) {
const saveable = Saveable.getDirty(widget);
const uri = NavigatableWidget.getUri(widget);
if (!uri) {
continue;
}
const uriString = uri.toString();
let relativePath: string;
if (uriString.includes(sketch.uri) && saveable && saveable.createSnapshot) {
// The main file will change its name during the copy process
// We need to store the new name in the map
if (sketch.mainFileUri === uriString) {
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
relativePath = '/' + lastPart;
} else {
relativePath = uri.toString().substring(sketch.uri.length);
}
snapshots.set(relativePath, saveable.createSnapshot());
}
}
await Promise.all(
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
const widgetUri = new URI(newSketchFolderUri + path);
try {
const widget = await editorManager.getOrCreateByUri(widgetUri);
const saveable = Saveable.get(widget);
if (saveable && saveable.applySnapshot) {
saveable.applySnapshot(snapshot);
await saveable.save();
}
} catch (e) {
console.error(e);
}
})
);
}

View File

@ -2,7 +2,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import { fetch } from 'cross-fetch';
import { SketchesService } from '../../common/protocol';
import { unit8ArrayToString } from '../../common/utils';
import { uint8ArrayToString } from '../../common/utils';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
@ -10,11 +10,11 @@ import * as createPaths from './create-paths';
import { posix } from './create-paths';
import { Create, CreateError } from './typings';
export interface ResponseResultProvider {
interface ResponseResultProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(response: Response): Promise<any>;
}
export namespace ResponseResultProvider {
namespace ResponseResultProvider {
export const NOOP: ResponseResultProvider = async () => undefined;
export const TEXT: ResponseResultProvider = (response) => response.text();
export const JSON: ResponseResultProvider = (response) => response.json();
@ -288,10 +288,9 @@ export class CreateApi {
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 : unit8ArrayToString(content)
typeof content === 'string' ? content : uint8ArrayToString(content)
)
.split(/\r?\n/)
.reduce((prev, curr) => {
@ -355,7 +354,7 @@ export class CreateApi {
const headers = await this.headers();
let data: string =
typeof content === 'string' ? content : unit8ArrayToString(content);
typeof content === 'string' ? content : uint8ArrayToString(content);
data = await this.toggleSecretsInclude(posixPath, data, 'remove');
const payload = { data: btoa(data) };

View File

@ -8,6 +8,9 @@ import { AuthenticationSession } from '../../node/auth/types';
import { ArduinoPreferences } from '../arduino-preferences';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
import { CreateUri } from './create-uri';
export type CloudSketchState = 'push' | 'pull';
@injectable()
export class CreateFeatures implements FrontendApplicationContribution {
@ -18,13 +21,22 @@ export class CreateFeatures implements FrontendApplicationContribution {
@inject(LocalCacheFsProvider)
private readonly localCacheFsProvider: LocalCacheFsProvider;
/**
* The keys are the Create URI of the sketches.
*/
private readonly _cloudSketchStates = new Map<string, CloudSketchState>();
private readonly onDidChangeSessionEmitter = new Emitter<
AuthenticationSession | undefined
>();
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
private readonly onDidChangeCloudSketchStateEmitter = new Emitter<{
uri: URI;
state: CloudSketchState | undefined;
}>();
private readonly toDispose = new DisposableCollection(
this.onDidChangeSessionEmitter,
this.onDidChangeEnabledEmitter
this.onDidChangeEnabledEmitter,
this.onDidChangeCloudSketchStateEmitter
);
private _enabled: boolean;
private _session: AuthenticationSession | undefined;
@ -64,14 +76,46 @@ export class CreateFeatures implements FrontendApplicationContribution {
return this.onDidChangeEnabledEmitter.event;
}
get enabled(): boolean {
return this._enabled;
get onDidChangeCloudSketchState(): Event<{
uri: URI;
state: CloudSketchState | undefined;
}> {
return this.onDidChangeCloudSketchStateEmitter.event;
}
get session(): AuthenticationSession | undefined {
return this._session;
}
get enabled(): boolean {
return this._enabled;
}
cloudSketchState(uri: URI): CloudSketchState | undefined {
return this._cloudSketchStates.get(uri.toString());
}
setCloudSketchState(uri: URI, state: CloudSketchState | undefined): void {
if (uri.scheme !== CreateUri.scheme) {
throw new Error(
`Expected a URI with '${uri.scheme}' scheme. Got: ${uri.toString()}`
);
}
const key = uri.toString();
if (!state) {
if (!this._cloudSketchStates.delete(key)) {
console.warn(
`Could not reset the cloud sketch state of ${key}. No state existed for the the cloud sketch.`
);
} else {
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state: undefined });
}
} else {
this._cloudSketchStates.set(key, state);
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state });
}
}
/**
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
* Returns with `undefined` if `dataDirUri` is `undefined`.
@ -83,7 +127,10 @@ export class CreateFeatures implements FrontendApplicationContribution {
);
return undefined;
}
return dataDirUri.isEqualOrParent(new URI(sketch.uri));
return dataDirUri
.resolve('RemoteSketchbook')
.resolve('ArduinoCloud')
.isEqualOrParent(new URI(sketch.uri));
}
cloudUri(sketch: Sketch): URI | undefined {

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.355 3.85509L2.85504 14.3551C2.76026 14.448 2.63281 14.5001 2.50006 14.5001C2.36731 14.5001 2.23986 14.448 2.14508 14.3551C2.0514 14.2607 1.99882 14.1331 1.99882 14.0001C1.99882 13.8671 2.0514 13.7395 2.14508 13.6451L3.82508 11.9651C3.24351 11.8742 2.70645 11.5991 2.29291 11.1802C1.87936 10.7613 1.61116 10.2208 1.52775 9.63811C1.44434 9.05543 1.55012 8.46136 1.82955 7.94328C2.10897 7.4252 2.54728 7.01047 3.08 6.76009C3.20492 6.18251 3.47405 5.64596 3.86232 5.20047C4.25058 4.75498 4.74532 4.41505 5.30042 4.21239C5.85552 4.00972 6.45289 3.9509 7.03686 4.04143C7.62082 4.13196 8.17236 4.36887 8.64004 4.73009C9.01346 4.56809 9.41786 4.48995 9.82475 4.50117C10.2316 4.51239 10.6311 4.6127 10.995 4.79503L12.645 3.14509C12.7392 3.05094 12.8669 2.99805 13 2.99805C13.1332 2.99805 13.2609 3.05094 13.355 3.14509C13.4492 3.23924 13.5021 3.36694 13.5021 3.50009C13.5021 3.63324 13.4492 3.76094 13.355 3.85509V3.85509Z" fill="#7F8C8D"/>
<path d="M14.5 9.25047C14.4987 9.97942 14.2086 10.6782 13.6931 11.1936C13.1777 11.709 12.479 11.9992 11.75 12.0005H6.70996L12.355 6.35547C12.38 6.43042 12.4 6.50547 12.4201 6.58044C13.0153 6.72902 13.5436 7.07272 13.9206 7.55669C14.2976 8.04066 14.5016 8.63699 14.5 9.25047V9.25047Z" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 9.24997C14.4987 9.97893 14.2086 10.6777 13.6932 11.1931C13.1777 11.7086 12.479 11.9987 11.75 12H4.25003C3.62476 11.9998 3.01822 11.7866 2.53034 11.3955C2.04247 11.0045 1.70238 10.4589 1.56612 9.84864C1.42986 9.2384 1.50556 8.59995 1.78074 8.0385C2.05593 7.47705 2.51418 7.0261 3.07998 6.75997C3.2049 6.18239 3.47404 5.64584 3.8623 5.20035C4.25056 4.75486 4.74531 4.41494 5.3004 4.21227C5.8555 4.0096 6.45288 3.95078 7.03684 4.04131C7.62081 4.13184 8.17234 4.36875 8.64003 4.72997C8.99025 4.57772 9.36814 4.49942 9.75003 4.49997C10.3635 4.49838 10.9598 4.70238 11.4438 5.07939C11.9278 5.45641 12.2715 5.9847 12.4201 6.57993C13.0153 6.7285 13.5436 7.07221 13.9206 7.55618C14.2976 8.04015 14.5016 8.63649 14.5 9.24997Z" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.42 6.58044C12.4 6.50549 12.38 6.43042 12.355 6.35547L11.525 7.18555C11.5575 7.27223 11.6136 7.34811 11.6869 7.40464C11.7603 7.46117 11.8479 7.4961 11.94 7.50549C12.3852 7.55476 12.7947 7.77259 13.0843 8.11428C13.374 8.45597 13.5218 8.89557 13.4975 9.34284C13.4732 9.7901 13.2785 10.2111 12.9536 10.5194C12.6286 10.8276 12.1979 10.9998 11.75 11.0005H7.70996L6.70996 12.0005H11.75C12.421 12.0001 13.0688 11.7545 13.5714 11.3099C14.074 10.8653 14.3969 10.2524 14.4792 9.58644C14.5615 8.92048 14.3977 8.24739 14.0184 7.69379C13.6392 7.14019 13.0708 6.74425 12.42 6.58044V6.58044Z" fill="#7F8C8D"/>
<path d="M13.355 3.14532C13.2606 3.05161 13.133 2.99902 13 2.99902C12.867 2.99902 12.7394 3.05161 12.6451 3.14532L10.995 4.79524C10.6311 4.61291 10.2316 4.5126 9.82472 4.50139C9.41783 4.49017 9.01343 4.56832 8.64002 4.73032C8.17233 4.3691 7.6208 4.13219 7.03684 4.04166C6.45287 3.95114 5.85549 4.00995 5.3004 4.21262C4.7453 4.41529 4.25056 4.75521 3.86229 5.2007C3.47403 5.64619 3.2049 6.18274 3.07997 6.76033C2.54726 7.01071 2.10896 7.42543 1.82954 7.9435C1.55013 8.46157 1.44434 9.05564 1.52775 9.63832C1.61115 10.221 1.87935 10.7615 2.29288 11.1804C2.70641 11.5993 3.24346 11.8744 3.82502 11.9653L2.14502 13.6453C2.05133 13.7397 1.99876 13.8673 1.99876 14.0003C1.99876 14.1333 2.05133 14.2609 2.14502 14.3553C2.23979 14.4482 2.36725 14.5003 2.5 14.5003C2.63275 14.5003 2.7602 14.4482 2.85498 14.3553L13.355 3.85528C13.4487 3.7609 13.5012 3.6333 13.5013 3.50031C13.5013 3.36732 13.4487 3.23972 13.355 3.14532V3.14532ZM4.79006 11.0003H4.25002C3.8356 11.0005 3.43458 10.8535 3.11841 10.5856C2.80224 10.3177 2.59145 9.94623 2.52362 9.5374C2.45578 9.12857 2.53529 8.70893 2.74799 8.35326C2.96069 7.99758 3.29275 7.72898 3.68502 7.59529C3.77434 7.56478 3.85319 7.50962 3.91248 7.43617C3.97176 7.36272 4.00904 7.274 4.02002 7.18025C4.09848 6.57783 4.39334 6.0245 4.84963 5.62341C5.30592 5.22233 5.89251 5.00087 6.50002 5.00032C7.1425 4.99652 7.76054 5.24628 8.21999 5.69539C8.30086 5.77275 8.40511 5.8211 8.5164 5.83285C8.6277 5.8446 8.73974 5.8191 8.83499 5.76033C9.10926 5.58886 9.42655 5.4987 9.75002 5.50032C9.9105 5.50127 10.0702 5.5231 10.225 5.56526L4.79006 11.0003Z" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.75 12H4.25C3.62484 11.9999 3.01838 11.7868 2.53053 11.3959C2.04269 11.0049 1.70257 10.4595 1.56622 9.84934C1.42987 9.23923 1.50542 8.60087 1.78043 8.03945C2.05543 7.47802 2.51348 7.02702 3.0791 6.76076C3.24864 5.97929 3.68041 5.27932 4.3027 4.77712C4.92499 4.27492 5.70035 4.00071 6.5 4.00002C7.27505 3.99715 8.02853 4.25513 8.63916 4.73244C9.00591 4.57154 9.40329 4.49243 9.8037 4.5006C10.2041 4.50877 10.5979 4.60403 10.9578 4.77976C11.3177 4.9555 11.635 5.20748 11.8876 5.51822C12.1403 5.82895 12.3223 6.19097 12.4209 6.57912C13.0715 6.74324 13.6398 7.13939 14.0188 7.69309C14.3979 8.24679 14.5616 8.91989 14.4792 9.58582C14.3967 10.2518 14.0739 10.8646 13.5713 11.3092C13.0687 11.7538 12.421 11.9995 11.75 12ZM6.5 5.00002C5.89213 5.00017 5.30514 5.22179 4.84885 5.62344C4.39257 6.02508 4.09826 6.57921 4.021 7.18215C4.0093 7.27546 3.97153 7.36357 3.91202 7.43638C3.85252 7.50918 3.77369 7.56374 3.68458 7.59377C3.29236 7.72769 2.9604 7.99647 2.7478 8.35224C2.5352 8.70801 2.45576 9.12768 2.52363 9.53654C2.5915 9.9454 2.80227 10.3169 3.11841 10.5849C3.43455 10.8529 3.83555 11 4.25 11H11.75C12.198 10.9996 12.6289 10.8275 12.9539 10.5191C13.279 10.2108 13.4735 9.7896 13.4975 9.34221C13.5215 8.89481 13.3732 8.45522 13.083 8.11384C12.7929 7.77246 12.3829 7.55524 11.9375 7.50686C11.8238 7.4948 11.7176 7.44411 11.6368 7.36325C11.5559 7.28238 11.5052 7.17624 11.4932 7.06252C11.4474 6.63255 11.2439 6.2348 10.9219 5.94619C10.6 5.65758 10.1824 5.49861 9.75 5.50002C9.42739 5.49791 9.11079 5.58731 8.83692 5.75783C8.74185 5.81746 8.62955 5.84352 8.51794 5.83184C8.40633 5.82015 8.30185 5.77141 8.22119 5.69338C7.76046 5.24569 7.14241 4.99672 6.5 5.00002V5.00002Z" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -15,10 +15,10 @@
.p-TabBar-tabIcon.cloud-sketchbook-tree-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./cloud-sketchbook-tree-icon.svg);
-webkit-mask: url(../icons/arduino-cloud.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
width: var(--theia-icon-size);
width: 19px !important;
height: var(--theia-icon-size);
-webkit-mask-size: 100%;
}
@ -26,7 +26,7 @@
.p-mod-current
.cloud-sketchbook-tree-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./cloud-sketchbook-tree-icon-filled.svg);
-webkit-mask: url(../icons/arduino-cloud-filled.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 100%;
@ -99,26 +99,7 @@
color: var(--theia-textLink-foreground);
}
.pull-sketch-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./pull-sketch-icon.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
}
.push-sketch-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./push-sketch-icon.svg);
-webkit-mask-position: center;
-webkit-mask-repeat: 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-private-sidebar-icon-size);
height: var(--theia-private-sidebar-icon-size);
border-radius: 50%;
@ -199,3 +180,12 @@
.arduino-share-sketch-dialog .sketch-link-embed textarea {
width: 100%;
}
.actions.item.flex-line .fa,
.theia-file-icons-js.file-icon .fa {
font-size: var(--theia-icon-size);
}
.theia-file-icons-js.file-icon.not-in-sync-offline .fa {
color: var(--theia-activityBar-inactiveForeground);
}

View File

@ -11,9 +11,9 @@
@font-face {
font-family: 'FontAwesome';
src:
url('fonts/FontAwesome.ttf?2jhpmq') format('truetype'),
url('fonts/FontAwesome.woff?2jhpmq') format('woff'),
url('fonts/FontAwesome.svg?2jhpmq#FontAwesome') format('svg');
url('fonts/FontAwesome.ttf?h959em') format('truetype'),
url('fonts/FontAwesome.woff?h959em') format('woff'),
url('fonts/FontAwesome.svg?h959em#FontAwesome') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@ -679,3 +679,21 @@
.fa-microchip:before {
content: "\f2db";
}
.fa-arduino-cloud-download:before {
content: "\e910";
}
.fa-arduino-cloud-upload:before {
content: "\e914";
}
.fa-arduino-cloud:before {
content: "\e915";
}
.fa-arduino-cloud-filled:before {
content: "\e912";
}
.fa-arduino-cloud-offline:before {
content: "\e913";
}
.fa-arduino-cloud-filled-offline:before {
content: "\e911";
}

View File

@ -23,6 +23,12 @@
<glyph unicode="&#xe90d;" glyph-name="arduino-monitor" horiz-adv-x="1536" d="M651.891 59.977c-92.835 0-179.095 28.493-250.5 77.197l-129.659-129.658c-22.494-22.496-58.964-22.496-81.458 0s-22.494 58.963 0 81.459l124.954 124.954c-67.75 78.157-108.777 180.090-108.777 291.489 0 245.759 199.68 445.439 445.44 445.439s445.44-199.679 445.44-445.439c0-245.761-199.68-445.441-445.44-445.441zM651.891 797.257c-161.28 0-291.84-130.559-291.84-291.839s130.56-291.841 291.84-291.841c160.512 0 291.84 130.561 291.84 291.841 0 160.511-130.56 291.839-291.84 291.839zM1149.562 472.766c0-35.423 28.717-64.138 64.141-64.138s64.134 28.716 64.134 64.138c0 35.423-28.71 64.139-64.134 64.139s-64.141-28.716-64.141-64.139zM64.064 408.62c-35.382 0-64.064 28.682-64.064 64.063s28.682 64.064 64.064 64.064c35.381 0 64.064-28.682 64.064-64.064s-28.683-64.063-64.064-64.063zM1458.707 408.628c-35.418 0-64.134 28.716-64.134 64.138s28.717 64.139 64.134 64.139c35.424 0 64.141-28.716 64.141-64.139s-28.717-64.138-64.141-64.138zM652.659 424.010c-44.961 0-81.408 36.447-81.408 81.407s36.447 81.408 81.408 81.408c44.96 0 81.408-36.447 81.408-81.408s-36.448-81.407-81.408-81.407z" />
<glyph unicode="&#xe90e;" glyph-name="arduino-sketch-tabs-menu" d="M511.998 347.425c50.495 0 91.432 40.936 91.432 91.432s-40.936 91.432-91.432 91.432c-50.495 0-91.432-40.936-91.432-91.432s40.936-91.432 91.432-91.432zM923.433 347.425c50.494 0 91.432 40.936 91.432 91.432s-40.937 91.432-91.432 91.432c-50.494 0-91.432-40.936-91.432-91.432s40.937-91.432 91.432-91.432zM100.565 347.425c50.495 0 91.432 40.936 91.432 91.432s-40.936 91.432-91.432 91.432c-50.495 0-91.432-40.936-91.432-91.432s40.936-91.432 91.432-91.432z" />
<glyph unicode="&#xe90f;" glyph-name="arduino-plotter" horiz-adv-x="862" d="M323.368-19.351c-20.263 0-39 11.42-48.21 29.788l-146.789 293.581h-74.474c-29.789 0-53.895 24.107-53.895 53.895s24.105 53.895 53.895 53.895h107.789c20.421 0 39.053-11.528 48.21-29.788l96.527-193.056 180.263 720.949c5.842 23.579 26.737 40.263 51 40.842 23.947 1.579 45.893-15.158 52.894-38.421l150.162-500.526h67.681c29.788 0 53.895-24.107 53.895-53.895s-24.107-53.895-53.895-53.895h-107.789c-23.789 0-44.787 15.629-51.631 38.422l-105.316 351.104-168.052-672.053c-5.474-21.897-23.948-38.055-46.368-40.529-2-0.21-3.947-0.313-5.895-0.313h-0.001z" />
<glyph unicode="&#xe910;" glyph-name="arduino-cloud-download" d="M684.256 156.891l-146.286-146.286c-6.932-6.802-16.255-10.606-25.964-10.606s-19.032 3.803-25.964 10.606l-146.286 146.286c-3.41 3.41-6.115 7.458-7.96 11.913s-2.796 9.23-2.796 14.052c-0.001 9.738 3.868 19.079 10.754 25.965s16.226 10.756 25.964 10.756c4.822 0 9.597-0.949 14.052-2.795s8.504-4.549 11.914-7.959l83.749-84.107v423.856c0 9.699 3.853 19.002 10.712 25.86s16.16 10.712 25.86 10.712c9.699 0 19.001-3.853 25.86-10.712s10.712-16.16 10.712-25.86v-423.856l83.749 84.107c6.886 6.886 16.227 10.756 25.966 10.756s19.079-3.869 25.966-10.756c6.886-6.886 10.755-16.227 10.755-25.966s-3.869-19.079-10.755-25.966zM786.286 292.572h-128c-9.699 0-19.001 3.852-25.86 10.711s-10.712 16.161-10.712 25.86c0 9.699 3.853 19.001 10.712 25.86s16.16 10.712 25.86 10.712h128c32.768 0.031 64.285 12.618 88.057 35.172 23.779 22.554 38.005 53.361 39.76 86.085s-9.092 64.877-30.318 89.846c-21.219 24.97-51.207 40.858-83.785 44.396-8.316 0.882-16.084 4.59-21.994 10.505-5.917 5.914-9.626 13.678-10.503 21.996-3.35 31.449-18.235 60.542-41.784 81.652-23.551 21.11-54.092 32.737-85.719 32.634-23.597 0.154-46.754-6.384-66.785-18.857-6.953-4.363-15.168-6.269-23.332-5.414s-15.805 4.42-21.704 10.128c-33.699 32.745-78.905 50.956-125.893 50.714-44.461-0.011-87.395-16.221-120.77-45.598s-54.9-69.908-60.551-114.009c-0.856-6.825-3.618-13.27-7.971-18.595s-10.119-9.315-16.636-11.512c-28.688-9.795-52.969-29.455-68.519-55.477s-21.361-56.718-16.396-86.623c4.964-29.905 20.381-57.078 43.504-76.68s52.454-30.362 82.768-30.363h128c9.699 0 19.002-3.853 25.86-10.712s10.711-16.16 10.711-25.86c0-9.699-3.853-19.002-10.711-25.86s-16.161-10.711-25.86-10.711h-128c-45.726 0.010-90.084 15.596-125.767 44.191s-60.559 68.491-70.532 113.116c-9.973 44.625-4.447 91.317 15.667 132.381s53.618 74.052 94.989 93.527c12.401 57.159 43.982 108.357 89.498 145.089s102.228 56.789 160.717 56.839c56.689 0.21 111.801-18.659 156.464-53.571 26.825 11.769 55.891 17.556 85.178 16.958s58.092-7.565 84.415-20.419c26.323-12.854 49.532-31.284 68.007-54.012 18.483-22.728 31.795-49.208 39.007-77.598 47.587-12.004 89.154-40.98 116.875-81.479 27.728-40.499 39.702-89.732 33.675-138.44-6.034-48.708-29.645-93.536-66.406-126.054s-84.136-50.488-133.215-50.527z" />
<glyph unicode="&#xe911;" glyph-name="arduino-cloud-filled-offline" d="M854.72 704.131l-671.997-672.001c-6.066-5.946-14.223-9.28-22.719-9.28s-16.653 3.334-22.719 9.28c-5.996 6.042-9.361 14.208-9.361 22.72s3.365 16.678 9.361 22.72l107.52 107.52c-37.22 5.818-71.592 23.424-98.059 50.234s-43.632 61.402-48.97 98.694c-5.338 37.292 1.432 75.312 19.315 108.469s45.935 59.7 80.029 75.724c7.995 36.965 25.219 71.304 50.068 99.816s56.512 50.267 92.038 63.237c35.526 12.971 73.758 16.735 111.132 10.941s72.672-20.956 102.604-44.074c23.899 10.368 49.78 15.369 75.821 14.651 26.038-0.718 51.606-7.138 74.896-18.807l105.6 105.596c6.029 6.026 14.202 9.411 22.72 9.411 8.525 0 16.698-3.385 22.72-9.411 6.029-6.026 9.414-14.198 9.414-22.72s-3.386-16.694-9.414-22.72v0zM928 358.827c-0.083-46.653-18.65-91.375-51.642-124.36-32.986-32.986-77.702-51.558-124.358-51.642h-322.563l361.283 361.282c1.6-4.797 2.88-9.6 4.166-14.398 38.093-9.509 71.904-31.506 96.032-62.48s37.184-69.139 37.082-108.402v0z" />
<glyph unicode="&#xe912;" glyph-name="arduino-cloud-filled" d="M928 358.859c-0.083-46.653-18.65-91.375-51.635-124.36-32.992-32.992-77.709-51.558-124.365-51.642h-479.998c-40.017 0.013-78.836 13.658-110.060 38.688-31.224 25.024-52.989 59.942-61.71 98.999-8.721 39.055-3.876 79.916 13.736 115.849s46.94 64.794 83.151 81.826c7.995 36.965 25.22 71.304 50.068 99.816s56.513 50.266 92.038 63.237c35.526 12.971 73.759 16.735 111.132 10.941s72.672-20.956 102.604-44.074c22.414 9.744 46.599 14.755 71.040 14.72 39.262 0.102 77.425-12.954 108.401-37.083s52.973-57.94 62.483-96.035c38.093-9.508 71.904-31.506 96.032-62.48s37.184-69.14 37.082-108.403z" />
<glyph unicode="&#xe913;" glyph-name="arduino-cloud-offline" d="M794.88 529.709c-1.28 4.797-2.56 9.601-4.16 14.398l-53.12-53.125c2.080-5.548 5.67-10.404 10.362-14.022 4.698-3.618 10.304-5.853 16.198-6.454 28.493-3.153 54.701-17.094 73.235-38.963 18.541-21.868 28-50.003 26.445-78.628s-14.016-55.569-34.81-75.3c-20.8-19.725-48.365-30.746-77.030-30.79h-258.563l-64-64h322.563c42.944 0.026 84.403 15.744 116.57 44.198s52.832 67.68 58.099 110.301c5.267 42.621-5.216 85.699-29.491 121.13-24.269 35.43-60.646 60.771-102.298 71.254v0zM854.72 749.557c-6.042 5.997-14.208 9.363-22.72 9.363s-16.678-3.366-22.714-9.363l-105.606-105.595c-23.29 11.669-48.858 18.089-74.898 18.806s-51.923-4.284-75.821-14.652c-29.932 23.118-65.23 38.28-102.604 44.074s-75.606 2.029-111.132-10.941c-35.526-12.971-67.19-34.726-92.039-63.237s-42.073-62.851-50.068-99.816c-34.093-16.024-62.145-42.566-80.028-75.723s-24.653-71.177-19.315-108.468c5.338-37.292 22.502-71.884 48.968-98.693s60.837-44.416 98.057-50.234l-107.52-107.52c-5.996-6.042-9.361-14.208-9.361-22.72s3.364-16.678 9.361-22.72c6.065-5.946 14.223-9.28 22.719-9.28s16.653 3.334 22.719 9.28l672.001 672.001c5.997 6.040 9.357 14.207 9.363 22.718 0 8.511-3.366 16.678-9.363 22.719v0zM306.564 246.838h-34.563c-26.523-0.013-52.188 9.395-72.423 26.541s-33.725 40.92-38.067 67.085c-4.342 26.165 0.747 53.022 14.36 75.785s34.865 39.954 59.97 48.51c5.716 1.953 10.763 5.483 14.557 10.184s6.18 10.379 6.883 16.379c5.021 38.555 23.892 73.968 53.095 99.638s66.744 39.843 105.625 39.878c41.119 0.243 80.673-15.741 110.078-44.484 5.176-4.951 11.848-8.045 18.97-8.797s14.294 0.88 20.39 4.641c17.553 10.974 37.86 16.744 58.562 16.641 10.271-0.061 20.492-1.458 30.399-4.156l-347.836-347.843z" />
<glyph unicode="&#xe914;" glyph-name="arduino-cloud-upload" d="M684.258 412.892c-6.932-6.799-16.255-10.607-25.964-10.607s-19.032 3.809-25.964 10.607l-83.751 84.118v-423.867c0-9.699-3.853-19.003-10.711-25.856-6.859-6.861-16.161-10.715-25.86-10.715s-19.001 3.855-25.86 10.715c-6.859 6.853-10.712 16.157-10.712 25.856v423.867l-83.749-84.118c-6.886-6.886-16.227-10.756-25.966-10.756s-19.079 3.869-25.966 10.756c-6.886 6.886-10.755 16.227-10.755 25.966s3.869 19.079 10.755 25.966l146.286 146.286c6.903 6.854 16.236 10.701 25.964 10.701s19.062-3.847 25.964-10.701l146.286-146.286c6.853-6.904 10.7-16.237 10.701-25.965s-3.845-19.062-10.698-25.966zM786.286 256.001h-128c-9.699 0-19.001 3.852-25.86 10.711s-10.712 16.161-10.712 25.86c0 9.699 3.853 19.001 10.712 25.86s16.16 10.712 25.86 10.712h128c32.768 0.031 64.285 12.618 88.057 35.172 23.779 22.554 38.005 53.361 39.76 86.085s-9.092 64.877-30.318 89.846c-21.219 24.97-51.207 40.858-83.785 44.396-8.316 0.882-16.084 4.59-21.994 10.505-5.917 5.914-9.626 13.678-10.503 21.996-3.35 31.449-18.235 60.542-41.784 81.652-23.551 21.11-54.092 32.737-85.719 32.634-23.597 0.154-46.754-6.384-66.785-18.857-6.954-4.362-15.168-6.268-23.331-5.413s-15.805 4.419-21.705 10.127c-33.699 32.745-78.905 50.956-125.893 50.714-44.461-0.011-87.395-16.221-120.77-45.598s-54.9-69.908-60.551-114.009c-0.856-6.825-3.618-13.27-7.971-18.595s-10.119-9.315-16.636-11.512c-28.688-9.795-52.969-29.455-68.519-55.477s-21.361-56.718-16.396-86.623c4.964-29.905 20.381-57.078 43.504-76.68s52.454-30.362 82.768-30.363h128c9.699 0 19.002-3.853 25.86-10.712s10.711-16.16 10.711-25.86c0-9.699-3.853-19.002-10.711-25.86s-16.161-10.711-25.86-10.711h-128c-45.726 0.010-90.084 15.596-125.767 44.191s-60.559 68.491-70.532 113.116c-9.973 44.625-4.447 91.317 15.667 132.381s53.618 74.052 94.989 93.527c12.401 57.159 43.982 108.357 89.498 145.089s102.228 56.789 160.717 56.839c56.689 0.21 111.801-18.659 156.464-53.571 26.825 11.769 55.891 17.556 85.178 16.958s58.092-7.565 84.415-20.419c26.323-12.854 49.532-31.284 68.007-54.012 18.483-22.728 31.795-49.208 39.007-77.598 47.587-12.004 89.154-40.98 116.875-81.479 27.728-40.499 39.702-89.732 33.675-138.44-6.034-48.708-29.645-93.536-66.406-126.054s-84.136-50.488-133.215-50.527z" />
<glyph unicode="&#xe915;" glyph-name="arduino-cloud" d="M752 182.857h-480c-40.010 0.006-78.824 13.645-110.046 38.662-31.222 25.024-52.989 59.93-61.716 98.98-8.726 39.047-3.891 79.902 13.709 115.833s46.915 64.796 83.115 81.836c10.851 50.014 38.484 94.812 78.31 126.953s89.45 49.69 140.627 49.734c49.603 0.184 97.826-16.327 136.906-46.875 23.472 10.298 48.904 15.361 74.531 14.838s50.829-6.62 73.862-17.866c23.034-11.247 43.341-27.374 59.507-47.261 16.173-19.887 27.821-43.056 34.131-67.898 41.638-10.504 78.010-35.857 102.266-71.294 24.262-35.437 34.739-78.515 29.466-121.135-5.28-42.623-25.939-81.842-58.106-110.296s-73.619-44.179-116.563-44.211zM416 630.856c-38.904-0.010-76.471-14.193-105.674-39.899s-48.038-61.169-52.982-99.757c-0.749-5.972-3.166-11.611-6.975-16.271s-8.853-8.151-14.556-10.073c-25.102-8.571-46.348-25.773-59.954-48.542s-18.691-49.628-14.347-75.795c4.344-26.167 17.833-49.943 38.066-67.095s45.897-26.566 72.422-26.566h480c28.672 0.026 56.25 11.040 77.050 30.778 20.806 19.731 33.254 46.688 34.79 75.321s-7.955 56.767-26.528 78.616c-18.566 21.848-44.806 35.75-73.312 38.847-7.277 0.772-14.074 4.016-19.245 9.191-5.178 5.176-8.422 11.969-9.19 19.247-2.931 27.518-15.955 52.974-36.563 71.445-20.602 18.471-47.328 28.645-75.002 28.555-20.647 0.135-40.909-5.587-58.437-16.5-6.084-3.816-13.272-5.484-20.415-4.737s-13.83 3.868-18.992 8.861c-29.487 28.652-69.042 44.586-110.156 44.375v0z" />
<glyph unicode="&#xf001;" glyph-name="music" horiz-adv-x="878" d="M877.714 822.857v-640c0-80.571-120.571-109.714-182.857-109.714s-182.857 29.143-182.857 109.714 120.571 109.714 182.857 109.714c37.714 0 75.429-6.857 109.714-22.286v306.857l-438.857-135.429v-405.143c0-80.571-120.571-109.714-182.857-109.714s-182.857 29.143-182.857 109.714 120.571 109.714 182.857 109.714c37.714 0 75.429-6.857 109.714-22.286v552.571c0 24 16 45.143 38.857 52.571l475.429 146.286c5.143 1.714 10.286 2.286 16 2.286 30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf002;" glyph-name="search" horiz-adv-x="951" d="M658.286 475.428c0 141.143-114.857 256-256 256s-256-114.857-256-256 114.857-256 256-256 256 114.857 256 256zM950.857 0c0-40-33.143-73.143-73.143-73.143-19.429 0-38.286 8-51.429 21.714l-196 195.429c-66.857-46.286-146.857-70.857-228-70.857-222.286 0-402.286 180-402.286 402.286s180 402.286 402.286 402.286 402.286-180 402.286-402.286c0-81.143-24.571-161.143-70.857-228l196-196c13.143-13.143 21.143-32 21.143-51.429z" />
<glyph unicode="&#xf003;" glyph-name="envelope-o" d="M950.857 91.428v438.857c-12-13.714-25.143-26.286-39.429-37.714-81.714-62.857-164-126.857-243.429-193.143-42.857-36-96-80-155.429-80h-1.143c-59.429 0-112.571 44-155.429 80-79.429 66.286-161.714 130.286-243.429 193.143-14.286 11.429-27.429 24-39.429 37.714v-438.857c0-9.714 8.571-18.286 18.286-18.286h841.143c9.714 0 18.286 8.571 18.286 18.286zM950.857 692c0 14.286 3.429 39.429-18.286 39.429h-841.143c-9.714 0-18.286-8.571-18.286-18.286 0-65.143 32.571-121.714 84-162.286 76.571-60 153.143-120.571 229.143-181.143 30.286-24.571 85.143-77.143 125.143-77.143h1.143c40 0 94.857 52.571 125.143 77.143 76 60.571 152.571 121.143 229.143 181.143 37.143 29.143 84 92.571 84 141.143zM1024 713.143v-621.714c0-50.286-41.143-91.429-91.429-91.429h-841.143c-50.286 0-91.429 41.143-91.429 91.429v621.714c0 50.286 41.143 91.429 91.429 91.429h841.143c50.286 0 91.429-41.143 91.429-91.429z" />

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

@ -77,7 +77,7 @@
.arduino-upload-sketch--toolbar-icon {
-webkit-mask: url(../icons/upload.svg) center no-repeat;
background-color: var(--theia-titleBar-activeBackground);
background-color: var(--theia-titleBar-activeBackground);
}
.toggle-serial-monitor-icon {
@ -114,6 +114,10 @@
z-index: 0;
}
.p-TabBar-toolbar .item > div {
text-align: center;
}
:root {
--theia-private-menubar-height: 40px; /* set the topbar height */
}

View File

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 992 B

View File

@ -3,13 +3,6 @@
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);
}
.p-TabBar-tabIcon.sketchbook-tree-icon {
background-color: var(--theia-foreground);
-webkit-mask: url(./sketchbook-tree-icon.svg);

View File

@ -1,106 +1,324 @@
import {
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus,
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { Disposable } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { Disposable } from '@theia/core/lib/common/disposable';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus,
} from '@theia/core/lib/browser/connection-status-service';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { ArduinoDaemon } from '../../../common/protocol';
import { assertUnreachable } from '../../../common/utils';
import { CreateFeatures } from '../../create/create-features';
import { NotificationCenter } from '../../notification-center';
import { nls } from '@theia/core/lib/common';
import debounce = require('lodash.debounce');
import isOnline = require('is-online');
@injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon;
export class IsOnline implements FrontendApplicationContribution {
private readonly onDidChangeOnlineEmitter = new Emitter<boolean>();
private _online = false;
private stopped = false;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
onStart(): void {
const checkOnline = async () => {
if (!this.stopped) {
try {
const online = await isOnline();
this.setOnline(online);
} finally {
window.setTimeout(() => checkOnline(), 6_000); // 6 seconds poll interval
}
}
};
checkOnline();
}
protected connectedPort: string | undefined;
onStop(): void {
this.stopped = true;
this.onDidChangeOnlineEmitter.dispose();
}
@postConstruct()
protected override async init(): Promise<void> {
this.schedulePing();
try {
this.connectedPort = await this.daemon.tryGetPort();
} catch {}
this.notificationCenter.onDaemonDidStart(
(port) => (this.connectedPort = port)
);
this.notificationCenter.onDaemonDidStop(
() => (this.connectedPort = undefined)
);
const refresh = debounce(() => {
this.updateStatus(!!this.connectedPort);
this.schedulePing();
}, this.options.offlineTimeout - 10);
this.wsConnectionProvider.onIncomingMessageActivity(() => refresh());
get online(): boolean {
return this._online;
}
get onDidChangeOnline(): Event<boolean> {
return this.onDidChangeOnlineEmitter.event;
}
private setOnline(online: boolean) {
const oldOnline = this._online;
this._online = online;
if (!this.stopped && this._online !== oldOnline) {
this.onDidChangeOnlineEmitter.fire(this._online);
}
}
}
@injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
export class DaemonPort implements FrontendApplicationContribution {
@inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon;
private readonly daemon: ArduinoDaemon;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
private readonly notificationCenter: NotificationCenter;
protected connectedPort: string | undefined;
private readonly onPortDidChangeEmitter = new Emitter<string | undefined>();
private _port: string | undefined;
onStart(): void {
this.daemon.tryGetPort().then(
(port) => this.setPort(port),
(reason) =>
console.warn('Could not retrieve the CLI daemon port.', reason)
);
this.notificationCenter.onDaemonDidStart((port) => this.setPort(port));
this.notificationCenter.onDaemonDidStop(() => this.setPort(undefined));
}
onStop(): void {
this.onPortDidChangeEmitter.dispose();
}
get port(): string | undefined {
return this._port;
}
get onDidChangePort(): Event<string | undefined> {
return this.onPortDidChangeEmitter.event;
}
private setPort(port: string | undefined): void {
const oldPort = this._port;
this._port = port;
if (this._port !== oldPort) {
this.onPortDidChangeEmitter.fire(this._port);
}
}
}
@injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(DaemonPort)
private readonly daemonPort: DaemonPort;
@inject(IsOnline)
private readonly isOnline: IsOnline;
@postConstruct()
protected async init(): Promise<void> {
protected override async init(): Promise<void> {
this.schedulePing();
const refresh = debounce(() => {
this.updateStatus(Boolean(this.daemonPort.port) && this.isOnline.online);
this.schedulePing();
}, this.options.offlineTimeout - 10);
this.wsConnectionProvider.onIncomingMessageActivity(() => refresh());
}
protected override async performPingRequest(): Promise<void> {
try {
this.connectedPort = await this.daemon.tryGetPort();
} catch {}
this.notificationCenter.onDaemonDidStart(
(port) => (this.connectedPort = port)
);
this.notificationCenter.onDaemonDidStop(
() => (this.connectedPort = undefined)
);
await this.pingService.ping();
this.updateStatus(this.isOnline.online);
} catch (e) {
this.updateStatus(false);
this.logger.error(e);
}
}
}
const connectionStatusStatusBar = 'connection-status';
const theiaOffline = 'theia-mod-offline';
export type OfflineConnectionStatus =
/**
* There is no websocket connection between the frontend and the backend.
*/
| 'backend'
/**
* The CLI daemon port is not available. Could not establish the gRPC connection between the backend and the CLI.
*/
| 'daemon'
/**
* Cloud not connect to the Internet from the browser.
*/
| 'internet';
@injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
@inject(DaemonPort)
private readonly daemonPort: DaemonPort;
@inject(IsOnline)
private readonly isOnline: IsOnline;
@inject(MessageService)
private readonly messageService: MessageService;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
private readonly offlineStatusDidChangeEmitter = new Emitter<
OfflineConnectionStatus | undefined
>();
private noInternetConnectionNotificationId: string | undefined;
private _offlineStatus: OfflineConnectionStatus | undefined;
get offlineStatus(): OfflineConnectionStatus | undefined {
return this._offlineStatus;
}
get onOfflineStatusDidChange(): Event<OfflineConnectionStatus | undefined> {
return this.offlineStatusDidChangeEmitter.event;
}
protected override onStateChange(state: ConnectionStatus): void {
if (!this.connectedPort && state === ConnectionStatus.ONLINE) {
if (
(!Boolean(this.daemonPort.port) || !this.isOnline.online) &&
state === ConnectionStatus.ONLINE
) {
return;
}
super.onStateChange(state);
}
protected override handleOffline(): void {
this.statusBar.setElement('connection-status', {
const params = {
port: this.daemonPort.port,
online: this.isOnline.online,
};
this._offlineStatus = offlineConnectionStatusType(params);
const { text, tooltip } = offlineMessage(params);
this.statusBar.setElement(connectionStatusStatusBar, {
alignment: StatusBarAlignment.LEFT,
text: this.connectedPort
? nls.localize('theia/core/offline', 'Offline')
: '$(bolt) ' +
nls.localize('theia/core/daemonOffline', 'CLI Daemon Offline'),
tooltip: this.connectedPort
? nls.localize(
'theia/core/cannotConnectBackend',
'Cannot connect to the backend.'
)
: nls.localize(
'theia/core/cannotConnectDaemon',
'Cannot connect to the CLI daemon.'
),
text,
tooltip,
priority: 5000,
});
this.toDisposeOnOnline.push(
Disposable.create(() => this.statusBar.removeElement('connection-status'))
);
document.body.classList.add('theia-mod-offline');
this.toDisposeOnOnline.push(
document.body.classList.add(theiaOffline);
this.toDisposeOnOnline.pushAll([
Disposable.create(() =>
document.body.classList.remove('theia-mod-offline')
)
);
this.statusBar.removeElement(connectionStatusStatusBar)
),
Disposable.create(() => document.body.classList.remove(theiaOffline)),
Disposable.create(() => {
this._offlineStatus = undefined;
this.fireStatusDidChange();
}),
]);
if (!this.isOnline.online) {
const text = nls.localize(
'arduino/connectionStatus/connectionLost',
"Connection lost. Cloud sketch actions and updates won't be available."
);
this.noInternetConnectionNotificationId = this.notificationManager[
'getMessageId'
]({ text, type: MessageType.Warning });
if (this.createFeatures.enabled) {
this.messageService.warn(text);
}
this.toDisposeOnOnline.push(
Disposable.create(() => this.clearNoInternetConnectionNotification())
);
}
this.fireStatusDidChange();
}
private clearNoInternetConnectionNotification(): void {
if (this.noInternetConnectionNotificationId) {
this.notificationManager.clear(this.noInternetConnectionNotificationId);
this.noInternetConnectionNotificationId = undefined;
}
}
private fireStatusDidChange(): void {
if (this.createFeatures.enabled) {
return this.offlineStatusDidChangeEmitter.fire(this._offlineStatus);
}
}
}
interface OfflineMessageParams {
readonly port: string | undefined;
readonly online: boolean;
}
interface OfflineMessage {
readonly text: string;
readonly tooltip: string;
}
/**
* (non-API) exported for testing
*
* The precedence of the offline states are the following:
* - No connection to the Theia backend,
* - CLI daemon is offline, and
* - There is no Internet connection.
*/
export function offlineMessage(params: OfflineMessageParams): OfflineMessage {
const statusType = offlineConnectionStatusType(params);
const text = getOfflineText(statusType);
const tooltip = getOfflineTooltip(statusType);
return { text, tooltip };
}
function offlineConnectionStatusType(
params: OfflineMessageParams
): OfflineConnectionStatus {
const { port, online } = params;
if (port && online) {
return 'backend';
}
if (!port) {
return 'daemon';
}
return 'internet';
}
export const backendOfflineText = nls.localize('theia/core/offline', 'Offline');
export const daemonOfflineText = nls.localize(
'theia/core/daemonOffline',
'CLI Daemon Offline'
);
export const offlineText = nls.localize('theia/core/offlineText', 'Offline');
export const backendOfflineTooltip = nls.localize(
'theia/core/cannotConnectBackend',
'Cannot connect to the backend.'
);
export const daemonOfflineTooltip = nls.localize(
'theia/core/cannotConnectDaemon',
'Cannot connect to the CLI daemon.'
);
export const offlineTooltip = offlineText;
function getOfflineText(statusType: OfflineConnectionStatus): string {
switch (statusType) {
case 'backend':
return backendOfflineText;
case 'daemon':
return '$(bolt) ' + daemonOfflineText;
case 'internet':
return '$(alert) ' + offlineText;
default:
assertUnreachable(statusType);
}
}
function getOfflineTooltip(statusType: OfflineConnectionStatus): string {
switch (statusType) {
case 'backend':
return backendOfflineTooltip;
case 'daemon':
return daemonOfflineTooltip;
case 'internet':
return offlineTooltip;
default:
assertUnreachable(statusType);
}
}

View File

@ -10,17 +10,21 @@ import {
import * as React from '@theia/core/shared/react';
import { accountMenu } from '../../contributions/account';
import { CreateFeatures } from '../../create/create-features';
import { ApplicationConnectionStatusContribution } from './connection-status-service';
@injectable()
export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatue: ApplicationConnectionStatusContribution;
@postConstruct()
protected init(): void {
this.toDispose.push(
this.createFeatures.onDidChangeSession(() => this.update())
);
this.toDispose.pushAll([
this.createFeatures.onDidChangeSession(() => this.update()),
this.connectionStatue.onOfflineStatusDidChange(() => this.update()),
]);
}
protected override onClick(
@ -28,7 +32,7 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
menuPath: MenuPath
): void {
const button = e.currentTarget.getBoundingClientRect();
this.contextMenuRenderer.render({
const options = {
menuPath,
includeAnchorArg: false,
anchor: {
@ -37,7 +41,9 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
// https://github.com/eclipse-theia/theia/discussions/12170
y: button.top,
},
});
showDisabled: true,
};
this.contextMenuRenderer.render(options);
}
protected override render(): React.ReactNode {
@ -55,7 +61,9 @@ export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
}
const arduinoAccount = menu.id === accountMenu.id;
const picture =
arduinoAccount && this.createFeatures.session?.account.picture;
arduinoAccount &&
this.connectionStatue.offlineStatus !== 'internet' &&
this.createFeatures.session?.account.picture;
const className = typeof picture === 'string' ? undefined : menu.iconClass;
return (
<i

View File

@ -1,18 +1,28 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { Widget } from '@theia/core/lib/browser/widgets/widget';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import { isOSX } from '@theia/core/lib/common/os';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigServiceClient } from '../../config/config-service-client';
import { CreateFeatures } from '../../create/create-features';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../sketches-service-client-impl';
@injectable()
export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
@ -22,12 +32,22 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
private readonly applicationShell: ApplicationShell;
@inject(WorkspaceService)
private readonly workspaceService: WorkspaceService;
private _previousRepresentedFilename: string | undefined;
@inject(SketchesServiceClientImpl)
private readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(ConfigServiceClient)
private readonly configServiceClient: ConfigServiceClient;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(EditorManager)
private readonly editorManager: EditorManager;
private readonly applicationName =
FrontendApplicationConfigProvider.get().applicationName;
private readonly toDispose = new DisposableCollection();
private previousRepresentedFilename: string | undefined;
private applicationVersion: string | undefined;
private hasCloudPrefix: boolean | undefined;
@postConstruct()
protected init(): void {
@ -43,6 +63,22 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
);
}
override onStart(app: FrontendApplication): void {
super.onStart(app);
this.toDispose.pushAll([
this.sketchesServiceClient.onCurrentSketchDidChange(() =>
this.maybeSetCloudPrefix()
),
this.configServiceClient.onDidChangeDataDirUri(() =>
this.maybeSetCloudPrefix()
),
]);
}
onStop(): void {
this.toDispose.dispose();
}
protected override handleWidgetChange(widget?: Widget | undefined): void {
if (isOSX) {
this.maybeUpdateRepresentedFilename(widget);
@ -54,7 +90,7 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
protected override updateTitleWidget(widget?: Widget | undefined): void {
let activeEditorShort = '';
const rootName = this.workspaceService.workspace?.name ?? '';
let rootName = this.workspaceService.workspace?.name ?? '';
let appName = `${this.applicationName}${
this.applicationVersion ? ` ${this.applicationVersion}` : ''
}`;
@ -69,6 +105,12 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
activeEditorShort = ` - ${base} `;
}
}
if (this.hasCloudPrefix) {
rootName = `[${nls.localize(
'arduino/title/cloud',
'Cloud'
)}] ${rootName}`;
}
this.windowTitleService.update({ rootName, appName, activeEditorShort });
}
@ -77,10 +119,32 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
const { uri } = widget.editor;
const filename = uri.path.toString();
// Do not necessarily require the current window if not needed. It's a synchronous, blocking call.
if (this._previousRepresentedFilename !== filename) {
if (this.previousRepresentedFilename !== filename) {
const currentWindow = remote.getCurrentWindow();
currentWindow.setRepresentedFilename(uri.path.toString());
this._previousRepresentedFilename = filename;
this.previousRepresentedFilename = filename;
}
}
}
private maybeSetCloudPrefix(): void {
if (typeof this.hasCloudPrefix === 'boolean') {
return;
}
const sketch = this.sketchesServiceClient.tryGetCurrentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
}
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
if (!dataDirUri) {
return;
}
this.hasCloudPrefix = this.createFeatures.isCloud(sketch, dataDirUri);
if (typeof this.hasCloudPrefix === 'boolean') {
const editor =
this.editorManager.activeEditor ?? this.editorManager.currentEditor;
if (editor) {
this.updateTitleWidget(editor);
}
}
}

View File

@ -1,6 +1,5 @@
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import type {
Message,
ProgressMessage,
ProgressUpdate,
} from '@theia/core/lib/common/message-service-protocol';
@ -46,11 +45,4 @@ export class NotificationManager extends TheiaNotificationManager {
}
return Math.min((update.work.done / update.work.total) * 100, 100);
}
/**
* For `public` visibility.
*/
override getMessageId(message: Message): string {
return super.getMessageId(message);
}
}

View File

@ -0,0 +1,88 @@
import { TreeNode } from '@theia/core/lib/browser/tree';
import { Command } from '@theia/core/lib/common/command';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
export namespace CloudSketchbookCommands {
export interface Arg {
model: CloudSketchbookTreeModel;
node: TreeNode;
event?: MouseEvent;
}
export namespace Arg {
export function is(arg: unknown): arg is Arg {
return (
typeof arg === 'object' &&
(<Arg>arg).model !== undefined &&
(<Arg>arg).model instanceof CloudSketchbookTreeModel &&
(<Arg>arg).node !== undefined &&
TreeNode.is((<Arg>arg).node)
);
}
}
export const TOGGLE_CLOUD_SKETCHBOOK = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--disable',
label: 'Show/Hide Cloud Sketchbook',
},
'arduino/cloud/showHideSketchbook'
);
export const PULL_SKETCH = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--pull-sketch',
label: 'Pull Sketch',
iconClass: 'fa fa-arduino-cloud-download',
},
'arduino/cloud/pullSketch'
);
export const PUSH_SKETCH = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--push-sketch',
label: 'Push Sketch',
iconClass: 'fa fa-arduino-cloud-upload',
},
'arduino/cloud/pullSketch'
);
export const PULL_SKETCH__TOOLBAR = {
...PULL_SKETCH,
id: `${PULL_SKETCH.id}-toolbar`,
};
export const PUSH_SKETCH__TOOLBAR = {
...PUSH_SKETCH,
id: `${PUSH_SKETCH.id}-toolbar`,
};
export const OPEN_IN_CLOUD_EDITOR = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--open-in-cloud-editor',
label: 'Open in Cloud Editor',
},
'arduino/cloud/openInCloudEditor'
);
export const OPEN_SKETCHBOOKSYNC_CONTEXT_MENU = Command.toLocalizedCommand(
{
id: 'arduino-sketchbook-sync--open-sketch-context-menu',
label: 'Options...',
iconClass: 'sketchbook-tree__opts',
},
'arduino/cloud/options'
);
export const OPEN_SKETCH_SHARE_DIALOG = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--share-modal',
label: 'Share...',
},
'arduino/cloud/share'
);
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu',
};
}

View File

@ -5,7 +5,7 @@ import {
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { CloudStatus } from './cloud-user-status';
import { CloudStatus } from './cloud-status';
import { nls } from '@theia/core/lib/common/nls';
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
@ -13,6 +13,7 @@ import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
import { CreateNew } from '../sketchbook/create-new';
import { AuthenticationSession } from '../../../node/auth/types';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
@injectable()
export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget<CloudSketchbookTreeWidget> {
@ -20,6 +21,9 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
private readonly authenticationService: AuthenticationClientService;
@inject(CloudSketchbookTreeWidget)
private readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private _session: AuthenticationSession | undefined;
constructor() {
@ -66,6 +70,7 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel
}
authenticationService={this.authenticationService}
connectionStatus={this.connectionStatus}
/>
</>
);

View File

@ -1,145 +1,94 @@
import { inject, injectable } from '@theia/core/shared/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';
} from '@theia/core/lib/browser/context-menu-renderer';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import {
PreferenceScope,
PreferenceService,
} from '@theia/core/lib/browser/preferences/preference-service';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CommandRegistry } from '@theia/core/lib/common/command';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { ShareSketchDialog } from '../../dialogs/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 {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../sketches-service-client-impl';
import { Contribution } from '../../contributions/contribution';
import { nls } from '@theia/core/lib/common/nls';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ArduinoPreferences } from '../../arduino-preferences';
import { MainMenuManager } from '../../../common/main-menu-manager';
import { nls } from '@theia/core/lib/common';
import { ConfigServiceClient } from '../../config/config-service-client';
import { CloudSketchContribution } from '../../contributions/cloud-contribution';
import {
Sketch,
TabBarToolbarRegistry,
} from '../../contributions/contribution';
import { ShareSketchDialog } from '../../dialogs/cloud-share-sketch-dialog';
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
import { CurrentSketch } from '../../sketches-service-client-impl';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
import { CloudSketchbookCommands } from './cloud-sketchbook-commands';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CreateUri } from '../../create/create-uri';
export const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context'];
const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context'];
// `Open Folder`, `Open in New Window`
export const SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP = [
const SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP = [
...SKETCHBOOKSYNC__CONTEXT,
'0_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.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--disable',
label: 'Show/Hide Cloud Sketchbook',
},
'arduino/cloud/showHideSketchbook'
);
export const PULL_SKETCH = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--pull-sketch',
label: 'Pull Sketch',
iconClass: 'pull-sketch-icon',
},
'arduino/cloud/pullSketch'
);
export const PUSH_SKETCH = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--push-sketch',
label: 'Push Sketch',
iconClass: 'push-sketch-icon',
},
'arduino/cloud/pullSketch'
);
export const OPEN_IN_CLOUD_EDITOR = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--open-in-cloud-editor',
label: 'Open in Cloud Editor',
},
'arduino/cloud/openInCloudEditor'
);
export const OPEN_SKETCHBOOKSYNC_CONTEXT_MENU = Command.toLocalizedCommand(
{
id: 'arduino-sketchbook-sync--open-sketch-context-menu',
label: 'Options...',
iconClass: 'sketchbook-tree__opts',
},
'arduino/cloud/options'
);
export const OPEN_SKETCH_SHARE_DIALOG = Command.toLocalizedCommand(
{
id: 'arduino-cloud-sketchbook--share-modal',
label: 'Share...',
},
'arduino/cloud/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;
export class CloudSketchbookContribution extends CloudSketchContribution {
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
private readonly contextMenuRenderer: ContextMenuRenderer;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
private readonly menuRegistry: MenuModelRegistry;
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(CreateApi)
protected readonly createApi: CreateApi;
private readonly windowService: WindowService;
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
private readonly arduinoPreferences: ArduinoPreferences;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
private readonly preferenceService: PreferenceService;
@inject(ConfigServiceClient)
private readonly configServiceClient: ConfigServiceClient;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
private readonly onDidChangeToolbarEmitter = new Emitter<void>();
private readonly toDisposeBeforeNewContextMenu = new DisposableCollection();
private readonly toDisposeOnStop = new DisposableCollection(
this.onDidChangeToolbarEmitter,
this.toDisposeBeforeNewContextMenu
);
private shell: ApplicationShell | undefined;
protected readonly toDisposeBeforeNewContextMenu = new DisposableCollection();
override onStart(app: FrontendApplication): void {
this.shell = app.shell;
this.toDisposeOnStop.pushAll([
this.connectionStatus.onOfflineStatusDidChange((offlineStatus) => {
if (!offlineStatus || offlineStatus === 'internet') {
this.fireToolbarChange();
}
}),
this.createFeatures.onDidChangeSession(() => this.fireToolbarChange()),
this.createFeatures.onDidChangeEnabled(() => this.fireToolbarChange()),
this.createFeatures.onDidChangeCloudSketchState(() =>
this.fireToolbarChange()
),
]);
}
onStop(): void {
this.toDisposeOnStop.dispose();
}
override registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(ArduinoMenus.FILE__ADVANCED_SUBMENU, {
@ -149,6 +98,23 @@ export class CloudSketchbookContribution extends Contribution {
});
}
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.id,
command: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.id,
tooltip: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.label,
priority: -2,
onDidChange: this.onDidChangeToolbar,
});
registry.registerItem({
id: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.id,
command: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.id,
tooltip: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.label,
priority: -1,
onDidChange: this.onDidChangeToolbar,
});
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloudSketchbookCommands.TOGGLE_CLOUD_SKETCHBOOK, {
execute: () => {
@ -158,32 +124,41 @@ export class CloudSketchbookContribution extends Contribution {
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),
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
});
registry.registerCommand(CloudSketchbookCommands.PUSH_SKETCH, {
execute: (arg) => arg.model.sketchbookTree().push(arg.node),
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
this.isCloudSketchDirNodeCommandArg(arg) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
this.isCloudSketchDirNodeCommandArg(arg) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node),
});
registry.registerCommand(CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR, {
execute: () =>
this.executeDelegateWithCurrentSketch(
CloudSketchbookCommands.PUSH_SKETCH.id
),
isEnabled: (arg) => this.isEnabledCloudSketchToolbar(arg),
isVisible: (arg) => this.isVisibleCloudSketchToolbar(arg),
});
registry.registerCommand(CloudSketchbookCommands.PULL_SKETCH__TOOLBAR, {
execute: () =>
this.executeDelegateWithCurrentSketch(
CloudSketchbookCommands.PULL_SKETCH.id
),
isEnabled: (arg) => this.isEnabledCloudSketchToolbar(arg),
isVisible: (arg) => this.isVisibleCloudSketchToolbar(arg),
});
registry.registerCommand(CloudSketchbookCommands.OPEN_IN_CLOUD_EDITOR, {
execute: (arg) => {
this.windowService.openNewWindow(
@ -191,12 +166,8 @@ export class CloudSketchbookContribution extends Contribution {
{ external: true }
);
},
isEnabled: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isVisible: (arg) =>
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
});
registry.registerCommand(CloudSketchbookCommands.OPEN_SKETCH_SHARE_DIALOG, {
@ -207,12 +178,8 @@ export class CloudSketchbookContribution extends Contribution {
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),
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
});
registry.registerCommand(
@ -316,7 +283,118 @@ export class CloudSketchbookContribution extends Contribution {
},
}
);
}
this.registerMenus(this.menuRegistry);
private get currentCloudSketch(): Sketch | undefined {
const currentSketch = this.sketchServiceClient.tryGetCurrentSketch();
// could not load sketch via CLI
if (!CurrentSketch.isValid(currentSketch)) {
return undefined;
}
// cannot determine if the sketch is in the cloud cache folder
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
if (!dataDirUri) {
return undefined;
}
// sketch is not in the cache folder
if (!this.createFeatures.isCloud(currentSketch, dataDirUri)) {
return undefined;
}
return currentSketch;
}
private isVisibleCloudSketchToolbar(arg: unknown): boolean {
// cloud preference is disabled
if (!this.createFeatures.enabled) {
return false;
}
if (!this.currentCloudSketch) {
return false;
}
if (arg instanceof Widget) {
return !!this.shell && this.shell.getWidgets('main').indexOf(arg) !== -1;
}
return false;
}
private isEnabledCloudSketchToolbar(arg: unknown): boolean {
if (!this.isVisibleCloudSketchToolbar(arg)) {
return false;
}
// not logged in
if (!this.createFeatures.session) {
return false;
}
// no Internet connection
if (this.connectionStatus.offlineStatus === 'internet') {
return false;
}
// no pull/push context for the current cloud sketch
const sketch = this.currentCloudSketch;
if (sketch) {
const cloudUri = this.createFeatures.cloudUri(sketch);
if (cloudUri) {
return !this.createFeatures.cloudSketchState(
CreateUri.toUri(cloudUri.path.toString())
);
}
}
return false;
}
private isCloudSketchDirNodeCommandArg(
arg: unknown
): arg is CloudSketchbookCommands.Arg & {
node: CloudSketchbookTree.CloudSketchDirNode;
} {
return (
CloudSketchbookCommands.Arg.is(arg) &&
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
!this.createFeatures.cloudSketchState(arg.node.remoteUri)
);
}
private async commandArgFromCurrentSketch(): Promise<
CloudSketchbookCommands.Arg | undefined
> {
const sketch = this.currentCloudSketch;
if (!sketch) {
return undefined;
}
const model = await this.treeModel();
if (!model) {
return undefined;
}
const cloudUri = this.createFeatures.cloudUri(sketch);
if (!cloudUri) {
return undefined;
}
const posixPath = cloudUri.path.toString();
const node = model.getNode(posixPath);
if (CloudSketchbookTree.CloudSketchDirNode.is(node)) {
return { model, node };
}
return undefined;
}
private async executeDelegateWithCurrentSketch(id: string): Promise<unknown> {
const arg = await this.commandArgFromCurrentSketch();
if (!arg) {
return;
}
if (!this.commandRegistry.getActiveHandler(id, arg)) {
throw new Error(
`No active handler was available for the delegate command: ${id}. Cloud sketch tree node: ${arg.node.id}`
);
}
return this.commandRegistry.executeCommand(id, arg);
}
private fireToolbarChange(): void {
this.onDidChangeToolbarEmitter.fire();
}
private get onDidChangeToolbar(): Event<void> {
return this.onDidChangeToolbarEmitter.event;
}
}

View File

@ -20,6 +20,7 @@ import URI from '@theia/core/lib/common/uri';
import { Create } from '../../create/typings';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
function sketchBaseDir(sketch: Create.Sketch): FileStat {
// extract the sketch path
@ -63,15 +64,22 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
private readonly authenticationService: AuthenticationClientService;
@inject(LocalCacheFsProvider)
private readonly localCacheFsProvider: LocalCacheFsProvider;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
private _localCacheFsProviderReady: Deferred<void> | undefined;
@postConstruct()
protected override init(): void {
super.init();
this.toDispose.push(
this.authenticationService.onSessionDidChange(() => this.updateRoot())
);
this.toDispose.pushAll([
this.authenticationService.onSessionDidChange(() => this.updateRoot()),
this.connectionStatus.onOfflineStatusDidChange((offlineStatus) => {
if (!offlineStatus) {
this.updateRoot();
}
}),
]);
}
override *getNodesByUri(uri: URI): IterableIterator<TreeNode> {

View File

@ -15,6 +15,7 @@ import { CompositeTreeNode } from '@theia/core/lib/browser';
import { shell } from '@theia/core/electron-shared/@electron/remote';
import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget';
import { nls } from '@theia/core/lib/common';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
@injectable()
export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
@ -27,6 +28,9 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
@inject(CloudSketchbookTree)
protected readonly cloudSketchbookTree: CloudSketchbookTree;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
protected override renderTree(model: TreeModel): React.ReactNode {
if (this.shouldShowWelcomeView()) return this.renderViewWelcome();
if (this.shouldShowEmptyView()) return this.renderEmptyView();
@ -91,10 +95,33 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
return classNames;
}
protected override renderIcon(
node: TreeNode,
props: NodeProps
): React.ReactNode {
if (CloudSketchbookTree.CloudSketchDirNode.is(node)) {
const synced = CloudSketchbookTree.CloudSketchTreeNode.isSynced(node);
const offline = this.connectionStatus.offlineStatus === 'internet';
const icon = `fa fa-arduino-cloud${synced ? '-filled' : ''}${
offline ? '-offline' : ''
}`;
return (
<div
className={`theia-file-icons-js file-icon${
!synced && offline ? ' not-in-sync-offline' : ''
}`}
>
<div className={icon} />
</div>
);
}
return super.renderIcon(node, props);
}
protected override renderInlineCommands(node: any): React.ReactNode {
if (CloudSketchbookTree.CloudSketchDirNode.is(node) && node.commands) {
return Array.from(new Set(node.commands)).map((command) =>
this.renderInlineCommand(command.id, node, {
this.renderInlineCommand(command, node, {
username: this.authenticationService.session?.account?.label,
})
);

View File

@ -22,15 +22,22 @@ import {
LocalCacheFsProvider,
LocalCacheUri,
} from '../../local-cache/local-cache-fs-provider';
import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
import { CloudSketchbookCommands } from './cloud-sketchbook-commands';
import { DoNotAskAgainConfirmDialog } from '../../dialogs/do-not-ask-again-dialog';
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
import { firstToUpperCase } from '../../../common/utils';
import { assertUnreachable } from '../../../common/utils';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
import { posix, splitSketchPath } from '../../create/create-paths';
import { Create } from '../../create/typings';
import { nls } from '@theia/core/lib/common';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
import { ExecuteWithProgress } from '../../../common/protocol/progressible';
import {
pullingSketch,
pushingSketch,
} from '../../contributions/cloud-contribution';
import { CloudSketchState, CreateFeatures } from '../../create/create-features';
const MESSAGE_TIMEOUT = 5 * 1000;
const deepmerge = require('deepmerge').default;
@ -54,6 +61,19 @@ export class CloudSketchbookTree extends SketchbookTree {
@inject(CreateApi)
private readonly createApi: CreateApi;
@inject(ApplicationConnectionStatusContribution)
private readonly connectionStatus: ApplicationConnectionStatusContribution;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
protected override init(): void {
this.toDispose.push(
this.connectionStatus.onOfflineStatusDidChange(() => this.refresh())
);
super.init();
}
async pushPublicWarn(
node: CloudSketchbookTree.CloudSketchDirNode
): Promise<boolean> {
@ -84,7 +104,7 @@ export class CloudSketchbookTree extends SketchbookTree {
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
async pull(arg: any): Promise<void> {
async pull(arg: any, noProgress = false): Promise<void> {
const {
// model,
node,
@ -118,32 +138,45 @@ export class CloudSketchbookTree extends SketchbookTree {
return;
}
}
return this.runWithState(node, 'pulling', async (node) => {
const commandsCopy = node.commands;
node.commands = [];
const localUri = await this.fileService.toUnderlyingResource(
LocalCacheUri.root.resolve(node.remoteUri.path)
);
await this.sync(node.remoteUri, localUri);
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
node.commands = commandsCopy;
this.messageService.info(
nls.localize(
'arduino/cloud/donePulling',
'Done pulling {0}.',
node.fileStat.name
),
{
timeout: MESSAGE_TIMEOUT,
}
);
});
return this.runWithState(
node,
'pull',
async (node) => {
await this.pullNode(node);
},
noProgress
);
}
async push(node: CloudSketchbookTree.CloudSketchDirNode): Promise<void> {
private async pullNode(node: CloudSketchbookTree.CloudSketchDirNode) {
const commandsCopy = node.commands;
node.commands = [];
const localUri = await this.fileService.toUnderlyingResource(
LocalCacheUri.root.resolve(node.remoteUri.path)
);
await this.sync(node.remoteUri, localUri);
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
node.commands = commandsCopy;
this.messageService.info(
nls.localize(
'arduino/cloud/donePulling',
"Done pulling '{0}'.",
node.fileStat.name
),
{
timeout: MESSAGE_TIMEOUT,
}
);
}
async push(
node: CloudSketchbookTree.CloudSketchDirNode,
noProgress = false,
ignorePushWarnings = false
): Promise<void> {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error(
nls.localize(
@ -158,7 +191,8 @@ export class CloudSketchbookTree extends SketchbookTree {
return;
}
const warn = this.arduinoPreferences['arduino.cloud.push.warn'];
const warn =
!ignorePushWarnings && this.arduinoPreferences['arduino.cloud.push.warn'];
if (warn) {
const ok = await new DoNotAskAgainConfirmDialog({
@ -178,37 +212,46 @@ export class CloudSketchbookTree extends SketchbookTree {
return;
}
}
return this.runWithState(node, 'pushing', async (node) => {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error(
nls.localize(
'arduino/cloud/pullFirst',
'You have to pull first to be able to push to the Cloud.'
)
);
}
const commandsCopy = node.commands;
node.commands = [];
return this.runWithState(
node,
'push',
async (node) => {
await this.pushNode(node);
},
noProgress
);
}
const localUri = await this.fileService.toUnderlyingResource(
LocalCacheUri.root.resolve(node.remoteUri.path)
);
await this.sync(localUri, node.remoteUri);
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
node.commands = commandsCopy;
this.messageService.info(
private async pushNode(node: CloudSketchbookTree.CloudSketchDirNode) {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error(
nls.localize(
'arduino/cloud/donePushing',
'Done pushing {0}.',
node.fileStat.name
),
{
timeout: MESSAGE_TIMEOUT,
}
'arduino/cloud/pullFirst',
'You have to pull first to be able to push to the Cloud.'
)
);
});
}
const commandsCopy = node.commands;
node.commands = [];
const localUri = await this.fileService.toUnderlyingResource(
LocalCacheUri.root.resolve(node.remoteUri.path)
);
await this.sync(localUri, node.remoteUri);
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
node.commands = commandsCopy;
this.messageService.info(
nls.localize(
'arduino/cloud/donePushing',
"Done pushing '{0}'.",
node.fileStat.name
),
{
timeout: MESSAGE_TIMEOUT,
}
);
}
private async recursiveURIs(uri: URI): Promise<URI[]> {
@ -310,31 +353,37 @@ export class CloudSketchbookTree extends SketchbookTree {
private async runWithState<T>(
node: CloudSketchbookTree.CloudSketchDirNode & Partial<DecoratedTreeNode>,
state: CloudSketchbookTree.CloudSketchDirNode.State,
task: (node: CloudSketchbookTree.CloudSketchDirNode) => MaybePromise<T>
state: CloudSketchState,
task: (node: CloudSketchbookTree.CloudSketchDirNode) => MaybePromise<T>,
noProgress = false
): Promise<T> {
const decoration: WidgetDecoration.TailDecoration = {
data: `${firstToUpperCase(state)}...`,
fontData: {
color: 'var(--theia-list-highlightForeground)',
},
};
this.createFeatures.setCloudSketchState(node.remoteUri, state);
try {
node.state = state;
this.mergeDecoration(node, { tailDecorations: [decoration] });
const result = await (noProgress
? task(node)
: ExecuteWithProgress.withProgress(
this.taskMessage(state, node.uri.path.name),
this.messageService,
async (progress) => {
progress.report({ work: { done: 0, total: NaN } });
return task(node);
}
));
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);
this.createFeatures.setCloudSketchState(node.remoteUri, undefined);
}
}
private taskMessage(state: CloudSketchState, input: string): string {
switch (state) {
case 'pull':
return pullingSketch(input);
case 'push':
return pushingSketch(input);
default:
assertUnreachable(state);
}
}
@ -501,7 +550,7 @@ export class CloudSketchbookTree extends SketchbookTree {
};
}
protected readonly notInSyncDecoration: WidgetDecoration.Data = {
protected readonly notInSyncOfflineDecoration: WidgetDecoration.Data = {
fontData: {
color: 'var(--theia-activityBar-inactiveForeground)',
},
@ -522,11 +571,15 @@ export class CloudSketchbookTree extends SketchbookTree {
node.fileStat.resource.path.toString()
);
const commands = [CloudSketchbookCommands.PULL_SKETCH];
const commands: Command[] = [];
if (this.connectionStatus.offlineStatus !== 'internet') {
commands.push(CloudSketchbookCommands.PULL_SKETCH);
}
if (
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) &&
this.connectionStatus.offlineStatus !== 'internet'
) {
commands.push(CloudSketchbookCommands.PUSH_SKETCH);
}
@ -557,14 +610,15 @@ export class CloudSketchbookTree extends SketchbookTree {
}
}
// add style decoration for not-in-sync files
// add style decoration for not-in-sync files when offline
if (
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) &&
this.connectionStatus.offlineStatus === 'internet'
) {
this.mergeDecoration(node, this.notInSyncDecoration);
this.mergeDecoration(node, this.notInSyncOfflineDecoration);
} else {
this.removeDecoration(node, this.notInSyncDecoration);
this.removeDecoration(node, this.notInSyncOfflineDecoration);
}
return node;
@ -644,7 +698,7 @@ export namespace CloudSketchbookTree {
export interface CloudSketchDirNode
extends Omit<SketchbookTree.SketchDirNode, 'fileStat'>,
CloudSketchTreeNode {
state?: CloudSketchDirNode.State;
state?: CloudSketchState;
isPublic?: boolean;
sketchId?: string;
commands?: Command[];
@ -653,7 +707,5 @@ export namespace CloudSketchbookTree {
export function is(node: TreeNode | undefined): node is CloudSketchDirNode {
return SketchbookTree.SketchDirNode.is(node);
}
export type State = 'syncing' | 'pulling' | 'pushing';
}
}

View File

@ -1,19 +1,17 @@
import * as React from '@theia/core/shared/react';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { nls } from '@theia/core/lib/common';
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
export class CloudStatus extends React.Component<
UserStatus.Props,
UserStatus.State
CloudStatus.Props,
CloudStatus.State
> {
protected readonly toDispose = new DisposableCollection();
constructor(props: UserStatus.Props) {
constructor(props: CloudStatus.Props) {
super(props);
this.state = {
status: this.status,
@ -22,17 +20,11 @@ export class CloudStatus extends React.Component<
}
override componentDidMount(): void {
const statusListener = () => this.setState({ status: this.status });
window.addEventListener('online', statusListener);
window.addEventListener('offline', statusListener);
this.toDispose.pushAll([
Disposable.create(() =>
window.removeEventListener('online', statusListener)
),
Disposable.create(() =>
window.removeEventListener('offline', statusListener)
),
]);
this.toDispose.push(
this.props.connectionStatus.onOfflineStatusDidChange(() =>
this.setState({ status: this.status })
)
);
}
override componentWillUnmount(): void {
@ -58,14 +50,21 @@ export class CloudStatus extends React.Component<
: nls.localize('arduino/cloud/offline', 'Offline')}
</div>
<div className="actions item flex-line">
<div
title={nls.localize('arduino/cloud/sync', 'Sync')}
className={`fa fa-reload ${
(this.state.refreshing && 'rotating') || ''
}`}
style={{ cursor: 'pointer' }}
onClick={this.onDidClickRefresh}
/>
{this.props.connectionStatus.offlineStatus === 'internet' ? (
<div
className="fa fa-arduino-cloud-offline"
title={nls.localize('arduino/cloud/offline', 'Offline')}
/>
) : (
<div
title={nls.localize('arduino/cloud/sync', 'Sync')}
className={`fa fa-reload ${
(this.state.refreshing && 'rotating') || ''
}`}
style={{ cursor: 'pointer' }}
onClick={this.onDidClickRefresh}
/>
)}
</div>
</div>
);
@ -83,14 +82,17 @@ export class CloudStatus extends React.Component<
};
private get status(): 'connected' | 'offline' {
return window.navigator.onLine ? 'connected' : 'offline';
return this.props.connectionStatus.offlineStatus === 'internet'
? 'offline'
: 'connected';
}
}
export namespace UserStatus {
export namespace CloudStatus {
export interface Props {
readonly model: CloudSketchbookTreeModel;
readonly authenticationService: AuthenticationClientService;
readonly connectionStatus: ApplicationConnectionStatusContribution;
}
export interface State {
status: 'connected' | 'offline';

View File

@ -27,17 +27,14 @@ export namespace SketchbookCommands {
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

@ -5,7 +5,7 @@ import {
postConstruct,
} from '@theia/core/shared/inversify';
import { TreeNode } from '@theia/core/lib/browser/tree/tree';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
import {
NodeProps,
TreeProps,
@ -23,7 +23,6 @@ import {
SketchesServiceClientImpl,
} from '../../sketches-service-client-impl';
import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection';
import { Sketch } from '../../contributions/contribution';
import { nls } from '@theia/core/lib/common';
const customTreeProps: TreeProps = {
@ -91,8 +90,8 @@ export class SketchbookTreeWidget extends FileTreeWidget {
node: TreeNode,
props: NodeProps
): React.ReactNode {
if (SketchbookTree.SketchDirNode.is(node) || Sketch.isSketchFile(node.id)) {
return <div className="sketch-folder-icon file-icon"></div>;
if (SketchbookTree.SketchDirNode.is(node)) {
return undefined;
}
const icon = this.toNodeIcon(node);
if (icon) {
@ -133,26 +132,34 @@ export class SketchbookTreeWidget extends FileTreeWidget {
protected renderInlineCommands(node: TreeNode): React.ReactNode {
if (SketchbookTree.SketchDirNode.is(node) && node.commands) {
return Array.from(new Set(node.commands)).map((command) =>
this.renderInlineCommand(command.id, node)
this.renderInlineCommand(command, node)
);
}
return undefined;
}
protected renderInlineCommand(
commandId: string,
command: Command | string | [command: string, label: string],
node: SketchbookTree.SketchDirNode,
options?: any
): React.ReactNode {
const command = this.commandRegistry.getCommand(commandId);
const icon = command?.iconClass;
const commandId = Command.is(command)
? command.id
: Array.isArray(command)
? command[0]
: command;
const resolvedCommand = this.commandRegistry.getCommand(commandId);
const icon = resolvedCommand?.iconClass;
const args = { model: this.model, node: node, ...options };
if (
command &&
resolvedCommand &&
icon &&
this.commandRegistry.isEnabled(commandId, args) &&
this.commandRegistry.isVisible(commandId, args)
) {
const label = Array.isArray(command)
? command[1]
: resolvedCommand.label ?? resolvedCommand.id;
const className = [
TREE_NODE_SEGMENT_CLASS,
TREE_NODE_TAIL_CLASS,
@ -164,7 +171,7 @@ export class SketchbookTreeWidget extends FileTreeWidget {
<div
key={`${commandId}--${node.id}`}
className={className}
title={command?.label || command.id}
title={label}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();

View File

@ -9,6 +9,7 @@ import {
WorkspaceRootNode,
} from '@theia/navigator/lib/browser/navigator-tree';
import { ArduinoPreferences } from '../../arduino-preferences';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class SketchbookTree extends FileNavigatorTree {
@ -18,7 +19,9 @@ export class SketchbookTree extends FileNavigatorTree {
@inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences;
override async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
override async resolveChildren(
parent: CompositeTreeNode
): Promise<TreeNode[]> {
const showAllFiles =
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
@ -71,7 +74,13 @@ export class SketchbookTree extends FileNavigatorTree {
protected async augmentSketchNode(node: DirNode): Promise<void> {
Object.assign(node, {
type: 'sketch',
commands: [SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU],
commands: [
[
'arduino-create-cloud-copy',
nls.localize('arduino/createCloudCopy', 'Push Sketch to Cloud'),
],
SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU,
],
});
}
@ -96,7 +105,10 @@ export class SketchbookTree extends FileNavigatorTree {
export namespace SketchbookTree {
export interface SketchDirNode extends DirNode {
readonly type: 'sketch';
readonly commands?: Command[];
/**
* Theia command, the command ID string, or a tuple of command ID and preferred UI label. If the array construct is used, the label is the 1<sup>st</sup> of the array.
*/
readonly commands?: (Command | string | [string, string])[];
}
export namespace SketchDirNode {
export function is(

View File

@ -106,7 +106,7 @@ export class SketchbookWidgetContribution
this.revealSketchNode(treeWidgetId, nodeUri),
});
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
execute: (arg) => this.openNewWindow(arg.node),
execute: (arg) => this.openNewWindow(arg.node, arg?.treeWidgetId),
isEnabled: (arg) =>
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
isVisible: (arg) =>
@ -209,14 +209,20 @@ export class SketchbookWidgetContribution
});
}
private openNewWindow(node: SketchbookTree.SketchDirNode): void {
const widget = this.tryGetWidget();
if (widget) {
const treeWidgetId = widget.activeTreeWidgetId();
if (!treeWidgetId) {
private openNewWindow(
node: SketchbookTree.SketchDirNode,
treeWidgetId?: string
): void {
if (!treeWidgetId) {
const widget = this.tryGetWidget();
if (!widget) {
console.warn(`Could not retrieve active sketchbook tree ID.`);
return;
}
treeWidgetId = widget.activeTreeWidgetId();
}
const widget = this.tryGetWidget();
if (widget) {
const nodeUri = node.uri.toString();
const options: WorkspaceInput = {};
Object.assign(options, {

View File

@ -74,12 +74,15 @@ export interface SketchesService {
isTemp(sketch: SketchRef): Promise<boolean>;
/**
* If `isTemp` is `true` for the `sketch`, you can call this method to move the sketch from the temp
* location to `directories.user`. Resolves with the URI of the sketch after the move. Rejects, when the sketch
* was not in the temp folder. This method always overrides. It's the callers responsibility to ask the user whether
* the files at the destination can be overwritten or not.
* Recursively copies the sketch folder content including all files into the destination folder.
* Resolves with the new URI of the sketch after the move. This method always overrides. It's the callers responsibility to ask the user whether
* the files at the destination can be overwritten or not. This method copies all filesystem files, if you want to copy only sketch files,
* but exclude, for example, language server log file, set the `onlySketchFiles` property to `true`. `onlySketchFiles` is `false` by default.
*/
copy(sketch: Sketch, options: { destinationUri: string }): Promise<string>;
copy(
sketch: Sketch,
options: { destinationUri: string; onlySketchFiles?: boolean }
): Promise<Sketch>;
/**
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, the promise resolves to `undefined`.

View File

@ -21,8 +21,15 @@ export function isNullOrUndefined(what: unknown): what is undefined | null {
return what === undefined || what === null;
}
// Use it for and exhaustive `switch` statements
// https://stackoverflow.com/a/39419171/5529090
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function assertUnreachable(_: never): never {
throw new Error();
}
// Text encoder can crash in electron browser: https://github.com/arduino/arduino-ide/issues/634#issuecomment-1440039171
export function unit8ArrayToString(uint8Array: Uint8Array): string {
export function uint8ArrayToString(uint8Array: Uint8Array): string {
return uint8Array.reduce(
(text, byte) => text + String.fromCharCode(byte),
''

View File

@ -6,7 +6,6 @@ import * as path from 'path';
import * as glob from 'glob';
import * as crypto from 'crypto';
import * as PQueue from 'p-queue';
import { ncp } from 'ncp';
import { Mutable } from '@theia/core/lib/common/types';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
@ -44,6 +43,7 @@ import {
startsWithUpperCase,
} from '../common/utils';
import { SettingsReader } from './settings-reader';
import cpy = require('cpy');
const RecentSketches = 'recent-sketches.json';
const DefaultIno = `void setup() {
@ -368,63 +368,66 @@ export class SketchesServiceImpl
const destinationUri = FileUri.create(
path.join(parentPath, sketch.name)
).toString();
const copiedSketchUri = await this.copy(sketch, { destinationUri });
return this.doLoadSketch(copiedSketchUri, false);
const copiedSketch = await this.copy(sketch, { destinationUri });
return this.doLoadSketch(copiedSketch.uri, false);
}
async createNewSketch(): Promise<Sketch> {
const monthNames = [
'jan',
'feb',
'mar',
'apr',
'may',
'jun',
'jul',
'aug',
'sep',
'oct',
'nov',
'dec',
];
const today = new Date();
async createNewSketch(name?: string, content?: string): Promise<Sketch> {
let sketchName: string | undefined = name;
const parentPath = await this.createTempFolder();
const sketchBaseName = `sketch_${
monthNames[today.getMonth()]
}${today.getDate()}`;
const { config } = await this.configService.getConfiguration();
const sketchbookPath = config?.sketchDirUri
? FileUri.fsPath(config?.sketchDirUri)
: os.homedir();
let sketchName: string | undefined;
if (!sketchName) {
const monthNames = [
'jan',
'feb',
'mar',
'apr',
'may',
'jun',
'jul',
'aug',
'sep',
'oct',
'nov',
'dec',
];
const today = new Date();
const sketchBaseName = `sketch_${
monthNames[today.getMonth()]
}${today.getDate()}`;
const { config } = await this.configService.getConfiguration();
const sketchbookPath = config?.sketchDirUri
? FileUri.fsPath(config?.sketchDirUri)
: os.homedir();
// If it's another day, reset the count of sketches created today
if (this.lastSketchBaseName !== sketchBaseName) this.sketchSuffixIndex = 1;
// If it's another day, reset the count of sketches created today
if (this.lastSketchBaseName !== sketchBaseName)
this.sketchSuffixIndex = 1;
let nameFound = false;
while (!nameFound) {
const sketchNameCandidate = `${sketchBaseName}${sketchIndexToLetters(
this.sketchSuffixIndex++
)}`;
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
const sketchExists = await exists(
path.join(sketchbookPath, sketchNameCandidate)
);
if (!sketchExists) {
nameFound = true;
sketchName = sketchNameCandidate;
let nameFound = false;
while (!nameFound) {
const sketchNameCandidate = `${sketchBaseName}${sketchIndexToLetters(
this.sketchSuffixIndex++
)}`;
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
const sketchExists = await exists(
path.join(sketchbookPath, sketchNameCandidate)
);
if (!sketchExists) {
nameFound = true;
sketchName = sketchNameCandidate;
}
}
this.lastSketchBaseName = sketchBaseName;
}
if (!sketchName) {
throw new Error('Cannot create a unique sketch name');
}
this.lastSketchBaseName = sketchBaseName;
const sketchDir = path.join(parentPath, sketchName);
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
const [inoContent] = await Promise.all([
this.loadInoContent(),
content ? content : this.loadInoContent(),
fs.mkdir(sketchDir, { recursive: true }),
]);
await fs.writeFile(sketchFile, inoContent, { encoding: 'utf8' });
@ -441,7 +444,7 @@ export class SketchesServiceImpl
* For example, on Windows, instead of getting an [8.3 filename](https://en.wikipedia.org/wiki/8.3_filename), callers will get a fully resolved path.
* `C:\\Users\\KITTAA~1\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2022615-21100-iahybb.yyvh\\sketch_jul15a` will be `C:\\Users\\kittaakos\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2022615-21100-iahybb.yyvh\\sketch_jul15a`
*/
private createTempFolder(): Promise<string> {
createTempFolder(): Promise<string> {
return new Promise<string>((resolve, reject) => {
temp.mkdir({ prefix: TempSketchPrefix }, (createError, dirPath) => {
if (createError) {
@ -499,58 +502,37 @@ export class SketchesServiceImpl
async copy(
sketch: Sketch,
{ destinationUri }: { destinationUri: string }
): Promise<string> {
const source = FileUri.fsPath(sketch.uri);
const sketchExists = await exists(source);
if (!sketchExists) {
throw new Error(`Sketch does not exist: ${sketch}`);
}
// Nothing to do when source and destination are the same.
if (sketch.uri === destinationUri) {
await this.doLoadSketch(sketch.uri, false); // Sanity check.
return sketch.uri;
}
const copy = async (sourcePath: string, destinationPath: string) => {
return new Promise<void>((resolve, reject) => {
ncp.ncp(sourcePath, destinationPath, async (error) => {
if (error) {
reject(error);
return;
}
const newName = path.basename(destinationPath);
try {
const oldPath = path.join(
destinationPath,
new URI(sketch.mainFileUri).path.base
);
const newPath = path.join(destinationPath, `${newName}.ino`);
if (oldPath !== newPath) {
await fs.rename(oldPath, newPath);
}
await this.doLoadSketch(
FileUri.create(destinationPath).toString(),
false
); // Sanity check.
resolve();
} catch (e) {
reject(e);
}
});
});
};
// https://github.com/arduino/arduino-ide/issues/65
// When copying `/path/to/sketchbook/sketch_A` to `/path/to/sketchbook/sketch_A/anything` on a non-POSIX filesystem,
// `ncp` makes a recursion and copies the folders over and over again. In such cases, we copy the source into a temp folder,
// then move it to the desired destination.
{
destinationUri,
onlySketchFiles,
}: { destinationUri: string; onlySketchFiles?: boolean }
): Promise<Sketch> {
const sourceUri = sketch.uri;
const source = FileUri.fsPath(sourceUri);
const destination = FileUri.fsPath(destinationUri);
let tempDestination = await this.createTempFolder();
tempDestination = path.join(tempDestination, sketch.name);
await fs.mkdir(tempDestination, { recursive: true });
await copy(source, tempDestination);
await copy(tempDestination, destination);
return FileUri.create(destination).toString();
if (source === destination) {
const reloadedSketch = await this.doLoadSketch(sourceUri, false);
return reloadedSketch;
}
const sourceFolderBasename = path.basename(source);
const destinationFolderBasename = path.basename(destination);
let filter: cpy.Options['filter'];
if (onlySketchFiles) {
const sketchFilePaths = Sketch.uris(sketch).map(FileUri.fsPath);
filter = (file) => sketchFilePaths.includes(file.path);
} else {
filter = () => true;
}
await cpy(source, destination, {
rename: (basename) =>
sourceFolderBasename !== destinationFolderBasename &&
basename === `${sourceFolderBasename}.ino`
? `${destinationFolderBasename}.ino`
: basename,
filter,
});
const copiedSketch = await this.doLoadSketch(destinationUri, false);
return copiedSketch;
}
async archive(sketch: Sketch, destinationUri: string): Promise<string> {

View File

@ -0,0 +1,46 @@
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
FrontendApplicationConfigProvider.set({});
import { expect } from 'chai';
import {
backendOfflineText,
backendOfflineTooltip,
daemonOfflineText,
daemonOfflineTooltip,
offlineText,
offlineTooltip,
offlineMessage,
} from '../../browser/theia/core/connection-status-service';
disableJSDOM();
describe('connection-status-service', () => {
describe('offlineMessage', () => {
it('should warn about the offline backend if connected to both CLI daemon and Internet but offline', () => {
const actual = offlineMessage({ port: '50051', online: true });
expect(actual.text).to.be.equal(backendOfflineText);
expect(actual.tooltip).to.be.equal(backendOfflineTooltip);
});
it('should warn about the offline CLI daemon if the CLI daemon port is missing but has Internet connection', () => {
const actual = offlineMessage({ port: undefined, online: true });
expect(actual.text.endsWith(daemonOfflineText)).to.be.true;
expect(actual.tooltip).to.be.equal(daemonOfflineTooltip);
});
it('should warn about the offline CLI daemon if the CLI daemon port is missing and has no Internet connection', () => {
const actual = offlineMessage({ port: undefined, online: false });
expect(actual.text.endsWith(daemonOfflineText)).to.be.true;
expect(actual.tooltip).to.be.equal(daemonOfflineTooltip);
});
it('should warn about no Internet connection if CLI daemon port is available but the Internet connection is offline', () => {
const actual = offlineMessage({ port: '50051', online: false });
expect(actual.text.endsWith(offlineText)).to.be.true;
expect(actual.tooltip).to.be.equal(offlineTooltip);
});
});
});

View File

@ -1,4 +1,4 @@
import { Disposable } from '@theia/core/lib/common/disposable';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Container } from '@theia/core/shared/inversify';
import { expect } from 'chai';
import { BoardSearch, BoardsService } from '../../common/protocol';
@ -10,26 +10,18 @@ import {
describe('boards-service-impl', () => {
let boardService: BoardsService;
let toDispose: Disposable[] = [];
let toDispose: DisposableCollection;
before(async function () {
configureBackendApplicationConfigProvider();
this.timeout(20_000);
toDispose = [];
toDispose = new DisposableCollection();
const container = createContainer();
await start(container, toDispose);
boardService = container.get<BoardsService>(BoardsService);
});
after(() => {
let disposable = toDispose.pop();
while (disposable) {
try {
disposable?.dispose();
} catch {}
disposable = toDispose.pop();
}
});
after(() => toDispose.dispose());
describe('search', () => {
it('should run search', async function () {
@ -37,7 +29,7 @@ describe('boards-service-impl', () => {
expect(result).is.not.empty;
});
it("should boost a result when 'types' includes 'arduino', and lower the score if deprecated", async function () {
it("should boost a result when 'types' includes 'arduino', and lower the score if deprecated", async () => {
const result = await boardService.search({});
const arduinoIndexes: number[] = [];
const otherIndexes: number[] = [];
@ -108,7 +100,7 @@ function createContainer(): Container {
async function start(
container: Container,
toDispose: Disposable[]
toDispose: DisposableCollection
): Promise<void> {
return startDaemon(container, toDispose);
}

View File

@ -1,6 +1,6 @@
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { Disposable } from '@theia/core/lib/common/disposable';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { isWindows } from '@theia/core/lib/common/os';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { Container, injectable } from '@theia/core/shared/inversify';
@ -23,7 +23,7 @@ const uno = 'arduino:avr:uno';
describe('core-service-impl', () => {
let container: Container;
let toDispose: Disposable[];
let toDispose: DisposableCollection;
before(() => {
configureBackendApplicationConfigProvider();
@ -31,20 +31,12 @@ describe('core-service-impl', () => {
beforeEach(async function () {
this.timeout(setupTimeout);
toDispose = [];
toDispose = new DisposableCollection();
container = createContainer();
await start(container, toDispose);
});
afterEach(() => {
let disposable = toDispose.pop();
while (disposable) {
try {
disposable?.dispose();
} catch {}
disposable = toDispose.pop();
}
});
afterEach(() => toDispose.dispose());
describe('compile', () => {
it('should execute a command with the build path', async function () {
@ -92,7 +84,7 @@ describe('core-service-impl', () => {
async function start(
container: Container,
toDispose: Disposable[]
toDispose: DisposableCollection
): Promise<void> {
await startDaemon(container, toDispose, async (container) => {
const boardService = container.get<BoardsService>(BoardsService);

View File

@ -1,4 +1,4 @@
import { Disposable } from '@theia/core/lib/common/disposable';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Container } from '@theia/core/shared/inversify';
import { expect } from 'chai';
import { LibrarySearch, LibraryService } from '../../common/protocol';
@ -11,26 +11,18 @@ import {
describe('library-service-impl', () => {
let libraryService: LibraryService;
let toDispose: Disposable[] = [];
let toDispose: DisposableCollection;
before(async function () {
configureBackendApplicationConfigProvider();
this.timeout(20_000);
toDispose = [];
toDispose = new DisposableCollection();
const container = createContainer();
await start(container, toDispose);
libraryService = container.get<LibraryService>(LibraryService);
});
after(() => {
let disposable = toDispose.pop();
while (disposable) {
try {
disposable?.dispose();
} catch {}
disposable = toDispose.pop();
}
});
after(() => toDispose.dispose());
describe('search', () => {
it('should run search', async function () {
@ -89,7 +81,7 @@ function createContainer(): Container {
async function start(
container: Container,
toDispose: Disposable[]
toDispose: DisposableCollection
): Promise<void> {
return startDaemon(container, toDispose);
}

View File

@ -0,0 +1,262 @@
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { Container } from '@theia/core/shared/inversify';
import { expect } from 'chai';
import { promises as fs } from 'fs';
import { basename, join } from 'path';
import { sync as rimrafSync } from 'rimraf';
import { Sketch, SketchesService } from '../../common/protocol';
import { SketchesServiceImpl } from '../../node/sketches-service-impl';
import { ErrnoException } from '../../node/utils/errors';
import {
configureBackendApplicationConfigProvider,
createBaseContainer,
startDaemon,
} from './test-bindings';
const testTimeout = 10_000;
describe('sketches-service-impl', () => {
let container: Container;
let toDispose: DisposableCollection;
before(async () => {
configureBackendApplicationConfigProvider();
toDispose = new DisposableCollection();
container = createContainer();
await start(container, toDispose);
});
after(() => toDispose.dispose());
describe('copy', () => {
it('should copy a sketch when the destination does not exist', async function () {
this.timeout(testTimeout);
const sketchesService =
container.get<SketchesServiceImpl>(SketchesService);
const destinationPath = await sketchesService['createTempFolder']();
let sketch = await sketchesService.createNewSketch();
toDispose.push(disposeSketch(sketch));
const sourcePath = FileUri.fsPath(sketch.uri);
const libBasename = 'lib.cpp';
const libContent = 'lib content';
const libPath = join(sourcePath, libBasename);
await fs.writeFile(libPath, libContent, { encoding: 'utf8' });
const headerBasename = 'header.h';
const headerContent = 'header content';
const headerPath = join(sourcePath, headerBasename);
await fs.writeFile(headerPath, headerContent, { encoding: 'utf8' });
sketch = await sketchesService.loadSketch(sketch.uri);
expect(Sketch.isInSketch(FileUri.create(libPath), sketch)).to.be.true;
expect(Sketch.isInSketch(FileUri.create(headerPath), sketch)).to.be.true;
const copied = await sketchesService.copy(sketch, {
destinationUri: FileUri.create(destinationPath).toString(),
});
toDispose.push(disposeSketch(copied));
expect(copied.name).to.be.equal(basename(destinationPath));
expect(
Sketch.isInSketch(
FileUri.create(
join(destinationPath, `${basename(destinationPath)}.ino`)
),
copied
)
).to.be.true;
expect(
Sketch.isInSketch(
FileUri.create(join(destinationPath, libBasename)),
copied
)
).to.be.true;
expect(
Sketch.isInSketch(
FileUri.create(join(destinationPath, headerBasename)),
copied
)
).to.be.true;
});
it("should copy only sketch files if 'onlySketchFiles' is true", async function () {
this.timeout(testTimeout);
const sketchesService =
container.get<SketchesServiceImpl>(SketchesService);
const destinationPath = await sketchesService['createTempFolder']();
let sketch = await sketchesService.createNewSketch();
toDispose.push(disposeSketch(sketch));
const sourcePath = FileUri.fsPath(sketch.uri);
const libBasename = 'lib.cpp';
const libContent = 'lib content';
const libPath = join(sourcePath, libBasename);
await fs.writeFile(libPath, libContent, { encoding: 'utf8' });
const headerBasename = 'header.h';
const headerContent = 'header content';
const headerPath = join(sourcePath, headerBasename);
await fs.writeFile(headerPath, headerContent, { encoding: 'utf8' });
const logBasename = 'inols-clangd-err.log';
const logContent = 'log file content';
const logPath = join(sourcePath, logBasename);
await fs.writeFile(logPath, logContent, { encoding: 'utf8' });
sketch = await sketchesService.loadSketch(sketch.uri);
expect(Sketch.isInSketch(FileUri.create(libPath), sketch)).to.be.true;
expect(Sketch.isInSketch(FileUri.create(headerPath), sketch)).to.be.true;
expect(Sketch.isInSketch(FileUri.create(logPath), sketch)).to.be.false;
const reloadedLogContent = await fs.readFile(logPath, {
encoding: 'utf8',
});
expect(reloadedLogContent).to.be.equal(logContent);
const copied = await sketchesService.copy(sketch, {
destinationUri: FileUri.create(destinationPath).toString(),
onlySketchFiles: true,
});
toDispose.push(disposeSketch(copied));
expect(copied.name).to.be.equal(basename(destinationPath));
expect(
Sketch.isInSketch(
FileUri.create(
join(destinationPath, `${basename(destinationPath)}.ino`)
),
copied
)
).to.be.true;
expect(
Sketch.isInSketch(
FileUri.create(join(destinationPath, libBasename)),
copied
)
).to.be.true;
expect(
Sketch.isInSketch(
FileUri.create(join(destinationPath, headerBasename)),
copied
)
).to.be.true;
expect(
Sketch.isInSketch(
FileUri.create(join(destinationPath, logBasename)),
copied
)
).to.be.false;
try {
await fs.readFile(join(destinationPath, logBasename), {
encoding: 'utf8',
});
expect.fail(
'Log file must not exist in the destination. Expected ENOENT when loading the log file.'
);
} catch (err) {
expect(ErrnoException.isENOENT(err)).to.be.true;
}
});
it('should copy sketch inside the sketch folder', async function () {
this.timeout(testTimeout);
const sketchesService =
container.get<SketchesServiceImpl>(SketchesService);
let sketch = await sketchesService.createNewSketch();
const destinationPath = join(FileUri.fsPath(sketch.uri), 'nested_copy');
toDispose.push(disposeSketch(sketch));
const sourcePath = FileUri.fsPath(sketch.uri);
const libBasename = 'lib.cpp';
const libContent = 'lib content';
const libPath = join(sourcePath, libBasename);
await fs.writeFile(libPath, libContent, { encoding: 'utf8' });
const headerBasename = 'header.h';
const headerContent = 'header content';
const headerPath = join(sourcePath, headerBasename);
await fs.writeFile(headerPath, headerContent, { encoding: 'utf8' });
sketch = await sketchesService.loadSketch(sketch.uri);
expect(Sketch.isInSketch(FileUri.create(libPath), sketch)).to.be.true;
expect(Sketch.isInSketch(FileUri.create(headerPath), sketch)).to.be.true;
const copied = await sketchesService.copy(sketch, {
destinationUri: FileUri.create(destinationPath).toString(),
});
toDispose.push(disposeSketch(copied));
expect(copied.name).to.be.equal(basename(destinationPath));
expect(
Sketch.isInSketch(
FileUri.create(
join(destinationPath, `${basename(destinationPath)}.ino`)
),
copied
)
).to.be.true;
expect(
Sketch.isInSketch(
FileUri.create(join(destinationPath, libBasename)),
copied
)
).to.be.true;
expect(
Sketch.isInSketch(
FileUri.create(join(destinationPath, headerBasename)),
copied
)
).to.be.true;
});
it('should copy sketch with overwrite when source and destination sketch folder names are the same', async function () {
this.timeout(testTimeout);
const sketchesService =
container.get<SketchesServiceImpl>(SketchesService);
const sketchFolderName = 'alma';
const contentOne = 'korte';
const contentTwo = 'szilva';
const [sketchOne, sketchTwo] = await Promise.all([
sketchesService.createNewSketch(sketchFolderName, contentOne),
sketchesService.createNewSketch(sketchFolderName, contentTwo),
]);
toDispose.push(disposeSketch(sketchOne, sketchTwo));
const [mainFileContentOne, mainFileContentTwo] = await Promise.all([
mainFileContentOf(sketchOne),
mainFileContentOf(sketchTwo),
]);
expect(mainFileContentOne).to.be.equal(contentOne);
expect(mainFileContentTwo).to.be.equal(contentTwo);
await sketchesService.copy(sketchOne, { destinationUri: sketchTwo.uri });
const [mainFileContentOneAfterCopy, mainFileContentTwoAfterCopy] =
await Promise.all([
mainFileContentOf(sketchOne),
mainFileContentOf(sketchTwo),
]);
expect(mainFileContentOneAfterCopy).to.be.equal(contentOne);
expect(mainFileContentTwoAfterCopy).to.be.equal(contentOne);
});
});
});
function disposeSketch(...sketch: Sketch[]): Disposable {
return new DisposableCollection(
...sketch
.map(({ uri }) => FileUri.fsPath(uri))
.map((path) =>
Disposable.create(() => rimrafSync(path, { maxBusyTries: 5 }))
)
);
}
async function mainFileContentOf(sketch: Sketch): Promise<string> {
return fs.readFile(FileUri.fsPath(sketch.mainFileUri), {
encoding: 'utf8',
});
}
async function start(
container: Container,
toDispose: DisposableCollection
): Promise<void> {
await startDaemon(container, toDispose);
}
function createContainer(): Container {
return createBaseContainer();
}

View File

@ -4,7 +4,10 @@ import {
CommandService,
} from '@theia/core/lib/common/command';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { Disposable } from '@theia/core/lib/common/disposable';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ILogger, Loggable } from '@theia/core/lib/common/logger';
import { LogLevel } from '@theia/core/lib/common/logger-protocol';
@ -289,18 +292,23 @@ export function createBaseContainer(
export async function startDaemon(
container: Container,
toDispose: Disposable[],
toDispose: DisposableCollection,
startCustomizations?: (
container: Container,
toDispose: Disposable[]
toDispose: DisposableCollection
) => Promise<void>
): Promise<void> {
const daemon = container.get<ArduinoDaemonImpl>(ArduinoDaemonImpl);
const configService = container.get<ConfigServiceImpl>(ConfigServiceImpl);
const coreClientProvider =
container.get<CoreClientProvider>(CoreClientProvider);
toDispose.push(Disposable.create(() => daemon.stop()));
configService.onStart();
daemon.onStart();
await waitForEvent(daemon.onDaemonStarted, 10_000);
await Promise.all([
waitForEvent(daemon.onDaemonStarted, 10_000),
coreClientProvider.client,
]);
if (startCustomizations) {
await startCustomizations(container, toDispose);
}

View File

@ -93,8 +93,8 @@
"cloudSketchbook": "Cloud Sketchbook",
"connected": "Connected",
"continue": "Continue",
"donePulling": "Done pulling {0}.",
"donePushing": "Done pushing {0}.",
"donePulling": "Done pulling '{0}'.",
"donePushing": "Done pushing '{0}'.",
"embed": "Embed:",
"emptySketchbook": "Your Sketchbook is empty",
"goToCloud": "Go to Cloud",
@ -179,6 +179,9 @@
"inaccessibleDirectory": "Could not access the sketchbook location at '{0}': {1}"
}
},
"connectionStatus": {
"connectionLost": "Connection lost. Cloud sketch actions and updates won't be available."
},
"contributions": {
"addFile": "Add File",
"fileAdded": "One file added to the sketch.",
@ -199,6 +202,7 @@
"copyError": "Copy error messages",
"noBoardSelected": "No board selected. Please select your Arduino board from the Tools > Board menu."
},
"createCloudCopy": "Push Sketch to Cloud",
"daemon": {
"restart": "Restart Daemon",
"start": "Start Daemon",
@ -462,6 +466,9 @@
"dismissSurvey": "Don't show again",
"surveyMessage": "Please help us improve by answering this super short survey. We value our community and would like to get to know our supporters a little better."
},
"title": {
"cloud": "Cloud"
},
"updateIndexes": {
"updateIndexes": "Update Indexes",
"updateLibraryIndex": "Update Library Index",
@ -494,6 +501,7 @@
"couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.",
"daemonOffline": "CLI Daemon Offline",
"offline": "Offline",
"offlineText": "Offline",
"quitTitle": "Are you sure you want to quit?"
},
"editor": {

245
yarn.lock
View File

@ -1184,6 +1184,11 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@leichtgewicht/ip-codec@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@lerna/add@6.1.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@lerna/add/-/add-6.1.0.tgz#0f09495c5e1af4c4f316344af34b6d1a91b15b19"
@ -1890,6 +1895,14 @@
semver "^7.3.5"
tar "^6.1.11"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
dependencies:
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.1.2.tgz#9571b87be3a3f2c46de05585470bc4f3af2f6f00"
@ -1933,6 +1946,11 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.stat@^1.1.2":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
@ -3294,7 +3312,7 @@
dependencies:
"@types/node" "*"
"@types/glob@*", "@types/glob@^7.2.0":
"@types/glob@*", "@types/glob@^7.1.1", "@types/glob@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
@ -3461,13 +3479,6 @@
dependencies:
"@types/express" "*"
"@types/ncp@^2.0.4":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@types/ncp/-/ncp-2.0.5.tgz#5c53b229a321946102a188b603306162137f4fb9"
integrity sha512-ocK0p8JuFmX7UkMabFPjY0F7apPvQyLWt5qtdvuvQEBz9i4m2dbzV+6L1zNaUp042RfnL6pHnxDE53OH6XQ9VQ==
dependencies:
"@types/node" "*"
"@types/node-fetch@^2.5.7":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
@ -4531,7 +4542,7 @@ array-sort@^0.1.4:
get-value "^2.0.6"
kind-of "^5.0.2"
array-union@^1.0.1:
array-union@^1.0.1, array-union@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==
@ -5187,6 +5198,11 @@ call-bind@^1.0.0, call-bind@^1.0.2:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
call-me-maybe@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa"
integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@ -5943,6 +5959,31 @@ cp-file@^6.1.0:
pify "^4.0.1"
safe-buffer "^5.0.1"
cp-file@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd"
integrity sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw==
dependencies:
graceful-fs "^4.1.2"
make-dir "^3.0.0"
nested-error-stacks "^2.0.0"
p-event "^4.1.0"
cpy@^8.1.2:
version "8.1.2"
resolved "https://registry.yarnpkg.com/cpy/-/cpy-8.1.2.tgz#e339ea54797ad23f8e3919a5cffd37bfc3f25935"
integrity sha512-dmC4mUesv0OYH2kNFEidtf/skUwv4zePmGeepjyyJ0qTo5+8KhA1o99oIAwVVLzQMAeDJml74d6wPPKb6EZUTg==
dependencies:
arrify "^2.0.1"
cp-file "^7.0.0"
globby "^9.2.0"
has-glob "^1.0.0"
junk "^3.1.0"
nested-error-stacks "^2.1.0"
p-all "^2.1.0"
p-filter "^2.1.0"
p-map "^3.0.0"
create-frame@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/create-frame/-/create-frame-1.0.0.tgz#8b95f2691e3249b6080443e33d0bad9f8f6975aa"
@ -6402,7 +6443,7 @@ diff@^5.0.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
dir-glob@^2.0.0:
dir-glob@^2.0.0, dir-glob@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==
@ -6416,6 +6457,20 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
dns-packet@^5.2.4:
version "5.4.0"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b"
integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==
dependencies:
"@leichtgewicht/ip-codec" "^2.0.1"
dns-socket@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/dns-socket/-/dns-socket-4.2.2.tgz#58b0186ec053ea0731feb06783c7eeac4b95b616"
integrity sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg==
dependencies:
dns-packet "^5.2.4"
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@ -7236,6 +7291,18 @@ fast-glob@3.2.7:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^2.2.6:
version "2.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
dependencies:
"@mrmlnc/readdir-enhanced" "^2.2.1"
"@nodelib/fs.stat" "^1.1.2"
glob-parent "^3.1.0"
is-glob "^4.0.0"
merge2 "^1.2.3"
micromatch "^3.1.10"
fast-glob@^3.2.5, fast-glob@^3.2.9:
version "3.2.11"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
@ -7925,6 +7992,14 @@ github-from-package@0.0.0:
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
integrity sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==
dependencies:
is-glob "^3.1.0"
path-dirname "^1.0.0"
glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@ -7932,6 +8007,11 @@ glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
dependencies:
is-glob "^4.0.1"
glob-to-regexp@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==
glob-to-regexp@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
@ -8049,6 +8129,20 @@ globby@^7.1.1:
pify "^3.0.0"
slash "^1.0.0"
globby@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==
dependencies:
"@types/glob" "^7.1.1"
array-union "^1.0.2"
dir-glob "^2.2.2"
fast-glob "^2.2.6"
glob "^7.1.3"
ignore "^4.0.3"
pify "^4.0.1"
slash "^2.0.0"
google-protobuf@3.12.4:
version "3.12.4"
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.12.4.tgz#fd89b7e5052cdb35a80f9b455612851d542a5c9f"
@ -8076,6 +8170,23 @@ got@^11.7.0, got@^11.8.5:
p-cancelable "^2.0.0"
responselike "^2.0.0"
got@^11.8.0:
version "11.8.6"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a"
integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==
dependencies:
"@sindresorhus/is" "^4.0.0"
"@szmarczak/http-timer" "^4.0.5"
"@types/cacheable-request" "^6.0.1"
"@types/responselike" "^1.0.0"
cacheable-lookup "^5.0.3"
cacheable-request "^7.0.2"
decompress-response "^6.0.0"
http2-wrapper "^1.0.0-beta.5.2"
lowercase-keys "^2.0.0"
p-cancelable "^2.0.0"
responselike "^2.0.0"
got@^8.3.1:
version "8.3.2"
resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937"
@ -8257,6 +8368,13 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-glob@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-glob/-/has-glob-1.0.0.tgz#9aaa9eedbffb1ba3990a7b0010fb678ee0081207"
integrity sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==
dependencies:
is-glob "^3.0.0"
has-property-descriptors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
@ -8595,7 +8713,7 @@ ignore@^3.3.5:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==
ignore@^4.0.6:
ignore@^4.0.3, ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
@ -8745,6 +8863,11 @@ invert-kv@^2.0.0:
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
ip-regex@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
ip@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
@ -8900,7 +9023,7 @@ is-extendable@^1.0.1:
dependencies:
is-plain-object "^2.0.4"
is-extglob@^2.1.1:
is-extglob@^2.1.0, is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
@ -8922,6 +9045,13 @@ is-fullwidth-code-point@^3.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^3.0.0, is-glob@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
integrity sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==
dependencies:
is-extglob "^2.1.0"
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
@ -8934,6 +9064,13 @@ is-interactive@^1.0.0:
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
is-ip@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8"
integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==
dependencies:
ip-regex "^4.0.0"
is-lambda@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
@ -9002,6 +9139,16 @@ is-odd@^0.1.2:
dependencies:
is-number "^3.0.0"
is-online@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/is-online/-/is-online-9.0.1.tgz#71a34202fa826bae6f3ff8bea420c56573448a5f"
integrity sha512-+08dRW0dcFOtleR2N3rHRVxDyZtQitUp9cC+KpKTds0mXibbQyW5js7xX0UGyQXkaLUJObe0w6uQ4ex34lX9LA==
dependencies:
got "^11.8.0"
p-any "^3.0.0"
p-timeout "^3.2.0"
public-ip "^4.0.4"
is-path-inside@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
@ -9418,6 +9565,11 @@ jsprim@^1.2.2:
array-includes "^3.1.5"
object.assign "^4.1.2"
junk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
just-diff-apply@^5.2.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.4.1.tgz#1debed059ad009863b4db0e8d8f333d743cdd83b"
@ -10167,7 +10319,7 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
merge2@^1.3.0, merge2@^1.4.1:
merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
@ -10372,7 +10524,7 @@ micromark@^3.0.0:
micromark-util-types "^1.0.1"
uvu "^0.5.0"
micromatch@^3.1.4:
micromatch@^3.1.10, micromatch@^3.1.4:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
@ -10841,7 +10993,7 @@ neo-async@^2.6.0, neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
nested-error-stacks@^2.0.0:
nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5"
integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==
@ -11438,6 +11590,21 @@ os-tmpdir@~1.0.2:
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
p-all@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-all/-/p-all-2.1.0.tgz#91419be56b7dee8fe4c5db875d55e0da084244a0"
integrity sha512-HbZxz5FONzz/z2gJfk6bFca0BCiSRF8jU3yCsWOen/vR6lZjfPOu/e7L3uFzTW1i0H8TlC3vqQstEJPQL4/uLA==
dependencies:
p-map "^2.0.0"
p-any@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-any/-/p-any-3.0.0.tgz#79847aeed70b5d3a10ea625296c0c3d2e90a87b9"
integrity sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w==
dependencies:
p-cancelable "^2.0.0"
p-some "^5.0.0"
p-cancelable@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0"
@ -11470,6 +11637,20 @@ p-event@^2.1.0:
dependencies:
p-timeout "^2.0.1"
p-event@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5"
integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==
dependencies:
p-timeout "^3.1.0"
p-filter@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c"
integrity sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==
dependencies:
p-map "^2.0.0"
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@ -11532,6 +11713,11 @@ p-map-series@^2.1.0:
resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2"
integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q==
p-map@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
p-map@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d"
@ -11569,6 +11755,14 @@ p-reduce@^2.0.0, p-reduce@^2.1.0:
resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a"
integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==
p-some@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-some/-/p-some-5.0.0.tgz#8b730c74b4fe5169d7264a240ad010b6ebc686a4"
integrity sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig==
dependencies:
aggregate-error "^3.0.0"
p-cancelable "^2.0.0"
p-timeout@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
@ -11576,7 +11770,7 @@ p-timeout@^2.0.1:
dependencies:
p-finally "^1.0.0"
p-timeout@^3.2.0:
p-timeout@^3.1.0, p-timeout@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
@ -11719,6 +11913,11 @@ path-case@^3.0.4:
dot-case "^3.0.4"
tslib "^2.0.3"
path-dirname@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
integrity sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@ -12147,6 +12346,15 @@ psl@^1.1.28, psl@^1.1.33:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
public-ip@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/public-ip/-/public-ip-4.0.4.tgz#b3784a5a1ff1b81d015b9a18450be65ffd929eb3"
integrity sha512-EJ0VMV2vF6Cu7BIPo3IMW1Maq6ME+fbR0NcPmqDfpfNGIRPue1X8QrGjrg/rfjDkOsIkKHIf2S5FlEa48hFMTA==
dependencies:
dns-socket "^4.2.2"
got "^9.6.0"
is-ip "^3.1.0"
pump@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
@ -13229,6 +13437,11 @@ slash@^1.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
integrity sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==
slash@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"