diff --git a/arduino-ide-extension/src/browser/arduino-file-menu.ts b/arduino-ide-extension/src/browser/arduino-file-menu.ts index d087c030..e27b28a0 100644 --- a/arduino-ide-extension/src/browser/arduino-file-menu.ts +++ b/arduino-ide-extension/src/browser/arduino-file-menu.ts @@ -2,7 +2,8 @@ import { injectable, inject } from "inversify"; import { MenuContribution, MenuModelRegistry, MenuPath, CommandRegistry, Command } from "@theia/core"; import { CommonMenus } from "@theia/core/lib/browser"; import { ArduinoCommands } from "./arduino-commands"; -import URI from "@theia/core/lib/common/uri"; +import { SketchesService, Sketch } from "../common/protocol/sketches-service"; +import { AWorkspaceService } from "./arduino-workspace-service"; export namespace ArduinoOpenSketchContextMenu { export const PATH: MenuPath = ['arduino-open-sketch-context-menu']; @@ -11,29 +12,46 @@ export namespace ArduinoOpenSketchContextMenu { export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...PATH, '3_examples']; } -export interface SketchMenuEntry { - name: string, - uri: URI -} - @injectable() export class ArduinoFileMenuContribution implements MenuContribution { @inject(CommandRegistry) protected readonly commands: CommandRegistry; + @inject(SketchesService) + protected readonly sketches: SketchesService; - protected async getWorkspaceSketches(): Promise { - return [ - { - name: 'foo', - uri: new URI('this/is/a/test/uri/foo') - }, - { - name: 'bar', - uri: new URI('this/is/a/test/uri/bar') + constructor( + @inject(AWorkspaceService) protected readonly workspaceService: AWorkspaceService, + @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry) { + workspaceService.onWorkspaceChanged(() => { + if (this.workspaceService.workspace) { + this.registerSketchesInMenu(menuRegistry); } - ] + }) + } + + protected registerSketchesInMenu(registry: MenuModelRegistry) { + this.getWorkspaceSketches().then(sketches => { + sketches.forEach(sketch => { + const command: Command = { + id: 'openSketch' + sketch.name + } + this.commands.registerCommand(command, { + execute: () => this.commands.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch) + }); + + registry.registerMenuAction(ArduinoOpenSketchContextMenu.WS_SKETCHES_GROUP, { + commandId: command.id, + label: sketch.name + }); + }) + }) + } + + protected async getWorkspaceSketches(): Promise { + const sketches = this.sketches.getSketches(this.workspaceService.workspace); + return sketches; } registerMenus(registry: MenuModelRegistry) { @@ -45,23 +63,5 @@ export class ArduinoFileMenuContribution implements MenuContribution { commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id, label: 'Open...' }); - - this.getWorkspaceSketches().then(sketches => { - sketches.forEach(sketch => { - - const command: Command = { - id: 'openSketch' + sketch.name - } - this.commands.registerCommand(command, { - execute: () => this.commands.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch) - }); - - registry.registerMenuAction(ArduinoOpenSketchContextMenu.WS_SKETCHES_GROUP, { - commandId: command.id, - label: sketch.name - }); - }) - }) - } } \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index eeaef960..6dee9775 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -24,7 +24,9 @@ import { EditorManager } from '@theia/editor/lib/browser'; import { open, ContextMenuRenderer, OpenerService, Widget } from '@theia/core/lib/browser'; import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { FileSystem } from '@theia/filesystem/lib/common'; -import { ArduinoOpenSketchContextMenu, SketchMenuEntry } from './arduino-file-menu'; +import { ArduinoOpenSketchContextMenu } from './arduino-file-menu'; +import { Sketch, SketchesService } from '../common/protocol/sketches-service'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; @injectable() export class ArduinoFrontendContribution extends DefaultFrontendApplicationContribution implements TabBarToolbarContribution, CommandContribution { @@ -68,15 +70,22 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; - @inject(FileDialogService) + @inject(FileDialogService) protected readonly fileDialogService: FileDialogService; - @inject(FileSystem) + @inject(FileSystem) protected readonly fileSystem: FileSystem; - @inject(OpenerService) + @inject(OpenerService) protected readonly openerService: OpenerService; + @inject(SketchesService) + protected readonly sketches: SketchesService; + + @inject(WindowService) + protected readonly windowService: WindowService; + + @postConstruct() protected async init(): Promise { // This is a hack. Otherwise, the backend services won't bind. @@ -165,7 +174,7 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr isEnabled: widget => this.isArduinoToolbar(widget), execute: async (widget: Widget, event: React.MouseEvent) => { const el = (event.target as HTMLElement).parentElement; - if(el) { + if (el) { this.contextMenuRenderer.render(ArduinoOpenSketchContextMenu.PATH, { x: el.getBoundingClientRect().left, y: el.getBoundingClientRect().top + el.offsetHeight @@ -179,8 +188,24 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr }) registry.registerCommand(ArduinoCommands.OPEN_SKETCH, { isEnabled: () => true, - execute: (sketch: SketchMenuEntry) => { - console.log("OPEN SOME SKETCH", sketch); + execute: async (sketch: Sketch) => { + // const url = new URL(window.location.href); + // if (this.workspaceService.workspace) { + // const wsUri = this.workspaceService.workspace.uri; + // const path = new URI(wsUri).path; + // url.hash = path + '?sketch=' + sketch.name + // } + // this.windowService.openNewWindow(url.toString()); + + const fileStat = await this.fileSystem.getFileStat(sketch.uri); + if (fileStat) { + const sketchFiles = await this.sketches.getSketchFiles(fileStat); + sketchFiles.forEach(sketchFile => { + const uri = new URI(sketchFile); + this.editorManager.open(uri); + }); + } + } }) registry.registerCommand(ArduinoCommands.NEW_SKETCH, new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, { diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 1233defe..73246f4a 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -12,6 +12,7 @@ import { ArduinoFrontendContribution } from './arduino-frontend-contribution'; import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution'; import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service'; +import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution'; import { CoreService, CoreServicePath } from '../common/protocol/core-service'; import { BoardsListWidget } from './boards/boards-list-widget'; @@ -70,6 +71,9 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un })); bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution); + // Sketch list service + bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope(); + // Boards Notification service for updating boards list // TODO (post-PoC): move this to boards service/backend bind(BoardsNotificationService).toSelf().inSingletonScope(); @@ -106,9 +110,11 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un container.get(CoreService); container.get(LibraryService); container.get(BoardsService); + container.get(SketchesService); return workspaceServiceExt; }); + bind(AWorkspaceService).toSelf().inSingletonScope(); rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope(); bind(SketchFactory).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts new file mode 100644 index 00000000..bea5ffc8 --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -0,0 +1,13 @@ +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 +} + +export interface Sketch { + name: string; + uri: string +} \ No newline at end of file diff --git a/arduino-ide-extension/src/node/arduino-backend-module.ts b/arduino-ide-extension/src/node/arduino-backend-module.ts index b1ccf18a..a81dc42d 100644 --- a/arduino-ide-extension/src/node/arduino-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-backend-module.ts @@ -17,6 +17,8 @@ import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core'; import { ToolOutputServiceServerImpl } from './tool-output-service-impl'; import { DefaultWorkspaceServerExt } from './default-workspace-server-ext'; import { WorkspaceServer } from '@theia/workspace/lib/common'; +import { SketchesServiceImpl } from './sketches-service-impl'; +import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ArduinoDaemon).toSelf().inSingletonScope(); @@ -30,6 +32,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { }); bind(ConnectionContainerModule).toConstantValue(libraryServiceConnectionModule); + // Sketches service + const sketchesServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { + bind(SketchesServiceImpl).toSelf().inSingletonScope(); + bind(SketchesService).toService(SketchesServiceImpl); + bindBackendService(SketchesServicePath, SketchesService); + }); + bind(ConnectionContainerModule).toConstantValue(sketchesServiceConnectionModule); + // Boards service const boardsServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(BoardsServiceImpl).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts new file mode 100644 index 00000000..661092cd --- /dev/null +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -0,0 +1,67 @@ +import { injectable } from "inversify"; +import { SketchesService, Sketch } from "../common/protocol/sketches-service"; +import URI from "@theia/core/lib/common/uri"; +import { FileStat } from "@theia/filesystem/lib/common"; +import * as fs from 'fs'; +import * as path from 'path'; + +export const ALLOWED_FILE_EXTENSIONS = [".c", ".cpp", ".h", ".hh", ".hpp", ".s", ".pde", ".ino"]; + +@injectable() +export class SketchesServiceImpl implements SketchesService { + + async getSketches(fileStat?: FileStat): Promise { + const sketches: Sketch[] = []; + if (fileStat && fileStat.isDirectory) { + const sketchFolderPath = this.getPath(fileStat); + const files = fs.readdirSync(sketchFolderPath); + files.forEach(file => { + const filePath = path.join(sketchFolderPath, file); + if (this.isSketchFolder(filePath, file)) { + sketches.push({ + name: file, + uri: filePath + }); + } + }); + } + return sketches; + } + + /** + * Return all allowed files. + * File extensions: "c", "cpp", "h", "hh", "hpp", "s", "pde", "ino" + */ + async getSketchFiles(sketchDir: FileStat): Promise { + const files: string[] = []; + const sketchDirPath = this.getPath(sketchDir); + const sketchDirContents = fs.readdirSync(sketchDirPath); + sketchDirContents.forEach(fileName => { + const filePath = path.join(sketchDirPath, fileName); + if (fs.existsSync(filePath) && + fs.lstatSync(filePath).isFile() && + ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1) { + files.push(filePath); + } + }); + return files; + } + + 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; + } + } + } + return false; + } + + protected getPath(fileStat: FileStat) { + const fileStatUri = fileStat.uri; + const uri = new URI(fileStatUri); + return uri.path.toString(); + } +} \ No newline at end of file