Merge pull request #130 from bcmi-labs/cli-0.11.0-rc1-62-g72c9655f

0.0.7 RC build
This commit is contained in:
Akos Kitta 2020-08-20 16:26:13 +02:00 committed by GitHub
commit 0363041b39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1647 additions and 898 deletions

View File

@ -83,6 +83,17 @@ This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-e
git push origin 1.2.3
```
### FAQ
- Q: Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?
- A: Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior.
- Q: I have understood that not all versions of the CLI is compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?
- A: [Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at:
- Windows: `C:\path\to\Arduino Pro IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
- macOS: `/path/to/Arduino Pro IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
- Linux: `/path/to/Arduino Pro IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
### Architecture overview
The Pro IDE consists of three major parts:

View File

@ -10,7 +10,7 @@
(() => {
const DEFAULT_VERSION = '0.11.0'; // require('moment')().format('YYYYMMDD');
const DEFAULT_VERSION = '0.12.0-rc2'; // require('moment')().format('YYYYMMDD');
const path = require('path');
const shell = require('shelljs');

View File

@ -1,49 +1,50 @@
import * as React from 'react';
import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService, BoardsServiceClient, CoreService, SketchesService, ToolOutputServiceClient, Port } from '../common/protocol';
import { ArduinoCommands } from './arduino-commands';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR } from '@theia/core';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService } from '@theia/core';
import {
ContextMenuRenderer, StatusBar, StatusBarAlignment, FrontendApplicationContribution,
FrontendApplication, KeybindingContribution, KeybindingRegistry, OpenerService
ContextMenuRenderer,
FrontendApplication, FrontendApplicationContribution,
OpenerService, StatusBar, StatusBarAlignment
} from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import { EditorMainMenu, EditorManager } from '@theia/editor/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem } from '@theia/filesystem/lib/common';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { OutputContribution } from '@theia/output/lib/browser/output-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 { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react';
import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsService, BoardsServiceClient, CoreService, Port, SketchesService, ToolOutputServiceClient } from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service';
import { FileSystemExt } from '../common/protocol/filesystem-ext';
import { ArduinoCommands } from './arduino-commands';
import { BoardsConfig } from './boards/boards-config';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsDataStore } from './boards/boards-data-store';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { EditorMode } from './editor-mode';
import { ArduinoMenus } from './menu/arduino-menus';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { WorkspaceService } from './theia/workspace/workspace-service';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutputContribution } from '@theia/output/lib/browser/output-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 { EditorMode } from './editor-mode';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service';
import { BoardsDataStore } from './boards/boards-data-store';
import { MainMenuManager } from '../common/main-menu-manager';
import { FileSystemExt } from '../common/protocol/filesystem-ext';
import { ArduinoMenus } from './menu/arduino-menus';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
@injectable()
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
TabBarToolbarContribution, CommandContribution, MenuContribution, KeybindingContribution, ColorContribution {
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@ -255,17 +256,25 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
});
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.unregisterKeybinding('ctrlcmd+n'); // Unregister the keybinding for `New File`, will be used by `New Sketch`. (eclipse-theia/theia#8170)
protected async openSketchFiles(uri: string): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri);
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
for (const uri of [mainFileUri, ...otherSketchFileUris, ...additionalFileUris]) {
await this.ensureOpened(uri);
}
await this.ensureOpened(mainFileUri, true);
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
this.messageService.error(message);
}
}
protected async openSketchFiles(uri: string): Promise<void> {
const uris = await this.sketchService.getSketchFiles(uri);
for (const uri of uris) {
await this.editorManager.open(new URI(uri));
}
if (uris.length) {
await this.editorManager.open(new URI(uris[0])); // Make sure the sketch file has the focus.
protected async ensureOpened(uri: string, forceOpen: boolean = false): Promise<any> {
const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri));
}
}
@ -313,6 +322,24 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
hc: 'activityBar.inactiveForeground'
},
description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.'
},
{
id: 'arduino.output.foreground',
defaults: {
dark: 'editor.foreground',
light: 'editor.foreground',
hc: 'editor.foreground'
},
description: 'Color of the text in the Output view.'
},
{
id: 'arduino.output.background',
defaults: {
dark: 'editor.background',
light: 'editor.background',
hc: 'editor.background'
},
description: 'Background color of the Output view.'
}
);
}

View File

@ -3,7 +3,7 @@ import { ContainerModule } from 'inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { TabBarToolbarContribution, TabBarToolbarFactory } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { FrontendApplicationContribution, FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application'
import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate';
@ -15,6 +15,7 @@ import { ArduinoLanguageGrammarContribution } from './language/arduino-language-
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
import { CoreService, CoreServicePath, CoreServiceClient } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@ -38,7 +39,6 @@ import { MonacoStatusBarContribution } from './theia/monaco/monaco-status-bar-co
import {
ApplicationShell as TheiaApplicationShell,
ShellLayoutRestorer as TheiaShellLayoutRestorer,
KeybindingContribution,
CommonFrontendContribution as TheiaCommonFrontendContribution,
KeybindingRegistry as TheiaKeybindingRegistry
} from '@theia/core/lib/browser';
@ -86,7 +86,11 @@ import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
import { BoardsDataStore } from './boards/boards-data-store';
import { ILogger } from '@theia/core';
import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
import { WorkspaceFrontendContribution as TheiaWorkspaceFrontendContribution, FileMenuContribution as TheiaFileMenuContribution } from '@theia/workspace/lib/browser';
import {
WorkspaceFrontendContribution as TheiaWorkspaceFrontendContribution,
FileMenuContribution as TheiaFileMenuContribution,
WorkspaceCommandContribution as TheiaWorkspaceCommandContribution
} from '@theia/workspace/lib/browser';
import { WorkspaceFrontendContribution, ArduinoFileMenuContribution } from './theia/workspace/workspace-frontend-contribution';
import { Contribution } from './contributions/contribution';
import { NewSketch } from './contributions/new-sketch';
@ -99,12 +103,20 @@ import { UploadSketch } from './contributions/upload-sketch';
import { CommonFrontendContribution } from './theia/core/common-frontend-contribution';
import { EditContributions } from './contributions/edit-contributions';
import { OpenSketchExternal } from './contributions/open-sketch-external';
import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/preferences/lib/browser/preference-contribution';
import { PreferencesContribution } from './theia/preferences/preference-contribution';
import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/preferences/lib/browser/preferences-contribution';
import { PreferencesContribution } from './theia/preferences/preferences-contribution';
import { QuitApp } from './contributions/quit-app';
import { SketchControl } from './contributions/sketch-control';
import { Settings } from './contributions/settings';
import { KeybindingRegistry } from './theia/core/keybindings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
import { OutputWidget } from './theia/output/output-widget';
const ElementQueries = require('css-element-queries/src/ElementQueries');
@ -124,7 +136,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(CommandContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).toService(ArduinoFrontendContribution);
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
bind(KeybindingContribution).toService(ArduinoFrontendContribution);
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
bind(ColorContribution).toService(ArduinoFrontendContribution);
@ -151,6 +162,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
// Config service
bind(ConfigService).toDynamicValue(context => {
@ -281,6 +293,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaCommonFrontendContribution).to(CommonFrontendContribution).inSingletonScope();
rebind(TheiaPreferencesContribution).to(PreferencesContribution).inSingletonScope();
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
rebind(TheiaWorkspaceCommandContribution).to(WorkspaceCommandContribution).inSingletonScope();
rebind(TheiaWorkspaceDeleteHandler).to(WorkspaceDeleteHandler).inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(({ container: parentContainer }) => () => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
});
bind(OutputWidget).toSelf().inSingletonScope();
rebind(TheiaOutputWidget).toService(OutputWidget);
// Show a disconnected status bar, when the daemon is not available
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();

View File

