Skip to content

Commit 6626701

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
ATL-815: Implemented Open Recent.
Signed-off-by: Akos Kitta <[email protected]>
1 parent 66b711f commit 6626701

10 files changed

+191
-14
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import { Sketchbook } from './contributions/sketchbook';
134134
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
135135
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
136136
import { BoardSelection } from './contributions/board-selection';
137+
import { OpenRecentSketch } from './contributions/open-recent-sketch';
137138

138139
const ElementQueries = require('css-element-queries/src/ElementQueries');
139140

@@ -337,6 +338,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
337338
Contribution.configure(bind, Debug);
338339
Contribution.configure(bind, Sketchbook);
339340
Contribution.configure(bind, BoardSelection);
341+
Contribution.configure(bind, OpenRecentSketch);
340342

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

Diff for: arduino-ide-extension/src/browser/contributions/save-as-sketch.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ export class SaveAsSketch extends SketchContribution {
6060
}
6161
const workspaceUri = await this.sketchService.copy(sketch, { destinationUri });
6262
if (workspaceUri && openAfterMove) {
63-
if (wipeOriginal) {
64-
await this.fileService.delete(new URI(sketch.uri));
63+
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
64+
await this.fileService.delete(new URI(sketch.uri), { recursive: true });
6565
}
6666
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
6767
}

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ 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 / Open Recent
16+
export const FILE__OPEN_RECENT_SUBMENU = [...FILE__SKETCH_GROUP, '0_open_recent'];
17+
1518
// -- File / Sketchbook
16-
export const FILE__SKETCHBOOK_SUBMENU = [...FILE__SKETCH_GROUP, '0_sketchbook'];
19+
export const FILE__SKETCHBOOK_SUBMENU = [...FILE__SKETCH_GROUP, '1_sketchbook'];
1720

1821
// -- File / Examples
19-
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '1_examples'];
22+
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '2_examples'];
2023
export const EXAMPLES__BUILT_IN_GROUP = [...FILE__EXAMPLES_SUBMENU, '0_built_ins'];
2124
export const EXAMPLES__ANY_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '1_any_board'];
2225
export const EXAMPLES__CURRENT_BOARD_GROUP = [...FILE__EXAMPLES_SUBMENU, '2_current_board'];

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

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
2222
protected readonly libraryUninstalledEmitter = new Emitter<{ item: LibraryPackage }>();
2323
protected readonly attachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
2424
protected readonly sketchbookChangedEmitter = new Emitter<{ created: Sketch[], removed: Sketch[] }>();
25+
protected readonly recentSketchesChangedEmitter = new Emitter<{ sketches: Sketch[] }>();
2526

2627
protected readonly toDispose = new DisposableCollection(
2728
this.indexUpdatedEmitter,
@@ -46,6 +47,7 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
4647
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
4748
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
4849
readonly onSketchbookChanged = this.sketchbookChangedEmitter.event;
50+
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
4951

5052
@postConstruct()
5153
protected init(): void {
@@ -96,4 +98,8 @@ export class NotificationCenter implements NotificationServiceClient, FrontendAp
9698
this.sketchbookChangedEmitter.fire(event);
9799
}
98100

101+
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
102+
this.recentSketchesChangedEmitter.fire(event);
103+
}
104+
99105
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service';
33
import { CommandService } from '@theia/core/lib/common/command';
44
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
55
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application';
6+
import { SketchesService } from '../../../common/protocol';
67
import { ArduinoCommands } from '../../arduino-commands';
78

