PROEDITOR-48: Open last sketch at start-up

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2019-09-23 15:31:41 +02:00
parent 7244694bd3
commit 55923be7fd
26 changed files with 357 additions and 318 deletions

View File

@ -31,6 +31,7 @@
"p-queue": "^5.0.0", "p-queue": "^5.0.0",
"ps-tree": "^1.2.0", "ps-tree": "^1.2.0",
"tree-kill": "^1.2.1", "tree-kill": "^1.2.1",
"upath": "^1.1.2",
"which": "^1.3.1" "which": "^1.3.1"
}, },
"scripts": { "scripts": {

View File

@ -15,8 +15,6 @@ import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-fro
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR } from '@theia/core'; import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR } from '@theia/core';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { SketchFactory } from './sketch-factory';
import { ArduinoToolbar } from './toolbar/arduino-toolbar'; import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser'; import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
import { import {
@ -26,8 +24,7 @@ import {
StatusBar, StatusBar,
ShellLayoutRestorer, ShellLayoutRestorer,
StatusBarAlignment, StatusBarAlignment,
QuickOpenService, QuickOpenService
LabelProvider
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
@ -47,6 +44,7 @@ import { MonitorService } from '../common/protocol/monitor-service';
import { ConfigService } from '../common/protocol/config-service'; import { ConfigService } from '../common/protocol/config-service';
import { MonitorConnection } from './monitor/monitor-connection'; import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution'; import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
export namespace ArduinoMenus { export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
@ -61,7 +59,6 @@ export namespace ArduinoAdvancedMode {
})(); })();
} }
@injectable() @injectable()
export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution, MenuContribution { export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution, MenuContribution {
@ -95,9 +92,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(SelectionService) @inject(SelectionService)
protected readonly selectionService: SelectionService; protected readonly selectionService: SelectionService;
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@ -117,7 +111,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
protected readonly windowService: WindowService; protected readonly windowService: WindowService;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketches: SketchesService; protected readonly sketchService: SketchesService;
@inject(BoardsConfigDialog) @inject(BoardsConfigDialog)
protected readonly boardsConfigDialog: BoardsConfigDialog; protected readonly boardsConfigDialog: BoardsConfigDialog;
@ -134,17 +128,15 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(ShellLayoutRestorer) @inject(ShellLayoutRestorer)
protected readonly layoutRestorer: ShellLayoutRestorer; protected readonly layoutRestorer: ShellLayoutRestorer;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(QuickOpenService) @inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService; protected readonly quickOpenService: QuickOpenService;
@inject(WorkspaceService) @inject(ArduinoWorkspaceService)
protected readonly workspaceService: WorkspaceService; protected readonly workspaceService: ArduinoWorkspaceService;
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(MonitorConnection) @inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection; protected readonly monitorConnection: MonitorConnection;
@ -304,7 +296,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.openSketchFilesInNewWindow(sketch.uri); this.workspaceService.openSketchFilesInNewWindow(sketch.uri);
} }
}) })
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, { registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
@ -322,7 +314,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
uri = uri.withPath(uri.path.dir.dir); uri = uri.withPath(uri.path.dir.dir);
} }
await this.sketchFactory.createNewSketch(uri); const sketch = await this.sketchService.createNewSketch(uri.toString());
this.workspaceService.openSketchFilesInNewWindow(sketch.uri);
} catch (e) { } catch (e) {
await this.messageService.error(e.toString()); await this.messageService.error(e.toString());
} }
@ -397,8 +390,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
return menuId; return menuId;
} }
protected registerSketchesInMenu(registry: MenuModelRegistry) { protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise<void> {
this.getWorkspaceSketches().then(sketches => { this.sketchService.getSketches().then(sketches => {
this.wsSketchCount = sketches.length; this.wsSketchCount = sketches.length;
sketches.forEach(sketch => { sketches.forEach(sketch => {
const command: Command = { const command: Command = {
@ -416,48 +409,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
}) })
} }
protected async getWorkspaceSketches(): Promise<Sketch[]> { async openSketchFiles(uri: string): Promise<void> {
this.sketchService.getSketchFiles(uri).then(uris => {
let sketches: Sketch[] = [];
const config = await this.configService.getConfiguration();
const stat = await this.fileSystem.getFileStat(config.sketchDirUri);
if (!!stat) {
sketches = await this.sketches.getSketches(stat);
}
return sketches;
}
protected async openSketchFilesInNewWindow(uri: 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()) {
this.messageService.info(`The '${this.labelProvider.getLongName(sketchUri)}' is already opened.`);
// NOOP.
return;
}
// Preserve the current window if the `sketch` is not in the `searchParams`.
url.searchParams.set('sketch', uri);
const hash = await this.fileSystem.getFsPath(sketchUri.toString());
if (hash) {
url.hash = hash;
}
if (!currentSketch) {
setTimeout(() => window.location.href = url.toString(), 100);
return;
}
this.windowService.openNewWindow(url.toString());
}
async openSketchFiles(uri: string) {
const fileStat = await this.fileSystem.getFileStat(uri);
if (fileStat) {
const uris = await this.sketches.getSketchFiles(fileStat);
for (const uri of uris) { for (const uri of uris) {
this.editorManager.open(new URI(uri)); this.editorManager.open(new URI(uri));
} }
} });
} }
/** /**
@ -481,7 +438,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.openSketchFilesInNewWindow(destinationFileUri.toString()); await this.workspaceService.openSketchFilesInNewWindow(destinationFileUri.toString());
return destinationFileUri; return destinationFileUri;
} else { } else {
this.messageService.warn(message); this.messageService.warn(message);

View File

@ -25,12 +25,11 @@ import { ToolOutputService } from '../common/protocol/tool-output-service';
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl'; import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { AWorkspaceService } from './arduino-workspace-service'; import { ArduinoWorkspaceService } from './arduino-workspace-service';
import { ThemeService } from '@theia/core/lib/browser/theming'; import { ThemeService } from '@theia/core/lib/browser/theming';
import { ArduinoTheme } from './arduino-theme'; import { ArduinoTheme } from './arduino-theme';
import { ArduinoToolbarMenuContribution } from './arduino-file-menu'; import { ArduinoToolbarMenuContribution } from './arduino-file-menu';
import { MenuContribution } from '@theia/core'; import { MenuContribution } from '@theia/core';
import { SketchFactory } from './sketch-factory';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { SilentOutlineViewContribution } from './customization/silent-outline-contribution'; import { SilentOutlineViewContribution } from './customization/silent-outline-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution'; import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
@ -41,12 +40,12 @@ import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contributi
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution'; import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { ArduinoOutputToolContribution } from './customization/silent-output-tool-contribution'; import { ArduinoOutputToolContribution } from './customization/silent-output-tool-contribution';
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution'; import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { CustomEditorContribution } from './customization/custom-editor-contribution'; import { ArduinoEditorContribution } from './customization/arduino-editor-contribution';
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution'; import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { SilentMonacoStatusBarContribution } from './customization/silent-monaco-status-bar-contribution'; import { ArduinoMonacoStatusBarContribution } from './customization/arduino-monaco-status-bar-contribution';
import { ApplicationShell } from '@theia/core/lib/browser'; import { ApplicationShell } from '@theia/core/lib/browser';
import { CustomApplicationShell } from './customization/custom-application-shell'; import { ArduinoApplicationShell } from './customization/arduino-application-shell';
import { CustomFrontendApplication } from './customization/custom-frontend-application'; import { ArduinoFrontendApplication } from './customization/arduino-frontend-application';
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog'; import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget'; import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution'; import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
@ -189,9 +188,8 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
return client; return client;
}).inSingletonScope(); }).inSingletonScope();
bind(AWorkspaceService).toSelf().inSingletonScope(); bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope(); rebind(WorkspaceService).to(ArduinoWorkspaceService).inSingletonScope();
bind(SketchFactory).toSelf().inSingletonScope();
const themeService = ThemeService.get(); const themeService = ThemeService.get();
themeService.register(...ArduinoTheme.themes); themeService.register(...ArduinoTheme.themes);
@ -207,11 +205,11 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
unbind(OutputToolbarContribution); unbind(OutputToolbarContribution);
bind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope(); bind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
unbind(EditorContribution); unbind(EditorContribution);
bind(EditorContribution).to(CustomEditorContribution).inSingletonScope(); bind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
unbind(MonacoStatusBarContribution); unbind(MonacoStatusBarContribution);
bind(MonacoStatusBarContribution).to(SilentMonacoStatusBarContribution).inSingletonScope(); bind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
unbind(ApplicationShell); unbind(ApplicationShell);
bind(ApplicationShell).to(CustomApplicationShell).inSingletonScope(); bind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
unbind(ScmContribution); unbind(ScmContribution);
bind(ScmContribution).to(SilentScmContribution).inSingletonScope(); bind(ScmContribution).to(SilentScmContribution).inSingletonScope();
unbind(SearchInWorkspaceFrontendContribution); unbind(SearchInWorkspaceFrontendContribution);
@ -221,7 +219,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
document.body.classList.add(ArduinoAdvancedMode.LS_ID); document.body.classList.add(ArduinoAdvancedMode.LS_ID);
} }
unbind(FrontendApplication); unbind(FrontendApplication);
bind(FrontendApplication).to(CustomFrontendApplication).inSingletonScope(); bind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
// monaco customizations // monaco customizations
unbind(MonacoEditorProvider); unbind(MonacoEditorProvider);

View File

@ -1,49 +1,129 @@
import { WorkspaceService } from "@theia/workspace/lib/browser/workspace-service"; import { injectable, inject } from 'inversify';
import { injectable, inject } from "inversify"; import { toUnix } from 'upath';
import { WorkspaceServer } from "@theia/workspace/lib/common"; import URI from '@theia/core/lib/common/uri';
import { FileSystem, FileStat } from "@theia/filesystem/lib/common"; import { isWindows } from '@theia/core/lib/common/os';
import URI from "@theia/core/lib/common/uri"; import { LabelProvider } from '@theia/core/lib/browser';
import { SketchFactory } from "./sketch-factory"; 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 { 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.
*/ */
@injectable() @injectable()
export class AWorkspaceService extends WorkspaceService { export class ArduinoWorkspaceService extends WorkspaceService {
@inject(WorkspaceServer) @inject(SketchesService)
protected readonly workspaceServer: WorkspaceServer; protected readonly sketchService: SketchesService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
protected async getDefaultWorkspacePath(): Promise<string | undefined> { @inject(LabelProvider)
let result = await super.getDefaultWorkspacePath(); protected readonly labelProvider: LabelProvider;
if (!result) {
const config = await this.configService.getConfiguration(); async getDefaultWorkspacePath(): Promise<string | undefined> {
result = config.sketchDirUri; 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 stat = await this.fileSystem.getFileStat(result); 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) { if (!stat) {
// workspace does not exist yet, create it // The folder for the workspace root does not exist yet, create it.
await this.fileSystem.createFolder(result); await this.fileSystem.createFolder(sketchDirUri);
await this.sketchFactory.createNewSketch(new URI(result)); await this.sketchService.createNewSketch(sketchDirUri);
} }
return result; 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;
} }
protected async setWorkspace(workspaceStat: FileStat | undefined): Promise<void> { private toUri(uriPath: string | undefined): string | undefined {
await super.setWorkspace(workspaceStat); if (uriPath) {
return new URI(toUnix(uriPath.slice(isWindows && uriPath.startsWith('/') ? 1 : 0))).withScheme('file').toString();
}
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;
}
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;
}
}

View File

@ -1,7 +1,7 @@
import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser'; import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser';
import { EditorWidget } from '@theia/editor/lib/browser'; import { EditorWidget } from '@theia/editor/lib/browser';
export class CustomApplicationShell extends ApplicationShell { export class ArduinoApplicationShell extends ApplicationShell {
protected refreshBottomPanelToggleButton() { protected refreshBottomPanelToggleButton() {
} }
@ -30,4 +30,4 @@ export class CustomApplicationShell extends ApplicationShell {
} }
} }
} }

View File

@ -1,10 +1,11 @@
import { injectable } from "inversify"; import { injectable } from 'inversify';
import { CommonFrontendContribution, CommonMenus, CommonCommands } from "@theia/core/lib/browser"; import { CommonFrontendContribution, CommonMenus, CommonCommands } from '@theia/core/lib/browser';
import { MenuModelRegistry } from "@theia/core"; import { MenuModelRegistry } from '@theia/core';
import { ArduinoAdvancedMode } from "../arduino-frontend-contribution"; import { ArduinoAdvancedMode } from '../arduino-frontend-contribution';
@injectable() @injectable()
export class CustomCommonFrontendContribution extends CommonFrontendContribution { export class ArduinoCommonFrontendContribution extends CommonFrontendContribution {
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
if (!ArduinoAdvancedMode.TOGGLED) { if (!ArduinoAdvancedMode.TOGGLED) {
registry.registerSubmenu(CommonMenus.FILE, 'File'); registry.registerSubmenu(CommonMenus.FILE, 'File');
@ -46,4 +47,5 @@ export class CustomCommonFrontendContribution extends CommonFrontendContribution
super.registerMenus(registry); super.registerMenus(registry);
} }
} }
}
}

View File

@ -1,8 +1,9 @@
import {EditorContribution} from '@theia/editor/lib/browser/editor-contribution'; import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { TextEditor } from '@theia/editor/lib/browser'; import { TextEditor } from '@theia/editor/lib/browser';
import { StatusBarAlignment } from '@theia/core/lib/browser'; import { StatusBarAlignment } from '@theia/core/lib/browser';
export class CustomEditorContribution extends EditorContribution { export class ArduinoEditorContribution extends EditorContribution {
protected updateLanguageStatus(editor: TextEditor | undefined): void { protected updateLanguageStatus(editor: TextEditor | undefined): void {
} }
@ -18,4 +19,5 @@ export class CustomEditorContribution extends EditorContribution {
priority: 100 priority: 100
}); });
} }
}
}

