Merge pull request #80 from bcmi-labs/0.3.0-rc

All in on PR for 0.0.3
This commit is contained in:
Akos Kitta 2019-12-09 09:58:59 +01:00 committed by GitHub
commit 94233a1a19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 2685 additions and 2030 deletions

22
.vscode/settings.json vendored
View File

@ -1,3 +1,21 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
"tslint.enable": true,
"tslint.configFile": "./tslint.json",
"editor.formatOnSave": true,
"files.exclude": {
"**/lib": false
},
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"[typescript]": {
"editor.tabSize": 4
},
"[json]": {
"editor.tabSize": 2
},
"[jsonc]": {
"editor.tabSize": 2
},
"files.insertFinalNewline": true,
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "0.0.2",
"version": "0.0.3",
"description": "An extension for Theia building the Arduino IDE",
"license": "MIT",
"engines": {
@ -58,7 +58,7 @@
"rimraf": "^2.6.1",
"shelljs": "^0.8.3",
"tslint": "^5.5.0",
"typescript": "2.9.1",
"typescript": "3.5.3",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},

View File

@ -3,7 +3,7 @@ import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
import { CommandContribution, CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService } from '../common/protocol/boards-service';
import { ArduinoCommands } from './arduino-commands';
@ -22,9 +22,11 @@ import {
OpenerService,
Widget,
StatusBar,
ShellLayoutRestorer,
StatusBarAlignment,
QuickOpenService
QuickOpenService,
ApplicationShell,
FrontendApplicationContribution,
FrontendApplication
} from '@theia/core/lib/browser';
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
@ -44,6 +46,14 @@ 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';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
import { EditorMode } from './editor-mode';
export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
@ -57,16 +67,8 @@ export namespace ArduinoToolbarContextMenu {
export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples'];
}
export namespace ArduinoAdvancedMode {
export const LS_ID = 'arduino-advanced-mode';
export const TOGGLED: boolean = (() => {
const advancedModeStr = window.localStorage.getItem(LS_ID);
return advancedModeStr === 'true';
})();
}
@injectable()
export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution, MenuContribution {
export class ArduinoFrontendContribution implements FrontendApplicationContribution, TabBarToolbarContribution, CommandContribution, MenuContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@ -126,13 +128,13 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
protected readonly menuRegistry: MenuModelRegistry;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
protected readonly commandRegistry: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(ShellLayoutRestorer)
protected readonly layoutRestorer: ShellLayoutRestorer;
@inject(ArduinoShellLayoutRestorer)
protected readonly layoutRestorer: ArduinoShellLayoutRestorer;
@inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService;
@ -146,8 +148,29 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
protected boardsToolbarItem: BoardsToolBarItem | null;
protected wsSketchCount: number = 0;
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(FileNavigatorContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(OutlineViewContribution)
protected readonly outlineContribution: OutlineViewContribution;
@inject(ProblemContribution)
protected readonly problemContribution: ProblemContribution;
@inject(ScmContribution)
protected readonly scmContribution: ScmContribution;
@inject(SearchInWorkspaceFrontendContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected application: FrontendApplication;
protected wsSketchCount: number = 0; // TODO: this does not belong here, does it?
@postConstruct()
protected async init(): Promise<void> {
@ -171,6 +194,22 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
}
onStart(app: FrontendApplication): void {
this.application = app;
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(this.application);
}
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: ArduinoCommands.VERIFY.id,
@ -196,8 +235,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
ref={ref => this.boardsToolbarItem = ref}
commands={this.commands}
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClient}
boardService={this.boardsService} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left'
@ -213,12 +251,48 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
command: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
tooltip: 'Toggle Advanced Mode',
text: (ArduinoAdvancedMode.TOGGLED ? '$(toggle-on)' : '$(toggle-off)'),
text: (this.editorMode.proMode ? '$(toggle-on)' : '$(toggle-off)'),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
}
registerCommands(registry: CommandRegistry): void {
// TODO: use proper API https://github.com/eclipse-theia/theia/pull/6599
const allHandlers: { [id: string]: CommandHandler[] } = (registry as any)._handlers;
// Make sure to reveal the `Explorer` before executing `New File` and `New Folder`.
for (const command of [WorkspaceCommands.NEW_FILE, WorkspaceCommands.NEW_FOLDER]) {
const { id } = command;
const handlers = allHandlers[id].slice();
registry.unregisterCommand(id);
registry.registerCommand(command);
for (const handler of handlers) {
const wrapper: CommandHandler = {
execute: (...args: any[]) => {
this.fileNavigatorContributions.openView({ reveal: true }).then(() => handler.execute(args));
},
isVisible: (...args: any[]) => {
return handler.isVisible!(args);
},
isEnabled: (args: any[]) => {
return handler.isEnabled!(args);
},
isToggled: (args: any[]) => {
return handler.isToggled!(args);
}
};
if (!handler.isEnabled) {
delete wrapper.isEnabled;
}
if (!handler.isToggled) {
delete wrapper.isToggled;
}
if (!handler.isVisible) {
delete wrapper.isVisible;
}
registry.registerHandler(id, wrapper);
}
}
registry.registerCommand(ArduinoCommands.VERIFY, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => true,
@ -296,7 +370,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
});
}
} else {
this.commands.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
this.commandRegistry.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
}
}
});
@ -342,18 +416,14 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
}
})
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
execute: () => {
const oldModeState = ArduinoAdvancedMode.TOGGLED;
window.localStorage.setItem(ArduinoAdvancedMode.LS_ID, oldModeState ? 'false' : 'true');
registry.executeCommand('reset.layout');
},
execute: () => this.editorMode.toggle(),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
isToggled: () => ArduinoAdvancedMode.TOGGLED
isToggled: () => this.editorMode.proMode
})
}
registerMenus(registry: MenuModelRegistry) {
if (!ArduinoAdvancedMode.TOGGLED) {
if (!this.editorMode.proMode) {
// If are not in pro-mode, we have to disable the context menu for the tabs.
// Such as `Close`, `Close All`, etc.
for (const command of [
@ -362,7 +432,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
CommonCommands.CLOSE_RIGHT_TABS,
CommonCommands.CLOSE_ALL_TABS,
CommonCommands.COLLAPSE_PANEL,
CommonCommands.TOGGLE_MAXIMIZED
CommonCommands.TOGGLE_MAXIMIZED,
FileNavigatorCommands.REVEAL_IN_NAVIGATOR
]) {
registry.unregisterMenuAction(command);
}
@ -370,8 +441,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
registry.unregisterMenuAction(FileSystemCommands.UPLOAD);
registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD);
registry.unregisterMenuAction(WorkspaceCommands.NEW_FOLDER);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE);
@ -425,8 +494,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
const command: Command = {
id: 'openSketch' + sketch.name
}
this.commands.registerCommand(command, {
execute: () => this.commands.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
this.commandRegistry.registerCommand(command, {
execute: () => this.commandRegistry.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
});
registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, {
@ -466,7 +535,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
if (destinationFile && !destinationFile.isDirectory) {
const message = await this.validate(destinationFile);
if (!message) {
await this.workspaceService.open(destinationFileUri);
this.workspaceService.open(destinationFileUri);
return destinationFileUri;
} else {
this.messageService.warn(message);

View File

@ -10,7 +10,7 @@ import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser
import { LanguageClientContribution } from '@theia/languages/lib/browser';
import { ArduinoLanguageClientContribution } from './language/arduino-language-client-contribution';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution, ArduinoAdvancedMode } from './arduino-frontend-contribution';
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, BoardsServiceClient } from '../common/protocol/boards-service';
@ -30,30 +30,28 @@ import { ThemeService } from '@theia/core/lib/browser/theming';
import { ArduinoTheme } from './arduino-theme';
import { MenuContribution } from '@theia/core';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { SilentOutlineViewContribution } from './customization/silent-outline-contribution';
import { ArduinoOutlineViewContribution } from './customization/arduino-outline-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { SilentProblemContribution } from './customization/silent-problem-contribution';
import { SilentNavigatorContribution } from './customization/silent-navigator-contribution';
import { ArduinoProblemContribution } from './customization/arduino-problem-contribution';
import { ArduinoNavigatorContribution } from './customization/arduino-navigator-contribution';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { ArduinoOutputToolContribution } from './customization/silent-output-tool-contribution';
import { ArduinoOutputToolContribution } from './customization/arduino-output-tool-contribution';
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { ArduinoEditorContribution } from './customization/arduino-editor-contribution';
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { ArduinoMonacoStatusBarContribution } from './customization/arduino-monaco-status-bar-contribution';
import { ApplicationShell } from '@theia/core/lib/browser';
import { ApplicationShell, ShellLayoutRestorer } from '@theia/core/lib/browser';
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';
import { SilentScmContribution } from './customization/silent-scm-contribution';
import { ArduinoScmContribution } from './customization/arduino-scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SilentSearchInWorkspaceContribution } from './customization/silent-search-in-workspace-contribution';
import { ArduinoSearchInWorkspaceContribution } from './customization/arduino-search-in-workspace-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { LibraryItemRenderer } from './library/library-item-renderer';
import { BoardItemRenderer } from './boards/boards-item-renderer';
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
@ -70,6 +68,9 @@ import { ArduinoProblemManager } from './markers/arduino-problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { AboutDialog } from '@theia/core/lib/browser/about-dialog';
import { ArduinoAboutDialog } from './customization/arduino-about-dialog';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
import { EditorMode } from './editor-mode';
import { ListItemRenderer } from './components/component-list/list-item-renderer';
const ElementQueries = require('css-element-queries/src/ElementQueries');
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
@ -90,9 +91,11 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope();
bind(LanguageClientContribution).to(ArduinoLanguageClientContribution).inSingletonScope();
// Renderer for both the library and the core widgets.
bind(ListItemRenderer).toSelf().inSingletonScope();
// Library service
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
// Library list widget
bind(LibraryListWidget).toSelf();
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
@ -101,7 +104,6 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
createWidget: () => context.container.get(LibraryListWidget)
}));
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
bind(LibraryItemRenderer).toSelf().inSingletonScope();
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
@ -135,7 +137,6 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
createWidget: () => context.container.get(BoardsListWidget)
}));
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
bind(BoardItemRenderer).toSelf().inSingletonScope();
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
@ -158,7 +159,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
}).inSingletonScope();
// The workspace service extension
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt) => {
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt: WorkspaceServiceExt) => {
WebSocketConnectionProvider.createProxy(container, WorkspaceServiceExtPath, workspaceServiceExt);
// Eagerly active the core, library, and boards services.
container.get(CoreService);
@ -198,50 +199,37 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
const themeService = ThemeService.get();
themeService.register(...ArduinoTheme.themes);
// Customizing default Theia layout
if (!ArduinoAdvancedMode.TOGGLED) {
unbind(OutlineViewContribution);
bind(OutlineViewContribution).to(SilentOutlineViewContribution).inSingletonScope();
unbind(ProblemContribution);
bind(ProblemContribution).to(SilentProblemContribution).inSingletonScope();
unbind(FileNavigatorContribution);
bind(FileNavigatorContribution).to(SilentNavigatorContribution).inSingletonScope();
unbind(OutputToolbarContribution);
bind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
unbind(EditorContribution);
bind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
unbind(MonacoStatusBarContribution);
bind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
unbind(ApplicationShell);
bind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
unbind(ScmContribution);
bind(ScmContribution).to(SilentScmContribution).inSingletonScope();
unbind(SearchInWorkspaceFrontendContribution);
bind(SearchInWorkspaceFrontendContribution).to(SilentSearchInWorkspaceContribution).inSingletonScope();
} else {
// We use this CSS class on the body to modify the visibility of the close button for the editors and views.
document.body.classList.add(ArduinoAdvancedMode.LS_ID);
}
unbind(FrontendApplication);
bind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
bind(EditorMode).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorMode);
rebind(OutlineViewContribution).to(ArduinoOutlineViewContribution).inSingletonScope();
rebind(ProblemContribution).to(ArduinoProblemContribution).inSingletonScope();
rebind(FileNavigatorContribution).to(ArduinoNavigatorContribution).inSingletonScope();
rebind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
rebind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
rebind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
rebind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
rebind(ScmContribution).to(ArduinoScmContribution).inSingletonScope();
rebind(SearchInWorkspaceFrontendContribution).to(ArduinoSearchInWorkspaceContribution).inSingletonScope();
rebind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
// Monaco customizations
unbind(MonacoEditorProvider);
bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope();
bind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
rebind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
// Decorator customizations
unbind(TabBarDecoratorService);
bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope();
bind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
rebind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
// Problem markers
unbind(ProblemManager);
bind(ArduinoProblemManager).toSelf().inSingletonScope();
bind(ProblemManager).toService(ArduinoProblemManager);
rebind(ProblemManager).toService(ArduinoProblemManager);
// About dialog to show the CLI version
unbind(AboutDialog);
bind(ArduinoAboutDialog).toSelf().inSingletonScope();
bind(AboutDialog).toService(ArduinoAboutDialog);
rebind(AboutDialog).toService(ArduinoAboutDialog);
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
bind(ArduinoShellLayoutRestorer).toSelf().inSingletonScope();
rebind(ShellLayoutRestorer).toService(ArduinoShellLayoutRestorer);
});