@ -1,3 +1,4 @@
import * as PQueue from 'p-queue';
import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu';
@ -27,71 +28,74 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> {
await this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
this.boardsDataStore.onChanged(async () => await this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
this.boardsServiceClient.onBoardsConfigChanged(async ({ selectedBoard }) => await this.updateMenuActions(selectedBoard));
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
this.boardsDataStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.updateMenuActions(selectedBoard));
}
protected async updateMenuActions(selectedBoard: Board | undefined): Promise<void> {
if (selectedBoard) {
return this.queue.add(async () => {
this.toDisposeOnBoardChange.dispose();
this.mainMenuManager.update();
const { fqbn } = selectedBoard;
if (fqbn) {
const { configOptions, programmers, selectedProgrammer } = await this.boardsDataStore.getData(fqbn);
if (configOptions.length) {
const boardsConfigMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z01_boardsConfig']; // `z_` is for ordering.
for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) {
const menuPath = [...boardsConfigMenuPath, `${option}`];
const commands = new Map<string, Disposable & { label: string }>()
for (const value of values) {
const id = `${fqbn}-${option}--${value.value}`;
const command = { id };
const selectedValue = value.value;
const handler = {
execute: () => this.boardsDataStore.selectConfigOption({ fqbn, option, selectedValue }),
isToggled: () => value.selected
};
commands.set(id, Object.assign(this.commandRegistry.registerCommand(command, handler), { label: value.label }));
if (selectedBoard) {
const { fqbn } = selectedBoard;
if (fqbn) {
const { configOptions, programmers, selectedProgrammer } = await this.boardsDataStore.getData(fqbn);
if (configOptions.length) {
const boardsConfigMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z01_boardsConfig']; // `z_` is for ordering.
for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) {
const menuPath = [...boardsConfigMenuPath, `${option}`];
const commands = new Map<string, Disposable & { label: string }>()
for (const value of values) {
const id = `${fqbn}-${option}--${value.value}`;
const command = { id };
const selectedValue = value.value;
const handler = {
execute: () => this.boardsDataStore.selectConfigOption({ fqbn, option, selectedValue }),
isToggled: () => value.selected
};
commands.set(id, Object.assign(this.commandRegistry.registerCommand(command, handler), { label: value.label }));
}
this.menuRegistry.registerSubmenu(menuPath, label);
this.toDisposeOnBoardChange.pushAll([
...commands.values(),
Disposable.create(() => this.unregisterSubmenu(menuPath)), // We cannot dispose submenu entries: https://github.com/eclipse-theia/theia/issues/7299
...Array.from(commands.keys()).map((commandId, i) => {
const { label } = commands.get(commandId)!;
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: `${i}`, label });
return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId));
})
]);
}
this.menuRegistry.registerSubmenu(menuPath, label);
this.toDisposeOnBoardChange.pushAll([
...commands.values(),
Disposable.create(() => this.unregisterSubmenu(menuPath)), // We cannot dispose submenu entries: https://github.com/eclipse-theia/theia/issues/7299
...Array.from(commands.keys()).map((commandId, i) => {
const { label } = commands.get(commandId)!;
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: `${i}`, label });
return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId));
})
]);
}
}
if (programmers.length) {
const programmersMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z02_programmers'];
const label = selectedProgrammer ? `Programmer: "${selectedProgrammer.name}"` : 'Programmer'
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
this.toDisposeOnBoardChange.push(Disposable.create(() => this.unregisterSubmenu(programmersMenuPath)));
for (const programmer of programmers) {
const { id, name } = programmer;
const command = { id: `${fqbn}-programmer--${id}` };
const handler = {
execute: () => this.boardsDataStore.selectProgrammer({ fqbn, selectedProgrammer: programmer }),
isToggled: () => Programmer.equals(programmer, selectedProgrammer)
};
this.menuRegistry.registerMenuAction(programmersMenuPath, { commandId: command.id, label: name });
this.commandRegistry.registerCommand(command, handler);
this.toDisposeOnBoardChange.pushAll([
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command.id))
]);
if (programmers.length) {
const programmersMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z02_programmers'];
const label = selectedProgrammer ? `Programmer: "${selectedProgrammer.name}"` : 'Programmer'
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
this.toDisposeOnBoardChange.push(Disposable.create(() => this.unregisterSubmenu(programmersMenuPath)));
for (const programmer of programmers) {
const { id, name } = programmer;
const command = { id: `${fqbn}-programmer--${id}` };
const handler = {
execute: () => this.boardsDataStore.selectProgrammer({ fqbn, selectedProgrammer: programmer }),
isToggled: () => Programmer.equals(programmer, selectedProgrammer)
};
this.menuRegistry.registerMenuAction(programmersMenuPath, { commandId: command.id, label: name });
this.commandRegistry.registerCommand(command, handler);
this.toDisposeOnBoardChange.pushAll([
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command.id))
]);
}
}
this.mainMenuManager.update();
}
this.mainMenuManager.update();
}
}
});
}
protected unregisterSubmenu(menuPath: string[]): void {

View File

@ -15,12 +15,12 @@ export class CloseSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloseSketch.Commands.CLOSE_SKETCH, {
execute: async () => {
const sketch = await this.currentSketch();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.currentSketchFile();
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
@ -57,7 +57,7 @@ export class CloseSketch extends SketchContribution {
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: CloseSketch.Commands.CLOSE_SKETCH.id,
keybinding: 'CtrlCmd+W' // TODO: Windows binding?
keybinding: 'CtrlCmd+W'
});
}

View File

@ -1,7 +1,6 @@
import { inject, injectable, interfaces } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { notEmpty } from '@theia/core/lib/common/objects';
import { FileSystem } from '@theia/filesystem/lib/common';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { MessageService } from '@theia/core/lib/common/message-service';
@ -11,8 +10,9 @@ import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu
import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser/keybinding';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { Command, CommandRegistry, CommandContribution, CommandService } from '@theia/core/lib/common/command';
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
import { EditorMode } from '../editor-mode';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
@ -69,30 +69,8 @@ export abstract class SketchContribution extends Contribution {
@inject(OpenerService)
protected readonly openerService: OpenerService;
protected async currentSketch(): Promise<Sketch | undefined> {
const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ uri }) => this.sketchService.getSketchFolder(uri)))).filter(notEmpty);
if (!sketches.length) {
return;
}
if (sketches.length > 1) {
console.log(`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(sketches)}`);
}
return sketches[0];
}
protected async currentSketchFile(): Promise<string | undefined> {
const sketch = await this.currentSketch();
if (sketch) {
const uri = new URI(sketch.uri).resolve(`${sketch.name}.ino`).toString();
const exists = await this.fileSystem.exists(uri);
if (!exists) {
this.messageService.warn(`Could not find sketch file: ${uri}`);
return undefined;
}
return uri;
}
return undefined;
}
@inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl;
}

View File

@ -151,33 +151,29 @@ ${value}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C'
keybinding: 'CtrlCmd+Shift+C',
when: 'editorFocus'
});
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
keybinding: 'CtrlCmd+Alt+C'
keybinding: 'CtrlCmd+Alt+C',
when: 'editorFocus'
});
registry.registerKeybinding({
command: EditContributions.Commands.GO_TO_LINE.id,
keybinding: 'CtrlCmd+L'
keybinding: 'CtrlCmd+L',
when: 'editorFocus'
});
registry.registerKeybinding({
command: EditContributions.Commands.TOGGLE_COMMENT.id,
keybinding: 'CtrlCmd+/'
});
registry.registerKeybinding({
command: EditContributions.Commands.INDENT_LINES.id,
keybinding: 'Tab'
});
registry.registerKeybinding({
command: EditContributions.Commands.OUTDENT_LINES.id,
keybinding: 'Shift+Tab'
keybinding: 'CtrlCmd+/',
when: 'editorFocus'
});
registry.registerKeybinding({
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=' // TODO: compare with the Java IDE. It uses `⌘+`. There is no `+` on EN_US.
keybinding: 'CtrlCmd+='
});
registry.registerKeybinding({
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
@ -213,8 +209,15 @@ ${value}
}
protected async currentValue(): Promise<string | undefined> {
const currentEditor = await this.current()
return currentEditor?.getValue();
const currentEditor = await this.current();
if (currentEditor) {
const selection = currentEditor.getSelection();
if (!selection || selection.isEmpty()) {
return currentEditor.getValue();
}
return currentEditor.getModel()?.getValueInRange(selection);
}
return undefined;
}
protected async run(commandId: string): Promise<any> {

View File

@ -28,7 +28,7 @@ export class OpenSketchExternal extends SketchContribution {
}
protected async openExternal(): Promise<void> {
const uri = await this.currentSketchFile();
const uri = await this.sketchServiceClient.currentSketchFile();
if (uri) {
const exists = this.fileSystem.exists(uri);
if (exists) {

View File

@ -116,10 +116,37 @@ export class OpenSketch extends SketchContribution {
if (filePaths.length > 1) {
this.logger.warn(`Multiple sketches were selected: ${filePaths}. Using the first one.`);
}
// TODO: validate sketch file name against the sketch folder. Move the file if required.
const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
return this.sketchService.getSketchFolder(sketchFileUri);
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
if (sketch) {
return sketch;
}
if (sketchFileUri.endsWith('.ino')) {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
const { response } = await remote.dialog.showMessageBox({
title: 'Moving',
type: 'question',
buttons: ['Cancel', 'OK'],
message: `The file "${nameWithExt}" needs to be inside a sketch folder named as "${name}".\nCreate this folder, move the file, and continue?`
});
if (response === 1) { // OK
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
const exists = await this.fileSystem.exists(newSketchUri.toString());
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: 'Error',
message: `A folder named "${name}" already exists. Can't open sketch.`
});
return undefined;
}
await this.fileSystem.createFolder(newSketchUri.toString());
await this.fileSystem.move(sketchFileUri, newSketchUri.resolve(nameWithExt).toString());
return this.sketchService.getSketchFolder(newSketchUri.toString());
}
}
}
}

View File

@ -1,6 +1,6 @@
import { injectable } from 'inversify';
import { remote } from 'electron';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { isOSX } from '@theia/core/lib/common/os';
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
@ -16,6 +16,7 @@ export class QuitApp extends Contribution {
}
registerMenus(registry: MenuModelRegistry): void {
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
if (!isOSX) {
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
commandId: QuitApp.Commands.QUIT_APP.id,
@ -29,7 +30,7 @@ export class QuitApp extends Contribution {
if (!isOSX) {
registry.registerKeybinding({
command: QuitApp.Commands.QUIT_APP.id,
keybinding: isWindows ? 'Alt+F4' : 'Ctrl+Q'
keybinding: 'CtrlCmd+Q'
});
}
}

View File

@ -31,8 +31,8 @@ export class SaveAsSketch extends SketchContribution {
/**
* Resolves `true` if the sketch was successfully saved as something.
*/
async saveAs({ execOnlyIfTemp, openAfterMove }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT): Promise<boolean> {
const sketch = await this.currentSketch();
async saveAs({ execOnlyIfTemp, openAfterMove, wipeOriginal }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return false;
}
@ -60,7 +60,10 @@ export class SaveAsSketch extends SketchContribution {
}
const workspaceUri = await this.sketchService.copy(sketch, { destinationUri });
if (workspaceUri && openAfterMove) {
this.workspaceService.open(new URI(workspaceUri));
if (wipeOriginal) {
await this.fileSystem.delete(sketch.uri);
}
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
}
return !!workspaceUri;
}
@ -76,11 +79,16 @@ export namespace SaveAsSketch {
export interface Options {
readonly execOnlyIfTemp?: boolean;
readonly openAfterMove?: boolean;
/**
* Ignored if `openAfterMove` is `false`.
*/
readonly wipeOriginal?: boolean;
}
export namespace Options {
export const DEFAULT: Options = {
execOnlyIfTemp: false,
openAfterMove: true
openAfterMove: true,
wipeOriginal: false
};
}
}

View File

@ -26,7 +26,7 @@ export class SketchControl extends SketchContribution {
isVisible: widget => this.shell.getWidgets('main').indexOf(widget) !== -1,
execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose();
const sketch = await this.currentSketch();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
@ -40,8 +40,8 @@ export class SketchControl extends SketchContribution {
return;
}
const uris = await this.sketchService.getSketchFiles(sketch.uri);
// TODO: order them! The Java IDE orders them by tab index. Use the shell and the editor manager to achieve it.
const { mainFileUri, otherSketchFileUris, additionalFileUris } = await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]);
const command = { id: `arduino-focus-file--${uri.toString()}` };
@ -78,7 +78,7 @@ export class SketchControl extends SketchContribution {
order: '1'
});
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
commandId: WorkspaceCommands.FILE_DELETE.id,
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: 'Delete',
order: '2'
});

View File

