Skip to content

Commit fb6785c

Browse files
author
Akos Kitta
committed
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>
1 parent de1f341 commit fb6785c

File tree

6 files changed

+160
-115
lines changed

6 files changed

+160
-115
lines changed

arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
302302
registry.registerCommand(ArduinoCommands.OPEN_SKETCH, {
303303
isEnabled: () => true,
304304
execute: async (sketch: Sketch) => {
305-
this.workspaceService.openSketchFilesInNewWindow(sketch.uri);
305+
this.workspaceService.open(new URI(sketch.uri));
306306
}
307307
})
308308
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
@@ -321,7 +321,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
321321
}
322322

323323
const sketch = await this.sketchService.createNewSketch(uri.toString());
324-
this.workspaceService.openSketchFilesInNewWindow(sketch.uri);
324+
this.workspaceService.open(new URI(sketch.uri));
325325
} catch (e) {
326326
await this.messageService.error(e.toString());
327327
}
@@ -461,7 +461,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
461461
if (destinationFile && !destinationFile.isDirectory) {
462462
const message = await this.validate(destinationFile);
463463
if (!message) {
464-
await this.workspaceService.openSketchFilesInNewWindow(destinationFileUri.toString());
464+
await this.workspaceService.open(destinationFileUri);
465465
return destinationFileUri;
466466
} else {
467467
this.messageService.warn(message);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { toUnix } from 'upath';
2+
import URI from '@theia/core/lib/common/uri';
3+
import { isWindows } from '@theia/core/lib/common/os';
4+
import { notEmpty } from '@theia/core/lib/common/objects';
5+
import { MaybePromise } from '@theia/core/lib/common/types';
6+
7+
/**
8+
* Class for determining the default workspace location from the
9+
* `location.hash`, the historical workspace locations, and recent sketch files.
10+
*
11+
* The following logic is used for determining the default workspace location:
12+
* - `hash` points to an exists in location?
13+
* - Yes
14+
* - `validate location`. Is valid sketch location?
15+
* - Yes
16+
* - Done.
17+
* - No
18+
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
19+
* - No
20+
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
21+
*/
22+
namespace ArduinoWorkspaceRootResolver {
23+
export interface InitOptions {
24+
readonly isValid: (uri: string) => MaybePromise<boolean>;
25+
}
26+
export interface ResolveOptions {
27+
readonly hash?: string
28+
readonly recentWorkspaces: string[];
29+
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
30+
readonly recentSketches: string[];
31+
}
32+
}
33+
export class ArduinoWorkspaceRootResolver {
34+
35+
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {
36+
}
37+
38+
async resolve(options: ArduinoWorkspaceRootResolver.ResolveOptions): Promise<{ uri: string } | undefined> {
39+
const { hash, recentWorkspaces, recentSketches } = options;
40+
for (const uri of [this.hashToUri(hash), ...recentWorkspaces, ...recentSketches].filter(notEmpty)) {
41+
const valid = await this.isValid(uri);
42+
if (valid) {
43+
return { uri };
44+
}
45+
}
46+
return undefined;
47+
}
48+
49+
protected isValid(uri: string): MaybePromise<boolean> {
50+
return this.options.isValid.bind(this)(uri);
51+
}
52+
53+
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
54+
// This is important for Windows only and a NOOP on POSIX.
55+
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
56+
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
57+
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
58+
protected hashToUri(hash: string | undefined): string | undefined {
59+
if (hash
60+
&& hash.length > 1
61+
&& hash.startsWith('#')) {
62+
const path = hash.slice(1); // Trim the leading `#`.
63+
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
64+
}
65+
return undefined;
66+
}
67+
68+
}
Lines changed: 43 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
import { injectable, inject } from 'inversify';
2-
import { toUnix } from 'upath';
3-
import URI from '@theia/core/lib/common/uri';
4-
import { isWindows } from '@theia/core/lib/common/os';
2+
// import { toUnix } from 'upath';
3+
// import URI from '@theia/core/lib/common/uri';
4+
// import { isWindows } from '@theia/core/lib/common/os';
55
import { LabelProvider } from '@theia/core/lib/browser';
66
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
77
import { ConfigService } from '../common/protocol/config-service';
88
import { SketchesService } from '../common/protocol/sketches-service';
9+
// import { ArduinoAdvancedMode } from './arduino-frontend-contribution';
10+
import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
911
import { ArduinoAdvancedMode } from './arduino-frontend-contribution';
1012

1113
/**
1214
* This is workaround to have custom frontend binding for the default workspace, although we
1315
* already have a custom binding for the backend.
16+
*
17+
* The following logic is used for determining the default workspace location:
18+
* - #hash exists in location?
19+
* - Yes
20+
* - `validateHash`. Is valid sketch location?
21+
* - Yes
22+
* - Done.
23+
* - No
24+
* - `checkHistoricalWorkspaceRoots`, `try open last modified sketch`,create new sketch`.
25+
* - No
26+
* - `checkHistoricalWorkspaceRoots`, `try open last modified sketch`, `create new sketch`.
1427
*/
1528
@injectable()
1629
export class ArduinoWorkspaceService extends WorkspaceService {
@@ -25,105 +38,38 @@ export class ArduinoWorkspaceService extends WorkspaceService {
2538
protected readonly labelProvider: LabelProvider;
2639

2740
async getDefaultWorkspacePath(): Promise<string | undefined> {
28-
const url = new URL(window.location.href);
29-
// If `sketch` is set and valid, we use it as is.
30-
// `sketch` is set as an encoded URI string.
31-
const sketch = url.searchParams.get('sketch');
32-
if (sketch) {
33-
const sketchDirUri = new URI(sketch).toString();
34-
if (await this.sketchService.isSketchFolder(sketchDirUri)) {
35-
if (await this.configService.isInSketchDir(sketchDirUri)) {
36-
if (ArduinoAdvancedMode.TOGGLED) {
37-
return (await this.configService.getConfiguration()).sketchDirUri
38-
} else {
39-
return sketchDirUri;
40-
}
41-
}
42-
return (await this.configService.getConfiguration()).sketchDirUri
43-
}
41+
const [hash, recentWorkspaces, recentSketches] = await Promise.all([
42+
window.location.hash,
43+
this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)),
44+
this.server.getRecentWorkspaces()
45+
]);
46+
const toOpen = await new ArduinoWorkspaceRootResolver({
47+
isValid: this.isValid.bind(this)
48+
}).resolve({
49+
hash,
50+
recentWorkspaces,
51+
recentSketches
52+
});
53+
if (toOpen) {
54+
const { uri } = toOpen;
55+
await this.server.setMostRecentlyUsedWorkspace(uri);
56+
return toOpen.uri;
4457
}
45-
46-
const { hash } = window.location;
47-
// Note: here, the `uriPath` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
48-
// This is important for Windows only and a NOOP on UNIX.
49-
if (hash.length > 1 && hash.startsWith('#')) {
50-
let uri = this.toUri(hash.slice(1));
51-
if (uri && await this.sketchService.isSketchFolder(uri)) {
52-
return this.openSketchFilesInNewWindow(uri);
53-
}
54-
}
55-
56-
// 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.
57-
// XXX: Check if `WorkspaceServer#getRecentWorkspaces()` returns with inverse-chrolonolgical order.
58-
const candidateUris = await this.server.getRecentWorkspaces();
59-
for (const uri of candidateUris) {
60-
if (await this.sketchService.isSketchFolder(uri)) {
61-
return this.openSketchFilesInNewWindow(uri);
62-
}
63-
}
64-
65-
const config = await this.configService.getConfiguration();
66-
const { sketchDirUri } = config;
67-
const stat = await this.fileSystem.getFileStat(sketchDirUri);
68-
if (!stat) {
69-
// The folder for the workspace root does not exist yet, create it.
70-
await this.fileSystem.createFolder(sketchDirUri);
71-
await this.sketchService.createNewSketch(sketchDirUri);
72-
}
73-
74-
const sketches = await this.sketchService.getSketches(sketchDirUri);
75-
if (!sketches.length) {
76-
const sketch = await this.sketchService.createNewSketch(sketchDirUri);
77-
sketches.unshift(sketch);
78-
}
79-
80-
const uri = sketches[0].uri;
81-
this.server.setMostRecentlyUsedWorkspace(uri);
82-
this.openSketchFilesInNewWindow(uri);
83-
if (ArduinoAdvancedMode.TOGGLED && await this.configService.isInSketchDir(uri)) {
84-
return (await this.configService.getConfiguration()).sketchDirUri;
85-
}
86-
return uri;
87-
}
88-
89-
private toUri(uriPath: string | undefined): string | undefined {
90-
if (uriPath) {
91-
return new URI(toUnix(uriPath.slice(isWindows && uriPath.startsWith('/') ? 1 : 0))).withScheme('file').toString();
92-
}
93-
return undefined;
58+
return (await this.sketchService.createNewSketch()).uri;
9459
}
9560

96-
async openSketchFilesInNewWindow(uri: string): Promise<string> {
97-
const url = new URL(window.location.href);
98-
const currentSketch = url.searchParams.get('sketch');
99-
// Nothing to do if we want to open the same sketch which is already opened.
100-
const sketchUri = new URI(uri);
101-
if (!!currentSketch && new URI(currentSketch).toString() === sketchUri.toString()) {
102-
return uri;
103-
}
104-
105-
url.searchParams.set('sketch', uri);
106-
// If in advanced mode, we root folder of all sketch folders as the hash, so the default workspace will be opened on the root
107-
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
108-
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
109-
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
110-
if (ArduinoAdvancedMode.TOGGLED && await this.configService.isInSketchDir(uri)) {
111-
url.hash = new URI((await this.configService.getConfiguration()).sketchDirUri).path.toString();
112-
} else {
113-
// Otherwise, we set the hash as is
114-
const hash = await this.fileSystem.getFsPath(sketchUri.toString());
115-
if (hash) {
116-
url.hash = sketchUri.path.toString()
117-
}
61+
private async isValid(uri: string): Promise<boolean> {
62+
const exists = await this.fileSystem.exists(uri);
63+
if (!exists) {
64+
return false;
11865
}
119-
120-
// Preserve the current window if the `sketch` is not in the `searchParams`.
121-
if (!currentSketch) {
122-
setTimeout(() => window.location.href = url.toString(), 100);
123-
return uri;
66+
// The workspace root location must exist. However, when opening a workspace root in pro-mode,
67+
// the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
68+
if (!ArduinoAdvancedMode.TOGGLED) {
69+
return true;
12470
}
125-
this.windowService.openNewWindow(url.toString());
126-
return uri;
71+
const sketchFolder = await this.sketchService.isSketchFolder(uri);
72+
return sketchFolder;
12773
}
12874

12975
}
Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
import { injectable, inject } from 'inversify';
22
import { FileSystem } from '@theia/filesystem/lib/common';
33
import { FrontendApplication } from '@theia/core/lib/browser';
4-
import { ArduinoFrontendContribution } from '../arduino-frontend-contribution';
4+
import { ArduinoFrontendContribution, ArduinoAdvancedMode } from '../arduino-frontend-contribution';
5+
import { WorkspaceService } from '@theia/workspace/lib/browser';
56

67
@injectable()
78
export class ArduinoFrontendApplication extends FrontendApplication {
89

9-
@inject(ArduinoFrontendContribution)
10-
protected readonly frontendContribution: ArduinoFrontendContribution;
11-
1210
@inject(FileSystem)
1311
protected readonly fileSystem: FileSystem;
1412

13+
@inject(WorkspaceService)
14+
protected readonly workspaceService: WorkspaceService;
15+
16+
@inject(ArduinoFrontendContribution)
17+
protected readonly frontendContribution: ArduinoFrontendContribution;
18+
1519
protected async initializeLayout(): Promise<void> {
16-
await super.initializeLayout();
17-
const location = new URL(window.location.href);
18-
const sketchPath = location.searchParams.get('sketch');
19-
if (sketchPath && await this.fileSystem.exists(sketchPath)) {
20-
this.frontendContribution.openSketchFiles(decodeURIComponent(sketchPath));
21-
}
20+
super.initializeLayout().then(() => {
21+
// If not in PRO mode, we open the sketch file with all the related files.
22+
// Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
23+
// TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
24+
if (!ArduinoAdvancedMode.TOGGLED) {
25+
this.workspaceService.roots.then(roots => {
26+
for (const root of roots) {
27+
this.fileSystem.exists(root.uri).then(exists => {
28+
if (exists) {
29+
this.frontendContribution.openSketchFiles(root.uri);
30+
}
31+
});
32+
}
33+
});
34+
}
35+
});
2236
}
2337

2438
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ export const SketchesService = Symbol('SketchesService');
33
export interface SketchesService {
44
/**
55
* Returns with the direct sketch folders from the location of the `fileStat`.
6-
* The sketches returns with inverchronological order, the first item is the most recent one.
6+
* The sketches returns with inverse-chronological order, the first item is the most recent one.
77
*/
88
getSketches(uri?: string): Promise<Sketch[]>
99
getSketchFiles(uri: string): Promise<string[]>
10-
createNewSketch(parentUri: string): Promise<Sketch>
10+
/**
11+
* Creates a new sketch folder in the `parentUri` location. If `parentUri` is not specified,
12+
* it falls back to the default `sketchDirUri` from the CLI.
13+
*/
14+
createNewSketch(parentUri?: string): Promise<Sketch>
1115
isSketchFolder(uri: string): Promise<boolean>
1216
}
1317

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,19 @@ export class SketchesServiceImpl implements SketchesService {
1616

1717
async getSketches(uri?: string): Promise<Sketch[]> {
1818
const sketches: Array<Sketch & { mtimeMs: number }> = [];
19-
const fsPath = FileUri.fsPath(uri ? uri : (await this.configService.getConfiguration()).sketchDirUri);
19+
let fsPath: undefined | string;
20+
if (!uri) {
21+
const { sketchDirUri } = (await this.configService.getConfiguration());
22+
fsPath = FileUri.fsPath(sketchDirUri);
23+
if (!fs.existsSync(fsPath)) {
24+
fs.mkdirpSync(fsPath);
25+
}
26+
} else {
27+
fsPath = FileUri.fsPath(uri);
28+
}
29+
if (!fs.existsSync(fsPath)) {
30+
return [];
31+
}
2032
const fileNames = fs.readdirSync(fsPath);
2133
for (const fileName of fileNames) {
2234
const filePath = path.join(fsPath, fileName);
@@ -56,12 +68,13 @@ export class SketchesServiceImpl implements SketchesService {
5668
return this.getSketchFiles(FileUri.create(sketchDir).toString());
5769
}
5870

59-
async createNewSketch(parentUri: string): Promise<Sketch> {
71+
async createNewSketch(parentUri?: string): Promise<Sketch> {
6072
const monthNames = ['january', 'february', 'march', 'april', 'may', 'june',
6173
'july', 'august', 'september', 'october', 'november', 'december'
6274
];
6375
const today = new Date();
64-
const parent = FileUri.fsPath(parentUri);
76+
const uri = !!parentUri ? parentUri : (await this.configService.getConfiguration()).sketchDirUri;
77+
const parent = FileUri.fsPath(uri);
6578

6679
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
6780
let sketchName: string | undefined;

0 commit comments

Comments
 (0)