mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-05 19:56:34 +00:00
Added the Sketchbook
menu with FS event tracking
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
1b6d9eccdc
commit
db2967084f
@ -130,6 +130,7 @@ import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-de
|
||||
import { Debug } from './contributions/debug';
|
||||
import { DebugSessionManager } from './theia/debug/debug-session-manager';
|
||||
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
|
||||
import { Sketchbook } from './contributions/sketchbook';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
@ -331,6 +332,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, IncludeLibrary);
|
||||
Contribution.configure(bind, About);
|
||||
Contribution.configure(bind, Debug);
|
||||
Contribution.configure(bind, Sketchbook);
|
||||
|
||||
bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => {
|
||||
WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService);
|
||||
|
@ -0,0 +1,69 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
|
||||
@injectable()
|
||||
export class Sketchbook extends SketchContribution {
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected toDisposePerSketch = new Map<string, DisposableCollection>();
|
||||
|
||||
onStart(): void {
|
||||
this.sketchService.getSketches().then(sketches => {
|
||||
this.register(sketches);
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
this.notificationCenter.onSketchbookChanged(({ created, removed }) => {
|
||||
this.unregister(removed);
|
||||
this.register(created);
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' });
|
||||
}
|
||||
|
||||
protected register(sketches: Sketch[]): void {
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const toDispose = this.toDisposePerSketch.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
const command = { id: `arduino-sketchbook-open--${uri}` };
|
||||
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, { commandId: command.id, label: sketch.name });
|
||||
this.toDisposePerSketch.set(sketch.uri, new DisposableCollection(
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
|
||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
protected unregister(sketches: Sketch[]): void {
|
||||
for (const { uri } of sketches) {
|
||||
const toDispose = this.toDisposePerSketch.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -12,8 +12,11 @@ export namespace ArduinoMenus {
|
||||
export const FILE__SETTINGS_GROUP = [...(isOSX ? MAIN_MENU_BAR : CommonMenus.FILE), '2_settings'];
|
||||
export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit'];
|
||||
|
||||
// -- File / Sketchbook
|
||||
export const FILE__SKETCHBOOK_SUBMENU = [...FILE__SKETCH_GROUP, '0_sketchbook'];
|
||||
|
||||
// -- File / Examples
|
||||
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '0_examples'];
|
||||
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '1_examples'];
|
||||
export const EXAMPLES__BUILT_IN_GROUP = [...FILE__EXAMPLES_SUBMENU, '0_built_ins'];
|
||||
export const EXAMPLES__ANY_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '1_any_board'];
|
||||
export const EXAMPLES__CURRENT_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '2_current_board'];
|
||||
|
@ -4,7 +4,7 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { NotificationServiceClient, NotificationServiceServer } from '../common/protocol/notification-service';
|
||||
import { AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config } from '../common/protocol';
|
||||
import { AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config, Sketch } from '../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class NotificationCenter implements NotificationServiceClient, FrontendApplicationContribution {
|
||||
@ -21,6 +21,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
|
||||
protected readonly libraryInstalledEmitter = new Emitter<{ item: LibraryPackage }>();
|
||||
protected readonly libraryUninstalledEmitter = new Emitter<{ item: LibraryPackage }>();
|
||||
protected readonly attachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly sketchbookChangedEmitter = new Emitter<{ created: Sketch[], removed: Sketch[] }>();
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.indexUpdatedEmitter,
|
||||
@ -31,7 +32,8 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
|
||||
this.platformUninstalledEmitter,
|
||||
this.libraryInstalledEmitter,
|
||||
this.libraryUninstalledEmitter,
|
||||
this.attachedBoardsChangedEmitter
|
||||
this.attachedBoardsChangedEmitter,
|
||||
this.sketchbookChangedEmitter
|
||||
);
|
||||
|
||||
readonly onIndexUpdated = this.indexUpdatedEmitter.event;
|
||||
@ -43,6 +45,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
|
||||
readonly onLibraryInstalled = this.libraryInstalledEmitter.event;
|
||||
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
|
||||
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
|
||||
readonly onSketchbookChanged = this.sketchbookChangedEmitter.event;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
@ -89,4 +92,8 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
|
||||
this.attachedBoardsChangedEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void {
|
||||
this.sketchbookChangedEmitter.fire(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
|
||||
CommonCommands.OPEN_PREFERENCES,
|
||||
CommonCommands.SELECT_ICON_THEME,
|
||||
CommonCommands.SELECT_COLOR_THEME,
|
||||
CommonCommands.ABOUT_COMMAND
|
||||
CommonCommands.ABOUT_COMMAND,
|
||||
CommonCommands.SAVE_WITHOUT_FORMATTING // Patched for https://github.com/eclipse-theia/theia/pull/8877
|
||||
]) {
|
||||
registry.unregisterMenuAction(command);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { LibraryPackage } from './library-service';
|
||||
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { BoardsPackage, AttachedBoardsChangeEvent } from './boards-service';
|
||||
import { Config } from './config-service';
|
||||
import { Sketch, Config, BoardsPackage, AttachedBoardsChangeEvent } from '../protocol';
|
||||
|
||||
export interface NotificationServiceClient {
|
||||
notifyIndexUpdated(): void;
|
||||
@ -13,6 +12,7 @@ export interface NotificationServiceClient {
|
||||
notifyLibraryInstalled(event: { item: LibraryPackage }): void;
|
||||
notifyLibraryUninstalled(event: { item: LibraryPackage }): void;
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
|
||||
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void;
|
||||
}
|
||||
|
||||
export const NotificationServicePath = '/services/notification-service';
|
||||
|
@ -75,7 +75,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bindBackendService(LibraryServicePath, LibraryService);
|
||||
}));
|
||||
|
||||
// Shred sketches service
|
||||
// Shared sketches service
|
||||
bind(SketchesServiceImpl).toSelf().inSingletonScope();
|
||||
bind(SketchesService).toService(SketchesServiceImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { NotificationServiceServer, NotificationServiceClient, AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config } from '../common/protocol';
|
||||
import { NotificationServiceServer, NotificationServiceClient, AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config, Sketch } from '../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class NotificationServiceServerImpl implements NotificationServiceServer {
|
||||
@ -42,6 +42,10 @@ export class NotificationServiceServerImpl implements NotificationServiceServer
|
||||
this.clients.forEach(client => client.notifyConfigChanged(event));
|
||||
}
|
||||
|
||||
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void {
|
||||
this.clients.forEach(client => client.notifySketchbookChanged(event));
|
||||
}
|
||||
|
||||
setClient(client: NotificationServiceClient): void {
|
||||
this.clients.push(client);
|
||||
}
|
||||
|
@ -2,16 +2,18 @@ import { injectable, inject } from 'inversify';
|
||||
import * as os from 'os';
|
||||
import * as temp from 'temp';
|
||||
import * as path from 'path';
|
||||
import * as nsfw from 'nsfw';
|
||||
import { ncp } from 'ncp';
|
||||
import { Stats } from 'fs';
|
||||
import * as fs from './fs-extra';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileUri } from '@theia/core/lib/node';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { isWindows } from '@theia/core/lib/common/os';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { SketchesService, Sketch } from '../common/protocol/sketches-service';
|
||||
import { firstToLowerCase } from '../common/utils';
|
||||
|
||||
import { NotificationServiceServerImpl } from './notification-service-server';
|
||||
|
||||
// As currently implemented on Linux,
|
||||
// the maximum number of symbolic links that will be followed while resolving a pathname is 40
|
||||
@ -28,8 +30,10 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(NotificationServiceServerImpl)
|
||||
protected readonly notificationService: NotificationServiceServerImpl;
|
||||
|
||||
async getSketches(uri?: string): Promise<Sketch[]> {
|
||||
const sketches: Array<Sketch & { mtimeMs: number }> = [];
|
||||
let fsPath: undefined | string;
|
||||
if (!uri) {
|
||||
const { sketchDirUri } = await this.configService.getConfiguration();
|
||||
@ -43,9 +47,62 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
if (!fs.existsSync(fsPath)) {
|
||||
return [];
|
||||
}
|
||||
const fileNames = await fs.readdir(fsPath);
|
||||
for (const fileName of fileNames) {
|
||||
const filePath = path.join(fsPath, fileName);
|
||||
const stat = await fs.stat(fsPath);
|
||||
if (!stat.isDirectory()) {
|
||||
return [];
|
||||
}
|
||||
return this.doGetSketches(fsPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dev note: The keys are filesystem paths, not URI strings.
|
||||
*/
|
||||
private sketchbooks = new Map<string, Sketch[] | Deferred<Sketch[]>>();
|
||||
private fireSoonHandle?: NodeJS.Timer;
|
||||
private bufferedSketchbookEvents: { type: 'created' | 'removed', sketch: Sketch }[] = [];
|
||||
|
||||
private fireSoon(type: 'created' | 'removed', sketch: Sketch): void {
|
||||
this.bufferedSketchbookEvents.push({ type, sketch });
|
||||
|
||||
if (this.fireSoonHandle) {
|
||||
clearTimeout(this.fireSoonHandle);
|
||||
}
|
||||
|
||||
this.fireSoonHandle = setTimeout(() => {
|
||||
const event: { created: Sketch[], removed: Sketch[] } = {
|
||||
created: [],
|
||||
removed: []
|
||||
};
|
||||
for (const { type, sketch } of this.bufferedSketchbookEvents) {
|
||||
if (type === 'created') {
|
||||
event.created.push(sketch);
|
||||
} else {
|
||||
event.removed.push(sketch);
|
||||
}
|
||||
}
|
||||
this.notificationService.notifySketchbookChanged(event);
|
||||
this.bufferedSketchbookEvents.length = 0;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes the `fsPath` points to an existing directory.
|
||||
*/
|
||||
private async doGetSketches(sketchbookPath: string): Promise<Sketch[]> {
|
||||
const resolvedSketches = this.sketchbooks.get(sketchbookPath);
|
||||
if (resolvedSketches) {
|
||||
if (Array.isArray(resolvedSketches)) {
|
||||
return resolvedSketches;
|
||||
}
|
||||
return resolvedSketches.promise;
|
||||
}
|
||||
|
||||
const deferred = new Deferred<Sketch[]>();
|
||||
this.sketchbooks.set(sketchbookPath, deferred);
|
||||
const sketches: Array<Sketch & { mtimeMs: number }> = [];
|
||||
const filenames = await fs.readdir(sketchbookPath);
|
||||
for (const fileName of filenames) {
|
||||
const filePath = path.join(sketchbookPath, fileName);
|
||||
if (await this.isSketchFolder(FileUri.create(filePath).toString())) {
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
@ -59,7 +116,84 @@ export class SketchesServiceImpl implements SketchesService {
|
||||
}
|
||||
}
|
||||
}
|
||||
return sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
const deleteSketch = (toDelete: Sketch & { mtimeMs: number }) => {
|
||||
const index = sketches.indexOf(toDelete);
|
||||
if (index !== -1) {
|
||||
console.log(`Sketch '${toDelete.name}' was removed from sketchbook '${sketchbookPath}'.`);
|
||||
sketches.splice(index, 1);
|
||||
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
this.fireSoon('removed', toDelete);
|
||||
}
|
||||
};
|
||||
const createSketch = async (path: string) => {
|
||||
try {
|
||||
const [stat, sketch] = await Promise.all([
|
||||
fs.stat(path),
|
||||
this.loadSketch(path)
|
||||
]);
|
||||
console.log(`New sketch '${sketch.name}' was crated in sketchbook '${sketchbookPath}'.`);
|
||||
sketches.push({ ...sketch, mtimeMs: stat.mtimeMs });
|
||||
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
this.fireSoon('created', sketch);
|
||||
} catch { }
|
||||
};
|
||||
const watcher = await nsfw(sketchbookPath, async (events: any) => {
|
||||
// We track `.ino` files changes only.
|
||||
for (const event of events) {
|
||||
switch (event.action) {
|
||||
case nsfw.ActionType.CREATED:
|
||||
if (event.file.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.file === `${path.basename(event.directory)}.ino`) {
|
||||
createSketch(event.directory);
|
||||
}
|
||||
break;
|
||||
case nsfw.ActionType.DELETED:
|
||||
let sketch: Sketch & { mtimeMs: number } | undefined = undefined
|
||||
// Deleting the `ino` file.
|
||||
if (event.file.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.file === `${path.basename(event.directory)}.ino`) {
|
||||
sketch = sketches.find(sketch => FileUri.fsPath(sketch.uri) === event.directory);
|
||||
} else if (event.directory === sketchbookPath) { // Deleting the sketch (or any folder folder in the sketchbook).
|
||||
sketch = sketches.find(sketch => FileUri.fsPath(sketch.uri) === path.join(event.directory, event.file));
|
||||
}
|
||||
if (sketch) {
|
||||
deleteSketch(sketch);
|
||||
}
|
||||
break;
|
||||
case nsfw.ActionType.RENAMED:
|
||||
let sketchToDelete: Sketch & { mtimeMs: number } | undefined = undefined
|
||||
// When renaming with the Java IDE we got an event where `directory` is the sketchbook and `oldFile` is the sketch.
|
||||
if (event.directory === sketchbookPath) {
|
||||
sketchToDelete = sketches.find(sketch => FileUri.fsPath(sketch.uri) === path.join(event.directory, event.oldFile));
|
||||
}
|
||||
|
||||
if (sketchToDelete) {
|
||||
deleteSketch(sketchToDelete);
|
||||
} else {
|
||||
// If it's not a deletion, check for creation. The `directory` is the new sketch and the `newFile` is the new `ino` file.
|
||||
// tslint:disable-next-line:max-line-length
|
||||
if (event.newFile.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.newFile === `${path.basename(event.directory)}.ino`) {
|
||||
createSketch(event.directory);
|
||||
} else {
|
||||
// When renaming the `ino` file directly on the filesystem. The `directory` is the sketch and `newFile` and `oldFile` is the `ino` file.
|
||||
// tslint:disable-next-line:max-line-length
|
||||
if (event.oldFile.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.oldFile === `${path.basename(event.directory)}.ino`) {
|
||||
sketchToDelete = sketches.find(sketch => FileUri.fsPath(sketch.uri) === event.directory, event.oldFile);
|
||||
}
|
||||
if (sketchToDelete) {
|
||||
deleteSketch(sketchToDelete);
|
||||
} else if (event.directory === sketchbookPath) {
|
||||
createSketch(path.join(event.directory, event.newFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
await watcher.start();
|
||||
deferred.resolve(sketches);
|
||||
this.sketchbooks.set(sketchbookPath, sketches);
|
||||
return sketches;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,6 +26,7 @@
|
||||
"src"
|
||||
],
|
||||
"files": [
|
||||
"../node_modules/@theia/core/src/typings/nsfw/index.d.ts",
|
||||
"../node_modules/@theia/monaco/src/typings/monaco/index.d.ts"
|
||||
]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user