From 2bd9eef1464a64f68c614d0be65de98f76f1f749 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 14 Jul 2020 17:18:46 +0200 Subject: [PATCH] Aligned the `New` and `Save As...` with the Java IDE. From now on, sketches are created in the temp folder, and will be moved to the `directories.user` location when the user performs a manual `Save`. A new sketch can be created with the `CtrlCmd+N` binding. Closes: arduino/arduino-pro-ide#260 Closes: arduino/arduino-pro-ide#261 Signed-off-by: Akos Kitta --- ...debug-frontend-application-contribution.ts | 2 +- arduino-ide-extension/package.json | 9 +- .../src/browser/arduino-commands.ts | 39 +++--- .../browser/arduino-frontend-contribution.tsx | 112 ++++++++++++++---- .../browser/arduino-ide-frontend-module.ts | 7 ++ .../src/browser/arduino-workspace-service.ts | 4 +- .../arduino-application-shell.ts | 13 +- ...arduino-workspace-frontend-contribution.ts | 62 ++++++++++ .../src/browser/style/main.css | 7 +- .../src/common/protocol/filesystem-ext.ts | 5 + .../src/common/protocol/sketches-service.ts | 40 +++++-- .../src/node/arduino-ide-backend-module.ts | 7 ++ .../src/node/node-filesystem-ext.ts | 12 ++ .../src/node/sketches-service-impl.ts | 85 ++++++++++--- yarn.lock | 7 ++ 15 files changed, 341 insertions(+), 70 deletions(-) create mode 100644 arduino-ide-extension/src/browser/customization/arduino-workspace-frontend-contribution.ts create mode 100644 arduino-ide-extension/src/common/protocol/filesystem-ext.ts create mode 100644 arduino-ide-extension/src/node/node-filesystem-ext.ts diff --git a/arduino-debugger-extension/src/browser/arduino-debug-frontend-application-contribution.ts b/arduino-debugger-extension/src/browser/arduino-debug-frontend-application-contribution.ts index 60f9dd21..7ce59452 100644 --- a/arduino-debugger-extension/src/browser/arduino-debug-frontend-application-contribution.ts +++ b/arduino-debugger-extension/src/browser/arduino-debug-frontend-application-contribution.ts @@ -114,7 +114,7 @@ export class ArduinoDebugFrontendApplicationContribution extends DebugFrontendAp id: ArduinoDebugCommands.START_DEBUG.id, command: ArduinoDebugCommands.START_DEBUG.id, tooltip: 'Start Debugging', - priority: 1 + priority: 3 }); } diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 8477daac..a6411651 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -40,9 +40,11 @@ "@types/google-protobuf": "^3.7.2", "@types/js-yaml": "^3.12.2", "@types/lodash.debounce": "^4.0.6", + "@types/ncp": "^2.0.4", "@types/ps-tree": "^1.1.0", "@types/react-select": "^3.0.0", "@types/sinon": "^7.5.2", + "@types/temp": "^0.8.34", "@types/which": "^1.3.1", "ajv": "^6.5.3", "css-element-queries": "^1.2.0", @@ -53,11 +55,13 @@ "google-protobuf": "^3.11.4", "lodash.debounce": "^4.0.8", "js-yaml": "^3.13.1", + "ncp": "^2.0.0", "p-queue": "^5.0.0", "ps-tree": "^1.2.0", "react-select": "^3.0.4", "semver": "^6.3.0", "string-natural-compare": "^2.0.3", + "temp": "^0.9.1", "tree-kill": "^1.2.1", "upath": "^1.1.2", "which": "^1.3.1" @@ -66,7 +70,6 @@ "@types/chai": "^4.2.7", "@types/chai-string": "^1.4.2", "@types/mocha": "^5.2.7", - "@types/temp": "^0.8.34", "chai": "^4.2.0", "chai-string": "^1.5.0", "decompress": "^4.2.0", @@ -76,11 +79,9 @@ "grpc_tools_node_protoc_ts": "^4.1.0", "mocha": "^7.0.0", "moment": "^2.24.0", - "ncp": "^2.0.0", - "protoc": "^1.0.4", + "protoc": "1.0.4", "shelljs": "^0.8.3", "sinon": "^9.0.1", - "temp": "^0.9.1", "uuid": "^3.2.1", "yargs": "^11.1.0" }, diff --git a/arduino-ide-extension/src/browser/arduino-commands.ts b/arduino-ide-extension/src/browser/arduino-commands.ts index faa042f4..cedfd683 100644 --- a/arduino-ide-extension/src/browser/arduino-commands.ts +++ b/arduino-ide-extension/src/browser/arduino-commands.ts @@ -7,22 +7,22 @@ export namespace ArduinoCommands { export const VERIFY: Command = { id: 'arduino-verify', label: 'Verify Sketch' - } + }; export const VERIFY_TOOLBAR: Command = { id: 'arduino-verify-toolbar', - } + }; export const UPLOAD: Command = { id: 'arduino-upload', label: 'Upload Sketch' - } + }; export const UPLOAD_TOOLBAR: Command = { id: 'arduino-upload-toolbar', - } + }; export const TOGGLE_COMPILE_FOR_DEBUG: Command = { - id: "arduino-toggle-compile-for-debug" - } + id: 'arduino-toggle-compile-for-debug' + }; export const SHOW_OPEN_CONTEXT_MENU: Command = { id: 'arduino-show-open-context-menu', @@ -32,39 +32,46 @@ export namespace ArduinoCommands { export const OPEN_FILE_NAVIGATOR: Command = { id: 'arduino-open-file-navigator' - } + }; export const OPEN_SKETCH: Command = { id: 'arduino-open-file' - } + }; /** * Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...) */ export const OPEN_SKETCH_FILES: Command = { id: 'arduino-open-sketch-files' - } + }; export const SAVE_SKETCH: Command = { - id: 'arduino-save-file' - } + id: 'arduino-save-sketch' + }; + + export const SAVE_SKETCH_AS: Command = { + id: 'arduino-save-sketch-as' + }; export const NEW_SKETCH: Command = { id: 'arduino-new-sketch', label: 'New Sketch', category - } + }; + export const NEW_SKETCH_TOOLBAR: Command = { + id: 'arduino-new-sketch-toolbar' + }; export const OPEN_BOARDS_DIALOG: Command = { id: 'arduino-open-boards-dialog' - } + }; export const TOGGLE_ADVANCED_MODE: Command = { id: 'arduino-toggle-advanced-mode' - } + }; export const TOGGLE_ADVANCED_MODE_TOOLBAR: Command = { - id: "arduino-toggle-advanced-mode-toolbar" - } + id: 'arduino-toggle-advanced-mode-toolbar' + }; export const OPEN_CLI_CONFIG: Command = { id: 'arduino-open-cli-config', diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index fbae515b..e1a8cc54 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import * as dateFormat from 'dateformat'; +import { remote } from 'electron'; import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; @@ -8,8 +10,8 @@ import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/li import { BoardsService, BoardsServiceClient, CoreService, Sketch, SketchesService, ToolOutputServiceClient } from '../common/protocol'; import { ArduinoCommands } from './arduino-commands'; 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, MenuPath } from '@theia/core'; +import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; +import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath, notEmpty } from '@theia/core'; import { ArduinoToolbar } from './toolbar/arduino-toolbar'; import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser'; import { @@ -44,6 +46,7 @@ import { ArduinoDaemon } from '../common/protocol/arduino-daemon'; import { ConfigService } from '../common/protocol/config-service'; import { BoardsConfigStore } from './boards/boards-config-store'; import { MainMenuManager } from './menu/main-menu-manager'; +import { FileSystemExt } from '../common/protocol/filesystem-ext'; export namespace ArduinoMenus { export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; @@ -152,6 +155,9 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut @inject(MainMenuManager) protected readonly mainMenuManager: MainMenuManager; + @inject(FileSystemExt) + protected readonly fileSystemExt: FileSystemExt; + protected application: FrontendApplication; protected wsSketchCount: number = 0; // TODO: this does not belong here, does it? @@ -194,24 +200,32 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut registry.registerItem({ id: ArduinoCommands.VERIFY.id, command: ArduinoCommands.VERIFY_TOOLBAR.id, - tooltip: 'Verify' + tooltip: 'Verify', + priority: 1 }); registry.registerItem({ id: ArduinoCommands.UPLOAD.id, command: ArduinoCommands.UPLOAD_TOOLBAR.id, - tooltip: 'Upload' + tooltip: 'Upload', + priority: 2 + }); + registry.registerItem({ + id: ArduinoCommands.NEW_SKETCH.id, + command: ArduinoCommands.NEW_SKETCH_TOOLBAR.id, + tooltip: 'New', + priority: 4 // Note: priority 3 was reserved by debug. }); registry.registerItem({ id: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id, command: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id, tooltip: 'Open', - priority: 2 + priority: 5 }); registry.registerItem({ id: ArduinoCommands.SAVE_SKETCH.id, command: ArduinoCommands.SAVE_SKETCH.id, tooltip: 'Save', - priority: 2 + priority: 6 }); registry.registerItem({ id: BoardsToolBarItem.TOOLBAR_ID, @@ -220,14 +234,13 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut commands={this.commandRegistry} boardsServiceClient={this.boardsServiceClientImpl} />, isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', - priority: 2 + priority: 6 }); registry.registerItem({ id: 'toggle-serial-monitor', command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR, - tooltip: 'Toggle Serial Monitor' + tooltip: 'Serial Monitor' }); - registry.registerItem({ id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id, command: ArduinoCommands.TOGGLE_ADVANCED_MODE_TOOLBAR.id, @@ -335,21 +348,65 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut } }); - registry.registerCommand(ArduinoCommands.NEW_SKETCH, new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, { - execute: async uri => { - try { - // hack: sometimes we don't get the workspace root, but the currently active file: correct for that - if (uri.path.ext !== "") { - uri = uri.withPath(uri.path.dir.dir); - } + registry.registerCommand(ArduinoCommands.SAVE_SKETCH_AS, { + execute: async ({ execOnlyIfTemp }: { execOnlyIfTemp: boolean } = { execOnlyIfTemp: false }) => { + const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ uri }) => this.sketchService.getSketchFolder(uri)))).filter(notEmpty); + if (!sketches.length) { + return; + } + if (sketches.length > 1) { + console.log(`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(sketches)}`); + } + const sketch = sketches[0]; + const isTemp = await this.sketchService.isTemp(sketch); + if (!isTemp && !!execOnlyIfTemp) { + return; + } - const sketch = await this.sketchService.createNewSketch(uri.toString()); + // If target does not exist, propose a `directories.user`/${sketch.name} path + // If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss} + const sketchDirUri = new URI((await this.configService.getConfiguration()).sketchDirUri); + const exists = await this.fileSystem.exists(sketchDirUri.resolve(sketch.name).toString()); + const defaultUri = exists + ? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString()) + : sketchDirUri.resolve(sketch.name); + const defaultPath = await this.fileSystem.getFsPath(defaultUri.toString())!; + const fsPath = await new Promise(resolve => { + remote.dialog.showSaveDialog({ + title: 'Save sketch folder as...', + defaultPath + }, (filename) => resolve(filename)); + }); + if (!fsPath) { // Canceled + return; + } + const destinationUri = await this.fileSystemExt.getUri(fsPath); + if (!destinationUri) { + return; + } + const workspaceUri = await this.sketchService.copy(sketch, { destinationUri }); + if (workspaceUri) { + this.workspaceService.open(new URI(workspaceUri)); + } + } + }); + + registry.registerCommand(ArduinoCommands.NEW_SKETCH, { + execute: async () => { + try { + const sketch = await this.sketchService.createNewSketch(); this.workspaceService.open(new URI(sketch.uri)); } catch (e) { await this.messageService.error(e.toString()); } } - })); + }); + registry.registerCommand(ArduinoCommands.NEW_SKETCH_TOOLBAR, { + isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', + execute: async () => { + return registry.executeCommand(ArduinoCommands.NEW_SKETCH.id); + } + }); registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, { execute: async () => { @@ -481,7 +538,6 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(TerminalMenus.TERMINAL)); registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(CommonMenus.VIEW)); } - registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch'); registry.registerMenuAction(ArduinoMenus.SKETCH, { commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id, @@ -517,6 +573,11 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut registry.registerMenuAction([...CommonMenus.FILE_SETTINGS_SUBMENU, '3_settings_cli'], { commandId: ArduinoCommands.OPEN_CLI_CONFIG.id }); + + registry.registerMenuAction(CommonMenus.FILE_SAVE, { + commandId: ArduinoCommands.SAVE_SKETCH_AS.id, + label: 'Save As...' + }); } protected getMenuId(menuPath: string[]): string { @@ -526,13 +587,22 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut } registerKeybindings(keybindings: KeybindingRegistry): void { + keybindings.unregisterKeybinding('ctrlcmd+n'); // Unregister the keybinding for `New File`, will be used by `New Sketch`. (eclipse-theia/theia#8170) keybindings.registerKeybinding({ command: ArduinoCommands.VERIFY.id, - keybinding: 'ctrlcmd+alt+v' + keybinding: 'CtrlCmd+Alt+V' }); keybindings.registerKeybinding({ command: ArduinoCommands.UPLOAD.id, - keybinding: 'ctrlcmd+alt+u' + keybinding: 'CtrlCmd+Alt+U' + }); + keybindings.registerKeybinding({ + command: ArduinoCommands.NEW_SKETCH.id, + keybinding: 'CtrlCmd+N' + }); + keybindings.registerKeybinding({ + command: ArduinoCommands.SAVE_SKETCH_AS.id, + keybinding: 'CtrlCmd+Shift+S' }); } diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index d444d55d..ed3cc929 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -78,6 +78,9 @@ import { CoreServiceClientImpl } from './core-service-client-impl'; import { BoardsDetailsMenuUpdater } from './boards/boards-details-menu-updater'; import { BoardsConfigStore } from './boards/boards-config-store'; import { ILogger } from '@theia/core'; +import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext'; +import { WorkspaceFrontendContribution } from '@theia/workspace/lib/browser'; +import { ArduinoWorkspaceFrontendContribution } from './customization/arduino-workspace-frontend-contribution'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -254,6 +257,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un rebind(ScmContribution).to(ArduinoScmContribution).inSingletonScope(); rebind(SearchInWorkspaceFrontendContribution).to(ArduinoSearchInWorkspaceContribution).inSingletonScope(); rebind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope(); + rebind(WorkspaceFrontendContribution).to(ArduinoWorkspaceFrontendContribution).inSingletonScope(); // Show a disconnected status bar, when the daemon is not available bind(ArduinoApplicationConnectionStatusContribution).toSelf().inSingletonScope(); @@ -293,4 +297,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath, client); return client; }).inSingletonScope(); + + // File-system extension + bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope(); }); diff --git a/arduino-ide-extension/src/browser/arduino-workspace-service.ts b/arduino-ide-extension/src/browser/arduino-workspace-service.ts index 1c2881b4..7a334712 100644 --- a/arduino-ide-extension/src/browser/arduino-workspace-service.ts +++ b/arduino-ide-extension/src/browser/arduino-workspace-service.ts @@ -47,9 +47,7 @@ export class ArduinoWorkspaceService extends WorkspaceService { await this.server.setMostRecentlyUsedWorkspace(uri); return toOpen.uri; } - const { sketchDirUri } = (await this.configService.getConfiguration()); - this.logger.info(`No valid workspace URI found. Creating new sketch in ${sketchDirUri}`) - return (await this.sketchService.createNewSketch(sketchDirUri)).uri; + return (await this.sketchService.createNewSketch()).uri; } catch (err) { this.logger.fatal(`Failed to determine the sketch directory: ${err}`) this.messageService.error( diff --git a/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts b/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts index f6922d56..73eaf4c6 100644 --- a/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts +++ b/arduino-ide-extension/src/browser/customization/arduino-application-shell.ts @@ -1,7 +1,10 @@ + +import { injectable, inject } from 'inversify'; import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser'; import { EditorWidget } from '@theia/editor/lib/browser'; -import { injectable, inject } from 'inversify'; import { EditorMode } from '../editor-mode'; +import { CommandService } from '@theia/core'; +import { ArduinoCommands } from '../arduino-commands'; @injectable() export class ArduinoApplicationShell extends ApplicationShell { @@ -9,6 +12,9 @@ export class ArduinoApplicationShell extends ApplicationShell { @inject(EditorMode) protected readonly editorMode: EditorMode; + @inject(CommandService) + protected readonly commandService: CommandService; + protected refreshBottomPanelToggleButton() { if (this.editorMode.proMode) { super.refreshBottomPanelToggleButton(); @@ -34,6 +40,11 @@ export class ArduinoApplicationShell extends ApplicationShell { } } + async save(): Promise { + await super.save(); + await this.commandService.executeCommand(ArduinoCommands.SAVE_SKETCH_AS.id, { execOnlyIfTemp: true }); + } + private disableClose(widget: Widget | undefined): void { if (widget instanceof EditorWidget) { const onCloseRequest = (_: Message) => { diff --git a/arduino-ide-extension/src/browser/customization/arduino-workspace-frontend-contribution.ts b/arduino-ide-extension/src/browser/customization/arduino-workspace-frontend-contribution.ts new file mode 100644 index 00000000..a687f26b --- /dev/null +++ b/arduino-ide-extension/src/browser/customization/arduino-workspace-frontend-contribution.ts @@ -0,0 +1,62 @@ +import { injectable } from 'inversify'; +import { isOSX } from '@theia/core/lib/common/os'; +import { environment } from '@theia/application-package/lib/environment'; +import { CommonMenus } from '@theia/core/lib/browser'; +import { CommandRegistry } from '@theia/core/lib/common/command'; +import { MenuModelRegistry } from '@theia/core/lib/common/menu'; +import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; +import { WorkspaceFrontendContribution } from '@theia/workspace/lib/browser/workspace-frontend-contribution'; + +// TODO: https://github.com/eclipse-theia/theia/issues/8175 +@injectable() +export class ArduinoWorkspaceFrontendContribution extends WorkspaceFrontendContribution { + + registerCommands(registry: CommandRegistry): void { + super.registerCommands(registry); + registry.unregisterCommand(WorkspaceCommands.SAVE_AS); + registry.unregisterCommand(WorkspaceCommands.OPEN_FOLDER); + } + + registerMenus(registry: MenuModelRegistry): void { + if (isOSX || !environment.electron.is()) { + registry.registerMenuAction(CommonMenus.FILE_OPEN, { + commandId: WorkspaceCommands.OPEN.id, + order: 'a00' + }); + } + if (!isOSX && environment.electron.is()) { + registry.registerMenuAction(CommonMenus.FILE_OPEN, { + commandId: WorkspaceCommands.OPEN_FILE.id, + label: `${WorkspaceCommands.OPEN_FILE.dialogLabel}...`, + order: 'a01' + }); + registry.registerMenuAction(CommonMenus.FILE_OPEN, { + commandId: WorkspaceCommands.OPEN_FOLDER.id, + label: `${WorkspaceCommands.OPEN_FOLDER.dialogLabel}...`, + order: 'a02' + }); + } + registry.registerMenuAction(CommonMenus.FILE_OPEN, { + commandId: WorkspaceCommands.OPEN_WORKSPACE.id, + order: 'a10' + }); + registry.registerMenuAction(CommonMenus.FILE_OPEN, { + commandId: WorkspaceCommands.OPEN_RECENT_WORKSPACE.id, + order: 'a20' + }); + registry.registerMenuAction(CommonMenus.FILE_OPEN, { + commandId: WorkspaceCommands.SAVE_WORKSPACE_AS.id, + order: 'a30' + }); + + registry.registerMenuAction(CommonMenus.FILE_CLOSE, { + commandId: WorkspaceCommands.CLOSE.id + }); + + // `Save As` + // menus.registerMenuAction(CommonMenus.FILE_SAVE, { + // commandId: WorkspaceCommands.SAVE_AS.id, + // }); + } + +} diff --git a/arduino-ide-extension/src/browser/style/main.css b/arduino-ide-extension/src/browser/style/main.css index 1f62e515..c56b2ec9 100644 --- a/arduino-ide-extension/src/browser/style/main.css +++ b/arduino-ide-extension/src/browser/style/main.css @@ -42,7 +42,7 @@ mask-size: 800%; } -.arduino-save-file-icon { +.arduino-save-sketch-icon { -webkit-mask-position: 59px -4px; mask-position: 59px -4px; } @@ -57,6 +57,11 @@ mask-position: 156px -4px; } +.arduino-new-sketch-icon { + -webkit-mask-position: 124px -4px; + mask-position: 124px -4px; +} + .arduino-show-open-context-menu-icon { -webkit-mask-position: 92px -4px; mask-position: 92px -4px; diff --git a/arduino-ide-extension/src/common/protocol/filesystem-ext.ts b/arduino-ide-extension/src/common/protocol/filesystem-ext.ts new file mode 100644 index 00000000..6d8c0972 --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/filesystem-ext.ts @@ -0,0 +1,5 @@ +export const FileSystemExtPath = '/services/file-system-ext'; +export const FileSystemExt = Symbol('FileSystemExt'); +export interface FileSystemExt { + getUri(fsPath: string): Promise; +} diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 8c883abf..138bc4f4 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -1,22 +1,44 @@ export const SketchesServicePath = '/services/sketches-service'; export const SketchesService = Symbol('SketchesService'); export interface SketchesService { + /** * Returns with the direct sketch folders from the location of the `fileStat`. * The sketches returns with inverse-chronological order, the first item is the most recent one. */ - getSketches(uri?: string): Promise - getSketchFiles(uri: string): Promise + getSketches(uri?: string): Promise; + + getSketchFiles(uri: string): Promise; + /** - * Creates a new sketch folder in the `parentUri` location. - * Normally, `parentUri` is the client's workspace root, or the default `sketchDirUri` from the CLI. - * Note, `parentUri` and `sketchDirUri` can be the same. + * Creates a new sketch folder in the temp location. */ - createNewSketch(parentUri: string): Promise - isSketchFolder(uri: string): Promise + createNewSketch(): Promise; + + isSketchFolder(uri: string): Promise; + + /** + * Sketches are created to the temp location by default and will be moved under `directories.user` on save. + * This method resolves to `true` if the `sketch` is still in the temp location. Otherwise, `false`. + */ + isTemp(sketch: Sketch): Promise; + + /** + * If `isTemp` is `true` for the `sketch`, you can call this method to move the sketch from the temp + * location to `directories.user`. Resolves with the URI of the sketch after the move. Rejects, when the sketch + * was not in the temp folder. This method always overrides. It's the callers responsibility to ask the user whether + * the files at the destination can be overwritten or not. + */ + copy(sketch: Sketch, options: { destinationUri: string }): Promise; + + /** + * Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, resolved `undefined`. + */ + getSketchFolder(uri: string): Promise; + } export interface Sketch { readonly name: string; - readonly uri: string -} \ No newline at end of file + readonly uri: string; +} diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index a62daa12..22f1f052 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -33,6 +33,8 @@ import { HostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-rea import { ConfigFileValidator } from './config-file-validator'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { ArduinoEnvVariablesServer } from './arduino-env-variables-server'; +import { NodeFileSystemExt } from './node-filesystem-ext'; +import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext'; export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(EnvVariablesServer).to(ArduinoEnvVariablesServer).inSingletonScope(); @@ -185,4 +187,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ArduinoHostedPluginReader).toSelf().inSingletonScope(); rebind(HostedPluginReader).toService(ArduinoHostedPluginReader); + + // File-system extension for mapping paths to URIs + bind(NodeFileSystemExt).toSelf().inSingletonScope(); + bind(FileSystemExt).toDynamicValue(context => context.container.get(NodeFileSystemExt)); + bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(FileSystemExtPath, () => context.container.get(FileSystemExt))).inSingletonScope(); }); diff --git a/arduino-ide-extension/src/node/node-filesystem-ext.ts b/arduino-ide-extension/src/node/node-filesystem-ext.ts new file mode 100644 index 00000000..93af5744 --- /dev/null +++ b/arduino-ide-extension/src/node/node-filesystem-ext.ts @@ -0,0 +1,12 @@ +import { injectable } from 'inversify'; +import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileSystemExt } from '../common/protocol/filesystem-ext'; + +@injectable() +export class NodeFileSystemExt implements FileSystemExt { + + async getUri(fsPath: string): Promise { + return FileUri.create(fsPath).toString() + } + +} diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 108ac50d..ac6f6677 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -1,19 +1,29 @@ import { injectable, inject } from 'inversify'; +import * as os from 'os'; +import * as temp from 'temp'; import * as path from 'path'; import * as fs from './fs-extra'; -import { FileUri } from '@theia/core/lib/node'; +import { ncp } from 'ncp'; +import { FileUri, BackendApplicationContribution } from '@theia/core/lib/node'; import { ConfigService } from '../common/protocol/config-service'; import { SketchesService, Sketch } from '../common/protocol/sketches-service'; +import URI from '@theia/core/lib/common/uri'; export const ALLOWED_FILE_EXTENSIONS = ['.c', '.cpp', '.h', '.hh', '.hpp', '.s', '.pde', '.ino']; // TODO: `fs`: use async API @injectable() -export class SketchesServiceImpl implements SketchesService { +export class SketchesServiceImpl implements SketchesService, BackendApplicationContribution { + + protected readonly temp = temp.track(); @inject(ConfigService) protected readonly configService: ConfigService; + onStop(): void { + this.temp.cleanupSync(); + } + async getSketches(uri?: string): Promise { const sketches: Array = []; let fsPath: undefined | string; @@ -69,19 +79,26 @@ export class SketchesServiceImpl implements SketchesService { return this.getSketchFiles(FileUri.create(sketchDir).toString()); } - async createNewSketch(parentUri?: string): Promise { - const monthNames = ['january', 'february', 'march', 'april', 'may', 'june', - 'july', 'august', 'september', 'october', 'november', 'december' - ]; + async createNewSketch(): Promise { + const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const today = new Date(); - const uri = !!parentUri ? parentUri : (await this.configService.getConfiguration()).sketchDirUri; - const parent = FileUri.fsPath(uri); - + const parent = await new Promise((resolve, reject) => { + this.temp.mkdir({ prefix: '.arduinoProIDE' }, (err, dirPath) => { + if (err) { + reject(err); + return; + } + resolve(dirPath); + }) + }) const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`; + const config = await this.configService.getConfiguration(); + const user = FileUri.fsPath(config.sketchDirUri); 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))) { + // Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder! + if (fs.existsSync(path.join(user, sketchNameCandidate))) { continue; } @@ -96,14 +113,13 @@ export class SketchesServiceImpl implements SketchesService { const sketchDir = path.join(parent, sketchName) const sketchFile = path.join(sketchDir, `${sketchName}.ino`); await fs.mkdirp(sketchDir); - await fs.writeFile(sketchFile, ` -void setup() { -// put your setup code here, to run once: + await fs.writeFile(sketchFile, `void setup() { + // put your setup code here, to run once: } void loop() { -// put your main code here, to run repeatedly: + // put your main code here, to run repeatedly: } `, { encoding: 'utf8' }); @@ -113,6 +129,23 @@ void loop() { } } + async getSketchFolder(uri: string): Promise { + if (!uri) { + return undefined; + } + let currentUri = new URI(uri); + while (currentUri && !currentUri.path.isRoot) { + if (await this.isSketchFolder(currentUri.toString())) { + return { + name: currentUri.path.base, + uri: currentUri.toString() + }; + } + currentUri = currentUri.parent; + } + return undefined; + } + async isSketchFolder(uri: string): Promise { const fsPath = FileUri.fsPath(uri); if (fs.existsSync(fsPath) && fs.lstatSync(fsPath).isDirectory()) { @@ -126,4 +159,28 @@ void loop() { } return false; } + + async isTemp(sketch: Sketch): Promise { + const sketchPath = FileUri.fsPath(sketch.uri); + return sketchPath.indexOf('.arduinoProIDE') !== -1 && sketchPath.startsWith(os.tmpdir()); + } + + async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise { + const source = FileUri.fsPath(sketch.uri); + if (await !fs.exists(source)) { + throw new Error(`Sketch does not exist: ${sketch}`); + } + const destination = FileUri.fsPath(destinationUri); + await new Promise((resolve, reject) => { + ncp.ncp(source, destination, error => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + return FileUri.create(destination).toString(); + } + } diff --git a/yarn.lock b/yarn.lock index 9995a965..85ce741f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2690,6 +2690,13 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== +"@types/ncp@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/ncp/-/ncp-2.0.4.tgz#16c9e7fa2c849d429a1b142648987164b06bf490" + integrity sha512-erpimpT1pH8QfeNg77ypnjwz6CGMqrnL4DewVbqFzD9FXzSULjmG3KzjZnLNe7bzTSZm2W9DpkHyqop1g1KmgQ== + dependencies: + "@types/node" "*" + "@types/node-fetch@^2.5.7": version "2.5.7" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"