@ -73,7 +73,7 @@ export class UploadSketch extends SketchContribution {
}
async uploadSketch(usingProgrammer: boolean = false): Promise<void> {
const uri = await this.currentSketchFile();
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
@ -86,29 +86,45 @@ export class UploadSketch extends SketchContribution {
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
const { selectedPort } = boardsConfig;
if (!selectedPort) {
throw new Error('No ports selected. Please select a port.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
}
const [fqbn, data] = await Promise.all([
const [fqbn, { selectedProgrammer }] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn),
this.boardsDataStore.getData(boardsConfig.selectedBoard.fqbn)
]);
this.outputChannelManager.getChannel('Arduino: upload').clear();
const programmer = usingProgrammer ? data.selectedProgrammer : undefined;
if (usingProgrammer && !programmer) {
this.messageService.warn('Programmer is not selected. Uploading without programmer.', { timeout: 2000 });
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = uri;
const optimizeForDebug = this.editorMode.compileForDebug;
if (usingProgrammer) {
const programmer = selectedProgrammer;
if (!programmer) {
throw new Error('Programmer is not selected. Please select a programmer.');
}
options = {
sketchUri,
fqbn,
optimizeForDebug,
programmer
};
} else {
const { selectedPort } = boardsConfig;
if (!selectedPort) {
throw new Error('No ports selected. Please select a port.');
}
const port = selectedPort.address;
options = {
sketchUri,
fqbn,
optimizeForDebug,
port
};
}
await this.coreService.upload({
sketchUri: uri,
fqbn,
port: selectedPort.address,
optimizeForDebug: this.editorMode.compileForDebug,
programmer
});
this.outputChannelManager.getChannel('Arduino: upload').clear();
await this.coreService.upload(options);
this.messageService.info('Done uploading.', { timeout: 1000 });
} catch (e) {
this.messageService.error(e.toString());

View File

@ -57,7 +57,7 @@ export class VerifySketch extends SketchContribution {
}
async verifySketch(): Promise<void> {
const uri = await this.currentSketchFile();
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
@ -69,16 +69,12 @@ export class VerifySketch extends SketchContribution {
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
}
const [data, fqbn] = await Promise.all([
this.boardsDataStore.getData(boardsConfig.selectedBoard.fqbn),
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn)
]);
const fqbn = await this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
this.outputChannelManager.getChannel('Arduino: compile').clear();
await this.coreService.compile({
sketchUri: uri,
fqbn,
optimizeForDebug: this.editorMode.compileForDebug,
programmer: data.selectedProgrammer
optimizeForDebug: this.editorMode.compileForDebug
});
this.messageService.info('Done compiling.', { timeout: 1000 });
} catch (e) {

View File

@ -108,7 +108,9 @@
"secondaryButton.hoverBackground": "#dae3e3",
"arduino.branding.primary": "#00979d",
"arduino.branding.secondary": "#b5c8c9",
"arduino.foreground": "#edf1f1"
"arduino.foreground": "#edf1f1",
"arduino.output.foreground": "#FFFFFF",
"arduino.output.background": "#000000"
},
"type": "light",
"name": "Arduino"

View File

@ -1,4 +1,5 @@
import { injectable, inject, postConstruct } from 'inversify';
import { deepClone } from '@theia/core/lib/common/objects';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@ -182,15 +183,19 @@ export class MonitorConnection {
}
async disconnect(): Promise<Status> {
if (!this.state) { // XXX: we user `this.state` instead of `this.connected` to make the type checker happy.
if (!this.connected) {
return Status.OK;
}
const stateCopy = deepClone(this.state);
if (!stateCopy) {
return Status.OK;
}
console.log('>>> Disposing existing monitor connection...');
const status = await this.monitorService.disconnect();
if (Status.isOK(status)) {
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(this.state)}`);
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(stateCopy)}`);
} else {
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`);
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(stateCopy)}`);
}
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);

View File

@ -87,25 +87,23 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
}
});
if (this.toggleCommand) {
commands.registerCommand(this.toggleCommand, {
execute: () => this.openView({
toggle: true,
activate: true
})
});
const toolbarCmd = {
id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR
}
commands.registerCommand(toolbarCmd, {
commands.registerCommand(this.toggleCommand, { execute: () => this.toggle() });
commands.registerCommand({ id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR }, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
execute: () => this.openView({
toggle: true,
activate: true
})
execute: () => this.toggle()
});
}
}
protected async toggle(): Promise<void> {
const widget = this.tryGetWidget();
if (widget) {
widget.dispose();
} else {
await this.openView({ activate: true, reveal: true });
}
}
protected renderAutoScrollButton(): React.ReactNode {
return <React.Fragment key='autoscroll-toolbar-item'>
<div

View File

@ -132,10 +132,6 @@
box-sizing: border-box;
}
.theia-output .monaco-editor .margin {
border-right: none;
}
.noWrapInfo {
white-space: nowrap;
overflow: hidden;
@ -150,8 +146,32 @@
background-color: var(--theia-arduino-foreground);
}
#arduino-open-sketch-control--toolbar {
background-color: var(--theia-tab-inactiveBackground);
border: 1px solid var(--theia-arduino-toolbar-background);
padding: 2px 0px 2px 9px;
#arduino-open-sketch-control--toolbar--container {
background-color: var(--theia-arduino-toolbar-background);
padding: 8px 8px 8px 8px; /* based on pure heuristics */
}
#arduino-open-sketch-control--toolbar {
height: unset;
width: unset;
line-height: unset;
color: var(--theia-titleBar-activeBackground);
}
/* Output */
.theia-output .editor-container {
background-color: var(--theia-arduino-output-background);
}
.theia-output .monaco-editor .lines-content.monaco-editor-background {
background-color: var(--theia-arduino-output-background);
}
.theia-output .monaco-editor .lines-content.monaco-editor-background .view-lines .view-line .mtk1:not(.theia-output-error):not(.theia-output-warning) {
color: var(--theia-arduino-output-foreground);
}
.theia-output .monaco-editor .margin {
border-right: none;
background-color: var(--theia-arduino-output-background);
}

View File

@ -2,10 +2,11 @@
import { injectable, inject } from 'inversify';
import { EditorWidget } from '@theia/editor/lib/browser';
import { CommandService } from '@theia/core/lib/common/command';
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
import { ApplicationShell as TheiaApplicationShell, Widget } from '@theia/core/lib/browser';
import { Sketch } from '../../../common/protocol';
import { EditorMode } from '../../editor-mode';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
@injectable()
export class ApplicationShell extends TheiaApplicationShell {
@ -16,29 +17,48 @@ export class ApplicationShell extends TheiaApplicationShell {
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
protected track(widget: Widget): void {
super.track(widget);
if (!this.editorMode.proMode) {
if (widget instanceof EditorWidget) {
// Always allow closing the whitelisted files.
// TODO: It would be better to blacklist the sketch files only.
if (['tasks.json',
'launch.json',
'settings.json',
'arduino-cli.yaml'].some(fileName => widget.editor.uri.toString().endsWith(fileName))) {
return;
if (!this.editorMode.proMode && widget instanceof EditorWidget) {
// Make the editor un-closeable asynchronously.
this.sketchesServiceClient.currentSketch().then(sketch => {
if (sketch) {
if (Sketch.isInSketch(widget.editor.uri, sketch)) {
widget.title.closable = false;
}
}
});
}
}
async addWidget(widget: Widget, options: Readonly<TheiaApplicationShell.WidgetOptions> = {}): Promise<void> {
// By default, Theia open a widget **next** to the currently active in the target area.
// Instead of this logic, we want to open the new widget after the last of the target area.
if (!widget.id) {
console.error('Widgets added to the application shell must have a unique id property.');
return;
}
let ref: Widget | undefined = options.ref;
let area: TheiaApplicationShell.Area = options.area || 'main';
if (!ref && (area === 'main' || area === 'bottom')) {
const tabBar = this.getTabBarFor(area);
if (tabBar) {
const last = tabBar.titles[tabBar.titles.length - 1];
if (last) {
ref = last.owner;
}
}
if (widget instanceof PreferencesWidget) {
return;
}
widget.title.closable = false;
}
return super.addWidget(widget, { ...options, ref });
}
async saveAll(): Promise<void> {
await super.saveAll();
await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: true, openAfterMove: true });
const options = { execOnlyIfTemp: true, openAfterMove: true };
await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, options);
}
}

View File

@ -19,8 +19,7 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
CommonCommands.AUTO_SAVE,
CommonCommands.OPEN_PREFERENCES,
CommonCommands.SELECT_ICON_THEME,
CommonCommands.SELECT_COLOR_THEME,
CommonCommands.OPEN_PREFERENCES
CommonCommands.SELECT_COLOR_THEME
]) {
registry.unregisterMenuAction(command);
}

View File

