Skip to content

Commit db29670

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
Added the Sketchbook menu with FS event tracking
Signed-off-by: Akos Kitta <[email protected]>
1 parent 1b6d9ec commit db29670

10 files changed

+235
-14
lines changed

Diff for: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-de
130130
import { Debug } from './contributions/debug';
131131
import { DebugSessionManager } from './theia/debug/debug-session-manager';
132132
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
133+
import { Sketchbook } from './contributions/sketchbook';
133134

134135
const ElementQueries = require('css-element-queries/src/ElementQueries');
135136

@@ -331,6 +332,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
331332
Contribution.configure(bind, IncludeLibrary);
332333
Contribution.configure(bind, About);
333334
Contribution.configure(bind, Debug);
335+
Contribution.configure(bind, Sketchbook);
334336

335337
bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => {
336338
WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { inject, injectable } from 'inversify';
2+
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
3+
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
4+
import { ArduinoMenus } from '../menu/arduino-menus';
5+
import { MainMenuManager } from '../../common/main-menu-manager';
6+
import { NotificationCenter } from '../notification-center';
7+
import { OpenSketch } from './open-sketch';
8+
9+
@injectable()
10+
export class Sketchbook extends SketchContribution {
11+
12+
@inject(CommandRegistry)
13+
protected readonly commandRegistry: CommandRegistry;
14+
15+
@inject(MenuModelRegistry)
16+
protected readonly menuRegistry: MenuModelRegistry;
17+
18+
@inject(MainMenuManager)
19+
protected readonly mainMenuManager: MainMenuManager;
20+
21+
@inject(NotificationCenter)
22+
protected readonly notificationCenter: NotificationCenter;
23+
24+
protected toDisposePerSketch = new Map<string, DisposableCollection>();
25+
26+
onStart(): void {
27+
this.sketchService.getSketches().then(sketches => {
28+
this.register(sketches);
29+
this.mainMenuManager.update();
30+
});
31+
this.notificationCenter.onSketchbookChanged(({ created, removed }) => {
32+
this.unregister(removed);
33+
this.register(created);
34+
this.mainMenuManager.update();
35+
});
36+
}
37+
38+
registerMenus(registry: MenuModelRegistry): void {
39+
registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' });
40+
}
41+
42+
protected register(sketches: Sketch[]): void {
43+
for (const sketch of sketches) {
44+
const { uri } = sketch;
45+
const toDispose = this.toDisposePerSketch.get(uri);
46+
if (toDispose) {
47+
toDispose.dispose();
48+
}
49+
const command = { id: `arduino-sketchbook-open--${uri}` };
50+
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
51+
this.commandRegistry.registerCommand(command, handler);
52+
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, { commandId: command.id, label: sketch.name });
53+
this.toDisposePerSketch.set(sketch.uri, new DisposableCollection(
54+
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
55+
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
56+
));
57+
}
58+
}
59+
60+
protected unregister(sketches: Sketch[]): void {
61+
for (const { uri } of sketches) {
62+
const toDispose = this.toDisposePerSketch.get(uri);
63+
if (toDispose) {
64+
toDispose.dispose();
65+
}
66+
}
67+
}
68+
69+
}

Diff for: arduino-ide-extension/src/browser/menu/arduino-menus.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ export namespace ArduinoMenus {
1212
export const FILE__SETTINGS_GROUP = [...(isOSX ? MAIN_MENU_BAR : CommonMenus.FILE), '2_settings'];
1313
export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit'];
1414

15+
// -- File / Sketchbook
16+
export const FILE__SKETCHBOOK_SUBMENU = [...FILE__SKETCH_GROUP, '0_sketchbook'];
17+
1518
// -- File / Examples
16-
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '0_examples'];
19+
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '1_examples'];
1720
export const EXAMPLES__BUILT_IN_GROUP = [...FILE__EXAMPLES_SUBMENU, '0_built_ins'];
1821
export const EXAMPLES__ANY_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '1_any_board'];
1922
export const EXAMPLES__CURRENT_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '2_current_board'];

Diff for: arduino-ide-extension/src/browser/notification-center.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
44
import { DisposableCollection } from '@theia/core/lib/common/disposable';
55
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
66
import { NotificationServiceClient, NotificationServiceServer } from '../common/protocol/notification-service';
7-
import { AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config } from '../common/protocol';
7+
import { AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config, Sketch } from '../common/protocol';
88