View File

@ -0,0 +1,11 @@
import { injectable } from 'inversify';
import { FileMenuContribution } from '@theia/workspace/lib/browser';
import { MenuModelRegistry } from '@theia/core';
@injectable()
export class ArduinoFileMenuContribution extends FileMenuContribution {
registerMenus(registry: MenuModelRegistry) {
}
}

View File

@ -0,0 +1,24 @@
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';
@injectable()
export class ArduinoFrontendApplication extends FrontendApplication {
@inject(ArduinoFrontendContribution)
protected readonly frontendContribution: ArduinoFrontendContribution;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
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));
}
}
}

View File

@ -0,0 +1,11 @@
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
export class ArduinoMonacoStatusBarContribution extends MonacoStatusBarContribution {
protected setConfigTabSizeWidget() {
}
protected setLineEndingWidget() {
}
}

View File

@ -1,10 +0,0 @@
import { injectable } from "inversify";
import { FileMenuContribution } from "@theia/workspace/lib/browser";
import { MenuModelRegistry } from "@theia/core";
@injectable()
export class CustomFileMenuContribution extends FileMenuContribution {
registerMenus(registry: MenuModelRegistry) {
}
}

View File

@ -1,19 +0,0 @@
import { injectable, inject } from "inversify";
import { FrontendApplication } from "@theia/core/lib/browser";
import { ArduinoFrontendContribution } from "../arduino-frontend-contribution";
@injectable()
export class CustomFrontendApplication extends FrontendApplication {
@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) {
this.frontendContribution.openSketchFiles(decodeURIComponent(sketchPath));
}
}
}