@ -0,0 +1,38 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { LabelIcon } from '@theia/core/lib/browser/label-parser';
import { TabBarToolbar as TheiaTabBarToolbar, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
@injectable()
export class TabBarToolbar extends TheiaTabBarToolbar {
/**
* Copied over from Theia. Added an ID to the parent of the toolbar item (`--container`).
* CSS3 does not support parent selectors but we want to style the parent of the toolbar item.
*/
protected renderItem(item: TabBarToolbarItem): React.ReactNode {
let innerText = '';
const classNames = [];
if (item.text) {
for (const labelPart of this.labelParser.parse(item.text)) {
if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
const className = `fa fa-${labelPart.name}${labelPart.animation ? ' fa-' + labelPart.animation : ''}`;
classNames.push(...className.split(' '));
} else {
innerText = labelPart;
}
}
}
const command = this.commands.getCommand(item.command);
const iconClass = (typeof item.icon === 'function' && item.icon()) || item.icon || (command && command.iconClass);
if (iconClass) {
classNames.push(iconClass);
}
const tooltip = item.tooltip || (command && command.label);
return <div id={`${item.id}--container`} key={item.id} className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM}${command && this.commandIsEnabled(command.id) ? ' enabled' : ''}`}
onMouseDown={this.onMouseDownEvent} onMouseUp={this.onMouseUpEvent} onMouseOut={this.onMouseUpEvent} >
<div id={item.id} className={classNames.join(' ')} onClick={this.executeCommand} title={tooltip}>{innerText}</div>
</div>;
}
}

View File

@ -0,0 +1,38 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser';
import { LabelProvider } from '@theia/core/lib/browser';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { SketchesService, Sketch } from '../../../common/protocol';
@injectable()
export class EditorWidgetFactory extends TheiaEditorWidgetFactory {
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
protected async createEditor(uri: URI): Promise<EditorWidget> {
const widget = await super.createEditor(uri);
return this.maybeUpdateCaption(widget);
}
protected async maybeUpdateCaption(widget: EditorWidget): Promise<EditorWidget> {
const sketch = await this.sketchesServiceClient.currentSketch();
const { uri } = widget.editor;
if (sketch && Sketch.isInSketch(uri, sketch)) {
const isTemp = await this.sketchesService.isTemp(sketch);
if (isTemp) {
widget.title.caption = `Unsaved ${this.labelProvider.getName(uri)}`;
}
}
return widget;
}
}

View File

@ -0,0 +1,15 @@
import { injectable } from 'inversify';
import { Message, Widget } from '@theia/core/lib/browser';
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
// Patched after https://github.com/eclipse-theia/theia/issues/8361
// Remove this module after ATL-222 and the Theia update.
@injectable()
export class OutputWidget extends TheiaOutputWidget {
protected onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.onResize(Widget.ResizeMessage.UnknownSize);
}
}

View File

@ -1,19 +1,17 @@
import { injectable } from 'inversify';
import { isOSX } from '@theia/core/lib/common/os';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser';
import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/preferences/lib/browser/preference-contribution';
import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/preferences/lib/browser/preferences-contribution';
@injectable()
export class PreferencesContribution extends TheiaPreferencesContribution {
registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
if (isOSX) {
// The settings group: preferences, CLI config is not part of the `File` menu on macOS.
registry.unregisterMenuAction(CommonCommands.OPEN_PREFERENCES.id, CommonMenus.FILE_SETTINGS_SUBMENU_OPEN);
}
// The settings group: preferences, CLI config is not part of the `File` menu on macOS.
// On Windows and Linux, we rebind it to `Preferences...`. It is safe to remove here.
registry.unregisterMenuAction(CommonCommands.OPEN_PREFERENCES.id, CommonMenus.FILE_SETTINGS_SUBMENU_OPEN);
}
registerKeybindings(registry: KeybindingRegistry): void {

View File

@ -0,0 +1,139 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { open } from '@theia/core/lib/browser/opener-service';
import { FileStat } from '@theia/filesystem/lib/common';
import { CommandRegistry, CommandService } from '@theia/core/lib/common/command';
import { WorkspaceCommandContribution as TheiaWorkspaceCommandContribution, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { Sketch } from '../../../common/protocol';
import { WorkspaceInputDialog } from './workspace-input-dialog';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SingleTextInputDialog } from '@theia/core/lib/browser';
@injectable()
export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribution {
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(CommandService)
protected readonly commandService: CommandService;
registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.unregisterCommand(WorkspaceCommands.NEW_FILE);
registry.registerCommand(WorkspaceCommands.NEW_FILE, this.newWorkspaceRootUriAwareCommandHandler({
execute: uri => this.newFile(uri)
}));
registry.unregisterCommand(WorkspaceCommands.FILE_RENAME);
registry.registerCommand(WorkspaceCommands.FILE_RENAME, this.newUriAwareCommandHandler({
execute: uri => this.renameFile(uri)
}));
}
protected async newFile(uri: URI | undefined): Promise<void> {
if (!uri) {
return;
}
const parent = await this.getDirectory(uri);
if (!parent) {
return;
}
const parentUri = new URI(parent.uri);
const dialog = new WorkspaceInputDialog({
title: 'Name for new file',
parentUri,
validate: name => this.validateFileName(name, parent, true)
}, this.labelProvider);
const name = await dialog.open();
const nameWithExt = this.maybeAppendInoExt(name);
if (nameWithExt) {
const fileUri = parentUri.resolve(nameWithExt);
await this.fileSystem.createFile(fileUri.toString());
this.fireCreateNewFile({ parent: parentUri, uri: fileUri });
open(this.openerService, fileUri);
}
}
protected async validateFileName(name: string, parent: FileStat, recursive: boolean = false): Promise<string> {
// In the Java IDE the followings are the rules:
// - `name` without an extension should default to `name.ino`.
// - `name` with a single trailing `.` also defaults to `name.ino`.
const nameWithExt = this.maybeAppendInoExt(name);
const errorMessage = await super.validateFileName(nameWithExt, parent, recursive);
if (errorMessage) {
return errorMessage;
}
const extension = nameWithExt.split('.').pop();
if (!extension) {
return 'Invalid filename.'; // XXX: this should not happen as we forcefully append `.ino` if it's not there.
}
if (Sketch.Extensions.ALL.indexOf(`.${extension}`) === -1) {
return `.${extension} is not a valid extension.`;
}
return '';
}
protected maybeAppendInoExt(name: string | undefined): string {
if (!name) {
return '';
}
if (name.trim().length) {
if (name.indexOf('.') === -1) {
return `${name}.ino`
}
if (name.lastIndexOf('.') === name.length - 1) {
return `${name.slice(0, -1)}.ino`
}
}
return name;
}
protected async renameFile(uri: URI | undefined): Promise<void> {
if (!uri) {
return;
}
const sketch = await this.sketchesServiceClient.currentSketch();
if (!sketch) {
return;
}
if (uri.toString() === sketch.mainFileUri) {
const options = {
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: true
};
await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, options);
return;
}
const parent = await this.getParent(uri);
if (!parent) {
return;
}
const initialValue = uri.path.base;
const dialog = new SingleTextInputDialog({
title: 'New name for file',
initialValue,
initialSelectionRange: {
start: 0,
end: uri.path.name.length
},
validate: (name, mode) => {
if (initialValue === name && mode === 'preview') {
return false;
}
return this.validateFileName(name, parent, false);
}
});
const newName = await dialog.open();
const newNameWithExt = this.maybeAppendInoExt(newName);
if (newNameWithExt) {
const oldUri = uri;
const newUri = uri.parent.resolve(newNameWithExt);
this.fileSystem.move(oldUri.toString(), newUri.toString());
}
}
}

View File

@ -0,0 +1,36 @@
import { inject, injectable } from 'inversify';
import { remote } from 'electron';
import URI from '@theia/core/lib/common/uri';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
@injectable()
export class WorkspaceDeleteHandler extends TheiaWorkspaceDeleteHandler {
@inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
async execute(uris: URI[]): Promise<void> {
const sketch = await this.sketchesServiceClient.currentSketch();
if (!sketch) {
return;
}
// Deleting the main sketch file.
if (uris.map(uri => uri.toString()).some(uri => uri === sketch.mainFileUri)) {
const { response } = await remote.dialog.showMessageBox({
title: 'Delete',
type: 'question',
buttons: ['Cancel', 'OK'],
message: 'Do you want to delete the current sketch?'
});
if (response === 1) { // OK
await Promise.all([...sketch.additionalFileUris, ...sketch.otherSketchFileUris, sketch.mainFileUri].map(uri => this.closeWithoutSaving(new URI(uri))));
await this.fileSystem.delete(sketch.uri);
window.close();
}
return;
}
return super.execute(uris);
}
}

View File

@ -0,0 +1,39 @@
import { inject } from 'inversify';
import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { DialogError, DialogMode } from '@theia/core/lib/browser/dialogs';
import { WorkspaceInputDialog as TheiaWorkspaceInputDialog, WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
protected wasTouched = false;
constructor(
@inject(WorkspaceInputDialogProps) protected readonly props: WorkspaceInputDialogProps,
@inject(LabelProvider) protected readonly labelProvider: LabelProvider,
) {
super(props, labelProvider);
this.appendCloseButton('Cancel');
}
protected appendParentPath(): void {
// NOOP
}
isValid(value: string, mode: DialogMode): MaybePromise<DialogError> {
if (value !== '') {
this.wasTouched = true;
}
return super.isValid(value, mode);
}
protected setErrorMessage(error: DialogError): void {
if (this.acceptButton) {
this.acceptButton.disabled = !DialogError.getResult(error);
}
if (this.wasTouched) {
this.errorMessageNode.innerText = DialogError.getMessage(error);
}
}
}

View File

@ -7,7 +7,6 @@ import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FocusTracker, Widget } from '@theia/core/lib/browser';
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { EditorMode } from '../../editor-mode';
import { ConfigService } from '../../../common/protocol/config-service';
import { SketchesService } from '../../../common/protocol/sketches-service';
import { ArduinoWorkspaceRootResolver } from '../../arduino-workspace-resolver';
@ -24,9 +23,6 @@ export class WorkspaceService extends TheiaWorkspaceService {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(MessageService)
protected readonly messageService: MessageService;
@ -82,13 +78,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
if (!exists) {
return false;
}
// 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 (this.editorMode.proMode) {
return true;
}
const sketchFolder = await this.sketchService.isSketchFolder(uri);
return sketchFolder;
return this.sketchService.isSketchFolder(uri);
}
protected onCurrentWidgetChange({ newValue }: FocusTracker.IChangedArgs<Widget>): void {

View File

@ -1,23 +1,41 @@
import { ToolOutputServiceClient } from '../../common/protocol/tool-output-service';
import { injectable, inject } from 'inversify';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager, OutputChannelSeverity } from '@theia/output/lib/common/output-channel';
import { ToolOutputServiceClient, ToolOutputMessage } from '../../common/protocol/tool-output-service';
@injectable()
export class ToolOutputServiceClientImpl implements ToolOutputServiceClient {
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
@inject(OutputContribution)
protected readonly outputContribution: OutputContribution;
protected outputContribution: OutputContribution;
onNewOutput(tool: string, chunk: string): void {
this.outputContribution.openView().then(() => {
const channel = this.outputChannelManager.getChannel(`Arduino: ${tool}`);
channel.show();
channel.append(chunk);
});
@inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager;
onMessageReceived(message: ToolOutputMessage): void {
const { tool, chunk } = message;
const name = `Arduino: ${tool}`;
const channel = this.outputChannelManager.getChannel(name);
// Zen-mode: we do not reveal the output for daemon messages.
const show: Promise<any> = tool === 'daemon'
// This will open and reveal the view but won't show it. You will see the toggle bottom panel on the status bar
? this.outputContribution.openView({ activate: false, reveal: false })
// This will open, reveal but do not activate the Output view.
: Promise.resolve(channel.show({ preserveFocus: true }));
show.then(() => channel.append(chunk, this.toOutputSeverity(message)));
}
private toOutputSeverity(message: ToolOutputMessage): OutputChannelSeverity {
if (message.severity) {
switch (message.severity) {
case 'error': return OutputChannelSeverity.Error
case 'warning': return OutputChannelSeverity.Warning
case 'info': return OutputChannelSeverity.Info
default: return OutputChannelSeverity.Info
}
}
return OutputChannelSeverity.Info
}
}

View File

@ -20,14 +20,13 @@ export namespace CoreService {
readonly sketchUri: string;
readonly fqbn: string;
readonly optimizeForDebug: boolean;
readonly programmer?: Programmer | undefined;
}
}
export namespace Upload {
export interface Options extends Compile.Options {
readonly port: string;
}
export type Options =
Compile.Options & Readonly<{ port: string }> |
Compile.Options & Readonly<{ programmer: Programmer }>;
}
}

View File

@ -0,0 +1,48 @@
import { inject, injectable } from 'inversify';
import { notEmpty } from '@theia/core/lib/common/objects';
import { FileSystem } from '@theia/filesystem/lib/common';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { Sketch, SketchesService } from '../../common/protocol';
@injectable()
export class SketchesServiceClientImpl {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
async currentSketch(): Promise<Sketch | undefined> {
const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ uri }) => this.sketchService.getSketchFolder(uri)))).filter(notEmpty);
if (!sketches.length) {
return undefined;
}
if (sketches.length > 1) {
console.log(`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(sketches)}`);
}
return sketches[0];
}
async currentSketchFile(): Promise<string | undefined> {
const sketch = await this.currentSketch();
if (sketch) {
const uri = sketch.mainFileUri;
const exists = await this.fileSystem.exists(uri);
if (!exists) {
this.messageService.warn(`Could not find sketch file: ${uri}`);
return undefined;
}
return uri;
}
return undefined;
}
}

View File

@ -1,3 +1,5 @@
import URI from '@theia/core/lib/common/uri';
export const SketchesServicePath = '/services/sketches-service';
export const SketchesService = Symbol('SketchesService');
export interface SketchesService {
@ -8,7 +10,12 @@ export interface SketchesService {
*/
getSketches(uri?: string): Promise<Sketch[]>;
getSketchFiles(uri: string): Promise<string[]>;
/**
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
* See: https://github.com/arduino/arduino-cli/issues/837
* Based on: https://github.com/arduino/arduino-cli/blob/eef3705c4afcba4317ec38b803d9ffce5dd59a28/arduino/builder/sketch.go#L100-L215
*/
loadSketch(uri: string): Promise<Sketch>;
/**
* Creates a new sketch folder in the temp location.
@ -40,10 +47,24 @@ export interface SketchesService {
export interface Sketch {
readonly name: string;
readonly uri: string;
readonly uri: string; // `LocationPath`
readonly mainFileUri: string; // `MainFile`
readonly otherSketchFileUris: string[]; // `OtherSketchFiles`
readonly additionalFileUris: string[]; // `AdditionalFiles`
}
export namespace Sketch {
export function is(arg: any): arg is Sketch {
return !!arg && 'name' in arg && 'uri' in arg && typeof arg.name === 'string' && typeof arg.uri === 'string';
}
export namespace Extensions {
export const MAIN = ['.ino', '.pde'];
export const SOURCE = ['.c', '.cpp', '.s'];
export const ADDITIONAL = ['.h', '.c', '.hpp', '.hh', '.cpp', '.s'];
export const ALL = Array.from(new Set([...MAIN, ...SOURCE, ...ADDITIONAL]));
}
export function isInSketch(uri: string | URI, sketch: Sketch): boolean {
const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch;
return [mainFileUri, ...otherSketchFileUris, ...additionalFileUris].indexOf(uri.toString()) !== -1;
}
}

View File

@ -1,16 +1,22 @@
import { JsonRpcServer } from "@theia/core";
import { JsonRpcServer } from '@theia/core';
export const ToolOutputServiceServer = Symbol("ToolOutputServiceServer");
export interface ToolOutputMessage {
readonly tool: string;
readonly chunk: string;
readonly severity?: 'error' | 'warning' | 'info';
}
export const ToolOutputServiceServer = Symbol('ToolOutputServiceServer');
export interface ToolOutputServiceServer extends JsonRpcServer<ToolOutputServiceClient> {
publishNewOutput(tool: string, chunk: string): void;
append(message: ToolOutputMessage): void;
disposeClient(client: ToolOutputServiceClient): void;
}
export const ToolOutputServiceClient = Symbol("ToolOutputServiceClient");
export const ToolOutputServiceClient = Symbol('ToolOutputServiceClient');
export interface ToolOutputServiceClient {
onNewOutput(tool: string, chunk: string): void;
onMessageReceived(message: ToolOutputMessage): void;
}
export namespace ToolOutputService {
export const SERVICE_PATH = "/tool-output-service";
export const SERVICE_PATH = '/tool-output-service';
}

View File

@ -3,3 +3,7 @@ export const naturalCompare: (left: string, right: string) => number = require('
export function notEmpty(arg: string | undefined | null): arg is string {
return !!arg;
}
export function firstToLowerCase(what: string): string {
return what.charAt(0).toLowerCase() + what.slice(1);
}

View File

@ -133,7 +133,7 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
if (this._execPath) {
return this._execPath;
}
this._execPath = await getExecPath('arduino-cli', this.onError.bind(this), 'version');
this._execPath = await getExecPath('arduino-cli', this.onError.bind(this));
return this._execPath;
}
@ -218,7 +218,7 @@ export class ArduinoDaemonImpl implements ArduinoDaemon, BackendApplicationContr
protected onData(message: string, options: { useOutput: boolean } = { useOutput: true }): void {
if (options.useOutput) {
this.toolOutputService.publishNewOutput('daemon', DaemonLog.toPrettyString(message));
this.toolOutputService.append({ tool: 'daemon', chunk: DaemonLog.toPrettyString(message) });
}
DaemonLog.log(this.logger, message);
}

View File

@ -167,13 +167,13 @@ export class BoardsServiceImpl implements BoardsService {
// The `BoardListResp` looks like this for a known attached board:
// [
// {
// "address": "COM10",
// "protocol": "serial",
// "protocol_label": "Serial Port (USB)",
// "boards": [
// 'address': 'COM10',
// 'protocol': 'serial',
// 'protocol_label': 'Serial Port (USB)',
// 'boards': [
// {
// "name": "Arduino MKR1000",
// "FQBN": "arduino:samd:mkr1000"
// 'name': 'Arduino MKR1000',
// 'FQBN': 'arduino:samd:mkr1000'
// }
// ]
// }
@ -181,9 +181,9 @@ export class BoardsServiceImpl implements BoardsService {
// And the `BoardListResp` looks like this for an unknown board:
// [
// {
// "address": "COM9",
// "protocol": "serial",
// "protocol_label": "Serial Port (USB)",
// 'address': 'COM9',
// 'protocol': 'serial',
// 'protocol_label': 'Serial Port (USB)',
// }
// ]
ports.push({ protocol, address });
@ -301,7 +301,7 @@ export class BoardsServiceImpl implements BoardsService {
const installedPlatforms = installedPlatformsResp.getInstalledPlatformList();
const req = new PlatformSearchReq();
req.setSearchArgs(options.query || "");
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)));
@ -317,9 +317,9 @@ export class BoardsServiceImpl implements BoardsService {
name: platform.getName(),
author: platform.getMaintainer(),
availableVersions: [platform.getLatest()],
description: platform.getBoardsList().map(b => b.getName()).join(", "),
description: platform.getBoardsList().map(b => b.getName()).join(', '),
installable: true,
summary: "Boards included in this package:",
summary: 'Boards included in this package:',
installedVersion,
boards: platform.getBoardsList().map(b => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
moreInfoLink: platform.getWebsite()
@ -378,7 +378,7 @@ export class BoardsServiceImpl implements BoardsService {
}
const { client, instance } = coreClient;
const [platform, architecture] = pkg.id.split(":");
const [platform, architecture] = pkg.id.split(':');
const req = new PlatformInstallReq();
req.setInstance(instance);
@ -386,12 +386,12 @@ export class BoardsServiceImpl implements BoardsService {
req.setPlatformPackage(platform);
req.setVersion(version);
console.info("Starting board installation", pkg);
console.info('Starting board installation', pkg);
const resp = client.platformInstall(req);
resp.on('data', (r: PlatformInstallResp) => {
const prog = r.getProgress();
if (prog && prog.getFile()) {
this.toolOutputService.publishNewOutput("board download", `downloading ${prog.getFile()}\n`)
this.toolOutputService.append({ tool: 'board download', chunk: `downloading ${prog.getFile()}\n` });
}
});
await new Promise<void>((resolve, reject) => {
@ -403,7 +403,7 @@ export class BoardsServiceImpl implements BoardsService {
const updatedPackage = packages.find(({ id }) => id === pkg.id) || pkg;
this.client.notifyBoardInstalled({ pkg: updatedPackage });
}
console.info("Board installation done", pkg);
console.info('Board installation done', pkg);
}
async uninstall(options: { item: BoardsPackage }): Promise<void> {
@ -414,19 +414,19 @@ export class BoardsServiceImpl implements BoardsService {
}
const { client, instance } = coreClient;
const [platform, architecture] = pkg.id.split(":");
const [platform, architecture] = pkg.id.split(':');
const req = new PlatformUninstallReq();
req.setInstance(instance);
req.setArchitecture(architecture);
req.setPlatformPackage(platform);
console.info("Starting board uninstallation", pkg);
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`)
this.toolOutputService.append({ tool: 'board uninstall', chunk: `uninstalling ${pkg.id}\n` });
logged = true;
}
})
@ -438,7 +438,7 @@ export class BoardsServiceImpl implements BoardsService {
// Here, unlike at `install` we send out the argument `pkg`. Otherwise, we would not know about the board FQBN.
this.client.notifyBoardUninstalled({ pkg });
}
console.info("Board uninstallation done", pkg);
console.info('Board uninstallation done', pkg);
}
}