99
@injectable()
1010
export class NotificationCenter implements NotificationServiceClient, FrontendApplicationContribution {
@@ -21,6 +21,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
2121
protected readonly libraryInstalledEmitter = new Emitter<{ item: LibraryPackage }>();
2222
protected readonly libraryUninstalledEmitter = new Emitter<{ item: LibraryPackage }>();
2323
protected readonly attachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
24+
protected readonly sketchbookChangedEmitter = new Emitter<{ created: Sketch[], removed: Sketch[] }>();
2425

2526
protected readonly toDispose = new DisposableCollection(
2627
this.indexUpdatedEmitter,
@@ -31,7 +32,8 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
3132
this.platformUninstalledEmitter,
3233
this.libraryInstalledEmitter,
3334
this.libraryUninstalledEmitter,
34-
this.attachedBoardsChangedEmitter
35+
this.attachedBoardsChangedEmitter,
36+
this.sketchbookChangedEmitter
3537
);
3638

3739
readonly onIndexUpdated = this.indexUpdatedEmitter.event;
@@ -43,6 +45,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
4345
readonly onLibraryInstalled = this.libraryInstalledEmitter.event;
4446
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
4547
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
48+
readonly onSketchbookChanged = this.sketchbookChangedEmitter.event;
4649

4750
@postConstruct()
4851
protected init(): void {
@@ -89,4 +92,8 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
8992
this.attachedBoardsChangedEmitter.fire(event);
9093
}
9194

95+
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void {
96+
this.sketchbookChangedEmitter.fire(event);
97+
}
98+
9299
}

Diff for: arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
2020
CommonCommands.OPEN_PREFERENCES,
2121
CommonCommands.SELECT_ICON_THEME,
2222
CommonCommands.SELECT_COLOR_THEME,
23-
CommonCommands.ABOUT_COMMAND
23+
CommonCommands.ABOUT_COMMAND,
24+
CommonCommands.SAVE_WITHOUT_FORMATTING // Patched for https://github.com/eclipse-theia/theia/pull/8877
2425
]) {
2526
registry.unregisterMenuAction(command);
2627
}

Diff for: arduino-ide-extension/src/common/protocol/notification-service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { LibraryPackage } from './library-service';
22
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
3-
import { BoardsPackage, AttachedBoardsChangeEvent } from './boards-service';
4-
import { Config } from './config-service';
3+
import { Sketch, Config, BoardsPackage, AttachedBoardsChangeEvent } from '../protocol';
54

65
export interface NotificationServiceClient {
76
notifyIndexUpdated(): void;
@@ -13,6 +12,7 @@ export interface NotificationServiceClient {
1312
notifyLibraryInstalled(event: { item: LibraryPackage }): void;
1413
notifyLibraryUninstalled(event: { item: LibraryPackage }): void;
1514
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
15+
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void;
1616
}
1717

1818
export const NotificationServicePath = '/services/notification-service';

Diff for: arduino-ide-extension/src/node/arduino-ide-backend-module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
7575
bindBackendService(LibraryServicePath, LibraryService);
7676
}));
7777

78-
// Shred sketches service
78+
// Shared sketches service
7979
bind(SketchesServiceImpl).toSelf().inSingletonScope();
8080
bind(SketchesService).toService(SketchesServiceImpl);
8181
bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(SketchesServicePath, () => context.container.get(SketchesService))).inSingletonScope();

Diff for: arduino-ide-extension/src/node/notification-service-server.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { injectable } from 'inversify';
2-
import { NotificationServiceServer, NotificationServiceClient, AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config } from '../common/protocol';
2+
import { NotificationServiceServer, NotificationServiceClient, AttachedBoardsChangeEvent, BoardsPackage, LibraryPackage, Config, Sketch } from '../common/protocol';
33

44
@injectable()
55
export class NotificationServiceServerImpl implements NotificationServiceServer {
@@ -42,6 +42,10 @@ export class NotificationServiceServerImpl implements NotificationServiceServer
4242
this.clients.forEach(client => client.notifyConfigChanged(event));
4343
}
4444

45+
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void {
46+
this.clients.forEach(client => client.notifySketchbookChanged(event));
47+
}
48+
4549
setClient(client: NotificationServiceClient): void {
4650
this.clients.push(client);
4751
}