View File

@ -1,11 +0,0 @@
import {MonacoStatusBarContribution} from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
export class SilentMonacoStatusBarContribution extends MonacoStatusBarContribution {
protected setConfigTabSizeWidget() {
}
protected setLineEndingWidget() {
}
}

View File

@ -1,9 +1,11 @@
import { injectable } from "inversify"; import { injectable } from 'inversify';
import { FileNavigatorContribution } from "@theia/navigator/lib/browser/navigator-contribution"; import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { FrontendApplication } from "@theia/core/lib/browser"; import { FrontendApplication } from '@theia/core/lib/browser';
@injectable() @injectable()
export class SilentNavigatorContribution extends FileNavigatorContribution { export class SilentNavigatorContribution extends FileNavigatorContribution {
async initializeLayout(app: FrontendApplication): Promise<void> { async initializeLayout(app: FrontendApplication): Promise<void> {
} }
}
}

View File

@ -1,19 +1,3 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { FrontendApplication } from '@theia/core/lib/browser'; import { FrontendApplication } from '@theia/core/lib/browser';
@ -23,4 +7,6 @@ export class SilentOutlineViewContribution extends OutlineViewContribution {
async initializeLayout(app: FrontendApplication): Promise<void> { async initializeLayout(app: FrontendApplication): Promise<void> {
} }
} }