View File

@ -9,6 +9,7 @@ const ARDUINO_JSON = MonacoThemeRegistry.SINGLETON.register(
export class ArduinoTheme {
static readonly arduino: Theme = {
type: 'light',
id: 'arduino-theme',
label: 'Arduino Light Theme',
description: 'Arduino Light Theme',

View File

@ -4,7 +4,7 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service
import { ConfigService } from '../common/protocol/config-service';
import { SketchesService } from '../common/protocol/sketches-service';
import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
import { ArduinoAdvancedMode } from './arduino-frontend-contribution';
import { EditorMode } from './editor-mode';
@injectable()
export class ArduinoWorkspaceService extends WorkspaceService {
@ -18,7 +18,10 @@ export class ArduinoWorkspaceService extends WorkspaceService {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
async getDefaultWorkspacePath(): Promise<string | undefined> {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async getDefaultWorkspaceUri(): Promise<string | undefined> {
const [hash, recentWorkspaces, recentSketches] = await Promise.all([
window.location.hash,
this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)),
@ -36,7 +39,8 @@ export class ArduinoWorkspaceService extends WorkspaceService {
await this.server.setMostRecentlyUsedWorkspace(uri);
return toOpen.uri;
}
return (await this.sketchService.createNewSketch()).uri;
const { sketchDirUri } = (await this.configService.getConfiguration());
return (await this.sketchService.createNewSketch(sketchDirUri)).uri;
}
private async isValid(uri: string): Promise<boolean> {
@ -46,7 +50,7 @@ export class ArduinoWorkspaceService extends WorkspaceService {
}
// The workspace root location must exist. However, when opening a workspace root in pro-mode,
// the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
if (!ArduinoAdvancedMode.TOGGLED) {
if (this.editorMode.proMode) {
return true;
}
const sketchFolder = await this.sketchService.isSketchFolder(uri);

View File

@ -4,7 +4,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten
import { BoardsService, Board } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { InstallationProgressDialog } from '../components/installation-progress-dialog';
import { InstallationProgressDialog } from '../components/progress-dialog';
import { BoardsConfig } from './boards-config';
@ -43,21 +43,21 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
// tslint:disable-next-line:max-line-length
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Yes', 'Install Manually').then(async answer => {
if (answer === 'Yes') {
const dialog = new InstallationProgressDialog(candidate.name);
const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]);
dialog.open();
try {
await this.boardsService.install(candidate);
await this.boardsService.install({ item: candidate });
} finally {
dialog.close();
}
}
if (answer) {
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
}
}
});
}
})
}
}
}
}

View File

