diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index d7185178b..5bcd73d35 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -19,10 +19,11 @@ import { SketchContribution, CommandRegistry, MenuModelRegistry, + URI, } from './contribution'; import { NotificationCenter } from '../notification-center'; import { Board, SketchRef, SketchContainer } from '../../common/protocol'; -import { nls } from '@theia/core/lib/common'; +import { nls } from '@theia/core/lib/common/nls'; @injectable() export abstract class Examples extends SketchContribution { @@ -150,10 +151,13 @@ export abstract class Examples extends SketchContribution { return { execute: async () => { const sketch = await this.sketchService.cloneExample(uri); - return this.commandService.executeCommand( - OpenSketch.Commands.OPEN_SKETCH.id, - sketch - ); + return this.commandService + .executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) + .then((result) => { + const name = new URI(uri).path.base; + this.sketchService.markAsRecentlyOpened({ name, sourceUri: uri }); // no await + return result; + }); }, }; } diff --git a/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts index 1c1f384ac..55bd54b69 100644 --- a/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/open-recent-sketch.ts @@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager'; import { OpenSketch } from './open-sketch'; import { NotificationCenter } from '../notification-center'; import { nls } from '@theia/core/lib/common'; +import { ExampleRef } from '../../common/protocol'; @injectable() export class OpenRecentSketch extends SketchContribution { @@ -55,26 +56,30 @@ export class OpenRecentSketch extends SketchContribution { ); } - private refreshMenu(sketches: Sketch[]): void { + private refreshMenu(sketches: (Sketch | ExampleRef)[]): void { this.register(sketches); this.mainMenuManager.update(); } - protected register(sketches: Sketch[]): void { + protected register(sketches: (Sketch | ExampleRef)[]): void { const order = 0; for (const sketch of sketches) { - const { uri } = sketch; + const uri = Sketch.is(sketch) ? sketch.uri : sketch.sourceUri; const toDispose = this.toDisposeBeforeRegister.get(uri); if (toDispose) { toDispose.dispose(); } const command = { id: `arduino-open-recent--${uri}` }; const handler = { - execute: () => + execute: async () => { + const toOpen = Sketch.is(sketch) + ? sketch + : await this.sketchService.cloneExample(sketch.sourceUri); this.commandRegistry.executeCommand( OpenSketch.Commands.OPEN_SKETCH.id, - sketch - ), + toOpen + ); + }, }; this.commandRegistry.registerCommand(command, handler); this.menuRegistry.registerMenuAction( @@ -86,7 +91,7 @@ export class OpenRecentSketch extends SketchContribution { } ); this.toDisposeBeforeRegister.set( - sketch.uri, + uri, new DisposableCollection( Disposable.create(() => this.commandRegistry.unregisterCommand(command) diff --git a/arduino-ide-extension/src/common/protocol/notification-service.ts b/arduino-ide-extension/src/common/protocol/notification-service.ts index e1b192ece..e474b4da5 100644 --- a/arduino-ide-extension/src/common/protocol/notification-service.ts +++ b/arduino-ide-extension/src/common/protocol/notification-service.ts @@ -5,6 +5,7 @@ import type { Config, ProgressMessage, Sketch, + ExampleRef, } from '../protocol'; import type { LibraryPackage } from './library-service'; @@ -27,7 +28,9 @@ export interface NotificationServiceClient { notifyLibraryDidInstall(event: { item: LibraryPackage }): void; notifyLibraryDidUninstall(event: { item: LibraryPackage }): void; notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void; - notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void; + notifyRecentSketchesDidChange(event: { + sketches: (Sketch | ExampleRef)[]; + }): void; } export const NotificationServicePath = '/services/notification-service'; diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index dbb7c2654..73543727d 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -78,12 +78,12 @@ export interface SketchesService { /** * Marks the sketch with the given URI as recently opened. It does nothing if the sketch is temp or not valid. */ - markAsRecentlyOpened(uri: string): Promise; + markAsRecentlyOpened(uriOrRef: string | ExampleRef): Promise; /** * Resolves to an array of sketches in inverse chronological order. The newest is the first. */ - recentlyOpenedSketches(): Promise; + recentlyOpenedSketches(): Promise<(Sketch | ExampleRef)[]>; /** * Archives the sketch, resolves to the archive URI. @@ -102,6 +102,27 @@ export interface SketchesService { deleteSketch(sketch: Sketch): Promise; } +export interface ExampleRef { + /** + * Name of the example. + */ + readonly name: string; + /** + * This is the location where the example is. IDE2 will clone the sketch from this location. + */ + readonly sourceUri: string; +} +export namespace ExampleRef { + export function is(arg: unknown): arg is ExampleRef { + return ( + (arg as ExampleRef).name !== undefined && + typeof (arg as ExampleRef).name === 'string' && + (arg as ExampleRef).sourceUri !== undefined && + typeof (arg as ExampleRef).sourceUri === 'string' + ); + } +} + export interface SketchRef { readonly name: string; readonly uri: string; // `LocationPath` diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index 12f397a08..bb85405b3 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -183,10 +183,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { ); for (const workspace of workspaces) { if (await this.isValidSketchPath(workspace.file)) { - if ( - this.isTempSketch.is(workspace.file) && - !this.isTempSketch.isExample(workspace.file) - ) { + if (this.isTempSketch.is(workspace.file)) { console.info( `Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.` ); @@ -430,7 +427,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { // Do not try to reopen the sketch if it was temp. // Unfortunately, IDE2 has two different logic of restoring recent sketches: the Theia default `recentworkspace.json` and there is the `recent-sketches.json`. const file = workspaceUri.fsPath; - if (this.isTempSketch.is(file) && !this.isTempSketch.isExample(file)) { + if (this.isTempSketch.is(file)) { console.info( `Ignored marking workspace as a closed sketch. The sketch was detected as temporary. Workspace URI: ${workspaceUri.toString()}.` ); diff --git a/arduino-ide-extension/src/node/is-temp-sketch.ts b/arduino-ide-extension/src/node/is-temp-sketch.ts index 5c4bd47f9..5c62716e9 100644 --- a/arduino-ide-extension/src/node/is-temp-sketch.ts +++ b/arduino-ide-extension/src/node/is-temp-sketch.ts @@ -3,11 +3,9 @@ import * as tempDir from 'temp-dir'; import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { injectable } from '@theia/core/shared/inversify'; import { firstToLowerCase } from '../common/utils'; -import { join } from 'path'; const Win32DriveRegex = /^[a-zA-Z]:\\/; export const TempSketchPrefix = '.arduinoIDE-unsaved'; -export const ExampleTempSketchPrefix = `${TempSketchPrefix}-example`; @injectable() export class IsTempSketch { @@ -35,16 +33,6 @@ export class IsTempSketch { console.debug(`isTempSketch: ${result}. Input was ${normalizedSketchPath}`); return result; } - - isExample(sketchPath: string): boolean { - const normalizedSketchPath = maybeNormalizeDrive(sketchPath); - const result = - normalizedSketchPath.startsWith(this.tempDirRealpath) && - normalizedSketchPath.includes( - join(this.tempDirRealpath, ExampleTempSketchPrefix) - ); - return result; - } } /** diff --git a/arduino-ide-extension/src/node/notification-service-server.ts b/arduino-ide-extension/src/node/notification-service-server.ts index 733edb336..e017f1b5d 100644 --- a/arduino-ide-extension/src/node/notification-service-server.ts +++ b/arduino-ide-extension/src/node/notification-service-server.ts @@ -8,6 +8,7 @@ import type { Config, Sketch, ProgressMessage, + ExampleRef, } from '../common/protocol'; @injectable() @@ -76,7 +77,9 @@ export class NotificationServiceServerImpl this.clients.forEach((client) => client.notifyConfigDidChange(event)); } - notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void { + notifyRecentSketchesDidChange(event: { + sketches: (Sketch | ExampleRef)[]; + }): void { this.clients.forEach((client) => client.notifyRecentSketchesDidChange(event) ); diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 7a0b4b77d..d59fae6f2 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -16,6 +16,7 @@ import { SketchRef, SketchContainer, SketchesError, + ExampleRef, } from '../common/protocol/sketches-service'; import { NotificationServiceServerImpl } from './notification-service-server'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; @@ -29,7 +30,6 @@ import * as glob from 'glob'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { ServiceError } from './service-error'; import { - ExampleTempSketchPrefix, IsTempSketch, maybeNormalizeDrive, TempSketchPrefix, @@ -258,9 +258,7 @@ export class SketchesServiceImpl .then((uri) => path.join(FileUri.fsPath(uri), 'recent-sketches.json')); } - private async loadRecentSketches( - fsPath: string - ): Promise> { + private async loadRecentSketches(fsPath: string): Promise { let data: Record = {}; try { const raw = await promisify(fs.readFile)(fsPath, { @@ -271,32 +269,39 @@ export class SketchesServiceImpl return data; } - async markAsRecentlyOpened(uri: string): Promise { + async markAsRecentlyOpened(uriOrRef: string | ExampleRef): Promise { + const isExample = typeof uriOrRef !== 'string'; + const uri = isExample ? uriOrRef.sourceUri : uriOrRef; let sketch: Sketch | undefined = undefined; try { sketch = await this.loadSketch(uri); } catch { return; } - if ( - (await this.isTemp(sketch)) && - !this.isTempSketch.isExample(FileUri.fsPath(sketch.uri)) - ) { + if (await this.isTemp(sketch)) { return; } const fsPath = await this.recentSketchesFsPath; const data = await this.loadRecentSketches(fsPath); const now = Date.now(); - data[sketch.uri] = now; + data[sketch.uri] = isExample ? { type: 'example', mtimeMs: now } : now; let toDeleteUri: string | undefined = undefined; if (Object.keys(data).length > 10) { let min = Number.MAX_SAFE_INTEGER; for (const uri of Object.keys(data)) { - if (min > data[uri]) { - min = data[uri]; - toDeleteUri = uri; + const value = data[uri]; + if (typeof value === 'number') { + if (min > value) { + min = value; + toDeleteUri = uri; + } + } else { + if (min > value.mtimeMs) { + min = value.mtimeMs; + toDeleteUri = uri; + } } } } @@ -311,13 +316,13 @@ export class SketchesServiceImpl ); } - async recentlyOpenedSketches(): Promise { + async recentlyOpenedSketches(): Promise<(Sketch | ExampleRef)[]> { const configDirUri = await this.envVariableServer.getConfigDirUri(); const fsPath = path.join( FileUri.fsPath(configDirUri), 'recent-sketches.json' ); - let data: Record = {}; + let data: RecentSketches = {}; try { const raw = await promisify(fs.readFile)(fsPath, { encoding: 'utf8', @@ -325,14 +330,25 @@ export class SketchesServiceImpl data = JSON.parse(raw); } catch {} - const sketches: SketchWithDetails[] = []; - for (const uri of Object.keys(data).sort( - (left, right) => data[right] - data[left] - )) { - try { - const sketch = await this.loadSketch(uri); - sketches.push(sketch); - } catch {} + const sketches: (Sketch | ExampleRef)[] = []; + for (const uri of Object.keys(data).sort((left, right) => { + const leftValue = data[left]; + const rightValue = data[right]; + const leftMtimeMs = + typeof leftValue === 'number' ? leftValue : leftValue.mtimeMs; + const rightMtimeMs = + typeof rightValue === 'number' ? rightValue : rightValue.mtimeMs; + return leftMtimeMs - rightMtimeMs; + })) { + const value = data[uri]; + if (typeof value === 'number') { + try { + const sketch = await this.loadSketch(uri); + sketches.push(sketch); + } catch {} + } else { + sketches.push({ name: new URI(uri).path.base, sourceUri: uri }); + } } return sketches; @@ -340,7 +356,7 @@ export class SketchesServiceImpl async cloneExample(uri: string): Promise { const sketch = await this.loadSketch(uri); - const parentPath = await this.createTempFolder(false); + const parentPath = await this.createTempFolder(); const destinationUri = FileUri.create( path.join(parentPath, sketch.name) ).toString(); @@ -421,24 +437,21 @@ void loop() { * For example, on Windows, instead of getting an [8.3 filename](https://en.wikipedia.org/wiki/8.3_filename), callers will get a fully resolved path. * `C:\\Users\\KITTAA~1\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2022615-21100-iahybb.yyvh\\sketch_jul15a` will be `C:\\Users\\kittaakos\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2022615-21100-iahybb.yyvh\\sketch_jul15a` */ - private createTempFolder(isTemp = true): Promise { + private createTempFolder(prefix: string = TempSketchPrefix): Promise { return new Promise((resolve, reject) => { - temp.mkdir( - { prefix: isTemp ? TempSketchPrefix : ExampleTempSketchPrefix }, - (createError, dirPath) => { - if (createError) { - reject(createError); + temp.mkdir({ prefix }, (createError, dirPath) => { + if (createError) { + reject(createError); + return; + } + fs.realpath.native(dirPath, (resolveError, resolvedDirPath) => { + if (resolveError) { + reject(resolveError); return; } - fs.realpath.native(dirPath, (resolveError, resolvedDirPath) => { - if (resolveError) { - reject(resolveError); - return; - } - resolve(resolvedDirPath); - }); - } - ); + resolve(resolvedDirPath); + }); + }); }); } @@ -641,3 +654,8 @@ function sketchIndexToLetters(num: number): string { } while (pow > 0); return out; } + +type RecentSketches = Record< + string, + number | { type: 'example'; mtimeMs: number } +>;