mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-17 00:06: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, {
|
registry.registerCommand(ArduinoCommands.OPEN_SKETCH, {
|
||||||
isEnabled: () => true,
|
isEnabled: () => true,
|
||||||
execute: async (sketch: Sketch) => {
|
execute: async (sketch: Sketch) => {
|
||||||
this.workspaceService.openSketchFilesInNewWindow(sketch.uri);
|
this.workspaceService.open(new URI(sketch.uri));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
|
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
|
||||||
@ -321,7 +321,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sketch = await this.sketchService.createNewSketch(uri.toString());
|
const sketch = await this.sketchService.createNewSketch(uri.toString());
|
||||||
this.workspaceService.openSketchFilesInNewWindow(sketch.uri);
|
this.workspaceService.open(new URI(sketch.uri));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.messageService.error(e.toString());
|
await this.messageService.error(e.toString());
|
||||||
}
|
}
|
||||||
@ -461,7 +461,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
|||||||
if (destinationFile && !destinationFile.isDirectory) {
|
if (destinationFile && !destinationFile.isDirectory) {
|
||||||
const message = await this.validate(destinationFile);
|
const message = await this.validate(destinationFile);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
await this.workspaceService.openSketchFilesInNewWindow(destinationFileUri.toString());
|
await this.workspaceService.open(destinationFileUri);
|
||||||
return destinationFileUri;
|
return destinationFileUri;
|
||||||
} else {
|
} else {
|
||||||
this.messageService.warn(message);
|
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 { injectable, inject } from 'inversify';
|
||||||
import { toUnix } from 'upath';
|
// import { toUnix } from 'upath';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
// import URI from '@theia/core/lib/common/uri';
|
||||||
import { isWindows } from '@theia/core/lib/common/os';
|
// import { isWindows } from '@theia/core/lib/common/os';
|
||||||
import { LabelProvider } from '@theia/core/lib/browser';
|
import { LabelProvider } from '@theia/core/lib/browser';
|
||||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||||
import { ConfigService } from '../common/protocol/config-service';
|
import { ConfigService } from '../common/protocol/config-service';
|
||||||
import { SketchesService } from '../common/protocol/sketches-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';
|
import { ArduinoAdvancedMode } from './arduino-frontend-contribution';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is workaround to have custom frontend binding for the default workspace, although we
|
* This is workaround to have custom frontend binding for the default workspace, although we
|
||||||
* already have a custom binding for the backend.
|
* 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()
|
@injectable()
|
||||||
export class ArduinoWorkspaceService extends WorkspaceService {
|
export class ArduinoWorkspaceService extends WorkspaceService {
|
||||||
@ -25,105 +38,38 @@ export class ArduinoWorkspaceService extends WorkspaceService {
|
|||||||
protected readonly labelProvider: LabelProvider;
|
protected readonly labelProvider: LabelProvider;
|
||||||
|
|
||||||
async getDefaultWorkspacePath(): Promise<string | undefined> {
|
async getDefaultWorkspacePath(): Promise<string | undefined> {
|
||||||
const url = new URL(window.location.href);
|
const [hash, recentWorkspaces, recentSketches] = await Promise.all([
|
||||||
// If `sketch` is set and valid, we use it as is.
|
window.location.hash,
|
||||||
// `sketch` is set as an encoded URI string.
|
this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)),
|
||||||
const sketch = url.searchParams.get('sketch');
|
this.server.getRecentWorkspaces()
|
||||||
if (sketch) {
|
]);
|
||||||
const sketchDirUri = new URI(sketch).toString();
|
const toOpen = await new ArduinoWorkspaceRootResolver({
|
||||||
if (await this.sketchService.isSketchFolder(sketchDirUri)) {
|
isValid: this.isValid.bind(this)
|
||||||
if (await this.configService.isInSketchDir(sketchDirUri)) {
|
}).resolve({
|
||||||
if (ArduinoAdvancedMode.TOGGLED) {
|
hash,
|
||||||
return (await this.configService.getConfiguration()).sketchDirUri
|
recentWorkspaces,
|
||||||
} else {
|
recentSketches
|
||||||
return sketchDirUri;
|
});
|
||||||
}
|
if (toOpen) {
|
||||||
}
|
const { uri } = toOpen;
|
||||||
return (await this.configService.getConfiguration()).sketchDirUri
|
await this.server.setMostRecentlyUsedWorkspace(uri);
|
||||||
}
|
return toOpen.uri;
|
||||||
}
|
}
|
||||||
|
return (await this.sketchService.createNewSketch()).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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private toUri(uriPath: string | undefined): string | undefined {
|
private async isValid(uri: string): Promise<boolean> {
|
||||||
if (uriPath) {
|
const exists = await this.fileSystem.exists(uri);
|
||||||
return new URI(toUnix(uriPath.slice(isWindows && uriPath.startsWith('/') ? 1 : 0))).withScheme('file').toString();
|
if (!exists) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return undefined;
|
// 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) {
|
||||||
async openSketchFilesInNewWindow(uri: string): Promise<string> {
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
const sketchFolder = await this.sketchService.isSketchFolder(uri);
|
||||||
url.searchParams.set('sketch', uri);
|
return sketchFolder;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,38 @@
|
|||||||
import { injectable, inject } from 'inversify';
|
import { injectable, inject } from 'inversify';
|
||||||
import { FileSystem } from '@theia/filesystem/lib/common';
|
import { FileSystem } from '@theia/filesystem/lib/common';
|
||||||
import { FrontendApplication } from '@theia/core/lib/browser';
|
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()
|
@injectable()
|
||||||
export class ArduinoFrontendApplication extends FrontendApplication {
|
export class ArduinoFrontendApplication extends FrontendApplication {
|
||||||
|
|
||||||
@inject(ArduinoFrontendContribution)
|
|
||||||
protected readonly frontendContribution: ArduinoFrontendContribution;
|
|
||||||
|
|
||||||
@inject(FileSystem)
|
@inject(FileSystem)
|
||||||
protected readonly fileSystem: FileSystem;
|
protected readonly fileSystem: FileSystem;
|
||||||
|
|
||||||
|
@inject(WorkspaceService)
|
||||||
|
protected readonly workspaceService: WorkspaceService;
|
||||||
|
|
||||||
|
@inject(ArduinoFrontendContribution)
|
||||||
|
protected readonly frontendContribution: ArduinoFrontendContribution;
|
||||||
|
|
||||||
protected async initializeLayout(): Promise<void> {
|
protected async initializeLayout(): Promise<void> {
|
||||||
await super.initializeLayout();
|
super.initializeLayout().then(() => {
|
||||||
const location = new URL(window.location.href);
|
// If not in PRO mode, we open the sketch file with all the related files.
|
||||||
const sketchPath = location.searchParams.get('sketch');
|
// Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
|
||||||
if (sketchPath && await this.fileSystem.exists(sketchPath)) {
|
// TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
|
||||||
this.frontendContribution.openSketchFiles(decodeURIComponent(sketchPath));
|
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 {
|
export interface SketchesService {
|
||||||
/**
|
/**
|
||||||
* Returns with the direct sketch folders from the location of the `fileStat`.
|
* 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[]>
|
getSketches(uri?: string): Promise<Sketch[]>
|
||||||
getSketchFiles(uri: string): Promise<string[]>
|
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>
|
isSketchFolder(uri: string): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,19 @@ export class SketchesServiceImpl implements SketchesService {
|
|||||||
|
|
||||||
async getSketches(uri?: string): Promise<Sketch[]> {
|
async getSketches(uri?: string): Promise<Sketch[]> {
|
||||||
const sketches: Array<Sketch & { mtimeMs: number }> = [];
|
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);
|
const fileNames = fs.readdirSync(fsPath);
|
||||||
for (const fileName of fileNames) {
|
for (const fileName of fileNames) {
|
||||||
const filePath = path.join(fsPath, fileName);
|
const filePath = path.join(fsPath, fileName);
|
||||||
@ -56,12 +68,13 @@ export class SketchesServiceImpl implements SketchesService {
|
|||||||
return this.getSketchFiles(FileUri.create(sketchDir).toString());
|
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',
|
const monthNames = ['january', 'february', 'march', 'april', 'may', 'june',
|
||||||
'july', 'august', 'september', 'october', 'november', 'december'
|
'july', 'august', 'september', 'october', 'november', 'december'
|
||||||
];
|
];
|
||||||
const today = new Date();
|
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()}`;
|
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
|
||||||
let sketchName: string | undefined;
|
let sketchName: string | undefined;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user