@ -1,54 +0,0 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
import { BoardPackage } from '../../common/protocol/boards-service';
@injectable()
export class BoardItemRenderer extends ListItemRenderer<BoardPackage> {
renderItem(item: BoardPackage, install: (item: BoardPackage) => Promise<void>): React.ReactNode {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>{item.author}</span>;
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed'>INSTALLED</span>
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const description = <div className='summary'>{item.description}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const installButton = item.installable && !item.installedVersion &&
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (!!item.installedVersion || availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
}
})();
return <div className='component-list-item noselect'>
<div className='header'>
<span>{name} by {author}</span>
{installedVersion}
</div>
<div className='content'>
{summary}
{description}
</div>
<div className='info'>
{moreInfo}
</div>
<div className='footer'>
{installButton}
{versions}
</div>
</div>;
}
}

View File

@ -1,7 +1,7 @@
import { inject, injectable } from 'inversify';
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../components/component-list/list-widget';
import { BoardItemRenderer } from './boards-item-renderer';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardPackage> {
@ -11,7 +11,7 @@ export class BoardsListWidget extends ListWidget<BoardPackage> {
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(BoardItemRenderer) protected itemRenderer: BoardItemRenderer) {
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardPackage>) {
super({
id: BoardsListWidget.WIDGET_ID,

View File

@ -3,7 +3,7 @@ import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port } from '../../common/protocol/boards-service';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
@injectable()
@ -16,6 +16,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
protected storageService: LocalStorageService;
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onBoardUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
@ -31,6 +32,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event;
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
@postConstruct()
@ -87,6 +89,11 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
this.onBoardInstalledEmitter.fire(event);
}
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
this.logger.info('Board uninstalled: ', JSON.stringify(event));
this.onBoardUninstalledEmitter.fire(event);
}
set boardsConfig(config: BoardsConfig.Config) {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;

View File

@ -1,25 +1,66 @@
import * as React from 'react';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListItemRenderer } from './list-item-renderer';
export class ComponentListItem<T> extends React.Component<ComponentListItem.Props<T>> {
export class ComponentListItem<T extends ArduinoComponent> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
constructor(props: ComponentListItem.Props<T>) {
super(props);
if (props.item.installable) {
const version = props.item.availableVersions.filter(version => version !== props.item.installedVersion)[0];
this.state = {
selectedVersion: version
};
}
}
protected async install(item: T): Promise<void> {
await this.props.install(item);
const toInstall = this.state.selectedVersion;
const version = this.props.item.availableVersions.filter(version => version !== this.state.selectedVersion)[0];
this.setState({
selectedVersion: version
});
try {
await this.props.install(item, toInstall);
} catch {
this.setState({
selectedVersion: toInstall
});
}
}
protected async uninstall(item: T): Promise<void> {
await this.props.uninstall(item);
}
protected onVersionChange(version: Installable.Version) {
this.setState({ selectedVersion: version });
}
render(): React.ReactNode {
const { item, itemRenderer, install } = this.props;
return itemRenderer.renderItem(item, install.bind(this));
const { item, itemRenderer } = this.props;
return itemRenderer.renderItem(
Object.assign(this.state, { item }),
this.install.bind(this),
this.uninstall.bind(this),
this.onVersionChange.bind(this)
);
}
}
export namespace ComponentListItem {
export interface Props<T> {
export interface Props<T extends ArduinoComponent> {
readonly item: T;
readonly install: (item: T) => Promise<void>;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly itemRenderer: ListItemRenderer<T>;
}
export interface State {
selectedVersion?: Installable.Version;
}
}

View File

@ -1,8 +1,10 @@
import * as React from 'react';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ComponentListItem } from './component-list-item';
import { ListItemRenderer } from './list-item-renderer';
export class ComponentList<T> extends React.Component<ComponentList.Props<T>> {
export class ComponentList<T extends ArduinoComponent> extends React.Component<ComponentList.Props<T>> {
protected container?: HTMLElement;
@ -29,18 +31,20 @@ export class ComponentList<T> extends React.Component<ComponentList.Props<T>> {
key={this.props.itemLabel(item)}
item={item}
itemRenderer={this.props.itemRenderer}
install={this.props.install} />
install={this.props.install}
uninstall={this.props.uninstall} />
}
}
export namespace ComponentList {
export interface Props<T> {
export interface Props<T extends ArduinoComponent> {
readonly items: T[];
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
readonly install: (item: T) => Promise<void>;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly resolveContainer: (element: HTMLElement) => void;
}

View File

@ -1,14 +1,17 @@
import * as React from 'react';
import debounce = require('lodash.debounce');
import { Event } from '@theia/core/lib/common/event';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { Searchable } from '../../../common/protocol/searchable';
import { Installable } from '../../../common/protocol/installable';
import { InstallationProgressDialog } from '../installation-progress-dialog';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { InstallationProgressDialog, UninstallationProgressDialog } from '../progress-dialog';
import { SearchBar } from './search-bar';
import { ListWidget } from './list-widget';
import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer';
export class FilterableListContainer<T> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
export class FilterableListContainer<T extends ArduinoComponent> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
constructor(props: Readonly<FilterableListContainer.Props<T>>) {
super(props);
@ -18,12 +21,18 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
};
}
componentWillMount(): void {
componentDidMount(): void {
this.search = debounce(this.search, 500);
this.handleFilterTextChange('');
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
}
componentDidUpdate(): void {
// See: arduino/arduino-pro-ide#101
// Resets the top of the perfect scroll-bar's thumb.
this.props.container.updateScrollBar();
}
render(): React.ReactNode {
return <div className={'filterable-list-container'}>
{this.renderSearchFilter()}
@ -51,6 +60,7 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
itemLabel={itemLabel}
itemRenderer={itemRenderer}
install={this.install.bind(this)}
uninstall={this.uninstall.bind(this)}
resolveContainer={resolveContainer}
/>
}
@ -75,12 +85,34 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right)));
}
protected async install(item: T): Promise<void> {
protected async install(item: T, version: Installable.Version): Promise<void> {
const { installable, searchable, itemLabel } = this.props;
const dialog = new InstallationProgressDialog(itemLabel(item));
const dialog = new InstallationProgressDialog(itemLabel(item), version);
dialog.open();
try {
await installable.install(item);
await installable.install({ item, version });
const { items } = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();
}
}
protected async uninstall(item: T): Promise<void> {
const uninstall = await new ConfirmDialog({
title: 'Uninstall',
msg: `Do you want to uninstall ${item.name}?`,
ok: 'Yes',
cancel: 'No'
}).open();
if (!uninstall) {
return;
}
const { installable, searchable, itemLabel } = this.props;
const dialog = new UninstallationProgressDialog(itemLabel(item));
dialog.open();
try {
await installable.uninstall({ item });
const { items } = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
@ -92,7 +124,8 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
export namespace FilterableListContainer {
export interface Props<T> {
export interface Props<T extends ArduinoComponent> {
readonly container: ListWidget<T>;
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly itemLabel: (item: T) => string;

View File

@ -1,14 +1,17 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ComponentListItem } from './component-list-item';
@injectable()
export abstract class ListItemRenderer<T> {
export class ListItemRenderer<T extends ArduinoComponent> {
@inject(WindowService)
protected windowService: WindowService;
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
protected onMoreInfoClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.windowService.openNewWindow(target.href, { external: true });
@ -16,6 +19,73 @@ export abstract class ListItemRenderer<T> {
}
}
abstract renderItem(item: T, install: (item: T) => Promise<void>): React.ReactNode;
renderItem(
input: ComponentListItem.State & { item: T },
install: (item: T) => Promise<void>,
uninstall: (item: T) => Promise<void>,
onVersionChange: (version: Installable.Version) => void
): React.ReactNode {
}
const { item } = input;
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>{item.author}</span>;
const onClickUninstall = () => uninstall(item);
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed' onClick={onClickUninstall} />
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const description = <div className='summary'>{item.description}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onMoreInfoClick}>More info</a>;
const onClickInstall = () => install(item);
const installButton = item.installable &&
<button className='install' onClick={onClickInstall}>INSTALL</button>;
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const version = event.target.value;
if (version) {
onVersionChange(version);
}
}
const versions = (() => {
const { availableVersions } = item;
if (availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select
value={input.selectedVersion}
onChange={onSelectChange}>
{
item.availableVersions
.filter(version => version !== item.installedVersion) // Filter the version that is currently installed.
.map(version => <option value={version} key={version}>{version}</option>)
}
</select>;
}
})();
return <div className='component-list-item noselect'>
<div className='header'>
<span>{name} by {author}</span>
{installedVersion}
</div>
<div className='content'>
{summary}
{description}
</div>
<div className='info'>
{moreInfo}
</div>
<div className='footer'>
{installButton}
{versions}
</div>
</div>;
}
}

View File

@ -1,10 +1,11 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListWidget } from './list-widget';
@injectable()
export abstract class ListWidgetFrontendContribution<T> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
export abstract class ListWidgetFrontendContribution<T extends ArduinoComponent> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
}