Diff for: arduino-ide-extension/src/node/sketches-service-impl.ts

+140-6
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@ import { injectable, inject } from 'inversify';
22
import * as os from 'os';
33
import * as temp from 'temp';
44
import * as path from 'path';
5+
import * as nsfw from 'nsfw';
56
import { ncp } from 'ncp';
67
import { Stats } from 'fs';
78
import * as fs from './fs-extra';
89
import URI from '@theia/core/lib/common/uri';
910
import { FileUri } from '@theia/core/lib/node';
11+
import { Deferred } from '@theia/core/lib/common/promise-util';
1012
import { isWindows } from '@theia/core/lib/common/os';
1113
import { ConfigService } from '../common/protocol/config-service';
1214
import { SketchesService, Sketch } from '../common/protocol/sketches-service';
1315
import { firstToLowerCase } from '../common/utils';
14-
16+
import { NotificationServiceServerImpl } from './notification-service-server';
1517

1618
// As currently implemented on Linux,
1719
// 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 {
2830
@inject(ConfigService)
2931
protected readonly configService: ConfigService;
3032

33+
@inject(NotificationServiceServerImpl)
34+
protected readonly notificationService: NotificationServiceServerImpl;
35+
3136
async getSketches(uri?: string): Promise<Sketch[]> {
32-
const sketches: Array<Sketch & { mtimeMs: number }> = [];
3337
let fsPath: undefined | string;
3438
if (!uri) {
3539
const { sketchDirUri } = await this.configService.getConfiguration();
@@ -43,9 +47,62 @@ export class SketchesServiceImpl implements SketchesService {
4347
if (!fs.existsSync(fsPath)) {
4448
return [];
4549
}
46-
const fileNames = await fs.readdir(fsPath);
47-
for (const fileName of fileNames) {
48-
const filePath = path.join(fsPath, fileName);
50+
const stat = await fs.stat(fsPath);
51+
if (!stat.isDirectory()) {
52+
return [];
53+
}
54+
return this.doGetSketches(fsPath);
55+
}
56+
57+
/**
58+
* Dev note: The keys are filesystem paths, not URI strings.
59+
*/
60+
private sketchbooks = new Map<string, Sketch[] | Deferred<Sketch[]>>();
61+
private fireSoonHandle?: NodeJS.Timer;
62+
private bufferedSketchbookEvents: { type: 'created' | 'removed', sketch: Sketch }[] = [];
63+
64+
private fireSoon(type: 'created' | 'removed', sketch: Sketch): void {
65+
this.bufferedSketchbookEvents.push({ type, sketch });
66+
67+
if (this.fireSoonHandle) {
68+
clearTimeout(this.fireSoonHandle);
69+
}
70+
71+
this.fireSoonHandle = setTimeout(() => {
72+
const event: { created: Sketch[], removed: Sketch[] } = {
73+
created: [],
74+
removed: []
75+
};
76+
for (const { type, sketch } of this.bufferedSketchbookEvents) {
77+
if (type === 'created') {
78+
event.created.push(sketch);
79+
} else {
80+
event.removed.push(sketch);
81+
}
82+
}
83+
this.notificationService.notifySketchbookChanged(event);
84+
this.bufferedSketchbookEvents.length = 0;
85+
}, 100);
86+
}
87+
88+
/**
89+
* Assumes the `fsPath` points to an existing directory.
90+
*/
91+
private async doGetSketches(sketchbookPath: string): Promise<Sketch[]> {
92+
const resolvedSketches = this.sketchbooks.get(sketchbookPath);
93+
if (resolvedSketches) {
94+
if (Array.isArray(resolvedSketches)) {
95+
return resolvedSketches;
96+
}
97+
return resolvedSketches.promise;
98+
}
99+
100+
const deferred = new Deferred<Sketch[]>();
101+
this.sketchbooks.set(sketchbookPath, deferred);
102+
const sketches: Array<Sketch & { mtimeMs: number }> = [];
103+
const filenames = await fs.readdir(sketchbookPath);
104+
for (const fileName of filenames) {
105+
const filePath = path.join(sketchbookPath, fileName);
49106
if (await this.isSketchFolder(FileUri.create(filePath).toString())) {
50107
try {
51108
const stat = await fs.stat(filePath);
@@ -59,7 +116,84 @@ export class SketchesServiceImpl implements SketchesService {
59116
}
60117
}
61118
}
62-
return sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
119+
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
120+
const deleteSketch = (toDelete: Sketch & { mtimeMs: number }) => {
121+
const index = sketches.indexOf(toDelete);
122+
if (index !== -1) {
123+
console.log(`Sketch '${toDelete.name}' was removed from sketchbook '${sketchbookPath}'.`);
124+
sketches.splice(index, 1);
125+
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
126+
this.fireSoon('removed', toDelete);
127+
}
128+
};
129+
const createSketch = async (path: string) => {
130+
try {
131+
const [stat, sketch] = await Promise.all([
132+
fs.stat(path),
133+
this.loadSketch(path)
134+
]);
135+
console.log(`New sketch '${sketch.name}' was crated in sketchbook '${sketchbookPath}'.`);
136+
sketches.push({ ...sketch, mtimeMs: stat.mtimeMs });
137+
sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
138+
this.fireSoon('created', sketch);
139+
} catch { }
140+
};
141+
const watcher = await nsfw(sketchbookPath, async (events: any) => {
142+
// We track `.ino` files changes only.
143+
for (const event of events) {
144+
switch (event.action) {
145+
case nsfw.ActionType.CREATED:
146+
if (event.file.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.file === `${path.basename(event.directory)}.ino`) {
147+
createSketch(event.directory);
148+
}
149+
break;
150+
case nsfw.ActionType.DELETED:
151+
let sketch: Sketch & { mtimeMs: number } | undefined = undefined
152+
// Deleting the `ino` file.
153+
if (event.file.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.file === `${path.basename(event.directory)}.ino`) {
154+
sketch = sketches.find(sketch => FileUri.fsPath(sketch.uri) === event.directory);
155+
} else if (event.directory === sketchbookPath) { // Deleting the sketch (or any folder folder in the sketchbook).
156+
sketch = sketches.find(sketch => FileUri.fsPath(sketch.uri) === path.join(event.directory, event.file));
157+
}
158+
if (sketch) {
159+
deleteSketch(sketch);
160+
}
161+
break;
162+
case nsfw.ActionType.RENAMED:
163+
let sketchToDelete: Sketch & { mtimeMs: number } | undefined = undefined
164+
// When renaming with the Java IDE we got an event where `directory` is the sketchbook and `oldFile` is the sketch.
165+
if (event.directory === sketchbookPath) {
166+
sketchToDelete = sketches.find(sketch => FileUri.fsPath(sketch.uri) === path.join(event.directory, event.oldFile));
167+
}
168+
169+
if (sketchToDelete) {
170+
deleteSketch(sketchToDelete);
171+
} else {
172+
// If it's not a deletion, check for creation. The `directory` is the new sketch and the `newFile` is the new `ino` file.
173+
// tslint:disable-next-line:max-line-length
174+
if (event.newFile.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.newFile === `${path.basename(event.directory)}.ino`) {
175+
createSketch(event.directory);
176+
} else {
177+
// When renaming the `ino` file directly on the filesystem. The `directory` is the sketch and `newFile` and `oldFile` is the `ino` file.
178+
// tslint:disable-next-line:max-line-length
179+
if (event.oldFile.endsWith('.ino') && path.join(event.directory, '..') === sketchbookPath && event.oldFile === `${path.basename(event.directory)}.ino`) {
180+
sketchToDelete = sketches.find(sketch => FileUri.fsPath(sketch.uri) === event.directory, event.oldFile);
181+
}
182+
if (sketchToDelete) {
183+
deleteSketch(sketchToDelete);
184+
} else if (event.directory === sketchbookPath) {
185+
createSketch(path.join(event.directory, event.newFile));
186+
}
187+
}
188+
}
189+
break;
190+
}
191+
}
192+
});
193+
await watcher.start();
194+
deferred.resolve(sketches);
195+
this.sketchbooks.set(sketchbookPath, sketches);
196+
return sketches;
63197
}
64198

65199
/**

Diff for: arduino-ide-extension/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"src"
2727
],
2828
"files": [
29+
"../node_modules/@theia/core/src/typings/nsfw/index.d.ts",
2930
"../node_modules/@theia/monaco/src/typings/monaco/index.d.ts"
3031
]
3132
}

0 commit comments

Comments
 (0)