another approach

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-09-07 16:35:09 +02:00 committed by Francesco Spissu
parent a04527d3b8
commit 750486a8f0
8 changed files with 111 additions and 72 deletions

View File

@ -19,10 +19,11 @@ import {
SketchContribution, SketchContribution,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
URI,
} 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 } from '../../common/protocol';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common/nls';
@injectable() @injectable()
export abstract class Examples extends SketchContribution { export abstract class Examples extends SketchContribution {
@ -150,10 +151,13 @@ export abstract class Examples extends SketchContribution {
return { return {
execute: async () => { execute: async () => {
const sketch = await this.sketchService.cloneExample(uri); const sketch = await this.sketchService.cloneExample(uri);
return this.commandService.executeCommand( return this.commandService
OpenSketch.Commands.OPEN_SKETCH.id, .executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch)
sketch .then((result) => {
); const name = new URI(uri).path.base;
this.sketchService.markAsRecentlyOpened({ name, sourceUri: uri }); // no await
return result;
});
}, },
}; };
} }

View File

@ -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 { ExampleRef } from '../../common/protocol';
@injectable() @injectable()
export class OpenRecentSketch extends SketchContribution { 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.register(sketches);
this.mainMenuManager.update(); this.mainMenuManager.update();
} }
protected register(sketches: Sketch[]): void { protected register(sketches: (Sketch | ExampleRef)[]): void {
const order = 0; const order = 0;
for (const sketch of sketches) { for (const sketch of sketches) {
const { uri } = sketch; const uri = Sketch.is(sketch) ? sketch.uri : sketch.sourceUri;
const toDispose = this.toDisposeBeforeRegister.get(uri); const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) { if (toDispose) {
toDispose.dispose(); toDispose.dispose();
} }
const command = { id: `arduino-open-recent--${uri}` }; const command = { id: `arduino-open-recent--${uri}` };
const handler = { const handler = {
execute: () => execute: async () => {
const toOpen = Sketch.is(sketch)
? sketch
: await this.sketchService.cloneExample(sketch.sourceUri);
this.commandRegistry.executeCommand( this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id, OpenSketch.Commands.OPEN_SKETCH.id,
sketch toOpen
), );
},
}; };
this.commandRegistry.registerCommand(command, handler); this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
@ -86,7 +91,7 @@ export class OpenRecentSketch extends SketchContribution {
} }
); );
this.toDisposeBeforeRegister.set( this.toDisposeBeforeRegister.set(
sketch.uri, uri,
new DisposableCollection( new DisposableCollection(
Disposable.create(() => Disposable.create(() =>
this.commandRegistry.unregisterCommand(command) this.commandRegistry.unregisterCommand(command)

View File

@ -5,6 +5,7 @@ import type {
Config, Config,
ProgressMessage, ProgressMessage,
Sketch, Sketch,
ExampleRef,
} from '../protocol'; } from '../protocol';
import type { LibraryPackage } from './library-service'; import type { LibraryPackage } from './library-service';
@ -27,7 +28,9 @@ export interface NotificationServiceClient {
notifyLibraryDidInstall(event: { item: LibraryPackage }): void; notifyLibraryDidInstall(event: { item: LibraryPackage }): void;
notifyLibraryDidUninstall(event: { item: LibraryPackage }): void; notifyLibraryDidUninstall(event: { item: LibraryPackage }): void;
notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void; notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void;
notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void; notifyRecentSketchesDidChange(event: {
sketches: (Sketch | ExampleRef)[];
}): void;
} }
export const NotificationServicePath = '/services/notification-service'; export const NotificationServicePath = '/services/notification-service';

View File

@ -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. * 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<void>; markAsRecentlyOpened(uriOrRef: string | ExampleRef): Promise<void>;
/** /**
* 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.
*/ */
recentlyOpenedSketches(): Promise<Sketch[]>; recentlyOpenedSketches(): Promise<(Sketch | ExampleRef)[]>;
/** /**
* Archives the sketch, resolves to the archive URI. * Archives the sketch, resolves to the archive URI.
@ -102,6 +102,27 @@ export interface SketchesService {
deleteSketch(sketch: Sketch): Promise<void>; deleteSketch(sketch: Sketch): Promise<void>;
} }
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 { export interface SketchRef {
readonly name: string; readonly name: string;
readonly uri: string; // `LocationPath` readonly uri: string; // `LocationPath`

View File

@ -183,10 +183,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
); );
for (const workspace of workspaces) { for (const workspace of workspaces) {
if (await this.isValidSketchPath(workspace.file)) { if (await this.isValidSketchPath(workspace.file)) {
if ( if (this.isTempSketch.is(workspace.file)) {
this.isTempSketch.is(workspace.file) &&
!this.isTempSketch.isExample(workspace.file)
) {
console.info( console.info(
`Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.` `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. // 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`. // 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; const file = workspaceUri.fsPath;
if (this.isTempSketch.is(file) && !this.isTempSketch.isExample(file)) { if (this.isTempSketch.is(file)) {
console.info( console.info(
`Ignored marking workspace as a closed sketch. The sketch was detected as temporary. Workspace URI: ${workspaceUri.toString()}.` `Ignored marking workspace as a closed sketch. The sketch was detected as temporary. Workspace URI: ${workspaceUri.toString()}.`
); );

View File

@ -3,11 +3,9 @@ import * as tempDir from 'temp-dir';
import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { isWindows, isOSX } from '@theia/core/lib/common/os';
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { firstToLowerCase } from '../common/utils'; import { firstToLowerCase } from '../common/utils';
import { join } from 'path';
const Win32DriveRegex = /^[a-zA-Z]:\\/; const Win32DriveRegex = /^[a-zA-Z]:\\/;
export const TempSketchPrefix = '.arduinoIDE-unsaved'; export const TempSketchPrefix = '.arduinoIDE-unsaved';
export const ExampleTempSketchPrefix = `${TempSketchPrefix}-example`;
@injectable() @injectable()
export class IsTempSketch { export class IsTempSketch {
@ -35,16 +33,6 @@ export class IsTempSketch {
console.debug(`isTempSketch: ${result}. Input was ${normalizedSketchPath}`); console.debug(`isTempSketch: ${result}. Input was ${normalizedSketchPath}`);
return result; return result;
} }
isExample(sketchPath: string): boolean {
const normalizedSketchPath = maybeNormalizeDrive(sketchPath);
const result =
normalizedSketchPath.startsWith(this.tempDirRealpath) &&
normalizedSketchPath.includes(
join(this.tempDirRealpath, ExampleTempSketchPrefix)
);
return result;
}
} }
/** /**

View File

@ -8,6 +8,7 @@ import type {
Config, Config,
Sketch, Sketch,
ProgressMessage, ProgressMessage,
ExampleRef,
} from '../common/protocol'; } from '../common/protocol';
@injectable() @injectable()
@ -76,7 +77,9 @@ export class NotificationServiceServerImpl
this.clients.forEach((client) => client.notifyConfigDidChange(event)); this.clients.forEach((client) => client.notifyConfigDidChange(event));
} }
notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void { notifyRecentSketchesDidChange(event: {
sketches: (Sketch | ExampleRef)[];
}): void {
this.clients.forEach((client) => this.clients.forEach((client) =>
client.notifyRecentSketchesDidChange(event) client.notifyRecentSketchesDidChange(event)
); );

View File

@ -16,6 +16,7 @@ import {
SketchRef, SketchRef,
SketchContainer, SketchContainer,
SketchesError, SketchesError,
ExampleRef,
} from '../common/protocol/sketches-service'; } from '../common/protocol/sketches-service';
import { NotificationServiceServerImpl } from './notification-service-server'; import { NotificationServiceServerImpl } from './notification-service-server';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; 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 { Deferred } from '@theia/core/lib/common/promise-util';
import { ServiceError } from './service-error'; import { ServiceError } from './service-error';
import { import {
ExampleTempSketchPrefix,
IsTempSketch, IsTempSketch,
maybeNormalizeDrive, maybeNormalizeDrive,
TempSketchPrefix, TempSketchPrefix,
@ -258,9 +258,7 @@ export class SketchesServiceImpl
.then((uri) => path.join(FileUri.fsPath(uri), 'recent-sketches.json')); .then((uri) => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
} }
private async loadRecentSketches( private async loadRecentSketches(fsPath: string): Promise<RecentSketches> {
fsPath: string
): Promise<Record<string, number>> {
let data: Record<string, number> = {}; let data: Record<string, number> = {};
try { try {
const raw = await promisify(fs.readFile)(fsPath, { const raw = await promisify(fs.readFile)(fsPath, {
@ -271,32 +269,39 @@ export class SketchesServiceImpl
return data; return data;
} }
async markAsRecentlyOpened(uri: string): Promise<void> { async markAsRecentlyOpened(uriOrRef: string | ExampleRef): Promise<void> {
const isExample = typeof uriOrRef !== 'string';
const uri = isExample ? uriOrRef.sourceUri : uriOrRef;
let sketch: Sketch | undefined = undefined; let sketch: Sketch | undefined = undefined;
try { try {
sketch = await this.loadSketch(uri); sketch = await this.loadSketch(uri);
} catch { } catch {
return; return;
} }
if ( if (await this.isTemp(sketch)) {
(await this.isTemp(sketch)) &&
!this.isTempSketch.isExample(FileUri.fsPath(sketch.uri))
) {
return; return;
} }
const fsPath = await this.recentSketchesFsPath; const fsPath = await this.recentSketchesFsPath;
const data = await this.loadRecentSketches(fsPath); const data = await this.loadRecentSketches(fsPath);
const now = Date.now(); const now = Date.now();
data[sketch.uri] = now; data[sketch.uri] = isExample ? { type: 'example', mtimeMs: now } : now;
let toDeleteUri: string | undefined = undefined; let toDeleteUri: string | 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 of Object.keys(data)) {
if (min > data[uri]) { const value = data[uri];
min = data[uri]; if (typeof value === 'number') {
toDeleteUri = uri; 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<Sketch[]> { async recentlyOpenedSketches(): Promise<(Sketch | ExampleRef)[]> {
const configDirUri = await this.envVariableServer.getConfigDirUri(); const configDirUri = await this.envVariableServer.getConfigDirUri();
const fsPath = path.join( const fsPath = path.join(
FileUri.fsPath(configDirUri), FileUri.fsPath(configDirUri),
'recent-sketches.json' 'recent-sketches.json'
); );
let data: Record<string, number> = {}; let data: RecentSketches = {};
try { try {
const raw = await promisify(fs.readFile)(fsPath, { const raw = await promisify(fs.readFile)(fsPath, {
encoding: 'utf8', encoding: 'utf8',
@ -325,14 +330,25 @@ export class SketchesServiceImpl
data = JSON.parse(raw); data = JSON.parse(raw);
} catch {} } catch {}
const sketches: SketchWithDetails[] = []; const sketches: (Sketch | ExampleRef)[] = [];
for (const uri of Object.keys(data).sort( for (const uri of Object.keys(data).sort((left, right) => {
(left, right) => data[right] - data[left] const leftValue = data[left];
)) { const rightValue = data[right];
try { const leftMtimeMs =
const sketch = await this.loadSketch(uri); typeof leftValue === 'number' ? leftValue : leftValue.mtimeMs;
sketches.push(sketch); const rightMtimeMs =
} catch {} 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; return sketches;
@ -340,7 +356,7 @@ export class SketchesServiceImpl
async cloneExample(uri: string): Promise<Sketch> { async cloneExample(uri: string): Promise<Sketch> {
const sketch = await this.loadSketch(uri); const sketch = await this.loadSketch(uri);
const parentPath = await this.createTempFolder(false); const parentPath = await this.createTempFolder();
const destinationUri = FileUri.create( const destinationUri = FileUri.create(
path.join(parentPath, sketch.name) path.join(parentPath, sketch.name)
).toString(); ).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. * 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` * `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<string> { private createTempFolder(prefix: string = TempSketchPrefix): Promise<string> {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
temp.mkdir( temp.mkdir({ prefix }, (createError, dirPath) => {
{ prefix: isTemp ? TempSketchPrefix : ExampleTempSketchPrefix }, if (createError) {
(createError, dirPath) => { reject(createError);
if (createError) { return;
reject(createError); }
fs.realpath.native(dirPath, (resolveError, resolvedDirPath) => {
if (resolveError) {
reject(resolveError);
return; return;
} }
fs.realpath.native(dirPath, (resolveError, resolvedDirPath) => { resolve(resolvedDirPath);
if (resolveError) { });
reject(resolveError); });
return;
}
resolve(resolvedDirPath);
});
}
);
}); });
} }
@ -641,3 +654,8 @@ function sketchIndexToLetters(num: number): string {
} while (pow > 0); } while (pow > 0);
return out; return out;
} }
type RecentSketches = Record<
string,
number | { type: 'example'; mtimeMs: number }
>;