View File

@ -69,9 +69,6 @@ export class CompileReq extends jspb.Message {
getExportDir(): string;
setExportDir(value: string): CompileReq;
getProgrammer(): string;
setProgrammer(value: string): CompileReq;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): CompileReq.AsObject;
@ -103,7 +100,6 @@ export namespace CompileReq {
optimizefordebug: boolean,
dryrun: boolean,
exportDir: string,
programmer: string,
}
}

View File

@ -114,8 +114,7 @@ proto.cc.arduino.cli.commands.CompileReq.toObject = function(includeInstance, ms
librariesList: (f = jspb.Message.getRepeatedField(msg, 15)) == null ? undefined : f,
optimizefordebug: jspb.Message.getBooleanFieldWithDefault(msg, 16, false),
dryrun: jspb.Message.getBooleanFieldWithDefault(msg, 17, false),
exportDir: jspb.Message.getFieldWithDefault(msg, 18, ""),
programmer: jspb.Message.getFieldWithDefault(msg, 19, "")
exportDir: jspb.Message.getFieldWithDefault(msg, 18, "")
};
if (includeInstance) {
@ -225,10 +224,6 @@ proto.cc.arduino.cli.commands.CompileReq.deserializeBinaryFromReader = function(
var value = /** @type {string} */ (reader.readString());
msg.setExportDir(value);
break;
case 19:
var value = /** @type {string} */ (reader.readString());
msg.setProgrammer(value);
break;
default:
reader.skipField();
break;
@ -385,13 +380,6 @@ proto.cc.arduino.cli.commands.CompileReq.serializeBinaryToWriter = function(mess
f
);
}
f = message.getProgrammer();
if (f.length > 0) {
writer.writeString(
19,
f
);
}
};
@ -776,24 +764,6 @@ proto.cc.arduino.cli.commands.CompileReq.prototype.setExportDir = function(value
};
/**
* optional string programmer = 19;
* @return {string}
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.getProgrammer = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 19, ""));
};
/**
* @param {string} value
* @return {!proto.cc.arduino.cli.commands.CompileReq} returns this
*/
proto.cc.arduino.cli.commands.CompileReq.prototype.setProgrammer = function(value) {
return jspb.Message.setProto3StringField(this, 19, value);
};

