diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 66c04ccc..50d34ff6 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -31,6 +31,7 @@ "p-queue": "^5.0.0", "ps-tree": "^1.2.0", "tree-kill": "^1.2.1", + "upath": "^1.1.2", "which": "^1.3.1" }, "scripts": { diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index d55a3299..41151d87 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -15,8 +15,6 @@ import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-fro import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; 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 { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser'; import { @@ -26,8 +24,7 @@ import { StatusBar, ShellLayoutRestorer, StatusBarAlignment, - QuickOpenService, - LabelProvider + QuickOpenService } from '@theia/core/lib/browser'; import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; 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 { MonitorConnection } from './monitor/monitor-connection'; import { MonitorViewContribution } from './monitor/monitor-view-contribution'; +import { ArduinoWorkspaceService } from './arduino-workspace-service'; export namespace ArduinoMenus { export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; @@ -61,7 +59,6 @@ export namespace ArduinoAdvancedMode { })(); } - @injectable() export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution, MenuContribution { @@ -95,9 +92,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(SelectionService) protected readonly selectionService: SelectionService; - @inject(SketchFactory) - protected readonly sketchFactory: SketchFactory; - @inject(EditorManager) protected readonly editorManager: EditorManager; @@ -117,7 +111,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C protected readonly windowService: WindowService; @inject(SketchesService) - protected readonly sketches: SketchesService; + protected readonly sketchService: SketchesService; @inject(BoardsConfigDialog) protected readonly boardsConfigDialog: BoardsConfigDialog; @@ -134,17 +128,15 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(ShellLayoutRestorer) protected readonly layoutRestorer: ShellLayoutRestorer; - @inject(LabelProvider) - protected readonly labelProvider: LabelProvider; - @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; - @inject(WorkspaceService) - protected readonly workspaceService: WorkspaceService; + @inject(ArduinoWorkspaceService) + protected readonly workspaceService: ArduinoWorkspaceService; @inject(ConfigService) protected readonly configService: ConfigService; + @inject(MonitorConnection) protected readonly monitorConnection: MonitorConnection; @@ -304,7 +296,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C registry.registerCommand(ArduinoCommands.OPEN_SKETCH, { isEnabled: () => true, execute: async (sketch: Sketch) => { - this.openSketchFilesInNewWindow(sketch.uri); + this.workspaceService.openSketchFilesInNewWindow(sketch.uri); } }) registry.registerCommand(ArduinoCommands.SAVE_SKETCH, { @@ -322,7 +314,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C 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) { await this.messageService.error(e.toString()); } @@ -397,8 +390,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C return menuId; } - protected registerSketchesInMenu(registry: MenuModelRegistry) { - this.getWorkspaceSketches().then(sketches => { + protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise { + this.sketchService.getSketches().then(sketches => { this.wsSketchCount = sketches.length; sketches.forEach(sketch => { const command: Command = { @@ -416,48 +409,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C }) } - protected async getWorkspaceSketches(): Promise { - - 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); + async openSketchFiles(uri: string): Promise { + this.sketchService.getSketchFiles(uri).then(uris => { for (const uri of uris) { this.editorManager.open(new URI(uri)); } - } + }); } /** @@ -481,7 +438,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C if (destinationFile && !destinationFile.isDirectory) { const message = await this.validate(destinationFile); if (!message) { - await this.openSketchFilesInNewWindow(destinationFileUri.toString()); + await this.workspaceService.openSketchFilesInNewWindow(destinationFileUri.toString()); return destinationFileUri; } else { this.messageService.warn(message); diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 15c85a9f..5e0784ab 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -25,12 +25,11 @@ import { ToolOutputService } from '../common/protocol/tool-output-service'; import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl'; import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; 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 { ArduinoTheme } from './arduino-theme'; import { ArduinoToolbarMenuContribution } from './arduino-file-menu'; import { MenuContribution } from '@theia/core'; -import { SketchFactory } from './sketch-factory'; import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; import { SilentOutlineViewContribution } from './customization/silent-outline-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 { ArduinoOutputToolContribution } from './customization/silent-output-tool-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 { 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 { CustomApplicationShell } from './customization/custom-application-shell'; -import { CustomFrontendApplication } from './customization/custom-frontend-application'; +import { ArduinoApplicationShell } from './customization/arduino-application-shell'; +import { ArduinoFrontendApplication } from './customization/arduino-frontend-application'; import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog'; import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget'; 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; }).inSingletonScope(); - bind(AWorkspaceService).toSelf().inSingletonScope(); - rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope(); - bind(SketchFactory).toSelf().inSingletonScope(); + bind(ArduinoWorkspaceService).toSelf().inSingletonScope(); + rebind(WorkspaceService).to(ArduinoWorkspaceService).inSingletonScope(); const themeService = ThemeService.get(); themeService.register(...ArduinoTheme.themes); @@ -207,11 +205,11 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un unbind(OutputToolbarContribution); bind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope(); unbind(EditorContribution); - bind(EditorContribution).to(CustomEditorContribution).inSingletonScope(); + bind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope(); unbind(MonacoStatusBarContribution); - bind(MonacoStatusBarContribution).to(SilentMonacoStatusBarContribution).inSingletonScope(); + bind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope(); unbind(ApplicationShell); - bind(ApplicationShell).to(CustomApplicationShell).inSingletonScope(); + bind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope(); unbind(ScmContribution); bind(ScmContribution).to(SilentScmContribution).inSingletonScope(); unbind(SearchInWorkspaceFrontendContribution); @@ -221,7 +219,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un document.body.classList.add(ArduinoAdvancedMode.LS_ID); } unbind(FrontendApplication); - bind(FrontendApplication).to(CustomFrontendApplication).inSingletonScope(); + bind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope(); // monaco customizations unbind(MonacoEditorProvider); diff --git a/arduino-ide-extension/src/browser/arduino-workspace-service.ts b/arduino-ide-extension/src/browser/arduino-workspace-service.ts index e47c6899..4e7d8d52 100644 --- a/arduino-ide-extension/src/browser/arduino-workspace-service.ts +++ b/arduino-ide-extension/src/browser/arduino-workspace-service.ts @@ -1,49 +1,129 @@ -import { WorkspaceService } from "@theia/workspace/lib/browser/workspace-service"; -import { injectable, inject } from "inversify"; -import { WorkspaceServer } from "@theia/workspace/lib/common"; -import { FileSystem, FileStat } from "@theia/filesystem/lib/common"; -import URI from "@theia/core/lib/common/uri"; -import { SketchFactory } from "./sketch-factory"; -import { ConfigService } from "../common/protocol/config-service"; +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 { 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'; /** * This is workaround to have custom frontend binding for the default workspace, although we * already have a custom binding for the backend. */ @injectable() -export class AWorkspaceService extends WorkspaceService { +export class ArduinoWorkspaceService extends WorkspaceService { - @inject(WorkspaceServer) - protected readonly workspaceServer: WorkspaceServer; - - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - - @inject(SketchFactory) - protected readonly sketchFactory: SketchFactory; + @inject(SketchesService) + protected readonly sketchService: SketchesService; @inject(ConfigService) protected readonly configService: ConfigService; - protected async getDefaultWorkspacePath(): Promise { - let result = await super.getDefaultWorkspacePath(); - if (!result) { - const config = await this.configService.getConfiguration(); - result = config.sketchDirUri; + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + async getDefaultWorkspacePath(): Promise { + 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) { - // workspace does not exist yet, create it - await this.fileSystem.createFolder(result); - await this.sketchFactory.createNewSketch(new URI(result)); + // The folder for the workspace root does not exist yet, create it. + await this.fileSystem.createFolder(sketchDirUri); + 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 { - await super.setWorkspace(workspaceStat); + private toUri(uriPath: string | undefined): string | undefined { + if (uriPath) { + return new URI(toUnix(uriPath.slice(isWindows && uriPath.startsWith('/') ? 1 : 0))).withScheme('file').toString(); + } + return undefined; } -} \ No newline at end of file + async openSketchFilesInNewWindow(uri: string): Promise { + 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; + } + +} diff --git a/arduino-ide-extension/src/browser/customization/custom-application-shell.ts b/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts similarity index 94% rename from arduino-ide-extension/src/browser/customization/custom-application-shell.ts rename to arduino-ide-extension/src/browser/customization/arduino-application-shell.ts index 8813d6e9..e0c4b629 100644 --- a/arduino-ide-extension/src/browser/customization/custom-application-shell.ts +++ b/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts @@ -1,7 +1,7 @@ import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser'; import { EditorWidget } from '@theia/editor/lib/browser'; -export class CustomApplicationShell extends ApplicationShell { +export class ArduinoApplicationShell extends ApplicationShell { protected refreshBottomPanelToggleButton() { } @@ -30,4 +30,4 @@ export class CustomApplicationShell extends ApplicationShell { } } -} \ No newline at end of file +} diff --git a/arduino-ide-extension/src/browser/customization/custom-common-frontend-contribution.ts b/arduino-ide-extension/src/browser/customization/arduino-common-frontend-contribution.ts similarity index 84% rename from arduino-ide-extension/src/browser/customization/custom-common-frontend-contribution.ts rename to arduino-ide-extension/src/browser/customization/arduino-common-frontend-contribution.ts index dc851a55..e4d833b5 100644 --- a/arduino-ide-extension/src/browser/customization/custom-common-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/customization/arduino-common-frontend-contribution.ts @@ -1,10 +1,11 @@ -import { injectable } from "inversify"; -import { CommonFrontendContribution, CommonMenus, CommonCommands } from "@theia/core/lib/browser"; -import { MenuModelRegistry } from "@theia/core"; -import { ArduinoAdvancedMode } from "../arduino-frontend-contribution"; +import { injectable } from 'inversify'; +import { CommonFrontendContribution, CommonMenus, CommonCommands } from '@theia/core/lib/browser'; +import { MenuModelRegistry } from '@theia/core'; +import { ArduinoAdvancedMode } from '../arduino-frontend-contribution'; @injectable() -export class CustomCommonFrontendContribution extends CommonFrontendContribution { +export class ArduinoCommonFrontendContribution extends CommonFrontendContribution { + registerMenus(registry: MenuModelRegistry): void { if (!ArduinoAdvancedMode.TOGGLED) { registry.registerSubmenu(CommonMenus.FILE, 'File'); @@ -46,4 +47,5 @@ export class CustomCommonFrontendContribution extends CommonFrontendContribution super.registerMenus(registry); } } -} \ No newline at end of file + +} diff --git a/arduino-ide-extension/src/browser/customization/custom-editor-contribution.ts b/arduino-ide-extension/src/browser/customization/arduino-editor-contribution.ts similarity index 80% rename from arduino-ide-extension/src/browser/customization/custom-editor-contribution.ts rename to arduino-ide-extension/src/browser/customization/arduino-editor-contribution.ts index d472de2a..25f64e6a 100644 --- a/arduino-ide-extension/src/browser/customization/custom-editor-contribution.ts +++ b/arduino-ide-extension/src/browser/customization/arduino-editor-contribution.ts @@ -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 { StatusBarAlignment } from '@theia/core/lib/browser'; -export class CustomEditorContribution extends EditorContribution { +export class ArduinoEditorContribution extends EditorContribution { + protected updateLanguageStatus(editor: TextEditor | undefined): void { } @@ -18,4 +19,5 @@ export class CustomEditorContribution extends EditorContribution { priority: 100 }); } -} \ No newline at end of file + +} diff --git a/arduino-ide-extension/src/browser/customization/arduino-file-menu-contribution.ts b/arduino-ide-extension/src/browser/customization/arduino-file-menu-contribution.ts new file mode 100644 index 00000000..1acf57f5 --- /dev/null +++ b/arduino-ide-extension/src/browser/customization/arduino-file-menu-contribution.ts @@ -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) { + } + +} diff --git a/arduino-ide-extension/src/browser/customization/arduino-frontend-application.ts b/arduino-ide-extension/src/browser/customization/arduino-frontend-application.ts new file mode 100644 index 00000000..58dba3b1 --- /dev/null +++ b/arduino-ide-extension/src/browser/customization/arduino-frontend-application.ts @@ -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 { + 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)); + } + } + +} diff --git a/arduino-ide-extension/src/browser/customization/arduino-monaco-status-bar-contribution.ts b/arduino-ide-extension/src/browser/customization/arduino-monaco-status-bar-contribution.ts new file mode 100644 index 00000000..39466c71 --- /dev/null +++ b/arduino-ide-extension/src/browser/customization/arduino-monaco-status-bar-contribution.ts @@ -0,0 +1,11 @@ +import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution'; + +export class ArduinoMonacoStatusBarContribution extends MonacoStatusBarContribution { + + protected setConfigTabSizeWidget() { + } + + protected setLineEndingWidget() { + } + +} diff --git a/arduino-ide-extension/src/browser/customization/custom-file-menu-contribution.ts b/arduino-ide-extension/src/browser/customization/custom-file-menu-contribution.ts deleted file mode 100644 index a83da571..00000000 --- a/arduino-ide-extension/src/browser/customization/custom-file-menu-contribution.ts +++ /dev/null @@ -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) { - - } -} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/customization/custom-frontend-application.ts b/arduino-ide-extension/src/browser/customization/custom-frontend-application.ts deleted file mode 100644 index aa26c869..00000000 --- a/arduino-ide-extension/src/browser/customization/custom-frontend-application.ts +++ /dev/null @@ -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 { - await super.initializeLayout(); - const location = new URL(window.location.href); - const sketchPath = location.searchParams.get('sketch'); - if (sketchPath) { - this.frontendContribution.openSketchFiles(decodeURIComponent(sketchPath)); - } - } -} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/customization/silent-monaco-status-bar-contribution.ts b/arduino-ide-extension/src/browser/customization/silent-monaco-status-bar-contribution.ts deleted file mode 100644 index ea6eac35..00000000 --- a/arduino-ide-extension/src/browser/customization/silent-monaco-status-bar-contribution.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {MonacoStatusBarContribution} from '@theia/monaco/lib/browser/monaco-status-bar-contribution'; - -export class SilentMonacoStatusBarContribution extends MonacoStatusBarContribution { - protected setConfigTabSizeWidget() { - - } - - protected setLineEndingWidget() { - - } -} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/customization/silent-navigator-contribution.ts b/arduino-ide-extension/src/browser/customization/silent-navigator-contribution.ts index 0ef0412b..3b219691 100644 --- a/arduino-ide-extension/src/browser/customization/silent-navigator-contribution.ts +++ b/arduino-ide-extension/src/browser/customization/silent-navigator-contribution.ts @@ -1,9 +1,11 @@ -import { injectable } from "inversify"; -import { FileNavigatorContribution } from "@theia/navigator/lib/browser/navigator-contribution"; -import { FrontendApplication } from "@theia/core/lib/browser"; +import { injectable } from 'inversify'; +import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution'; +import { FrontendApplication } from '@theia/core/lib/browser'; @injectable() export class SilentNavigatorContribution extends FileNavigatorContribution { + async initializeLayout(app: FrontendApplication): Promise { } -} \ No newline at end of file + +} diff --git a/arduino-ide-extension/src/browser/customization/silent-outline-contribution.ts b/arduino-ide-extension/src/browser/customization/silent-outline-contribution.ts index 87535ebb..5563f757 100644 --- a/arduino-ide-extension/src/browser/customization/silent-outline-contribution.ts +++ b/arduino-ide-extension/src/browser/customization/silent-outline-contribution.ts @@ -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 { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; import { FrontendApplication } from '@theia/core/lib/browser'; @@ -23,4 +7,6 @@ export class SilentOutlineViewContribution extends OutlineViewContribution { async initializeLayout(app: FrontendApplication): Promise { } + } + diff --git a/arduino-ide-extension/src/browser/customization/silent-output-tool-contribution.ts b/arduino-ide-extension/src/browser/customization/silent-output-tool-contribution.ts index c29cc066..1ca63fa8 100644 --- a/arduino-ide-extension/src/browser/customization/silent-output-tool-contribution.ts +++ b/arduino-ide-extension/src/browser/customization/silent-output-tool-contribution.ts @@ -1,9 +1,11 @@ -import { OutputToolbarContribution } from "@theia/output/lib/browser/output-toolbar-contribution"; -import { TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar"; -import { injectable } from "inversify"; +import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution'; +import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { injectable } from 'inversify'; @injectable() export class ArduinoOutputToolContribution extends OutputToolbarContribution { + async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise { } -} \ No newline at end of file + +} diff --git a/arduino-ide-extension/src/browser/customization/silent-problem-contribution.ts b/arduino-ide-extension/src/browser/customization/silent-problem-contribution.ts index b468d406..d90147f2 100644 --- a/arduino-ide-extension/src/browser/customization/silent-problem-contribution.ts +++ b/arduino-ide-extension/src/browser/customization/silent-problem-contribution.ts @@ -11,4 +11,5 @@ export class SilentProblemContribution extends ProblemContribution { protected setStatusBarElement(problemStat: ProblemStat) { } + } diff --git a/arduino-ide-extension/src/browser/customization/silent-scm-contribution.ts b/arduino-ide-extension/src/browser/customization/silent-scm-contribution.ts index fdc37725..0ad49f21 100644 --- a/arduino-ide-extension/src/browser/customization/silent-scm-contribution.ts +++ b/arduino-ide-extension/src/browser/customization/silent-scm-contribution.ts @@ -1,6 +1,6 @@ -import { injectable } from "inversify"; -import { ScmContribution } from "@theia/scm/lib/browser/scm-contribution"; -import { StatusBarEntry } from "@theia/core/lib/browser"; +import { injectable } from 'inversify'; +import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution'; +import { StatusBarEntry } from '@theia/core/lib/browser'; @injectable() export class SilentScmContribution extends ScmContribution { @@ -9,6 +9,6 @@ export class SilentScmContribution extends ScmContribution { } protected setStatusBarEntry(id: string, entry: StatusBarEntry): void { - } -} \ No newline at end of file + +} diff --git a/arduino-ide-extension/src/browser/customization/silent-search-in-workspace-contribution.ts b/arduino-ide-extension/src/browser/customization/silent-search-in-workspace-contribution.ts index 9c7a11f7..1d12dcaa 100644 --- a/arduino-ide-extension/src/browser/customization/silent-search-in-workspace-contribution.ts +++ b/arduino-ide-extension/src/browser/customization/silent-search-in-workspace-contribution.ts @@ -1,10 +1,11 @@ -import { injectable } from "inversify"; -import { SearchInWorkspaceFrontendContribution } from "@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution"; -import { FrontendApplication } from "@theia/core/lib/browser"; +import { injectable } from 'inversify'; +import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution'; +import { FrontendApplication } from '@theia/core/lib/browser'; @injectable() export class SilentSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution { - async initializeLayout(app: FrontendApplication): Promise { + async initializeLayout(app: FrontendApplication): Promise { } -} \ No newline at end of file + +} diff --git a/arduino-ide-extension/src/browser/sketch-factory.ts b/arduino-ide-extension/src/browser/sketch-factory.ts deleted file mode 100644 index 2a50aa61..00000000 --- a/arduino-ide-extension/src/browser/sketch-factory.ts +++ /dev/null @@ -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 { - 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); - } - } - -} \ No newline at end of file diff --git a/arduino-ide-extension/src/common/protocol/config-service.ts b/arduino-ide-extension/src/common/protocol/config-service.ts index fb8c90cd..aaddf2c6 100644 --- a/arduino-ide-extension/src/common/protocol/config-service.ts +++ b/arduino-ide-extension/src/common/protocol/config-service.ts @@ -3,6 +3,8 @@ export const ConfigService = Symbol('ConfigService'); export interface ConfigService { getConfiguration(): Promise; + isInDataDir(uri: string): Promise; + isInSketchDir(uri: string): Promise; } export interface Config { diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index bea5ffc8..4b07712d 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -1,13 +1,17 @@ -import { FileStat } from "@theia/filesystem/lib/common"; - export const SketchesServicePath = '/services/sketches-service'; export const SketchesService = Symbol('SketchesService'); export interface SketchesService { - getSketches(fileStat?: FileStat): Promise - getSketchFiles(fileStat: FileStat): Promise + /** + * 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 + getSketchFiles(uri: string): Promise + createNewSketch(parentUri: string): Promise + isSketchFolder(uri: string): Promise } export interface Sketch { - name: string; - uri: string + readonly name: string; + readonly uri: string } \ No newline at end of file diff --git a/arduino-ide-extension/src/node/config-service-impl.ts b/arduino-ide-extension/src/node/config-service-impl.ts index f869fd9a..e864a846 100644 --- a/arduino-ide-extension/src/node/config-service-impl.ts +++ b/arduino-ide-extension/src/node/config-service-impl.ts @@ -1,6 +1,7 @@ -import { injectable, inject } from "inversify"; -import { ConfigService, Config } from "../common/protocol/config-service"; -import { ArduinoCli } from "./arduino-cli"; +import { injectable, inject } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { ConfigService, Config } from '../common/protocol/config-service'; +import { ArduinoCli } from './arduino-cli'; @injectable() export class ConfigServiceImpl implements ConfigService { @@ -11,4 +12,13 @@ export class ConfigServiceImpl implements ConfigService { async getConfiguration(): Promise { return this.cli.getDefaultConfig(); } -} \ No newline at end of file + + async isInDataDir(uri: string): Promise { + return this.getConfiguration().then(({ dataDirUri }) => new URI(dataDirUri).isEqualOrParent(new URI(uri))); + } + + async isInSketchDir(uri: string): Promise { + return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri))); + } + +} diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 2107ec0d..73c2590e 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -1,80 +1,126 @@ -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 { injectable, inject } from 'inversify'; 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() export class SketchesServiceImpl implements SketchesService { - @inject(FileSystem) - protected readonly filesystem: FileSystem; + @inject(ConfigService) + protected readonly configService: ConfigService; - async getSketches(fileStat?: FileStat): Promise { - const sketches: Sketch[] = []; - if (fileStat && fileStat.isDirectory) { - const uri = new URI(fileStat.uri); - const sketchFolderPath = await this.filesystem.getFsPath(uri.toString()); - if (sketchFolderPath) { - const fileNames = fs.readdirSync(sketchFolderPath); - for (const fileName of fileNames) { - const filePath = path.join(sketchFolderPath, fileName); - if (this.isSketchFolder(filePath, fileName)) { - sketches.push({ - name: fileName, - uri: FileUri.create(filePath).toString() - }); - } - } + async getSketches(uri?: string): Promise { + const sketches: Array = []; + const fsPath = FileUri.fsPath(uri ? uri : (await this.configService.getConfiguration()).sketchDirUri); + const fileNames = fs.readdirSync(fsPath); + for (const fileName of fileNames) { + const filePath = path.join(fsPath, fileName); + if (await this.isSketchFolder(FileUri.create(filePath).toString())) { + const stat = fs.statSync(filePath); + sketches.push({ + mtimeMs: stat.mtimeMs, + name: fileName, + uri: FileUri.create(filePath).toString() + }); } } - return sketches; + return sketches.sort((left, right) => right.mtimeMs - left.mtimeMs); } /** * 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 { + async getSketchFiles(uri: string): Promise { const uris: string[] = []; - const sketchUri = new URI(sketchFileStat.uri); - const sketchPath = await this.filesystem.getFsPath(sketchUri.toString()); - if (sketchPath) { - if (sketchFileStat.isDirectory && this.isSketchFolder(sketchPath, sketchUri.displayName)) { - const fileNames = fs.readdirSync(sketchPath); - for (const fileName of fileNames) { - const filePath = path.join(sketchPath, fileName); - if (ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1 - && fs.existsSync(filePath) - && fs.lstatSync(filePath).isFile()) { - 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); - } + const fsPath = FileUri.fsPath(uri); + const stats = fs.lstatSync(fsPath); + if (stats.isDirectory && await this.isSketchFolder(uri)) { + const fileNames = fs.readdirSync(fsPath); + for (const fileName of fileNames) { + const filePath = path.join(fsPath, fileName); + if (ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1 + && fs.existsSync(filePath) + && fs.lstatSync(filePath).isFile()) { + uris.push(FileUri.create(filePath).toString()) } } + return uris; } - return uris; + const sketchDir = path.dirname(fsPath); + return this.getSketchFiles(FileUri.create(sketchDir).toString()); } - protected isSketchFolder(path: string, name: string): boolean { - if (fs.existsSync(path) && fs.lstatSync(path).isDirectory()) { - const files = fs.readdirSync(path); - for (let i = 0; i < files.length; i++) { - if (files[i] === name + '.ino') { - return true; - } + async createNewSketch(parentUri: string): Promise { + const monthNames = ['january', 'february', 'march', 'april', 'may', 'june', + 'july', 'august', 'september', 'october', 'november', 'december' + ]; + const today = new Date(); + 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 { + 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((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; diff --git a/arduino-ide-extension/tslint.json b/arduino-ide-extension/tslint.json index 09107cd5..55b00628 100644 --- a/arduino-ide-extension/tslint.json +++ b/arduino-ide-extension/tslint.json @@ -8,7 +8,6 @@ "max-line-length": [true, 180], "no-trailing-whitespace": false, "no-unused-expression": true, - "no-use-before-declare": true, "no-var-keyword": true, "one-line": [true, "check-open-brace", diff --git a/yarn.lock b/yarn.lock index 4b144a1b..5541f33f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10371,6 +10371,11 @@ upath@^1.0.0, upath@^1.1.1: version "1.1.2" 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: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"