View File

@ -1,9 +1,11 @@
import { OutputToolbarContribution } from "@theia/output/lib/browser/output-toolbar-contribution"; import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar"; import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { injectable } from "inversify"; import { injectable } from 'inversify';
@injectable() @injectable()
export class ArduinoOutputToolContribution extends OutputToolbarContribution { export class ArduinoOutputToolContribution extends OutputToolbarContribution {
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> { async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
} }
}
}

View File

@ -11,4 +11,5 @@ export class SilentProblemContribution extends ProblemContribution {
protected setStatusBarElement(problemStat: ProblemStat) { protected setStatusBarElement(problemStat: ProblemStat) {
} }
} }

View File

@ -1,6 +1,6 @@
import { injectable } from "inversify"; import { injectable } from 'inversify';
import { ScmContribution } from "@theia/scm/lib/browser/scm-contribution"; import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { StatusBarEntry } from "@theia/core/lib/browser"; import { StatusBarEntry } from '@theia/core/lib/browser';
@injectable() @injectable()
export class SilentScmContribution extends ScmContribution { export class SilentScmContribution extends ScmContribution {
@ -9,6 +9,6 @@ export class SilentScmContribution extends ScmContribution {
} }
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void { protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
} }
}
}

View File

@ -1,10 +1,11 @@
import { injectable } from "inversify"; import { injectable } from 'inversify';
import { SearchInWorkspaceFrontendContribution } from "@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution"; import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FrontendApplication } from "@theia/core/lib/browser"; import { FrontendApplication } from '@theia/core/lib/browser';
@injectable() @injectable()
export class SilentSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution { export class SilentSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
async initializeLayout(app: FrontendApplication): Promise<void> {
} }
}
}