View File

@ -1,12 +1,12 @@
import * as grpc from '@grpc/grpc-js';
import { inject, injectable } from 'inversify';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { ToolOutputServiceServer } from '../common/protocol';
import { GrpcClientProvider } from './grpc-client-provider';
import { ArduinoCoreClient } from './cli-protocol/commands/commands_grpc_pb';
import * as commandsGrpcPb from './cli-protocol/commands/commands_grpc_pb';
import { Instance } from './cli-protocol/commands/common_pb';
import { InitReq, InitResp, UpdateIndexReq, UpdateIndexResp, UpdateLibrariesIndexResp, UpdateLibrariesIndexReq } from './cli-protocol/commands/commands_pb';
import { Event, Emitter } from '@theia/core/lib/common/event';
@injectable()
export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Client> {
@ -69,11 +69,11 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
indexUpdateSucceeded = true;
break;
} catch (e) {
this.toolOutputService.publishNewOutput("daemon", `Error while updating index in attempt ${i}: ${e}`);
this.toolOutputService.append({ tool: 'daemon', chunk: `Error while updating index in attempt ${i}: ${e}`, severity: 'error' });
}
}
if (!indexUpdateSucceeded) {
this.toolOutputService.publishNewOutput("daemon", `Was unable to update the index. Please restart to try again.`);
this.toolOutputService.append({ tool: 'daemon', chunk: 'Was unable to update the index. Please restart to try again.', severity: 'error' });
}
let libIndexUpdateSucceeded = true;
@ -83,11 +83,11 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
libIndexUpdateSucceeded = true;
break;
} catch (e) {
this.toolOutputService.publishNewOutput("daemon", `Error while updating library index in attempt ${i}: ${e}`);
this.toolOutputService.append({ tool: 'daemon', chunk: `Error while updating library index in attempt ${i}: ${e}`, severity: 'error' });
}
}
if (!libIndexUpdateSucceeded) {
this.toolOutputService.publishNewOutput("daemon", `Was unable to update the library index. Please restart to try again.`);
this.toolOutputService.append({ tool: 'daemon', chunk: `Was unable to update the library index. Please restart to try again.`, severity: 'error' });
}
if (indexUpdateSucceeded && libIndexUpdateSucceeded) {
@ -109,12 +109,12 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
if (progress.getCompleted()) {
if (file) {
if (/\s/.test(file)) {
this.toolOutputService.publishNewOutput("daemon", `${file} completed.\n`);
this.toolOutputService.append({ tool: 'daemon', chunk: `${file} completed.\n` });
} else {
this.toolOutputService.publishNewOutput("daemon", `Download of '${file}' completed.\n'`);
this.toolOutputService.append({ tool: 'daemon', chunk: `Download of '${file}' completed.\n'` });
}
} else {
this.toolOutputService.publishNewOutput("daemon", `The library index has been successfully updated.\n'`);
this.toolOutputService.append({ tool: 'daemon', chunk: `The library index has been successfully updated.\n'` });
}
file = undefined;
}
@ -140,12 +140,12 @@ export class CoreClientProvider extends GrpcClientProvider<CoreClientProvider.Cl
if (progress.getCompleted()) {
if (file) {
if (/\s/.test(file)) {
this.toolOutputService.publishNewOutput("daemon", `${file} completed.\n`);
this.toolOutputService.append({ tool: 'daemon', chunk: `${file} completed.\n` });
} else {
this.toolOutputService.publishNewOutput("daemon", `Download of '${file}' completed.\n'`);
this.toolOutputService.append({ tool: 'daemon', chunk: `Download of '${file}' completed.\n'` });
}
} else {
this.toolOutputService.publishNewOutput("daemon", `The index has been successfully updated.\n'`);
this.toolOutputService.append({ tool: 'daemon', chunk: `The index has been successfully updated.\n'` });
}
file = undefined;
}

View File

@ -35,7 +35,7 @@ export class CoreServiceImpl implements CoreService {
}
async compile(options: CoreService.Compile.Options): Promise<void> {
this.toolOutputService.publishNewOutput('compile', 'Compiling...\n' + JSON.stringify(options, null, 2) + '\n');
this.toolOutputService.append({ tool: 'compile', chunk: 'Compiling...\n' + JSON.stringify(options, null, 2) + '\n--------------------------\n' });
const { sketchUri, fqbn } = options;
const sketchFilePath = await this.fileSystem.getFsPath(sketchUri);
if (!sketchFilePath) {
@ -61,30 +61,27 @@ export class CoreServiceImpl implements CoreService {
compilerReq.setPreprocess(false);
compilerReq.setVerbose(true);
compilerReq.setQuiet(false);
if (options.programmer) {
compilerReq.setProgrammer(options.programmer.id);
}
const result = client.compile(compilerReq);
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (cr: CompileResp) => {
this.toolOutputService.publishNewOutput("compile", Buffer.from(cr.getOutStream_asU8()).toString());
this.toolOutputService.publishNewOutput("compile", Buffer.from(cr.getErrStream_asU8()).toString());
this.toolOutputService.append({ tool: 'compile', chunk: Buffer.from(cr.getOutStream_asU8()).toString() });
this.toolOutputService.append({ tool: 'compile', chunk: Buffer.from(cr.getErrStream_asU8()).toString() });
});
result.on('error', error => reject(error));
result.on('end', () => resolve());
});
this.toolOutputService.publishNewOutput("compile", "Compilation complete.\n");
this.toolOutputService.append({ tool: 'compile', chunk: '\n--------------------------\nCompilation complete.\n' });
} catch (e) {
this.toolOutputService.publishNewOutput("compile", `Compilation error: ${e}\n`);
this.toolOutputService.append({ tool: 'compile', chunk: `Compilation error: ${e}\n`, severity: 'error' });
throw e;
}
}
async upload(options: CoreService.Upload.Options): Promise<void> {
await this.compile(options);
this.toolOutputService.publishNewOutput('upload', 'Uploading...\n' + JSON.stringify(options, null, 2) + '\n');
this.toolOutputService.append({ tool: 'upload', chunk: 'Uploading...\n' + JSON.stringify(options, null, 2) + '\n--------------------------\n' });
const { sketchUri, fqbn } = options;
const sketchFilePath = await this.fileSystem.getFsPath(sketchUri);
if (!sketchFilePath) {
@ -106,8 +103,9 @@ export class CoreServiceImpl implements CoreService {
uploadReq.setInstance(instance);
uploadReq.setSketchPath(sketchpath);
uploadReq.setFqbn(fqbn);
uploadReq.setPort(options.port);
if (options.programmer) {
if ('port' in options) {
uploadReq.setPort(options.port);
} else {
uploadReq.setProgrammer(options.programmer.id);
}
const result = client.upload(uploadReq);
@ -115,15 +113,15 @@ export class CoreServiceImpl implements CoreService {
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (cr: UploadResp) => {
this.toolOutputService.publishNewOutput("upload", Buffer.from(cr.getOutStream_asU8()).toString());
this.toolOutputService.publishNewOutput("upload", Buffer.from(cr.getErrStream_asU8()).toString());
this.toolOutputService.append({ tool: 'upload', chunk: Buffer.from(cr.getOutStream_asU8()).toString() });
this.toolOutputService.append({ tool: 'upload', chunk: Buffer.from(cr.getErrStream_asU8()).toString() });
});
result.on('error', error => reject(error));
result.on('end', () => resolve());
});
this.toolOutputService.publishNewOutput("upload", "Upload complete.\n");
this.toolOutputService.append({ tool: 'upload', chunk: '\n--------------------------\nUpload complete.\n' });
} catch (e) {
this.toolOutputService.publishNewOutput("upload", `Upload error: ${e}\n`);
this.toolOutputService.append({ tool: 'upload', chunk: `Upload error: ${e}\n`, severity: 'error' });
throw e;
}
}

View File

