mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-19 01:06:39 +00:00
Refresh menus when opening example/recent fails.
Closes #53 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
32b70efd5c
commit
da22f1ed11
@ -21,16 +21,23 @@ import {
|
|||||||
MenuModelRegistry,
|
MenuModelRegistry,
|
||||||
} from './contribution';
|
} from './contribution';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
|
import {
|
||||||
|
Board,
|
||||||
|
SketchRef,
|
||||||
|
SketchContainer,
|
||||||
|
SketchesError,
|
||||||
|
Sketch,
|
||||||
|
CoreService,
|
||||||
|
} from '../../common/protocol';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export abstract class Examples extends SketchContribution {
|
export abstract class Examples extends SketchContribution {
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
private readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
@inject(MenuModelRegistry)
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
private readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@inject(MainMenuManager)
|
@inject(MainMenuManager)
|
||||||
protected readonly menuManager: MainMenuManager;
|
protected readonly menuManager: MainMenuManager;
|
||||||
@ -38,6 +45,9 @@ export abstract class Examples extends SketchContribution {
|
|||||||
@inject(ExamplesService)
|
@inject(ExamplesService)
|
||||||
protected readonly examplesService: ExamplesService;
|
protected readonly examplesService: ExamplesService;
|
||||||
|
|
||||||
|
@inject(CoreService)
|
||||||
|
protected readonly coreService: CoreService;
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||||
|
|
||||||
@ -50,10 +60,16 @@ export abstract class Examples extends SketchContribution {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
||||||
protected handleBoardChanged(board: Board | undefined): void {
|
protected handleBoardChanged(board: Board | undefined): void {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract update(options?: {
|
||||||
|
board?: Board | undefined;
|
||||||
|
forceRefresh?: boolean;
|
||||||
|
}): void;
|
||||||
|
|
||||||
override registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
try {
|
try {
|
||||||
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
||||||
@ -149,23 +165,54 @@ export abstract class Examples extends SketchContribution {
|
|||||||
protected createHandler(uri: string): CommandHandler {
|
protected createHandler(uri: string): CommandHandler {
|
||||||
return {
|
return {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const sketch = await this.sketchService.cloneExample(uri);
|
const sketch = await this.clone(uri);
|
||||||
|
if (sketch) {
|
||||||
|
try {
|
||||||
return this.commandService.executeCommand(
|
return this.commandService.executeCommand(
|
||||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
sketch
|
sketch
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||||
|
this.update({
|
||||||
|
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||||
|
forceRefresh: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async clone(uri: string): Promise<Sketch | undefined> {
|
||||||
|
try {
|
||||||
|
const sketch = await this.sketchService.cloneExample(uri);
|
||||||
|
return sketch;
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
this.messageService.error(err.message);
|
||||||
|
this.update({
|
||||||
|
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||||
|
forceRefresh: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BuiltInExamples extends Examples {
|
export class BuiltInExamples extends Examples {
|
||||||
override async onReady(): Promise<void> {
|
override async onReady(): Promise<void> {
|
||||||
this.register(); // no `await`
|
this.update(); // no `await`
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async register(): Promise<void> {
|
protected override async update(): Promise<void> {
|
||||||
let sketchContainers: SketchContainer[] | undefined;
|
let sketchContainers: SketchContainer[] | undefined;
|
||||||
try {
|
try {
|
||||||
sketchContainers = await this.examplesService.builtIns();
|
sketchContainers = await this.examplesService.builtIns();
|
||||||
@ -197,29 +244,34 @@ export class BuiltInExamples extends Examples {
|
|||||||
@injectable()
|
@injectable()
|
||||||
export class LibraryExamples extends Examples {
|
export class LibraryExamples extends Examples {
|
||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
private readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||||
|
|
||||||
override onStart(): void {
|
override onStart(): void {
|
||||||
this.notificationCenter.onLibraryDidInstall(() => this.register());
|
this.notificationCenter.onLibraryDidInstall(() => this.update());
|
||||||
this.notificationCenter.onLibraryDidUninstall(() => this.register());
|
this.notificationCenter.onLibraryDidUninstall(() => this.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
override async onReady(): Promise<void> {
|
override async onReady(): Promise<void> {
|
||||||
this.register(); // no `await`
|
this.update(); // no `await`
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override handleBoardChanged(board: Board | undefined): void {
|
protected override handleBoardChanged(board: Board | undefined): void {
|
||||||
this.register(board);
|
this.update({ board });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async register(
|
protected override async update(
|
||||||
board: Board | undefined = this.boardsServiceClient.boardsConfig
|
options: { board?: Board; forceRefresh?: boolean } = {
|
||||||
.selectedBoard
|
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||||
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { board, forceRefresh } = options;
|
||||||
return this.queue.add(async () => {
|
return this.queue.add(async () => {
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
|
if (forceRefresh) {
|
||||||
|
await this.coreService.refresh();
|
||||||
|
}
|
||||||
const fqbn = board?.fqbn;
|
const fqbn = board?.fqbn;
|
||||||
const name = board?.name;
|
const name = board?.name;
|
||||||
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
import { injectable } from '@theia/core/shared/inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
SketchContribution,
|
||||||
URI,
|
URI,
|
||||||
@ -17,11 +16,6 @@ export class NewSketch extends SketchContribution {
|
|||||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
||||||
execute: () => this.newSketch(),
|
execute: () => this.newSketch(),
|
||||||
});
|
});
|
||||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
|
|
||||||
isVisible: (widget) =>
|
|
||||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
|
||||||
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
@ -54,8 +48,5 @@ export namespace NewSketch {
|
|||||||
export const NEW_SKETCH: Command = {
|
export const NEW_SKETCH: Command = {
|
||||||
id: 'arduino-new-sketch',
|
id: 'arduino-new-sketch',
|
||||||
};
|
};
|
||||||
export const NEW_SKETCH__TOOLBAR: Command = {
|
|
||||||
id: 'arduino-new-sketch--toolbar',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
|
|||||||
import { OpenSketch } from './open-sketch';
|
import { OpenSketch } from './open-sketch';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { SketchesError } from '../../common/protocol';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenRecentSketch extends SketchContribution {
|
export class OpenRecentSketch extends SketchContribution {
|
||||||
@ -33,7 +34,7 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
protected readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
protected toDispose = new DisposableCollection();
|
||||||
|
|
||||||
override onStart(): void {
|
override onStart(): void {
|
||||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||||
@ -42,8 +43,12 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async onReady(): Promise<void> {
|
override async onReady(): Promise<void> {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private update(forceUpdate?: boolean): void {
|
||||||
this.sketchService
|
this.sketchService
|
||||||
.recentlyOpenedSketches()
|
.recentlyOpenedSketches(forceUpdate)
|
||||||
.then((sketches) => this.refreshMenu(sketches));
|
.then((sketches) => this.refreshMenu(sketches));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,19 +67,25 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
|
|
||||||
protected register(sketches: Sketch[]): void {
|
protected register(sketches: Sketch[]): void {
|
||||||
const order = 0;
|
const order = 0;
|
||||||
|
this.toDispose.dispose();
|
||||||
for (const sketch of sketches) {
|
for (const sketch of sketches) {
|
||||||
const { uri } = sketch;
|
const { uri } = sketch;
|
||||||
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
|
||||||
if (toDispose) {
|
|
||||||
toDispose.dispose();
|
|
||||||
}
|
|
||||||
const command = { id: `arduino-open-recent--${uri}` };
|
const command = { id: `arduino-open-recent--${uri}` };
|
||||||
const handler = {
|
const handler = {
|
||||||
execute: () =>
|
execute: async () => {
|
||||||
this.commandRegistry.executeCommand(
|
try {
|
||||||
|
await this.commandRegistry.executeCommand(
|
||||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
sketch
|
sketch
|
||||||
),
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
this.update(true);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
this.commandRegistry.registerCommand(command, handler);
|
this.commandRegistry.registerCommand(command, handler);
|
||||||
this.menuRegistry.registerMenuAction(
|
this.menuRegistry.registerMenuAction(
|
||||||
@ -85,8 +96,7 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
order: String(order),
|
order: String(order),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.toDisposeBeforeRegister.set(
|
this.toDispose.pushAll([
|
||||||
sketch.uri,
|
|
||||||
new DisposableCollection(
|
new DisposableCollection(
|
||||||
Disposable.create(() =>
|
Disposable.create(() =>
|
||||||
this.commandRegistry.unregisterCommand(command)
|
this.commandRegistry.unregisterCommand(command)
|
||||||
@ -94,8 +104,8 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
Disposable.create(() =>
|
Disposable.create(() =>
|
||||||
this.menuRegistry.unregisterMenuAction(command)
|
this.menuRegistry.unregisterMenuAction(command)
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,115 +1,44 @@
|
|||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
||||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import {
|
import { SketchesError, SketchRef } from '../../common/protocol';
|
||||||
Disposable,
|
|
||||||
DisposableCollection,
|
|
||||||
} from '@theia/core/lib/common/disposable';
|
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
|
||||||
Sketch,
|
|
||||||
URI,
|
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry,
|
CommandRegistry,
|
||||||
MenuModelRegistry,
|
|
||||||
KeybindingRegistry,
|
KeybindingRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
Sketch,
|
||||||
|
SketchContribution,
|
||||||
|
URI,
|
||||||
} from './contribution';
|
} from './contribution';
|
||||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
|
||||||
import { BuiltInExamples } from './examples';
|
export type SketchLocation = string | URI | SketchRef;
|
||||||
import { Sketchbook } from './sketchbook';
|
export namespace SketchLocation {
|
||||||
import { SketchContainer } from '../../common/protocol';
|
export function toUri(location: SketchLocation): URI {
|
||||||
import { nls } from '@theia/core/lib/common';
|
if (typeof location === 'string') {
|
||||||
|
return new URI(location);
|
||||||
|
} else if (SketchRef.is(location)) {
|
||||||
|
return toUri(location.uri);
|
||||||
|
} else {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function is(arg: unknown): arg is SketchLocation {
|
||||||
|
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenSketch extends SketchContribution {
|
export class OpenSketch extends SketchContribution {
|
||||||
@inject(MenuModelRegistry)
|
|
||||||
private readonly menuRegistry: MenuModelRegistry;
|
|
||||||
|
|
||||||
@inject(ContextMenuRenderer)
|
|
||||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
|
||||||
|
|
||||||
@inject(BuiltInExamples)
|
|
||||||
private readonly builtInExamples: BuiltInExamples;
|
|
||||||
|
|
||||||
@inject(ExamplesService)
|
|
||||||
private readonly examplesService: ExamplesService;
|
|
||||||
|
|
||||||
@inject(Sketchbook)
|
|
||||||
private readonly sketchbook: Sketchbook;
|
|
||||||
|
|
||||||
private readonly toDispose = new DisposableCollection();
|
|
||||||
|
|
||||||
override registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||||
execute: (arg) =>
|
execute: async (arg) => {
|
||||||
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
|
const toOpen = !SketchLocation.is(arg)
|
||||||
});
|
? await this.selectSketch()
|
||||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
: arg;
|
||||||
isVisible: (widget) =>
|
if (toOpen) {
|
||||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
return this.openSketch(toOpen);
|
||||||
execute: async (_: Widget, target: EventTarget) => {
|
|
||||||
const container = await this.sketchService.getSketches({
|
|
||||||
exclude: ['**/hardware/**'],
|
|
||||||
});
|
|
||||||
if (SketchContainer.isEmpty(container)) {
|
|
||||||
this.openSketch();
|
|
||||||
} else {
|
|
||||||
this.toDispose.dispose();
|
|
||||||
if (!(target instanceof HTMLElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { parentElement } = target;
|
|
||||||
if (!parentElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.menuRegistry.registerMenuAction(
|
|
||||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
|
|
||||||
{
|
|
||||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
|
||||||
label: nls.localize(
|
|
||||||
'vscode/workspaceActions/openFileFolder',
|
|
||||||
'Open...'
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
this.toDispose.push(
|
|
||||||
Disposable.create(() =>
|
|
||||||
this.menuRegistry.unregisterMenuAction(
|
|
||||||
OpenSketch.Commands.OPEN_SKETCH
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.sketchbook.registerRecursively(
|
|
||||||
[...container.children, ...container.sketches],
|
|
||||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
|
|
||||||
this.toDispose
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
const containers = await this.examplesService.builtIns();
|
|
||||||
for (const container of containers) {
|
|
||||||
this.builtInExamples.registerRecursively(
|
|
||||||
container,
|
|
||||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
|
|
||||||
this.toDispose
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error when collecting built-in examples.', e);
|
|
||||||
}
|
|
||||||
const options = {
|
|
||||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
|
||||||
anchor: {
|
|
||||||
x: parentElement.getBoundingClientRect().left,
|
|
||||||
y:
|
|
||||||
parentElement.getBoundingClientRect().top +
|
|
||||||
parentElement.offsetHeight,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.contextMenuRenderer.render(options);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -130,13 +59,20 @@ export class OpenSketch extends SketchContribution {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openSketch(
|
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
|
||||||
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
|
if (!toOpen) {
|
||||||
): Promise<void> {
|
return;
|
||||||
const sketch = await toOpen;
|
|
||||||
if (sketch) {
|
|
||||||
this.workspaceService.open(new URI(sketch.uri));
|
|
||||||
}
|
}
|
||||||
|
const uri = SketchLocation.toUri(toOpen);
|
||||||
|
try {
|
||||||
|
await this.sketchService.loadSketch(uri.toString());
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
this.messageService.error(err.message);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
this.workspaceService.open(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async selectSketch(): Promise<Sketch | undefined> {
|
private async selectSketch(): Promise<Sketch | undefined> {
|
||||||
@ -220,8 +156,5 @@ export namespace OpenSketch {
|
|||||||
export const OPEN_SKETCH: Command = {
|
export const OPEN_SKETCH: Command = {
|
||||||
id: 'arduino-open-sketch',
|
id: 'arduino-open-sketch',
|
||||||
};
|
};
|
||||||
export const OPEN_SKETCH__TOOLBAR: Command = {
|
|
||||||
id: 'arduino-open-sketch--toolbar',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { injectable } from '@theia/core/shared/inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
|
||||||
import { SaveAsSketch } from './save-as-sketch';
|
import { SaveAsSketch } from './save-as-sketch';
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
SketchContribution,
|
||||||
@ -19,12 +18,6 @@ export class SaveSketch extends SketchContribution {
|
|||||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
||||||
execute: () => this.saveSketch(),
|
execute: () => this.saveSketch(),
|
||||||
});
|
});
|
||||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
|
|
||||||
isVisible: (widget) =>
|
|
||||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
|
||||||
execute: () =>
|
|
||||||
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
@ -68,8 +61,5 @@ export namespace SaveSketch {
|
|||||||
export const SAVE_SKETCH: Command = {
|
export const SAVE_SKETCH: Command = {
|
||||||
id: 'arduino-save-sketch',
|
id: 'arduino-save-sketch',
|
||||||
};
|
};
|
||||||
export const SAVE_SKETCH__TOOLBAR: Command = {
|
|
||||||
id: 'arduino-save-sketch--toolbar',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,14 @@
|
|||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||||
import { CommandRegistry, MenuModelRegistry } from './contribution';
|
import { MenuModelRegistry } from './contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
|
||||||
import { NotificationCenter } from '../notification-center';
|
|
||||||
import { Examples } from './examples';
|
import { Examples } from './examples';
|
||||||
import {
|
import { SketchContainer, SketchesError } from '../../common/protocol';
|
||||||
SketchContainer,
|
|
||||||
SketchesError,
|
|
||||||
SketchRef,
|
|
||||||
} from '../../common/protocol';
|
|
||||||
import { OpenSketch } from './open-sketch';
|
import { OpenSketch } from './open-sketch';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class Sketchbook extends Examples {
|
export class Sketchbook extends Examples {
|
||||||
@inject(CommandRegistry)
|
|
||||||
protected override readonly commandRegistry: CommandRegistry;
|
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
|
||||||
protected override readonly menuRegistry: MenuModelRegistry;
|
|
||||||
|
|
||||||
@inject(MainMenuManager)
|
|
||||||
protected readonly mainMenuManager: MainMenuManager;
|
|
||||||
|
|
||||||
@inject(NotificationCenter)
|
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
|
||||||
|
|
||||||
override onStart(): void {
|
override onStart(): void {
|
||||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||||
}
|
}
|
||||||
@ -35,10 +17,10 @@ export class Sketchbook extends Examples {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
protected override update(): void {
|
||||||
this.sketchService.getSketches({}).then((container) => {
|
this.sketchService.getSketches({}).then((container) => {
|
||||||
this.register(container);
|
this.register(container);
|
||||||
this.mainMenuManager.update();
|
this.menuManager.update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +32,7 @@ export class Sketchbook extends Examples {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected register(container: SketchContainer): void {
|
private register(container: SketchContainer): void {
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
this.registerRecursively(
|
this.registerRecursively(
|
||||||
[...container.children, ...container.sketches],
|
[...container.children, ...container.sketches],
|
||||||
@ -62,23 +44,18 @@ export class Sketchbook extends Examples {
|
|||||||
protected override createHandler(uri: string): CommandHandler {
|
protected override createHandler(uri: string): CommandHandler {
|
||||||
return {
|
return {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
let sketch: SketchRef | undefined = undefined;
|
|
||||||
try {
|
try {
|
||||||
sketch = await this.sketchService.loadSketch(uri);
|
|
||||||
} catch (err) {
|
|
||||||
if (SketchesError.NotFound.is(err)) {
|
|
||||||
// To handle the following:
|
|
||||||
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
|
|
||||||
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
|
|
||||||
this.messageService.error(err.message);
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sketch) {
|
|
||||||
await this.commandService.executeCommand(
|
await this.commandService.executeCommand(
|
||||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
sketch
|
uri
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
// Force update the menu items to remove the absent sketch.
|
||||||
|
this.update();
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
|
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
|
||||||
import { duration } from '../../../common/decorators';
|
|
||||||
|
|
||||||
export class AboutDialog extends TheiaAboutDialog {
|
export class AboutDialog extends TheiaAboutDialog {
|
||||||
@duration({ name: 'theia-about#init' })
|
|
||||||
protected override async init(): Promise<void> {
|
protected override async init(): Promise<void> {
|
||||||
// NOOP
|
// NOOP
|
||||||
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.
|
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.
|
||||||
|
@ -108,6 +108,10 @@ export interface CoreService {
|
|||||||
compile(options: CoreService.Options.Compile): Promise<void>;
|
compile(options: CoreService.Options.Compile): Promise<void>;
|
||||||
upload(options: CoreService.Options.Upload): Promise<void>;
|
upload(options: CoreService.Options.Upload): Promise<void>;
|
||||||
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
|
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Refreshes the underling core gRPC client for the Arduino CLI.
|
||||||
|
*/
|
||||||
|
refresh(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace CoreService {
|
export namespace CoreService {
|
||||||
|
@ -21,16 +21,9 @@ export const SketchesService = Symbol('SketchesService');
|
|||||||
export interface SketchesService {
|
export interface SketchesService {
|
||||||
/**
|
/**
|
||||||
* Resolves to a sketch container representing the hierarchical structure of the sketches.
|
* Resolves to a sketch container representing the hierarchical structure of the sketches.
|
||||||
* If `uri` is not given, `directories.user` will be user instead. Specify `exclude` global patterns to filter folders from the sketch container.
|
* If `uri` is not given, `directories.user` will be user instead.
|
||||||
* If `exclude` is not set `['**\/libraries\/**', '**\/hardware\/**']` will be used instead.
|
|
||||||
*/
|
*/
|
||||||
getSketches({
|
getSketches({ uri }: { uri?: string }): Promise<SketchContainer>;
|
||||||
uri,
|
|
||||||
exclude,
|
|
||||||
}: {
|
|
||||||
uri?: string;
|
|
||||||
exclude?: string[];
|
|
||||||
}): Promise<SketchContainer>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
|
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
|
||||||
@ -71,7 +64,7 @@ export interface SketchesService {
|
|||||||
copy(sketch: Sketch, options: { destinationUri: string }): Promise<string>;
|
copy(sketch: Sketch, options: { destinationUri: string }): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, resolved `undefined`.
|
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, the promise resolves to `undefined`.
|
||||||
*/
|
*/
|
||||||
getSketchFolder(uri: string): Promise<Sketch | undefined>;
|
getSketchFolder(uri: string): Promise<Sketch | undefined>;
|
||||||
|
|
||||||
@ -82,8 +75,10 @@ export interface SketchesService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves to an array of sketches in inverse chronological order. The newest is the first.
|
* Resolves to an array of sketches in inverse chronological order. The newest is the first.
|
||||||
|
* If `forceUpdate` is `true`, the array of recently opened sketches will be recalculated.
|
||||||
|
* Invalid and missing sketches will be removed from the list. It's `false` by default.
|
||||||
*/
|
*/
|
||||||
recentlyOpenedSketches(): Promise<Sketch[]>;
|
recentlyOpenedSketches(forceUpdate?: boolean): Promise<Sketch[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Archives the sketch, resolves to the archive URI.
|
* Archives the sketch, resolves to the archive URI.
|
||||||
@ -114,6 +109,19 @@ export namespace SketchRef {
|
|||||||
uri: typeof uriLike === 'string' ? uriLike : uriLike.toString(),
|
uri: typeof uriLike === 'string' ? uriLike : uriLike.toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export function is(arg: unknown): arg is SketchRef {
|
||||||
|
if (typeof arg === 'object') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const object = arg as any;
|
||||||
|
return (
|
||||||
|
'name' in object &&
|
||||||
|
typeof object['name'] === 'string' &&
|
||||||
|
'uri' in object &&
|
||||||
|
typeof object['name'] === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export interface Sketch extends SketchRef {
|
export interface Sketch extends SketchRef {
|
||||||
readonly mainFileUri: string; // `MainFile`
|
readonly mainFileUri: string; // `MainFile`
|
||||||
@ -122,15 +130,26 @@ export interface Sketch extends SketchRef {
|
|||||||
readonly rootFolderFileUris: string[]; // `RootFolderFiles` (does not include the main sketch file)
|
readonly rootFolderFileUris: string[]; // `RootFolderFiles` (does not include the main sketch file)
|
||||||
}
|
}
|
||||||
export namespace Sketch {
|
export namespace Sketch {
|
||||||
export function is(arg: any): arg is Sketch {
|
export function is(arg: unknown): arg is Sketch {
|
||||||
|
if (!SketchRef.is(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof arg === 'object') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const object = arg as any;
|
||||||
return (
|
return (
|
||||||
!!arg &&
|
'mainFileUri' in object &&
|
||||||
'name' in arg &&
|
typeof object['mainFileUri'] === 'string' &&
|
||||||
'uri' in arg &&
|
'otherSketchFileUris' in object &&
|
||||||
typeof arg.name === 'string' &&
|
Array.isArray(object['otherSketchFileUris']) &&
|
||||||
typeof arg.uri === 'string'
|
'additionalFileUris' in object &&
|
||||||
|
Array.isArray(object['additionalFileUris']) &&
|
||||||
|
'rootFolderFileUris' in object &&
|
||||||
|
Array.isArray(object['rootFolderFileUris'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
export namespace Extensions {
|
export namespace Extensions {
|
||||||
export const MAIN = ['.ino', '.pde'];
|
export const MAIN = ['.ino', '.pde'];
|
||||||
export const SOURCE = ['.c', '.cpp', '.s'];
|
export const SOURCE = ['.c', '.cpp', '.s'];
|
||||||
|
@ -332,6 +332,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
'fwuploader', // Arduino Firmware uploader
|
'fwuploader', // Arduino Firmware uploader
|
||||||
'discovery-log', // Boards discovery
|
'discovery-log', // Boards discovery
|
||||||
'config', // Logger for the CLI config reading and manipulation
|
'config', // Logger for the CLI config reading and manipulation
|
||||||
|
'sketches-service', // For creating, loading, and cloning sketches
|
||||||
MonitorManagerName, // Logger for the monitor manager and its services
|
MonitorManagerName, // Logger for the monitor manager and its services
|
||||||
MonitorServiceName,
|
MonitorServiceName,
|
||||||
].forEach((name) => bindChildLogger(bind, name));
|
].forEach((name) => bindChildLogger(bind, name));
|
||||||
|
@ -26,7 +26,6 @@ import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
|
|||||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||||
import { deepClone } from '@theia/core';
|
import { deepClone } from '@theia/core';
|
||||||
import { duration } from '../common/decorators';
|
|
||||||
|
|
||||||
const deepmerge = require('deepmerge');
|
const deepmerge = require('deepmerge');
|
||||||
|
|
||||||
@ -129,7 +128,6 @@ export class ConfigServiceImpl
|
|||||||
return this.daemon.getVersion();
|
return this.daemon.getVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@duration()
|
|
||||||
protected async loadCliConfig(
|
protected async loadCliConfig(
|
||||||
initializeIfAbsent = true
|
initializeIfAbsent = true
|
||||||
): Promise<DefaultCliConfig | undefined> {
|
): Promise<DefaultCliConfig | undefined> {
|
||||||
|
@ -94,6 +94,11 @@ export class CoreClientProvider {
|
|||||||
return this.onClientDidRefreshEmitter.event;
|
return this.onClientDidRefreshEmitter.event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refresh(): Promise<void> {
|
||||||
|
const client = await this.client;
|
||||||
|
await this.initInstance(client);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates both the gRPC core client creation (`CreateRequest`) and initialization (`InitRequest`).
|
* Encapsulates both the gRPC core client creation (`CreateRequest`) and initialization (`InitRequest`).
|
||||||
*/
|
*/
|
||||||
@ -415,6 +420,10 @@ export abstract class CoreClientAware {
|
|||||||
protected get onClientDidRefresh(): Event<CoreClientProvider.Client> {
|
protected get onClientDidRefresh(): Event<CoreClientProvider.Client> {
|
||||||
return this.coreClientProvider.onClientDidRefresh;
|
return this.coreClientProvider.onClientDidRefresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh(): Promise<void> {
|
||||||
|
return this.coreClientProvider.refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IndexUpdateRequiredBeforeInitError extends Error {
|
class IndexUpdateRequiredBeforeInitError extends Error {
|
||||||
|
@ -11,14 +11,10 @@ import {
|
|||||||
SketchContainer,
|
SketchContainer,
|
||||||
} from '../common/protocol/sketches-service';
|
} from '../common/protocol/sketches-service';
|
||||||
import { ExamplesService } from '../common/protocol/examples-service';
|
import { ExamplesService } from '../common/protocol/examples-service';
|
||||||
import {
|
import { LibraryLocation, LibraryPackage } from '../common/protocol';
|
||||||
LibraryLocation,
|
|
||||||
LibraryPackage,
|
|
||||||
LibraryService,
|
|
||||||
} from '../common/protocol';
|
|
||||||
import { duration } from '../common/decorators';
|
|
||||||
import { URI } from '@theia/core/lib/common/uri';
|
import { URI } from '@theia/core/lib/common/uri';
|
||||||
import { Path } from '@theia/core/lib/common/path';
|
import { Path } from '@theia/core/lib/common/path';
|
||||||
|
import { LibraryServiceImpl } from './library-service-impl';
|
||||||
|
|
||||||
interface BuiltInSketchRef {
|
interface BuiltInSketchRef {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
@ -84,8 +80,8 @@ export class BuiltInExamplesServiceImpl {
|
|||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ExamplesServiceImpl implements ExamplesService {
|
export class ExamplesServiceImpl implements ExamplesService {
|
||||||
@inject(LibraryService)
|
@inject(LibraryServiceImpl)
|
||||||
private readonly libraryService: LibraryService;
|
private readonly libraryService: LibraryServiceImpl;
|
||||||
|
|
||||||
@inject(BuiltInExamplesServiceImpl)
|
@inject(BuiltInExamplesServiceImpl)
|
||||||
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
|
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
|
||||||
@ -94,7 +90,6 @@ export class ExamplesServiceImpl implements ExamplesService {
|
|||||||
return this.builtInExamplesService.builtIns();
|
return this.builtInExamplesService.builtIns();
|
||||||
}
|
}
|
||||||
|
|
||||||
@duration()
|
|
||||||
async installed({ fqbn }: { fqbn?: string }): Promise<{
|
async installed({ fqbn }: { fqbn?: string }): Promise<{
|
||||||
user: SketchContainer[];
|
user: SketchContainer[];
|
||||||
current: SketchContainer[];
|
current: SketchContainer[];
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||||
import * as fs from 'fs';
|
import { promises as fs, realpath, lstat, Stats, constants, rm } from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as temp from 'temp';
|
import * as temp from 'temp';
|
||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as glob from 'glob';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import * as PQueue from 'p-queue';
|
||||||
import { ncp } from 'ncp';
|
import { ncp } from 'ncp';
|
||||||
import { promisify } from 'util';
|
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { FileUri } from '@theia/core/lib/node';
|
import { ILogger } from '@theia/core/lib/common/logger';
|
||||||
|
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||||
import { ConfigServiceImpl } from './config-service-impl';
|
import { ConfigServiceImpl } from './config-service-impl';
|
||||||
import {
|
import {
|
||||||
SketchesService,
|
SketchesService,
|
||||||
@ -24,8 +25,6 @@ import {
|
|||||||
ArchiveSketchRequest,
|
ArchiveSketchRequest,
|
||||||
LoadSketchRequest,
|
LoadSketchRequest,
|
||||||
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
|
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
|
||||||
import { duration } from '../common/decorators';
|
|
||||||
import * as glob from 'glob';
|
|
||||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
import { ServiceError } from './service-error';
|
import { ServiceError } from './service-error';
|
||||||
import {
|
import {
|
||||||
@ -34,6 +33,8 @@ import {
|
|||||||
TempSketchPrefix,
|
TempSketchPrefix,
|
||||||
} from './is-temp-sketch';
|
} from './is-temp-sketch';
|
||||||
|
|
||||||
|
const RecentSketches = 'recent-sketches.json';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SketchesServiceImpl
|
export class SketchesServiceImpl
|
||||||
extends CoreClientAware
|
extends CoreClientAware
|
||||||
@ -41,6 +42,15 @@ export class SketchesServiceImpl
|
|||||||
{
|
{
|
||||||
private sketchSuffixIndex = 1;
|
private sketchSuffixIndex = 1;
|
||||||
private lastSketchBaseName: string;
|
private lastSketchBaseName: string;
|
||||||
|
private recentSketches: SketchWithDetails[] | undefined;
|
||||||
|
private readonly markAsRecentSketchQueue = new PQueue({
|
||||||
|
autoStart: true,
|
||||||
|
concurrency: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
@inject(ILogger)
|
||||||
|
@named('sketches-service')
|
||||||
|
private readonly logger: ILogger;
|
||||||
|
|
||||||
@inject(ConfigServiceImpl)
|
@inject(ConfigServiceImpl)
|
||||||
private readonly configService: ConfigServiceImpl;
|
private readonly configService: ConfigServiceImpl;
|
||||||
@ -54,28 +64,7 @@ export class SketchesServiceImpl
|
|||||||
@inject(IsTempSketch)
|
@inject(IsTempSketch)
|
||||||
private readonly isTempSketch: IsTempSketch;
|
private readonly isTempSketch: IsTempSketch;
|
||||||
|
|
||||||
async getSketches({
|
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
|
||||||
uri,
|
|
||||||
exclude,
|
|
||||||
}: {
|
|
||||||
uri?: string;
|
|
||||||
exclude?: string[];
|
|
||||||
}): Promise<SketchContainer> {
|
|
||||||
const [/*old,*/ _new] = await Promise.all([
|
|
||||||
// this.getSketchesOld({ uri, exclude }),
|
|
||||||
this.getSketchesNew({ uri, exclude }),
|
|
||||||
]);
|
|
||||||
return _new;
|
|
||||||
}
|
|
||||||
|
|
||||||
@duration()
|
|
||||||
async getSketchesNew({
|
|
||||||
uri,
|
|
||||||
exclude,
|
|
||||||
}: {
|
|
||||||
uri?: string;
|
|
||||||
exclude?: string[];
|
|
||||||
}): Promise<SketchContainer> {
|
|
||||||
const root = await this.root(uri);
|
const root = await this.root(uri);
|
||||||
const pathToAllSketchFiles = await new Promise<string[]>(
|
const pathToAllSketchFiles = await new Promise<string[]>(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
@ -138,7 +127,7 @@ export class SketchesServiceImpl
|
|||||||
for (const pathToSketchFile of pathToAllSketchFiles) {
|
for (const pathToSketchFile of pathToAllSketchFiles) {
|
||||||
const relative = path.relative(root, pathToSketchFile);
|
const relative = path.relative(root, pathToSketchFile);
|
||||||
if (!relative) {
|
if (!relative) {
|
||||||
console.warn(
|
this.logger.warn(
|
||||||
`Could not determine relative sketch path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Relative path was: ${relative}`
|
`Could not determine relative sketch path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Relative path was: ${relative}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@ -146,7 +135,7 @@ export class SketchesServiceImpl
|
|||||||
const segments = relative.split(path.sep);
|
const segments = relative.split(path.sep);
|
||||||
if (segments.length < 2) {
|
if (segments.length < 2) {
|
||||||
// folder name, and sketch name.
|
// folder name, and sketch name.
|
||||||
console.warn(
|
this.logger.warn(
|
||||||
`Expected at least one segment relative path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Segments were: ${segments}.`
|
`Expected at least one segment relative path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Segments were: ${segments}.`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@ -160,7 +149,7 @@ export class SketchesServiceImpl
|
|||||||
''
|
''
|
||||||
);
|
);
|
||||||
if (sketchFileExtension !== '.ino' && sketchFileExtension !== '.pde') {
|
if (sketchFileExtension !== '.ino' && sketchFileExtension !== '.pde') {
|
||||||
console.warn(
|
this.logger.warn(
|
||||||
`Mismatching sketch file <${sketchFilename}> and sketch folder name <${sketchName}>. Skipping`
|
`Mismatching sketch file <${sketchFilename}> and sketch folder name <${sketchName}>. Skipping`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@ -169,7 +158,7 @@ export class SketchesServiceImpl
|
|||||||
if (child) {
|
if (child) {
|
||||||
child.sketches.push({
|
child.sketches.push({
|
||||||
name: sketchName,
|
name: sketchName,
|
||||||
uri: FileUri.create(pathToSketchFile).toString(),
|
uri: FileUri.create(path.dirname(pathToSketchFile)).toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,8 +180,8 @@ export class SketchesServiceImpl
|
|||||||
const requestSketchPath = FileUri.fsPath(uri);
|
const requestSketchPath = FileUri.fsPath(uri);
|
||||||
req.setSketchPath(requestSketchPath);
|
req.setSketchPath(requestSketchPath);
|
||||||
req.setInstance(instance);
|
req.setInstance(instance);
|
||||||
const stat = new Deferred<fs.Stats | Error>();
|
const stat = new Deferred<Stats | Error>();
|
||||||
fs.lstat(requestSketchPath, (err, result) =>
|
lstat(requestSketchPath, (err, result) =>
|
||||||
err ? stat.resolve(err) : stat.resolve(result)
|
err ? stat.resolve(err) : stat.resolve(result)
|
||||||
);
|
);
|
||||||
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
|
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
|
||||||
@ -200,27 +189,20 @@ export class SketchesServiceImpl
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(
|
reject(
|
||||||
isNotFoundError(err)
|
isNotFoundError(err)
|
||||||
? SketchesError.NotFound(
|
? SketchesError.NotFound(err.details, uri)
|
||||||
fixErrorMessage(
|
|
||||||
err,
|
|
||||||
requestSketchPath,
|
|
||||||
this.configService.cliConfiguration?.directories.user
|
|
||||||
),
|
|
||||||
uri
|
|
||||||
)
|
|
||||||
: err
|
: err
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const responseSketchPath = maybeNormalizeDrive(resp.getLocationPath());
|
const responseSketchPath = maybeNormalizeDrive(resp.getLocationPath());
|
||||||
if (requestSketchPath !== responseSketchPath) {
|
if (requestSketchPath !== responseSketchPath) {
|
||||||
console.warn(
|
this.logger.warn(
|
||||||
`Warning! The request sketch path was different than the response sketch path from the CLI. This could be a potential bug. Request: <${requestSketchPath}>, response: <${responseSketchPath}>.`
|
`Warning! The request sketch path was different than the response sketch path from the CLI. This could be a potential bug. Request: <${requestSketchPath}>, response: <${responseSketchPath}>.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const resolvedStat = await stat.promise;
|
const resolvedStat = await stat.promise;
|
||||||
if (resolvedStat instanceof Error) {
|
if (resolvedStat instanceof Error) {
|
||||||
console.error(
|
this.logger.error(
|
||||||
`The CLI could load the sketch from ${requestSketchPath}, but stating the folder has failed.`
|
`The CLI could load the sketch from ${requestSketchPath}, but stating the folder has failed.`
|
||||||
);
|
);
|
||||||
reject(resolvedStat);
|
reject(resolvedStat);
|
||||||
@ -254,89 +236,160 @@ export class SketchesServiceImpl
|
|||||||
private get recentSketchesFsPath(): Promise<string> {
|
private get recentSketchesFsPath(): Promise<string> {
|
||||||
return this.envVariableServer
|
return this.envVariableServer
|
||||||
.getConfigDirUri()
|
.getConfigDirUri()
|
||||||
.then((uri) => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
|
.then((uri) => path.join(FileUri.fsPath(uri), RecentSketches));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadRecentSketches(
|
private async loadRecentSketches(): Promise<Record<string, number>> {
|
||||||
fsPath: string
|
this.logger.debug(`>>> Loading recently opened sketches data.`);
|
||||||
): Promise<Record<string, number>> {
|
const fsPath = await this.recentSketchesFsPath;
|
||||||
let data: Record<string, number> = {};
|
let data: Record<string, number> = {};
|
||||||
try {
|
try {
|
||||||
const raw = await promisify(fs.readFile)(fsPath, {
|
const raw = await fs.readFile(fsPath, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
data = JSON.parse(raw);
|
data = JSON.parse(raw);
|
||||||
} catch {}
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Could not parse recently opened sketches. Raw input was: ${raw}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if ('code' in err && err.code === 'ENOENT') {
|
||||||
|
this.logger.debug(
|
||||||
|
`<<< '${RecentSketches}' does not exist yet. This is normal behavior. Falling back to empty data.`
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
this.logger.debug(
|
||||||
|
`<<< Successfully loaded recently opened sketches data: ${JSON.stringify(
|
||||||
|
data
|
||||||
|
)}`
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async saveRecentSketches(
|
||||||
|
data: Record<string, number>
|
||||||
|
): Promise<void> {
|
||||||
|
this.logger.debug(
|
||||||
|
`>>> Saving recently opened sketches data: ${JSON.stringify(data)}`
|
||||||
|
);
|
||||||
|
const fsPath = await this.recentSketchesFsPath;
|
||||||
|
await fs.writeFile(fsPath, JSON.stringify(data, null, 2));
|
||||||
|
this.logger.debug('<<< Successfully saved recently opened sketches data.');
|
||||||
|
}
|
||||||
|
|
||||||
async markAsRecentlyOpened(uri: string): Promise<void> {
|
async markAsRecentlyOpened(uri: string): Promise<void> {
|
||||||
|
return this.markAsRecentSketchQueue.add(async () => {
|
||||||
|
this.logger.debug(`Marking sketch at '${uri}' as recently opened.`);
|
||||||
|
if (this.isTempSketch.is(FileUri.fsPath(uri))) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Sketch at '${uri}' is pointing to a temp location. Not marking as recently opened.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let sketch: Sketch | undefined = undefined;
|
let sketch: Sketch | undefined = undefined;
|
||||||
try {
|
try {
|
||||||
sketch = await this.loadSketch(uri);
|
sketch = await this.loadSketch(uri);
|
||||||
} catch {
|
this.logger.debug(
|
||||||
|
`Loaded sketch ${JSON.stringify(
|
||||||
|
sketch
|
||||||
|
)} before marking it as recently opened.`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Could not load sketch from '${uri}'. Not marking as recently opened.`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await this.isTemp(sketch)) {
|
this.logger.error(
|
||||||
return;
|
`Unexpected error occurred while loading sketch from '${uri}'.`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fsPath = await this.recentSketchesFsPath;
|
const data = await this.loadRecentSketches();
|
||||||
const data = await this.loadRecentSketches(fsPath);
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
this.logger.debug(
|
||||||
|
`Marking sketch '${uri}' as recently opened with timestamp: '${now}'.`
|
||||||
|
);
|
||||||
data[sketch.uri] = now;
|
data[sketch.uri] = now;
|
||||||
|
|
||||||
let toDeleteUri: string | undefined = undefined;
|
let toDelete: [string, number] | undefined = undefined;
|
||||||
if (Object.keys(data).length > 10) {
|
if (Object.keys(data).length > 10) {
|
||||||
let min = Number.MAX_SAFE_INTEGER;
|
let min = Number.MAX_SAFE_INTEGER;
|
||||||
for (const uri of Object.keys(data)) {
|
for (const [uri, timestamp] of Object.entries(data)) {
|
||||||
if (min > data[uri]) {
|
if (min > timestamp) {
|
||||||
min = data[uri];
|
min = data[uri];
|
||||||
toDeleteUri = uri;
|
toDelete = [uri, timestamp];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toDeleteUri) {
|
if (toDelete) {
|
||||||
|
const [toDeleteUri] = toDelete;
|
||||||
delete data[toDeleteUri];
|
delete data[toDeleteUri];
|
||||||
}
|
this.logger.debug(
|
||||||
|
`Deleted sketch entry ${JSON.stringify(
|
||||||
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
|
toDelete
|
||||||
this.recentlyOpenedSketches().then((sketches) =>
|
)} from recently opened.`
|
||||||
this.notificationService.notifyRecentSketchesDidChange({ sketches })
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async recentlyOpenedSketches(): Promise<Sketch[]> {
|
await this.saveRecentSketches(data);
|
||||||
const configDirUri = await this.envVariableServer.getConfigDirUri();
|
this.logger.debug(`Marked sketch '${uri}' as recently opened.`);
|
||||||
const fsPath = path.join(
|
const sketches = await this.recentlyOpenedSketches(data);
|
||||||
FileUri.fsPath(configDirUri),
|
this.notificationService.notifyRecentSketchesDidChange({ sketches });
|
||||||
'recent-sketches.json'
|
|
||||||
);
|
|
||||||
let data: Record<string, number> = {};
|
|
||||||
try {
|
|
||||||
const raw = await promisify(fs.readFile)(fsPath, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
});
|
||||||
data = JSON.parse(raw);
|
}
|
||||||
} catch {}
|
|
||||||
|
|
||||||
|
async recentlyOpenedSketches(
|
||||||
|
forceUpdate?: Record<string, number> | boolean
|
||||||
|
): Promise<Sketch[]> {
|
||||||
|
if (!this.recentSketches || forceUpdate) {
|
||||||
|
const data =
|
||||||
|
forceUpdate && typeof forceUpdate === 'object'
|
||||||
|
? forceUpdate
|
||||||
|
: await this.loadRecentSketches();
|
||||||
const sketches: SketchWithDetails[] = [];
|
const sketches: SketchWithDetails[] = [];
|
||||||
|
let needsUpdate = false;
|
||||||
for (const uri of Object.keys(data).sort(
|
for (const uri of Object.keys(data).sort(
|
||||||
(left, right) => data[right] - data[left]
|
(left, right) => data[right] - data[left]
|
||||||
)) {
|
)) {
|
||||||
|
let sketch: SketchWithDetails | undefined = undefined;
|
||||||
try {
|
try {
|
||||||
const sketch = await this.loadSketch(uri);
|
sketch = await this.loadSketch(uri);
|
||||||
sketches.push(sketch);
|
|
||||||
} catch {}
|
} catch {}
|
||||||
|
if (!sketch) {
|
||||||
|
needsUpdate = true;
|
||||||
|
} else {
|
||||||
|
sketches.push(sketch);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return sketches;
|
if (needsUpdate) {
|
||||||
|
const data = sketches.reduce((acc, curr) => {
|
||||||
|
acc[curr.uri] = curr.mtimeMs;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
await this.saveRecentSketches(data);
|
||||||
|
this.notificationService.notifyRecentSketchesDidChange({ sketches });
|
||||||
|
}
|
||||||
|
this.recentSketches = sketches;
|
||||||
|
}
|
||||||
|
return this.recentSketches;
|
||||||
}
|
}
|
||||||
|
|
||||||
async cloneExample(uri: string): Promise<Sketch> {
|
async cloneExample(uri: string): Promise<Sketch> {
|
||||||
const sketch = await this.loadSketch(uri);
|
const [sketch, parentPath] = await Promise.all([
|
||||||
const parentPath = await this.createTempFolder();
|
this.loadSketch(uri),
|
||||||
|
this.createTempFolder(),
|
||||||
|
]);
|
||||||
const destinationUri = FileUri.create(
|
const destinationUri = FileUri.create(
|
||||||
path.join(parentPath, sketch.name)
|
path.join(parentPath, sketch.name)
|
||||||
).toString();
|
).toString();
|
||||||
@ -377,7 +430,7 @@ export class SketchesServiceImpl
|
|||||||
this.sketchSuffixIndex++
|
this.sketchSuffixIndex++
|
||||||
)}`;
|
)}`;
|
||||||
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
|
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
|
||||||
const sketchExists = await promisify(fs.exists)(
|
const sketchExists = await this.exists(
|
||||||
path.join(sketchbookPath, sketchNameCandidate)
|
path.join(sketchbookPath, sketchNameCandidate)
|
||||||
);
|
);
|
||||||
if (!sketchExists) {
|
if (!sketchExists) {
|
||||||
@ -393,8 +446,8 @@ export class SketchesServiceImpl
|
|||||||
|
|
||||||
const sketchDir = path.join(parentPath, sketchName);
|
const sketchDir = path.join(parentPath, sketchName);
|
||||||
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
|
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
|
||||||
await promisify(fs.mkdir)(sketchDir, { recursive: true });
|
await fs.mkdir(sketchDir, { recursive: true });
|
||||||
await promisify(fs.writeFile)(
|
await fs.writeFile(
|
||||||
sketchFile,
|
sketchFile,
|
||||||
`void setup() {
|
`void setup() {
|
||||||
// put your setup code here, to run once:
|
// put your setup code here, to run once:
|
||||||
@ -424,7 +477,7 @@ void loop() {
|
|||||||
reject(createError);
|
reject(createError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fs.realpath.native(dirPath, (resolveError, resolvedDirPath) => {
|
realpath.native(dirPath, (resolveError, resolvedDirPath) => {
|
||||||
if (resolveError) {
|
if (resolveError) {
|
||||||
reject(resolveError);
|
reject(resolveError);
|
||||||
return;
|
return;
|
||||||
@ -478,7 +531,7 @@ void loop() {
|
|||||||
{ destinationUri }: { destinationUri: string }
|
{ destinationUri }: { destinationUri: string }
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const source = FileUri.fsPath(sketch.uri);
|
const source = FileUri.fsPath(sketch.uri);
|
||||||
const exists = await promisify(fs.exists)(source);
|
const exists = await this.exists(source);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new Error(`Sketch does not exist: ${sketch}`);
|
throw new Error(`Sketch does not exist: ${sketch}`);
|
||||||
}
|
}
|
||||||
@ -503,7 +556,7 @@ void loop() {
|
|||||||
);
|
);
|
||||||
const newPath = path.join(destinationPath, `${newName}.ino`);
|
const newPath = path.join(destinationPath, `${newName}.ino`);
|
||||||
if (oldPath !== newPath) {
|
if (oldPath !== newPath) {
|
||||||
await promisify(fs.rename)(oldPath, newPath);
|
await fs.rename(oldPath, newPath);
|
||||||
}
|
}
|
||||||
await this.loadSketch(FileUri.create(destinationPath).toString()); // Sanity check.
|
await this.loadSketch(FileUri.create(destinationPath).toString()); // Sanity check.
|
||||||
resolve();
|
resolve();
|
||||||
@ -520,7 +573,7 @@ void loop() {
|
|||||||
const destination = FileUri.fsPath(destinationUri);
|
const destination = FileUri.fsPath(destinationUri);
|
||||||
let tempDestination = await this.createTempFolder();
|
let tempDestination = await this.createTempFolder();
|
||||||
tempDestination = path.join(tempDestination, sketch.name);
|
tempDestination = path.join(tempDestination, sketch.name);
|
||||||
await fs.promises.mkdir(tempDestination, { recursive: true });
|
await fs.mkdir(tempDestination, { recursive: true });
|
||||||
await copy(source, tempDestination);
|
await copy(source, tempDestination);
|
||||||
await copy(tempDestination, destination);
|
await copy(tempDestination, destination);
|
||||||
return FileUri.create(destination).toString();
|
return FileUri.create(destination).toString();
|
||||||
@ -531,8 +584,8 @@ void loop() {
|
|||||||
const { client } = await this.coreClient;
|
const { client } = await this.coreClient;
|
||||||
const archivePath = FileUri.fsPath(destinationUri);
|
const archivePath = FileUri.fsPath(destinationUri);
|
||||||
// The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
|
// The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
|
||||||
if (await promisify(fs.exists)(archivePath)) {
|
if (await this.exists(archivePath)) {
|
||||||
await promisify(fs.unlink)(archivePath);
|
await fs.unlink(archivePath);
|
||||||
}
|
}
|
||||||
const req = new ArchiveSketchRequest();
|
const req = new ArchiveSketchRequest();
|
||||||
req.setSketchPath(FileUri.fsPath(sketch.uri));
|
req.setSketchPath(FileUri.fsPath(sketch.uri));
|
||||||
@ -556,7 +609,7 @@ void loop() {
|
|||||||
|
|
||||||
async getIdeTempFolderPath(sketch: Sketch): Promise<string> {
|
async getIdeTempFolderPath(sketch: Sketch): Promise<string> {
|
||||||
const sketchPath = FileUri.fsPath(sketch.uri);
|
const sketchPath = FileUri.fsPath(sketch.uri);
|
||||||
await fs.promises.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
|
await fs.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
|
||||||
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');
|
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');
|
||||||
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
|
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
|
||||||
}
|
}
|
||||||
@ -564,53 +617,32 @@ void loop() {
|
|||||||
async deleteSketch(sketch: Sketch): Promise<void> {
|
async deleteSketch(sketch: Sketch): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const sketchPath = FileUri.fsPath(sketch.uri);
|
const sketchPath = FileUri.fsPath(sketch.uri);
|
||||||
fs.rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
|
rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`Failed to delete sketch at ${sketchPath}.`, error);
|
this.logger.error(`Failed to delete sketch at ${sketchPath}.`, error);
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Successfully deleted sketch at ${sketchPath}.`);
|
this.logger.info(`Successfully deleted sketch at ${sketchPath}.`);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async exists(pathLike: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(pathLike, constants.R_OK | constants.W_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SketchWithDetails extends Sketch {
|
interface SketchWithDetails extends Sketch {
|
||||||
readonly mtimeMs: number;
|
readonly mtimeMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/arduino/arduino-cli/issues/1797
|
|
||||||
function fixErrorMessage(
|
|
||||||
err: ServiceError,
|
|
||||||
sketchPath: string,
|
|
||||||
sketchbookPath: string | undefined
|
|
||||||
): string {
|
|
||||||
if (!sketchbookPath) {
|
|
||||||
return err.details; // No way to repair the error message. The current sketchbook path is not available.
|
|
||||||
}
|
|
||||||
// Original: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing /Users/a.kitta/Documents/Arduino/Arduino.ino`
|
|
||||||
// Fixed: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing $sketchPath`
|
|
||||||
const message = err.details;
|
|
||||||
const incorrectMessageSuffix = path.join(sketchbookPath, 'Arduino.ino');
|
|
||||||
if (
|
|
||||||
message.startsWith("Can't open sketch: no valid sketch found in") &&
|
|
||||||
message.endsWith(`${incorrectMessageSuffix}`)
|
|
||||||
) {
|
|
||||||
const sketchName = path.basename(sketchPath);
|
|
||||||
const correctMessagePrefix = message.substring(
|
|
||||||
0,
|
|
||||||
message.length - incorrectMessageSuffix.length
|
|
||||||
);
|
|
||||||
return `${correctMessagePrefix}${path.join(
|
|
||||||
sketchPath,
|
|
||||||
`${sketchName}.ino`
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
return err.details;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNotFoundError(err: unknown): err is ServiceError {
|
function isNotFoundError(err: unknown): err is ServiceError {
|
||||||
return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user