View File

@ -1,65 +0,0 @@
import { injectable, inject } from "inversify";
import URI from "@theia/core/lib/common/uri";
import { FileSystem } from "@theia/filesystem/lib/common";
import { WindowService } from "@theia/core/lib/browser/window/window-service";
@injectable()
export class SketchFactory {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(WindowService)
protected readonly windowService: WindowService;
public async createNewSketch(parent: URI): Promise<void> {
const monthNames = ["january", "february", "march", "april", "may", "june",
"july", "august", "september", "october", "november", "december"
];
const today = new Date();
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDay()}`;
let sketchName: string | undefined;
for (let i = 97; i < 97 + 26; i++) {
let sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(i)}`;
if (await this.fileSystem.exists(parent.resolve(sketchNameCandidate).toString())) {
continue;
}
sketchName = sketchNameCandidate;
break;
}
if (!sketchName) {
throw new Error("Cannot create a unique sketch name");
}
try {
const sketchDir = parent.resolve(sketchName);
const sketchFile = sketchDir.resolve(`${sketchName}.ino`);
this.fileSystem.createFolder(sketchDir.toString());
this.fileSystem.createFile(sketchFile.toString(), {
content: `
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
` });
const location = new URL(window.location.href);
location.searchParams.set('sketch', sketchDir.toString());
const hash = await this.fileSystem.getFsPath(sketchDir.toString());
if (hash) {
location.hash = hash;
}
this.windowService.openNewWindow(location.toString());
} catch (e) {
throw new Error("Cannot create new sketch: " + e);
}
}
}

View File