@ -1,12 +1,16 @@
import * as fs from 'fs';
import { promisify } from 'util';
export const constants = fs.constants;
export const existsSync = fs.existsSync;
export const lstatSync = fs.lstatSync;
export const readdirSync = fs.readdirSync;
export const statSync = fs.statSync;
export const writeFileSync = fs.writeFileSync;
export const readFileSync = fs.readFileSync;
export const accessSync = fs.accessSync;
export const renameSync = fs.renameSync;
export const exists = promisify(fs.exists);
export const lstat = promisify(fs.lstat);
@ -14,6 +18,8 @@ export const readdir = promisify(fs.readdir);
export const stat = promisify(fs.stat);
export const writeFile = promisify(fs.writeFile);
export const readFile = promisify(fs.readFile);
export const access = promisify(fs.access);
export const rename = promisify(fs.rename);
export const watchFile = fs.watchFile;
export const unwatchFile = fs.unwatchFile;

View File

@ -30,7 +30,7 @@ export class ArduinoLanguageServerContribution extends BaseLanguageServerContrib
const [languageServer, clangd, cli] = await Promise.all([
getExecPath('arduino-language-server', this.onError.bind(this)),
getExecPath('clangd', this.onError.bind(this), '--version', os.platform() !== 'win32'),
getExecPath('arduino-cli', this.onError.bind(this), 'version')
getExecPath('arduino-cli', this.onError.bind(this))
]);
// Add '-log' argument to enable logging to files
const args: string[] = ['-clangd', clangd, '-cli', cli];

View File

