mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-15 23:36:33 +00:00
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:
parent
de1f341d19
commit
fb6785c5d3
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user