@ -3,6 +3,8 @@ export const ConfigService = Symbol('ConfigService');
export interface ConfigService { export interface ConfigService {
getConfiguration(): Promise<Config>; getConfiguration(): Promise<Config>;
isInDataDir(uri: string): Promise<boolean>;
isInSketchDir(uri: string): Promise<boolean>;
} }
export interface Config { export interface Config {

View File

@ -1,13 +1,17 @@
import { FileStat } from "@theia/filesystem/lib/common";
export const SketchesServicePath = '/services/sketches-service'; export const SketchesServicePath = '/services/sketches-service';
export const SketchesService = Symbol('SketchesService'); export const SketchesService = Symbol('SketchesService');
export interface SketchesService { export interface SketchesService {
getSketches(fileStat?: FileStat): Promise<Sketch[]> /**
getSketchFiles(fileStat: FileStat): Promise<string[]> * 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.
*/
getSketches(uri?: string): Promise<Sketch[]>
getSketchFiles(uri: string): Promise<string[]>
createNewSketch(parentUri: string): Promise<Sketch>
isSketchFolder(uri: string): Promise<boolean>
} }
export interface Sketch { export interface Sketch {
name: string; readonly name: string;
uri: string readonly uri: string
} }

View File

@ -1,6 +1,7 @@
import { injectable, inject } from "inversify"; import { injectable, inject } from 'inversify';
import { ConfigService, Config } from "../common/protocol/config-service"; import URI from '@theia/core/lib/common/uri';
import { ArduinoCli } from "./arduino-cli"; import { ConfigService, Config } from '../common/protocol/config-service';
import { ArduinoCli } from './arduino-cli';
@injectable() @injectable()
export class ConfigServiceImpl implements ConfigService { export class ConfigServiceImpl implements ConfigService {
@ -11,4 +12,13 @@ export class ConfigServiceImpl implements ConfigService {
async getConfiguration(): Promise<Config> { async getConfiguration(): Promise<Config> {
return this.cli.getDefaultConfig(); return this.cli.getDefaultConfig();
} }
}
async isInDataDir(uri: string): Promise<boolean> {
return this.getConfiguration().then(({ dataDirUri }) => new URI(dataDirUri).isEqualOrParent(new URI(uri)));
}
async isInSketchDir(uri: string): Promise<boolean> {
return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri)));
}
}

View File