View File

@ -7,11 +7,12 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { Installable } from '../../../common/protocol/installable';
import { Searchable } from '../../../common/protocol/searchable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { FilterableListContainer } from './filterable-list-container';
import { ListItemRenderer } from './list-item-renderer';
@injectable()
export abstract class ListWidget<T> extends ReactWidget {
export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget {
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
@ -61,6 +62,7 @@ export abstract class ListWidget<T> extends ReactWidget {
render(): React.ReactNode {
return <FilterableListContainer<T>
container={this}
resolveContainer={this.deferredContainer.resolve}
resolveFocus={this.onFocusResolved}
searchable={this.options.searchable}
@ -74,10 +76,16 @@ export abstract class ListWidget<T> extends ReactWidget {
this.deferredContainer.promise.then(() => this.filterTextChangeEmitter.fire(filterText));
}
updateScrollBar(): void {
if (this.scrollBar) {
this.scrollBar.update();
}
}
}
export namespace ListWidget {
export interface Options<T> {
export interface Options<T extends ArduinoComponent> {
readonly id: string;
readonly label: string;
readonly iconClass: string;

View File

@ -1,12 +0,0 @@
import { AbstractDialog } from '@theia/core/lib/browser';
export class InstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string) {
super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName}. Please wait.`;
}
}

View File

@ -0,0 +1,23 @@
import { AbstractDialog } from '@theia/core/lib/browser';
export class InstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string, version: string) {
super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName} [${version}]. Please wait...`;
}
}
export class UninstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string) {
super({ title: 'Uninstallation in progress' });
this.contentNode.textContent = `Uninstalling ${componentName}. Please wait...`;
}
}

View File

@ -1,22 +1,35 @@
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';
@injectable()
export class ArduinoApplicationShell extends ApplicationShell {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected refreshBottomPanelToggleButton() {
if (this.editorMode.proMode) {
super.refreshBottomPanelToggleButton();
}
}
protected async track(widget: Widget): Promise<void> {
const tracker = (this as any).tracker as FocusTracker<Widget>;
tracker.add(widget);
this.disableClose(Saveable.apply(widget));
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
for (const toTrack of await widget.getTrackableWidgets()) {
tracker.add(toTrack);
this.disableClose(Saveable.apply(toTrack));
}
if (widget.onDidChangeTrackableWidgets) {
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
if (this.editorMode.proMode) {
super.track(widget);
} else {
const tracker = (this as any).tracker as FocusTracker<Widget>;
tracker.add(widget);
this.disableClose(Saveable.apply(widget));
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
for (const toTrack of await widget.getTrackableWidgets()) {
tracker.add(toTrack);
this.disableClose(Saveable.apply(toTrack));
}
if (widget.onDidChangeTrackableWidgets) {
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
}
}
}
}

View File

@ -1,11 +0,0 @@
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

@ -2,7 +2,8 @@ import { injectable, inject } from 'inversify';
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ArduinoFrontendContribution, ArduinoAdvancedMode } from '../arduino-frontend-contribution';
import { EditorMode } from '../editor-mode';
import { ArduinoFrontendContribution } from '../arduino-frontend-contribution';
@injectable()
export class ArduinoFrontendApplication extends FrontendApplication {
@ -16,12 +17,15 @@ export class ArduinoFrontendApplication extends FrontendApplication {
@inject(ArduinoFrontendContribution)
protected readonly frontendContribution: ArduinoFrontendContribution;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected async initializeLayout(): Promise<void> {
super.initializeLayout().then(() => {
// If not in PRO mode, we open the sketch file with all the related files.
// Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
// TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
if (!ArduinoAdvancedMode.TOGGLED) {
if (!this.editorMode.proMode) {
this.workspaceService.roots.then(roots => {
for (const root of roots) {
this.fileSystem.exists(root.uri).then(exists => {

View File

@ -0,0 +1,18 @@
import { injectable, inject } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoNavigatorContribution extends FileNavigatorContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@ -0,0 +1,19 @@
import { injectable, inject } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoOutlineViewContribution extends OutlineViewContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@ -1,11 +1,18 @@
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { inject, injectable } from 'inversify';
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 { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoOutputToolContribution extends OutputToolbarContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
if (this.editorMode.proMode) {
super.registerToolbarItems(toolbarRegistry);
}
}
}

View File

@ -0,0 +1,25 @@
import { inject, injectable } from 'inversify';
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoProblemContribution extends ProblemContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
protected setStatusBarElement(problemStat: ProblemStat): void {
if (this.editorMode.proMode) {
super.setStatusBarElement(problemStat);
}
}
}

View File

@ -0,0 +1,24 @@
import { inject, injectable } from 'inversify';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoScmContribution extends ScmContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout();
}
}
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
if (this.editorMode.proMode) {
super.setStatusBarEntry(id, entry);
}
}
}

View File

@ -0,0 +1,18 @@
import { inject, injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@ -1,11 +0,0 @@
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<void> {
}
}

View File

@ -1,12 +0,0 @@
import { injectable } from 'inversify';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { FrontendApplication } from '@theia/core/lib/browser';
@injectable()
export class SilentOutlineViewContribution extends OutlineViewContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
}
}

View File

@ -1,15 +0,0 @@
import { injectable } from 'inversify';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
import { FrontendApplication } from '@theia/core/lib/browser';
@injectable()
export class SilentProblemContribution extends ProblemContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
}
protected setStatusBarElement(problemStat: ProblemStat) {
}
}

View File

@ -1,14 +0,0 @@
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 {
async initializeLayout(): Promise<void> {
}
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
}
}

View File

@ -1,11 +0,0 @@
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<void> {
}
}

View File

@ -0,0 +1,48 @@
import { injectable } from 'inversify';
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { EditorWidget } from '@theia/editor/lib/browser';
@injectable()
export class EditorMode implements FrontendApplicationContribution {
protected app: FrontendApplication;
onStart(app: FrontendApplication): void {
this.app = app;
if (this.proMode) {
// We use this CSS class on the body to modify the visibility of the close button for the editors and views.
document.body.classList.add(EditorMode.PRO_MODE_KEY);
}
}
get proMode(): boolean {
const value = window.localStorage.getItem(EditorMode.PRO_MODE_KEY);
return value === 'true';
}
async toggle(): Promise<void> {
const oldState = this.proMode;
const inAdvancedMode = !oldState;
window.localStorage.setItem(EditorMode.PRO_MODE_KEY, String(inAdvancedMode));
if (!inAdvancedMode) {
const { shell } = this.app;
// Close all widget that is neither editor nor `Output`.
for (const area of ['left', 'right', 'bottom', 'main'] as Array<ApplicationShell.Area>) {
shell.closeTabs(area, ({ owner }) => !(owner instanceof EditorWidget || owner instanceof OutputWidget));
}
}
// `storeLayout` has a sync API but the implementation is async, we store the layout manually before we reload the page.
// See: https://github.com/eclipse-theia/theia/issues/6579
// XXX: hack instead of injecting the `ArduinoShellLayoutRestorer` we have to retrieve it from the application to avoid DI cycle.
const layoutRestorer = (this.app as any).layoutRestorer as ArduinoShellLayoutRestorer
await layoutRestorer.storeLayoutAsync(this.app);
window.location.reload(true);
}
}
export namespace EditorMode {
export const PRO_MODE_KEY = 'arduino-advanced-mode';
}

View File

@ -2,6 +2,7 @@ import { injectable, inject, postConstruct } from 'inversify';
import { BaseLanguageClientContribution } from '@theia/languages/lib/browser';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { Board, BoardPackage } from '../../common/protocol/boards-service';
@injectable()
export class ArduinoLanguageClientContribution extends BaseLanguageClientContribution {
@ -25,6 +26,18 @@ export class ArduinoLanguageClientContribution extends BaseLanguageClientContrib
@postConstruct()
protected init() {
this.boardsServiceClient.onBoardsConfigChanged(this.selectBoard.bind(this));
const restartIfAffected = (pkg: BoardPackage) => {
if (!this.boardConfig) {
this.restart();
return;
}
const { selectedBoard } = this.boardConfig;
if (selectedBoard && pkg.boards.some(board => Board.sameAs(board, selectedBoard))) {
this.restart();
}
}
this.boardsServiceClient.onBoardInstalled(({ pkg }) => restartIfAffected(pkg));
this.boardsServiceClient.onBoardUninstalled(({ pkg }) => restartIfAffected(pkg));
}
selectBoard(config: BoardsConfig.Config): void {

View File

@ -1,52 +0,0 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { Library } from '../../common/protocol/library-service';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class LibraryItemRenderer extends ListItemRenderer<Library> {
renderItem(item: Library, install: (item: Library) => Promise<void>): React.ReactNode {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>by {item.author}</span>;
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed'>INSTALLED</span>
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const installButton = item.installable && !item.installedVersion &&
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (!!item.installedVersion || availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
}
})();
return <div className='component-list-item noselect'>
<div className='header'>
<span>{name} {author}</span>
{installedVersion}
</div>
<div className='content'>
{summary}
</div>
<div className='info'>
{moreInfo}
</div>
<div className='footer'>
{installButton}
{versions}
</div>
</div>;
}
}

View File

@ -1,7 +1,7 @@
import { inject, injectable } from 'inversify';
import { Library, LibraryService } from '../../common/protocol/library-service';
import { ListWidget } from '../components/component-list/list-widget';
import { LibraryItemRenderer } from './library-item-renderer';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class LibraryListWidget extends ListWidget<Library> {
@ -11,7 +11,7 @@ export class LibraryListWidget extends ListWidget<Library> {
constructor(
@inject(LibraryService) protected service: LibraryService,
@inject(LibraryItemRenderer) protected itemRenderer: LibraryItemRenderer) {
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<Library>) {
super({
id: LibraryListWidget.WIDGET_ID,

View File

@ -0,0 +1,24 @@
import { injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ShellLayoutRestorer } from '@theia/core/lib/browser/shell/shell-layout-restorer';
@injectable()
export class ArduinoShellLayoutRestorer extends ShellLayoutRestorer {
// Workaround for https://github.com/eclipse-theia/theia/issues/6579.
async storeLayoutAsync(app: FrontendApplication): Promise<void> {
if (this.shouldStoreLayout) {
try {
this.logger.info('>>> Storing the layout...');
const layoutData = app.shell.getLayoutData();
const serializedLayoutData = this.deflate(layoutData);
await this.storageService.setData(this.storageKey, serializedLayoutData);
this.logger.info('<<< The layout has been successfully stored.');
} catch (error) {
await this.storageService.setData(this.storageKey, undefined);
this.logger.error('Error during serialization of layout data', error);
}
}
}
}

View File

@ -35,7 +35,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
#select-board-dialog .selectBoardContainer .search input,
#select-board-dialog .selectBoardContainer .list,
#select-board-dialog .selectBoardContainer .list {
background: white; /* TODO find a theia color instead! */
background: var(--theia-layout-color0);
}
#select-board-dialog .selectBoardContainer .body .search input {

View File

@ -61,6 +61,14 @@
background-color: var(--theia-layout-color2);
}
/* Perfect scrollbar does not like if we explicitly set the `background-color` of the contained elements.
See above: `.filterable-list-container .items-container > div:nth-child(odd|event)`.
We have to increase `z-index` of the scroll-bar thumb. Otherwise, the thumb is not visible.
https://github.com/arduino/arduino-pro-ide/issues/82 */
.arduino-list-widget .ps__rail-y > .ps__thumb-y {
z-index: 1;
}
.component-list-item {
padding: 10px 10px 10px 15px;
font-size: var(--theia-ui-font-size1);
@ -108,8 +116,9 @@
color: var(--theia-ui-font-color2);
}
.component-list-item .header .installed {
.component-list-item .header .installed:before {
margin-left: 4px;
display: inline-block;
justify-self: end;
background-color: var(--theia-accent-color1);
padding: 2px 4px 2px 4px;
@ -117,6 +126,13 @@
font-weight: bold;
max-height: calc(1em + 4px);
color: var(--theia-inverse-ui-font-color0);
content: 'INSTALLED';
}
.component-list-item .header .installed:hover:before {
background-color: var(--theia-inverse-ui-font-color0);
color: var(--theia-accent-color1);
content: 'UNINSTALL';
}
.component-list-item[min-width~="170px"] .footer {

View File

@ -1,3 +1,4 @@
import { Installable } from './installable';
export interface ArduinoComponent {
readonly name: string;
@ -6,8 +7,8 @@ export interface ArduinoComponent {
readonly description: string;
readonly moreInfoLink?: string;
readonly availableVersions: string[];
readonly availableVersions: Installable.Version[];
readonly installable: boolean;
readonly installedVersion?: string;
readonly installedVersion?: Installable.Version;
}

View File

@ -46,10 +46,15 @@ export interface BoardInstalledEvent {
readonly pkg: Readonly<BoardPackage>;
}
export interface BoardUninstalledEvent {
readonly pkg: Readonly<BoardPackage>;
}
export const BoardsServiceClient = Symbol('BoardsServiceClient');
export interface BoardsServiceClient {
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
notifyBoardInstalled(event: BoardInstalledEvent): void
notifyBoardUninstalled(event: BoardUninstalledEvent): void
}
export const BoardsServicePath = '/services/boards-service';
@ -130,7 +135,7 @@ export namespace Port {
}
if (isOSX) {
// Example: `/dev/cu.usbmodem14401`
if (/(tty|cu)\..*/.test(address.substring('/dev/'.length))) {
if (/(tty|cu)\..*/.test(address.substring('/dev/'.length))) {
return [
'/dev/cu.MALS',
'/dev/cu.SOC',

View File

@ -1,3 +1,23 @@
export interface Installable<T> {
install(item: T): Promise<void>;
}
const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
import { ArduinoComponent } from './arduino-component';
export interface Installable<T extends ArduinoComponent> {
/**
* If `options.version` is specified, that will be installed. Otherwise, `item.availableVersions[0]`.
*/
install(options: { item: T, version?: Installable.Version }): Promise<void>;
/**
* Uninstalls the given component. It is a NOOP if not installed.
*/
uninstall(options: { item: T }): Promise<void>;
}
export namespace Installable {
export type Version = string;
export namespace Version {
/**
* Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.)
*/
export const COMPARATOR = (left: Version, right: Version) => naturalCompare(right, left);
}
}

View File

@ -5,14 +5,9 @@ import { ArduinoComponent } from './arduino-component';
export const LibraryServicePath = '/services/library-service';
export const LibraryService = Symbol('LibraryService');
export interface LibraryService extends Installable<Library>, Searchable<Library> {
install(library: Library): Promise<void>;
install(options: { item: Library, version?: Installable.Version }): Promise<void>;
}
export interface Library extends ArduinoComponent {
readonly builtIn?: boolean;
}
export namespace Library {
// TODO: figure out whether we need a dedicated `version` type.
export type Version = string;
}

View File

@ -8,10 +8,11 @@ export interface SketchesService {
getSketches(uri?: string): Promise<Sketch[]>
getSketchFiles(uri: string): Promise<string[]>
/**
* Creates a new sketch folder in the `parentUri` location. If `parentUri` is not specified,
* it falls back to the default `sketchDirUri` from the CLI.
* 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.
*/
createNewSketch(parentUri?: string): Promise<Sketch>
createNewSketch(parentUri: string): Promise<Sketch>
isSketchFolder(uri: string): Promise<boolean>
}

View File

@ -38,10 +38,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ArduinoCliContribution).toSelf().inSingletonScope();
bind(CliContribution).toService(ArduinoCliContribution);
// Provides the path of the Ardunio CLI.
// Provides the path of the Arduino CLI.
bind(ArduinoCli).toSelf().inSingletonScope();
// Shared daemonn
// Shared daemon
bind(ArduinoDaemon).toSelf().inSingletonScope();
bind(BackendApplicationContribution).toService(ArduinoDaemon);

View File

@ -26,30 +26,15 @@ export class ArduinoCli {
const buildVersion = await this.spawn(`"${buildCli}"`, ['version']);
const buildShortVersion = (buildVersion.match(version) || [])[0];
this.execPath = buildCli;
try {
const pathCli = await new Promise<string>((resolve, reject) => {
which(cli, (error, path) => {
if (error) {
reject(error);
return;
}
resolve(path);
});
});
if (!pathCli) {
return buildCli;
}
const pathVersion = await this.spawn(`"${pathCli}"`, ['version']);
const pathShortVersion = (pathVersion.match(version) || [])[0];
if (semver.gt(pathShortVersion, buildShortVersion)) {
this.execPath = pathCli;
return pathCli;
}
} catch (error) {
this.logger.warn(`Could not check for Arduino CLI in $PATH, using embedded CLI instead:`, error);
// Any errors here should be safe to ignore, e.g.:
// - Could not search for CLI in $PATH
// - Could not get version of CLI in $PATH
const pathCli = await new Promise<string | undefined>(resolve => which(cli, (error, path) => resolve(error ? undefined : path)));
if (!pathCli) {
return buildCli;
}
const pathVersion = await this.spawn(`"${pathCli}"`, ['version']);
const pathShortVersion = (pathVersion.match(version) || [])[0];
if (semver.gt(pathShortVersion, buildShortVersion)) {
this.execPath = pathCli;
return pathCli;
}
return buildCli;
}

View File

@ -2,14 +2,28 @@ import * as PQueue from 'p-queue';
import { injectable, inject, postConstruct, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient, Port } from '../common/protocol/boards-service';
import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp } from './cli-protocol/commands/core_pb';
import {
PlatformSearchReq,
PlatformSearchResp,
PlatformInstallReq,
PlatformInstallResp,
PlatformListReq,
PlatformListResp,
Platform,
PlatformUninstallReq,
PlatformUninstallResp
} from './cli-protocol/commands/core_pb';
import { CoreClientProvider } from './core-client-provider';
import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { Installable } from '../common/protocol/installable';
@injectable()
export class BoardsServiceImpl implements BoardsService {
@inject(ILogger)
protected logger: ILogger;
@inject(ILogger)
@named('discovery')
protected discoveryLogger: ILogger;
@ -24,11 +38,11 @@ export class BoardsServiceImpl implements BoardsService {
protected discoveryTimer: NodeJS.Timeout | undefined;
/**
* Poor man's serial discovery:
* Stores the state of the currently discovered, attached boards.
* This state is updated via periodical polls.
* Stores the state of the currently discovered and attached boards.
* This state is updated via periodical polls. If there diff, a change event will be sent out to the frontend.
*/
protected _attachedBoards: { boards: Board[] } = { boards: [] };
protected _availablePorts: { ports: Port[] } = { ports: [] };
protected attachedBoards: { boards: Board[] } = { boards: [] };
protected availablePorts: { ports: Port[] } = { ports: [] };
protected client: BoardsServiceClient | undefined;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
@ -38,8 +52,8 @@ export class BoardsServiceImpl implements BoardsService {
this.discoveryLogger.trace('Discovering attached boards and available ports...');
this.doGetAttachedBoardsAndAvailablePorts().then(({ boards, ports }) => {
const update = (oldBoards: Board[], newBoards: Board[], oldPorts: Port[], newPorts: Port[], message: string) => {
this._attachedBoards = { boards: newBoards };
this._availablePorts = { ports: newPorts };
this.attachedBoards = { boards: newBoards };
this.availablePorts = { ports: newPorts };
this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newBoards)} and available ports: ${JSON.stringify(newPorts)}`);
if (this.client) {
this.client.notifyAttachedBoardsChanged({
@ -95,17 +109,21 @@ export class BoardsServiceImpl implements BoardsService {
}
dispose(): void {
this.logger.info('>>> Disposing boards service...')
this.queue.pause();
this.queue.clear();
if (this.discoveryTimer !== undefined) {
clearInterval(this.discoveryTimer);
}
this.logger.info('<<< Disposed boards service.')
}
async getAttachedBoards(): Promise<{ boards: Board[] }> {
return this._attachedBoards;
return this.attachedBoards;
}
async getAvailablePorts(): Promise<{ ports: Port[] }> {
return this._availablePorts;
return this.availablePorts;
}
private async doGetAttachedBoardsAndAvailablePorts(): Promise<{ boards: Board[], ports: Port[] }> {
@ -207,35 +225,76 @@ export class BoardsServiceImpl implements BoardsService {
const req = new PlatformSearchReq();
req.setSearchArgs(options.query || "");
req.setAllVersions(true);
req.setInstance(instance);
const resp = await new Promise<PlatformSearchResp>((resolve, reject) => client.platformSearch(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
let items = resp.getSearchOutputList().map(item => {
const packages = new Map<string, BoardPackage>();
const toPackage = (platform: Platform) => {
let installedVersion: string | undefined;
const matchingPlatform = installedPlatforms.find(ip => ip.getId() === item.getId());
const matchingPlatform = installedPlatforms.find(ip => ip.getId() === platform.getId());
if (!!matchingPlatform) {
installedVersion = matchingPlatform.getInstalled();
}
const result: BoardPackage = {
id: item.getId(),
name: item.getName(),
author: item.getMaintainer(),
availableVersions: [item.getLatest()],
description: item.getBoardsList().map(b => b.getName()).join(", "),
return {
id: platform.getId(),
name: platform.getName(),
author: platform.getMaintainer(),
availableVersions: [platform.getLatest()],
description: platform.getBoardsList().map(b => b.getName()).join(", "),
installable: true,
summary: "Boards included in this package:",
installedVersion,
boards: item.getBoardsList().map(b => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
moreInfoLink: item.getWebsite()
boards: platform.getBoardsList().map(b => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
moreInfoLink: platform.getWebsite()
}
return result;
});
}
return { items };
// We must group the cores by ID, and sort platforms by, first the installed version, then version alphabetical order.
// Otherwise we lose the FQBN information.
const groupedById: Map<string, Platform[]> = new Map();
for (const platform of resp.getSearchOutputList()) {
const id = platform.getId();
if (groupedById.has(id)) {
groupedById.get(id)!.push(platform);
} else {
groupedById.set(id, [platform]);
}
}
const installedAwareVersionComparator = (left: Platform, right: Platform) => {
// XXX: we cannot rely on `platform.getInstalled()`, it is always an empty string.
const leftInstalled = !!installedPlatforms.find(ip => ip.getId() === left.getId() && ip.getInstalled() === left.getLatest());
const rightInstalled = !!installedPlatforms.find(ip => ip.getId() === right.getId() && ip.getInstalled() === right.getLatest());
if (leftInstalled && !rightInstalled) {
return -1;
}
if (!leftInstalled && rightInstalled) {
return 1;
}
return Installable.Version.COMPARATOR(right.getLatest(), left.getLatest()); // Higher version comes first.
}
for (const id of groupedById.keys()) {
groupedById.get(id)!.sort(installedAwareVersionComparator);
}
for (const id of groupedById.keys()) {
for (const platform of groupedById.get(id)!) {
const id = platform.getId();
const pkg = packages.get(id);
if (pkg) {
pkg.availableVersions.push(platform.getLatest());
pkg.availableVersions.sort(Installable.Version.COMPARATOR);
} else {
packages.set(id, toPackage(platform));
}
}
}
return { items: [...packages.values()] };
}
async install(pkg: BoardPackage): Promise<void> {
async install(options: { item: BoardPackage, version?: Installable.Version }): Promise<void> {
const pkg = options.item;
const version = !!options.version ? options.version : pkg.availableVersions[0];
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return;
@ -248,7 +307,7 @@ export class BoardsServiceImpl implements BoardsService {
req.setInstance(instance);
req.setArchitecture(boardName);
req.setPlatformPackage(platform);
req.setVersion(pkg.availableVersions[0]);
req.setVersion(version);
console.info("Starting board installation", pkg);
const resp = client.platformInstall(req);
@ -268,4 +327,38 @@ export class BoardsServiceImpl implements BoardsService {
console.info("Board installation done", pkg);
}
async uninstall(options: { item: BoardPackage }): Promise<void> {
const pkg = options.item;
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return;
}
const { client, instance } = coreClient;
const [platform, boardName] = pkg.id.split(":");
const req = new PlatformUninstallReq();
req.setInstance(instance);
req.setArchitecture(boardName);
req.setPlatformPackage(platform);
console.info("Starting board uninstallation", pkg);
let logged = false;
const resp = client.platformUninstall(req);
resp.on('data', (_: PlatformUninstallResp) => {
if (!logged) {
this.toolOutputService.publishNewOutput("board uninstall", `uninstalling ${pkg.id}\n`)
logged = true;
}
})
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
if (this.client) {
this.client.notifyBoardUninstalled({ pkg });
}
console.info("Board uninstallation done", pkg);
}
}

View File

@ -2,6 +2,7 @@
// file: commands/board.proto
/* tslint:disable */
/* eslint-disable */
import * as jspb from "google-protobuf";
import * as commands_common_pb from "../commands/common_pb";

View File

@ -2,6 +2,7 @@
// file: commands/commands.proto
/* tslint:disable */
/* eslint-disable */
import * as grpc from "@grpc/grpc-js";
import * as commands_commands_pb from "../commands/commands_pb";

View File

@ -2,6 +2,7 @@
// file: commands/commands.proto
/* tslint:disable */
/* eslint-disable */
import * as jspb from "google-protobuf";
import * as commands_common_pb from "../commands/common_pb";

View File

@ -2,6 +2,7 @@
// file: commands/common.proto
/* tslint:disable */
/* eslint-disable */
import * as jspb from "google-protobuf";

View File

@ -2,6 +2,7 @@
// file: commands/compile.proto
/* tslint:disable */
/* eslint-disable */
import * as jspb from "google-protobuf";
import * as commands_common_pb from "../commands/common_pb";

View File

@ -2,6 +2,7 @@
// file: commands/core.proto
/* tslint:disable */
/* eslint-disable */
import * as jspb from "google-protobuf";
import * as commands_common_pb from "../commands/common_pb";
@ -262,6 +263,9 @@ export class PlatformSearchReq extends jspb.Message {
getSearchArgs(): string;
setSearchArgs(value: string): void;
getAllVersions(): boolean;
setAllVersions(value: boolean): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): PlatformSearchReq.AsObject;
@ -277,6 +281,7 @@ export namespace PlatformSearchReq {
export type AsObject = {
instance?: commands_common_pb.Instance.AsObject,
searchArgs: string,
allVersions: boolean,
}
}

View File

@ -1705,7 +1705,8 @@ proto.cc.arduino.cli.commands.PlatformSearchReq.prototype.toObject = function(op
proto.cc.arduino.cli.commands.PlatformSearchReq.toObject = function(includeInstance, msg) {
var f, obj = {
instance: (f = msg.getInstance()) && commands_common_pb.Instance.toObject(includeInstance, f),
searchArgs: jspb.Message.getFieldWithDefault(msg, 2, "")
searchArgs: jspb.Message.getFieldWithDefault(msg, 2, ""),
allVersions: jspb.Message.getFieldWithDefault(msg, 3, false)
};
if (includeInstance) {
@ -1751,6 +1752,10 @@ proto.cc.arduino.cli.commands.PlatformSearchReq.deserializeBinaryFromReader = fu
var value = /** @type {string} */ (reader.readString());
msg.setSearchArgs(value);
break;
case 3:
var value = /** @type {boolean} */ (reader.readBool());
msg.setAllVersions(value);
break;
default:
reader.skipField();
break;
@ -1795,6 +1800,13 @@ proto.cc.arduino.cli.commands.PlatformSearchReq.serializeBinaryToWriter = functi
f
);
}
f = message.getAllVersions();
if (f) {
writer.writeBool(
3,
f
);
}
};
@ -1843,6 +1855,23 @@ proto.cc.arduino.cli.commands.PlatformSearchReq.prototype.setSearchArgs = functi
};
/**
* optional bool all_versions = 3;
* Note that Boolean fields may be set to 0/1 when serialized from a Java server.
* You should avoid comparisons like {@code val === true/false} in those cases.
* @return {boolean}
*/
proto.cc.arduino.cli.commands.PlatformSearchReq.prototype.getAllVersions = function() {
return /** @type {boolean} */ (jspb.Message.getFieldWithDefault(this, 3, false));
};
/** @param {boolean} value */
proto.cc.arduino.cli.commands.PlatformSearchReq.prototype.setAllVersions = function(value) {
jspb.Message.setProto3BooleanField(this, 3, value);
};
/**
* Generated by JsPbCodeGenerator.

View File

@ -2,6 +2,7 @@
// file: commands/lib.proto
/* tslint:disable */
/* eslint-disable */
import * as jspb from "google-protobuf";
import * as commands_common_pb from "../commands/common_pb";

View File

@ -2,6 +2,7 @@
// file: commands/upload.proto
/* tslint:disable */
/* eslint-disable */
import * as jspb from "google-protobuf";
import * as commands_common_pb from "../commands/common_pb";

View File

@ -2,6 +2,7 @@
// file: monitor/monitor.proto
/* tslint:disable */
/* eslint-disable */
import * as grpc from "@grpc/grpc-js";
import * as monitor_monitor_pb from "../monitor/monitor_pb";

View File

@ -2,6 +2,7 @@
// file: monitor/monitor.proto
/* tslint:disable */
/* eslint-disable */
import * as jspb from "google-protobuf";
import * as google_protobuf_struct_pb from "google-protobuf/google/protobuf/struct_pb";

View File

@ -42,7 +42,24 @@ export class CoreClientProviderImpl implements CoreClientProvider {
async getClient(workspaceRootOrResourceUri?: string): Promise<Client | undefined> {
return this.clientRequestQueue.add(() => new Promise<Client | undefined>(async resolve => {
const roots = await this.workspaceServiceExt.roots();
let roots = undefined;
try {
roots = await this.workspaceServiceExt.roots();
} catch (e) {
if (e instanceof Error && e.message === 'Connection got disposed.') {
console.info('The frontend has already disconnected.');
// Ignore it for now: https://github.com/eclipse-theia/theia/issues/6499
// Client has disconnected, and the server still runs the serial board poll.
// The poll requires the client's workspace roots, but the client has disconnected :/
} else {
throw e;
}
}
if (!roots) {
resolve(undefined);
return
}
if (!workspaceRootOrResourceUri) {
resolve(this.getOrCreateClient(roots[0]));
return;

View File

@ -1,9 +1,20 @@
import { injectable, inject } from 'inversify';
import { Library, LibraryService } from '../common/protocol/library-service';
import { CoreClientProvider } from './core-client-provider';
import { LibrarySearchReq, LibrarySearchResp, LibraryListReq, LibraryListResp, LibraryRelease,
InstalledLibrary, LibraryInstallReq, LibraryInstallResp } from './cli-protocol/commands/lib_pb';
import {
LibrarySearchReq,
LibrarySearchResp,
LibraryListReq,
LibraryListResp,
LibraryRelease,
InstalledLibrary,
LibraryInstallReq,
LibraryInstallResp,
LibraryUninstallReq,
LibraryUninstallResp
} from './cli-protocol/commands/lib_pb';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { Installable } from '../common/protocol/installable';
@injectable()
export class LibraryServiceImpl implements LibraryService {
@ -43,6 +54,8 @@ export class LibraryServiceImpl implements LibraryService {
.filter(item => !!item.getLatest())
.slice(0, 50)
.map(item => {
// TODO: This seems to contain only the latest item instead of all of the items.
const availableVersions = item.getReleasesMap().getEntryList().map(([key, _]) => key).sort(Installable.Version.COMPARATOR);
let installedVersion: string | undefined;
const installed = installedLibsIdx.get(item.getName());
if (installed) {
@ -51,14 +64,16 @@ export class LibraryServiceImpl implements LibraryService {
return toLibrary({
name: item.getName(),
installable: true,
installedVersion
}, item.getLatest()!)
installedVersion,
}, item.getLatest()!, availableVersions)
})
return { items };
}
async install(library: Library): Promise<void> {
async install(options: { item: Library, version?: Installable.Version }): Promise<void> {
const library = options.item;
const version = !!options.version ? options.version : library.availableVersions[0];
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return;
@ -68,7 +83,7 @@ export class LibraryServiceImpl implements LibraryService {
const req = new LibraryInstallReq();
req.setInstance(instance);
req.setName(library.name);
req.setVersion(library.availableVersions[0]);
req.setVersion(version);
const resp = client.libraryInstall(req);
resp.on('data', (r: LibraryInstallResp) => {
@ -83,16 +98,43 @@ export class LibraryServiceImpl implements LibraryService {
});
}
async uninstall(options: { item: Library }): Promise<void> {
const library = options.item;
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return;
}
const { client, instance } = coreClient;
const req = new LibraryUninstallReq();
req.setInstance(instance);
req.setName(library.name);
req.setVersion(library.installedVersion!);
let logged = false;
const resp = client.libraryUninstall(req);
resp.on('data', (_: LibraryUninstallResp) => {
if (!logged) {
this.toolOutputService.publishNewOutput("library uninstall", `uninstalling ${library.name}:${library.installedVersion}%\n`)
logged = true;
}
});
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
}
}
function toLibrary(tpl: Partial<Library>, release: LibraryRelease): Library {
function toLibrary(tpl: Partial<Library>, release: LibraryRelease, availableVersions: string[]): Library {
return {
name: "",
installable: false,
...tpl,
author: release.getAuthor(),
availableVersions: [release.getVersion()],
availableVersions,
description: release.getSentence(),
moreInfoLink: release.getWebsite(),
summary: release.getParagraph()

View File

@ -52,14 +52,16 @@ export class SketchesServiceImpl implements SketchesService {
const uris: string[] = [];
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())
if (stats.isDirectory) {
if (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;

View File

@ -28,4 +28,4 @@
"files": [
"../node_modules/@theia/monaco/src/typings/monaco/index.d.ts"
]
}
}

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "browser-app",
"version": "0.0.2",
"version": "0.0.3",
"license": "MIT",
"dependencies": {
"@theia/core": "next",
@ -18,7 +18,7 @@
"@theia/terminal": "next",
"@theia/workspace": "next",
"@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.2"
"arduino-ide-extension": "0.0.3"
},
"devDependencies": {
"@theia/cli": "next"

View File

@ -1,50 +1,49 @@
{
"private": true,
"name": "electron-app",
"version": "0.0.2",
"license": "MIT",
"dependencies": {
"@theia/core": "next",
"@theia/cpp": "next",
"@theia/editor": "next",
"@theia/electron": "next",
"@theia/file-search": "next",
"@theia/filesystem": "next",
"@theia/languages": "next",
"@theia/messages": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
"@theia/preferences": "next",
"@theia/process": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.2"
},
"devDependencies": {
"@theia/cli": "next",
"electron": "^4.2.0"
},
"scripts": {
"prepare": "theia build --mode development",
"start": "theia start",
"watch": "theia build --watch --mode development"
},
"theia": {
"target": "electron",
"frontend": {
"config": {
"applicationName": "Arduino Pro IDE",
"defaultTheme": "arduino-theme",
"preferences": {
"editor.autoSave": "on"
}
}
},
"generator": {
"config": {
"preloadTemplate": "<div class='theia-preload' style='background-color: rgb(237, 241, 242);'></div>"
}
"private": true,
"name": "electron-app",
"version": "0.0.3",
"license": "MIT",
"dependencies": {
"@theia/core": "next",
"@theia/cpp": "next",
"@theia/editor": "next",
"@theia/electron": "next",
"@theia/file-search": "next",
"@theia/filesystem": "next",
"@theia/languages": "next",
"@theia/messages": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
"@theia/preferences": "next",
"@theia/process": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.3"
},
"devDependencies": {
"@theia/cli": "next"
},
"scripts": {
"prepare": "theia build --mode development",
"start": "theia start",
"watch": "theia build --watch --mode development"
},
"theia": {
"target": "electron",
"frontend": {
"config": {
"applicationName": "Arduino Pro IDE",
"defaultTheme": "arduino-theme",
"preferences": {
"editor.autoSave": "on"
}
}
},
"generator": {
"config": {
"preloadTemplate": "<div class='theia-preload' style='background-color: rgb(237, 241, 242);'></div>"
}
}
}
}

View File

@ -2,7 +2,7 @@
"name": "arduino.Pro.IDE",
"description": "Arduino Pro IDE",
"main": "src-gen/frontend/electron-main.js",
"author": "TypeFox",
"author": "Arduino SA",
"dependencies": {
"google-protobuf": "^3.5.0",
"arduino-ide-extension": "file:../working-copy/arduino-ide-extension"
@ -27,14 +27,12 @@
"url": "git+https://github.com/arduino/arduino-pro-ide.git"
},
"// Notes:": [
"The `electronVersion` version was pinned for `@grpc/grpc-js` -> Node.js version constraints.",
"`google-protobuf` was declared as it is not picked up by the `electron-builder` as a runtime dependency.",
"The resolution for `fs-extra` was required due to this: https://spectrum.chat/theia/general/our-theia-electron-builder-app-no-longer-starts~f5cf09a0-6d88-448b-8818-24ad0ec2ee7c"
],
"build": {
"productName": "Arduino Pro IDE",
"appId": "arduino.Pro.IDE",
"electronVersion": "4.2.0",
"asar": false,
"directories": {
"buildResources": "resources"
@ -46,7 +44,6 @@
"!node_modules/**/*.spec.js",
"!node_modules/@theia/**/test/*",
"!node_modules/@theia/**/src/*.ts",
"!node_modules/@theia/java/download",
"!node_modules/@theia/**/lib/*browser/*",
"!node_modules/@typefox/monaco-editor-core/*",
"!node_modules/oniguruma/*",

View File

@ -9,8 +9,8 @@
"cli": "./cli"
},
"keywords": [],
"author": "",
"license": "ISC",
"author": "Arduino SA",
"license": "MIT",
"dependencies": {
"depcheck": "^0.7.1",
"shelljs": "^0.8.3",

View File

@ -1,25 +1,25 @@
{
"name": "arduino-editor",
"version": "0.0.1",
"description": "Arduino IDE built using Eclipse Theia",
"version": "0.0.3",
"description": "Arduino Pro IDE",
"main": "index.js",
"repository": "https://github.com/bcmi-labs/arduino-editor.git",
"author": "Christian Weichel <christian.weichel@typefox.io>",
"author": "Arduino SA",
"license": "MIT",
"private": true,
"devDependencies": {
"lerna": "^3.13.3"
},
"scripts": {
"prepare": "lerna run prepare",
"rebuild:browser": "theia rebuild:browser",
"rebuild:electron": "theia rebuild:electron",
"start": "yarn --cwd ./browser-app start",
"watch": "lerna run watch --parallel"
"prepare": "lerna run prepare",
"rebuild:browser": "theia rebuild:browser",
"rebuild:electron": "theia rebuild:electron",
"start": "yarn --cwd ./browser-app start",
"watch": "lerna run watch --parallel"
},
"workspaces": [
"arduino-ide-extension",
"electron-app",
"browser-app"
"electron-app",
"browser-app"
]
}

3247
yarn.lock

File diff suppressed because it is too large Load Diff