@ -89,7 +89,7 @@ export class LibraryServiceImpl implements LibraryService {
resp.on('data', (r: LibraryInstallResp) => {
const prog = r.getProgress();
if (prog) {
this.toolOutputService.publishNewOutput("library download", `downloading ${prog.getFile()}: ${prog.getCompleted()}%\n`)
this.toolOutputService.append({ tool: 'library', chunk: `downloading ${prog.getFile()}: ${prog.getCompleted()}%\n` });
}
});
await new Promise<void>((resolve, reject) => {
@ -115,7 +115,7 @@ export class LibraryServiceImpl implements LibraryService {
const resp = client.libraryUninstall(req);
resp.on('data', (_: LibraryUninstallResp) => {
if (!logged) {
this.toolOutputService.publishNewOutput("library uninstall", `uninstalling ${library.name}:${library.installedVersion}%\n`)
this.toolOutputService.append({ tool: 'library', chunk: `uninstalling ${library.name}:${library.installedVersion}%\n` });
logged = true;
}
});
@ -129,7 +129,7 @@ export class LibraryServiceImpl implements LibraryService {
function toLibrary(tpl: Partial<Library>, release: LibraryRelease, availableVersions: string[]): Library {
return {
name: "",
name: '',
installable: false,
...tpl,

View File

@ -2,14 +2,22 @@ import { injectable, inject } from 'inversify';
import * as os from 'os';
import * as temp from 'temp';
import * as path from 'path';
import * as fs from './fs-extra';
import { ncp } from 'ncp';
import { Stats } from 'fs';
import * as fs from './fs-extra';
import URI from '@theia/core/lib/common/uri';
import { isWindows } from '@theia/core/lib/common/os';
import { FileUri, BackendApplicationContribution } from '@theia/core/lib/node';
import { ConfigService } from '../common/protocol/config-service';
import { SketchesService, Sketch } from '../common/protocol/sketches-service';
import URI from '@theia/core/lib/common/uri';
import { firstToLowerCase } from '../common/utils';
export const ALLOWED_FILE_EXTENSIONS = ['.c', '.cpp', '.h', '.hh', '.hpp', '.s', '.pde', '.ino'];
// As currently implemented on Linux,
// the maximum number of symbolic links that will be followed while resolving a pathname is 40
const MAX_FILESYSTEM_DEPTH = 40;
const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/;
// TODO: `fs`: use async API
@injectable()
@ -43,53 +51,215 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
for (const fileName of fileNames) {
const filePath = path.join(fsPath, fileName);
if (await this.isSketchFolder(FileUri.create(filePath).toString())) {
const stat = await fs.stat(filePath);
sketches.push({
mtimeMs: stat.mtimeMs,
name: fileName,
uri: FileUri.create(filePath).toString()
});
try {
const stat = await fs.stat(filePath);
const sketch = await this.loadSketch(FileUri.create(filePath).toString());
sketches.push({
...sketch,
mtimeMs: stat.mtimeMs
});
} catch {
console.warn(`Could not load sketch from ${filePath}.`);
}
}
}
return sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
}
/**
* Return all allowed files.
* File extensions: 'c', 'cpp', 'h', 'hh', 'hpp', 's', 'pde', 'ino'
* This is the TS implementation of `SketchLoad` from the CLI.
* See: https://github.com/arduino/arduino-cli/issues/837
* Based on: https://github.com/arduino/arduino-cli/blob/eef3705c4afcba4317ec38b803d9ffce5dd59a28/arduino/builder/sketch.go#L100-L215
*/
async getSketchFiles(uri: string): Promise<string[]> {
const uris: string[] = [];
const fsPath = FileUri.fsPath(uri);
if (fs.lstatSync(fsPath).isDirectory()) {
if (await this.isSketchFolder(uri)) {
const basename = path.basename(fsPath)
const fileNames = await fs.readdir(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()) {
const uri = FileUri.create(filePath).toString();
if (fileName === basename + '.ino') {
uris.unshift(uri); // The sketch file is the first.
} else {
uris.push(uri);
}
async loadSketch(uri: string): Promise<Sketch> {
const sketchPath = FileUri.fsPath(uri);
const exists = await fs.exists(sketchPath);
if (!exists) {
throw new Error(`${uri} does not exist.`);
}
const stat = await fs.lstat(sketchPath);
let sketchFolder: string | undefined;
let mainSketchFile: string | undefined;
// If a sketch folder was passed, save the parent and point sketchPath to the main sketch file
if (stat.isDirectory()) {
sketchFolder = sketchPath;
// Allowed extensions are .ino and .pde (but not both)
for (const extension of Sketch.Extensions.MAIN) {
const candidateSketchFile = path.join(sketchPath, `${path.basename(sketchPath)}${extension}`);
const candidateExists = await fs.exists(candidateSketchFile);
if (candidateExists) {
if (!mainSketchFile) {
mainSketchFile = candidateSketchFile;
} else {
throw new Error(`Multiple main sketch files found (${path.basename(mainSketchFile)}, ${path.basename(candidateSketchFile)})`);
}
}
}
return uris;
// Check main file was found.
if (!mainSketchFile) {
throw new Error(`Unable to find a sketch file in directory ${sketchFolder}`);
}
// Check main file is readable.
try {
await fs.access(mainSketchFile, fs.constants.R_OK);
} catch {
throw new Error('Unable to open the main sketch file.');
}
const mainSketchFileStat = await fs.lstat(mainSketchFile);
if (mainSketchFileStat.isDirectory()) {
throw new Error(`Sketch must not be a directory.`);
}
} else {
sketchFolder = path.dirname(sketchPath);
mainSketchFile = sketchPath;
}
const files: string[] = [];
let rootVisited = false;
const err = await this.simpleLocalWalk(sketchFolder, MAX_FILESYSTEM_DEPTH, async (fsPath: string, info: Stats, error: Error | undefined) => {
if (error) {
console.log(`Error during sketch processing: ${error}`);
return error;
}
const name = path.basename(fsPath);
if (info.isDirectory()) {
if (rootVisited) {
if (name.startsWith('.') || name === 'CVS' || name === 'RCS') {
return new SkipDir();
}
} else {
rootVisited = true
}
return undefined;
}
if (name.startsWith('.')) {
return undefined;
}
const ext = path.extname(fsPath);
const isMain = Sketch.Extensions.MAIN.indexOf(ext) !== -1;
const isAdditional = Sketch.Extensions.ADDITIONAL.indexOf(ext) !== -1;
if (!isMain && !isAdditional) {
return undefined;
}
try {
await fs.access(fsPath, fs.constants.R_OK);
files.push(fsPath);
} catch { }
return undefined;
});
if (err) {
console.error(`There was an error while collecting the sketch files: ${sketchPath}`)
throw err;
}
return this.newSketch(sketchFolder, mainSketchFile, files);
}
private newSketch(sketchFolderPath: string, mainFilePath: string, allFilesPaths: string[]): Sketch {
let mainFile: string | undefined;
const paths = new Set<string>();
for (const p of allFilesPaths) {
if (p === mainFilePath) {
mainFile = p;
} else {
paths.add(p);
}
}
if (!mainFile) {
throw new Error('Could not locate main sketch file.');
}
const additionalFiles: string[] = [];
const otherSketchFiles: string[] = [];
for (const p of Array.from(paths)) {
const ext = path.extname(p);
if (Sketch.Extensions.MAIN.indexOf(ext) !== -1) {
if (path.dirname(p) === sketchFolderPath) {
otherSketchFiles.push(p);
}
} else if (Sketch.Extensions.ADDITIONAL.indexOf(ext) !== -1) {
// XXX: this is a caveat with the CLI, we do not know the `buildPath`.
// https://github.com/arduino/arduino-cli/blob/0483882b4f370c288d5318913657bbaa0325f534/arduino/sketch/sketch.go#L108-L110
additionalFiles.push(p);
} else {
throw new Error(`Unknown sketch file extension '${ext}'.`);
}
}
additionalFiles.sort();
otherSketchFiles.sort();
return {
uri: FileUri.create(sketchFolderPath).toString(),
mainFileUri: FileUri.create(mainFile).toString(),
name: path.basename(sketchFolderPath),
additionalFileUris: additionalFiles.map(p => FileUri.create(p).toString()),
otherSketchFileUris: otherSketchFiles.map(p => FileUri.create(p).toString())
}
}
protected async simpleLocalWalk(
root: string,
maxDepth: number,
walk: (fsPath: string, info: Stats | undefined, err: Error | undefined) => Promise<Error | undefined>): Promise<Error | undefined> {
let { info, err } = await this.lstat(root);
if (err) {
return walk(root, undefined, err);
}
if (!info) {
return new Error(`Could not stat file: ${root}.`);
}
err = await walk(root, info, err);
if (err instanceof SkipDir) {
return undefined;
}
if (info.isDirectory()) {
if (maxDepth <= 0) {
return walk(root, info, new Error(`Filesystem bottom is too deep (directory recursion or filesystem really deep): ${root}`));
}
maxDepth--;
const files: string[] = [];
try {
files.push(...await fs.readdir(root));
} catch { }
for (const file of files) {
err = await this.simpleLocalWalk(path.join(root, file), maxDepth, walk);
if (err instanceof SkipDir) {
return undefined;
}
}
}
return undefined;
}
private async lstat(fsPath: string): Promise<{ info: Stats, err: undefined } | { info: undefined, err: Error }> {
const exists = await fs.exists(fsPath);
if (!exists) {
return { info: undefined, err: new Error(`${fsPath} does not exist`) };
}
try {
const info = await fs.lstat(fsPath);
return { info, err: undefined };
} catch (err) {
return { info: undefined, err };
}
const sketchDir = path.dirname(fsPath);
return this.getSketchFiles(FileUri.create(sketchDir).toString());
}
async createNewSketch(): Promise<Sketch> {
const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const today = new Date();
const parent = await new Promise<string>((resolve, reject) => {
this.temp.mkdir({ prefix: '.arduinoProIDE' }, (err, dirPath) => {
this.temp.mkdir({ prefix: '.arduinoProIDE-unsaved' }, (err, dirPath) => {
if (err) {
reject(err);
return;
@ -129,10 +299,7 @@ void loop() {
}
`, { encoding: 'utf8' });
return {
name: sketchName,
uri: FileUri.create(sketchDir).toString()
}
return this.loadSketch(FileUri.create(sketchDir).toString());
}
async getSketchFolder(uri: string): Promise<Sketch | undefined> {
@ -142,10 +309,7 @@ void loop() {
let currentUri = new URI(uri);
while (currentUri && !currentUri.path.isRoot) {
if (await this.isSketchFolder(currentUri.toString())) {
return {
name: currentUri.path.base,
uri: currentUri.toString()
};
return this.loadSketch(currentUri.toString());
}
currentUri = currentUri.parent;
}
@ -159,7 +323,10 @@ void loop() {
const files = await fs.readdir(fsPath);
for (let i = 0; i < files.length; i++) {
if (files[i] === basename + '.ino') {
return true;
try {
await this.loadSketch(fsPath);
return true;
} catch { }
}
}
}
@ -167,26 +334,52 @@ void loop() {
}
async isTemp(sketch: Sketch): Promise<boolean> {
const sketchPath = FileUri.fsPath(sketch.uri);
return sketchPath.indexOf('.arduinoProIDE') !== -1 && sketchPath.startsWith(os.tmpdir());
let sketchPath = FileUri.fsPath(sketch.uri);
let temp = os.tmpdir();
// Note: VS Code URI normalizes the drive letter. `C:` will be converted into `c:`.
// https://github.com/Microsoft/vscode/issues/68325#issuecomment-462239992
if (isWindows) {
if (WIN32_DRIVE_REGEXP.exec(sketchPath)) {
sketchPath = firstToLowerCase(sketchPath);
}
if (WIN32_DRIVE_REGEXP.exec(temp)) {
temp = firstToLowerCase(temp);
}
}
return sketchPath.indexOf('.arduinoProIDE-unsaved') !== -1 && sketchPath.startsWith(temp);
}
async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise<string> {
const source = FileUri.fsPath(sketch.uri);
if (await !fs.exists(source)) {
const exists = await fs.exists(source);
if (!exists) {
throw new Error(`Sketch does not exist: ${sketch}`);
}
const destination = FileUri.fsPath(destinationUri);
await new Promise<void>((resolve, reject) => {
ncp.ncp(source, destination, error => {
ncp.ncp(source, destination, async error => {
if (error) {
reject(error);
return;
}
resolve();
const newName = path.basename(destination);
try {
await fs.rename(path.join(destination, new URI(sketch.mainFileUri).path.base), path.join(destination, `${newName}.ino`));
await this.loadSketch(destinationUri); // Sanity check.
resolve();
} catch (e) {
reject(e);
}
});
});
return FileUri.create(destination).toString();
}
}
class SkipDir extends Error {
constructor() {
super('skip this directory');
Object.setPrototypeOf(this, SkipDir.prototype);
}
}

View File

@ -1,32 +1,40 @@
import { injectable } from "inversify";
import { ToolOutputServiceServer, ToolOutputServiceClient } from "../common/protocol/tool-output-service";
import { injectable } from 'inversify';
import { ToolOutputServiceServer, ToolOutputServiceClient, ToolOutputMessage } from '../common/protocol/tool-output-service';
@injectable()
export class ToolOutputServiceServerImpl implements ToolOutputServiceServer {
protected clients: ToolOutputServiceClient[] = [];
publishNewOutput(tool: string, chunk: string): void {
if (!chunk) {
append(message: ToolOutputMessage): void {
if (!message.chunk) {
return;
}
this.clients.forEach(c => c.onNewOutput(tool, chunk));
for (const client of this.clients) {
client.onMessageReceived(message);
}
}
setClient(client: ToolOutputServiceClient | undefined): void {
if (!client) {
return;
}
this.clients.push(client);
}
disposeClient(client: ToolOutputServiceClient): void {
const index = this.clients.indexOf(client);
if (index === -1) {
console.warn(`Could not dispose tools output client. It was not registered.`);
return;
}
this.clients.splice(index, 1);
}
dispose(): void {
this.clients = [];
for (const client of this.clients) {
this.disposeClient(client);
}
this.clients.length = 0;
}
}

View File

@ -1,5 +1,5 @@
import * as fs from 'fs';
import * as net from 'net';
// import * as net from 'net';
import * as path from 'path';
import * as temp from 'temp';
import { fail } from 'assert';
@ -51,45 +51,51 @@ describe('arduino-daemon-impl', () => {
track.cleanupSync();
})
it('should parse an error - address already in use error [json]', async () => {
let server: net.Server | undefined = undefined;
try {
server = await new Promise<net.Server>(resolve => {
const server = net.createServer();
server.listen(() => resolve(server));
});
const address = server.address() as net.AddressInfo;
await new SilentArduinoDaemonImpl(address.port, 'json').spawnDaemonProcess();
fail('Expected a failure.')
} catch (e) {
expect(e).to.be.instanceOf(DaemonError);
expect(e.code).to.be.equal(DaemonError.ADDRESS_IN_USE);
} finally {
if (server) {
server.close();
}
}
});
// it('should parse an error - address already in use error [json]', async function (): Promise<void> {
// if (process.platform === 'win32') {
// this.skip();
// }
// let server: net.Server | undefined = undefined;
// try {
// server = await new Promise<net.Server>(resolve => {
// const server = net.createServer();
// server.listen(() => resolve(server));
// });
// const address = server.address() as net.AddressInfo;
// await new SilentArduinoDaemonImpl(address.port, 'json').spawnDaemonProcess();
// fail('Expected a failure.')
// } catch (e) {
// expect(e).to.be.instanceOf(DaemonError);
// expect(e.code).to.be.equal(DaemonError.ADDRESS_IN_USE);
// } finally {
// if (server) {
// server.close();
// }
// }
// });
it('should parse an error - address already in use error [text]', async () => {
let server: net.Server | undefined = undefined;
try {
server = await new Promise<net.Server>(resolve => {
const server = net.createServer();
server.listen(() => resolve(server));
});
const address = server.address() as net.AddressInfo;
await new SilentArduinoDaemonImpl(address.port, 'text').spawnDaemonProcess();
fail('Expected a failure.')
} catch (e) {
expect(e).to.be.instanceOf(DaemonError);
expect(e.code).to.be.equal(DaemonError.ADDRESS_IN_USE);
} finally {
if (server) {
server.close();
}
}
});
// it('should parse an error - address already in use error [text]', async function (): Promise<void> {
// if (process.platform === 'win32') {
// this.skip();
// }
// let server: net.Server | undefined = undefined;
// try {
// server = await new Promise<net.Server>(resolve => {
// const server = net.createServer();
// server.listen(() => resolve(server));
// });
// const address = server.address() as net.AddressInfo;
// await new SilentArduinoDaemonImpl(address.port, 'text').spawnDaemonProcess();
// fail('Expected a failure.')
// } catch (e) {
// expect(e).to.be.instanceOf(DaemonError);
// expect(e.code).to.be.equal(DaemonError.ADDRESS_IN_USE);
// } finally {
// if (server) {
// server.close();
// }
// }
// });
it('should parse an error - unknown address [json]', async () => {
try {

View File

@ -38,6 +38,7 @@
"preferences": {
"editor.autoSave": "on",
"editor.minimap.enabled": false,
"editor.tabSize": 2,
"editor.scrollBeyondLastLine": false
}
}

View File

@ -41,6 +41,7 @@
"preferences": {
"editor.autoSave": "on",
"editor.minimap.enabled": false,
"editor.tabSize": 2,
"editor.scrollBeyondLastLine": false
}
}

View File

@ -22,6 +22,9 @@ const { fork } = require('child_process');
const { app, dialog, shell, BrowserWindow, ipcMain, Menu, globalShortcut } = electron;
const { ElectronSecurityToken } = require('@theia/core/lib/electron-common/electron-token');
// Fix the window reloading issue, see: https://github.com/electron/electron/issues/22119
app.allowRendererProcessReuse = false;
const applicationName = `Arduino Pro IDE`;
const isSingleInstance = false;
const disallowReloadKeybinding = false;

View File

@ -124,7 +124,6 @@
},
"theiaPluginsDir": "plugins",
"theiaPlugins": {
"vscode-builtin-cpp": "http://open-vsx.org/api/vscode/cpp/1.44.2/file/vscode.cpp-1.44.2.vsix",
"vscode-yaml": "https://open-vsx.org/api/redhat/vscode-yaml/0.7.2/file/redhat.vscode-yaml-0.7.2.vsix"
"vscode-builtin-cpp": "http://open-vsx.org/api/vscode/cpp/1.44.2/file/vscode.cpp-1.44.2.vsix"
}
}

View File

@ -34,7 +34,6 @@
],
"theiaPluginsDir": "plugins",
"theiaPlugins": {
"vscode-builtin-cpp": "http://open-vsx.org/api/vscode/cpp/1.44.2/file/vscode.cpp-1.44.2.vsix",
"vscode-yaml": "https://open-vsx.org/api/redhat/vscode-yaml/0.7.2/file/redhat.vscode-yaml-0.7.2.vsix"
"vscode-builtin-cpp": "http://open-vsx.org/api/vscode/cpp/1.44.2/file/vscode.cpp-1.44.2.vsix"
}
}

935
yarn.lock

File diff suppressed because it is too large Load Diff