@ -1,80 +1,126 @@
import { injectable, inject } from "inversify"; import { injectable, inject } from 'inversify';
import { SketchesService, Sketch } from "../common/protocol/sketches-service";
import URI from "@theia/core/lib/common/uri";
import { FileStat, FileSystem } from "@theia/filesystem/lib/common";
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { FileUri } from "@theia/core/lib/node"; import * as fs from 'fs-extra';
import { FileUri } from '@theia/core/lib/node';
import { ConfigService } from '../common/protocol/config-service';
import { SketchesService, Sketch } from '../common/protocol/sketches-service';
export const ALLOWED_FILE_EXTENSIONS = [".c", ".cpp", ".h", ".hh", ".hpp", ".s", ".pde", ".ino"]; export const ALLOWED_FILE_EXTENSIONS = ['.c', '.cpp', '.h', '.hh', '.hpp', '.s', '.pde', '.ino'];
// TODO: `fs`: use async API
@injectable() @injectable()
export class SketchesServiceImpl implements SketchesService { export class SketchesServiceImpl implements SketchesService {
@inject(FileSystem) @inject(ConfigService)
protected readonly filesystem: FileSystem; protected readonly configService: ConfigService;
async getSketches(fileStat?: FileStat): Promise<Sketch[]> { async getSketches(uri?: string): Promise<Sketch[]> {
const sketches: Sketch[] = []; const sketches: Array<Sketch & { mtimeMs: number }> = [];
if (fileStat && fileStat.isDirectory) { const fsPath = FileUri.fsPath(uri ? uri : (await this.configService.getConfiguration()).sketchDirUri);
const uri = new URI(fileStat.uri); const fileNames = fs.readdirSync(fsPath);
const sketchFolderPath = await this.filesystem.getFsPath(uri.toString()); for (const fileName of fileNames) {
if (sketchFolderPath) { const filePath = path.join(fsPath, fileName);
const fileNames = fs.readdirSync(sketchFolderPath); if (await this.isSketchFolder(FileUri.create(filePath).toString())) {
for (const fileName of fileNames) { const stat = fs.statSync(filePath);
const filePath = path.join(sketchFolderPath, fileName); sketches.push({
if (this.isSketchFolder(filePath, fileName)) { mtimeMs: stat.mtimeMs,
sketches.push({ name: fileName,
name: fileName, uri: FileUri.create(filePath).toString()
uri: FileUri.create(filePath).toString() });
});
}
}
} }
} }
return sketches; return sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
} }
/** /**
* Return all allowed files. * Return all allowed files.
* File extensions: "c", "cpp", "h", "hh", "hpp", "s", "pde", "ino" * File extensions: 'c', 'cpp', 'h', 'hh', 'hpp', 's', 'pde', 'ino'
*/ */
async getSketchFiles(sketchFileStat: FileStat): Promise<string[]> { async getSketchFiles(uri: string): Promise<string[]> {
const uris: string[] = []; const uris: string[] = [];
const sketchUri = new URI(sketchFileStat.uri); const fsPath = FileUri.fsPath(uri);
const sketchPath = await this.filesystem.getFsPath(sketchUri.toString()); const stats = fs.lstatSync(fsPath);
if (sketchPath) { if (stats.isDirectory && await this.isSketchFolder(uri)) {
if (sketchFileStat.isDirectory && this.isSketchFolder(sketchPath, sketchUri.displayName)) { const fileNames = fs.readdirSync(fsPath);
const fileNames = fs.readdirSync(sketchPath); for (const fileName of fileNames) {
for (const fileName of fileNames) { const filePath = path.join(fsPath, fileName);
const filePath = path.join(sketchPath, fileName); if (ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1
if (ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1 && fs.existsSync(filePath)
&& fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
&& fs.lstatSync(filePath).isFile()) { uris.push(FileUri.create(filePath).toString())
uris.push(FileUri.create(filePath).toString())
}
}
} else {
const sketchDir = path.dirname(sketchPath);
if (sketchDir && this.isSketchFolder(sketchDir, sketchUri.path.dir.name)) {
const sketchFolderStat = await this.filesystem.getFileStat(sketchUri.path.dir.toString());
if (sketchFolderStat) {
const sketchDirContents = await this.getSketchFiles(sketchFolderStat);
uris.push(...sketchDirContents);
}
} }
} }
return uris;
} }
return uris; const sketchDir = path.dirname(fsPath);
return this.getSketchFiles(FileUri.create(sketchDir).toString());
} }
protected isSketchFolder(path: string, name: string): boolean { async createNewSketch(parentUri: string): Promise<Sketch> {
if (fs.existsSync(path) && fs.lstatSync(path).isDirectory()) { const monthNames = ['january', 'february', 'march', 'april', 'may', 'june',
const files = fs.readdirSync(path); 'july', 'august', 'september', 'october', 'november', 'december'
for (let i = 0; i < files.length; i++) { ];
if (files[i] === name + '.ino') { const today = new Date();
return true; const parent = FileUri.fsPath(parentUri);
}
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
let sketchName: string | undefined;
for (let i = 97; i < 97 + 26; i++) {
let sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(i)}`;
if (fs.existsSync(path.join(parent, sketchNameCandidate))) {
continue;
}
sketchName = sketchNameCandidate;
break;
}
if (!sketchName) {
throw new Error('Cannot create a unique sketch name');
}
const sketchDir = path.join(parent, sketchName)
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
fs.mkdirSync(sketchDir);
fs.writeFileSync(sketchFile, `
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
`, { encoding: 'utf8' });
return {
name: sketchName,
uri: FileUri.create(sketchDir).toString()
}
}
async isSketchFolder(uri: string): Promise<boolean> {
const fsPath = FileUri.fsPath(uri);
const exists = await fs.pathExists(fsPath);
if (exists) {
const stats = await fs.lstat(fsPath);
if (stats.isDirectory()) {
const basename = path.basename(fsPath);
return new Promise<boolean>((resolve, reject) => {
fs.readdir(fsPath, (error, files) => {
if (error) {
reject(error);
return;
}
for (let i = 0; i < files.length; i++) {
if (files[i] === basename + '.ino') {
resolve(true);
return;
}
}
resolve(false);
});
})
} }
} }
return false; return false;

View File

@ -8,7 +8,6 @@
"max-line-length": [true, 180], "max-line-length": [true, 180],
"no-trailing-whitespace": false, "no-trailing-whitespace": false,
"no-unused-expression": true, "no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true, "no-var-keyword": true,
"one-line": [true, "one-line": [true,
"check-open-brace", "check-open-brace",

View File

@ -10371,6 +10371,11 @@ upath@^1.0.0, upath@^1.1.1:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068"
upath@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
uri-js@^4.2.2: uri-js@^4.2.2:
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"