89
@injectable()
@@ -17,12 +18,16 @@ export class FrontendApplication extends TheiaFrontendApplication {
1718
@inject(CommandService)
1819
protected readonly commandService: CommandService;
1920

21+
@inject(SketchesService)
22+
protected readonly sketchesService: SketchesService;
23+
2024
protected async initializeLayout(): Promise<void> {
2125
await super.initializeLayout();
2226
const roots = await this.workspaceService.roots;
2327
for (const root of roots) {
2428
const exists = await this.fileService.exists(root.resource);
2529
if (exists) {
30+
this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu
2631
await this.commandService.executeCommand(ArduinoCommands.OPEN_SKETCH_FILES.id, root.resource);
2732
}
2833
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface NotificationServiceClient {
1313
notifyLibraryUninstalled(event: { item: LibraryPackage }): void;
1414
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
1515
notifySketchbookChanged(event: { created: Sketch[], removed: Sketch[] }): void;
16+
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void;
1617
}
1718

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

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ export interface SketchesService {
4848
*/
4949
getSketchFolder(uri: string): Promise<Sketch | undefined>;
5050

51+
/**
52+
* Marks the sketch with the given URI as recently opened. It does nothing if the sketch is temp or not valid.
53+
*/
54+
markAsRecentlyOpened(uri: string): Promise<void>;
55+
56+
/**
57+
* Resolves to an array of sketches in inverse chronological order. The newest is the first.
58+
*/
59+
recentlyOpenedSketches(): Promise<Sketch[]>;
60+
5161
}
5262

5363
export interface Sketch {
@@ -72,4 +82,3 @@ export namespace Sketch {
7282
return [mainFileUri, ...otherSketchFileUris, ...additionalFileUris].indexOf(uri.toString()) !== -1;
7383
}
7484
}
75-

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

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export class NotificationServiceServerImpl implements NotificationServiceServer
4646
this.clients.forEach(client => client.notifySketchbookChanged(event));
4747
}
4848

49+
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
50+
this.clients.forEach(client => client.notifyRecentSketchesChanged(event));
51+
}
52+
4953
setClient(client: NotificationServiceClient): void {
5054
this.clients.push(client);
5155
}

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

+94-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { ConfigService } from '../common/protocol/config-service';
1414
import { SketchesService, Sketch } from '../common/protocol/sketches-service';
1515
import { firstToLowerCase } from '../common/utils';
1616
import { NotificationServiceServerImpl } from './notification-service-server';
17+
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
18+
import { notEmpty } from '@theia/core';
1719

1820
// As currently implemented on Linux,
1921
// the maximum number of symbolic links that will be followed while resolving a pathname is 40
@@ -33,7 +35,10 @@ export class SketchesServiceImpl implements SketchesService {
3335
@inject(NotificationServiceServerImpl)
3436
protected readonly notificationService: NotificationServiceServerImpl;
3537

36-
async getSketches(uri?: string): Promise<Sketch[]> {
38+
@inject(EnvVariablesServer)
39+
protected readonly envVariableServer: EnvVariablesServer;
40+
41+
async getSketches(uri?: string): Promise<SketchWithDetails[]> {
3742
let fsPath: undefined | string;
3843
if (!uri) {
3944
const { sketchDirUri } = await this.configService.getConfiguration();
@@ -57,7 +62,7 @@ export class SketchesServiceImpl implements SketchesService {
5762
/**
5863
* Dev note: The keys are filesystem paths, not URI strings.
5964
*/
60-
private sketchbooks = new Map<string, Sketch[] | Deferred<Sketch[]>>();
65+
private sketchbooks = new Map<string, SketchWithDetails[] | Deferred<SketchWithDetails[]>>();
6166
private fireSoonHandle?: NodeJS.Timer;
6267
private bufferedSketchbookEvents: { type: 'created' | 'removed', sketch: Sketch }[] = [];
6368

@@ -88,7 +93,7 @@ export class SketchesServiceImpl implements SketchesService {
8893
/**
8994
* Assumes the `fsPath` points to an existing directory.
9095
*/
91-
private async doGetSketches(sketchbookPath: string): Promise<Sketch[]> {
96+
private async doGetSketches(sketchbookPath: string): Promise<SketchWithDetails[]> {
9297
const resolvedSketches = this.sketchbooks.get(sketchbookPath);
9398
if (resolvedSketches) {
9499
if (Array.isArray(resolvedSketches)) {
@@ -97,9 +102,9 @@ export class SketchesServiceImpl implements SketchesService {
97102
return resolvedSketches.promise;
98103
}
99104

100-
const deferred = new Deferred<Sketch[]>();
105+
const deferred = new Deferred<SketchWithDetails[]>();
101106
this.sketchbooks.set(sketchbookPath, deferred);
102-
const sketches: Array<Sketch & { mtimeMs: number }> = [];
107+
const sketches: Array<SketchWithDetails> = [];
103108
const filenames = await fs.readdir(sketchbookPath);
104109
for (const fileName of filenames) {
105110
const filePath = path.join(sketchbookPath, fileName);
@@ -201,7 +206,7 @@ export class SketchesServiceImpl implements SketchesService {
201206
* See: https://github.com/arduino/arduino-cli/issues/837
202207
* Based on: https://github.com/arduino/arduino-cli/blob/eef3705c4afcba4317ec38b803d9ffce5dd59a28/arduino/builder/sketch.go#L100-L215
203208
*/
204-
async loadSketch(uri: string): Promise<Sketch> {
209+
async loadSketch(uri: string): Promise<SketchWithDetails> {
205210
const sketchPath = FileUri.fsPath(uri);
206211
const exists = await fs.exists(sketchPath);
207212
if (!exists) {
@@ -294,7 +299,80 @@ export class SketchesServiceImpl implements SketchesService {
294299

295300
}
296301

297-
private newSketch(sketchFolderPath: string, mainFilePath: string, allFilesPaths: string[]): Sketch {
302+
private get recentSketchesFsPath(): Promise<string> {
303+
return this.envVariableServer.getConfigDirUri().then(uri => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
304+
}
305+
306+
private async loadRecentSketches(fsPath: string): Promise<Record<string, number>> {
307+
let data: Record<string, number> = {};
308+
try {
309+
const raw = await fs.readFile(fsPath, { encoding: 'utf8' });
310+
data = JSON.parse(raw);
311+
} catch { }
312+
return data;
313+
}
314+
315+
async markAsRecentlyOpened(uri: string): Promise<void> {
316+
let sketch: Sketch | undefined = undefined;
317+
try {
318+
sketch = await this.loadSketch(uri);
319+
} catch {
320+
return;
321+
}
322+
if (await this.isTemp(sketch)) {
323+
return;
324+
}
325+
326+
const fsPath = await this.recentSketchesFsPath;
327+
const data = await this.loadRecentSketches(fsPath);
328+
const now = Date.now();
329+
data[sketch.uri] = now;
330+
331+
let toDeleteUri: string | undefined = undefined;
332+
if (Object.keys(data).length > 10) {
333+
let min = Number.MAX_SAFE_INTEGER;
334+
for (const uri of Object.keys(data)) {
335+
if (min > data[uri]) {
336+
min = data[uri];
337+
toDeleteUri = uri;
338+
}
339+
}
340+
}
341+
342+
if (toDeleteUri) {
343+
delete data[toDeleteUri];
344+
}
345+
346+
await fs.writeFile(fsPath, JSON.stringify(data, null, 2));
347+
this.recentlyOpenedSketches().then(sketches => this.notificationService.notifyRecentSketchesChanged({ sketches }));
348+
}
349+
350+
async recentlyOpenedSketches(): Promise<Sketch[]> {
351+
const configDirUri = await this.envVariableServer.getConfigDirUri();
352+
const fsPath = path.join(FileUri.fsPath(configDirUri), 'recent-sketches.json');
353+
let data: Record<string, number> = {};
354+
try {
355+
const raw = await fs.readFile(fsPath, { encoding: 'utf8' });
356+
data = JSON.parse(raw);
357+
} catch { }
358+
359+
const loadSketchSafe = (uri: string) => {
360+
try {
361+
return this.loadSketch(uri);
362+
} catch {
363+
return undefined;
364+
}
365+
}
366+
367+
const sketches = await Promise.all(Object.keys(data)
368+
.sort((left, right) => data[right] - data[left])
369+
.map(loadSketchSafe)
370+
.filter(notEmpty));
371+
372+
return sketches;
373+
}
374+
375+
private async newSketch(sketchFolderPath: string, mainFilePath: string, allFilesPaths: string[]): Promise<SketchWithDetails> {
298376
let mainFile: string | undefined;
299377
const paths = new Set<string>();
300378
for (const p of allFilesPaths) {
@@ -326,13 +404,15 @@ export class SketchesServiceImpl implements SketchesService {
326404
additionalFiles.sort();
327405
otherSketchFiles.sort();
328406

407+
const { mtimeMs } = await fs.lstat(sketchFolderPath);
329408
return {
330409
uri: FileUri.create(sketchFolderPath).toString(),
331410
mainFileUri: FileUri.create(mainFile).toString(),
332411
name: path.basename(sketchFolderPath),
333412
additionalFileUris: additionalFiles.map(p => FileUri.create(p).toString()),
334-
otherSketchFileUris: otherSketchFiles.map(p => FileUri.create(p).toString())
335-
}
413+
otherSketchFileUris: otherSketchFiles.map(p => FileUri.create(p).toString()),
414+
mtimeMs
415+
};
336416
}
337417

338418
async cloneExample(uri: string): Promise<Sketch> {
@@ -538,3 +618,8 @@ class SkipDir extends Error {
538618
Object.setPrototypeOf(this, SkipDir.prototype);
539619
}
540620
}
621+
622+
interface SketchWithDetails extends Sketch {
623+
readonly mtimeMs: number;
624+
}
625+

0 commit comments

Comments
 (0)