PROEDITOR-53: Changed the way we set the workspace

Got rid of the `sketch` search parameter from the URL.

Rules:
 - Get the desired workspace location from the
  - `Path` defined as the `window.location.hash` of the URL,
  - most recent workspaces,
  - most recent sketches from the default sketch folder.
 - Validate the location.
 - If no valid location was found, create a new sketch in the default sketch folder.

Note: when validating the location of the workspace root, the root must always exist. However, when in pro-mode, the desired workspace root must
not be a sketch directory with the `.ino` file, but can be any existing location.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2019-10-22 14:59:31 +02:00
parent de1f341d19
commit fb6785c5d3
6 changed files with 160 additions and 115 deletions

View File

@ -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);

View File

@ -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<boolean>;
}
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<boolean> {
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;
}
}

View File

@ -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<string | undefined> {
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<boolean> {
const exists = await this.fileSystem.exists(uri);
if (!exists) {
return false;
}
return undefined;
}
async openSketchFilesInNewWindow(uri: string): Promise<string> {
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;
}
}

View File

@ -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<void> {
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);
}
});
}
});
}
});
}
}

View File

@ -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<Sketch[]>
getSketchFiles(uri: string): Promise<string[]>
createNewSketch(parentUri: string): Promise<Sketch>
/**
* 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<Sketch>
isSketchFolder(uri: string): Promise<boolean>
}

View File

@ -16,7 +16,19 @@ export class SketchesServiceImpl implements SketchesService {
async getSketches(uri?: string): Promise<Sketch[]> {
const sketches: Array<Sketch & { mtimeMs: number }> = [];
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<Sketch> {
async createNewSketch(parentUri?: string): Promise<Sketch> {
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;