diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 416e5d9c..ef84eb41 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -302,7 +302,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C registry.registerCommand(ArduinoCommands.OPEN_SKETCH, { isEnabled: () => true, execute: async (sketch: Sketch) => { - this.workspaceService.openSketchFilesInNewWindow(sketch.uri); + this.workspaceService.open(new URI(sketch.uri)); } }) registry.registerCommand(ArduinoCommands.SAVE_SKETCH, { @@ -321,7 +321,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C } const sketch = await this.sketchService.createNewSketch(uri.toString()); - this.workspaceService.openSketchFilesInNewWindow(sketch.uri); + this.workspaceService.open(new URI(sketch.uri)); } catch (e) { await this.messageService.error(e.toString()); } @@ -461,7 +461,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C if (destinationFile && !destinationFile.isDirectory) { const message = await this.validate(destinationFile); if (!message) { - await this.workspaceService.openSketchFilesInNewWindow(destinationFileUri.toString()); + await this.workspaceService.open(destinationFileUri); return destinationFileUri; } else { this.messageService.warn(message); diff --git a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts new file mode 100644 index 00000000..3ee087c5 --- /dev/null +++ b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts @@ -0,0 +1,68 @@ +import { toUnix } from 'upath'; +import URI from '@theia/core/lib/common/uri'; +import { isWindows } from '@theia/core/lib/common/os'; +import { notEmpty } from '@theia/core/lib/common/objects'; +import { MaybePromise } from '@theia/core/lib/common/types'; + +/** + * Class for determining the default workspace location from the + * `location.hash`, the historical workspace locations, and recent sketch files. + * + * The following logic is used for determining the default workspace location: + * - `hash` points to an exists in location? + * - Yes + * - `validate location`. Is valid sketch location? + * - Yes + * - Done. + * - No + * - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`. + * - No + * - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`. + */ +namespace ArduinoWorkspaceRootResolver { + export interface InitOptions { + readonly isValid: (uri: string) => MaybePromise; + } + export interface ResolveOptions { + readonly hash?: string + readonly recentWorkspaces: string[]; + // Gathered from the default sketch folder. The default sketch folder is defined by the CLI. + readonly recentSketches: string[]; + } +} +export class ArduinoWorkspaceRootResolver { + + constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) { + } + + async resolve(options: ArduinoWorkspaceRootResolver.ResolveOptions): Promise<{ uri: string } | undefined> { + const { hash, recentWorkspaces, recentSketches } = options; + for (const uri of [this.hashToUri(hash), ...recentWorkspaces, ...recentSketches].filter(notEmpty)) { + const valid = await this.isValid(uri); + if (valid) { + return { uri }; + } + } + return undefined; + } + + protected isValid(uri: string): MaybePromise { + return this.options.isValid.bind(this)(uri); + } + + // Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first. + // This is important for Windows only and a NOOP on POSIX. + // Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See: + // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and + // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423 + protected hashToUri(hash: string | undefined): string | undefined { + if (hash + && hash.length > 1 + && hash.startsWith('#')) { + const path = hash.slice(1); // Trim the leading `#`. + return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString(); + } + return undefined; + } + +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/arduino-workspace-service.ts b/arduino-ide-extension/src/browser/arduino-workspace-service.ts index 4e7d8d52..a56f0033 100644 --- a/arduino-ide-extension/src/browser/arduino-workspace-service.ts +++ b/arduino-ide-extension/src/browser/arduino-workspace-service.ts @@ -1,16 +1,29 @@ import { injectable, inject } from 'inversify'; -import { toUnix } from 'upath'; -import URI from '@theia/core/lib/common/uri'; -import { isWindows } from '@theia/core/lib/common/os'; +// import { toUnix } from 'upath'; +// import URI from '@theia/core/lib/common/uri'; +// import { isWindows } from '@theia/core/lib/common/os'; import { LabelProvider } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { ConfigService } from '../common/protocol/config-service'; import { SketchesService } from '../common/protocol/sketches-service'; +// import { ArduinoAdvancedMode } from './arduino-frontend-contribution'; +import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver'; import { ArduinoAdvancedMode } from './arduino-frontend-contribution'; /** * This is workaround to have custom frontend binding for the default workspace, although we * already have a custom binding for the backend. + * + * The following logic is used for determining the default workspace location: + * - #hash exists in location? + * - Yes + * - `validateHash`. Is valid sketch location? + * - Yes + * - Done. + * - No + * - `checkHistoricalWorkspaceRoots`, `try open last modified sketch`,create new sketch`. + * - No + * - `checkHistoricalWorkspaceRoots`, `try open last modified sketch`, `create new sketch`. */ @injectable() export class ArduinoWorkspaceService extends WorkspaceService { @@ -25,105 +38,38 @@ export class ArduinoWorkspaceService extends WorkspaceService { protected readonly labelProvider: LabelProvider; async getDefaultWorkspacePath(): Promise { - const url = new URL(window.location.href); - // If `sketch` is set and valid, we use it as is. - // `sketch` is set as an encoded URI string. - const sketch = url.searchParams.get('sketch'); - if (sketch) { - const sketchDirUri = new URI(sketch).toString(); - if (await this.sketchService.isSketchFolder(sketchDirUri)) { - if (await this.configService.isInSketchDir(sketchDirUri)) { - if (ArduinoAdvancedMode.TOGGLED) { - return (await this.configService.getConfiguration()).sketchDirUri - } else { - return sketchDirUri; - } - } - return (await this.configService.getConfiguration()).sketchDirUri - } + const [hash, recentWorkspaces, recentSketches] = await Promise.all([ + window.location.hash, + this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)), + this.server.getRecentWorkspaces() + ]); + const toOpen = await new ArduinoWorkspaceRootResolver({ + isValid: this.isValid.bind(this) + }).resolve({ + hash, + recentWorkspaces, + recentSketches + }); + if (toOpen) { + const { uri } = toOpen; + await this.server.setMostRecentlyUsedWorkspace(uri); + return toOpen.uri; } - - const { hash } = window.location; - // Note: here, the `uriPath` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first. - // This is important for Windows only and a NOOP on UNIX. - if (hash.length > 1 && hash.startsWith('#')) { - let uri = this.toUri(hash.slice(1)); - if (uri && await this.sketchService.isSketchFolder(uri)) { - return this.openSketchFilesInNewWindow(uri); - } - } - - // If we cannot acquire the FS path from the `location.hash` we try to get the most recently used workspace that was a valid sketch folder. - // XXX: Check if `WorkspaceServer#getRecentWorkspaces()` returns with inverse-chrolonolgical order. - const candidateUris = await this.server.getRecentWorkspaces(); - for (const uri of candidateUris) { - if (await this.sketchService.isSketchFolder(uri)) { - return this.openSketchFilesInNewWindow(uri); - } - } - - const config = await this.configService.getConfiguration(); - const { sketchDirUri } = config; - const stat = await this.fileSystem.getFileStat(sketchDirUri); - if (!stat) { - // The folder for the workspace root does not exist yet, create it. - await this.fileSystem.createFolder(sketchDirUri); - await this.sketchService.createNewSketch(sketchDirUri); - } - - const sketches = await this.sketchService.getSketches(sketchDirUri); - if (!sketches.length) { - const sketch = await this.sketchService.createNewSketch(sketchDirUri); - sketches.unshift(sketch); - } - - const uri = sketches[0].uri; - this.server.setMostRecentlyUsedWorkspace(uri); - this.openSketchFilesInNewWindow(uri); - if (ArduinoAdvancedMode.TOGGLED && await this.configService.isInSketchDir(uri)) { - return (await this.configService.getConfiguration()).sketchDirUri; - } - return uri; + return (await this.sketchService.createNewSketch()).uri; } - private toUri(uriPath: string | undefined): string | undefined { - if (uriPath) { - return new URI(toUnix(uriPath.slice(isWindows && uriPath.startsWith('/') ? 1 : 0))).withScheme('file').toString(); + private async isValid(uri: string): Promise { + const exists = await this.fileSystem.exists(uri); + if (!exists) { + return false; } - return undefined; - } - - async openSketchFilesInNewWindow(uri: string): Promise { - const url = new URL(window.location.href); - const currentSketch = url.searchParams.get('sketch'); - // Nothing to do if we want to open the same sketch which is already opened. - const sketchUri = new URI(uri); - if (!!currentSketch && new URI(currentSketch).toString() === sketchUri.toString()) { - return uri; + // The workspace root location must exist. However, when opening a workspace root in pro-mode, + // the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance. + if (!ArduinoAdvancedMode.TOGGLED) { + return true; } - - url.searchParams.set('sketch', uri); - // If in advanced mode, we root folder of all sketch folders as the hash, so the default workspace will be opened on the root - // Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See: - // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and - // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423 - if (ArduinoAdvancedMode.TOGGLED && await this.configService.isInSketchDir(uri)) { - url.hash = new URI((await this.configService.getConfiguration()).sketchDirUri).path.toString(); - } else { - // Otherwise, we set the hash as is - const hash = await this.fileSystem.getFsPath(sketchUri.toString()); - if (hash) { - url.hash = sketchUri.path.toString() - } - } - - // Preserve the current window if the `sketch` is not in the `searchParams`. - if (!currentSketch) { - setTimeout(() => window.location.href = url.toString(), 100); - return uri; - } - this.windowService.openNewWindow(url.toString()); - return uri; + const sketchFolder = await this.sketchService.isSketchFolder(uri); + return sketchFolder; } } diff --git a/arduino-ide-extension/src/browser/customization/arduino-frontend-application.ts b/arduino-ide-extension/src/browser/customization/arduino-frontend-application.ts index 58dba3b1..eb5dd642 100644 --- a/arduino-ide-extension/src/browser/customization/arduino-frontend-application.ts +++ b/arduino-ide-extension/src/browser/customization/arduino-frontend-application.ts @@ -1,24 +1,38 @@ import { injectable, inject } from 'inversify'; import { FileSystem } from '@theia/filesystem/lib/common'; import { FrontendApplication } from '@theia/core/lib/browser'; -import { ArduinoFrontendContribution } from '../arduino-frontend-contribution'; +import { ArduinoFrontendContribution, ArduinoAdvancedMode } from '../arduino-frontend-contribution'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; @injectable() export class ArduinoFrontendApplication extends FrontendApplication { - @inject(ArduinoFrontendContribution) - protected readonly frontendContribution: ArduinoFrontendContribution; - @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + @inject(ArduinoFrontendContribution) + protected readonly frontendContribution: ArduinoFrontendContribution; + protected async initializeLayout(): Promise { - await super.initializeLayout(); - const location = new URL(window.location.href); - const sketchPath = location.searchParams.get('sketch'); - if (sketchPath && await this.fileSystem.exists(sketchPath)) { - this.frontendContribution.openSketchFiles(decodeURIComponent(sketchPath)); - } + super.initializeLayout().then(() => { + // If not in PRO mode, we open the sketch file with all the related files. + // Otherwise, we reuse the workbench's restore functionality and we do not open anything at all. + // TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them? + if (!ArduinoAdvancedMode.TOGGLED) { + this.workspaceService.roots.then(roots => { + for (const root of roots) { + this.fileSystem.exists(root.uri).then(exists => { + if (exists) { + this.frontendContribution.openSketchFiles(root.uri); + } + }); + } + }); + } + }); } } diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 4b07712d..eeba1683 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -3,11 +3,15 @@ export const SketchesService = Symbol('SketchesService'); export interface SketchesService { /** * Returns with the direct sketch folders from the location of the `fileStat`. - * The sketches returns with inverchronological order, the first item is the most recent one. + * The sketches returns with inverse-chronological order, the first item is the most recent one. */ getSketches(uri?: string): Promise getSketchFiles(uri: string): Promise - createNewSketch(parentUri: string): Promise + /** + * Creates a new sketch folder in the `parentUri` location. If `parentUri` is not specified, + * it falls back to the default `sketchDirUri` from the CLI. + */ + createNewSketch(parentUri?: string): Promise isSketchFolder(uri: string): Promise } diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 73c2590e..5d5d949d 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -16,7 +16,19 @@ export class SketchesServiceImpl implements SketchesService { async getSketches(uri?: string): Promise { const sketches: Array = []; - const fsPath = FileUri.fsPath(uri ? uri : (await this.configService.getConfiguration()).sketchDirUri); + let fsPath: undefined | string; + if (!uri) { + const { sketchDirUri } = (await this.configService.getConfiguration()); + fsPath = FileUri.fsPath(sketchDirUri); + if (!fs.existsSync(fsPath)) { + fs.mkdirpSync(fsPath); + } + } else { + fsPath = FileUri.fsPath(uri); + } + if (!fs.existsSync(fsPath)) { + return []; + } const fileNames = fs.readdirSync(fsPath); for (const fileName of fileNames) { const filePath = path.join(fsPath, fileName); @@ -56,12 +68,13 @@ export class SketchesServiceImpl implements SketchesService { return this.getSketchFiles(FileUri.create(sketchDir).toString()); } - async createNewSketch(parentUri: string): Promise { + async createNewSketch(parentUri?: string): Promise { const monthNames = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december' ]; const today = new Date(); - const parent = FileUri.fsPath(parentUri); + const uri = !!parentUri ? parentUri : (await this.configService.getConfiguration()).sketchDirUri; + const parent = FileUri.fsPath(uri); const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`; let sketchName: string | undefined;