Make tab width 2 spaces (#445)

This commit is contained in:
Francesco Stasi
2021-07-09 10:14:42 +02:00
committed by GitHub
parent 40a73af82b
commit e10f0f1683
205 changed files with 19676 additions and 20141 deletions

View File

@@ -1,13 +1,6 @@
{ {
"singleQuote": true, "singleQuote": true,
"tabWidth": 4, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"overrides": [ "printWidth": 80
{
"files": "*.{json,yml}",
"options": {
"tabWidth": 2
}
}
]
} }

View File

@@ -4,18 +4,18 @@ import { Command } from '@theia/core/lib/common/command';
* @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions. * @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions.
*/ */
export namespace ArduinoCommands { export namespace ArduinoCommands {
export const TOGGLE_COMPILE_FOR_DEBUG: Command = { export const TOGGLE_COMPILE_FOR_DEBUG: Command = {
id: 'arduino-toggle-compile-for-debug', id: 'arduino-toggle-compile-for-debug',
}; };
/** /**
* Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...) * Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...)
*/ */
export const OPEN_SKETCH_FILES: Command = { export const OPEN_SKETCH_FILES: Command = {
id: 'arduino-open-sketch-files', id: 'arduino-open-sketch-files',
}; };
export const OPEN_BOARDS_DIALOG: Command = { export const OPEN_BOARDS_DIALOG: Command = {
id: 'arduino-open-boards-dialog', id: 'arduino-open-boards-dialog',
}; };
} }

View File

@@ -1,18 +1,38 @@
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger, DisposableCollection } from '@theia/core';
import { import {
ContextMenuRenderer, MAIN_MENU_BAR,
FrontendApplication, FrontendApplicationContribution, MenuContribution,
OpenerService, StatusBar, StatusBarAlignment MenuModelRegistry,
SelectionService,
ILogger,
DisposableCollection,
} from '@theia/core';
import {
ContextMenuRenderer,
FrontendApplication,
FrontendApplicationContribution,
OpenerService,
StatusBar,
StatusBarAlignment,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import {
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; 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 { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { EditorMainMenu, EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser'; import {
EditorMainMenu,
EditorManager,
EditorOpenerOptions,
} from '@theia/editor/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution'; import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
@@ -26,7 +46,14 @@ import { inject, injectable, postConstruct } from 'inversify';
import * as React from 'react'; import * as React from 'react';
import { remote } from 'electron'; import { remote } from 'electron';
import { MainMenuManager } from '../common/main-menu-manager'; import { MainMenuManager } from '../common/main-menu-manager';
import { BoardsService, CoreService, Port, SketchesService, ExecutableService, Sketch } from '../common/protocol'; import {
BoardsService,
CoreService,
Port,
SketchesService,
ExecutableService,
Sketch,
} from '../common/protocol';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon'; import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service'; import { ConfigService } from '../common/protocol/config-service';
import { FileSystemExt } from '../common/protocol/filesystem-ext'; import { FileSystemExt } from '../common/protocol/filesystem-ext';
@@ -53,434 +80,514 @@ import { FrontendApplicationStateService } from '@theia/core/lib/browser/fronten
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution'; import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
@injectable() @injectable()
export class ArduinoFrontendContribution implements FrontendApplicationContribution, export class ArduinoFrontendContribution
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution { implements
FrontendApplicationContribution,
TabBarToolbarContribution,
CommandContribution,
MenuContribution,
ColorContribution
{
@inject(ILogger)
protected logger: ILogger;
@inject(ILogger) @inject(MessageService)
protected logger: ILogger; protected readonly messageService: MessageService;
@inject(MessageService) @inject(BoardsService)
protected readonly messageService: MessageService; protected readonly boardsService: BoardsService;
@inject(BoardsService) @inject(CoreService)
protected readonly boardsService: BoardsService; protected readonly coreService: CoreService;
@inject(CoreService) @inject(BoardsServiceProvider)
protected readonly coreService: CoreService; protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(BoardsServiceProvider) @inject(SelectionService)
protected readonly boardsServiceClientImpl: BoardsServiceProvider; protected readonly selectionService: SelectionService;
@inject(SelectionService) @inject(EditorManager)
protected readonly selectionService: SelectionService; protected readonly editorManager: EditorManager;
@inject(EditorManager) @inject(ContextMenuRenderer)
protected readonly editorManager: EditorManager; protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(ContextMenuRenderer) @inject(FileDialogService)
protected readonly contextMenuRenderer: ContextMenuRenderer; protected readonly fileDialogService: FileDialogService;
@inject(FileDialogService) @inject(FileService)
protected readonly fileDialogService: FileDialogService; protected readonly fileService: FileService;
@inject(FileService) @inject(SketchesService)
protected readonly fileService: FileService; protected readonly sketchService: SketchesService;
@inject(SketchesService) @inject(BoardsConfigDialog)
protected readonly sketchService: SketchesService; protected readonly boardsConfigDialog: BoardsConfigDialog;
@inject(BoardsConfigDialog) @inject(MenuModelRegistry)
protected readonly boardsConfigDialog: BoardsConfigDialog; protected readonly menuRegistry: MenuModelRegistry;
@inject(MenuModelRegistry) @inject(CommandRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(CommandRegistry) @inject(StatusBar)
protected readonly commandRegistry: CommandRegistry; protected readonly statusBar: StatusBar;
@inject(StatusBar) @inject(WorkspaceService)
protected readonly statusBar: StatusBar; protected readonly workspaceService: WorkspaceService;
@inject(WorkspaceService) @inject(MonitorConnection)
protected readonly workspaceService: WorkspaceService; protected readonly monitorConnection: MonitorConnection;
@inject(MonitorConnection) @inject(FileNavigatorContribution)
protected readonly monitorConnection: MonitorConnection; protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(FileNavigatorContribution) @inject(OutputContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution; protected readonly outputContribution: OutputContribution;
@inject(OutputContribution) @inject(OutlineViewContribution)
protected readonly outputContribution: OutputContribution; protected readonly outlineContribution: OutlineViewContribution;
@inject(OutlineViewContribution) @inject(ProblemContribution)
protected readonly outlineContribution: OutlineViewContribution; protected readonly problemContribution: ProblemContribution;
@inject(ProblemContribution) @inject(ScmContribution)
protected readonly problemContribution: ProblemContribution; protected readonly scmContribution: ScmContribution;
@inject(ScmContribution) @inject(SearchInWorkspaceFrontendContribution)
protected readonly scmContribution: ScmContribution; protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(SearchInWorkspaceFrontendContribution) @inject(SketchbookWidgetContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution; protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution;
@inject(SketchbookWidgetContribution) @inject(EditorMode)
protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution; protected readonly editorMode: EditorMode;
@inject(EditorMode) @inject(ArduinoDaemon)
protected readonly editorMode: EditorMode; protected readonly daemon: ArduinoDaemon;
@inject(ArduinoDaemon) @inject(OpenerService)
protected readonly daemon: ArduinoDaemon; protected readonly openerService: OpenerService;
@inject(OpenerService) @inject(ConfigService)
protected readonly openerService: OpenerService; protected readonly configService: ConfigService;
@inject(ConfigService) @inject(BoardsDataStore)
protected readonly configService: ConfigService; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsDataStore) @inject(MainMenuManager)
protected readonly boardsDataStore: BoardsDataStore; protected readonly mainMenuManager: MainMenuManager;
@inject(MainMenuManager) @inject(FileSystemExt)
protected readonly mainMenuManager: MainMenuManager; protected readonly fileSystemExt: FileSystemExt;
@inject(FileSystemExt) @inject(HostedPluginSupport)
protected readonly fileSystemExt: FileSystemExt; protected hostedPluginSupport: HostedPluginSupport;
@inject(HostedPluginSupport) @inject(ExecutableService)
protected hostedPluginSupport: HostedPluginSupport; protected executableService: ExecutableService;
@inject(ExecutableService) @inject(ResponseService)
protected executableService: ExecutableService; protected readonly responseService: ResponseService;
@inject(ResponseService) @inject(ArduinoPreferences)
protected readonly responseService: ResponseService; protected readonly arduinoPreferences: ArduinoPreferences;
@inject(ArduinoPreferences) @inject(SketchesServiceClientImpl)
protected readonly arduinoPreferences: ArduinoPreferences; protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(SketchesServiceClientImpl) @inject(FrontendApplicationStateService)
protected readonly sketchServiceClient: SketchesServiceClientImpl; protected readonly appStateService: FrontendApplicationStateService;
@inject(FrontendApplicationStateService) protected invalidConfigPopup:
protected readonly appStateService: FrontendApplicationStateService; | Promise<void | 'No' | 'Yes' | undefined>
| undefined;
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined; protected toDisposeOnStop = new DisposableCollection();
protected toDisposeOnStop = new DisposableCollection();
@postConstruct()
protected async init(): Promise<void> {
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn('You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.');
}
const updateStatusBar = ({ selectedBoard, selectedPort }: BoardsConfig.Config) => {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: selectedBoard ? `$(microchip) ${selectedBoard.name}` : '$(close) no board selected',
className: 'arduino-selected-board'
});
if (selectedBoard) {
this.statusBar.setElement('arduino-selected-port', {
alignment: StatusBarAlignment.RIGHT,
text: selectedPort ? `on ${Port.toString(selectedPort)}` : '[not connected]',
className: 'arduino-selected-port'
});
}
}
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.appStateService.reachedState('ready').then(async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && (!await this.sketchService.isTemp(sketch))) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(this.fileService.onDidFilesChange(async event => {
for (const { type, resource } of event.changes) {
if (type === FileChangeType.ADDED && resource.parent.toString() === sketch.uri) {
const reloadedSketch = await this.sketchService.loadSketch(sketch.uri)
if (Sketch.isInSketch(resource, reloadedSketch)) {
this.ensureOpened(resource.toString(), true, { mode: 'open' });
}
}
}
}));
}
});
@postConstruct()
protected async init(): Promise<void> {
if (!window.navigator.onLine) {
// tslint:disable-next-line:max-line-length
this.messageService.warn(
'You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.'
);
} }
const updateStatusBar = ({
onStart(app: FrontendApplication): void { selectedBoard,
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode. selectedPort,
for (const viewContribution of [ }: BoardsConfig.Config) => {
this.fileNavigatorContributions, this.statusBar.setElement('arduino-selected-board', {
this.outputContribution, alignment: StatusBarAlignment.RIGHT,
this.outlineContribution, text: selectedBoard
this.problemContribution, ? `$(microchip) ${selectedBoard.name}`
this.scmContribution, : '$(close) no board selected',
this.siwContribution, className: 'arduino-selected-board',
this.sketchbookWidgetContribution] as Array<FrontendApplicationContribution>) { });
if (viewContribution.initializeLayout) { if (selectedBoard) {
viewContribution.initializeLayout(app); this.statusBar.setElement('arduino-selected-port', {
} alignment: StatusBarAlignment.RIGHT,
} text: selectedPort
const start = async ({ selectedBoard }: BoardsConfig.Config) => { ? `on ${Port.toString(selectedPort)}`
if (selectedBoard) { : '[not connected]',
const { name, fqbn } = selectedBoard; className: 'arduino-selected-port',
if (fqbn) { });
this.startLanguageServer(fqbn, name); }
};
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.appStateService.reachedState('ready').then(async () => {
const sketch = await this.sketchServiceClient.currentSketch();
if (sketch && !(await this.sketchService.isTemp(sketch))) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {
if (
type === FileChangeType.ADDED &&
resource.parent.toString() === sketch.uri
) {
const reloadedSketch = await this.sketchService.loadSketch(
sketch.uri
);
if (Sketch.isInSketch(resource, reloadedSketch)) {
this.ensureOpened(resource.toString(), true, {
mode: 'open',
});
} }
}
} }
}; })
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) {
start(this.boardsServiceClientImpl.boardsConfig);
}
});
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'arduino.window.zoomLevel' && typeof event.newValue === 'number' && event.newValue !== event.oldValue) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
});
app.shell.leftPanelHandler.removeMenu('settings-menu');
}
onStop(): void {
this.toDisposeOnStop.dispose();
}
protected languageServerFqbn?: string;
protected languageServerStartMutex = new Mutex();
protected async startLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginSupport.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
console.info(`Could not start language server for ${fqbn}. The core is not installed for the board.`);
if (this.languageServerFqbn) {
try {
await this.commandRegistry.executeCommand('arduino.languageserver.stop');
console.info(`Stopped language server process for ${this.languageServerFqbn}.`);
this.languageServerFqbn = undefined;
} catch (e) {
console.error(`Failed to start language server process for ${this.languageServerFqbn}`, e);
throw e;
}
}
return;
}
if (fqbn === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileService.fsPath(new URI(currentSketch.uri));
}
}
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(cliUri)),
this.fileService.fsPath(new URI(lsUri)),
this.fileService.fsPath(new URI(await this.configService.getCliConfigFileUri()))
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${20_000} ms.`)), 20_000)),
this.commandRegistry.executeCommand<string>('arduino.languageserver.start', {
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliConfigPath,
board: {
fqbn,
name: name ? `"${name}"` : undefined
}
})
]);
} catch (e) {
console.log(`Failed to start language server for ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
release();
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClientImpl} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 7
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Serial Monitor'
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
execute: () => this.editorMode.toggleCompileForDebug(),
isToggled: () => this.editorMode.compileForDebug
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
execute: async (uri: URI) => {
this.openSketchFiles(uri);
}
});
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
}
});
}
registerMenus(registry: MenuModelRegistry) {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '4'
});
}
protected async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
await this.ensureOpened(mainFileUri, true);
if (mainFileUri.endsWith('.pde')) {
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
this.messageService.info(message, 'Later', 'Yes').then(async answer => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false });
}
});
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
this.messageService.error(message);
}
}
protected async ensureOpened(uri: string, forceOpen = false, options?: EditorOpenerOptions | undefined): Promise<any> {
const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri), options);
}
}
registerColors(colors: ColorRegistry): void {
colors.register(
{
id: 'arduino.branding.primary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background'
},
description: 'The primary branding color, such as dialog titles, library, and board manager list labels.'
},
{
id: 'arduino.branding.secondary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background'
},
description: 'Secondary branding color for list selections, dropdowns, and widget borders.'
},
{
id: 'arduino.foreground',
defaults: {
dark: 'editorWidget.background',
light: 'editorWidget.background',
hc: 'editorWidget.background'
},
description: 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.'
},
{
id: 'arduino.toolbar.background',
defaults: {
dark: 'button.background',
light: 'button.background',
hc: 'activityBar.inactiveForeground'
},
description: 'Background color of the toolbar items. Such as Upload, Verify, etc.'
},
{
id: 'arduino.toolbar.hoverBackground',
defaults: {
dark: 'button.hoverBackground',
light: 'button.foreground',
hc: 'textLink.foreground'
},
description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.'
},
{
id: 'arduino.toolbar.toggleBackground',
defaults: {
dark: 'editor.selectionBackground',
light: 'editor.selectionBackground',
hc: 'textPreformat.foreground'
},
description: 'Toggle color of the toolbar items when they are currently toggled (the command is in progress)'
},
{
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.'
}
); );
} }
});
}
onStart(app: FrontendApplication): void {
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outputContribution,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution,
this.sketchbookWidgetContribution,
] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(app);
}
}
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
if (selectedBoard) {
const { name, fqbn } = selectedBoard;
if (fqbn) {
this.startLanguageServer(fqbn, name);
}
}
};
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.language.log' &&
event.newValue !== event.oldValue
) {
start(this.boardsServiceClientImpl.boardsConfig);
}
});
this.arduinoPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
});
this.arduinoPreferences.onPreferenceChanged((event) => {
if (
event.preferenceName === 'arduino.window.zoomLevel' &&
typeof event.newValue === 'number' &&
event.newValue !== event.oldValue
) {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
}
});
app.shell.leftPanelHandler.removeMenu('settings-menu');
}
onStop(): void {
this.toDisposeOnStop.dispose();
}
protected languageServerFqbn?: string;
protected languageServerStartMutex = new Mutex();
protected async startLanguageServer(
fqbn: string,
name: string | undefined
): Promise<void> {
const release = await this.languageServerStartMutex.acquire();
try {
await this.hostedPluginSupport.didStart;
const details = await this.boardsService.getBoardDetails({ fqbn });
if (!details) {
// Core is not installed for the selected board.
console.info(
`Could not start language server for ${fqbn}. The core is not installed for the board.`
);
if (this.languageServerFqbn) {
try {
await this.commandRegistry.executeCommand(
'arduino.languageserver.stop'
);
console.info(
`Stopped language server process for ${this.languageServerFqbn}.`
);
this.languageServerFqbn = undefined;
} catch (e) {
console.error(
`Failed to start language server process for ${this.languageServerFqbn}`,
e
);
throw e;
}
}
return;
}
if (fqbn === this.languageServerFqbn) {
// NOOP
return;
}
this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log');
let currentSketchPath: string | undefined = undefined;
if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch();
if (currentSketch) {
currentSketchPath = await this.fileService.fsPath(
new URI(currentSketch.uri)
);
}
}
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([
this.fileService.fsPath(new URI(clangdUri)),
this.fileService.fsPath(new URI(cliUri)),
this.fileService.fsPath(new URI(lsUri)),
this.fileService.fsPath(
new URI(await this.configService.getCliConfigFileUri())
),
]);
this.languageServerFqbn = await Promise.race([
new Promise<undefined>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${20_000} ms.`)),
20_000
)
),
this.commandRegistry.executeCommand<string>(
'arduino.languageserver.start',
{
lsPath,
cliPath,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliConfigPath,
board: {
fqbn,
name: name ? `"${name}"` : undefined,
},
}
),
]);
} catch (e) {
console.log(`Failed to start language server for ${fqbn}`, e);
this.languageServerFqbn = undefined;
} finally {
release();
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => (
<BoardsToolBarItem
key="boardsToolbarItem"
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClientImpl}
/>
),
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 7,
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
tooltip: 'Serial Monitor',
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
execute: () => this.editorMode.toggleCompileForDebug(),
isToggled: () => this.editorMode.compileForDebug,
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
execute: async (uri: URI) => {
this.openSketchFiles(uri);
},
});
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
execute: async (query?: string | undefined) => {
const boardsConfig = await this.boardsConfigDialog.open(query);
if (boardsConfig) {
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
},
});
}
registerMenus(registry: MenuModelRegistry) {
const menuId = (menuPath: string[]): string => {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
};
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
label: 'Optimize for Debugging',
order: '4',
});
}
protected async openSketchFiles(uri: URI): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
await this.ensureOpened(mainFileUri, true);
if (mainFileUri.endsWith('.pde')) {
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
this.messageService
.info(message, 'Later', 'Yes')
.then(async (answer) => {
if (answer === 'Yes') {
this.commandRegistry.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{
execOnlyIfTemp: false,
openAfterMove: true,
wipeOriginal: false,
}
);
}
});
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : JSON.stringify(e);
this.messageService.error(message);
}
}
protected async ensureOpened(
uri: string,
forceOpen = false,
options?: EditorOpenerOptions | undefined
): Promise<any> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
if (!widget || forceOpen) {
return this.editorManager.open(new URI(uri), options);
}
}
registerColors(colors: ColorRegistry): void {
colors.register(
{
id: 'arduino.branding.primary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background',
},
description:
'The primary branding color, such as dialog titles, library, and board manager list labels.',
},
{
id: 'arduino.branding.secondary',
defaults: {
dark: 'statusBar.background',
light: 'statusBar.background',
},
description:
'Secondary branding color for list selections, dropdowns, and widget borders.',
},
{
id: 'arduino.foreground',
defaults: {
dark: 'editorWidget.background',
light: 'editorWidget.background',
hc: 'editorWidget.background',
},
description:
'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.',
},
{
id: 'arduino.toolbar.background',
defaults: {
dark: 'button.background',
light: 'button.background',
hc: 'activityBar.inactiveForeground',
},
description:
'Background color of the toolbar items. Such as Upload, Verify, etc.',
},
{
id: 'arduino.toolbar.hoverBackground',
defaults: {
dark: 'button.hoverBackground',
light: 'button.foreground',
hc: 'textLink.foreground',
},
description:
'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.',
},
{
id: 'arduino.toolbar.toggleBackground',
defaults: {
dark: 'editor.selectionBackground',
light: 'editor.selectionBackground',
hc: 'textPreformat.foreground',
},
description:
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
},
{
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

@@ -1,156 +1,155 @@
import { interfaces } from 'inversify'; import { interfaces } from 'inversify';
import { import {
createPreferenceProxy, createPreferenceProxy,
PreferenceProxy, PreferenceProxy,
PreferenceService, PreferenceService,
PreferenceContribution, PreferenceContribution,
PreferenceSchema, PreferenceSchema,
} from '@theia/core/lib/browser/preferences'; } from '@theia/core/lib/browser/preferences';
import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol'; import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol';
export const ArduinoConfigSchema: PreferenceSchema = { export const ArduinoConfigSchema: PreferenceSchema = {
type: 'object', type: 'object',
properties: { properties: {
'arduino.language.log': { 'arduino.language.log': {
type: 'boolean', type: 'boolean',
description: description:
"True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.", "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
default: false, default: false,
},
'arduino.compile.verbose': {
type: 'boolean',
description: 'True for verbose compile output. False by default',
default: false,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description:
"Tells gcc which warning level to use. It's 'None' by default",
default: 'None',
},
'arduino.upload.verbose': {
type: 'boolean',
description: 'True for verbose upload output. False by default.',
default: false,
},
'arduino.upload.verify': {
type: 'boolean',
default: false,
},
'arduino.window.autoScale': {
type: 'boolean',
description:
'True if the user interface automatically scales with the font size.',
default: true,
},
'arduino.window.zoomLevel': {
type: 'number',
description:
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
default: 0,
},
'arduino.ide.autoUpdate': {
type: 'boolean',
description:
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
default: true,
},
'arduino.sketchbook.showAllFiles': {
type: 'boolean',
description:
'True to show all sketch files inside the sketch. It is false by default.',
default: false,
},
'arduino.cloud.enabled': {
type: 'boolean',
description:
'True if the sketch sync functions are enabled. Defaults to true.',
default: true,
},
'arduino.cloud.pull.warn': {
type: 'boolean',
description:
'True if users should be warned before pulling a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.push.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.pushpublic.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.',
default: true,
},
'arduino.cloud.sketchSyncEnpoint': {
type: 'string',
description:
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.',
default: 'https://api2.arduino.cc/create',
},
'arduino.auth.clientID': {
type: 'string',
description: 'The OAuth2 client ID.',
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
},
'arduino.auth.domain': {
type: 'string',
description: 'The OAuth2 domain.',
default: 'login.arduino.cc',
},
'arduino.auth.audience': {
type: 'string',
description: 'The 0Auth2 audience.',
default: 'https://api.arduino.cc',
},
'arduino.auth.registerUri': {
type: 'string',
description: 'The URI used to register a new user.',
default: 'https://auth.arduino.cc/login#/register',
},
}, },
'arduino.compile.verbose': {
type: 'boolean',
description: 'True for verbose compile output. False by default',
default: false,
},
'arduino.compile.warnings': {
enum: [...CompilerWarningLiterals],
description:
"Tells gcc which warning level to use. It's 'None' by default",
default: 'None',
},
'arduino.upload.verbose': {
type: 'boolean',
description: 'True for verbose upload output. False by default.',
default: false,
},
'arduino.upload.verify': {
type: 'boolean',
default: false,
},
'arduino.window.autoScale': {
type: 'boolean',
description:
'True if the user interface automatically scales with the font size.',
default: true,
},
'arduino.window.zoomLevel': {
type: 'number',
description:
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
default: 0,
},
'arduino.ide.autoUpdate': {
type: 'boolean',
description:
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
default: true,
},
'arduino.sketchbook.showAllFiles': {
type: 'boolean',
description:
'True to show all sketch files inside the sketch. It is false by default.',
default: false,
},
'arduino.cloud.enabled': {
type: 'boolean',
description:
'True if the sketch sync functions are enabled. Defaults to true.',
default: true,
},
'arduino.cloud.pull.warn': {
type: 'boolean',
description:
'True if users should be warned before pulling a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.push.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a cloud sketch. Defaults to true.',
default: true,
},
'arduino.cloud.pushpublic.warn': {
type: 'boolean',
description:
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.',
default: true,
},
'arduino.cloud.sketchSyncEnpoint': {
type: 'string',
description:
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.',
default: 'https://api2.arduino.cc/create',
},
'arduino.auth.clientID': {
type: 'string',
description: 'The OAuth2 client ID.',
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
},
'arduino.auth.domain': {
type: 'string',
description: 'The OAuth2 domain.',
default: 'login.arduino.cc',
},
'arduino.auth.audience': {
type: 'string',
description: 'The 0Auth2 audience.',
default: 'https://api.arduino.cc',
},
'arduino.auth.registerUri': {
type: 'string',
description: 'The URI used to register a new user.',
default: 'https://auth.arduino.cc/login#/register',
},
},
}; };
export interface ArduinoConfiguration { export interface ArduinoConfiguration {
'arduino.language.log': boolean; 'arduino.language.log': boolean;
'arduino.compile.verbose': boolean; 'arduino.compile.verbose': boolean;
'arduino.compile.warnings': CompilerWarnings; 'arduino.compile.warnings': CompilerWarnings;
'arduino.upload.verbose': boolean; 'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean; 'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean; 'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number; 'arduino.window.zoomLevel': number;
'arduino.ide.autoUpdate': boolean; 'arduino.ide.autoUpdate': boolean;
'arduino.sketchbook.showAllFiles': boolean; 'arduino.sketchbook.showAllFiles': boolean;
'arduino.cloud.enabled': boolean; 'arduino.cloud.enabled': boolean;
'arduino.cloud.pull.warn': boolean; 'arduino.cloud.pull.warn': boolean;
'arduino.cloud.push.warn': boolean; 'arduino.cloud.push.warn': boolean;
'arduino.cloud.pushpublic.warn': boolean; 'arduino.cloud.pushpublic.warn': boolean;
'arduino.cloud.sketchSyncEnpoint': string; 'arduino.cloud.sketchSyncEnpoint': string;
'arduino.auth.clientID': string; 'arduino.auth.clientID': string;
'arduino.auth.domain': string; 'arduino.auth.domain': string;
'arduino.auth.audience': string; 'arduino.auth.audience': string;
'arduino.auth.registerUri': string; 'arduino.auth.registerUri': string;
} }
export const ArduinoPreferences = Symbol('ArduinoPreferences'); export const ArduinoPreferences = Symbol('ArduinoPreferences');
export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>; export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
export function createArduinoPreferences( export function createArduinoPreferences(
preferences: PreferenceService preferences: PreferenceService
): ArduinoPreferences { ): ArduinoPreferences {
return createPreferenceProxy(preferences, ArduinoConfigSchema); return createPreferenceProxy(preferences, ArduinoConfigSchema);
} }
export function bindArduinoPreferences(bind: interfaces.Bind): void { export function bindArduinoPreferences(bind: interfaces.Bind): void {
bind(ArduinoPreferences).toDynamicValue((ctx) => { bind(ArduinoPreferences).toDynamicValue((ctx) => {
const preferences = const preferences = ctx.container.get<PreferenceService>(PreferenceService);
ctx.container.get<PreferenceService>(PreferenceService); return createArduinoPreferences(preferences);
return createArduinoPreferences(preferences); });
}); bind(PreferenceContribution).toConstantValue({
bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema,
schema: ArduinoConfigSchema, });
});
} }

View File

@@ -20,54 +20,54 @@ import { MaybePromise } from '@theia/core/lib/common/types';
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`. * - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
*/ */
namespace ArduinoWorkspaceRootResolver { namespace ArduinoWorkspaceRootResolver {
export interface InitOptions { export interface InitOptions {
readonly isValid: (uri: string) => MaybePromise<boolean>; readonly isValid: (uri: string) => MaybePromise<boolean>;
} }
export interface ResolveOptions { export interface ResolveOptions {
readonly hash?: string; readonly hash?: string;
readonly recentWorkspaces: string[]; readonly recentWorkspaces: string[];
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI. // Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
readonly recentSketches: string[]; readonly recentSketches: string[];
} }
} }
export class ArduinoWorkspaceRootResolver { export class ArduinoWorkspaceRootResolver {
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {} constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {}
async resolve( async resolve(
options: ArduinoWorkspaceRootResolver.ResolveOptions options: ArduinoWorkspaceRootResolver.ResolveOptions
): Promise<{ uri: string } | undefined> { ): Promise<{ uri: string } | undefined> {
const { hash, recentWorkspaces, recentSketches } = options; const { hash, recentWorkspaces, recentSketches } = options;
for (const uri of [ for (const uri of [
this.hashToUri(hash), this.hashToUri(hash),
...recentWorkspaces, ...recentWorkspaces,
...recentSketches, ...recentSketches,
].filter(notEmpty)) { ].filter(notEmpty)) {
const valid = await this.isValid(uri); const valid = await this.isValid(uri);
if (valid) { if (valid) {
return { uri }; return { uri };
} }
}
return undefined;
} }
return undefined;
}
protected isValid(uri: string): MaybePromise<boolean> { protected isValid(uri: string): MaybePromise<boolean> {
return this.options.isValid(uri); return this.options.isValid(uri);
} }
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first. // Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
// This is important for Windows only and a NOOP on POSIX. // This is important for Windows only and a NOOP on POSIX.
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See: // Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423 // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
protected hashToUri(hash: string | undefined): string | undefined { protected hashToUri(hash: string | undefined): string | undefined {
if (hash && hash.length > 1 && hash.startsWith('#')) { if (hash && hash.length > 1 && hash.startsWith('#')) {
const path = hash.slice(1); // Trim the leading `#`. const path = hash.slice(1); // Trim the leading `#`.
return new URI( return new URI(
toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)) toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))
) )
.withScheme('file') .withScheme('file')
.toString(); .toString();
}
return undefined;
} }
return undefined;
}
} }

View File

@@ -5,13 +5,13 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
CommandRegistry, CommandRegistry,
CommandContribution, CommandContribution,
} from '@theia/core/lib/common/command'; } from '@theia/core/lib/common/command';
import { import {
AuthenticationService, AuthenticationService,
AuthenticationServiceClient, AuthenticationServiceClient,
AuthenticationSession, AuthenticationSession,
} from '../../common/protocol/authentication-service'; } from '../../common/protocol/authentication-service';
import { CloudUserCommands } from './cloud-user-commands'; import { CloudUserCommands } from './cloud-user-commands';
import { serverPort } from '../../node/auth/authentication-server'; import { serverPort } from '../../node/auth/authentication-server';
@@ -20,74 +20,74 @@ import { ArduinoPreferences } from '../arduino-preferences';
@injectable() @injectable()
export class AuthenticationClientService export class AuthenticationClientService
implements implements
FrontendApplicationContribution, FrontendApplicationContribution,
CommandContribution, CommandContribution,
AuthenticationServiceClient AuthenticationServiceClient
{ {
@inject(AuthenticationService) @inject(AuthenticationService)
protected readonly service: JsonRpcProxy<AuthenticationService>; protected readonly service: JsonRpcProxy<AuthenticationService>;
@inject(WindowService) @inject(WindowService)
protected readonly windowService: WindowService; protected readonly windowService: WindowService;
@inject(ArduinoPreferences) @inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences; protected readonly arduinoPreferences: ArduinoPreferences;
protected authOptions: AuthOptions; protected authOptions: AuthOptions;
protected _session: AuthenticationSession | undefined; protected _session: AuthenticationSession | undefined;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
protected readonly onSessionDidChangeEmitter = new Emitter< protected readonly onSessionDidChangeEmitter = new Emitter<
AuthenticationSession | undefined AuthenticationSession | undefined
>(); >();
readonly onSessionDidChange = this.onSessionDidChangeEmitter.event; readonly onSessionDidChange = this.onSessionDidChangeEmitter.event;
onStart(): void { onStart(): void {
this.toDispose.push(this.onSessionDidChangeEmitter); this.toDispose.push(this.onSessionDidChangeEmitter);
this.service.setClient(this); this.service.setClient(this);
this.service this.service
.session() .session()
.then((session) => this.notifySessionDidChange(session)); .then((session) => this.notifySessionDidChange(session));
this.setOptions();
this.arduinoPreferences.onPreferenceChanged((event) => {
if (event.preferenceName.startsWith('arduino.auth.')) {
this.setOptions(); this.setOptions();
this.arduinoPreferences.onPreferenceChanged((event) => { }
if (event.preferenceName.startsWith('arduino.auth.')) { });
this.setOptions(); }
}
});
}
setOptions(): void { setOptions(): void {
this.service.setOptions({ this.service.setOptions({
redirectUri: `http://localhost:${serverPort}/callback`, redirectUri: `http://localhost:${serverPort}/callback`,
responseType: 'code', responseType: 'code',
clientID: this.arduinoPreferences['arduino.auth.clientID'], clientID: this.arduinoPreferences['arduino.auth.clientID'],
domain: this.arduinoPreferences['arduino.auth.domain'], domain: this.arduinoPreferences['arduino.auth.domain'],
audience: this.arduinoPreferences['arduino.auth.audience'], audience: this.arduinoPreferences['arduino.auth.audience'],
registerUri: this.arduinoPreferences['arduino.auth.registerUri'], registerUri: this.arduinoPreferences['arduino.auth.registerUri'],
scopes: ['openid', 'profile', 'email', 'offline_access'], scopes: ['openid', 'profile', 'email', 'offline_access'],
}); });
} }
protected updateSession(session?: AuthenticationSession | undefined) { protected updateSession(session?: AuthenticationSession | undefined) {
this._session = session; this._session = session;
this.onSessionDidChangeEmitter.fire(this._session); this.onSessionDidChangeEmitter.fire(this._session);
} }
get session(): AuthenticationSession | undefined { get session(): AuthenticationSession | undefined {
return this._session; return this._session;
} }
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloudUserCommands.LOGIN, { registry.registerCommand(CloudUserCommands.LOGIN, {
execute: () => this.service.login(), execute: () => this.service.login(),
}); });
registry.registerCommand(CloudUserCommands.LOGOUT, { registry.registerCommand(CloudUserCommands.LOGOUT, {
execute: () => this.service.logout(), execute: () => this.service.logout(),
}); });
} }
notifySessionDidChange(session: AuthenticationSession | undefined): void { notifySessionDidChange(session: AuthenticationSession | undefined): void {
this.updateSession(session); this.updateSession(session);
} }
} }

View File

@@ -1,18 +1,18 @@
import { Command } from '@theia/core/lib/common/command'; import { Command } from '@theia/core/lib/common/command';
export namespace CloudUserCommands { export namespace CloudUserCommands {
export const LOGIN: Command = { export const LOGIN: Command = {
id: 'arduino-cloud--login', id: 'arduino-cloud--login',
label: 'Sign in', label: 'Sign in',
}; };
export const LOGOUT: Command = { export const LOGOUT: Command = {
id: 'arduino-cloud--logout', id: 'arduino-cloud--logout',
label: 'Sign Out', label: 'Sign Out',
}; };
export const OPEN_PROFILE_CONTEXT_MENU: Command = { export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu', id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu', label: 'Contextual menu',
}; };
} }

View File

@@ -2,9 +2,9 @@ import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
BoardsService, BoardsService,
BoardsPackage, BoardsPackage,
Board, Board,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
@@ -18,111 +18,103 @@ import { ResponseServiceImpl } from '../response-service-impl';
*/ */
@injectable() @injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution { export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(MessageService) @inject(MessageService)
protected readonly messageService: MessageService; protected readonly messageService: MessageService;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ResponseServiceImpl) @inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl; protected readonly responseService: ResponseServiceImpl;
@inject(BoardsListWidgetFrontendContribution) @inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution; protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
// Workaround for https://github.com/eclipse-theia/theia/issues/9349 // Workaround for https://github.com/eclipse-theia/theia/issues/9349
protected notifications: Board[] = []; protected notifications: Board[] = [];
onStart(): void { onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged( this.boardsServiceClient.onBoardsConfigChanged(
this.ensureCoreExists.bind(this) this.ensureCoreExists.bind(this)
);
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (
selectedBoard &&
!this.notifications.find((board) => Board.sameAs(board, selectedBoard))
) {
this.notifications.push(selectedBoard);
this.boardsService.search({}).then((packages) => {
// filter packagesForBoard selecting matches from the cli (installed packages)
// and matches based on the board name
// NOTE: this ensures the Deprecated & new packages are all in the array
// so that we can check if any of the valid packages is already installed
const packagesForBoard = packages.filter(
(pkg) =>
BoardsPackage.contains(selectedBoard, pkg) ||
pkg.boards.some((board) => board.name === selectedBoard.name)
); );
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void { // check if one of the packages for the board is already installed. if so, no hint
const { selectedBoard } = config;
if ( if (
selectedBoard && packagesForBoard.some(({ installedVersion }) => !!installedVersion)
!this.notifications.find((board) =>
Board.sameAs(board, selectedBoard)
)
) { ) {
this.notifications.push(selectedBoard); return;
this.boardsService.search({}).then((packages) => { }
// filter packagesForBoard selecting matches from the cli (installed packages)
// and matches based on the board name
// NOTE: this ensures the Deprecated & new packages are all in the array
// so that we can check if any of the valid packages is already installed
const packagesForBoard = packages.filter(
(pkg) =>
BoardsPackage.contains(selectedBoard, pkg) ||
pkg.boards.some(
(board) => board.name === selectedBoard.name
)
);
// check if one of the packages for the board is already installed. if so, no hint // filter the installable (not installed) packages,
if ( // CLI returns the packages already sorted with the deprecated ones at the end of the list
packagesForBoard.some( // in order to ensure the new ones are preferred
({ installedVersion }) => !!installedVersion const candidates = packagesForBoard.filter(
) ({ installable, installedVersion }) =>
) { installable && !installedVersion
return; );
}
// filter the installable (not installed) packages, const candidate = candidates[0];
// CLI returns the packages already sorted with the deprecated ones at the end of the list if (candidate) {
// in order to ensure the new ones are preferred const version = candidate.availableVersions[0]
const candidates = packagesForBoard.filter( ? `[v ${candidate.availableVersions[0]}]`
({ installable, installedVersion }) => : '';
installable && !installedVersion // tslint:disable-next-line:max-line-length
); this.messageService
.info(
const candidate = candidates[0]; `The \`"${candidate.name} ${version}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`,
if (candidate) { 'Install Manually',
const version = candidate.availableVersions[0] 'Yes'
? `[v ${candidate.availableVersions[0]}]` )
: ''; .then(async (answer) => {
// tslint:disable-next-line:max-line-length const index = this.notifications.findIndex((board) =>
this.messageService Board.sameAs(board, selectedBoard)
.info( );
`The \`"${candidate.name} ${version}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, if (index !== -1) {
'Install Manually', this.notifications.splice(index, 1);
'Yes' }
) if (answer === 'Yes') {
.then(async (answer) => { await Installable.installWithProgress({
const index = this.notifications.findIndex( installable: this.boardsService,
(board) => Board.sameAs(board, selectedBoard) item: candidate,
); messageService: this.messageService,
if (index !== -1) { responseService: this.responseService,
this.notifications.splice(index, 1); version: candidate.availableVersions[0],
} });
if (answer === 'Yes') { return;
await Installable.installWithProgress({ }
installable: this.boardsService, if (answer) {
item: candidate, this.boardsManagerFrontendContribution
messageService: this.messageService, .openView({ reveal: true })
responseService: this.responseService, .then((widget) =>
version: candidate.availableVersions[0], widget.refresh(candidate.name.toLocaleLowerCase())
}); );
return; }
}
if (answer) {
this.boardsManagerFrontendContribution
.openView({ reveal: true })
.then((widget) =>
widget.refresh(
candidate.name.toLocaleLowerCase()
)
);
}
});
}
}); });
} }
});
} }
}
} }

View File

@@ -9,64 +9,62 @@ import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class BoardsConfigDialogWidget extends ReactWidget { export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>(); protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
protected readonly onBoardConfigChangedEmitter = protected readonly onBoardConfigChangedEmitter =
new Emitter<BoardsConfig.Config>(); new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event; readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
protected focusNode: HTMLElement | undefined; protected focusNode: HTMLElement | undefined;
constructor() { constructor() {
super(); super();
this.id = 'select-board-dialog'; this.id = 'select-board-dialog';
this.toDispose.pushAll([ this.toDispose.pushAll([
this.onBoardConfigChangedEmitter, this.onBoardConfigChangedEmitter,
this.onFilterTextDidChangeEmitter, this.onFilterTextDidChangeEmitter,
]); ]);
} }
search(query: string): void { search(query: string): void {
this.onFilterTextDidChangeEmitter.fire(query); this.onFilterTextDidChangeEmitter.fire(query);
} }
protected fireConfigChanged = (config: BoardsConfig.Config) => { protected fireConfigChanged = (config: BoardsConfig.Config) => {
this.onBoardConfigChangedEmitter.fire(config); this.onBoardConfigChangedEmitter.fire(config);
}; };
protected setFocusNode = (element: HTMLElement | undefined) => { protected setFocusNode = (element: HTMLElement | undefined) => {
this.focusNode = element; this.focusNode = element;
}; };
protected render(): React.ReactNode { protected render(): React.ReactNode {
return ( return (
<div className="selectBoardContainer"> <div className="selectBoardContainer">
<BoardsConfig <BoardsConfig
boardsServiceProvider={this.boardsServiceClient} boardsServiceProvider={this.boardsServiceClient}
notificationCenter={this.notificationCenter} notificationCenter={this.notificationCenter}
onConfigChange={this.fireConfigChanged} onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} onFocusNodeSet={this.setFocusNode}
onFilteredTextDidChangeEvent={ onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event}
this.onFilterTextDidChangeEmitter.event />
} </div>
/> );
</div> }
);
} protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
protected onActivateRequest(msg: Message): void { if (this.focusNode instanceof HTMLInputElement) {
super.onActivateRequest(msg); this.focusNode.select();
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
} }
(this.focusNode || this.node).focus();
}
} }

View File

@@ -1,10 +1,10 @@
import { injectable, inject, postConstruct } from 'inversify'; import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging'; import { Message } from '@phosphor/messaging';
import { import {
AbstractDialog, AbstractDialog,
DialogProps, DialogProps,
Widget, Widget,
DialogError, DialogError,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { BoardsConfig } from './boards-config'; import { BoardsConfig } from './boards-config';
import { BoardsService } from '../../common/protocol/boards-service'; import { BoardsService } from '../../common/protocol/boards-service';
@@ -16,119 +16,119 @@ export class BoardsConfigDialogProps extends DialogProps {}
@injectable() @injectable()
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> { export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
@inject(BoardsConfigDialogWidget) @inject(BoardsConfigDialogWidget)
protected readonly widget: BoardsConfigDialogWidget; protected readonly widget: BoardsConfigDialogWidget;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardService: BoardsService; protected readonly boardService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
protected config: BoardsConfig.Config = {}; protected config: BoardsConfig.Config = {};
constructor( constructor(
@inject(BoardsConfigDialogProps) @inject(BoardsConfigDialogProps)
protected readonly props: BoardsConfigDialogProps protected readonly props: BoardsConfigDialogProps
) { ) {
super(props); super(props);
this.contentNode.classList.add('select-board-dialog'); this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription()); this.contentNode.appendChild(this.createDescription());
this.appendCloseButton('CANCEL'); this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK'); this.appendAcceptButton('OK');
} }
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.toDispose.push( this.toDispose.push(
this.boardsServiceClient.onBoardsConfigChanged((config) => { this.boardsServiceClient.onBoardsConfigChanged((config) => {
this.config = config; this.config = config;
this.update();
})
);
}
/**
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
*/
async open(
query: string | undefined = undefined
): Promise<BoardsConfig.Config | undefined> {
if (typeof query === 'string') {
this.widget.search(query);
}
return super.open();
}
protected createDescription(): HTMLElement {
const head = document.createElement('div');
head.classList.add('head');
const title = document.createElement('div');
title.textContent = 'Select Other Board & Port';
title.classList.add('title');
head.appendChild(title);
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
for (const paragraph of [
'Select both a Board and a Port if you want to upload a sketch.',
'If you only select a Board you will be able just to compile, but not to upload your sketch.',
]) {
const p = document.createElement('div');
p.textContent = paragraph;
text.appendChild(p);
}
return head;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(
this.widget.onBoardConfigChanged((config) => {
this.config = config;
this.update();
})
);
super.onAfterAttach(msg);
this.update(); this.update();
})
);
}
/**
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
*/
async open(
query: string | undefined = undefined
): Promise<BoardsConfig.Config | undefined> {
if (typeof query === 'string') {
this.widget.search(query);
}
return super.open();
}
protected createDescription(): HTMLElement {
const head = document.createElement('div');
head.classList.add('head');
const title = document.createElement('div');
title.textContent = 'Select Other Board & Port';
title.classList.add('title');
head.appendChild(title);
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
for (const paragraph of [
'Select both a Board and a Port if you want to upload a sketch.',
'If you only select a Board you will be able just to compile, but not to upload your sketch.',
]) {
const p = document.createElement('div');
p.textContent = paragraph;
text.appendChild(p);
} }
protected onUpdateRequest(msg: Message) { return head;
super.onUpdateRequest(msg); }
this.widget.update();
}
protected onActivateRequest(msg: Message): void { protected onAfterAttach(msg: Message): void {
super.onActivateRequest(msg); if (this.widget.isAttached) {
this.widget.activate(); Widget.detach(this.widget);
} }
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(
this.widget.onBoardConfigChanged((config) => {
this.config = config;
this.update();
})
);
super.onAfterAttach(msg);
this.update();
}
protected handleEnter(event: KeyboardEvent): boolean | void { protected onUpdateRequest(msg: Message) {
if (event.target instanceof HTMLTextAreaElement) { super.onUpdateRequest(msg);
return false; this.widget.update();
} }
}
protected isValid(value: BoardsConfig.Config): DialogError { protected onActivateRequest(msg: Message): void {
if (!value.selectedBoard) { super.onActivateRequest(msg);
if (value.selectedPort) { this.widget.activate();
return 'Please pick a board connected to the port you have selected.'; }
}
return false;
}
return '';
}
get value(): BoardsConfig.Config { protected handleEnter(event: KeyboardEvent): boolean | void {
return this.config; if (event.target instanceof HTMLTextAreaElement) {
return false;
} }
}
protected isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return 'Please pick a board connected to the port you have selected.';
}
return false;
}
return '';
}
get value(): BoardsConfig.Config {
return this.config;
}
} }

View File

@@ -4,408 +4,396 @@ import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { import {
Board, Board,
Port, Port,
AttachedBoardsChangeEvent, AttachedBoardsChangeEvent,
BoardWithPackage, BoardWithPackage,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsServiceProvider } from './boards-service-provider';
export namespace BoardsConfig { export namespace BoardsConfig {
export interface Config { export interface Config {
selectedBoard?: Board; selectedBoard?: Board;
selectedPort?: Port; selectedPort?: Port;
} }
export interface Props { export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider; readonly boardsServiceProvider: BoardsServiceProvider;
readonly notificationCenter: NotificationCenter; readonly notificationCenter: NotificationCenter;
readonly onConfigChange: (config: Config) => void; readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void; readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
readonly onFilteredTextDidChangeEvent: Event<string>; readonly onFilteredTextDidChangeEvent: Event<string>;
} }
export interface State extends Config { export interface State extends Config {
searchResults: Array<BoardWithPackage>; searchResults: Array<BoardWithPackage>;
knownPorts: Port[]; knownPorts: Port[];
showAllPorts: boolean; showAllPorts: boolean;
query: string; query: string;
} }
} }
export abstract class Item<T> extends React.Component<{ export abstract class Item<T> extends React.Component<{
item: T; item: T;
label: string; label: string;
selected: boolean; selected: boolean;
onClick: (item: T) => void; onClick: (item: T) => void;
missing?: boolean; missing?: boolean;
details?: string; details?: string;
}> { }> {
render(): React.ReactNode { render(): React.ReactNode {
const { selected, label, missing, details } = this.props; const { selected, label, missing, details } = this.props;
const classNames = ['item']; const classNames = ['item'];
if (selected) { if (selected) {
classNames.push('selected'); classNames.push('selected');
}
if (missing === true) {
classNames.push('missing');
}
return (
<div
onClick={this.onClick}
className={classNames.join(' ')}
title={`${label}${!details ? '' : details}`}
>
<div className="label">{label}</div>
{!details ? '' : <div className="details">{details}</div>}
{!selected ? (
''
) : (
<div className="selected-icon">
<i className="fa fa-check" />
</div>
)}
</div>
);
} }
if (missing === true) {
classNames.push('missing');
}
return (
<div
onClick={this.onClick}
className={classNames.join(' ')}
title={`${label}${!details ? '' : details}`}
>
<div className="label">{label}</div>
{!details ? '' : <div className="details">{details}</div>}
{!selected ? (
''
) : (
<div className="selected-icon">
<i className="fa fa-check" />
</div>
)}
</div>
);
}
protected onClick = () => { protected onClick = () => {
this.props.onClick(this.props.item); this.props.onClick(this.props.item);
}; };
} }
export class BoardsConfig extends React.Component< export class BoardsConfig extends React.Component<
BoardsConfig.Props, BoardsConfig.Props,
BoardsConfig.State BoardsConfig.State
> { > {
protected toDispose = new DisposableCollection(); protected toDispose = new DisposableCollection();
constructor(props: BoardsConfig.Props) { constructor(props: BoardsConfig.Props) {
super(props); super(props);
const { boardsConfig } = props.boardsServiceProvider; const { boardsConfig } = props.boardsServiceProvider;
this.state = { this.state = {
searchResults: [], searchResults: [],
knownPorts: [], knownPorts: [],
showAllPorts: false, showAllPorts: false,
query: '', query: '',
...boardsConfig, ...boardsConfig,
}; };
} }
componentDidMount() { componentDidMount() {
this.updateBoards(); this.updateBoards();
this.updatePorts(
this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty)
);
this.toDispose.pushAll([
this.props.notificationCenter.onAttachedBoardsChanged((event) =>
this.updatePorts( this.updatePorts(
this.props.boardsServiceProvider.availableBoards event.newState.ports,
.map(({ port }) => port) AttachedBoardsChangeEvent.diff(event).detached.ports
.filter(notEmpty) )
); ),
this.toDispose.pushAll([ this.props.boardsServiceProvider.onBoardsConfigChanged(
this.props.notificationCenter.onAttachedBoardsChanged((event) => ({ selectedBoard, selectedPort }) => {
this.updatePorts( this.setState({ selectedBoard, selectedPort }, () =>
event.newState.ports, this.fireConfigChanged()
AttachedBoardsChangeEvent.diff(event).detached.ports );
)
),
this.props.boardsServiceProvider.onBoardsConfigChanged(
({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () =>
this.fireConfigChanged()
);
}
),
this.props.notificationCenter.onPlatformInstalled(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformUninstalled(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdated(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStarted(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStopped(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) =>
this.setState({ query }, () =>
this.updateBoards(this.state.query)
)
),
]);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
protected updateBoards = (
eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
) => {
const query =
typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase();
this.setState({ query });
this.queryBoards({ query }).then((searchResults) =>
this.setState({ searchResults })
);
};
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
let { selectedPort } = this.state;
// If the currently selected port is not available anymore, unset the selected port.
if (removedPorts.some((port) => Port.equals(port, selectedPort))) {
selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () =>
this.fireConfigChanged()
);
});
};
protected queryBoards = (
options: { query?: string } = {}
): Promise<Array<BoardWithPackage>> => {
return this.props.boardsServiceProvider.searchBoards(options);
};
protected get availablePorts(): MaybePromise<Port[]> {
return this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty);
}
protected queryPorts = async (
availablePorts: MaybePromise<Port[]> = this.availablePorts
) => {
const ports = await availablePorts;
return { knownPorts: ports.sort(Port.compare) };
};
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
};
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
};
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
};
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
};
render(): React.ReactNode {
return (
<div className="body">
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</div>
);
}
protected renderContainer(
title: string,
contentRenderer: () => React.ReactNode,
footerRenderer?: () => React.ReactNode
): React.ReactNode {
return (
<div className="container">
<div className="content">
<div className="title">{title}</div>
{contentRenderer()}
<div className="footer">
{footerRenderer ? footerRenderer() : ''}
</div>
</div>
</div>
);
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults, query } = this.state;
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
!!fqbn
? `${name}-${packageName}-${fqbn}`
: `${name}-${packageName}`;
for (const board of Board.decorateBoards(
selectedBoard,
searchResults
)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
} }
),
this.props.notificationCenter.onPlatformInstalled(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onPlatformUninstalled(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexUpdated(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStarted(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonStopped(() =>
this.setState({ searchResults: [] })
),
this.props.onFilteredTextDidChangeEvent((query) =>
this.setState({ query }, () => this.updateBoards(this.state.query))
),
]);
}
return ( componentWillUnmount(): void {
<React.Fragment> this.toDispose.dispose();
<div className="search"> }
<input
type="search" protected fireConfigChanged() {
value={query} const { selectedBoard, selectedPort } = this.state;
className="theia-input" this.props.onConfigChange({ selectedBoard, selectedPort });
placeholder="SEARCH BOARD" }
onChange={this.updateBoards}
ref={this.focusNodeSet} protected updateBoards = (
/> eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
<i className="fa fa-search"></i> ) => {
</div> const query =
<div className="boards list"> typeof eventOrQuery === 'string'
{Array.from(distinctBoards.values()).map((board) => ( ? eventOrQuery
<Item<BoardWithPackage> : eventOrQuery.target.value.toLowerCase();
key={`${board.name}-${board.packageName}`} this.setState({ query });
item={board} this.queryBoards({ query }).then((searchResults) =>
label={board.name} this.setState({ searchResults })
details={board.details} );
selected={board.selected} };
onClick={this.selectBoard}
missing={board.missing} protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
/> this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
))} let { selectedPort } = this.state;
</div> // If the currently selected port is not available anymore, unset the selected port.
</React.Fragment> if (removedPorts.some((port) => Port.equals(port, selectedPort))) {
); selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () =>
this.fireConfigChanged()
);
});
};
protected queryBoards = (
options: { query?: string } = {}
): Promise<Array<BoardWithPackage>> => {
return this.props.boardsServiceProvider.searchBoards(options);
};
protected get availablePorts(): MaybePromise<Port[]> {
return this.props.boardsServiceProvider.availableBoards
.map(({ port }) => port)
.filter(notEmpty);
}
protected queryPorts = async (
availablePorts: MaybePromise<Port[]> = this.availablePorts
) => {
const ports = await availablePorts;
return { knownPorts: ports.sort(Port.compare) };
};
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
};
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
};
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
};
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
};
render(): React.ReactNode {
return (
<div className="body">
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
</div>
);
}
protected renderContainer(
title: string,
contentRenderer: () => React.ReactNode,
footerRenderer?: () => React.ReactNode
): React.ReactNode {
return (
<div className="container">
<div className="content">
<div className="title">{title}</div>
{contentRenderer()}
<div className="footer">{footerRenderer ? footerRenderer() : ''}</div>
</div>
</div>
);
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults, query } = this.state;
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
// It is tricky when the core is not yet installed, no FQBNs are available.
const distinctBoards = new Map<string, Board.Detailed>();
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
!!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
const key = toKey(board);
if (!distinctBoards.has(key)) {
distinctBoards.set(key, board);
}
} }
protected renderPorts(): React.ReactNode { return (
const filter = this.state.showAllPorts ? () => true : Port.isBoardPort; <React.Fragment>
const ports = this.state.knownPorts.filter(filter); <div className="search">
return !ports.length ? ( <input
<div className="loading noselect">No ports discovered</div> type="search"
) : ( value={query}
<div className="ports list"> className="theia-input"
{ports.map((port) => ( placeholder="SEARCH BOARD"
<Item<Port> onChange={this.updateBoards}
key={Port.toString(port)} ref={this.focusNodeSet}
item={port} />
label={Port.toString(port)} <i className="fa fa-search"></i>
selected={Port.equals(this.state.selectedPort, port)} </div>
onClick={this.selectPort} <div className="boards list">
/> {Array.from(distinctBoards.values()).map((board) => (
))} <Item<BoardWithPackage>
</div> key={`${board.name}-${board.packageName}`}
); item={board}
} label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
))}
</div>
</React.Fragment>
);
}
protected renderPortsFooter(): React.ReactNode { protected renderPorts(): React.ReactNode {
return ( const filter = this.state.showAllPorts ? () => true : Port.isBoardPort;
<div className="noselect"> const ports = this.state.knownPorts.filter(filter);
<label title="Shows all available ports when enabled"> return !ports.length ? (
<input <div className="loading noselect">No ports discovered</div>
type="checkbox" ) : (
defaultChecked={this.state.showAllPorts} <div className="ports list">
onChange={this.toggleFilterPorts} {ports.map((port) => (
/> <Item<Port>
<span>Show all ports</span> key={Port.toString(port)}
</label> item={port}
</div> label={Port.toString(port)}
); selected={Port.equals(this.state.selectedPort, port)}
} onClick={this.selectPort}
/>
))}
</div>
);
}
protected renderPortsFooter(): React.ReactNode {
return (
<div className="noselect">
<label title="Shows all available ports when enabled">
<input
type="checkbox"
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>Show all ports</span>
</label>
</div>
);
}
} }
export namespace BoardsConfig { export namespace BoardsConfig {
export namespace Config { export namespace Config {
export function sameAs(config: Config, other: Config | Board): boolean { export function sameAs(config: Config, other: Config | Board): boolean {
const { selectedBoard, selectedPort } = config; const { selectedBoard, selectedPort } = config;
if (Board.is(other)) { if (Board.is(other)) {
return ( return (
!!selectedBoard && !!selectedBoard &&
Board.equals(other, selectedBoard) && Board.equals(other, selectedBoard) &&
Port.sameAs(selectedPort, other.port) Port.sameAs(selectedPort, other.port)
); );
} }
return sameAs(config, other); return sameAs(config, other);
}
export function equals(left: Config, right: Config): boolean {
return (
left.selectedBoard === right.selectedBoard &&
left.selectedPort === right.selectedPort
);
}
export function toString(
config: Config,
options: { default: string } = { default: '' }
): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
}
export function setConfig(
config: Config | undefined,
urlToAttachTo: URL
): URL {
const copy = new URL(urlToAttachTo.toString());
if (!config) {
copy.searchParams.delete('boards-config');
return copy;
}
const selectedBoard = config.selectedBoard
? {
name: config.selectedBoard.name,
fqbn: config.selectedBoard.fqbn,
}
: undefined;
const selectedPort = config.selectedPort
? {
protocol: config.selectedPort.protocol,
address: config.selectedPort.address,
}
: undefined;
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
copy.searchParams.set(
'boards-config',
encodeURIComponent(jsonConfig)
);
return copy;
}
export function getConfig(url: URL): Config | undefined {
const encoded = url.searchParams.get('boards-config');
if (!encoded) {
return undefined;
}
try {
const raw = decodeURIComponent(encoded);
const candidate = JSON.parse(raw);
if (typeof candidate === 'object') {
return candidate;
}
console.warn(
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
);
return undefined;
} catch (e) {
console.log(`Could not get board config from URL: ${url}.`, e);
return undefined;
}
}
} }
export function equals(left: Config, right: Config): boolean {
return (
left.selectedBoard === right.selectedBoard &&
left.selectedPort === right.selectedPort
);
}
export function toString(
config: Config,
options: { default: string } = { default: '' }
): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
}
export function setConfig(
config: Config | undefined,
urlToAttachTo: URL
): URL {
const copy = new URL(urlToAttachTo.toString());
if (!config) {
copy.searchParams.delete('boards-config');
return copy;
}
const selectedBoard = config.selectedBoard
? {
name: config.selectedBoard.name,
fqbn: config.selectedBoard.fqbn,
}
: undefined;
const selectedPort = config.selectedPort
? {
protocol: config.selectedPort.protocol,
address: config.selectedPort.address,
}
: undefined;
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
return copy;
}
export function getConfig(url: URL): Config | undefined {
const encoded = url.searchParams.get('boards-config');
if (!encoded) {
return undefined;
}
try {
const raw = decodeURIComponent(encoded);
const candidate = JSON.parse(raw);
if (typeof candidate === 'object') {
return candidate;
}
console.warn(
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
);
return undefined;
} catch (e) {
console.log(`Could not get board config from URL: ${url}.`, e);
return undefined;
}
}
}
} }

View File

@@ -3,8 +3,8 @@ import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command'; import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { BoardsServiceProvider } from './boards-service-provider'; import { BoardsServiceProvider } from './boards-service-provider';
import { Board, ConfigOption, Programmer } from '../../common/protocol'; import { Board, ConfigOption, Programmer } from '../../common/protocol';
@@ -15,178 +15,145 @@ import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
@injectable() @injectable()
export class BoardsDataMenuUpdater implements FrontendApplicationContribution { export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(BoardsDataStore) @inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDisposeOnBoardChange = new DisposableCollection(); protected readonly toDisposeOnBoardChange = new DisposableCollection();
async onStart(): Promise<void> { async onStart(): Promise<void> {
this.updateMenuActions( this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
this.boardsServiceClient.boardsConfig.selectedBoard this.boardsDataStore.onChanged(() =>
); this.updateMenuActions(
this.boardsDataStore.onChanged(() => this.boardsServiceClient.boardsConfig.selectedBoard
this.updateMenuActions( )
this.boardsServiceClient.boardsConfig.selectedBoard );
) this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
); this.updateMenuActions(selectedBoard)
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => );
this.updateMenuActions(selectedBoard) }
);
}
protected async updateMenuActions( protected async updateMenuActions(
selectedBoard: Board | undefined selectedBoard: Board | undefined
): Promise<void> { ): Promise<void> {
return this.queue.add(async () => { return this.queue.add(async () => {
this.toDisposeOnBoardChange.dispose(); this.toDisposeOnBoardChange.dispose();
this.mainMenuManager.update(); this.mainMenuManager.update();
if (selectedBoard) { if (selectedBoard) {
const { fqbn } = selectedBoard; const { fqbn } = selectedBoard;
if (fqbn) { if (fqbn) {
const { configOptions, programmers, selectedProgrammer } = const { configOptions, programmers, selectedProgrammer } =
await this.boardsDataStore.getData(fqbn); await this.boardsDataStore.getData(fqbn);
if (configOptions.length) { if (configOptions.length) {
const boardsConfigMenuPath = [ const boardsConfigMenuPath = [
...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, ...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP,
'z01_boardsConfig', 'z01_boardsConfig',
]; // `z_` is for ordering. ]; // `z_` is for ordering.
for (const { for (const { label, option, values } of configOptions.sort(
label, ConfigOption.LABEL_COMPARATOR
option, )) {
values, const menuPath = [...boardsConfigMenuPath, `${option}`];
} of configOptions.sort( const commands = new Map<
ConfigOption.LABEL_COMPARATOR string,
)) { Disposable & { label: string }
const menuPath = [ >();
...boardsConfigMenuPath, for (const value of values) {
`${option}`, const id = `${fqbn}-${option}--${value.value}`;
]; const command = { id };
const commands = new Map< const selectedValue = value.value;
string, const handler = {
Disposable & { label: string } execute: () =>
>(); this.boardsDataStore.selectConfigOption({
for (const value of values) { fqbn,
const id = `${fqbn}-${option}--${value.value}`; option,
const command = { id }; selectedValue,
const selectedValue = value.value; }),
const handler = { isToggled: () => value.selected,
execute: () => };
this.boardsDataStore.selectConfigOption( commands.set(
{ fqbn, option, selectedValue } id,
), Object.assign(
isToggled: () => value.selected, this.commandRegistry.registerCommand(command, handler),
}; { label: value.label }
commands.set( )
id, );
Object.assign( }
this.commandRegistry.registerCommand( this.menuRegistry.registerSubmenu(menuPath, label);
command, this.toDisposeOnBoardChange.pushAll([
handler ...commands.values(),
), Disposable.create(() =>
{ label: value.label } unregisterSubmenu(menuPath, this.menuRegistry)
) ),
); ...Array.from(commands.keys()).map((commandId, i) => {
} const { label } = commands.get(commandId)!;
this.menuRegistry.registerSubmenu(menuPath, label); this.menuRegistry.registerMenuAction(menuPath, {
this.toDisposeOnBoardChange.pushAll([ commandId,
...commands.values(), order: `${i}`,
Disposable.create(() => label,
unregisterSubmenu( });
menuPath, return Disposable.create(() =>
this.menuRegistry this.menuRegistry.unregisterMenuAction(commandId)
) );
), }),
...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(() =>
unregisterSubmenu(
programmersMenuPath,
this.menuRegistry
)
)
);
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();
}
} }
}); }
} 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(() =>
unregisterSubmenu(programmersMenuPath, this.menuRegistry)
)
);
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();
}
}
});
}
} }

View File

@@ -4,277 +4,269 @@ import { deepClone } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { Event, Emitter } from '@theia/core/lib/common/event'; import { Event, Emitter } from '@theia/core/lib/common/event';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
LocalStorageService, LocalStorageService,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { notEmpty } from '../../common/utils'; import { notEmpty } from '../../common/utils';
import { import {
BoardsService, BoardsService,
ConfigOption, ConfigOption,
Installable, Installable,
BoardDetails, BoardDetails,
Programmer, Programmer,
} from '../../common/protocol'; } from '../../common/protocol';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class BoardsDataStore implements FrontendApplicationContribution { export class BoardsDataStore implements FrontendApplicationContribution {
@inject(ILogger) @inject(ILogger)
@named('store') @named('store')
protected readonly logger: ILogger; protected readonly logger: ILogger;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(LocalStorageService) @inject(LocalStorageService)
protected readonly storageService: LocalStorageService; protected readonly storageService: LocalStorageService;
protected readonly onChangedEmitter = new Emitter<void>(); protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void { onStart(): void {
this.notificationCenter.onPlatformInstalled(async ({ item }) => { this.notificationCenter.onPlatformInstalled(async ({ item }) => {
const { installedVersion: version } = item; const { installedVersion: version } = item;
if (!version) { if (!version) {
return; return;
} }
let shouldFireChanged = false; let shouldFireChanged = false;
for (const fqbn of item.boards for (const fqbn of item.boards
.map(({ fqbn }) => fqbn) .map(({ fqbn }) => fqbn)
.filter(notEmpty) .filter(notEmpty)
.filter((fqbn) => !!fqbn)) { .filter((fqbn) => !!fqbn)) {
const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<
ConfigOption[] | undefined
>(key);
if (!data || !data.length) {
const details = await this.getBoardDetailsSafe(fqbn);
if (details) {
data = details.configOptions;
if (data.length) {
await this.storageService.setData(key, data);
shouldFireChanged = true;
}
}
}
}
if (shouldFireChanged) {
this.fireChanged();
}
});
}
get onChanged(): Event<void> {
return this.onChangedEmitter.event;
}
async appendConfigToFqbn(
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<string | undefined> {
if (!fqbn) {
return undefined;
}
const { configOptions } = await this.getData(
fqbn,
boardsPackageVersion
);
return ConfigOption.decorate(fqbn, configOptions);
}
async getData(
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<BoardsDataStore.Data> {
if (!fqbn) {
return BoardsDataStore.Data.EMPTY;
}
const version = await boardsPackageVersion;
if (!version) {
return BoardsDataStore.Data.EMPTY;
}
const key = this.getStorageKey(fqbn, version); const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData< let data = await this.storageService.getData<
BoardsDataStore.Data | undefined ConfigOption[] | undefined
>(key, undefined); >(key);
if (BoardsDataStore.Data.is(data)) { if (!data || !data.length) {
return data; const details = await this.getBoardDetailsSafe(fqbn);
} if (details) {
data = details.configOptions;
const boardDetails = await this.getBoardDetailsSafe(fqbn); if (data.length) {
if (!boardDetails) { await this.storageService.setData(key, data);
return BoardsDataStore.Data.EMPTY; shouldFireChanged = true;
}
data = {
configOptions: boardDetails.configOptions,
programmers: boardDetails.programmers,
};
await this.storageService.setData(key, data);
return data;
}
async selectProgrammer(
{
fqbn,
selectedProgrammer,
}: { fqbn: string; selectedProgrammer: Programmer },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { programmers } = data;
if (
!programmers.find((p) => Programmer.equals(selectedProgrammer, p))
) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({
fqbn,
data: { ...data, selectedProgrammer },
version,
});
this.fireChanged();
return true;
}
async selectConfigOption(
{
fqbn,
option,
selectedValue,
}: { fqbn: string; option: string; selectedValue: string },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
if (!configOption) {
return false;
}
let updated = false;
for (const value of configOption.values) {
if (value.value === selectedValue) {
(value as any).selected = true;
updated = true;
} else {
(value as any).selected = false;
} }
}
} }
if (!updated) { }
return false; if (shouldFireChanged) {
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({ fqbn, data, version });
this.fireChanged(); this.fireChanged();
return true; }
});
}
get onChanged(): Event<void> {
return this.onChangedEmitter.event;
}
async appendConfigToFqbn(
fqbn: string | undefined,
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<string | undefined> {
if (!fqbn) {
return undefined;
} }
protected async setData({ const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
fqbn, return ConfigOption.decorate(fqbn, configOptions);
data, }
version,
}: { async getData(
fqbn: string; fqbn: string | undefined,
data: BoardsDataStore.Data; boardsPackageVersion: MaybePromise<
version: Installable.Version; Installable.Version | undefined
}): Promise<void> { > = this.getBoardsPackageVersion(fqbn)
const key = this.getStorageKey(fqbn, version); ): Promise<BoardsDataStore.Data> {
return this.storageService.setData(key, data); if (!fqbn) {
return BoardsDataStore.Data.EMPTY;
} }
protected getStorageKey( const version = await boardsPackageVersion;
fqbn: string, if (!version) {
version: Installable.Version return BoardsDataStore.Data.EMPTY;
): string { }
return `.arduinoIDE-configOptions-${version}-${fqbn}`; const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<
BoardsDataStore.Data | undefined
>(key, undefined);
if (BoardsDataStore.Data.is(data)) {
return data;
} }
protected async getBoardDetailsSafe( const boardDetails = await this.getBoardDetailsSafe(fqbn);
fqbn: string if (!boardDetails) {
): Promise<BoardDetails | undefined> { return BoardsDataStore.Data.EMPTY;
try {
const details = this.boardsService.getBoardDetails({ fqbn });
return details;
} catch (err) {
if (
err instanceof Error &&
err.message.includes('loading board data') &&
err.message.includes('is not installed')
) {
this.logger.warn(
`The boards package is not installed for board with FQBN: ${fqbn}`
);
} else {
this.logger.error(
`An unexpected error occurred while retrieving the board details for ${fqbn}.`,
err
);
}
return undefined;
}
} }
protected fireChanged(): void { data = {
this.onChangedEmitter.fire(); configOptions: boardDetails.configOptions,
programmers: boardDetails.programmers,
};
await this.storageService.setData(key, data);
return data;
}
async selectProgrammer(
{
fqbn,
selectedProgrammer,
}: { fqbn: string; selectedProgrammer: Programmer },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { programmers } = data;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
return false;
} }
protected async getBoardsPackageVersion( const version = await boardsPackageVersion;
fqbn: string | undefined if (!version) {
): Promise<Installable.Version | undefined> { return false;
if (!fqbn) { }
return undefined;
} await this.setData({
const boardsPackage = await this.boardsService.getContainerBoardPackage( fqbn,
{ fqbn } data: { ...data, selectedProgrammer },
version,
});
this.fireChanged();
return true;
}
async selectConfigOption(
{
fqbn,
option,
selectedValue,
}: { fqbn: string; option: string; selectedValue: string },
boardsPackageVersion: MaybePromise<
Installable.Version | undefined
> = this.getBoardsPackageVersion(fqbn)
): Promise<boolean> {
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
if (!configOption) {
return false;
}
let updated = false;
for (const value of configOption.values) {
if (value.value === selectedValue) {
(value as any).selected = true;
updated = true;
} else {
(value as any).selected = false;
}
}
if (!updated) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setData({ fqbn, data, version });
this.fireChanged();
return true;
}
protected async setData({
fqbn,
data,
version,
}: {
fqbn: string;
data: BoardsDataStore.Data;
version: Installable.Version;
}): Promise<void> {
const key = this.getStorageKey(fqbn, version);
return this.storageService.setData(key, data);
}
protected getStorageKey(fqbn: string, version: Installable.Version): string {
return `.arduinoIDE-configOptions-${version}-${fqbn}`;
}
protected async getBoardDetailsSafe(
fqbn: string
): Promise<BoardDetails | undefined> {
try {
const details = this.boardsService.getBoardDetails({ fqbn });
return details;
} catch (err) {
if (
err instanceof Error &&
err.message.includes('loading board data') &&
err.message.includes('is not installed')
) {
this.logger.warn(
`The boards package is not installed for board with FQBN: ${fqbn}`
); );
if (!boardsPackage) { } else {
return undefined; this.logger.error(
} `An unexpected error occurred while retrieving the board details for ${fqbn}.`,
return boardsPackage.installedVersion; err
);
}
return undefined;
} }
}
protected fireChanged(): void {
this.onChangedEmitter.fire();
}
protected async getBoardsPackageVersion(
fqbn: string | undefined
): Promise<Installable.Version | undefined> {
if (!fqbn) {
return undefined;
}
const boardsPackage = await this.boardsService.getContainerBoardPackage({
fqbn,
});
if (!boardsPackage) {
return undefined;
}
return boardsPackage.installedVersion;
}
} }
export namespace BoardsDataStore { export namespace BoardsDataStore {
export interface Data { export interface Data {
readonly configOptions: ConfigOption[]; readonly configOptions: ConfigOption[];
readonly programmers: Programmer[]; readonly programmers: Programmer[];
readonly selectedProgrammer?: Programmer; readonly selectedProgrammer?: Programmer;
} }
export namespace Data { export namespace Data {
export const EMPTY: Data = { export const EMPTY: Data = {
configOptions: [], configOptions: [],
programmers: [], programmers: [],
}; };
export function is(arg: any): arg is Data { export function is(arg: any): arg is Data {
return ( return (
!!arg && !!arg &&
'configOptions' in arg && 'configOptions' in arg &&
Array.isArray(arg['configOptions']) && Array.isArray(arg['configOptions']) &&
'programmers' in arg && 'programmers' in arg &&
Array.isArray(arg['programmers']) Array.isArray(arg['programmers'])
); );
}
} }
}
} }

View File

@@ -1,73 +1,73 @@
import { inject, injectable, postConstruct } from 'inversify'; import { inject, injectable, postConstruct } from 'inversify';
import { import {
BoardsPackage, BoardsPackage,
BoardsService, BoardsService,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { ListWidget } from '../widgets/component-list/list-widget'; import { ListWidget } from '../widgets/component-list/list-widget';
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
@injectable() @injectable()
export class BoardsListWidget extends ListWidget<BoardsPackage> { export class BoardsListWidget extends ListWidget<BoardsPackage> {
static WIDGET_ID = 'boards-list-widget'; static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager'; static WIDGET_LABEL = 'Boards Manager';
constructor( constructor(
@inject(BoardsService) protected service: BoardsService, @inject(BoardsService) protected service: BoardsService,
@inject(ListItemRenderer) @inject(ListItemRenderer)
protected itemRenderer: ListItemRenderer<BoardsPackage> protected itemRenderer: ListItemRenderer<BoardsPackage>
) { ) {
super({ super({
id: BoardsListWidget.WIDGET_ID, id: BoardsListWidget.WIDGET_ID,
label: BoardsListWidget.WIDGET_LABEL, label: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip', iconClass: 'fa fa-microchip',
searchable: service, searchable: service,
installable: service, installable: service,
itemLabel: (item: BoardsPackage) => item.name, itemLabel: (item: BoardsPackage) => item.name,
itemDeprecated: (item: BoardsPackage) => item.deprecated, itemDeprecated: (item: BoardsPackage) => item.deprecated,
itemRenderer, itemRenderer,
}); });
} }
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
super.init(); super.init();
this.toDispose.pushAll([ this.toDispose.pushAll([
this.notificationCenter.onPlatformInstalled(() => this.notificationCenter.onPlatformInstalled(() =>
this.refresh(undefined) this.refresh(undefined)
), ),
this.notificationCenter.onPlatformUninstalled(() => this.notificationCenter.onPlatformUninstalled(() =>
this.refresh(undefined) this.refresh(undefined)
), ),
]); ]);
} }
protected async install({ protected async install({
item, item,
progressId, progressId,
version, version,
}: { }: {
item: BoardsPackage; item: BoardsPackage;
progressId: string; progressId: string;
version: string; version: string;
}): Promise<void> { }): Promise<void> {
await super.install({ item, progressId, version }); await super.install({ item, progressId, version });
this.messageService.info( this.messageService.info(
`Successfully installed platform ${item.name}:${version}`, `Successfully installed platform ${item.name}:${version}`,
{ timeout: 3000 } { timeout: 3000 }
); );
} }
protected async uninstall({ protected async uninstall({
item, item,
progressId, progressId,
}: { }: {
item: BoardsPackage; item: BoardsPackage;
progressId: string; progressId: string;
}): Promise<void> { }): Promise<void> {
await super.uninstall({ item, progressId }); await super.uninstall({ item, progressId });
this.messageService.info( this.messageService.info(
`Successfully uninstalled platform ${item.name}:${item.installedVersion}`, `Successfully uninstalled platform ${item.name}:${item.installedVersion}`,
{ timeout: 3000 } { timeout: 3000 }
); );
} }
} }

View File

@@ -6,232 +6,217 @@ import { Port } from '../../common/protocol';
import { BoardsConfig } from './boards-config'; import { BoardsConfig } from './boards-config';
import { ArduinoCommands } from '../arduino-commands'; import { ArduinoCommands } from '../arduino-commands';
import { import {
BoardsServiceProvider, BoardsServiceProvider,
AvailableBoard, AvailableBoard,
} from './boards-service-provider'; } from './boards-service-provider';
export interface BoardsDropDownListCoords { export interface BoardsDropDownListCoords {
readonly top: number; readonly top: number;
readonly left: number; readonly left: number;
readonly width: number; readonly width: number;
readonly paddingTop: number; readonly paddingTop: number;
} }
export namespace BoardsDropDown { export namespace BoardsDropDown {
export interface Props { export interface Props {
readonly coords: BoardsDropDownListCoords | 'hidden'; readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Array< readonly items: Array<AvailableBoard & { onClick: () => void; port: Port }>;
AvailableBoard & { onClick: () => void; port: Port } readonly openBoardsConfig: () => void;
>; }
readonly openBoardsConfig: () => void;
}
} }
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> { export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
protected dropdownElement: HTMLElement; protected dropdownElement: HTMLElement;
constructor(props: BoardsDropDown.Props) { constructor(props: BoardsDropDown.Props) {
super(props); super(props);
let list = document.getElementById('boards-dropdown-container'); let list = document.getElementById('boards-dropdown-container');
if (!list) { if (!list) {
list = document.createElement('div'); list = document.createElement('div');
list.id = 'boards-dropdown-container'; list.id = 'boards-dropdown-container';
document.body.appendChild(list); document.body.appendChild(list);
this.dropdownElement = list; this.dropdownElement = list;
}
} }
}
render(): React.ReactNode { render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement); return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
} }
protected renderNode(): React.ReactNode { protected renderNode(): React.ReactNode {
const { coords, items } = this.props; const { coords, items } = this.props;
if (coords === 'hidden') { if (coords === 'hidden') {
return ''; return '';
}
return (
<div
className="arduino-boards-dropdown-list"
style={{
position: 'absolute',
...coords,
}}
>
{this.renderItem({
label: 'Select Other Board & Port',
onClick: () => this.props.openBoardsConfig(),
})}
{items
.map(({ name, port, selected, onClick }) => ({
label: `${name} at ${Port.toString(port)}`,
selected,
onClick,
}))
.map(this.renderItem)}
</div>
);
} }
return (
<div
className="arduino-boards-dropdown-list"
style={{
position: 'absolute',
...coords,
}}
>
{this.renderItem({
label: 'Select Other Board & Port',
onClick: () => this.props.openBoardsConfig(),
})}
{items
.map(({ name, port, selected, onClick }) => ({
label: `${name} at ${Port.toString(port)}`,
selected,
onClick,
}))
.map(this.renderItem)}
</div>
);
}
protected renderItem({ protected renderItem({
label, label,
selected, selected,
onClick, onClick,
}: { }: {
label: string; label: string;
selected?: boolean; selected?: boolean;
onClick: () => void; onClick: () => void;
}): React.ReactNode { }): React.ReactNode {
return ( return (
<div <div
key={label} key={label}
className={`arduino-boards-dropdown-item ${ className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`}
selected ? 'selected' : '' onClick={onClick}
}`} >
onClick={onClick} <div>{label}</div>
> {selected ? <span className="fa fa-check" /> : ''}
<div>{label}</div> </div>
{selected ? <span className="fa fa-check" /> : ''} );
</div> }
);
}
} }
export class BoardsToolBarItem extends React.Component< export class BoardsToolBarItem extends React.Component<
BoardsToolBarItem.Props, BoardsToolBarItem.Props,
BoardsToolBarItem.State BoardsToolBarItem.State
> { > {
static TOOLBAR_ID: 'boards-toolbar'; static TOOLBAR_ID: 'boards-toolbar';
protected readonly toDispose: DisposableCollection = protected readonly toDispose: DisposableCollection =
new DisposableCollection(); new DisposableCollection();
constructor(props: BoardsToolBarItem.Props) { constructor(props: BoardsToolBarItem.Props) {
super(props); super(props);
const { availableBoards } = props.boardsServiceClient; const { availableBoards } = props.boardsServiceClient;
this.state = { this.state = {
availableBoards, availableBoards,
coords: 'hidden', coords: 'hidden',
};
document.addEventListener('click', () => {
this.setState({ coords: 'hidden' });
});
}
componentDidMount() {
this.props.boardsServiceClient.onAvailableBoardsChanged(
(availableBoards) => this.setState({ availableBoards })
);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
const rect = element.getBoundingClientRect();
this.setState({
coords: {
top: rect.top,
left: rect.left,
width: rect.width,
paddingTop: rect.height,
},
});
} else {
this.setState({ coords: 'hidden' });
}
}
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
}; };
render(): React.ReactNode { document.addEventListener('click', () => {
const { coords, availableBoards } = this.state; this.setState({ coords: 'hidden' });
const boardsConfig = this.props.boardsServiceClient.boardsConfig; });
const title = BoardsConfig.Config.toString(boardsConfig, { }
default: 'no board selected',
componentDidMount() {
this.props.boardsServiceClient.onAvailableBoardsChanged((availableBoards) =>
this.setState({ availableBoards })
);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
const rect = element.getBoundingClientRect();
this.setState({
coords: {
top: rect.top,
left: rect.left,
width: rect.width,
paddingTop: rect.height,
},
}); });
const decorator = (() => { } else {
const selectedBoard = availableBoards.find(
({ selected }) => selected
);
if (!selectedBoard || !selectedBoard.port) {
return 'fa fa-times notAttached';
}
if (selectedBoard.state === AvailableBoard.State.guessed) {
return 'fa fa-exclamation-triangle guessed';
}
return '';
})();
return (
<React.Fragment>
<div className="arduino-boards-toolbar-item-container">
<div className="arduino-boards-toolbar-item" title={title}>
<div className="inner-container" onClick={this.show}>
<span className={decorator} />
<div className="label noWrapInfo">
<div className="noWrapInfo noselect">
{title}
</div>
</div>
<span className="fa fa-caret-down caret" />
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={availableBoards
.filter(AvailableBoard.hasPort)
.map((board) => ({
...board,
onClick: () => {
if (
board.state ===
AvailableBoard.State.incomplete
) {
this.props.boardsServiceClient.boardsConfig =
{
selectedPort: board.port,
};
this.openDialog();
} else {
this.props.boardsServiceClient.boardsConfig =
{
selectedBoard: board,
selectedPort: board.port,
};
}
},
}))}
openBoardsConfig={this.openDialog}
></BoardsDropDown>
</React.Fragment>
);
}
protected openDialog = () => {
this.props.commands.executeCommand(
ArduinoCommands.OPEN_BOARDS_DIALOG.id
);
this.setState({ coords: 'hidden' }); this.setState({ coords: 'hidden' });
}; }
}
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
render(): React.ReactNode {
const { coords, availableBoards } = this.state;
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
const title = BoardsConfig.Config.toString(boardsConfig, {
default: 'no board selected',
});
const decorator = (() => {
const selectedBoard = availableBoards.find(({ selected }) => selected);
if (!selectedBoard || !selectedBoard.port) {
return 'fa fa-times notAttached';
}
if (selectedBoard.state === AvailableBoard.State.guessed) {
return 'fa fa-exclamation-triangle guessed';
}
return '';
})();
return (
<React.Fragment>
<div className="arduino-boards-toolbar-item-container">
<div className="arduino-boards-toolbar-item" title={title}>
<div className="inner-container" onClick={this.show}>
<span className={decorator} />
<div className="label noWrapInfo">
<div className="noWrapInfo noselect">{title}</div>
</div>
<span className="fa fa-caret-down caret" />
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={availableBoards
.filter(AvailableBoard.hasPort)
.map((board) => ({
...board,
onClick: () => {
if (board.state === AvailableBoard.State.incomplete) {
this.props.boardsServiceClient.boardsConfig = {
selectedPort: board.port,
};
this.openDialog();
} else {
this.props.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: board.port,
};
}
},
}))}
openBoardsConfig={this.openDialog}
></BoardsDropDown>
</React.Fragment>
);
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ coords: 'hidden' });
};
} }
export namespace BoardsToolBarItem { export namespace BoardsToolBarItem {
export interface Props { export interface Props {
readonly boardsServiceClient: BoardsServiceProvider; readonly boardsServiceClient: BoardsServiceProvider;
readonly commands: CommandRegistry; readonly commands: CommandRegistry;
} }
export interface State { export interface State {
availableBoards: AvailableBoard[]; availableBoards: AvailableBoard[];
coords: BoardsDropDownListCoords | 'hidden'; coords: BoardsDropDownListCoords | 'hidden';
} }
} }

View File

@@ -5,20 +5,20 @@ import { ListWidgetFrontendContribution } from '../widgets/component-list/list-w
@injectable() @injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> { export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
constructor() { constructor() {
super({ super({
widgetId: BoardsListWidget.WIDGET_ID, widgetId: BoardsListWidget.WIDGET_ID,
widgetName: BoardsListWidget.WIDGET_LABEL, widgetName: BoardsListWidget.WIDGET_LABEL,
defaultWidgetOptions: { defaultWidgetOptions: {
area: 'left', area: 'left',
rank: 2, rank: 2,
}, },
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`, toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
toggleKeybinding: 'CtrlCmd+Shift+B', toggleKeybinding: 'CtrlCmd+Shift+B',
}); });
} }
async initializeLayout(): Promise<void> { async initializeLayout(): Promise<void> {
this.openView(); this.openView();
} }
} }

View File

@@ -5,119 +5,113 @@ import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
CommandRegistry, CommandRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ConfigService } from '../../common/protocol'; import { ConfigService } from '../../common/protocol';
@injectable() @injectable()
export class About extends Contribution { export class About extends Contribution {
@inject(ClipboardService) @inject(ClipboardService)
protected readonly clipboardService: ClipboardService; protected readonly clipboardService: ClipboardService;
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(About.Commands.ABOUT_APP, { registry.registerCommand(About.Commands.ABOUT_APP, {
execute: () => this.showAbout(), execute: () => this.showAbout(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
commandId: About.Commands.ABOUT_APP.id, commandId: About.Commands.ABOUT_APP.id,
label: `About ${this.applicationName}`, label: `About ${this.applicationName}`,
order: '0', order: '0',
}); });
} }
async showAbout(): Promise<void> { async showAbout(): Promise<void> {
const { const {
version, version,
commit, commit,
status: cliStatus, status: cliStatus,
} = await this.configService.getVersion(); } = await this.configService.getVersion();
const buildDate = this.buildDate; const buildDate = this.buildDate;
const detail = ( const detail = (showAll: boolean) => `Version: ${remote.app.getVersion()}
showAll: boolean
) => `Version: ${remote.app.getVersion()}
Date: ${buildDate ? buildDate : 'dev build'}${ Date: ${buildDate ? buildDate : 'dev build'}${
buildDate && showAll ? ` (${this.ago(buildDate)})` : '' buildDate && showAll ? ` (${this.ago(buildDate)})` : ''
} }
CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}] CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}]
${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''} ${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
`; `;
const ok = 'OK'; const ok = 'OK';
const copy = 'Copy'; const copy = 'Copy';
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy]; const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
const { response } = await remote.dialog.showMessageBox( const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(), remote.getCurrentWindow(),
{ {
message: `${this.applicationName}`, message: `${this.applicationName}`,
title: `${this.applicationName}`, title: `${this.applicationName}`,
type: 'info', type: 'info',
detail: detail(true), detail: detail(true),
buttons, buttons,
noLink: true, noLink: true,
defaultId: buttons.indexOf(ok), defaultId: buttons.indexOf(ok),
cancelId: buttons.indexOf(ok), cancelId: buttons.indexOf(ok),
} }
); );
if (buttons[response] === copy) { if (buttons[response] === copy) {
await this.clipboardService.writeText(detail(false).trim()); await this.clipboardService.writeText(detail(false).trim());
}
} }
}
protected get applicationName(): string { protected get applicationName(): string {
return FrontendApplicationConfigProvider.get().applicationName; return FrontendApplicationConfigProvider.get().applicationName;
} }
protected get buildDate(): string | undefined { protected get buildDate(): string | undefined {
return FrontendApplicationConfigProvider.get().buildDate; return FrontendApplicationConfigProvider.get().buildDate;
} }
protected ago(isoTime: string): string { protected ago(isoTime: string): string {
const now = moment(Date.now()); const now = moment(Date.now());
const other = moment(isoTime); const other = moment(isoTime);
let result = now.diff(other, 'minute'); let result = now.diff(other, 'minute');
if (result < 60) { if (result < 60) {
return result === 1 return result === 1 ? `${result} minute ago` : `${result} minute ago`;
? `${result} minute ago`
: `${result} minute ago`;
}
result = now.diff(other, 'hour');
if (result < 25) {
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
}
result = now.diff(other, 'day');
if (result < 8) {
return result === 1 ? `${result} day ago` : `${result} days ago`;
}
result = now.diff(other, 'week');
if (result < 5) {
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
}
result = now.diff(other, 'month');
if (result < 13) {
return result === 1
? `${result} month ago`
: `${result} months ago`;
}
result = now.diff(other, 'year');
return result === 1 ? `${result} year ago` : `${result} years ago`;
} }
result = now.diff(other, 'hour');
if (result < 25) {
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
}
result = now.diff(other, 'day');
if (result < 8) {
return result === 1 ? `${result} day ago` : `${result} days ago`;
}
result = now.diff(other, 'week');
if (result < 5) {
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
}
result = now.diff(other, 'month');
if (result < 13) {
return result === 1 ? `${result} month ago` : `${result} months ago`;
}
result = now.diff(other, 'year');
return result === 1 ? `${result} year ago` : `${result} years ago`;
}
} }
export namespace About { export namespace About {
export namespace Commands { export namespace Commands {
export const ABOUT_APP: Command = { export const ABOUT_APP: Command = {
id: 'arduino-about', id: 'arduino-about',
}; };
} }
} }

View File

@@ -2,74 +2,74 @@ import { inject, injectable } from 'inversify';
import { remote } from 'electron'; import { remote } from 'electron';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
URI, URI,
} from './contribution'; } from './contribution';
import { FileDialogService } from '@theia/filesystem/lib/browser'; import { FileDialogService } from '@theia/filesystem/lib/browser';
@injectable() @injectable()
export class AddFile extends SketchContribution { export class AddFile extends SketchContribution {
@inject(FileDialogService) @inject(FileDialogService)
protected readonly fileDialogService: FileDialogService; protected readonly fileDialogService: FileDialogService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, { registry.registerCommand(AddFile.Commands.ADD_FILE, {
execute: () => this.addFile(), execute: () => this.addFile(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, { registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: AddFile.Commands.ADD_FILE.id, commandId: AddFile.Commands.ADD_FILE.id,
label: 'Add File...', label: 'Add File...',
order: '2', order: '2',
}); });
} }
protected async addFile(): Promise<void> { protected async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch(); const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) { if (!sketch) {
return; return;
}
const toAddUri = await this.fileDialogService.showOpenDialog({
title: 'Add File',
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
});
if (!toAddUri) {
return;
}
const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
const exists = await this.fileService.exists(targetUri);
if (exists) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
title: 'Replace',
buttons: ['Cancel', 'OK'],
message: `Replace the existing version of ${filename}?`,
});
if (response === 0) {
// Cancel
return;
}
}
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
this.messageService.info('One file added to the sketch.', {
timeout: 2000,
});
} }
const toAddUri = await this.fileDialogService.showOpenDialog({
title: 'Add File',
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
});
if (!toAddUri) {
return;
}
const sketchUri = new URI(sketch.uri);
const filename = toAddUri.path.base;
const targetUri = sketchUri.resolve('data').resolve(filename);
const exists = await this.fileService.exists(targetUri);
if (exists) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
title: 'Replace',
buttons: ['Cancel', 'OK'],
message: `Replace the existing version of ${filename}?`,
});
if (response === 0) {
// Cancel
return;
}
}
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
this.messageService.info('One file added to the sketch.', {
timeout: 2000,
});
}
} }
export namespace AddFile { export namespace AddFile {
export namespace Commands { export namespace Commands {
export const ADD_FILE: Command = { export const ADD_FILE: Command = {
id: 'arduino-add-file', id: 'arduino-add-file',
}; };
} }
} }

View File

@@ -7,133 +7,127 @@ import { ArduinoMenus } from '../menu/arduino-menus';
import { ResponseServiceImpl } from '../response-service-impl'; import { ResponseServiceImpl } from '../response-service-impl';
import { Installable, LibraryService } from '../../common/protocol'; import { Installable, LibraryService } from '../../common/protocol';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class AddZipLibrary extends SketchContribution { export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer) @inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer; protected readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceImpl) @inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl; protected readonly responseService: ResponseServiceImpl;
@inject(LibraryService) @inject(LibraryService)
protected readonly libraryService: LibraryService; protected readonly libraryService: LibraryService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, { registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
execute: () => this.addZipLibrary(), execute: () => this.addZipLibrary(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
const includeLibMenuPath = [ const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP, ...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include', '0_include',
]; ];
// TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt. // TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt.
registry.registerSubmenu(includeLibMenuPath, 'Include Library', { registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1', order: '1',
}); });
registry.registerMenuAction([...includeLibMenuPath, '1_install'], { registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id, commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
label: 'Add .ZIP Library...', label: 'Add .ZIP Library...',
order: '1', order: '1',
}); });
} }
async addZipLibrary(): Promise<void> { async addZipLibrary(): Promise<void> {
const homeUri = await this.envVariableServer.getHomeDirUri(); const homeUri = await this.envVariableServer.getHomeDirUri();
const defaultPath = await this.fileService.fsPath(new URI(homeUri)); const defaultPath = await this.fileService.fsPath(new URI(homeUri));
const { canceled, filePaths } = await remote.dialog.showOpenDialog({ const { canceled, filePaths } = await remote.dialog.showOpenDialog({
title: "Select a zip file containing the library you'd like to add", title: "Select a zip file containing the library you'd like to add",
defaultPath, defaultPath,
properties: ['openFile'], properties: ['openFile'],
filters: [ filters: [
{ {
name: 'Library', name: 'Library',
extensions: ['zip'], extensions: ['zip'],
}, },
], ],
}); });
if (!canceled && filePaths.length) { if (!canceled && filePaths.length) {
const zipUri = await this.fileSystemExt.getUri(filePaths[0]); const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
try { try {
await this.doInstall(zipUri); await this.doInstall(zipUri);
} catch (error) { } catch (error) {
if (error instanceof AlreadyInstalledError) { if (error instanceof AlreadyInstalledError) {
const result = await new ConfirmDialog({ const result = await new ConfirmDialog({
msg: error.message, msg: error.message,
title: 'Do you want to overwrite the existing library?', title: 'Do you want to overwrite the existing library?',
ok: 'Yes', ok: 'Yes',
cancel: 'No', cancel: 'No',
}).open(); }).open();
if (result) { if (result) {
await this.doInstall(zipUri, true); await this.doInstall(zipUri, true);
} }
}
}
} }
}
} }
}
private async doInstall( private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
zipUri: string, try {
overwrite?: boolean await Installable.doWithProgress({
): Promise<void> { messageService: this.messageService,
try { progressText: `Processing ${new URI(zipUri).path.base}`,
await Installable.doWithProgress({ responseService: this.responseService,
messageService: this.messageService, run: () => this.libraryService.installZip({ zipUri, overwrite }),
progressText: `Processing ${new URI(zipUri).path.base}`, });
responseService: this.responseService, this.messageService.info(
run: () => `Successfully installed library from ${
this.libraryService.installZip({ zipUri, overwrite }), new URI(zipUri).path.base
}); } archive`,
this.messageService.info( { timeout: 3000 }
`Successfully installed library from ${ );
new URI(zipUri).path.base } catch (error) {
} archive`, if (error instanceof Error) {
{ timeout: 3000 } const match = error.message.match(/library (.*?) already installed/);
if (match && match.length >= 2) {
const name = match[1].trim();
if (name) {
throw new AlreadyInstalledError(
`A library folder named ${name} already exists. Do you want to overwrite it?`,
name
); );
} catch (error) { } else {
if (error instanceof Error) { throw new AlreadyInstalledError(
const match = error.message.match( 'A library already exists. Do you want to overwrite it?'
/library (.*?) already installed/ );
); }
if (match && match.length >= 2) {
const name = match[1].trim();
if (name) {
throw new AlreadyInstalledError(
`A library folder named ${name} already exists. Do you want to overwrite it?`,
name
);
} else {
throw new AlreadyInstalledError(
'A library already exists. Do you want to overwrite it?'
);
}
}
}
this.messageService.error(error.toString());
throw error;
} }
}
this.messageService.error(error.toString());
throw error;
} }
}
} }
class AlreadyInstalledError extends Error { class AlreadyInstalledError extends Error {
constructor(message: string, readonly libraryName?: string) { constructor(message: string, readonly libraryName?: string) {
super(message); super(message);
Object.setPrototypeOf(this, AlreadyInstalledError.prototype); Object.setPrototypeOf(this, AlreadyInstalledError.prototype);
} }
} }
export namespace AddZipLibrary { export namespace AddZipLibrary {
export namespace Commands { export namespace Commands {
export const ADD_ZIP_LIBRARY: Command = { export const ADD_ZIP_LIBRARY: Command = {
id: 'arduino-add-zip-library', id: 'arduino-add-zip-library',
}; };
} }
} }

View File

@@ -4,65 +4,65 @@ import * as dateFormat from 'dateformat';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class ArchiveSketch extends SketchContribution { export class ArchiveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, { registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
execute: () => this.archiveSketch(), execute: () => this.archiveSketch(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id, commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
label: 'Archive Sketch', label: 'Archive Sketch',
order: '1', order: '1',
}); });
} }
protected async archiveSketch(): Promise<void> { protected async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([ const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(), this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(), this.configService.getConfiguration(),
]); ]);
if (!sketch) { if (!sketch) {
return; return;
}
const archiveBasename = `${sketch.name}-${dateFormat(
new Date(),
'yymmdd'
)}a.zip`;
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
defaultPath,
});
if (!filePath || canceled) {
return;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return;
}
await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info(`Created archive '${archiveBasename}'.`, {
timeout: 2000,
});
} }
const archiveBasename = `${sketch.name}-${dateFormat(
new Date(),
'yymmdd'
)}a.zip`;
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
defaultPath,
});
if (!filePath || canceled) {
return;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return;
}
await this.sketchService.archive(sketch, destinationUri.toString());
this.messageService.info(`Created archive '${archiveBasename}'.`, {
timeout: 2000,
});
}
} }
export namespace ArchiveSketch { export namespace ArchiveSketch {
export namespace Commands { export namespace Commands {
export const ARCHIVE_SKETCH: Command = { export const ARCHIVE_SKETCH: Command = {
id: 'arduino-archive-sketch', id: 'arduino-archive-sketch',
}; };
} }
} }

View File

@@ -2,8 +2,8 @@ import { inject, injectable } from 'inversify';
import { remote } from 'electron'; import { remote } from 'electron';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { import {
DisposableCollection, DisposableCollection,
Disposable, Disposable,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { firstToUpperCase } from '../../common/utils'; import { firstToUpperCase } from '../../common/utils';
import { BoardsConfig } from '../boards/boards-config'; import { BoardsConfig } from '../boards/boards-config';
@@ -12,327 +12,302 @@ import { BoardsListWidget } from '../boards/boards-list-widget';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
ArduinoMenus, ArduinoMenus,
PlaceholderMenuNode, PlaceholderMenuNode,
unregisterSubmenu, unregisterSubmenu,
} from '../menu/arduino-menus'; } from '../menu/arduino-menus';
import { import {
BoardsService, BoardsService,
InstalledBoardWithPackage, InstalledBoardWithPackage,
AvailablePorts, AvailablePorts,
Port, Port,
} from '../../common/protocol'; } from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution'; import { SketchContribution, Command, CommandRegistry } from './contribution';
@injectable() @injectable()
export class BoardSelection extends SketchContribution { export class BoardSelection extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuModelRegistry: MenuModelRegistry; protected readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider; protected readonly boardsServiceProvider: BoardsServiceProvider;
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection(); protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, { registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => { execute: async () => {
const { selectedBoard, selectedPort } = const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig; this.boardsServiceProvider.boardsConfig;
if (!selectedBoard) { if (!selectedBoard) {
this.messageService.info( this.messageService.info(
'Please select a board to obtain board info.' 'Please select a board to obtain board info.'
); );
return; return;
} }
if (!selectedBoard.fqbn) { if (!selectedBoard.fqbn) {
this.messageService.info( this.messageService.info(
`The platform for the selected '${selectedBoard.name}' board is not installed.` `The platform for the selected '${selectedBoard.name}' board is not installed.`
); );
return; return;
} }
if (!selectedPort) { if (!selectedPort) {
this.messageService.info( this.messageService.info(
'Please select a port to obtain board info.' 'Please select a port to obtain board info.'
); );
return; return;
} }
const boardDetails = await this.boardsService.getBoardDetails({ const boardDetails = await this.boardsService.getBoardDetails({
fqbn: selectedBoard.fqbn, fqbn: selectedBoard.fqbn,
}); });
if (boardDetails) { if (boardDetails) {
const { VID, PID } = boardDetails; const { VID, PID } = boardDetails;
const detail = `BN: ${selectedBoard.name} const detail = `BN: ${selectedBoard.name}
VID: ${VID} VID: ${VID}
PID: ${PID}`; PID: ${PID}`;
await remote.dialog.showMessageBox( await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
remote.getCurrentWindow(), message: 'Board Info',
{ title: 'Board Info',
message: 'Board Info', type: 'info',
title: 'Board Info', detail,
type: 'info', buttons: ['OK'],
detail, });
buttons: ['OK'], }
} },
); });
} }
},
}); onStart(): void {
this.updateMenus();
this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this));
this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailableBoardsChanged(
this.updateMenus.bind(this)
);
}
protected async updateMenus(): Promise<void> {
const [installedBoards, availablePorts, config] = await Promise.all([
this.installedBoards(),
this.boardsService.getState(),
this.boardsServiceProvider.boardsConfig,
]);
this.rebuildMenus(installedBoards, availablePorts, config);
}
protected rebuildMenus(
installedBoards: InstalledBoardWithPackage[],
availablePorts: AvailablePorts,
config: BoardsConfig.Config
): void {
this.toDisposeBeforeMenuRebuild.dispose();
// Boards submenu
const boardsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'1_boards',
];
const boardsSubmenuLabel = config.selectedBoard?.name;
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
this.menuModelRegistry.registerSubmenu(
boardsSubmenuPath,
`Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`,
{ order: '100' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)
)
);
// Ports submenu
const portsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
`Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`,
{ order: '101' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)
)
);
const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: 'Get Board Info',
order: '103',
};
this.menuModelRegistry.registerMenuAction(
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
getBoardInfo
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuAction(getBoardInfo)
)
);
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
label: 'Boards Manager...',
});
// Installed boards
for (const board of installedBoards) {
const { packageId, packageName, fqbn, name } = board;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageName);
const id = `arduino-select-board--${fqbn}`;
const command = { id };
const handler = {
execute: () => {
if (
fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard: {
name,
fqbn,
port: this.boardsServiceProvider.boardsConfig.selectedBoard
?.port, // TODO: verify!
},
selectedPort:
this.boardsServiceProvider.boardsConfig.selectedPort,
};
}
},
isToggled: () =>
fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
};
// Board menu
const menuAction = { commandId: id, label: name };
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
);
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
} }
onStart(): void { // Installed ports
this.updateMenus(); const registerPorts = (ports: AvailablePorts) => {
this.notificationCenter.onPlatformInstalled( const addresses = Object.keys(ports);
this.updateMenus.bind(this) if (!addresses.length) {
); return;
this.notificationCenter.onPlatformUninstalled( }
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onBoardsConfigChanged(
this.updateMenus.bind(this)
);
this.boardsServiceProvider.onAvailableBoardsChanged(
this.updateMenus.bind(this)
);
}
protected async updateMenus(): Promise<void> { // Register placeholder for protocol
const [installedBoards, availablePorts, config] = await Promise.all([ const [port] = ports[addresses[0]];
this.installedBoards(), const protocol = port.protocol;
this.boardsService.getState(), const menuPath = [...portsSubmenuPath, protocol];
this.boardsServiceProvider.boardsConfig, const placeholder = new PlaceholderMenuNode(
]); menuPath,
this.rebuildMenus(installedBoards, availablePorts, config); `${firstToUpperCase(port.protocol)} ports`
} );
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuNode(placeholder.id)
)
);
protected rebuildMenus( for (const address of addresses) {
installedBoards: InstalledBoardWithPackage[], if (!!ports[address]) {
availablePorts: AvailablePorts, const [port, boards] = ports[address];
config: BoardsConfig.Config if (!boards.length) {
): void { boards.push({
this.toDisposeBeforeMenuRebuild.dispose(); name: '',
});
// Boards submenu }
const boardsSubmenuPath = [ for (const { name, fqbn } of boards) {
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, const id = `arduino-select-port--${address}${
'1_boards', fqbn ? `--${fqbn}` : ''
]; }`;
const boardsSubmenuLabel = config.selectedBoard?.name;
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
this.menuModelRegistry.registerSubmenu(
boardsSubmenuPath,
`Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`,
{ order: '100' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)
)
);
// Ports submenu
const portsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'2_ports',
];
const portsSubmenuLabel = config.selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
`Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`,
{ order: '101' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)
)
);
const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: 'Get Board Info',
order: '103',
};
this.menuModelRegistry.registerMenuAction(
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
getBoardInfo
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuAction(getBoardInfo)
)
);
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
label: 'Boards Manager...',
});
// Installed boards
for (const board of installedBoards) {
const { packageId, packageName, fqbn, name } = board;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
this.menuModelRegistry.registerSubmenu(
platformMenuPath,
packageName
);
const id = `arduino-select-board--${fqbn}`;
const command = { id }; const command = { id };
const handler = { const handler = {
execute: () => { execute: () => {
if ( if (
fqbn !== !Port.equals(
this.boardsServiceProvider.boardsConfig.selectedBoard port,
?.fqbn this.boardsServiceProvider.boardsConfig.selectedPort
) { )
this.boardsServiceProvider.boardsConfig = { ) {
selectedBoard: { this.boardsServiceProvider.boardsConfig = {
name, selectedBoard:
fqbn, this.boardsServiceProvider.boardsConfig.selectedBoard,
port: this.boardsServiceProvider.boardsConfig selectedPort: port,
.selectedBoard?.port, // TODO: verify! };
}, }
selectedPort: },
this.boardsServiceProvider.boardsConfig isToggled: () =>
.selectedPort, Port.equals(
}; port,
} this.boardsServiceProvider.boardsConfig.selectedPort
}, ),
isToggled: () => };
fqbn === const label = `${address}${name ? ` (${name})` : ''}`;
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn, const menuAction = {
commandId: id,
label,
order: `1${label}`, // `1` comes after the placeholder which has order `0`
}; };
// Board menu
const menuAction = { commandId: id, label: name };
this.commandRegistry.registerCommand(command, handler); this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push( this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() => Disposable.create(() =>
this.commandRegistry.unregisterCommand(command) this.commandRegistry.unregisterCommand(command)
) )
); );
this.menuModelRegistry.registerMenuAction( this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
platformMenuPath, }
menuAction
);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
} }
}
};
// Installed ports const { serial, network, unknown } =
const registerPorts = (ports: AvailablePorts) => { AvailablePorts.groupByProtocol(availablePorts);
const addresses = Object.keys(ports); registerPorts(serial);
if (!addresses.length) { registerPorts(network);
return; registerPorts(unknown);
}
// Register placeholder for protocol this.mainMenuManager.update();
const [port] = ports[addresses[0]]; }
const protocol = port.protocol;
const menuPath = [...portsSubmenuPath, protocol];
const placeholder = new PlaceholderMenuNode(
menuPath,
`${firstToUpperCase(port.protocol)} ports`
);
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuNode(placeholder.id)
)
);
for (const address of addresses) { protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
if (!!ports[address]) { const allBoards = await this.boardsService.searchBoards({});
const [port, boards] = ports[address]; return allBoards.filter(InstalledBoardWithPackage.is);
if (!boards.length) { }
boards.push({
name: '',
});
}
for (const { name, fqbn } of boards) {
const id = `arduino-select-port--${address}${
fqbn ? `--${fqbn}` : ''
}`;
const command = { id };
const handler = {
execute: () => {
if (
!Port.equals(
port,
this.boardsServiceProvider.boardsConfig
.selectedPort
)
) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard:
this.boardsServiceProvider
.boardsConfig.selectedBoard,
selectedPort: port,
};
}
},
isToggled: () =>
Port.equals(
port,
this.boardsServiceProvider.boardsConfig
.selectedPort
),
};
const label = `${address}${name ? ` (${name})` : ''}`;
const menuAction = {
commandId: id,
label,
order: `1${label}`, // `1` comes after the placeholder which has order `0`
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
)
);
this.menuModelRegistry.registerMenuAction(
menuPath,
menuAction
);
}
}
}
};
const { serial, network, unknown } =
AvailablePorts.groupByProtocol(availablePorts);
registerPorts(serial);
registerPorts(network);
registerPorts(unknown);
this.mainMenuManager.update();
}
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
const allBoards = await this.boardsService.searchBoards({});
return allBoards.filter(InstalledBoardWithPackage.is);
}
} }
export namespace BoardSelection { export namespace BoardSelection {
export namespace Commands { export namespace Commands {
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' }; export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
} }
} }

View File

@@ -6,87 +6,85 @@ import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection'; import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class BurnBootloader extends SketchContribution { export class BurnBootloader extends SketchContribution {
@inject(CoreService) @inject(CoreService)
protected readonly coreService: CoreService; protected readonly coreService: CoreService;
@inject(MonitorConnection) @inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection; protected readonly monitorConnection: MonitorConnection;
@inject(BoardsDataStore) @inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider; protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(OutputChannelManager) @inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager; protected readonly outputChannelManager: OutputChannelManager;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, { registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
execute: () => this.burnBootloader(), execute: () => this.burnBootloader(),
}); });
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
label: 'Burn Bootloader',
order: 'z99',
});
}
async burnBootloader(): Promise<void> {
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
} }
try {
registerMenus(registry: MenuModelRegistry): void { const { boardsConfig } = this.boardsServiceClientImpl;
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, { const port = boardsConfig.selectedPort?.address;
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id, const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
label: 'Burn Bootloader', await Promise.all([
order: 'z99', this.boardsDataStore.appendConfigToFqbn(
}); boardsConfig.selectedBoard?.fqbn
} ),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
async burnBootloader(): Promise<void> { this.preferences.get('arduino.upload.verify'),
const monitorConfig = this.monitorConnection.monitorConfig; this.preferences.get('arduino.upload.verbose'),
if (monitorConfig) { ]);
await this.monitorConnection.disconnect(); this.outputChannelManager.getChannel('Arduino').clear();
} await this.coreService.burnBootloader({
try { fqbn,
const { boardsConfig } = this.boardsServiceClientImpl; programmer,
const port = boardsConfig.selectedPort?.address; port,
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = verify,
await Promise.all([ verbose,
this.boardsDataStore.appendConfigToFqbn( });
boardsConfig.selectedBoard?.fqbn this.messageService.info('Done burning bootloader.', {
), timeout: 3000,
this.boardsDataStore.getData( });
boardsConfig.selectedBoard?.fqbn } catch (e) {
), this.messageService.error(e.toString());
this.preferences.get('arduino.upload.verify'), } finally {
this.preferences.get('arduino.upload.verbose'), if (monitorConfig) {
]); await this.monitorConnection.connect(monitorConfig);
this.outputChannelManager.getChannel('Arduino').clear(); }
await this.coreService.burnBootloader({
fqbn,
programmer,
port,
verify,
verbose,
});
this.messageService.info('Done burning bootloader.', {
timeout: 3000,
});
} catch (e) {
this.messageService.error(e.toString());
} finally {
if (monitorConfig) {
await this.monitorConnection.connect(monitorConfig);
}
}
} }
}
} }
export namespace BurnBootloader { export namespace BurnBootloader {
export namespace Commands { export namespace Commands {
export const BURN_BOOTLOADER: Command = { export const BURN_BOOTLOADER: Command = {
id: 'arduino-burn-bootloader', id: 'arduino-burn-bootloader',
}; };
} }
} }

View File

@@ -8,12 +8,12 @@ import { FrontendApplication } from '@theia/core/lib/browser/frontend-applicatio
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { SaveAsSketch } from './save-as-sketch'; import { SaveAsSketch } from './save-as-sketch';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
URI, URI,
} from './contribution'; } from './contribution';
/** /**
@@ -21,118 +21,112 @@ import {
*/ */
@injectable() @injectable()
export class Close extends SketchContribution { export class Close extends SketchContribution {
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
protected shell: ApplicationShell; protected shell: ApplicationShell;
onStart(app: FrontendApplication): void { onStart(app: FrontendApplication): void {
this.shell = app.shell; this.shell = app.shell;
} }
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Close.Commands.CLOSE, { registry.registerCommand(Close.Commands.CLOSE, {
execute: async () => { execute: async () => {
// Close current editor if closeable. // Close current editor if closeable.
const { currentEditor } = this.editorManager; const { currentEditor } = this.editorManager;
if (currentEditor && currentEditor.title.closable) { if (currentEditor && currentEditor.title.closable) {
currentEditor.close(); currentEditor.close();
return; return;
}
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (
currentWidgetInMain &&
currentWidgetInMain.title.closable
) {
return currentWidgetInMain.close();
}
}
// Close the sketch (window).
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
if (isTemp && (await this.wasTouched(uri))) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
buttons: ["Don't Save", 'Cancel', 'Save'],
message:
'Do you want to save changes to this sketch before closing?',
detail: "If you don't save, your changes will be lost.",
});
if (response === 1) {
// Cancel
return;
}
if (response === 2) {
// Save
const saved = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{ openAfterMove: false, execOnlyIfTemp: true }
);
if (!saved) {
// If it was not saved, do bail the close.
return;
}
}
}
window.close();
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: 'Close',
order: '5',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W',
});
}
/**
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
*/
protected async wasTouched(uri: string): Promise<boolean> {
const editorWidget = await this.editorManager.getByUri(new URI(uri));
if (editorWidget) {
const { editor } = editorWidget;
if (editor instanceof MonacoEditor) {
const versionId = editor
.getControl()
.getModel()
?.getVersionId();
if (Number.isInteger(versionId) && versionId! > 1) {
return true;
}
}
} }
return false;
// Close current widget from the main area if possible.
const { currentWidget } = this.shell;
if (currentWidget) {
const currentWidgetInMain = toArray(
this.shell.mainPanel.widgets()
).find((widget) => widget === currentWidget);
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
return currentWidgetInMain.close();
}
}
// Close the sketch (window).
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
const isTemp = await this.sketchService.isTemp(sketch);
const uri = await this.sketchServiceClient.currentSketchFile();
if (!uri) {
return;
}
if (isTemp && (await this.wasTouched(uri))) {
const { response } = await remote.dialog.showMessageBox({
type: 'question',
buttons: ["Don't Save", 'Cancel', 'Save'],
message:
'Do you want to save changes to this sketch before closing?',
detail: "If you don't save, your changes will be lost.",
});
if (response === 1) {
// Cancel
return;
}
if (response === 2) {
// Save
const saved = await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
{ openAfterMove: false, execOnlyIfTemp: true }
);
if (!saved) {
// If it was not saved, do bail the close.
return;
}
}
}
window.close();
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: 'Close',
order: '5',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: Close.Commands.CLOSE.id,
keybinding: 'CtrlCmd+W',
});
}
/**
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
*/
protected async wasTouched(uri: string): Promise<boolean> {
const editorWidget = await this.editorManager.getByUri(new URI(uri));
if (editorWidget) {
const { editor } = editorWidget;
if (editor instanceof MonacoEditor) {
const versionId = editor.getControl().getModel()?.getVersionId();
if (Number.isInteger(versionId) && versionId! > 1) {
return true;
}
}
} }
return false;
}
} }
export namespace Close { export namespace Close {
export namespace Commands { export namespace Commands {
export const CLOSE: Command = { export const CLOSE: Command = {
id: 'arduino-close', id: 'arduino-close',
}; };
} }
} }

View File

@@ -11,147 +11,144 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service
import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { import {
MenuModelRegistry, MenuModelRegistry,
MenuContribution, MenuContribution,
} from '@theia/core/lib/common/menu'; } from '@theia/core/lib/common/menu';
import { import {
KeybindingRegistry, KeybindingRegistry,
KeybindingContribution, KeybindingContribution,
} from '@theia/core/lib/browser/keybinding'; } from '@theia/core/lib/browser/keybinding';
import { import {
TabBarToolbarContribution, TabBarToolbarContribution,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
FrontendApplication, FrontendApplication,
} from '@theia/core/lib/browser/frontend-application'; } from '@theia/core/lib/browser/frontend-application';
import { import {
Command, Command,
CommandRegistry, CommandRegistry,
CommandContribution, CommandContribution,
CommandService, CommandService,
} from '@theia/core/lib/common/command'; } from '@theia/core/lib/common/command';
import { EditorMode } from '../editor-mode'; import { EditorMode } from '../editor-mode';
import { SettingsService } from '../settings'; import { SettingsService } from '../settings';
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl'; import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
import { import {
SketchesService, SketchesService,
ConfigService, ConfigService,
FileSystemExt, FileSystemExt,
Sketch, Sketch,
} from '../../common/protocol'; } from '../../common/protocol';
import { ArduinoPreferences } from '../arduino-preferences'; import { ArduinoPreferences } from '../arduino-preferences';
export { export {
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
URI, URI,
Sketch, Sketch,
open, open,
}; };
@injectable() @injectable()
export abstract class Contribution export abstract class Contribution
implements implements
CommandContribution, CommandContribution,
MenuContribution, MenuContribution,
KeybindingContribution, KeybindingContribution,
TabBarToolbarContribution, TabBarToolbarContribution,
FrontendApplicationContribution FrontendApplicationContribution
{ {
@inject(ILogger) @inject(ILogger)
protected readonly logger: ILogger; protected readonly logger: ILogger;
@inject(MessageService) @inject(MessageService)
protected readonly messageService: MessageService; protected readonly messageService: MessageService;
@inject(CommandService) @inject(CommandService)
protected readonly commandService: CommandService; protected readonly commandService: CommandService;
@inject(WorkspaceService) @inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService; protected readonly workspaceService: WorkspaceService;
@inject(EditorMode) @inject(EditorMode)
protected readonly editorMode: EditorMode; protected readonly editorMode: EditorMode;
@inject(LabelProvider) @inject(LabelProvider)
protected readonly labelProvider: LabelProvider; protected readonly labelProvider: LabelProvider;
@inject(SettingsService) @inject(SettingsService)
protected readonly settingsService: SettingsService; protected readonly settingsService: SettingsService;
onStart(app: FrontendApplication): MaybePromise<void> {} onStart(app: FrontendApplication): MaybePromise<void> {}
registerCommands(registry: CommandRegistry): void {} registerCommands(registry: CommandRegistry): void {}
registerMenus(registry: MenuModelRegistry): void {} registerMenus(registry: MenuModelRegistry): void {}
registerKeybindings(registry: KeybindingRegistry): void {} registerKeybindings(registry: KeybindingRegistry): void {}
registerToolbarItems(registry: TabBarToolbarRegistry): void {} registerToolbarItems(registry: TabBarToolbarRegistry): void {}
} }
@injectable() @injectable()
export abstract class SketchContribution extends Contribution { export abstract class SketchContribution extends Contribution {
@inject(FileService) @inject(FileService)
protected readonly fileService: FileService; protected readonly fileService: FileService;
@inject(FileSystemExt) @inject(FileSystemExt)
protected readonly fileSystemExt: FileSystemExt; protected readonly fileSystemExt: FileSystemExt;
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchService: SketchesService; protected readonly sketchService: SketchesService;
@inject(OpenerService) @inject(OpenerService)
protected readonly openerService: OpenerService; protected readonly openerService: OpenerService;
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchServiceClient: SketchesServiceClientImpl; protected readonly sketchServiceClient: SketchesServiceClientImpl;
@inject(ArduinoPreferences) @inject(ArduinoPreferences)
protected readonly preferences: ArduinoPreferences; protected readonly preferences: ArduinoPreferences;
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@inject(OutputChannelManager) @inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager; protected readonly outputChannelManager: OutputChannelManager;
protected async sourceOverride(): Promise<Record<string, string>> { protected async sourceOverride(): Promise<Record<string, string>> {
const override: Record<string, string> = {}; const override: Record<string, string> = {};
const sketch = await this.sketchServiceClient.currentSketch(); const sketch = await this.sketchServiceClient.currentSketch();
if (sketch) { if (sketch) {
for (const editor of this.editorManager.all) { for (const editor of this.editorManager.all) {
const uri = editor.editor.uri; const uri = editor.editor.uri;
if ( if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
Saveable.isDirty(editor) && override[uri.toString()] = editor.editor.document.getText();
Sketch.isInSketch(uri, sketch)
) {
override[uri.toString()] = editor.editor.document.getText();
}
}
} }
return override; }
} }
return override;
}
} }
export namespace Contribution { export namespace Contribution {
export function configure<T>( export function configure<T>(
bind: interfaces.Bind, bind: interfaces.Bind,
serviceIdentifier: typeof Contribution serviceIdentifier: typeof Contribution
): void { ): void {
bind(serviceIdentifier).toSelf().inSingletonScope(); bind(serviceIdentifier).toSelf().inSingletonScope();
bind(CommandContribution).toService(serviceIdentifier); bind(CommandContribution).toService(serviceIdentifier);
bind(MenuContribution).toService(serviceIdentifier); bind(MenuContribution).toService(serviceIdentifier);
bind(KeybindingContribution).toService(serviceIdentifier); bind(KeybindingContribution).toService(serviceIdentifier);
bind(TabBarToolbarContribution).toService(serviceIdentifier); bind(TabBarToolbarContribution).toService(serviceIdentifier);
bind(FrontendApplicationContribution).toService(serviceIdentifier); bind(FrontendApplicationContribution).toService(serviceIdentifier);
} }
} }

View File

@@ -6,164 +6,159 @@ import { NotificationCenter } from '../notification-center';
import { Board, BoardsService, ExecutableService } from '../../common/protocol'; import { Board, BoardsService, ExecutableService } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
URI, URI,
Command, Command,
CommandRegistry, CommandRegistry,
SketchContribution, SketchContribution,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class Debug extends SketchContribution { export class Debug extends SketchContribution {
@inject(HostedPluginSupport) @inject(HostedPluginSupport)
protected hostedPluginSupport: HostedPluginSupport; protected hostedPluginSupport: HostedPluginSupport;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(ExecutableService) @inject(ExecutableService)
protected readonly executableService: ExecutableService; protected readonly executableService: ExecutableService;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardService: BoardsService; protected readonly boardService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider; protected readonly boardsServiceProvider: BoardsServiceProvider;
/** /**
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled. * If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
*/ */
protected _disabledMessages?: string = 'No board selected'; // Initial pessimism. protected _disabledMessages?: string = 'No board selected'; // Initial pessimism.
protected disabledMessageDidChangeEmitter = new Emitter< protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
string | undefined protected onDisabledMessageDidChange =
>(); this.disabledMessageDidChangeEmitter.event;
protected onDisabledMessageDidChange =
this.disabledMessageDidChangeEmitter.event;
protected get disabledMessage(): string | undefined { protected get disabledMessage(): string | undefined {
return this._disabledMessages; return this._disabledMessages;
} }
protected set disabledMessage(message: string | undefined) { protected set disabledMessage(message: string | undefined) {
this._disabledMessages = message; this._disabledMessages = message;
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages); this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
} }
protected readonly debugToolbarItem = { protected readonly debugToolbarItem = {
id: Debug.Commands.START_DEBUGGING.id, id: Debug.Commands.START_DEBUGGING.id,
command: Debug.Commands.START_DEBUGGING.id, command: Debug.Commands.START_DEBUGGING.id,
tooltip: `${ tooltip: `${
this.disabledMessage this.disabledMessage
? `Debug - ${this.disabledMessage}` ? `Debug - ${this.disabledMessage}`
: 'Start Debugging' : 'Start Debugging'
}`, }`,
priority: 3, priority: 3,
onDidChange: this.onDisabledMessageDidChange as Event<void>, onDidChange: this.onDisabledMessageDidChange as Event<void>,
};
onStart(): void {
this.onDisabledMessageDidChange(
() =>
(this.debugToolbarItem.tooltip = `${
this.disabledMessage
? `Debug - ${this.disabledMessage}`
: 'Start Debugging'
}`)
);
const refreshState = async (
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
) => {
if (!board) {
this.disabledMessage = 'No board selected';
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
} else {
this.disabledMessage = undefined;
}
}; };
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
refreshState(selectedBoard)
);
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
}
onStart(): void { registerCommands(registry: CommandRegistry): void {
this.onDisabledMessageDidChange( registry.registerCommand(Debug.Commands.START_DEBUGGING, {
() => execute: () => this.startDebug(),
(this.debugToolbarItem.tooltip = `${ isVisible: (widget) =>
this.disabledMessage ArduinoToolbar.is(widget) && widget.side === 'left',
? `Debug - ${this.disabledMessage}` isEnabled: () => !this.disabledMessage,
: 'Start Debugging' });
}`) }
);
const refreshState = async (
board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
) => {
if (!board) {
this.disabledMessage = 'No board selected';
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = `Platform is not installed for '${board.name}'`;
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
} else {
this.disabledMessage = undefined;
}
};
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
refreshState(selectedBoard)
);
this.notificationCenter.onPlatformInstalled(() => refreshState());
this.notificationCenter.onPlatformUninstalled(() => refreshState());
refreshState();
}
registerCommands(registry: CommandRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerCommand(Debug.Commands.START_DEBUGGING, { registry.registerItem(this.debugToolbarItem);
execute: () => this.startDebug(), }
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.disabledMessage,
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void { protected async startDebug(
registry.registerItem(this.debugToolbarItem); board: Board | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<void> {
if (!board) {
return;
} }
const { name, fqbn } = board;
protected async startDebug( if (!fqbn) {
board: Board | undefined = this.boardsServiceProvider.boardsConfig return;
.selectedBoard
): Promise<void> {
if (!board) {
return;
}
const { name, fqbn } = board;
if (!fqbn) {
return;
}
await this.hostedPluginSupport.didStart;
const [sketch, executables] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!sketch) {
return;
}
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
sketch
);
const [cliPath, sketchPath, configPath] = await Promise.all([
this.fileService.fsPath(new URI(executables.cliUri)),
this.fileService.fsPath(new URI(sketch.uri)),
this.fileService.fsPath(new URI(ideTempFolderUri)),
]);
const config = {
cliPath,
board: {
fqbn,
name,
},
sketchPath,
configPath,
};
return this.commandService.executeCommand(
'arduino.debug.start',
config
);
} }
await this.hostedPluginSupport.didStart;
const [sketch, executables] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!sketch) {
return;
}
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
sketch
);
const [cliPath, sketchPath, configPath] = await Promise.all([
this.fileService.fsPath(new URI(executables.cliUri)),
this.fileService.fsPath(new URI(sketch.uri)),
this.fileService.fsPath(new URI(ideTempFolderUri)),
]);
const config = {
cliPath,
board: {
fqbn,
name,
},
sketchPath,
configPath,
};
return this.commandService.executeCommand('arduino.debug.start', config);
}
} }
export namespace Debug { export namespace Debug {
export namespace Commands { export namespace Commands {
export const START_DEBUGGING: Command = { export const START_DEBUGGING: Command = {
id: 'arduino-start-debug', id: 'arduino-start-debug',
label: 'Start Debugging', label: 'Start Debugging',
category: 'Arduino', category: 'Arduino',
}; };
} }
} }

View File

@@ -4,11 +4,11 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
CommandRegistry, CommandRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
@@ -16,312 +16,305 @@ import { ArduinoMenus } from '../menu/arduino-menus';
// Depends on https://github.com/eclipse-theia/theia/pull/7964 // Depends on https://github.com/eclipse-theia/theia/pull/7964
@injectable() @injectable()
export class EditContributions extends Contribution { export class EditContributions extends Contribution {
@inject(MonacoEditorService) @inject(MonacoEditorService)
protected readonly codeEditorService: MonacoEditorService; protected readonly codeEditorService: MonacoEditorService;
@inject(ClipboardService) @inject(ClipboardService)
protected readonly clipboardService: ClipboardService; protected readonly clipboardService: ClipboardService;
@inject(PreferenceService) @inject(PreferenceService)
protected readonly preferences: PreferenceService; protected readonly preferences: PreferenceService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, { registry.registerCommand(EditContributions.Commands.GO_TO_LINE, {
execute: () => this.run('editor.action.gotoLine'), execute: () => this.run('editor.action.gotoLine'),
}); });
registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, { registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, {
execute: () => this.run('editor.action.commentLine'), execute: () => this.run('editor.action.commentLine'),
}); });
registry.registerCommand(EditContributions.Commands.INDENT_LINES, { registry.registerCommand(EditContributions.Commands.INDENT_LINES, {
execute: () => this.run('editor.action.indentLines'), execute: () => this.run('editor.action.indentLines'),
}); });
registry.registerCommand(EditContributions.Commands.OUTDENT_LINES, { registry.registerCommand(EditContributions.Commands.OUTDENT_LINES, {
execute: () => this.run('editor.action.outdentLines'), execute: () => this.run('editor.action.outdentLines'),
}); });
registry.registerCommand(EditContributions.Commands.FIND, { registry.registerCommand(EditContributions.Commands.FIND, {
execute: () => this.run('actions.find'), execute: () => this.run('actions.find'),
}); });
registry.registerCommand(EditContributions.Commands.FIND_NEXT, { registry.registerCommand(EditContributions.Commands.FIND_NEXT, {
execute: () => this.run('actions.findWithSelection'), execute: () => this.run('actions.findWithSelection'),
}); });
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, { registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, {
execute: () => this.run('editor.action.nextMatchFindAction'), execute: () => this.run('editor.action.nextMatchFindAction'),
}); });
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, { registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
execute: () => execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
this.run('editor.action.previousSelectionMatchFindAction'), });
}); registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
registry.registerCommand( execute: async () => {
EditContributions.Commands.INCREASE_FONT_SIZE, const settings = await this.settingsService.settings();
{ if (settings.autoScaleInterface) {
execute: async () => { settings.interfaceScale = settings.interfaceScale + 1;
const settings = await this.settingsService.settings(); } else {
if (settings.autoScaleInterface) { settings.editorFontSize = settings.editorFontSize + 1;
settings.interfaceScale = settings.interfaceScale + 1; }
} else { await this.settingsService.update(settings);
settings.editorFontSize = settings.editorFontSize + 1; await this.settingsService.save();
} },
await this.settingsService.update(settings); });
await this.settingsService.save(); registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
}, execute: async () => {
} const settings = await this.settingsService.settings();
); if (settings.autoScaleInterface) {
registry.registerCommand( settings.interfaceScale = settings.interfaceScale - 1;
EditContributions.Commands.DECREASE_FONT_SIZE, } else {
{ settings.editorFontSize = settings.editorFontSize - 1;
execute: async () => { }
const settings = await this.settingsService.settings(); await this.settingsService.update(settings);
if (settings.autoScaleInterface) { await this.settingsService.save();
settings.interfaceScale = settings.interfaceScale - 1; },
} else { });
settings.editorFontSize = settings.editorFontSize - 1; /* Tools */ registry.registerCommand(
} EditContributions.Commands.AUTO_FORMAT,
await this.settingsService.update(settings); { execute: () => this.run('editor.action.formatDocument') }
await this.settingsService.save(); );
}, registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
} execute: async () => {
); const value = await this.currentValue();
/* Tools */ registry.registerCommand( if (value !== undefined) {
EditContributions.Commands.AUTO_FORMAT, this.clipboardService.writeText(`[code]
{ execute: () => this.run('editor.action.formatDocument') }
);
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
execute: async () => {
const value = await this.currentValue();
if (value !== undefined) {
this.clipboardService.writeText(`[code]
${value} ${value}
[/code]`); [/code]`);
} }
}, },
}); });
registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, { registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, {
execute: async () => { execute: async () => {
const value = await this.currentValue(); const value = await this.currentValue();
if (value !== undefined) { if (value !== undefined) {
this.clipboardService.writeText(`\`\`\`cpp this.clipboardService.writeText(`\`\`\`cpp
${value} ${value}
\`\`\``); \`\`\``);
}
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.CUT.id,
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.COPY.id,
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
label: 'Copy for Forum',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
label: 'Copy for GitHub',
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.PASTE.id,
order: '4',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.SELECT_ALL.id,
order: '5',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.GO_TO_LINE.id,
label: 'Go to Line...',
order: '6',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
label: 'Comment/Uncomment',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.INDENT_LINES.id,
label: 'Increase Indent',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.OUTDENT_LINES.id,
label: 'Decrease Indent',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: 'Increase Font Size',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
label: 'Decrease Font Size',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: 'Find',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_NEXT.id,
label: 'Find Next',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
label: 'Find Previous',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.USE_FOR_FIND.id,
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
order: '3',
});
// `Tools`
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: EditContributions.Commands.AUTO_FORMAT.id,
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
order: '0',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
keybinding: 'CtrlCmd+Alt+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.GO_TO_LINE.id,
keybinding: 'CtrlCmd+L',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.TOGGLE_COMMENT.id,
keybinding: 'CtrlCmd+/',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=',
});
registry.registerKeybinding({
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND.id,
keybinding: 'CtrlCmd+F',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_NEXT.id,
keybinding: 'CtrlCmd+G',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_PREVIOUS.id,
keybinding: 'CtrlCmd+Shift+G',
});
registry.registerKeybinding({
command: EditContributions.Commands.USE_FOR_FIND.id,
keybinding: 'CtrlCmd+E',
});
// `Tools`
registry.registerKeybinding({
command: EditContributions.Commands.AUTO_FORMAT.id,
keybinding: 'CtrlCmd+T',
});
}
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor()
);
}
protected async currentValue(): Promise<string | undefined> {
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> { registerMenus(registry: MenuModelRegistry): void {
const editor = await this.current(); registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
if (editor) { commandId: CommonCommands.CUT.id,
const action = editor.getAction(commandId); order: '0',
if (action) { });
return action.run(); registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
} commandId: CommonCommands.COPY.id,
} order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
label: 'Copy for Forum',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
label: 'Copy for GitHub',
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.PASTE.id,
order: '4',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: CommonCommands.SELECT_ALL.id,
order: '5',
});
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
commandId: EditContributions.Commands.GO_TO_LINE.id,
label: 'Go to Line...',
order: '6',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
label: 'Comment/Uncomment',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.INDENT_LINES.id,
label: 'Increase Indent',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
commandId: EditContributions.Commands.OUTDENT_LINES.id,
label: 'Decrease Indent',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: 'Increase Font Size',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
label: 'Decrease Font Size',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: 'Find',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_NEXT.id,
label: 'Find Next',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
label: 'Find Previous',
order: '2',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.USE_FOR_FIND.id,
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
order: '3',
});
// `Tools`
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: EditContributions.Commands.AUTO_FORMAT.id,
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
order: '0',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_FORUM.id,
keybinding: 'CtrlCmd+Shift+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
keybinding: 'CtrlCmd+Alt+C',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.GO_TO_LINE.id,
keybinding: 'CtrlCmd+L',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.TOGGLE_COMMENT.id,
keybinding: 'CtrlCmd+/',
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=',
});
registry.registerKeybinding({
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND.id,
keybinding: 'CtrlCmd+F',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_NEXT.id,
keybinding: 'CtrlCmd+G',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND_PREVIOUS.id,
keybinding: 'CtrlCmd+Shift+G',
});
registry.registerKeybinding({
command: EditContributions.Commands.USE_FOR_FIND.id,
keybinding: 'CtrlCmd+E',
});
// `Tools`
registry.registerKeybinding({
command: EditContributions.Commands.AUTO_FORMAT.id,
keybinding: 'CtrlCmd+T',
});
}
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
return (
this.codeEditorService.getFocusedCodeEditor() ||
this.codeEditorService.getActiveCodeEditor()
);
}
protected async currentValue(): Promise<string | undefined> {
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> {
const editor = await this.current();
if (editor) {
const action = editor.getAction(commandId);
if (action) {
return action.run();
}
}
}
} }
export namespace EditContributions { export namespace EditContributions {
export namespace Commands { export namespace Commands {
export const COPY_FOR_FORUM: Command = { export const COPY_FOR_FORUM: Command = {
id: 'arduino-copy-for-forum', id: 'arduino-copy-for-forum',
}; };
export const COPY_FOR_GITHUB: Command = { export const COPY_FOR_GITHUB: Command = {
id: 'arduino-copy-for-github', id: 'arduino-copy-for-github',
}; };
export const GO_TO_LINE: Command = { export const GO_TO_LINE: Command = {
id: 'arduino-go-to-line', id: 'arduino-go-to-line',
}; };
export const TOGGLE_COMMENT: Command = { export const TOGGLE_COMMENT: Command = {
id: 'arduino-toggle-comment', id: 'arduino-toggle-comment',
}; };
export const INDENT_LINES: Command = { export const INDENT_LINES: Command = {
id: 'arduino-indent-lines', id: 'arduino-indent-lines',
}; };
export const OUTDENT_LINES: Command = { export const OUTDENT_LINES: Command = {
id: 'arduino-outdent-lines', id: 'arduino-outdent-lines',
}; };
export const FIND: Command = { export const FIND: Command = {
id: 'arduino-find', id: 'arduino-find',
}; };
export const FIND_NEXT: Command = { export const FIND_NEXT: Command = {
id: 'arduino-find-next', id: 'arduino-find-next',
}; };
export const FIND_PREVIOUS: Command = { export const FIND_PREVIOUS: Command = {
id: 'arduino-find-previous', id: 'arduino-find-previous',
}; };
export const USE_FOR_FIND: Command = { export const USE_FOR_FIND: Command = {
id: 'arduino-for-find', id: 'arduino-for-find',
}; };
export const INCREASE_FONT_SIZE: Command = { export const INCREASE_FONT_SIZE: Command = {
id: 'arduino-increase-font-size', id: 'arduino-increase-font-size',
}; };
export const DECREASE_FONT_SIZE: Command = { export const DECREASE_FONT_SIZE: Command = {
id: 'arduino-decrease-font-size', id: 'arduino-decrease-font-size',
}; };
export const AUTO_FORMAT: Command = { export const AUTO_FORMAT: Command = {
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`. id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
}; };
} }
} }

View File

@@ -2,13 +2,13 @@ import * as PQueue from 'p-queue';
import { inject, injectable, postConstruct } from 'inversify'; import { inject, injectable, postConstruct } from 'inversify';
import { CommandHandler } from '@theia/core/lib/common/command'; import { CommandHandler } from '@theia/core/lib/common/command';
import { import {
MenuPath, MenuPath,
CompositeMenuNode, CompositeMenuNode,
SubMenuOptions, SubMenuOptions,
} from '@theia/core/lib/common/menu'; } from '@theia/core/lib/common/menu';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch'; import { OpenSketch } from './open-sketch';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
@@ -16,243 +16,231 @@ import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService } from '../../common/protocol/examples-service'; import { ExamplesService } from '../../common/protocol/examples-service';
import { import {
SketchContribution, SketchContribution,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
} from './contribution'; } from './contribution';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { Board, Sketch, SketchContainer } from '../../common/protocol'; import { Board, Sketch, SketchContainer } from '../../common/protocol';
@injectable() @injectable()
export abstract class Examples extends SketchContribution { export abstract class Examples extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly menuManager: MainMenuManager; protected readonly menuManager: MainMenuManager;
@inject(ExamplesService) @inject(ExamplesService)
protected readonly examplesService: ExamplesService; protected readonly examplesService: ExamplesService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
@postConstruct() @postConstruct()
init(): void { init(): void {
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
this.handleBoardChanged(selectedBoard) this.handleBoardChanged(selectedBoard)
); );
}
protected handleBoardChanged(board: Board | undefined): void {
// NOOP
}
registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
const groupPath =
index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
parent.addNode(examples);
} catch (e) {
console.error(e);
console.warn('Could not patch menu ordering.');
} }
// Registering the same submenu multiple times has no side-effect.
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
registry.registerSubmenu(ArduinoMenus.FILE__EXAMPLES_SUBMENU, 'Examples', {
order: '4',
});
}
protected handleBoardChanged(board: Board | undefined): void { registerRecursively(
// NOOP sketchContainerOrPlaceholder:
} | SketchContainer
| (Sketch | SketchContainer)[]
| string,
menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection(),
subMenuOptions?: SubMenuOptions | undefined
): void {
if (typeof sketchContainerOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(
menuPath,
sketchContainerOrPlaceholder
);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
pushToDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(placeholder.id)
)
);
} else {
const sketches: Sketch[] = [];
const children: SketchContainer[] = [];
let submenuPath = menuPath;
registerMenus(registry: MenuModelRegistry): void { if (SketchContainer.is(sketchContainerOrPlaceholder)) {
try { const { label } = sketchContainerOrPlaceholder;
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222. submenuPath = [...menuPath, label];
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1; this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index]; sketches.push(...sketchContainerOrPlaceholder.sketches);
const groupPath = children.push(...sketchContainerOrPlaceholder.children);
index === 0 } else {
? [] for (const sketchOrContainer of sketchContainerOrPlaceholder) {
: ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index); if (SketchContainer.is(sketchOrContainer)) {
const parent: CompositeMenuNode = (registry as any).findGroup( children.push(sketchOrContainer);
groupPath } else {
); sketches.push(sketchOrContainer);
const examples = new CompositeMenuNode(menuId, '', { order: '4' }); }
parent.addNode(examples);
} catch (e) {
console.error(e);
console.warn('Could not patch menu ordering.');
} }
// Registering the same submenu multiple times has no side-effect. }
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300 children.forEach((child) =>
registry.registerSubmenu( this.registerRecursively(child, submenuPath, pushToDispose)
ArduinoMenus.FILE__EXAMPLES_SUBMENU, );
'Examples', for (const sketch of sketches) {
{ order: '4' } const { uri } = sketch;
const commandId = `arduino-open-example-${submenuPath.join(
':'
)}--${uri}`;
const command = { id: commandId };
const handler = this.createHandler(uri);
pushToDispose.push(
this.commandRegistry.registerCommand(command, handler)
); );
this.menuRegistry.registerMenuAction(submenuPath, {
commandId,
label: sketch.name,
order: sketch.name.toLocaleLowerCase(),
});
pushToDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
);
}
} }
}
registerRecursively( protected createHandler(uri: string): CommandHandler {
sketchContainerOrPlaceholder: return {
| SketchContainer execute: async () => {
| (Sketch | SketchContainer)[] const sketch = await this.sketchService.cloneExample(uri);
| string, return this.commandService.executeCommand(
menuPath: MenuPath, OpenSketch.Commands.OPEN_SKETCH.id,
pushToDispose: DisposableCollection = new DisposableCollection(), sketch
subMenuOptions?: SubMenuOptions | undefined );
): void { },
if (typeof sketchContainerOrPlaceholder === 'string') { };
const placeholder = new PlaceholderMenuNode( }
menuPath,
sketchContainerOrPlaceholder
);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
pushToDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(placeholder.id)
)
);
} else {
const sketches: Sketch[] = [];
const children: SketchContainer[] = [];
let submenuPath = menuPath;
if (SketchContainer.is(sketchContainerOrPlaceholder)) {
const { label } = sketchContainerOrPlaceholder;
submenuPath = [...menuPath, label];
this.menuRegistry.registerSubmenu(
submenuPath,
label,
subMenuOptions
);
sketches.push(...sketchContainerOrPlaceholder.sketches);
children.push(...sketchContainerOrPlaceholder.children);
} else {
for (const sketchOrContainer of sketchContainerOrPlaceholder) {
if (SketchContainer.is(sketchOrContainer)) {
children.push(sketchOrContainer);
} else {
sketches.push(sketchOrContainer);
}
}
}
children.forEach((child) =>
this.registerRecursively(child, submenuPath, pushToDispose)
);
for (const sketch of sketches) {
const { uri } = sketch;
const commandId = `arduino-open-example-${submenuPath.join(
':'
)}--${uri}`;
const command = { id: commandId };
const handler = this.createHandler(uri);
pushToDispose.push(
this.commandRegistry.registerCommand(command, handler)
);
this.menuRegistry.registerMenuAction(submenuPath, {
commandId,
label: sketch.name,
order: sketch.name.toLocaleLowerCase(),
});
pushToDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
);
}
}
}
protected createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
},
};
}
} }
@injectable() @injectable()
export class BuiltInExamples extends Examples { export class BuiltInExamples extends Examples {
onStart(): void { onStart(): void {
this.register(); // no `await` this.register(); // no `await`
} }
protected async register(): Promise<void> { protected async register(): Promise<void> {
let sketchContainers: SketchContainer[] | undefined; let sketchContainers: SketchContainer[] | undefined;
try { try {
sketchContainers = await this.examplesService.builtIns(); sketchContainers = await this.examplesService.builtIns();
} catch (e) { } catch (e) {
console.error('Could not initialize built-in examples.', e); console.error('Could not initialize built-in examples.', e);
this.messageService.error( this.messageService.error('Could not initialize built-in examples.');
'Could not initialize built-in examples.' return;
);
return;
}
this.toDispose.dispose();
for (const container of ['Built-in examples', ...sketchContainers]) {
this.registerRecursively(
container,
ArduinoMenus.EXAMPLES__BUILT_IN_GROUP,
this.toDispose
);
}
this.menuManager.update();
} }
this.toDispose.dispose();
for (const container of ['Built-in examples', ...sketchContainers]) {
this.registerRecursively(
container,
ArduinoMenus.EXAMPLES__BUILT_IN_GROUP,
this.toDispose
);
}
this.menuManager.update();
}
} }
@injectable() @injectable()
export class LibraryExamples extends Examples { export class LibraryExamples extends Examples {
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
onStart(): void { onStart(): void {
this.register(); // no `await` this.register(); // no `await`
this.notificationCenter.onLibraryInstalled(() => this.register()); this.notificationCenter.onLibraryInstalled(() => this.register());
this.notificationCenter.onLibraryUninstalled(() => this.register()); this.notificationCenter.onLibraryUninstalled(() => this.register());
} }
protected handleBoardChanged(board: Board | undefined): void { protected handleBoardChanged(board: Board | undefined): void {
this.register(board); this.register(board);
} }
protected async register( protected async register(
board: Board | undefined = this.boardsServiceClient.boardsConfig board: Board | undefined = this.boardsServiceClient.boardsConfig
.selectedBoard .selectedBoard
): Promise<void> { ): Promise<void> {
return this.queue.add(async () => { return this.queue.add(async () => {
this.toDispose.dispose(); this.toDispose.dispose();
const fqbn = board?.fqbn; const fqbn = board?.fqbn;
const name = board?.name; const name = board?.name;
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed. // Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
const { user, current, any } = await this.examplesService.installed( const { user, current, any } = await this.examplesService.installed({
{ fqbn } fqbn,
); });
if (user.length) { if (user.length) {
(user as any).unshift('Examples from Custom Libraries'); (user as any).unshift('Examples from Custom Libraries');
} }
if (name && fqbn && current.length) { if (name && fqbn && current.length) {
(current as any).unshift(`Examples for ${name}`); (current as any).unshift(`Examples for ${name}`);
} }
if (any.length) { if (any.length) {
(any as any).unshift('Examples for any board'); (any as any).unshift('Examples for any board');
} }
for (const container of user) { for (const container of user) {
this.registerRecursively( this.registerRecursively(
container, container,
ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP,
this.toDispose this.toDispose
); );
} }
for (const container of current) { for (const container of current) {
this.registerRecursively( this.registerRecursively(
container, container,
ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP, ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP,
this.toDispose this.toDispose
); );
} }
for (const container of any) { for (const container of any) {
this.registerRecursively( this.registerRecursively(
container, container,
ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP, ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP,
this.toDispose this.toDispose
); );
} }
this.menuManager.update(); this.menuManager.update();
}); });
} }
} }

View File

@@ -6,167 +6,160 @@ import { CommandHandler } from '@theia/core/lib/common/command';
import { QuickInputService } from '@theia/core/lib/browser/quick-open/quick-input-service'; import { QuickInputService } from '@theia/core/lib/browser/quick-open/quick-input-service';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
CommandRegistry, CommandRegistry,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class Help extends Contribution { export class Help extends Contribution {
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@inject(WindowService) @inject(WindowService)
protected readonly windowService: WindowService; protected readonly windowService: WindowService;
@inject(QuickInputService) @inject(QuickInputService)
protected readonly quickInputService: QuickInputService; protected readonly quickInputService: QuickInputService;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
const open = (url: string) => const open = (url: string) =>
this.windowService.openNewWindow(url, { external: true }); this.windowService.openNewWindow(url, { external: true });
const createOpenHandler = (url: string) => const createOpenHandler = (url: string) =>
<CommandHandler>{ <CommandHandler>{
execute: () => open(url), execute: () => open(url),
}; };
registry.registerCommand( registry.registerCommand(
Help.Commands.GETTING_STARTED, Help.Commands.GETTING_STARTED,
createOpenHandler('https://www.arduino.cc/en/Guide') createOpenHandler('https://www.arduino.cc/en/Guide')
); );
registry.registerCommand( registry.registerCommand(
Help.Commands.ENVIRONMENT, Help.Commands.ENVIRONMENT,
createOpenHandler('https://www.arduino.cc/en/Guide/Environment') createOpenHandler('https://www.arduino.cc/en/Guide/Environment')
); );
registry.registerCommand( registry.registerCommand(
Help.Commands.TROUBLESHOOTING, Help.Commands.TROUBLESHOOTING,
createOpenHandler('https://support.arduino.cc/hc/en-us') createOpenHandler('https://support.arduino.cc/hc/en-us')
); );
registry.registerCommand( registry.registerCommand(
Help.Commands.REFERENCE, Help.Commands.REFERENCE,
createOpenHandler('https://www.arduino.cc/reference/en/') createOpenHandler('https://www.arduino.cc/reference/en/')
); );
registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, { registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, {
execute: async () => { execute: async () => {
let searchFor: string | undefined = undefined; let searchFor: string | undefined = undefined;
const { currentEditor } = this.editorManager; const { currentEditor } = this.editorManager;
if ( if (currentEditor && currentEditor.editor instanceof MonacoEditor) {
currentEditor && const codeEditor = currentEditor.editor.getControl();
currentEditor.editor instanceof MonacoEditor const selection = codeEditor.getSelection();
) { const model = codeEditor.getModel();
const codeEditor = currentEditor.editor.getControl(); if (model && selection && !monaco.Range.isEmpty(selection)) {
const selection = codeEditor.getSelection(); searchFor = model.getValueInRange(selection);
const model = codeEditor.getModel(); }
if ( }
model && if (!searchFor) {
selection && searchFor = await this.quickInputService.open({
!monaco.Range.isEmpty(selection) prompt: 'Search on Arduino.cc',
) { placeHolder: 'Type a keyword',
searchFor = model.getValueInRange(selection); });
} }
} if (searchFor) {
if (!searchFor) { return open(
searchFor = await this.quickInputService.open({ `https://www.arduino.cc/search?q=${encodeURIComponent(
prompt: 'Search on Arduino.cc', searchFor
placeHolder: 'Type a keyword', )}&tab=reference`
}); );
} }
if (searchFor) { },
return open( });
`https://www.arduino.cc/search?q=${encodeURIComponent( registry.registerCommand(
searchFor Help.Commands.FAQ,
)}&tab=reference` createOpenHandler('https://support.arduino.cc/hc/en-us')
); );
} registry.registerCommand(
}, Help.Commands.VISIT_ARDUINO,
}); createOpenHandler('https://www.arduino.cc/')
registry.registerCommand( );
Help.Commands.FAQ, }
createOpenHandler('https://support.arduino.cc/hc/en-us')
);
registry.registerCommand(
Help.Commands.VISIT_ARDUINO,
createOpenHandler('https://www.arduino.cc/')
);
}
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.GETTING_STARTED.id, commandId: Help.Commands.GETTING_STARTED.id,
order: '0', order: '0',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.ENVIRONMENT.id, commandId: Help.Commands.ENVIRONMENT.id,
order: '1', order: '1',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.TROUBLESHOOTING.id, commandId: Help.Commands.TROUBLESHOOTING.id,
order: '2', order: '2',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
commandId: Help.Commands.REFERENCE.id, commandId: Help.Commands.REFERENCE.id,
order: '3', order: '3',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.FIND_IN_REFERENCE.id, commandId: Help.Commands.FIND_IN_REFERENCE.id,
order: '4', order: '4',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.FAQ.id, commandId: Help.Commands.FAQ.id,
order: '5', order: '5',
}); });
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, { registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
commandId: Help.Commands.VISIT_ARDUINO.id, commandId: Help.Commands.VISIT_ARDUINO.id,
order: '6', order: '6',
}); });
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: Help.Commands.FIND_IN_REFERENCE.id, command: Help.Commands.FIND_IN_REFERENCE.id,
keybinding: 'CtrlCmd+Shift+F', keybinding: 'CtrlCmd+Shift+F',
}); });
} }
} }
export namespace Help { export namespace Help {
export namespace Commands { export namespace Commands {
export const GETTING_STARTED: Command = { export const GETTING_STARTED: Command = {
id: 'arduino-getting-started', id: 'arduino-getting-started',
label: 'Getting Started', label: 'Getting Started',
category: 'Arduino', category: 'Arduino',
}; };
export const ENVIRONMENT: Command = { export const ENVIRONMENT: Command = {
id: 'arduino-environment', id: 'arduino-environment',
label: 'Environment', label: 'Environment',
category: 'Arduino', category: 'Arduino',
}; };
export const TROUBLESHOOTING: Command = { export const TROUBLESHOOTING: Command = {
id: 'arduino-troubleshooting', id: 'arduino-troubleshooting',
label: 'Troubleshooting', label: 'Troubleshooting',
category: 'Arduino', category: 'Arduino',
}; };
export const REFERENCE: Command = { export const REFERENCE: Command = {
id: 'arduino-reference', id: 'arduino-reference',
label: 'Reference', label: 'Reference',
category: 'Arduino', category: 'Arduino',
}; };
export const FIND_IN_REFERENCE: Command = { export const FIND_IN_REFERENCE: Command = {
id: 'arduino-find-in-reference', id: 'arduino-find-in-reference',
label: 'Find in Reference', label: 'Find in Reference',
category: 'Arduino', category: 'Arduino',
}; };
export const FAQ: Command = { export const FAQ: Command = {
id: 'arduino-faq', id: 'arduino-faq',
label: 'Frequently Asked Questions', label: 'Frequently Asked Questions',
category: 'Arduino', category: 'Arduino',
}; };
export const VISIT_ARDUINO: Command = { export const VISIT_ARDUINO: Command = {
id: 'arduino-visit-arduino', id: 'arduino-visit-arduino',
label: 'Visit Arduino.cc', label: 'Visit Arduino.cc',
category: 'Arduino', category: 'Arduino',
}; };
} }
} }

View File

@@ -5,8 +5,8 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser'; import { EditorManager } from '@theia/editor/lib/browser';
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu'; import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { LibraryPackage, LibraryService } from '../../common/protocol'; import { LibraryPackage, LibraryService } from '../../common/protocol';
@@ -18,209 +18,198 @@ import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class IncludeLibrary extends SketchContribution { export class IncludeLibrary extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(LibraryService) @inject(LibraryService)
protected readonly libraryService: LibraryService; protected readonly libraryService: LibraryService;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
onStart(): void { onStart(): void {
this.updateMenuActions(); this.updateMenuActions();
this.boardsServiceClient.onBoardsConfigChanged(() => this.boardsServiceClient.onBoardsConfigChanged(() =>
this.updateMenuActions() this.updateMenuActions()
); );
this.notificationCenter.onLibraryInstalled(() => this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
this.updateMenuActions() this.notificationCenter.onLibraryUninstalled(() =>
); this.updateMenuActions()
this.notificationCenter.onLibraryUninstalled(() => );
this.updateMenuActions() }
);
registerMenus(registry: MenuModelRegistry): void {
// `Include Library` submenu
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1',
});
// `Manage Libraries...` group.
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
label: 'Manage Libraries...',
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
execute: async (arg) => {
if (LibraryPackage.is(arg)) {
this.includeLibrary(arg);
}
},
});
}
protected async updateMenuActions(): Promise<void> {
return this.queue.add(async () => {
this.toDispose.dispose();
this.mainMenuManager.update();
const libraries: LibraryPackage[] = [];
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
// Show all libraries, when no board is selected.
// Otherwise, show libraries only for the selected board.
libraries.push(...(await this.libraryService.list({ fqbn })));
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
// `Add .ZIP Library...`
// TODO: implement it
// `Arduino libraries`
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
const { user, rest } = LibraryPackage.groupByLocation(libraries);
if (rest.length) {
(rest as any).unshift('Arduino libraries');
}
if (user.length) {
(user as any).unshift('Contributed libraries');
}
for (const library of user) {
this.toDispose.push(this.registerLibrary(library, userMenuPath));
}
for (const library of rest) {
this.toDispose.push(this.registerLibrary(library, packageMenuPath));
}
this.mainMenuManager.update();
});
}
protected registerLibrary(
libraryOrPlaceholder: LibraryPackage | string,
menuPath: MenuPath
): Disposable {
if (typeof libraryOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(
menuPath,
libraryOrPlaceholder
);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
return Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(placeholder.id)
);
}
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
const command = { id: commandId };
const handler = {
execute: () =>
this.commandRegistry.executeCommand(
IncludeLibrary.Commands.INCLUDE_LIBRARY.id,
libraryOrPlaceholder
),
};
const menuAction = { commandId, label: libraryOrPlaceholder.name };
this.menuRegistry.registerMenuAction(menuPath, menuAction);
return new DisposableCollection(
this.commandRegistry.registerCommand(command, handler),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(menuAction)
)
);
}
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.
// Otherwise, we pick the editor of the main sketch file.
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
const editor = this.editorManager.currentEditor?.editor;
if (editor instanceof MonacoEditor) {
if (
sketch.additionalFileUris.some((uri) => uri === editor.uri.toString())
) {
codeEditor = editor.getControl();
}
} }
registerMenus(registry: MenuModelRegistry): void { if (!codeEditor) {
// `Include Library` submenu const widget = await this.editorManager.open(new URI(sketch.mainFileUri));
const includeLibMenuPath = [ if (widget.editor instanceof MonacoEditor) {
...ArduinoMenus.SKETCH__UTILS_GROUP, codeEditor = widget.editor.getControl();
'0_include', }
];
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
order: '1',
});
// `Manage Libraries...` group.
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
label: 'Manage Libraries...',
});
} }
registerCommands(registry: CommandRegistry): void { if (!codeEditor) {
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, { return;
execute: async (arg) => {
if (LibraryPackage.is(arg)) {
this.includeLibrary(arg);
}
},
});
} }
protected async updateMenuActions(): Promise<void> { const textModel = codeEditor.getModel();
return this.queue.add(async () => { if (!textModel) {
this.toDispose.dispose(); return;
this.mainMenuManager.update();
const libraries: LibraryPackage[] = [];
const fqbn =
this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
// Show all libraries, when no board is selected.
// Otherwise, show libraries only for the selected board.
libraries.push(...(await this.libraryService.list({ fqbn })));
const includeLibMenuPath = [
...ArduinoMenus.SKETCH__UTILS_GROUP,
'0_include',
];
// `Add .ZIP Library...`
// TODO: implement it
// `Arduino libraries`
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
const { user, rest } = LibraryPackage.groupByLocation(libraries);
if (rest.length) {
(rest as any).unshift('Arduino libraries');
}
if (user.length) {
(user as any).unshift('Contributed libraries');
}
for (const library of user) {
this.toDispose.push(
this.registerLibrary(library, userMenuPath)
);
}
for (const library of rest) {
this.toDispose.push(
this.registerLibrary(library, packageMenuPath)
);
}
this.mainMenuManager.update();
});
}
protected registerLibrary(
libraryOrPlaceholder: LibraryPackage | string,
menuPath: MenuPath
): Disposable {
if (typeof libraryOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(
menuPath,
libraryOrPlaceholder
);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
return Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(placeholder.id)
);
}
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
const command = { id: commandId };
const handler = {
execute: () =>
this.commandRegistry.executeCommand(
IncludeLibrary.Commands.INCLUDE_LIBRARY.id,
libraryOrPlaceholder
),
};
const menuAction = { commandId, label: libraryOrPlaceholder.name };
this.menuRegistry.registerMenuAction(menuPath, menuAction);
return new DisposableCollection(
this.commandRegistry.registerCommand(command, handler),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(menuAction)
)
);
}
protected async includeLibrary(library: LibraryPackage): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
// If the current editor is one of the additional files from the sketch, we use that.
// Otherwise, we pick the editor of the main sketch file.
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
const editor = this.editorManager.currentEditor?.editor;
if (editor instanceof MonacoEditor) {
if (
sketch.additionalFileUris.some(
(uri) => uri === editor.uri.toString()
)
) {
codeEditor = editor.getControl();
}
}
if (!codeEditor) {
const widget = await this.editorManager.open(
new URI(sketch.mainFileUri)
);
if (widget.editor instanceof MonacoEditor) {
codeEditor = widget.editor.getControl();
}
}
if (!codeEditor) {
return;
}
const textModel = codeEditor.getModel();
if (!textModel) {
return;
}
const cursorState = codeEditor.getSelections() || [];
const eol = textModel.getEOL();
const includes = library.includes.slice();
includes.push(''); // For the trailing new line.
const text = includes
.map((include) => (include ? `#include <${include}>` : eol))
.join(eol);
textModel.pushStackElement(); // Start a fresh operation.
textModel.pushEditOperations(
cursorState,
[
{
range: new monaco.Range(1, 1, 1, 1),
text,
forceMoveMarkers: true,
},
],
() => cursorState
);
textModel.pushStackElement(); // Make it undoable.
} }
const cursorState = codeEditor.getSelections() || [];
const eol = textModel.getEOL();
const includes = library.includes.slice();
includes.push(''); // For the trailing new line.
const text = includes
.map((include) => (include ? `#include <${include}>` : eol))
.join(eol);
textModel.pushStackElement(); // Start a fresh operation.
textModel.pushEditOperations(
cursorState,
[
{
range: new monaco.Range(1, 1, 1, 1),
text,
forceMoveMarkers: true,
},
],
() => cursorState
);
textModel.pushStackElement(); // Make it undoable.
}
} }
export namespace IncludeLibrary { export namespace IncludeLibrary {
export namespace Commands { export namespace Commands {
export const INCLUDE_LIBRARY: Command = { export const INCLUDE_LIBRARY: Command = {
id: 'arduino-include-library', id: 'arduino-include-library',
}; };
} }
} }

View File

@@ -2,70 +2,69 @@ import { injectable } from 'inversify';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
SketchContribution, SketchContribution,
URI, URI,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class NewSketch extends SketchContribution { export class NewSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, { registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch(), execute: () => this.newSketch(),
}); });
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, { registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id), });
}); }
}
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id, commandId: NewSketch.Commands.NEW_SKETCH.id,
label: 'New', label: 'New',
order: '0', order: '0',
}); });
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: NewSketch.Commands.NEW_SKETCH.id, command: NewSketch.Commands.NEW_SKETCH.id,
keybinding: 'CtrlCmd+N', keybinding: 'CtrlCmd+N',
}); });
} }
registerToolbarItems(registry: TabBarToolbarRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({ registry.registerItem({
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id, id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id, command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
tooltip: 'New', tooltip: 'New',
priority: 3, priority: 3,
}); });
} }
async newSketch(): Promise<void> { async newSketch(): Promise<void> {
try { try {
const sketch = await this.sketchService.createNewSketch(); const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri)); this.workspaceService.open(new URI(sketch.uri));
} catch (e) { } catch (e) {
await this.messageService.error(e.toString()); await this.messageService.error(e.toString());
}
} }
}
} }
export namespace NewSketch { export namespace NewSketch {
export namespace Commands { export namespace Commands {
export const NEW_SKETCH: Command = { export const NEW_SKETCH: Command = {
id: 'arduino-new-sketch', id: 'arduino-new-sketch',
}; };
export const NEW_SKETCH__TOOLBAR: Command = { export const NEW_SKETCH__TOOLBAR: Command = {
id: 'arduino-new-sketch--toolbar', id: 'arduino-new-sketch--toolbar',
}; };
} }
} }

View File

@@ -1,14 +1,14 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol'; import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { import {
SketchContribution, SketchContribution,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
Sketch, Sketch,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
@@ -17,78 +17,78 @@ import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class OpenRecentSketch extends SketchContribution { export class OpenRecentSketch extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(WorkspaceServer) @inject(WorkspaceServer)
protected readonly workspaceServer: WorkspaceServer; protected readonly workspaceServer: WorkspaceServer;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>(); protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
onStart(): void { onStart(): void {
const refreshMenu = (sketches: Sketch[]) => { const refreshMenu = (sketches: Sketch[]) => {
this.register(sketches); this.register(sketches);
this.mainMenuManager.update(); this.mainMenuManager.update();
}; };
this.notificationCenter.onRecentSketchesChanged(({ sketches }) => this.notificationCenter.onRecentSketchesChanged(({ sketches }) =>
refreshMenu(sketches) refreshMenu(sketches)
); );
this.sketchService.recentlyOpenedSketches().then(refreshMenu); this.sketchService.recentlyOpenedSketches().then(refreshMenu);
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu( registry.registerSubmenu(
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
'Open Recent', 'Open Recent',
{ order: '2' } { order: '2' }
); );
} }
protected register(sketches: Sketch[]): void { protected register(sketches: Sketch[]): void {
const order = 0; const order = 0;
for (const sketch of sketches) { for (const sketch of sketches) {
const { uri } = sketch; const { uri } = sketch;
const toDispose = this.toDisposeBeforeRegister.get(uri); const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) { if (toDispose) {
toDispose.dispose(); toDispose.dispose();
} }
const command = { id: `arduino-open-recent--${uri}` }; const command = { id: `arduino-open-recent--${uri}` };
const handler = { const handler = {
execute: () => execute: () =>
this.commandRegistry.executeCommand( this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id, OpenSketch.Commands.OPEN_SKETCH.id,
sketch sketch
), ),
}; };
this.commandRegistry.registerCommand(command, handler); this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
{ {
commandId: command.id, commandId: command.id,
label: sketch.name, label: sketch.name,
order: String(order), order: String(order),
}
);
this.toDisposeBeforeRegister.set(
sketch.uri,
new DisposableCollection(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
)
);
} }
);
this.toDisposeBeforeRegister.set(
sketch.uri,
new DisposableCollection(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
),
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
)
);
} }
}
} }

View File

@@ -3,54 +3,54 @@ import { remote } from 'electron';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class OpenSketchExternal extends SketchContribution { export class OpenSketchExternal extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, { registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
execute: () => this.openExternal(), execute: () => this.openExternal(),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, { registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id, commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
label: 'Show Sketch Folder', label: 'Show Sketch Folder',
order: '0', order: '0',
}); });
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id, command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
keybinding: 'CtrlCmd+Alt+K', keybinding: 'CtrlCmd+Alt+K',
}); });
} }
protected async openExternal(): Promise<void> { protected async openExternal(): Promise<void> {
const uri = await this.sketchServiceClient.currentSketchFile(); const uri = await this.sketchServiceClient.currentSketchFile();
if (uri) { if (uri) {
const exists = this.fileService.exists(new URI(uri)); const exists = this.fileService.exists(new URI(uri));
if (exists) { if (exists) {
const fsPath = await this.fileService.fsPath(new URI(uri)); const fsPath = await this.fileService.fsPath(new URI(uri));
if (fsPath) { if (fsPath) {
remote.shell.showItemInFolder(fsPath); remote.shell.showItemInFolder(fsPath);
}
}
} }
}
} }
}
} }
export namespace OpenSketchExternal { export namespace OpenSketchExternal {
export namespace Commands { export namespace Commands {
export const OPEN_EXTERNAL: Command = { export const OPEN_EXTERNAL: Command = {
id: 'arduino-open-sketch-external', id: 'arduino-open-sketch-external',
}; };
} }
} }

View File

@@ -3,20 +3,20 @@ import { remote } from 'electron';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser'; import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
SketchContribution, SketchContribution,
Sketch, Sketch,
URI, URI,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service'; import { ExamplesService } from '../../common/protocol/examples-service';
import { BuiltInExamples } from './examples'; import { BuiltInExamples } from './examples';
@@ -25,204 +25,194 @@ import { SketchContainer } from '../../common/protocol';
@injectable() @injectable()
export class OpenSketch extends SketchContribution { export class OpenSketch extends SketchContribution {
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer) @inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer; protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(BuiltInExamples) @inject(BuiltInExamples)
protected readonly builtInExamples: BuiltInExamples; protected readonly builtInExamples: BuiltInExamples;
@inject(ExamplesService) @inject(ExamplesService)
protected readonly examplesService: ExamplesService; protected readonly examplesService: ExamplesService;
@inject(Sketchbook) @inject(Sketchbook)
protected readonly sketchbook: Sketchbook; protected readonly sketchbook: Sketchbook;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, { registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
execute: (arg) => execute: (arg) =>
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(), Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
});
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (_: Widget, target: EventTarget) => {
const container = await this.sketchService.getSketches({
exclude: ['**/hardware/**'],
}); });
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, { if (SketchContainer.isEmpty(container)) {
isVisible: (widget) => this.openSketch();
ArduinoToolbar.is(widget) && widget.side === 'left', } else {
execute: async (_: Widget, target: EventTarget) => { this.toDispose.dispose();
const container = await this.sketchService.getSketches({ if (!(target instanceof HTMLElement)) {
exclude: ['**/hardware/**'], return;
}); }
if (SketchContainer.isEmpty(container)) { const { parentElement } = target;
this.openSketch(); if (!parentElement) {
} else { return;
this.toDispose.dispose(); }
if (!(target instanceof HTMLElement)) {
return;
}
const { parentElement } = target;
if (!parentElement) {
return;
}
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP, ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
{ {
commandId: OpenSketch.Commands.OPEN_SKETCH.id, commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...', label: 'Open...',
}
);
this.toDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
OpenSketch.Commands.OPEN_SKETCH
)
)
);
this.sketchbook.registerRecursively(
[...container.children, ...container.sketches],
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
this.toDispose
);
try {
const containers =
await this.examplesService.builtIns();
for (const container of containers) {
this.builtInExamples.registerRecursively(
container,
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
this.toDispose
);
}
} catch (e) {
console.error(
'Error when collecting built-in examples.',
e
);
}
const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: {
x: parentElement.getBoundingClientRect().left,
y:
parentElement.getBoundingClientRect().top +
parentElement.offsetHeight,
},
};
this.contextMenuRenderer.render(options);
}
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...',
order: '1',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
tooltip: 'Open',
priority: 4,
});
}
async openSketch(
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
): Promise<void> {
const sketch = await toOpen;
if (sketch) {
this.workspaceService.open(new URI(sketch.uri));
}
}
protected async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: 'Sketch',
extensions: ['ino', 'pde'],
},
],
});
if (!filePaths.length) {
return undefined;
}
if (filePaths.length > 1) {
this.logger.warn(
`Multiple sketches were selected: ${filePaths}. Using the first one.`
);
}
const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
if (sketch) {
return sketch;
}
if (Sketch.isSketchFile(sketchFileUri)) {
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.fileService.exists(newSketchUri);
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.fileService.createFolder(newSketchUri);
await this.fileService.move(
new URI(sketchFileUri),
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return this.sketchService.getSketchFolder(
newSketchUri.toString()
);
} }
);
this.toDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
OpenSketch.Commands.OPEN_SKETCH
)
)
);
this.sketchbook.registerRecursively(
[...container.children, ...container.sketches],
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
this.toDispose
);
try {
const containers = await this.examplesService.builtIns();
for (const container of containers) {
this.builtInExamples.registerRecursively(
container,
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
this.toDispose
);
}
} catch (e) {
console.error('Error when collecting built-in examples.', e);
}
const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: {
x: parentElement.getBoundingClientRect().left,
y:
parentElement.getBoundingClientRect().top +
parentElement.offsetHeight,
},
};
this.contextMenuRenderer.render(options);
} }
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: 'Open...',
order: '1',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: OpenSketch.Commands.OPEN_SKETCH.id,
keybinding: 'CtrlCmd+O',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
tooltip: 'Open',
priority: 4,
});
}
async openSketch(
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
): Promise<void> {
const sketch = await toOpen;
if (sketch) {
this.workspaceService.open(new URI(sketch.uri));
} }
}
protected async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: 'Sketch',
extensions: ['ino', 'pde'],
},
],
});
if (!filePaths.length) {
return undefined;
}
if (filePaths.length > 1) {
this.logger.warn(
`Multiple sketches were selected: ${filePaths}. Using the first one.`
);
}
const sketchFilePath = filePaths[0];
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
if (sketch) {
return sketch;
}
if (Sketch.isSketchFile(sketchFileUri)) {
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.fileService.exists(newSketchUri);
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.fileService.createFolder(newSketchUri);
await this.fileService.move(
new URI(sketchFileUri),
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return this.sketchService.getSketchFolder(newSketchUri.toString());
}
}
}
} }
export namespace OpenSketch { export namespace OpenSketch {
export namespace Commands { export namespace Commands {
export const OPEN_SKETCH: Command = { export const OPEN_SKETCH: Command = {
id: 'arduino-open-sketch', id: 'arduino-open-sketch',
}; };
export const OPEN_SKETCH__TOOLBAR: Command = { export const OPEN_SKETCH__TOOLBAR: Command = {
id: 'arduino-open-sketch--toolbar', id: 'arduino-open-sketch--toolbar',
}; };
} }
} }

View File

@@ -2,49 +2,49 @@ import { injectable } from 'inversify';
import { remote } from 'electron'; import { remote } from 'electron';
import { isOSX } from '@theia/core/lib/common/os'; import { isOSX } from '@theia/core/lib/common/os';
import { import {
Contribution, Contribution,
Command, Command,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
CommandRegistry, CommandRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
@injectable() @injectable()
export class QuitApp extends Contribution { export class QuitApp extends Contribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
if (!isOSX) { if (!isOSX) {
registry.registerCommand(QuitApp.Commands.QUIT_APP, { registry.registerCommand(QuitApp.Commands.QUIT_APP, {
execute: () => remote.app.quit(), execute: () => remote.app.quit(),
}); });
}
} }
}
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it. // On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
if (!isOSX) { if (!isOSX) {
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
commandId: QuitApp.Commands.QUIT_APP.id, commandId: QuitApp.Commands.QUIT_APP.id,
label: 'Quit', label: 'Quit',
order: '0', order: '0',
}); });
}
} }
}
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
if (!isOSX) { if (!isOSX) {
registry.registerKeybinding({ registry.registerKeybinding({
command: QuitApp.Commands.QUIT_APP.id, command: QuitApp.Commands.QUIT_APP.id,
keybinding: 'CtrlCmd+Q', keybinding: 'CtrlCmd+Q',
}); });
}
} }
}
} }
export namespace QuitApp { export namespace QuitApp {
export namespace Commands { export namespace Commands {
export const QUIT_APP: Command = { export const QUIT_APP: Command = {
id: 'arduino-quit-app', id: 'arduino-quit-app',
}; };
} }
} }

View File

@@ -3,129 +3,126 @@ import { remote } from 'electron';
import * as dateFormat from 'dateformat'; import * as dateFormat from 'dateformat';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { import {
SketchContribution, SketchContribution,
URI, URI,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class SaveAsSketch extends SketchContribution { export class SaveAsSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, { registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
execute: (args) => this.saveAs(args), execute: (args) => this.saveAs(args),
}); });
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
label: 'Save As...',
order: '7',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
keybinding: 'CtrlCmd+Shift+S',
});
}
/**
* Resolves `true` if the sketch was successfully saved as something.
*/
async saveAs(
{
execOnlyIfTemp,
openAfterMove,
wipeOriginal,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return false;
} }
registerMenus(registry: MenuModelRegistry): void { const isTemp = await this.sketchService.isTemp(sketch);
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { if (!isTemp && !!execOnlyIfTemp) {
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id, return false;
label: 'Save As...',
order: '7',
});
} }
registerKeybindings(registry: KeybindingRegistry): void { // If target does not exist, propose a `directories.user`/${sketch.name} path
registry.registerKeybinding({ // If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id, const sketchDirUri = new URI(
keybinding: 'CtrlCmd+Shift+S', (await this.configService.getConfiguration()).sketchDirUri
}); );
const exists = await this.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = exists
? sketchDirUri.resolve(
sketchDirUri
.resolve(
`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
)
.toString()
)
: sketchDirUri.resolve(sketch.name);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
defaultPath,
});
if (!filePath || canceled) {
return false;
} }
const destinationUri = await this.fileSystemExt.getUri(filePath);
/** if (!destinationUri) {
* Resolves `true` if the sketch was successfully saved as something. return false;
*/
async saveAs(
{
execOnlyIfTemp,
openAfterMove,
wipeOriginal,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return false;
}
const isTemp = await this.sketchService.isTemp(sketch);
if (!isTemp && !!execOnlyIfTemp) {
return false;
}
// If target does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
const sketchDirUri = new URI(
(await this.configService.getConfiguration()).sketchDirUri
);
const exists = await this.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = exists
? sketchDirUri.resolve(
sketchDirUri
.resolve(
`${sketch.name}_copy_${dateFormat(
new Date(),
'yyyymmddHHMMss'
)}`
)
.toString()
)
: sketchDirUri.resolve(sketch.name);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: 'Save sketch folder as...',
defaultPath,
});
if (!filePath || canceled) {
return false;
}
const destinationUri = await this.fileSystemExt.getUri(filePath);
if (!destinationUri) {
return false;
}
const workspaceUri = await this.sketchService.copy(sketch, {
destinationUri,
});
if (workspaceUri && openAfterMove) {
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
await this.fileService.delete(new URI(sketch.uri), {
recursive: true,
});
} catch {
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
}
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
}
return !!workspaceUri;
} }
const workspaceUri = await this.sketchService.copy(sketch, {
destinationUri,
});
if (workspaceUri && openAfterMove) {
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
try {
await this.fileService.delete(new URI(sketch.uri), {
recursive: true,
});
} catch {
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
}
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
}
return !!workspaceUri;
}
} }
export namespace SaveAsSketch { export namespace SaveAsSketch {
export namespace Commands { export namespace Commands {
export const SAVE_AS_SKETCH: Command = { export const SAVE_AS_SKETCH: Command = {
id: 'arduino-save-as-sketch', id: 'arduino-save-as-sketch',
}; };
} }
export interface Options { export interface Options {
readonly execOnlyIfTemp?: boolean; readonly execOnlyIfTemp?: boolean;
readonly openAfterMove?: boolean; readonly openAfterMove?: boolean;
/** /**
* Ignored if `openAfterMove` is `false`. * Ignored if `openAfterMove` is `false`.
*/ */
readonly wipeOriginal?: boolean; readonly wipeOriginal?: boolean;
} }
export namespace Options { export namespace Options {
export const DEFAULT: Options = { export const DEFAULT: Options = {
execOnlyIfTemp: false, execOnlyIfTemp: false,
openAfterMove: true, openAfterMove: true,
wipeOriginal: false, wipeOriginal: false,
}; };
} }
} }

View File

@@ -3,64 +3,64 @@ import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribu
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class SaveSketch extends SketchContribution { export class SaveSketch extends SketchContribution {
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, { registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(), execute: () => this.saveSketch(),
}); });
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, { registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => execute: () =>
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id), registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id, commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: 'Save', label: 'Save',
order: '6', order: '6',
}); });
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: SaveSketch.Commands.SAVE_SKETCH.id, command: SaveSketch.Commands.SAVE_SKETCH.id,
keybinding: 'CtrlCmd+S', keybinding: 'CtrlCmd+S',
}); });
} }
registerToolbarItems(registry: TabBarToolbarRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({ registry.registerItem({
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id, id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id, command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
tooltip: 'Save', tooltip: 'Save',
priority: 5, priority: 5,
}); });
} }
async saveSketch(): Promise<void> { async saveSketch(): Promise<void> {
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id); return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
} }
} }
export namespace SaveSketch { export namespace SaveSketch {
export namespace Commands { export namespace Commands {
export const SAVE_SKETCH: Command = { export const SAVE_SKETCH: Command = {
id: 'arduino-save-sketch', id: 'arduino-save-sketch',
}; };
export const SAVE_SKETCH__TOOLBAR: Command = { export const SAVE_SKETCH__TOOLBAR: Command = {
id: 'arduino-save-sketch--toolbar', id: 'arduino-save-sketch--toolbar',
}; };
} }
} }

View File

@@ -1,68 +1,65 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { import {
Command, Command,
MenuModelRegistry, MenuModelRegistry,
CommandRegistry, CommandRegistry,
SketchContribution, SketchContribution,
KeybindingRegistry, KeybindingRegistry,
} from './contribution'; } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { Settings as Preferences, SettingsDialog } from '../settings'; import { Settings as Preferences, SettingsDialog } from '../settings';
@injectable() @injectable()
export class Settings extends SketchContribution { export class Settings extends SketchContribution {
@inject(SettingsDialog) @inject(SettingsDialog)
protected readonly settingsDialog: SettingsDialog; protected readonly settingsDialog: SettingsDialog;
protected settingsOpened = false; protected settingsOpened = false;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Settings.Commands.OPEN, { registry.registerCommand(Settings.Commands.OPEN, {
execute: async () => { execute: async () => {
let settings: Preferences | undefined = undefined; let settings: Preferences | undefined = undefined;
try { try {
this.settingsOpened = true; this.settingsOpened = true;
settings = await this.settingsDialog.open(); settings = await this.settingsDialog.open();
} finally { } finally {
this.settingsOpened = false; this.settingsOpened = false;
} }
if (settings) { if (settings) {
await this.settingsService.update(settings); await this.settingsService.update(settings);
await this.settingsService.save(); await this.settingsService.save();
} else { } else {
await this.settingsService.reset(); await this.settingsService.reset();
} }
}, },
isEnabled: () => !this.settingsOpened, isEnabled: () => !this.settingsOpened,
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
commandId: Settings.Commands.OPEN.id, commandId: Settings.Commands.OPEN.id,
label: 'Preferences...', label: 'Preferences...',
order: '0', order: '0',
}); });
registry.registerSubmenu( registry.registerSubmenu(ArduinoMenus.FILE__ADVANCED_SUBMENU, 'Advanced');
ArduinoMenus.FILE__ADVANCED_SUBMENU, }
'Advanced'
);
}
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({ registry.registerKeybinding({
command: Settings.Commands.OPEN.id, command: Settings.Commands.OPEN.id,
keybinding: 'CtrlCmd+,', keybinding: 'CtrlCmd+,',
}); });
} }
} }
export namespace Settings { export namespace Settings {
export namespace Commands { export namespace Commands {
export const OPEN: Command = { export const OPEN: Command = {
id: 'arduino-settings-open', id: 'arduino-settings-open',
label: 'Open Preferences...', label: 'Open Preferences...',
category: 'Arduino', category: 'Arduino',
}; };
} }
} }

View File

@@ -4,18 +4,18 @@ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shel
import { WorkspaceCommands } from '@theia/workspace/lib/browser'; import { WorkspaceCommands } from '@theia/workspace/lib/browser';
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer'; import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { import {
URI, URI,
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
open, open,
} from './contribution'; } from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
@@ -24,260 +24,254 @@ import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
@injectable() @injectable()
export class SketchControl extends SketchContribution { export class SketchControl extends SketchContribution {
@inject(ApplicationShell) @inject(ApplicationShell)
protected readonly shell: ApplicationShell; protected readonly shell: ApplicationShell;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer) @inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer; protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager) @inject(EditorManager)
protected readonly editorManager: EditorManager; protected readonly editorManager: EditorManager;
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl; protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LocalCacheFsProvider) @inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider; protected readonly localCacheFsProvider: LocalCacheFsProvider;
protected readonly toDisposeBeforeCreateNewContextMenu = protected readonly toDisposeBeforeCreateNewContextMenu =
new DisposableCollection(); new DisposableCollection();
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand( registry.registerCommand(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR, SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR,
{ {
isVisible: (widget) => isVisible: (widget) =>
this.shell.getWidgets('main').indexOf(widget) !== -1, this.shell.getWidgets('main').indexOf(widget) !== -1,
execute: async () => { execute: async () => {
this.toDisposeBeforeCreateNewContextMenu.dispose(); this.toDisposeBeforeCreateNewContextMenu.dispose();
const sketch = const sketch = await this.sketchServiceClient.currentSketch();
await this.sketchServiceClient.currentSketch(); if (!sketch) {
if (!sketch) { return;
return; }
}
const target = document.getElementById( const target = document.getElementById(
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
); );
if (!(target instanceof HTMLElement)) { if (!(target instanceof HTMLElement)) {
return; return;
} }
const { parentElement } = target; const { parentElement } = target;
if (!parentElement) { if (!parentElement) {
return; return;
} }
const { mainFileUri, rootFolderFileUris } = const { mainFileUri, rootFolderFileUris } =
await this.sketchService.loadSketch(sketch.uri); await this.sketchService.loadSketch(sketch.uri);
const uris = [mainFileUri, ...rootFolderFileUris]; const uris = [mainFileUri, ...rootFolderFileUris];
const currentSketch = const currentSketch =
await this.sketchesServiceClient.currentSketch(); await this.sketchesServiceClient.currentSketch();
const parentsketchUri = this.editorManager.currentEditor const parentsketchUri = this.editorManager.currentEditor
?.getResourceUri() ?.getResourceUri()
?.toString(); ?.toString();
const parentsketch = const parentsketch = await this.sketchService.getSketchFolder(
await this.sketchService.getSketchFolder( parentsketchUri || ''
parentsketchUri || '' );
);
// if the current file is in the current opened sketch, show extra menus // if the current file is in the current opened sketch, show extra menus
if ( if (
currentSketch && currentSketch &&
parentsketch && parentsketch &&
parentsketch.uri === currentSketch.uri && parentsketch.uri === currentSketch.uri &&
(await this.allowRename(parentsketch.uri)) (await this.allowRename(parentsketch.uri))
) { ) {
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{ {
commandId: WorkspaceCommands.FILE_RENAME.id, commandId: WorkspaceCommands.FILE_RENAME.id,
label: 'Rename', label: 'Rename',
order: '1', order: '1',
} }
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuAction( this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_RENAME WorkspaceCommands.FILE_RENAME
) )
) )
); );
} else { } else {
const renamePlaceholder = new PlaceholderMenuNode( const renamePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
'Rename' 'Rename'
); );
this.menuRegistry.registerMenuNode( this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
renamePlaceholder renamePlaceholder
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuNode( this.menuRegistry.unregisterMenuNode(renamePlaceholder.id)
renamePlaceholder.id )
) );
) }
);
}
if ( if (
currentSketch && currentSketch &&
parentsketch && parentsketch &&
parentsketch.uri === currentSketch.uri && parentsketch.uri === currentSketch.uri &&
(await this.allowDelete(parentsketch.uri)) (await this.allowDelete(parentsketch.uri))
) { ) {
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{ {
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window. commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
label: 'Delete', label: 'Delete',
order: '2', order: '2',
} }
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuAction( this.menuRegistry.unregisterMenuAction(
WorkspaceCommands.FILE_DELETE WorkspaceCommands.FILE_DELETE
) )
) )
); );
} else { } else {
const deletePlaceholder = new PlaceholderMenuNode( const deletePlaceholder = new PlaceholderMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
'Delete' 'Delete'
); );
this.menuRegistry.registerMenuNode( this.menuRegistry.registerMenuNode(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
deletePlaceholder deletePlaceholder
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuNode( this.menuRegistry.unregisterMenuNode(deletePlaceholder.id)
deletePlaceholder.id )
) );
) }
);
}
for (let i = 0; i < uris.length; i++) { for (let i = 0; i < uris.length; i++) {
const uri = new URI(uris[i]); const uri = new URI(uris[i]);
// focus on the opened sketch // focus on the opened sketch
const command = { const command = {
id: `arduino-focus-file--${uri.toString()}`, id: `arduino-focus-file--${uri.toString()}`,
}; };
const handler = { const handler = {
execute: () => open(this.openerService, uri), execute: () => open(this.openerService, uri),
}; };
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
registry.registerCommand(command, handler) registry.registerCommand(command, handler)
); );
this.menuRegistry.registerMenuAction( this.menuRegistry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP, ArduinoMenus.SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP,
{ {
commandId: command.id, commandId: command.id,
label: this.labelProvider.getName(uri), label: this.labelProvider.getName(uri),
order: `${i}`, order: `${i}`,
} }
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command) this.menuRegistry.unregisterMenuAction(command)
) )
); );
} }
const options = { const options = {
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT, menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
anchor: { anchor: {
x: parentElement.getBoundingClientRect().left, x: parentElement.getBoundingClientRect().left,
y: y:
parentElement.getBoundingClientRect().top + parentElement.getBoundingClientRect().top +
parentElement.offsetHeight, parentElement.offsetHeight,
}, },
}; };
this.contextMenuRenderer.render(options); this.contextMenuRenderer.render(options);
}, },
} }
); );
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
{
commandId: WorkspaceCommands.NEW_FILE.id,
label: 'New Tab',
order: '0',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
{
commandId: CommonCommands.PREVIOUS_TAB.id,
label: 'Previous Tab',
order: '0',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
{
commandId: CommonCommands.NEXT_TAB.id,
label: 'Next Tab',
order: '0',
}
);
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: WorkspaceCommands.NEW_FILE.id,
keybinding: 'CtrlCmd+Shift+N',
});
registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
});
registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id,
keybinding: 'CtrlCmd+Alt+Right',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
});
}
protected async isCloudSketch(uri: string) {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
} }
return false;
}
registerMenus(registry: MenuModelRegistry): void { protected async allowRename(uri: string) {
registry.registerMenuAction( return !this.isCloudSketch(uri);
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, }
{
commandId: WorkspaceCommands.NEW_FILE.id,
label: 'New Tab',
order: '0',
}
);
registry.registerMenuAction( protected async allowDelete(uri: string) {
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, return !this.isCloudSketch(uri);
{ }
commandId: CommonCommands.PREVIOUS_TAB.id,
label: 'Previous Tab',
order: '0',
}
);
registry.registerMenuAction(
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
{
commandId: CommonCommands.NEXT_TAB.id,
label: 'Next Tab',
order: '0',
}
);
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: WorkspaceCommands.NEW_FILE.id,
keybinding: 'CtrlCmd+Shift+N',
});
registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
});
registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id,
keybinding: 'CtrlCmd+Alt+Right',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
});
}
protected async isCloudSketch(uri: string) {
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
if (cloudCacheLocation) {
return true;
}
return false;
}
protected async allowRename(uri: string) {
return !this.isCloudSketch(uri);
}
protected async allowDelete(uri: string) {
return !this.isCloudSketch(uri);
}
} }
export namespace SketchControl { export namespace SketchControl {
export namespace Commands { export namespace Commands {
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = { export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
id: 'arduino-open-sketch-control--toolbar', id: 'arduino-open-sketch-control--toolbar',
iconClass: 'fa fa-caret-down', iconClass: 'fa fa-caret-down',
}; };
} }
} }

View File

@@ -10,57 +10,57 @@ import { OpenSketch } from './open-sketch';
@injectable() @injectable()
export class Sketchbook extends Examples { export class Sketchbook extends Examples {
@inject(CommandRegistry) @inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
onStart(): void { onStart(): void {
this.sketchService.getSketches({}).then((container) => { this.sketchService.getSketches({}).then((container) => {
this.register(container); this.register(container);
this.mainMenuManager.update(); this.mainMenuManager.update();
}); });
this.sketchServiceClient.onSketchbookDidChange(() => { this.sketchServiceClient.onSketchbookDidChange(() => {
this.sketchService.getSketches({}).then((container) => { this.sketchService.getSketches({}).then((container) => {
this.register(container); this.register(container);
this.mainMenuManager.update(); this.mainMenuManager.update();
}); });
}); });
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu( registry.registerSubmenu(
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
'Sketchbook', 'Sketchbook',
{ order: '3' } { order: '3' }
);
}
protected register(container: SketchContainer): void {
this.toDispose.dispose();
this.registerRecursively(
[...container.children, ...container.sketches],
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
this.toDispose
);
}
protected createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.loadSketch(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
); );
} },
};
protected register(container: SketchContainer): void { }
this.toDispose.dispose();
this.registerRecursively(
[...container.children, ...container.sketches],
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
this.toDispose
);
}
protected createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.loadSketch(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
},
};
}
} }

View File

@@ -7,205 +7,200 @@ import { BoardsDataStore } from '../boards/boards-data-store';
import { MonitorConnection } from '../monitor/monitor-connection'; import { MonitorConnection } from '../monitor/monitor-connection';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class UploadSketch extends SketchContribution { export class UploadSketch extends SketchContribution {
@inject(CoreService) @inject(CoreService)
protected readonly coreService: CoreService; protected readonly coreService: CoreService;
@inject(MonitorConnection) @inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection; protected readonly monitorConnection: MonitorConnection;
@inject(BoardsDataStore) @inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider; protected readonly boardsServiceClientImpl: BoardsServiceProvider;
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>(); protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event; readonly onDidChange = this.onDidChangeEmitter.event;
protected uploadInProgress = false; protected uploadInProgress = false;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: () => this.uploadSketch(), execute: () => this.uploadSketch(),
isEnabled: () => !this.uploadInProgress, isEnabled: () => !this.uploadInProgress,
}); });
registry.registerCommand( registry.registerCommand(
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
{ {
execute: () => this.uploadSketch(true), execute: () => this.uploadSketch(true),
isEnabled: () => !this.uploadInProgress, isEnabled: () => !this.uploadInProgress,
} }
); );
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.uploadInProgress, isEnabled: () => !this.uploadInProgress,
isToggled: () => this.uploadInProgress, isToggled: () => this.uploadInProgress,
execute: () => execute: () =>
registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id), registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id),
}); });
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: 'Upload',
order: '1',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: 'Upload Using Programmer',
order: '2',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
keybinding: 'CtrlCmd+U',
});
registry.registerKeybinding({
command: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
keybinding: 'CtrlCmd+Shift+U',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
tooltip: 'Upload',
priority: 1,
onDidChange: this.onDidChange,
});
}
async uploadSketch(usingProgrammer = false): Promise<void> {
// even with buttons disabled, better to double check if an upload is already in progress
if (this.uploadInProgress) {
return;
} }
registerMenus(registry: MenuModelRegistry): void { // toggle the toolbar button and menu item state.
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { // uploadInProgress will be set to false whether the upload fails or not
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id, this.uploadInProgress = true;
label: 'Upload', this.onDidChangeEmitter.fire();
order: '1', const sketch = await this.sketchServiceClient.currentSketch();
}); if (!sketch) {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { return;
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: 'Upload Using Programmer',
order: '2',
});
} }
let shouldAutoConnect = false;
registerKeybindings(registry: KeybindingRegistry): void { const monitorConfig = this.monitorConnection.monitorConfig;
registry.registerKeybinding({ if (monitorConfig) {
command: UploadSketch.Commands.UPLOAD_SKETCH.id, await this.monitorConnection.disconnect();
keybinding: 'CtrlCmd+U', if (this.monitorConnection.autoConnect) {
}); shouldAutoConnect = true;
registry.registerKeybinding({ }
command: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id, this.monitorConnection.autoConnect = false;
keybinding: 'CtrlCmd+Shift+U',
});
} }
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
boardsConfig.selectedBoard?.fqbn
),
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
this.sourceOverride(),
]);
registerToolbarItems(registry: TabBarToolbarRegistry): void { let options: CoreService.Upload.Options | undefined = undefined;
registry.registerItem({ const sketchUri = sketch.uri;
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id, const optimizeForDebug = this.editorMode.compileForDebug;
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id, const { selectedPort } = boardsConfig;
tooltip: 'Upload', const port = selectedPort?.address;
priority: 1,
onDidChange: this.onDidChange,
});
}
async uploadSketch(usingProgrammer = false): Promise<void> { if (usingProgrammer) {
// even with buttons disabled, better to double check if an upload is already in progress const programmer = selectedProgrammer;
if (this.uploadInProgress) { options = {
return; sketchUri,
} fqbn,
optimizeForDebug,
programmer,
port,
verbose,
verify,
sourceOverride,
};
} else {
options = {
sketchUri,
fqbn,
optimizeForDebug,
port,
verbose,
verify,
sourceOverride,
};
}
this.outputChannelManager.getChannel('Arduino').clear();
if (usingProgrammer) {
await this.coreService.uploadUsingProgrammer(options);
} else {
await this.coreService.upload(options);
}
this.messageService.info('Done uploading.', { timeout: 3000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
// toggle the toolbar button and menu item state. if (monitorConfig) {
// uploadInProgress will be set to false whether the upload fails or not const { board, port } = monitorConfig;
this.uploadInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
let shouldAutoConnect = false;
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
if (this.monitorConnection.autoConnect) {
shouldAutoConnect = true;
}
this.monitorConnection.autoConnect = false;
}
try { try {
const { boardsConfig } = this.boardsServiceClientImpl; await this.boardsServiceClientImpl.waitUntilAvailable(
const [ Object.assign(board, { port }),
fqbn, 10_000
{ selectedProgrammer }, );
verify, if (shouldAutoConnect) {
verbose, // Enabling auto-connect will trigger a connect.
sourceOverride, this.monitorConnection.autoConnect = true;
] = await Promise.all([ } else {
this.boardsDataStore.appendConfigToFqbn( await this.monitorConnection.connect(monitorConfig);
boardsConfig.selectedBoard?.fqbn }
), } catch (waitError) {
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn), this.messageService.error(
this.preferences.get('arduino.upload.verify'), `Could not reconnect to serial monitor. ${waitError.toString()}`
this.preferences.get('arduino.upload.verbose'), );
this.sourceOverride(),
]);
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort?.address;
if (usingProgrammer) {
const programmer = selectedProgrammer;
options = {
sketchUri,
fqbn,
optimizeForDebug,
programmer,
port,
verbose,
verify,
sourceOverride,
};
} else {
options = {
sketchUri,
fqbn,
optimizeForDebug,
port,
verbose,
verify,
sourceOverride,
};
}
this.outputChannelManager.getChannel('Arduino').clear();
if (usingProgrammer) {
await this.coreService.uploadUsingProgrammer(options);
} else {
await this.coreService.upload(options);
}
this.messageService.info('Done uploading.', { timeout: 3000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
if (monitorConfig) {
const { board, port } = monitorConfig;
try {
await this.boardsServiceClientImpl.waitUntilAvailable(
Object.assign(board, { port }),
10_000
);
if (shouldAutoConnect) {
// Enabling auto-connect will trigger a connect.
this.monitorConnection.autoConnect = true;
} else {
await this.monitorConnection.connect(monitorConfig);
}
} catch (waitError) {
this.messageService.error(
`Could not reconnect to serial monitor. ${waitError.toString()}`
);
}
}
} }
}
} }
}
} }
export namespace UploadSketch { export namespace UploadSketch {
export namespace Commands { export namespace Commands {
export const UPLOAD_SKETCH: Command = { export const UPLOAD_SKETCH: Command = {
id: 'arduino-upload-sketch', id: 'arduino-upload-sketch',
}; };
export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = { export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = {
id: 'arduino-upload-sketch-using-programmer', id: 'arduino-upload-sketch-using-programmer',
}; };
export const UPLOAD_SKETCH_TOOLBAR: Command = { export const UPLOAD_SKETCH_TOOLBAR: Command = {
id: 'arduino-upload-sketch--toolbar', id: 'arduino-upload-sketch--toolbar',
}; };
} }
} }

View File

@@ -6,140 +6,138 @@ import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store'; import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
SketchContribution, SketchContribution,
Command, Command,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
KeybindingRegistry, KeybindingRegistry,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from './contribution'; } from './contribution';
@injectable() @injectable()
export class VerifySketch extends SketchContribution { export class VerifySketch extends SketchContribution {
@inject(CoreService) @inject(CoreService)
protected readonly coreService: CoreService; protected readonly coreService: CoreService;
@inject(BoardsDataStore) @inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore; protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider; protected readonly boardsServiceClientImpl: BoardsServiceProvider;
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>(); protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event; readonly onDidChange = this.onDidChangeEmitter.event;
protected verifyInProgress = false; protected verifyInProgress = false;
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
execute: () => this.verifySketch(), execute: () => this.verifySketch(),
isEnabled: () => !this.verifyInProgress, isEnabled: () => !this.verifyInProgress,
}); });
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, { registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
execute: () => this.verifySketch(true), execute: () => this.verifySketch(true),
isEnabled: () => !this.verifyInProgress, isEnabled: () => !this.verifyInProgress,
}); });
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, { registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
isVisible: (widget) => isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left', ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.verifyInProgress, isEnabled: () => !this.verifyInProgress,
isToggled: () => this.verifyInProgress, isToggled: () => this.verifyInProgress,
execute: () => execute: () =>
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id), registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
}); });
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
label: 'Verify/Compile',
order: '0',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: VerifySketch.Commands.EXPORT_BINARIES.id,
label: 'Export compiled Binary',
order: '3',
});
}
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: VerifySketch.Commands.VERIFY_SKETCH.id,
keybinding: 'CtrlCmd+R',
});
registry.registerKeybinding({
command: VerifySketch.Commands.EXPORT_BINARIES.id,
keybinding: 'CtrlCmd+Alt+S',
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
tooltip: 'Verify',
priority: 0,
onDidChange: this.onDidChange,
});
}
async verifySketch(exportBinaries?: boolean): Promise<void> {
// even with buttons disabled, better to double check if a verify is already in progress
if (this.verifyInProgress) {
return;
} }
registerMenus(registry: MenuModelRegistry): void { // toggle the toolbar button and menu item state.
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { // verifyInProgress will be set to false whether the compilation fails or not
commandId: VerifySketch.Commands.VERIFY_SKETCH.id, this.verifyInProgress = true;
label: 'Verify/Compile', this.onDidChangeEmitter.fire();
order: '0', const sketch = await this.sketchServiceClient.currentSketch();
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { if (!sketch) {
commandId: VerifySketch.Commands.EXPORT_BINARIES.id, return;
label: 'Export compiled Binary',
order: '3',
});
} }
try {
registerKeybindings(registry: KeybindingRegistry): void { const { boardsConfig } = this.boardsServiceClientImpl;
registry.registerKeybinding({ const [fqbn, sourceOverride] = await Promise.all([
command: VerifySketch.Commands.VERIFY_SKETCH.id, this.boardsDataStore.appendConfigToFqbn(
keybinding: 'CtrlCmd+R', boardsConfig.selectedBoard?.fqbn
}); ),
registry.registerKeybinding({ this.sourceOverride(),
command: VerifySketch.Commands.EXPORT_BINARIES.id, ]);
keybinding: 'CtrlCmd+Alt+S', const verbose = this.preferences.get('arduino.compile.verbose');
}); const compilerWarnings = this.preferences.get('arduino.compile.warnings');
} this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
registerToolbarItems(registry: TabBarToolbarRegistry): void { sketchUri: sketch.uri,
registry.registerItem({ fqbn,
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id, optimizeForDebug: this.editorMode.compileForDebug,
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id, verbose,
tooltip: 'Verify', exportBinaries,
priority: 0, sourceOverride,
onDidChange: this.onDidChange, compilerWarnings,
}); });
} this.messageService.info('Done compiling.', { timeout: 3000 });
} catch (e) {
async verifySketch(exportBinaries?: boolean): Promise<void> { this.messageService.error(e.toString());
// even with buttons disabled, better to double check if a verify is already in progress } finally {
if (this.verifyInProgress) { this.verifyInProgress = false;
return; this.onDidChangeEmitter.fire();
}
// toggle the toolbar button and menu item state.
// verifyInProgress will be set to false whether the compilation fails or not
this.verifyInProgress = true;
this.onDidChangeEmitter.fire();
const sketch = await this.sketchServiceClient.currentSketch();
if (!sketch) {
return;
}
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, sourceOverride] = await Promise.all([
this.boardsDataStore.appendConfigToFqbn(
boardsConfig.selectedBoard?.fqbn
),
this.sourceOverride(),
]);
const verbose = this.preferences.get('arduino.compile.verbose');
const compilerWarnings = this.preferences.get(
'arduino.compile.warnings'
);
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: sketch.uri,
fqbn,
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,
sourceOverride,
compilerWarnings,
});
this.messageService.info('Done compiling.', { timeout: 3000 });
} catch (e) {
this.messageService.error(e.toString());
} finally {
this.verifyInProgress = false;
this.onDidChangeEmitter.fire();
}
} }
}
} }
export namespace VerifySketch { export namespace VerifySketch {
export namespace Commands { export namespace Commands {
export const VERIFY_SKETCH: Command = { export const VERIFY_SKETCH: Command = {
id: 'arduino-verify-sketch', id: 'arduino-verify-sketch',
}; };
export const EXPORT_BINARIES: Command = { export const EXPORT_BINARIES: Command = {
id: 'arduino-export-binaries', id: 'arduino-export-binaries',
}; };
export const VERIFY_SKETCH_TOOLBAR: Command = { export const VERIFY_SKETCH_TOOLBAR: Command = {
id: 'arduino-verify-sketch--toolbar', id: 'arduino-verify-sketch--toolbar',
}; };
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,26 +2,26 @@ import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Event } from '@theia/core/lib/common/event'; import { Event } from '@theia/core/lib/common/event';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
Stat, Stat,
FileType, FileType,
FileChange, FileChange,
FileWriteOptions, FileWriteOptions,
FileDeleteOptions, FileDeleteOptions,
FileOverwriteOptions, FileOverwriteOptions,
FileSystemProvider, FileSystemProvider,
FileSystemProviderError, FileSystemProviderError,
FileSystemProviderErrorCode, FileSystemProviderErrorCode,
FileSystemProviderCapabilities, FileSystemProviderCapabilities,
WatchOptions, WatchOptions,
} from '@theia/filesystem/lib/common/files'; } from '@theia/filesystem/lib/common/files';
import { import {
FileService, FileService,
FileServiceContribution, FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service'; } from '@theia/filesystem/lib/browser/file-service';
import { AuthenticationClientService } from '../auth/authentication-client-service'; import { AuthenticationClientService } from '../auth/authentication-client-service';
import { Create, CreateApi } from './create-api'; import { Create, CreateApi } from './create-api';
@@ -33,176 +33,174 @@ export const REMOTE_ONLY_FILES = ['sketch.json'];
@injectable() @injectable()
export class CreateFsProvider export class CreateFsProvider
implements implements
FileSystemProvider, FileSystemProvider,
FrontendApplicationContribution, FrontendApplicationContribution,
FileServiceContribution FileServiceContribution
{ {
@inject(AuthenticationClientService) @inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService; protected readonly authenticationService: AuthenticationClientService;
@inject(CreateApi) @inject(CreateApi)
protected readonly createApi: CreateApi; protected readonly createApi: CreateApi;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchesService: SketchesService; protected readonly sketchesService: SketchesService;
@inject(ArduinoPreferences) @inject(ArduinoPreferences)
protected readonly arduinoPreferences: ArduinoPreferences; protected readonly arduinoPreferences: ArduinoPreferences;
protected readonly toDispose = new DisposableCollection(); protected readonly toDispose = new DisposableCollection();
readonly onFileWatchError: Event<void> = Event.None; readonly onFileWatchError: Event<void> = Event.None;
readonly onDidChangeFile: Event<readonly FileChange[]> = Event.None; readonly onDidChangeFile: Event<readonly FileChange[]> = Event.None;
readonly onDidChangeCapabilities: Event<void> = Event.None; readonly onDidChangeCapabilities: Event<void> = Event.None;
readonly capabilities: FileSystemProviderCapabilities = readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.PathCaseSensitive |
FileSystemProviderCapabilities.Access; FileSystemProviderCapabilities.Access;
onStop(): void { onStop(): void {
this.toDispose.dispose(); this.toDispose.dispose();
} }
registerFileSystemProviders(service: FileService): void { registerFileSystemProviders(service: FileService): void {
service.onWillActivateFileSystemProvider((event) => { service.onWillActivateFileSystemProvider((event) => {
if (event.scheme === CreateUri.scheme) { if (event.scheme === CreateUri.scheme) {
event.waitUntil( event.waitUntil(
(async () => { (async () => {
service.registerProvider(CreateUri.scheme, this); service.registerProvider(CreateUri.scheme, this);
})() })()
);
}
});
}
watch(uri: URI, opts: WatchOptions): Disposable {
return Disposable.NULL;
}
async stat(uri: URI): Promise<Stat> {
if (CreateUri.equals(CreateUri.root, uri)) {
this.getCreateApi; // This will throw when not logged in.
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0,
};
}
const resource = await this.getCreateApi.stat(uri.path.toString());
const mtime = Date.parse(resource.modified_at);
return {
type: this.toFileType(resource.type),
ctime: mtime,
mtime,
size: 0,
};
}
async mkdir(uri: URI): Promise<void> {
await this.getCreateApi.createDirectory(uri.path.toString());
}
async readdir(uri: URI): Promise<[string, FileType][]> {
const resources = await this.getCreateApi.readDirectory(
uri.path.toString(),
{
secrets: true,
}
); );
return resources }
.filter((res) => !REMOTE_ONLY_FILES.includes(res.name)) });
.map(({ name, type }) => [name, this.toFileType(type)]); }
watch(uri: URI, opts: WatchOptions): Disposable {
return Disposable.NULL;
}
async stat(uri: URI): Promise<Stat> {
if (CreateUri.equals(CreateUri.root, uri)) {
this.getCreateApi; // This will throw when not logged in.
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0,
};
} }
const resource = await this.getCreateApi.stat(uri.path.toString());
const mtime = Date.parse(resource.modified_at);
return {
type: this.toFileType(resource.type),
ctime: mtime,
mtime,
size: 0,
};
}
async delete(uri: URI, opts: FileDeleteOptions): Promise<void> { async mkdir(uri: URI): Promise<void> {
return; await this.getCreateApi.createDirectory(uri.path.toString());
}
if (!opts.recursive) { async readdir(uri: URI): Promise<[string, FileType][]> {
throw new Error( const resources = await this.getCreateApi.readDirectory(
'Arduino Create file-system provider does not support non-recursive deletion.' uri.path.toString(),
); {
} secrets: true,
const stat = await this.stat(uri); }
if (!stat) { );
throw new FileSystemProviderError( return resources
'File not found.', .filter((res) => !REMOTE_ONLY_FILES.includes(res.name))
FileSystemProviderErrorCode.FileNotFound .map(({ name, type }) => [name, this.toFileType(type)]);
); }
}
switch (stat.type) { async delete(uri: URI, opts: FileDeleteOptions): Promise<void> {
case FileType.Directory: { return;
await this.getCreateApi.deleteDirectory(uri.path.toString());
break; if (!opts.recursive) {
} throw new Error(
case FileType.File: { 'Arduino Create file-system provider does not support non-recursive deletion.'
await this.getCreateApi.deleteFile(uri.path.toString()); );
break;
}
default: {
throw new FileSystemProviderError(
`Unexpected file type '${
stat.type
}' for resource: ${uri.toString()}`,
FileSystemProviderErrorCode.Unknown
);
}
}
} }
const stat = await this.stat(uri);
async rename( if (!stat) {
oldUri: URI, throw new FileSystemProviderError(
newUri: URI, 'File not found.',
options: FileOverwriteOptions FileSystemProviderErrorCode.FileNotFound
): Promise<void> { );
await this.getCreateApi.rename( }
oldUri.path.toString(), switch (stat.type) {
newUri.path.toString() case FileType.Directory: {
await this.getCreateApi.deleteDirectory(uri.path.toString());
break;
}
case FileType.File: {
await this.getCreateApi.deleteFile(uri.path.toString());
break;
}
default: {
throw new FileSystemProviderError(
`Unexpected file type '${stat.type}' for resource: ${uri.toString()}`,
FileSystemProviderErrorCode.Unknown
); );
}
}
}
async rename(
oldUri: URI,
newUri: URI,
options: FileOverwriteOptions
): Promise<void> {
await this.getCreateApi.rename(
oldUri.path.toString(),
newUri.path.toString()
);
}
async readFile(uri: URI): Promise<Uint8Array> {
const content = await this.getCreateApi.readFile(uri.path.toString());
return new TextEncoder().encode(content);
}
async writeFile(
uri: URI,
content: Uint8Array,
options: FileWriteOptions
): Promise<void> {
await this.getCreateApi.writeFile(uri.path.toString(), content);
}
async access(uri: URI, mode?: number): Promise<void> {
this.getCreateApi; // Will throw if not logged in.
}
public toFileType(type: Create.ResourceType): FileType {
switch (type) {
case 'file':
return FileType.File;
case 'sketch':
case 'folder':
return FileType.Directory;
default:
return FileType.Unknown;
}
}
private get getCreateApi(): CreateApi {
const { session } = this.authenticationService;
if (!session) {
throw new FileSystemProviderError(
'Not logged in.',
FileSystemProviderErrorCode.NoPermissions
);
} }
async readFile(uri: URI): Promise<Uint8Array> { return this.createApi.init(
const content = await this.getCreateApi.readFile(uri.path.toString()); this.authenticationService,
return new TextEncoder().encode(content); this.arduinoPreferences
} );
}
async writeFile(
uri: URI,
content: Uint8Array,
options: FileWriteOptions
): Promise<void> {
await this.getCreateApi.writeFile(uri.path.toString(), content);
}
async access(uri: URI, mode?: number): Promise<void> {
this.getCreateApi; // Will throw if not logged in.
}
public toFileType(type: Create.ResourceType): FileType {
switch (type) {
case 'file':
return FileType.File;
case 'sketch':
case 'folder':
return FileType.Directory;
default:
return FileType.Unknown;
}
}
private get getCreateApi(): CreateApi {
const { session } = this.authenticationService;
if (!session) {
throw new FileSystemProviderError(
'Not logged in.',
FileSystemProviderErrorCode.NoPermissions
);
}
return this.createApi.init(
this.authenticationService,
this.arduinoPreferences
);
}
} }

View File

@@ -2,22 +2,22 @@ export const posix = { sep: '/' };
// TODO: poor man's `path.join(path, '..')` in the browser. // TODO: poor man's `path.join(path, '..')` in the browser.
export function parentPosix(path: string): string { export function parentPosix(path: string): string {
const segments = path.split(posix.sep) || []; const segments = path.split(posix.sep) || [];
segments.pop(); segments.pop();
let modified = segments.join(posix.sep); let modified = segments.join(posix.sep);
if (path.charAt(path.length - 1) === posix.sep) { if (path.charAt(path.length - 1) === posix.sep) {
modified += posix.sep; modified += posix.sep;
} }
return modified; return modified;
} }
export function basename(path: string): string { export function basename(path: string): string {
const segments = path.split(posix.sep) || []; const segments = path.split(posix.sep) || [];
return segments.pop()!; return segments.pop()!;
} }
export function posixSegments(posixPath: string): string[] { export function posixSegments(posixPath: string): string[] {
return posixPath.split(posix.sep).filter((segment) => !!segment); return posixPath.split(posix.sep).filter((segment) => !!segment);
} }
/** /**
@@ -32,28 +32,28 @@ export function posixSegments(posixPath: string): string[] {
* ``` * ```
*/ */
export function splitSketchPath( export function splitSketchPath(
raw: string, raw: string,
sep = '/sketches_v2/' sep = '/sketches_v2/'
): [string, string] { ): [string, string] {
if (!sep) { if (!sep) {
throw new Error('Invalid separator. Cannot be zero length.'); throw new Error('Invalid separator. Cannot be zero length.');
} }
const index = raw.indexOf(sep); const index = raw.indexOf(sep);
if (index === -1) { if (index === -1) {
throw new Error(`Invalid path pattern. Raw path was '${raw}'.`); throw new Error(`Invalid path pattern. Raw path was '${raw}'.`);
} }
const createRoot = raw.substring(0, index + sep.length - 1); // TODO: validate the `createRoot` format. const createRoot = raw.substring(0, index + sep.length - 1); // TODO: validate the `createRoot` format.
const posixPath = raw.substr(index + sep.length - 1); const posixPath = raw.substr(index + sep.length - 1);
if (!posixPath) { if (!posixPath) {
throw new Error(`Could not extract POSIX path from '${raw}'.`); throw new Error(`Could not extract POSIX path from '${raw}'.`);
} }
return [createRoot, posixPath]; return [createRoot, posixPath];
} }
export function toPosixPath(raw: string): string { export function toPosixPath(raw: string): string {
if (raw === posix.sep) { if (raw === posix.sep) {
return posix.sep; // Handles the root resource case. return posix.sep; // Handles the root resource case.
} }
const [, posixPath] = splitSketchPath(raw); const [, posixPath] = splitSketchPath(raw);
return posixPath; return posixPath;
} }

View File

@@ -4,36 +4,34 @@ import { Create } from './create-api';
import { toPosixPath, parentPosix, posix } from './create-paths'; import { toPosixPath, parentPosix, posix } from './create-paths';
export namespace CreateUri { export namespace CreateUri {
export const scheme = 'arduino-create'; export const scheme = 'arduino-create';
export const root = toUri(posix.sep); export const root = toUri(posix.sep);
export function toUri(posixPathOrResource: string | Create.Resource): URI { export function toUri(posixPathOrResource: string | Create.Resource): URI {
const posixPath = const posixPath =
typeof posixPathOrResource === 'string' typeof posixPathOrResource === 'string'
? posixPathOrResource ? posixPathOrResource
: toPosixPath(posixPathOrResource.path); : toPosixPath(posixPathOrResource.path);
return new URI( return new URI(Uri.parse(posixPath).with({ scheme, authority: 'create' }));
Uri.parse(posixPath).with({ scheme, authority: 'create' }) }
);
}
export function is(uri: URI): boolean { export function is(uri: URI): boolean {
return uri.scheme === scheme; return uri.scheme === scheme;
} }
export function equals(left: URI, right: URI): boolean { export function equals(left: URI, right: URI): boolean {
return is(left) && is(right) && left.toString() === right.toString(); return is(left) && is(right) && left.toString() === right.toString();
} }
export function parent(uri: URI): URI { export function parent(uri: URI): URI {
if (!is(uri)) { if (!is(uri)) {
throw new Error( throw new Error(
`Invalid URI scheme. Expected '${scheme}' got '${uri.scheme}' instead.` `Invalid URI scheme. Expected '${scheme}' got '${uri.scheme}' instead.`
); );
}
if (equals(uri, root)) {
return uri;
}
return toUri(parentPosix(uri.path.toString()));
} }
if (equals(uri, root)) {
return uri;
}
return toUri(parentPosix(uri.path.toString()));
}
} }

View File

@@ -4,172 +4,172 @@ import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging'; import { Message } from '@phosphor/messaging';
import { clipboard } from 'electron'; import { clipboard } from 'electron';
import { import {
AbstractDialog, AbstractDialog,
ReactWidget, ReactWidget,
DialogProps, DialogProps,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { CreateApi } from '../create/create-api'; import { CreateApi } from '../create/create-api';
const RadioButton = (props: { const RadioButton = (props: {
id: string; id: string;
changed: (evt: React.BaseSyntheticEvent) => void; changed: (evt: React.BaseSyntheticEvent) => void;
value: string; value: string;
isSelected: boolean; isSelected: boolean;
isDisabled: boolean; isDisabled: boolean;
label: string; label: string;
}) => { }) => {
return ( return (
<p className="RadioButton"> <p className="RadioButton">
<input <input
id={props.id} id={props.id}
onChange={props.changed} onChange={props.changed}
value={props.value} value={props.value}
type="radio" type="radio"
checked={props.isSelected} checked={props.isSelected}
disabled={props.isDisabled} disabled={props.isDisabled}
/> />
<label htmlFor={props.id}>{props.label}</label> <label htmlFor={props.id}>{props.label}</label>
</p> </p>
); );
}; };
export const ShareSketchComponent = ({ export const ShareSketchComponent = ({
treeNode, treeNode,
createApi, createApi,
domain = 'https://create.arduino.cc', domain = 'https://create.arduino.cc',
}: { }: {
treeNode: any; treeNode: any;
createApi: CreateApi; createApi: CreateApi;
domain?: string; domain?: string;
}): React.ReactElement => { }): React.ReactElement => {
// const [publicVisibility, setPublicVisibility] = React.useState<boolean>( // const [publicVisibility, setPublicVisibility] = React.useState<boolean>(
// treeNode.isPublic // treeNode.isPublic
// ); // );
const [loading, setloading] = React.useState<boolean>(false); const [loading, setloading] = React.useState<boolean>(false);
const radioChangeHandler = async (event: React.BaseSyntheticEvent) => { const radioChangeHandler = async (event: React.BaseSyntheticEvent) => {
setloading(true); setloading(true);
const sketch = await createApi.editSketch({ const sketch = await createApi.editSketch({
id: treeNode.sketchId, id: treeNode.sketchId,
params: { params: {
is_public: event.target.value === 'private' ? false : true, is_public: event.target.value === 'private' ? false : true,
}, },
}); });
// setPublicVisibility(sketch.is_public); // setPublicVisibility(sketch.is_public);
treeNode.isPublic = sketch.is_public; treeNode.isPublic = sketch.is_public;
setloading(false); setloading(false);
}; };
const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`; const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`;
const embedLink = `<iframe src="${sketchLink}?embed" style="height:510px;width:100%;margin:10px 0" frameborder=0></iframe>`; const embedLink = `<iframe src="${sketchLink}?embed" style="height:510px;width:100%;margin:10px 0" frameborder=0></iframe>`;
return ( return (
<div id="widget-container arduino-sharesketch-dialog"> <div id="widget-container arduino-sharesketch-dialog">
<p>Choose visibility of your Sketch:</p> <p>Choose visibility of your Sketch:</p>
<RadioButton <RadioButton
changed={radioChangeHandler} changed={radioChangeHandler}
id="1" id="1"
isSelected={treeNode.isPublic === false} isSelected={treeNode.isPublic === false}
label="Private. Only you can view the Sketch." label="Private. Only you can view the Sketch."
value="private" value="private"
isDisabled={loading} isDisabled={loading}
/>
<RadioButton
changed={radioChangeHandler}
id="2"
isSelected={treeNode.isPublic === true}
label="Public. Anyone with the link can view the Sketch."
value="public"
isDisabled={loading}
/>
{treeNode.isPublic && (
<div>
<p>Link:</p>
<div className="sketch-link">
<input
type="text"
readOnly
value={sketchLink}
className="theia-input"
/> />
<RadioButton <button
changed={radioChangeHandler} onClick={() => clipboard.writeText(sketchLink)}
id="2" value="copy"
isSelected={treeNode.isPublic === true} className="theia-button secondary"
label="Public. Anyone with the link can view the Sketch." >
value="public" Copy
isDisabled={loading} </button>
</div>
<p>Embed:</p>
<div className="sketch-link-embed">
<textarea
readOnly
value={embedLink}
className="theia-input stretch"
/> />
</div>
{treeNode.isPublic && (
<div>
<p>Link:</p>
<div className="sketch-link">
<input
type="text"
readOnly
value={sketchLink}
className="theia-input"
/>
<button
onClick={() => clipboard.writeText(sketchLink)}
value="copy"
className="theia-button secondary"
>
Copy
</button>
</div>
<p>Embed:</p>
<div className="sketch-link-embed">
<textarea
readOnly
value={embedLink}
className="theia-input stretch"
/>
</div>
</div>
)}
</div> </div>
); )}
</div>
);
}; };
@injectable() @injectable()
export class ShareSketchWidget extends ReactWidget { export class ShareSketchWidget extends ReactWidget {
constructor(private treeNode: any, private createApi: CreateApi) { constructor(private treeNode: any, private createApi: CreateApi) {
super(); super();
} }
protected render(): React.ReactNode { protected render(): React.ReactNode {
return ( return (
<ShareSketchComponent <ShareSketchComponent
treeNode={this.treeNode} treeNode={this.treeNode}
createApi={this.createApi} createApi={this.createApi}
/> />
); );
} }
} }
@injectable() @injectable()
export class ShareSketchDialogProps extends DialogProps { export class ShareSketchDialogProps extends DialogProps {
readonly node: any; readonly node: any;
readonly createApi: CreateApi; readonly createApi: CreateApi;
} }
@injectable() @injectable()
export class ShareSketchDialog extends AbstractDialog<void> { export class ShareSketchDialog extends AbstractDialog<void> {
protected widget: ShareSketchWidget; protected widget: ShareSketchWidget;
constructor( constructor(
@inject(ShareSketchDialogProps) @inject(ShareSketchDialogProps)
protected readonly props: ShareSketchDialogProps protected readonly props: ShareSketchDialogProps
) { ) {
super({ title: props.title }); super({ title: props.title });
this.contentNode.classList.add('arduino-share-sketch-dialog'); this.contentNode.classList.add('arduino-share-sketch-dialog');
this.widget = new ShareSketchWidget(props.node, props.createApi); this.widget = new ShareSketchWidget(props.node, props.createApi);
} }
get value(): void { get value(): void {
return; return;
} }
protected onAfterAttach(msg: Message): void { protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) { if (this.widget.isAttached) {
Widget.detach(this.widget); Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
} }
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void { protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg); super.onUpdateRequest(msg);
this.widget.update(); this.widget.update();
} }
protected onActivateRequest(msg: Message): void { protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg); super.onActivateRequest(msg);
this.widget.activate(); this.widget.activate();
} }
} }

View File

@@ -2,68 +2,68 @@ import { inject, injectable } from 'inversify';
import { Widget } from '@phosphor/widgets'; import { Widget } from '@phosphor/widgets';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { import {
ConfirmDialog, ConfirmDialog,
ConfirmDialogProps, ConfirmDialogProps,
DialogError, DialogError,
} from '@theia/core/lib/browser/dialogs'; } from '@theia/core/lib/browser/dialogs';
@injectable() @injectable()
export class DoNotAskAgainConfirmDialogProps extends ConfirmDialogProps { export class DoNotAskAgainConfirmDialogProps extends ConfirmDialogProps {
readonly onAccept: () => Promise<void>; readonly onAccept: () => Promise<void>;
} }
@injectable() @injectable()
export class DoNotAskAgainConfirmDialog extends ConfirmDialog { export class DoNotAskAgainConfirmDialog extends ConfirmDialog {
protected readonly doNotAskAgainCheckbox: HTMLInputElement; protected readonly doNotAskAgainCheckbox: HTMLInputElement;
constructor( constructor(
@inject(DoNotAskAgainConfirmDialogProps) @inject(DoNotAskAgainConfirmDialogProps)
protected readonly props: DoNotAskAgainConfirmDialogProps protected readonly props: DoNotAskAgainConfirmDialogProps
) { ) {
super(props); super(props);
this.controlPanel.removeChild(this.errorMessageNode); this.controlPanel.removeChild(this.errorMessageNode);
const doNotAskAgainNode = document.createElement('div'); const doNotAskAgainNode = document.createElement('div');
doNotAskAgainNode.setAttribute('style', 'flex: 2'); doNotAskAgainNode.setAttribute('style', 'flex: 2');
this.controlPanel.insertBefore( this.controlPanel.insertBefore(
doNotAskAgainNode, doNotAskAgainNode,
this.controlPanel.firstChild this.controlPanel.firstChild
); );
const doNotAskAgainLabel = document.createElement('label'); const doNotAskAgainLabel = document.createElement('label');
doNotAskAgainLabel.classList.add('flex-line'); doNotAskAgainLabel.classList.add('flex-line');
doNotAskAgainNode.appendChild(doNotAskAgainLabel); doNotAskAgainNode.appendChild(doNotAskAgainLabel);
doNotAskAgainLabel.textContent = "Don't ask again"; doNotAskAgainLabel.textContent = "Don't ask again";
this.doNotAskAgainCheckbox = document.createElement('input'); this.doNotAskAgainCheckbox = document.createElement('input');
this.doNotAskAgainCheckbox.setAttribute('align-self', 'center'); this.doNotAskAgainCheckbox.setAttribute('align-self', 'center');
doNotAskAgainLabel.appendChild(this.doNotAskAgainCheckbox); doNotAskAgainLabel.appendChild(this.doNotAskAgainCheckbox);
this.doNotAskAgainCheckbox.type = 'checkbox'; this.doNotAskAgainCheckbox.type = 'checkbox';
} }
protected async accept(): Promise<void> { protected async accept(): Promise<void> {
if (!this.resolve) { if (!this.resolve) {
return; return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
if (this.doNotAskAgainCheckbox.checked) {
await this.props.onAccept();
}
this.resolve(value);
Widget.detach(this);
}
} }
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
if (this.doNotAskAgainCheckbox.checked) {
await this.props.onAccept();
}
this.resolve(value);
Widget.detach(this);
}
}
protected setErrorMessage(error: DialogError): void { protected setErrorMessage(error: DialogError): void {
if (this.acceptButton) { if (this.acceptButton) {
this.acceptButton.disabled = !DialogError.getResult(error); this.acceptButton.disabled = !DialogError.getResult(error);
}
} }
}
} }

View File

@@ -1,39 +1,37 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
FrontendApplication, FrontendApplication,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { MainMenuManager } from '../common/main-menu-manager'; import { MainMenuManager } from '../common/main-menu-manager';
@injectable() @injectable()
export class EditorMode implements FrontendApplicationContribution { export class EditorMode implements FrontendApplicationContribution {
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager; protected readonly mainMenuManager: MainMenuManager;
protected app: FrontendApplication; protected app: FrontendApplication;
onStart(app: FrontendApplication): void { onStart(app: FrontendApplication): void {
this.app = app; this.app = app;
} }
get compileForDebug(): boolean { get compileForDebug(): boolean {
const value = window.localStorage.getItem( const value = window.localStorage.getItem(EditorMode.COMPILE_FOR_DEBUG_KEY);
EditorMode.COMPILE_FOR_DEBUG_KEY return value === 'true';
); }
return value === 'true';
}
async toggleCompileForDebug(): Promise<void> { async toggleCompileForDebug(): Promise<void> {
const oldState = this.compileForDebug; const oldState = this.compileForDebug;
const newState = !oldState; const newState = !oldState;
window.localStorage.setItem( window.localStorage.setItem(
EditorMode.COMPILE_FOR_DEBUG_KEY, EditorMode.COMPILE_FOR_DEBUG_KEY,
String(newState) String(newState)
); );
this.mainMenuManager.update(); this.mainMenuManager.update();
} }
} }
export namespace EditorMode { export namespace EditorMode {
export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug'; export const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
} }

View File

@@ -3,8 +3,8 @@ import { Message } from '@phosphor/messaging';
import { addEventListener } from '@theia/core/lib/browser/widgets/widget'; import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs'; import { AbstractDialog, DialogProps } from '@theia/core/lib/browser/dialogs';
import { import {
LibraryPackage, LibraryPackage,
LibraryService, LibraryService,
} from '../../common/protocol/library-service'; } from '../../common/protocol/library-service';
import { ListWidget } from '../widgets/component-list/list-widget'; import { ListWidget } from '../widgets/component-list/list-widget';
import { Installable } from '../../common/protocol'; import { Installable } from '../../common/protocol';
@@ -12,190 +12,184 @@ import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
@injectable() @injectable()
export class LibraryListWidget extends ListWidget<LibraryPackage> { export class LibraryListWidget extends ListWidget<LibraryPackage> {
static WIDGET_ID = 'library-list-widget'; static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager'; static WIDGET_LABEL = 'Library Manager';
constructor( constructor(
@inject(LibraryService) protected service: LibraryService, @inject(LibraryService) protected service: LibraryService,
@inject(ListItemRenderer) @inject(ListItemRenderer)
protected itemRenderer: ListItemRenderer<LibraryPackage> protected itemRenderer: ListItemRenderer<LibraryPackage>
) { ) {
super({ super({
id: LibraryListWidget.WIDGET_ID, id: LibraryListWidget.WIDGET_ID,
label: LibraryListWidget.WIDGET_LABEL, label: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon', iconClass: 'library-tab-icon',
searchable: service, searchable: service,
installable: service, installable: service,
itemLabel: (item: LibraryPackage) => item.name, itemLabel: (item: LibraryPackage) => item.name,
itemDeprecated: (item: LibraryPackage) => item.deprecated, itemDeprecated: (item: LibraryPackage) => item.deprecated,
itemRenderer, itemRenderer,
}); });
}
@postConstruct()
protected init(): void {
super.init();
this.toDispose.pushAll([
this.notificationCenter.onLibraryInstalled(() => this.refresh(undefined)),
this.notificationCenter.onLibraryUninstalled(() =>
this.refresh(undefined)
),
]);
}
protected async install({
item,
progressId,
version,
}: {
item: LibraryPackage;
progressId: string;
version: Installable.Version;
}): Promise<void> {
const dependencies = await this.service.listDependencies({
item,
version,
filterSelf: true,
});
let installDependencies: boolean | undefined = undefined;
if (dependencies.length) {
const message = document.createElement('div');
message.innerHTML = `The library <b>${item.name}:${version}</b> needs ${
dependencies.length === 1
? 'another dependency'
: 'some other dependencies'
} currently not installed:`;
const listContainer = document.createElement('div');
listContainer.style.maxHeight = '300px';
listContainer.style.overflowY = 'auto';
const list = document.createElement('ul');
list.style.listStyleType = 'none';
for (const { name } of dependencies) {
const listItem = document.createElement('li');
listItem.textContent = ` - ${name}`;
listItem.style.fontWeight = 'bold';
list.appendChild(listItem);
}
listContainer.appendChild(list);
message.appendChild(listContainer);
const question = document.createElement('div');
question.textContent = `Would you like to install ${
dependencies.length === 1
? 'the missing dependency'
: 'all the missing dependencies'
}?`;
message.appendChild(question);
const result = await new MessageBoxDialog({
title: `Dependencies for library ${item.name}:${version}`,
message,
buttons: ['Install all', `Install ${item.name} only`, 'Cancel'],
maxWidth: 740, // Aligned with `settings-dialog.css`.
}).open();
if (result) {
const { response } = result;
if (response === 0) {
// All
installDependencies = true;
} else if (response === 1) {
// Current only
installDependencies = false;
}
}
} else {
// The lib does not have any dependencies.
installDependencies = false;
} }
@postConstruct() if (typeof installDependencies === 'boolean') {
protected init(): void { await this.service.install({
super.init();
this.toDispose.pushAll([
this.notificationCenter.onLibraryInstalled(() =>
this.refresh(undefined)
),
this.notificationCenter.onLibraryUninstalled(() =>
this.refresh(undefined)
),
]);
}
protected async install({
item, item,
progressId,
version, version,
}: {
item: LibraryPackage;
progressId: string;
version: Installable.Version;
}): Promise<void> {
const dependencies = await this.service.listDependencies({
item,
version,
filterSelf: true,
});
let installDependencies: boolean | undefined = undefined;
if (dependencies.length) {
const message = document.createElement('div');
message.innerHTML = `The library <b>${
item.name
}:${version}</b> needs ${
dependencies.length === 1
? 'another dependency'
: 'some other dependencies'
} currently not installed:`;
const listContainer = document.createElement('div');
listContainer.style.maxHeight = '300px';
listContainer.style.overflowY = 'auto';
const list = document.createElement('ul');
list.style.listStyleType = 'none';
for (const { name } of dependencies) {
const listItem = document.createElement('li');
listItem.textContent = ` - ${name}`;
listItem.style.fontWeight = 'bold';
list.appendChild(listItem);
}
listContainer.appendChild(list);
message.appendChild(listContainer);
const question = document.createElement('div');
question.textContent = `Would you like to install ${
dependencies.length === 1
? 'the missing dependency'
: 'all the missing dependencies'
}?`;
message.appendChild(question);
const result = await new MessageBoxDialog({
title: `Dependencies for library ${item.name}:${version}`,
message,
buttons: ['Install all', `Install ${item.name} only`, 'Cancel'],
maxWidth: 740, // Aligned with `settings-dialog.css`.
}).open();
if (result) {
const { response } = result;
if (response === 0) {
// All
installDependencies = true;
} else if (response === 1) {
// Current only
installDependencies = false;
}
}
} else {
// The lib does not have any dependencies.
installDependencies = false;
}
if (typeof installDependencies === 'boolean') {
await this.service.install({
item,
version,
progressId,
installDependencies,
});
this.messageService.info(
`Successfully installed library ${item.name}:${version}`,
{ timeout: 3000 }
);
}
}
protected async uninstall({
item,
progressId, progressId,
}: { installDependencies,
item: LibraryPackage; });
progressId: string; this.messageService.info(
}): Promise<void> { `Successfully installed library ${item.name}:${version}`,
await super.uninstall({ item, progressId }); { timeout: 3000 }
this.messageService.info( );
`Successfully uninstalled library ${item.name}:${item.installedVersion}`,
{ timeout: 3000 }
);
} }
}
protected async uninstall({
item,
progressId,
}: {
item: LibraryPackage;
progressId: string;
}): Promise<void> {
await super.uninstall({ item, progressId });
this.messageService.info(
`Successfully uninstalled library ${item.name}:${item.installedVersion}`,
{ timeout: 3000 }
);
}
} }
class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> { class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
protected response: number; protected response: number;
constructor(protected readonly options: MessageBoxDialog.Options) { constructor(protected readonly options: MessageBoxDialog.Options) {
super(options); super(options);
this.contentNode.appendChild( this.contentNode.appendChild(this.createMessageNode(this.options.message));
this.createMessageNode(this.options.message) (options.buttons || ['OK']).forEach((text, index) => {
); const button = this.createButton(text);
(options.buttons || ['OK']).forEach((text, index) => { button.classList.add(index === 0 ? 'main' : 'secondary');
const button = this.createButton(text); this.controlPanel.appendChild(button);
button.classList.add(index === 0 ? 'main' : 'secondary'); this.toDisposeOnDetach.push(
this.controlPanel.appendChild(button); addEventListener(button, 'click', () => {
this.toDisposeOnDetach.push( this.response = index;
addEventListener(button, 'click', () => { this.accept();
this.response = index; })
this.accept(); );
}) });
); }
});
}
protected onCloseRequest(message: Message): void { protected onCloseRequest(message: Message): void {
super.onCloseRequest(message); super.onCloseRequest(message);
this.accept(); this.accept();
} }
get value(): MessageBoxDialog.Result { get value(): MessageBoxDialog.Result {
return { response: this.response }; return { response: this.response };
} }
protected createMessageNode(message: string | HTMLElement): HTMLElement { protected createMessageNode(message: string | HTMLElement): HTMLElement {
if (typeof message === 'string') { if (typeof message === 'string') {
const messageNode = document.createElement('div'); const messageNode = document.createElement('div');
messageNode.textContent = message; messageNode.textContent = message;
return messageNode; return messageNode;
}
return message;
} }
return message;
}
protected handleEnter(event: KeyboardEvent): boolean | void { protected handleEnter(event: KeyboardEvent): boolean | void {
this.response = 0; this.response = 0;
super.handleEnter(event); super.handleEnter(event);
} }
} }
export namespace MessageBoxDialog { export namespace MessageBoxDialog {
export interface Options extends DialogProps { export interface Options extends DialogProps {
/** /**
* When empty, `['OK']` will be inferred. * When empty, `['OK']` will be inferred.
*/ */
buttons?: string[]; buttons?: string[];
message: string | HTMLElement; message: string | HTMLElement;
} }
export interface Result { export interface Result {
/** /**
* The index of `buttons` that was clicked. * The index of `buttons` that was clicked.
*/ */
readonly response: number; readonly response: number;
} }
} }

View File

@@ -7,33 +7,33 @@ import { ArduinoMenus } from '../menu/arduino-menus';
@injectable() @injectable()
export class LibraryListWidgetFrontendContribution export class LibraryListWidgetFrontendContribution
extends AbstractViewContribution<LibraryListWidget> extends AbstractViewContribution<LibraryListWidget>
implements FrontendApplicationContribution implements FrontendApplicationContribution
{ {
constructor() { constructor() {
super({ super({
widgetId: LibraryListWidget.WIDGET_ID, widgetId: LibraryListWidget.WIDGET_ID,
widgetName: LibraryListWidget.WIDGET_LABEL, widgetName: LibraryListWidget.WIDGET_LABEL,
defaultWidgetOptions: { defaultWidgetOptions: {
area: 'left', area: 'left',
rank: 3, rank: 3,
}, },
toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`, toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
toggleKeybinding: 'CtrlCmd+Shift+I', toggleKeybinding: 'CtrlCmd+Shift+I',
}); });
} }
async initializeLayout(): Promise<void> { async initializeLayout(): Promise<void> {
this.openView(); this.openView();
} }
registerMenus(menus: MenuModelRegistry): void { registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) { if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id, commandId: this.toggleCommand.id,
label: 'Manage Libraries...', label: 'Manage Libraries...',
order: '3', order: '3',
}); });
}
} }
}
} }

View File

@@ -3,165 +3,158 @@ import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util'; import { Deferred } from '@theia/core/lib/common/promise-util';
import { import {
FileSystemProvider, FileSystemProvider,
FileSystemProviderError, FileSystemProviderError,
FileSystemProviderErrorCode, FileSystemProviderErrorCode,
} from '@theia/filesystem/lib/common/files'; } from '@theia/filesystem/lib/common/files';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider'; import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider';
import { import {
FileService, FileService,
FileServiceContribution, FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service'; } from '@theia/filesystem/lib/browser/file-service';
import { AuthenticationClientService } from '../auth/authentication-client-service'; import { AuthenticationClientService } from '../auth/authentication-client-service';
import { AuthenticationSession } from '../../common/protocol/authentication-service'; import { AuthenticationSession } from '../../common/protocol/authentication-service';
import { ConfigService } from '../../common/protocol'; import { ConfigService } from '../../common/protocol';
export namespace LocalCacheUri { export namespace LocalCacheUri {
export const scheme = 'arduino-local-cache'; export const scheme = 'arduino-local-cache';
export const root = new URI( export const root = new URI(
Uri.parse('/').with({ scheme, authority: 'create' }) Uri.parse('/').with({ scheme, authority: 'create' })
); );
} }
@injectable() @injectable()
export class LocalCacheFsProvider export class LocalCacheFsProvider
implements implements FileServiceContribution, DelegatingFileSystemProvider.URIConverter
FileServiceContribution,
DelegatingFileSystemProvider.URIConverter
{ {
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(AuthenticationClientService) @inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService; protected readonly authenticationService: AuthenticationClientService;
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`? // TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
readonly ready = new Deferred<void>(); readonly ready = new Deferred<void>();
private _localCacheRoot: URI; private _localCacheRoot: URI;
registerFileSystemProviders(fileService: FileService): void { registerFileSystemProviders(fileService: FileService): void {
fileService.onWillActivateFileSystemProvider(async (event) => { fileService.onWillActivateFileSystemProvider(async (event) => {
if (event.scheme === LocalCacheUri.scheme) { if (event.scheme === LocalCacheUri.scheme) {
event.waitUntil( event.waitUntil(
(async () => { (async () => {
this.init(fileService); this.init(fileService);
const provider = await this.createProvider(fileService); const provider = await this.createProvider(fileService);
fileService.registerProvider( fileService.registerProvider(LocalCacheUri.scheme, provider);
LocalCacheUri.scheme, })()
provider
);
})()
);
}
});
}
to(resource: URI): URI | undefined {
const relativePath = LocalCacheUri.root.relative(resource);
if (relativePath) {
return this.currentUserUri.resolve(relativePath).normalizePath();
}
return undefined;
}
from(resource: URI): URI | undefined {
const relativePath = this.currentUserUri.relative(resource);
if (relativePath) {
return LocalCacheUri.root.resolve(relativePath);
}
return undefined;
}
protected async createProvider(
fileService: FileService
): Promise<FileSystemProvider> {
const delegate = await fileService.activateProvider('file');
await this.ready.promise;
return new DelegatingFileSystemProvider(
delegate,
{
uriConverter: this,
},
new DisposableCollection(
delegate.watch(this.localCacheRoot, {
excludes: [],
recursive: true,
})
)
); );
} }
});
}
protected async init(fileService: FileService): Promise<void> { to(resource: URI): URI | undefined {
const config = await this.configService.getConfiguration(); const relativePath = LocalCacheUri.root.relative(resource);
this._localCacheRoot = new URI(config.dataDirUri); if (relativePath) {
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) { return this.currentUserUri.resolve(relativePath).normalizePath();
this._localCacheRoot = this._localCacheRoot.resolve(segment); }
await fileService.createFolder(this._localCacheRoot); return undefined;
} }
this.session(fileService).then(() => this.ready.resolve());
from(resource: URI): URI | undefined {
const relativePath = this.currentUserUri.relative(resource);
if (relativePath) {
return LocalCacheUri.root.resolve(relativePath);
}
return undefined;
}
protected async createProvider(
fileService: FileService
): Promise<FileSystemProvider> {
const delegate = await fileService.activateProvider('file');
await this.ready.promise;
return new DelegatingFileSystemProvider(
delegate,
{
uriConverter: this,
},
new DisposableCollection(
delegate.watch(this.localCacheRoot, {
excludes: [],
recursive: true,
})
)
);
}
protected async init(fileService: FileService): Promise<void> {
const config = await this.configService.getConfiguration();
this._localCacheRoot = new URI(config.dataDirUri);
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
this._localCacheRoot = this._localCacheRoot.resolve(segment);
await fileService.createFolder(this._localCacheRoot);
}
this.session(fileService).then(() => this.ready.resolve());
this.authenticationService.onSessionDidChange(async (session) => {
if (session) {
await this.ensureExists(session, fileService);
}
});
}
private get currentUserUri(): URI {
const { session } = this.authenticationService;
if (!session) {
throw new FileSystemProviderError(
'Not logged in.',
FileSystemProviderErrorCode.NoPermissions
);
}
return this.toUri(session);
}
private get localCacheRoot(): URI {
return this._localCacheRoot;
}
private async session(
fileService: FileService
): Promise<AuthenticationSession> {
return new Promise<AuthenticationSession>(async (resolve) => {
const { session } = this.authenticationService;
if (session) {
await this.ensureExists(session, fileService);
resolve(session);
return;
}
const toDispose = new DisposableCollection();
toDispose.push(
this.authenticationService.onSessionDidChange(async (session) => { this.authenticationService.onSessionDidChange(async (session) => {
if (session) { if (session) {
await this.ensureExists(session, fileService); await this.ensureExists(session, fileService);
} toDispose.dispose();
}); resolve(session);
} }
})
);
});
}
private get currentUserUri(): URI { private async ensureExists(
const { session } = this.authenticationService; session: AuthenticationSession,
if (!session) { fileService: FileService
throw new FileSystemProviderError( ): Promise<URI> {
'Not logged in.', const uri = this.toUri(session);
FileSystemProviderErrorCode.NoPermissions const exists = await fileService.exists(uri);
); if (!exists) {
} await fileService.createFolder(uri);
return this.toUri(session);
} }
return uri;
}
private get localCacheRoot(): URI { private toUri(session: AuthenticationSession): URI {
return this._localCacheRoot; // Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
} return this._localCacheRoot.resolve(session.id.split('|')[1]);
}
private async session(
fileService: FileService
): Promise<AuthenticationSession> {
return new Promise<AuthenticationSession>(async (resolve) => {
const { session } = this.authenticationService;
if (session) {
await this.ensureExists(session, fileService);
resolve(session);
return;
}
const toDispose = new DisposableCollection();
toDispose.push(
this.authenticationService.onSessionDidChange(
async (session) => {
if (session) {
await this.ensureExists(session, fileService);
toDispose.dispose();
resolve(session);
}
}
)
);
});
}
private async ensureExists(
session: AuthenticationSession,
fileService: FileService
): Promise<URI> {
const uri = this.toUri(session);
const exists = await fileService.exists(uri);
if (!exists) {
await fileService.createFolder(uri);
}
return uri;
}
private toUri(session: AuthenticationSession): URI {
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
return this._localCacheRoot.resolve(session.id.split('|')[1]);
}
} }

View File

@@ -1,148 +1,148 @@
import { isOSX } from '@theia/core/lib/common/os'; import { isOSX } from '@theia/core/lib/common/os';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { import {
MAIN_MENU_BAR, MAIN_MENU_BAR,
MenuModelRegistry, MenuModelRegistry,
MenuNode, MenuNode,
MenuPath, MenuPath,
SubMenuOptions, SubMenuOptions,
} from '@theia/core/lib/common/menu'; } from '@theia/core/lib/common/menu';
export namespace ArduinoMenus { export namespace ArduinoMenus {
// Main menu // Main menu
// -- File // -- File
export const FILE__SKETCH_GROUP = [...CommonMenus.FILE, '0_sketch']; export const FILE__SKETCH_GROUP = [...CommonMenus.FILE, '0_sketch'];
export const FILE__PRINT_GROUP = [...CommonMenus.FILE, '1_print']; export const FILE__PRINT_GROUP = [...CommonMenus.FILE, '1_print'];
// XXX: on macOS, the "Preferences" and "Advanced" group is not under `File` // XXX: on macOS, the "Preferences" and "Advanced" group is not under `File`
// The empty path ensures no top level menu is created for the preferences, even if they contains sub menus // The empty path ensures no top level menu is created for the preferences, even if they contains sub menus
export const FILE__PREFERENCES_GROUP = [ export const FILE__PREFERENCES_GROUP = [
...(isOSX ? [''] : CommonMenus.FILE), ...(isOSX ? [''] : CommonMenus.FILE),
'2_preferences', '2_preferences',
]; ];
export const FILE__ADVANCED_GROUP = [ export const FILE__ADVANCED_GROUP = [
...(isOSX ? [''] : CommonMenus.FILE), ...(isOSX ? [''] : CommonMenus.FILE),
'3_advanced', '3_advanced',
]; ];
export const FILE__ADVANCED_SUBMENU = [ export const FILE__ADVANCED_SUBMENU = [
...FILE__ADVANCED_GROUP, ...FILE__ADVANCED_GROUP,
'0_advanced_sub', '0_advanced_sub',
]; ];
export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit']; export const FILE__QUIT_GROUP = [...CommonMenus.FILE, '3_quit'];
// -- File / Open Recent // -- File / Open Recent
export const FILE__OPEN_RECENT_SUBMENU = [ export const FILE__OPEN_RECENT_SUBMENU = [
...FILE__SKETCH_GROUP, ...FILE__SKETCH_GROUP,
'0_open_recent', '0_open_recent',
]; ];
// -- File / Sketchbook // -- File / Sketchbook
export const FILE__SKETCHBOOK_SUBMENU = [ export const FILE__SKETCHBOOK_SUBMENU = [
...FILE__SKETCH_GROUP, ...FILE__SKETCH_GROUP,
'1_sketchbook', '1_sketchbook',
]; ];
// -- File / Examples // -- File / Examples
export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '2_examples']; export const FILE__EXAMPLES_SUBMENU = [...FILE__SKETCH_GROUP, '2_examples'];
export const EXAMPLES__BUILT_IN_GROUP = [ export const EXAMPLES__BUILT_IN_GROUP = [
...FILE__EXAMPLES_SUBMENU, ...FILE__EXAMPLES_SUBMENU,
'0_built_ins', '0_built_ins',
]; ];
export const EXAMPLES__ANY_BOARD_GROUP = [ export const EXAMPLES__ANY_BOARD_GROUP = [
...FILE__EXAMPLES_SUBMENU, ...FILE__EXAMPLES_SUBMENU,
'1_any_board', '1_any_board',
]; ];
export const EXAMPLES__CURRENT_BOARD_GROUP = [ export const EXAMPLES__CURRENT_BOARD_GROUP = [
...FILE__EXAMPLES_SUBMENU, ...FILE__EXAMPLES_SUBMENU,
'2_current_board', '2_current_board',
]; ];
export const EXAMPLES__USER_LIBS_GROUP = [ export const EXAMPLES__USER_LIBS_GROUP = [
...FILE__EXAMPLES_SUBMENU, ...FILE__EXAMPLES_SUBMENU,
'3_user_libs', '3_user_libs',
]; ];
// -- Edit // -- Edit
// `Copy`, `Copy to Forum`, `Paste`, etc. // `Copy`, `Copy to Forum`, `Paste`, etc.
// Note: `1_undo` is the first group from Theia, we start with `2` // Note: `1_undo` is the first group from Theia, we start with `2`
export const EDIT__TEXT_CONTROL_GROUP = [ export const EDIT__TEXT_CONTROL_GROUP = [
...CommonMenus.EDIT, ...CommonMenus.EDIT,
'2_text_control', '2_text_control',
]; ];
// `Comment/Uncomment`, etc. // `Comment/Uncomment`, etc.
export const EDIT__CODE_CONTROL_GROUP = [ export const EDIT__CODE_CONTROL_GROUP = [
...CommonMenus.EDIT, ...CommonMenus.EDIT,
'3_code_control', '3_code_control',
]; ];
export const EDIT__FONT_CONTROL_GROUP = [ export const EDIT__FONT_CONTROL_GROUP = [
...CommonMenus.EDIT, ...CommonMenus.EDIT,
'4_font_control', '4_font_control',
]; ];
export const EDIT__FIND_GROUP = [...CommonMenus.EDIT, '5_find']; export const EDIT__FIND_GROUP = [...CommonMenus.EDIT, '5_find'];
// -- Sketch // -- Sketch
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
export const SKETCH__MAIN_GROUP = [...SKETCH, '0_main']; export const SKETCH__MAIN_GROUP = [...SKETCH, '0_main'];
export const SKETCH__UTILS_GROUP = [...SKETCH, '1_utils']; export const SKETCH__UTILS_GROUP = [...SKETCH, '1_utils'];
// -- Tools // -- Tools
export const TOOLS = [...MAIN_MENU_BAR, '4_tools']; export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
// `Auto Format`, `Library Manager...`, `Boards Manager...` // `Auto Format`, `Library Manager...`, `Boards Manager...`
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main']; export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
// `Board`, `Port`, and `Get Board Info`. // `Board`, `Port`, and `Get Board Info`.
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection']; export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader` // Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings']; export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
// -- Help // -- Help
// `Getting Started`, `Environment`, `Troubleshooting`, etc. // `Getting Started`, `Environment`, `Troubleshooting`, etc.
export const HELP__MAIN_GROUP = [...CommonMenus.HELP, '0_main']; export const HELP__MAIN_GROUP = [...CommonMenus.HELP, '0_main'];
// `Find in reference`, `FAQ`, etc. // `Find in reference`, `FAQ`, etc.
export const HELP__FIND_GROUP = [...CommonMenus.HELP, '1_find']; export const HELP__FIND_GROUP = [...CommonMenus.HELP, '1_find'];
// `Advanced Mode`. // `Advanced Mode`.
// XXX: this will be removed. // XXX: this will be removed.
export const HELP__CONTROL_GROUP = [...CommonMenus.HELP, '2_control']; export const HELP__CONTROL_GROUP = [...CommonMenus.HELP, '2_control'];
// `About` group // `About` group
// XXX: on macOS, the about group is not under `Help` // XXX: on macOS, the about group is not under `Help`
export const HELP__ABOUT_GROUP = [ export const HELP__ABOUT_GROUP = [
...(isOSX ? MAIN_MENU_BAR : CommonMenus.HELP), ...(isOSX ? MAIN_MENU_BAR : CommonMenus.HELP),
'999_about', '999_about',
]; ];
// ------------ // ------------
// Context menus // Context menus
// -- Open // -- Open
export const OPEN_SKETCH__CONTEXT = ['arduino-open-sketch--context']; export const OPEN_SKETCH__CONTEXT = ['arduino-open-sketch--context'];
export const OPEN_SKETCH__CONTEXT__OPEN_GROUP = [ export const OPEN_SKETCH__CONTEXT__OPEN_GROUP = [
...OPEN_SKETCH__CONTEXT, ...OPEN_SKETCH__CONTEXT,
'0_open', '0_open',
]; ];
export const OPEN_SKETCH__CONTEXT__RECENT_GROUP = [ export const OPEN_SKETCH__CONTEXT__RECENT_GROUP = [
...OPEN_SKETCH__CONTEXT, ...OPEN_SKETCH__CONTEXT,
'1_recent', '1_recent',
]; ];
export const OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP = [ export const OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP = [
...OPEN_SKETCH__CONTEXT, ...OPEN_SKETCH__CONTEXT,
'2_examples', '2_examples',
]; ];
// -- Sketch control // -- Sketch control
export const SKETCH_CONTROL__CONTEXT = ['arduino-sketch-control--context']; export const SKETCH_CONTROL__CONTEXT = ['arduino-sketch-control--context'];
// `New Tab`, `Rename`, `Delete` // `New Tab`, `Rename`, `Delete`
export const SKETCH_CONTROL__CONTEXT__MAIN_GROUP = [ export const SKETCH_CONTROL__CONTEXT__MAIN_GROUP = [
...SKETCH_CONTROL__CONTEXT, ...SKETCH_CONTROL__CONTEXT,
'0_main', '0_main',
]; ];
// `Previous Tab`, `Next Tab` // `Previous Tab`, `Next Tab`
export const SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP = [ export const SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP = [
...SKETCH_CONTROL__CONTEXT, ...SKETCH_CONTROL__CONTEXT,
'1_navigation', '1_navigation',
]; ];
// Sketch files opened in editors // Sketch files opened in editors
export const SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP = [ export const SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP = [
...SKETCH_CONTROL__CONTEXT, ...SKETCH_CONTROL__CONTEXT,
'2_resources', '2_resources',
]; ];
} }
/** /**
@@ -150,49 +150,49 @@ export namespace ArduinoMenus {
* Theia cannot dispose submenu entries with a proper API: https://github.com/eclipse-theia/theia/issues/7299 * Theia cannot dispose submenu entries with a proper API: https://github.com/eclipse-theia/theia/issues/7299
*/ */
export function unregisterSubmenu( export function unregisterSubmenu(
menuPath: string[], menuPath: string[],
menuRegistry: MenuModelRegistry menuRegistry: MenuModelRegistry
): void { ): void {
if (menuPath.length < 2) { if (menuPath.length < 2) {
throw new Error( throw new Error(
`Expected at least two item as a menu-path. Got ${JSON.stringify( `Expected at least two item as a menu-path. Got ${JSON.stringify(
menuPath menuPath
)} instead.` )} instead.`
); );
} }
const toRemove = menuPath[menuPath.length - 1]; const toRemove = menuPath[menuPath.length - 1];
const parentMenuPath = menuPath.slice(0, menuPath.length - 1); const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation. // This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
// https://github.com/eclipse-theia/theia/issues/7300 // https://github.com/eclipse-theia/theia/issues/7300
const parent = menuRegistry.getMenu(parentMenuPath); const parent = menuRegistry.getMenu(parentMenuPath);
const index = parent.children.findIndex(({ id }) => id === toRemove); const index = parent.children.findIndex(({ id }) => id === toRemove);
if (index === -1) { if (index === -1) {
throw new Error( throw new Error(
`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.` `Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`
); );
} }
(parent.children as Array<MenuNode>).splice(index, 1); (parent.children as Array<MenuNode>).splice(index, 1);
} }
/** /**
* Special menu node that is not backed by any commands and is always disabled. * Special menu node that is not backed by any commands and is always disabled.
*/ */
export class PlaceholderMenuNode implements MenuNode { export class PlaceholderMenuNode implements MenuNode {
constructor( constructor(
protected readonly menuPath: MenuPath, protected readonly menuPath: MenuPath,
readonly label: string, readonly label: string,
protected options: SubMenuOptions = { order: '0' } protected options: SubMenuOptions = { order: '0' }
) {} ) {}
get icon(): string | undefined { get icon(): string | undefined {
return this.options?.iconClass; return this.options?.iconClass;
} }
get sortString(): string { get sortString(): string {
return this.options?.order || this.label; return this.options?.order || this.label;
} }
get id(): string { get id(): string {
return [...this.menuPath, 'placeholder'].join('-'); return [...this.menuPath, 'placeholder'].join('-');
} }
} }

View File

@@ -4,17 +4,17 @@ import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { import {
MonitorService, MonitorService,
MonitorConfig, MonitorConfig,
MonitorError, MonitorError,
Status, Status,
} from '../../common/protocol/monitor-service'; } from '../../common/protocol/monitor-service';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { import {
Port, Port,
Board, Board,
BoardsService, BoardsService,
AttachedBoardsChangeEvent, AttachedBoardsChangeEvent,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { MonitorServiceClientImpl } from './monitor-service-client-impl'; import { MonitorServiceClientImpl } from './monitor-service-client-impl';
import { BoardsConfig } from '../boards/boards-config'; import { BoardsConfig } from '../boards/boards-config';
@@ -23,343 +23,332 @@ import { NotificationCenter } from '../notification-center';
@injectable() @injectable()
export class MonitorConnection { export class MonitorConnection {
@inject(MonitorModel) @inject(MonitorModel)
protected readonly monitorModel: MonitorModel; protected readonly monitorModel: MonitorModel;
@inject(MonitorService) @inject(MonitorService)
protected readonly monitorService: MonitorService; protected readonly monitorService: MonitorService;
@inject(MonitorServiceClientImpl) @inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl; protected readonly monitorServiceClient: MonitorServiceClientImpl;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardsService: BoardsService; protected readonly boardsService: BoardsService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider; protected readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
@inject(MessageService) @inject(MessageService)
protected messageService: MessageService; protected messageService: MessageService;
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
protected readonly applicationState: FrontendApplicationStateService; protected readonly applicationState: FrontendApplicationStateService;
protected state: MonitorConnection.State | undefined; protected state: MonitorConnection.State | undefined;
/** /**
* Note: The idea is to toggle this property from the UI (`Monitor` view) * Note: The idea is to toggle this property from the UI (`Monitor` view)
* and the boards config and the boards attachment/detachment logic can be at on place, here. * and the boards config and the boards attachment/detachment logic can be at on place, here.
*/ */
protected _autoConnect = false; protected _autoConnect = false;
protected readonly onConnectionChangedEmitter = new Emitter< protected readonly onConnectionChangedEmitter = new Emitter<
MonitorConnection.State | undefined MonitorConnection.State | undefined
>(); >();
/** /**
* This emitter forwards all read events **iff** the connection is established. * This emitter forwards all read events **iff** the connection is established.
*/ */
protected readonly onReadEmitter = new Emitter<{ message: string }>(); protected readonly onReadEmitter = new Emitter<{ message: string }>();
/** /**
* Array for storing previous monitor errors received from the server, and based on the number of elements in this array, * Array for storing previous monitor errors received from the server, and based on the number of elements in this array,
* we adjust the reconnection delay. * we adjust the reconnection delay.
* Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array. * Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array.
*/ */
protected monitorErrors: MonitorError[] = []; protected monitorErrors: MonitorError[] = [];
protected reconnectTimeout?: number; protected reconnectTimeout?: number;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.monitorServiceClient.onError(async (error) => { this.monitorServiceClient.onError(async (error) => {
let shouldReconnect = false; let shouldReconnect = false;
if (this.state) { if (this.state) {
const { code, config } = error; const { code, config } = error;
const { board, port } = config; const { board, port } = config;
const options = { timeout: 3000 }; const options = { timeout: 3000 };
switch (code) { switch (code) {
case MonitorError.ErrorCodes.CLIENT_CANCEL: { case MonitorError.ErrorCodes.CLIENT_CANCEL: {
console.debug( console.debug(
`Connection was canceled by client: ${MonitorConnection.State.toString( `Connection was canceled by client: ${MonitorConnection.State.toString(
this.state this.state
)}.` )}.`
);
break;
}
case MonitorError.ErrorCodes.DEVICE_BUSY: {
this.messageService.warn(
`Connection failed. Serial port is busy: ${Port.toString(
port
)}.`,
options
);
shouldReconnect = this.autoConnect;
this.monitorErrors.push(error);
break;
}
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(
`Disconnected ${Board.toString(board, {
useFqbn: false,
})} from ${Port.toString(port)}.`,
options
);
break;
}
case undefined: {
this.messageService.error(
`Unexpected error. Reconnecting ${Board.toString(
board
)} on port ${Port.toString(port)}.`,
options
);
console.error(JSON.stringify(error));
shouldReconnect = this.connected && this.autoConnect;
break;
}
}
const oldState = this.state;
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
if (shouldReconnect) {
if (this.monitorErrors.length >= 10) {
this.messageService.warn(
`Failed to reconnect ${Board.toString(board, {
useFqbn: false,
})} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(
port
)} serial port is busy. after 10 consecutive attempts.`
);
this.monitorErrors.length = 0;
} else {
const attempts = this.monitorErrors.length || 1;
if (this.reconnectTimeout !== undefined) {
// Clear the previous timer.
window.clearTimeout(this.reconnectTimeout);
}
const timeout = attempts * 1000;
this.messageService.warn(
`Reconnecting ${Board.toString(board, {
useFqbn: false,
})} to ${Port.toString(
port
)} in ${attempts} seconds...`,
{ timeout }
);
this.reconnectTimeout = window.setTimeout(
() => this.connect(oldState.config),
timeout
);
}
}
}
});
this.boardsServiceProvider.onBoardsConfigChanged(
this.handleBoardConfigChange.bind(this)
);
this.notificationCenter.onAttachedBoardsChanged((event) => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (
attached.boards.some(
(board) =>
!!board.port &&
BoardsConfig.Config.sameAs(boardsConfig, board)
)
) {
const { selectedBoard: board, selectedPort: port } =
boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect().then(() =>
this.connect({ board, port, baudRate })
);
}
}
}
});
// Handles the `baudRate` changes by reconnecting if required.
this.monitorModel.onChange(({ property }) => {
if (property === 'baudRate' && this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
}
});
}
get connected(): boolean {
return !!this.state;
}
get monitorConfig(): MonitorConfig | undefined {
return this.state ? this.state.config : undefined;
}
get autoConnect(): boolean {
return this._autoConnect;
}
set autoConnect(value: boolean) {
const oldValue = this._autoConnect;
this._autoConnect = value;
// When we enable the auto-connect, we have to connect
if (!oldValue && value) {
// We have to make sure the previous boards config has been restored.
// Otherwise, we might start the auto-connection without configured boards.
this.applicationState
.reachedState('started_contributions')
.then(() => {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
});
} else if (oldValue && !value) {
if (this.reconnectTimeout !== undefined) {
window.clearTimeout(this.reconnectTimeout);
this.monitorErrors.length = 0;
}
}
}
async connect(config: MonitorConfig): Promise<Status> {
if (this.connected) {
const disconnectStatus = await this.disconnect();
if (!Status.isOK(disconnectStatus)) {
return disconnectStatus;
}
}
console.info(
`>>> Creating serial monitor connection for ${Board.toString(
config.board
)} on port ${Port.toString(config.port)}...`
);
const connectStatus = await this.monitorService.connect(config);
if (Status.isOK(connectStatus)) {
const requestMessage = () => {
this.monitorService.request().then(({ message }) => {
if (this.connected) {
this.onReadEmitter.fire({ message });
requestMessage();
}
});
};
requestMessage();
this.state = { config };
console.info(
`<<< Serial monitor connection created for ${Board.toString(
config.board,
{ useFqbn: false }
)} on port ${Port.toString(config.port)}.`
); );
} break;
this.onConnectionChangedEmitter.fire(this.state); }
return Status.isOK(connectStatus); case MonitorError.ErrorCodes.DEVICE_BUSY: {
} this.messageService.warn(
`Connection failed. Serial port is busy: ${Port.toString(port)}.`,
async disconnect(): Promise<Status> { options
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(
stateCopy
)}`
); );
} else { shouldReconnect = this.autoConnect;
console.warn( this.monitorErrors.push(error);
`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString( break;
stateCopy }
)}` case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(
`Disconnected ${Board.toString(board, {
useFqbn: false,
})} from ${Port.toString(port)}.`,
options
); );
break;
}
case undefined: {
this.messageService.error(
`Unexpected error. Reconnecting ${Board.toString(
board
)} on port ${Port.toString(port)}.`,
options
);
console.error(JSON.stringify(error));
shouldReconnect = this.connected && this.autoConnect;
break;
}
} }
const oldState = this.state;
this.state = undefined; this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state); this.onConnectionChangedEmitter.fire(this.state);
return status; if (shouldReconnect) {
} if (this.monitorErrors.length >= 10) {
this.messageService.warn(
/** `Failed to reconnect ${Board.toString(board, {
* Sends the data to the connected serial monitor. useFqbn: false,
* The desired EOL is appended to `data`, you do not have to add it. })} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(
* It is a NOOP if connected. port
*/ )} serial port is busy. after 10 consecutive attempts.`
async send(data: string): Promise<Status> { );
if (!this.connected) { this.monitorErrors.length = 0;
return Status.NOT_CONNECTED; } else {
} const attempts = this.monitorErrors.length || 1;
return new Promise<Status>((resolve) => { if (this.reconnectTimeout !== undefined) {
this.monitorService // Clear the previous timer.
.send(data + this.monitorModel.lineEnding) window.clearTimeout(this.reconnectTimeout);
.then(() => resolve(Status.OK));
});
}
get onConnectionChanged(): Event<MonitorConnection.State | undefined> {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<{ message: string }> {
return this.onReadEmitter.event;
}
protected async handleBoardConfigChange(
boardsConfig: BoardsConfig.Config
): Promise<void> {
if (this.autoConnect) {
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
this.boardsService.getAvailablePorts().then((ports) => {
if (
ports.some((port) =>
Port.equals(port, boardsConfig.selectedPort)
)
) {
new Promise<void>((resolve) => {
// First, disconnect if connected.
if (this.connected) {
this.disconnect().then(() => resolve());
return;
}
resolve();
}).then(() => {
// Then (re-)connect.
const { selectedBoard: board, selectedPort: port } =
boardsConfig;
const { baudRate } = this.monitorModel;
this.connect({ board, port, baudRate });
});
}
});
} }
const timeout = attempts * 1000;
this.messageService.warn(
`Reconnecting ${Board.toString(board, {
useFqbn: false,
})} to ${Port.toString(port)} in ${attempts} seconds...`,
{ timeout }
);
this.reconnectTimeout = window.setTimeout(
() => this.connect(oldState.config),
timeout
);
}
} }
}
});
this.boardsServiceProvider.onBoardsConfigChanged(
this.handleBoardConfigChange.bind(this)
);
this.notificationCenter.onAttachedBoardsChanged((event) => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (
attached.boards.some(
(board) =>
!!board.port && BoardsConfig.Config.sameAs(boardsConfig, board)
)
) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect().then(() =>
this.connect({ board, port, baudRate })
);
}
}
}
});
// Handles the `baudRate` changes by reconnecting if required.
this.monitorModel.onChange(({ property }) => {
if (property === 'baudRate' && this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
}
});
}
get connected(): boolean {
return !!this.state;
}
get monitorConfig(): MonitorConfig | undefined {
return this.state ? this.state.config : undefined;
}
get autoConnect(): boolean {
return this._autoConnect;
}
set autoConnect(value: boolean) {
const oldValue = this._autoConnect;
this._autoConnect = value;
// When we enable the auto-connect, we have to connect
if (!oldValue && value) {
// We have to make sure the previous boards config has been restored.
// Otherwise, we might start the auto-connection without configured boards.
this.applicationState.reachedState('started_contributions').then(() => {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
});
} else if (oldValue && !value) {
if (this.reconnectTimeout !== undefined) {
window.clearTimeout(this.reconnectTimeout);
this.monitorErrors.length = 0;
}
} }
}
async connect(config: MonitorConfig): Promise<Status> {
if (this.connected) {
const disconnectStatus = await this.disconnect();
if (!Status.isOK(disconnectStatus)) {
return disconnectStatus;
}
}
console.info(
`>>> Creating serial monitor connection for ${Board.toString(
config.board
)} on port ${Port.toString(config.port)}...`
);
const connectStatus = await this.monitorService.connect(config);
if (Status.isOK(connectStatus)) {
const requestMessage = () => {
this.monitorService.request().then(({ message }) => {
if (this.connected) {
this.onReadEmitter.fire({ message });
requestMessage();
}
});
};
requestMessage();
this.state = { config };
console.info(
`<<< Serial monitor connection created for ${Board.toString(
config.board,
{ useFqbn: false }
)} on port ${Port.toString(config.port)}.`
);
}
this.onConnectionChangedEmitter.fire(this.state);
return Status.isOK(connectStatus);
}
async disconnect(): Promise<Status> {
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(
stateCopy
)}`
);
} else {
console.warn(
`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(
stateCopy
)}`
);
}
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
return status;
}
/**
* Sends the data to the connected serial monitor.
* The desired EOL is appended to `data`, you do not have to add it.
* It is a NOOP if connected.
*/
async send(data: string): Promise<Status> {
if (!this.connected) {
return Status.NOT_CONNECTED;
}
return new Promise<Status>((resolve) => {
this.monitorService
.send(data + this.monitorModel.lineEnding)
.then(() => resolve(Status.OK));
});
}
get onConnectionChanged(): Event<MonitorConnection.State | undefined> {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<{ message: string }> {
return this.onReadEmitter.event;
}
protected async handleBoardConfigChange(
boardsConfig: BoardsConfig.Config
): Promise<void> {
if (this.autoConnect) {
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
this.boardsService.getAvailablePorts().then((ports) => {
if (
ports.some((port) => Port.equals(port, boardsConfig.selectedPort))
) {
new Promise<void>((resolve) => {
// First, disconnect if connected.
if (this.connected) {
this.disconnect().then(() => resolve());
return;
}
resolve();
}).then(() => {
// Then (re-)connect.
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.connect({ board, port, baudRate });
});
}
});
}
}
}
} }
export namespace MonitorConnection { export namespace MonitorConnection {
export interface State { export interface State {
readonly config: MonitorConfig; readonly config: MonitorConfig;
} }
export namespace State { export namespace State {
export function toString(state: State): string { export function toString(state: State): string {
const { config } = state; const { config } = state;
const { board, port } = config; const { board, port } = config;
return `${Board.toString(board)} ${Port.toString(port)}`; return `${Board.toString(board)} ${Port.toString(port)}`;
}
} }
}
} }

View File

@@ -2,143 +2,143 @@ import { injectable, inject } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event'; import { Emitter, Event } from '@theia/core/lib/common/event';
import { MonitorConfig } from '../../common/protocol/monitor-service'; import { MonitorConfig } from '../../common/protocol/monitor-service';
import { import {
FrontendApplicationContribution, FrontendApplicationContribution,
LocalStorageService, LocalStorageService,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
@injectable() @injectable()
export class MonitorModel implements FrontendApplicationContribution { export class MonitorModel implements FrontendApplicationContribution {
protected static STORAGE_ID = 'arduino-monitor-model'; protected static STORAGE_ID = 'arduino-monitor-model';
@inject(LocalStorageService) @inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService; protected readonly localStorageService: LocalStorageService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly onChangeEmitter: Emitter< protected readonly onChangeEmitter: Emitter<
MonitorModel.State.Change<keyof MonitorModel.State> MonitorModel.State.Change<keyof MonitorModel.State>
>; >;
protected _autoscroll: boolean; protected _autoscroll: boolean;
protected _timestamp: boolean; protected _timestamp: boolean;
protected _baudRate: MonitorConfig.BaudRate; protected _baudRate: MonitorConfig.BaudRate;
protected _lineEnding: MonitorModel.EOL; protected _lineEnding: MonitorModel.EOL;
constructor() { constructor() {
this._autoscroll = true; this._autoscroll = true;
this._timestamp = false; this._timestamp = false;
this._baudRate = MonitorConfig.BaudRate.DEFAULT; this._baudRate = MonitorConfig.BaudRate.DEFAULT;
this._lineEnding = MonitorModel.EOL.DEFAULT; this._lineEnding = MonitorModel.EOL.DEFAULT;
this.onChangeEmitter = new Emitter< this.onChangeEmitter = new Emitter<
MonitorModel.State.Change<keyof MonitorModel.State> MonitorModel.State.Change<keyof MonitorModel.State>
>(); >();
} }
onStart(): void { onStart(): void {
this.localStorageService this.localStorageService
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID) .getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
.then((state) => { .then((state) => {
if (state) { if (state) {
this.restoreState(state); this.restoreState(state);
} }
}); });
} }
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> { get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
return this.onChangeEmitter.event; return this.onChangeEmitter.event;
} }
get autoscroll(): boolean { get autoscroll(): boolean {
return this._autoscroll; return this._autoscroll;
} }
toggleAutoscroll(): void { toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll; this._autoscroll = !this._autoscroll;
this.storeState(); this.storeState();
this.storeState().then(() => this.storeState().then(() =>
this.onChangeEmitter.fire({ this.onChangeEmitter.fire({
property: 'autoscroll', property: 'autoscroll',
value: this._autoscroll, value: this._autoscroll,
}) })
); );
} }
get timestamp(): boolean { get timestamp(): boolean {
return this._timestamp; return this._timestamp;
} }
toggleTimestamp(): void { toggleTimestamp(): void {
this._timestamp = !this._timestamp; this._timestamp = !this._timestamp;
this.storeState().then(() => this.storeState().then(() =>
this.onChangeEmitter.fire({ this.onChangeEmitter.fire({
property: 'timestamp', property: 'timestamp',
value: this._timestamp, value: this._timestamp,
}) })
); );
} }
get baudRate(): MonitorConfig.BaudRate { get baudRate(): MonitorConfig.BaudRate {
return this._baudRate; return this._baudRate;
} }
set baudRate(baudRate: MonitorConfig.BaudRate) { set baudRate(baudRate: MonitorConfig.BaudRate) {
this._baudRate = baudRate; this._baudRate = baudRate;
this.storeState().then(() => this.storeState().then(() =>
this.onChangeEmitter.fire({ this.onChangeEmitter.fire({
property: 'baudRate', property: 'baudRate',
value: this._baudRate, value: this._baudRate,
}) })
); );
} }
get lineEnding(): MonitorModel.EOL { get lineEnding(): MonitorModel.EOL {
return this._lineEnding; return this._lineEnding;
} }
set lineEnding(lineEnding: MonitorModel.EOL) { set lineEnding(lineEnding: MonitorModel.EOL) {
this._lineEnding = lineEnding; this._lineEnding = lineEnding;
this.storeState().then(() => this.storeState().then(() =>
this.onChangeEmitter.fire({ this.onChangeEmitter.fire({
property: 'lineEnding', property: 'lineEnding',
value: this._lineEnding, value: this._lineEnding,
}) })
); );
} }
protected restoreState(state: MonitorModel.State): void { protected restoreState(state: MonitorModel.State): void {
this._autoscroll = state.autoscroll; this._autoscroll = state.autoscroll;
this._timestamp = state.timestamp; this._timestamp = state.timestamp;
this._baudRate = state.baudRate; this._baudRate = state.baudRate;
this._lineEnding = state.lineEnding; this._lineEnding = state.lineEnding;
} }
protected async storeState(): Promise<void> { protected async storeState(): Promise<void> {
return this.localStorageService.setData(MonitorModel.STORAGE_ID, { return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
autoscroll: this._autoscroll, autoscroll: this._autoscroll,
timestamp: this._timestamp, timestamp: this._timestamp,
baudRate: this._baudRate, baudRate: this._baudRate,
lineEnding: this._lineEnding, lineEnding: this._lineEnding,
}); });
} }
} }
export namespace MonitorModel { export namespace MonitorModel {
export interface State { export interface State {
autoscroll: boolean; autoscroll: boolean;
timestamp: boolean; timestamp: boolean;
baudRate: MonitorConfig.BaudRate; baudRate: MonitorConfig.BaudRate;
lineEnding: EOL; lineEnding: EOL;
} }
export namespace State { export namespace State {
export interface Change<K extends keyof State> { export interface Change<K extends keyof State> {
readonly property: K; readonly property: K;
readonly value: State[K]; readonly value: State[K];
}
} }
}
export type EOL = '' | '\n' | '\r' | '\r\n'; export type EOL = '' | '\n' | '\r' | '\r\n';
export namespace EOL { export namespace EOL {
export const DEFAULT: EOL = '\n'; export const DEFAULT: EOL = '\n';
} }
} }

View File

@@ -1,16 +1,16 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { import {
MonitorServiceClient, MonitorServiceClient,
MonitorError, MonitorError,
} from '../../common/protocol/monitor-service'; } from '../../common/protocol/monitor-service';
@injectable() @injectable()
export class MonitorServiceClientImpl implements MonitorServiceClient { export class MonitorServiceClientImpl implements MonitorServiceClient {
protected readonly onErrorEmitter = new Emitter<MonitorError>(); protected readonly onErrorEmitter = new Emitter<MonitorError>();
readonly onError = this.onErrorEmitter.event; readonly onError = this.onErrorEmitter.event;
notifyError(error: MonitorError): void { notifyError(error: MonitorError): void {
this.onErrorEmitter.fire(error); this.onErrorEmitter.fire(error);
} }
} }

View File

@@ -4,153 +4,153 @@ import { AbstractViewContribution } from '@theia/core/lib/browser';
import { MonitorWidget } from './monitor-widget'; import { MonitorWidget } from './monitor-widget';
import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core'; import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core';
import { import {
TabBarToolbarContribution, TabBarToolbarContribution,
TabBarToolbarRegistry, TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { MonitorModel } from './monitor-model'; import { MonitorModel } from './monitor-model';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
export namespace SerialMonitor { export namespace SerialMonitor {
export namespace Commands { export namespace Commands {
export const AUTOSCROLL: Command = { export const AUTOSCROLL: Command = {
id: 'serial-monitor-autoscroll', id: 'serial-monitor-autoscroll',
label: 'Autoscroll', label: 'Autoscroll',
}; };
export const TIMESTAMP: Command = { export const TIMESTAMP: Command = {
id: 'serial-monitor-timestamp', id: 'serial-monitor-timestamp',
label: 'Timestamp', label: 'Timestamp',
}; };
export const CLEAR_OUTPUT: Command = { export const CLEAR_OUTPUT: Command = {
id: 'serial-monitor-clear-output', id: 'serial-monitor-clear-output',
label: 'Clear Output', label: 'Clear Output',
iconClass: 'clear-all', iconClass: 'clear-all',
}; };
} }
} }
@injectable() @injectable()
export class MonitorViewContribution export class MonitorViewContribution
extends AbstractViewContribution<MonitorWidget> extends AbstractViewContribution<MonitorWidget>
implements TabBarToolbarContribution implements TabBarToolbarContribution
{ {
static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle'; static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR = static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
MonitorWidget.ID + ':toggle-toolbar'; MonitorWidget.ID + ':toggle-toolbar';
@inject(MonitorModel) protected readonly model: MonitorModel; @inject(MonitorModel) protected readonly model: MonitorModel;
constructor() { constructor() {
super({ super({
widgetId: MonitorWidget.ID, widgetId: MonitorWidget.ID,
widgetName: 'Serial Monitor', widgetName: 'Serial Monitor',
defaultWidgetOptions: { defaultWidgetOptions: {
area: 'bottom', area: 'bottom',
}, },
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR, toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
toggleKeybinding: 'CtrlCmd+Shift+M', toggleKeybinding: 'CtrlCmd+Shift+M',
}); });
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor',
order: '5',
});
} }
}
registerMenus(menus: MenuModelRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
if (this.toggleCommand) { registry.registerItem({
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { id: 'monitor-autoscroll',
commandId: this.toggleCommand.id, render: () => this.renderAutoScrollButton(),
label: 'Serial Monitor', isVisible: (widget) => widget instanceof MonitorWidget,
order: '5', onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
}); });
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: (widget) => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output',
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: (widget) => widget instanceof MonitorWidget,
isVisible: (widget) => widget instanceof MonitorWidget,
execute: (widget) => {
if (widget instanceof MonitorWidget) {
widget.clearConsole();
} }
} },
});
registerToolbarItems(registry: TabBarToolbarRegistry): void { if (this.toggleCommand) {
registry.registerItem({ commands.registerCommand(this.toggleCommand, {
id: 'monitor-autoscroll', execute: () => this.toggle(),
render: () => this.renderAutoScrollButton(), });
isVisible: (widget) => widget instanceof MonitorWidget, commands.registerCommand(
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/ { id: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR },
}); {
registry.registerItem({ isVisible: (widget) =>
id: 'monitor-timestamp', ArduinoToolbar.is(widget) && widget.side === 'right',
render: () => this.renderTimestampButton(), execute: () => this.toggle(),
isVisible: (widget) => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output',
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: (widget) => widget instanceof MonitorWidget,
isVisible: (widget) => widget instanceof MonitorWidget,
execute: (widget) => {
if (widget instanceof MonitorWidget) {
widget.clearConsole();
}
},
});
if (this.toggleCommand) {
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.toggle(),
}
);
} }
);
} }
}
protected async toggle(): Promise<void> { protected async toggle(): Promise<void> {
const widget = this.tryGetWidget(); const widget = this.tryGetWidget();
if (widget) { if (widget) {
widget.dispose(); widget.dispose();
} else { } else {
await this.openView({ activate: true, reveal: true }); await this.openView({ activate: true, reveal: true });
}
} }
}
protected renderAutoScrollButton(): React.ReactNode { protected renderAutoScrollButton(): React.ReactNode {
return ( return (
<React.Fragment key="autoscroll-toolbar-item"> <React.Fragment key="autoscroll-toolbar-item">
<div <div
title="Toggle Autoscroll" title="Toggle Autoscroll"
className={`item enabled fa fa-angle-double-down arduino-monitor ${ className={`item enabled fa fa-angle-double-down arduino-monitor ${
this.model.autoscroll ? 'toggled' : '' this.model.autoscroll ? 'toggled' : ''
}`} }`}
onClick={this.toggleAutoScroll} onClick={this.toggleAutoScroll}
></div> ></div>
</React.Fragment> </React.Fragment>
); );
} }
protected readonly toggleAutoScroll = () => this.doToggleAutoScroll(); protected readonly toggleAutoScroll = () => this.doToggleAutoScroll();
protected async doToggleAutoScroll(): Promise<void> { protected async doToggleAutoScroll(): Promise<void> {
this.model.toggleAutoscroll(); this.model.toggleAutoscroll();
} }
protected renderTimestampButton(): React.ReactNode { protected renderTimestampButton(): React.ReactNode {
return ( return (
<React.Fragment key="line-ending-toolbar-item"> <React.Fragment key="line-ending-toolbar-item">
<div <div
title="Toggle Timestamp" title="Toggle Timestamp"
className={`item enabled fa fa-clock-o arduino-monitor ${ className={`item enabled fa fa-clock-o arduino-monitor ${
this.model.timestamp ? 'toggled' : '' this.model.timestamp ? 'toggled' : ''
}`} }`}
onClick={this.toggleTimestamp} onClick={this.toggleTimestamp}
></div> ></div>
</React.Fragment> </React.Fragment>
); );
} }
protected readonly toggleTimestamp = () => this.doToggleTimestamp(); protected readonly toggleTimestamp = () => this.doToggleTimestamp();
protected async doToggleTimestamp(): Promise<void> { protected async doToggleTimestamp(): Promise<void> {
this.model.toggleTimestamp(); this.model.toggleTimestamp();
} }
} }

View File

@@ -6,14 +6,14 @@ import { isOSX } from '@theia/core/lib/common/os';
import { Event, Emitter } from '@theia/core/lib/common/event'; import { Event, Emitter } from '@theia/core/lib/common/event';
import { Key, KeyCode } from '@theia/core/lib/browser/keys'; import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { import {
DisposableCollection, DisposableCollection,
Disposable, Disposable,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { import {
ReactWidget, ReactWidget,
Message, Message,
Widget, Widget,
MessageLoop, MessageLoop,
} from '@theia/core/lib/browser/widgets'; } from '@theia/core/lib/browser/widgets';
import { Board, Port } from '../../common/protocol/boards-service'; import { Board, Port } from '../../common/protocol/boards-service';
import { MonitorConfig } from '../../common/protocol/monitor-service'; import { MonitorConfig } from '../../common/protocol/monitor-service';
@@ -24,379 +24,371 @@ import { MonitorServiceClientImpl } from './monitor-service-client-impl';
@injectable() @injectable()
export class MonitorWidget extends ReactWidget { export class MonitorWidget extends ReactWidget {
static readonly ID = 'serial-monitor'; static readonly ID = 'serial-monitor';
@inject(MonitorModel) @inject(MonitorModel)
protected readonly monitorModel: MonitorModel; protected readonly monitorModel: MonitorModel;
@inject(MonitorConnection) @inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection; protected readonly monitorConnection: MonitorConnection;
@inject(MonitorServiceClientImpl) @inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl; protected readonly monitorServiceClient: MonitorServiceClientImpl;
protected widgetHeight: number; protected widgetHeight: number;
/** /**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation. * Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/ */
protected focusNode: HTMLElement | undefined; protected focusNode: HTMLElement | undefined;
/** /**
* Guard against re-rendering the view after the close was requested. * Guard against re-rendering the view after the close was requested.
* See: https://github.com/eclipse-theia/theia/issues/6704 * See: https://github.com/eclipse-theia/theia/issues/6704
*/ */
protected closing = false; protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>(); protected readonly clearOutputEmitter = new Emitter<void>();
constructor() { constructor() {
super(); super();
this.id = MonitorWidget.ID; this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor'; this.title.label = 'Serial Monitor';
this.title.iconClass = 'monitor-tab-icon'; this.title.iconClass = 'monitor-tab-icon';
this.title.closable = true; this.title.closable = true;
this.scrollOptions = undefined; this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter); this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push( this.toDispose.push(
Disposable.create(() => { Disposable.create(() => {
this.monitorConnection.autoConnect = false; this.monitorConnection.autoConnect = false;
if (this.monitorConnection.connected) { if (this.monitorConnection.connected) {
this.monitorConnection.disconnect(); this.monitorConnection.disconnect();
}
})
);
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(
this.monitorConnection.onConnectionChanged(() =>
this.clearConsole()
)
);
}
clearConsole(): void {
this.clearOutputEmitter.fire(undefined);
this.update();
}
dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.monitorConnection.autoConnect = true;
}
onCloseRequest(msg: Message): void {
this.closing = true;
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
super.onUpdateRequest(msg);
} }
})
);
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(
this.monitorConnection.onConnectionChanged(() => this.clearConsole())
);
}
clearConsole(): void {
this.clearOutputEmitter.fire(undefined);
this.update();
}
dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.monitorConnection.autoConnect = true;
}
onCloseRequest(msg: Message): void {
this.closing = true;
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
super.onUpdateRequest(msg);
} }
}
protected onResize(msg: Widget.ResizeMessage): void { protected onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg); super.onResize(msg);
this.widgetHeight = msg.height; this.widgetHeight = msg.height;
this.update(); this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
if (this.closing || !this.isAttached) {
return;
} }
this.focusNode = element;
requestAnimationFrame(() =>
MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest)
);
};
protected onActivateRequest(msg: Message): void { protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
super.onActivateRequest(msg); return [
(this.focusNode || this.node).focus(); {
} label: 'No Line Ending',
value: '',
},
{
label: 'New Line',
value: '\n',
},
{
label: 'Carriage Return',
value: '\r',
},
{
label: 'Both NL & CR',
value: '\r\n',
},
];
}
protected onFocusResolved = (element: HTMLElement | undefined) => { protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
if (this.closing || !this.isAttached) { const baudRates: Array<MonitorConfig.BaudRate> = [
return; 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
} ];
this.focusNode = element; return baudRates.map((baudRate) => ({
requestAnimationFrame(() => label: baudRate + ' baud',
MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest) value: baudRate,
); }));
}; }
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> { protected render(): React.ReactNode {
return [ const { baudRates, lineEndings } = this;
{ const lineEnding =
label: 'No Line Ending', lineEndings.find((item) => item.value === this.monitorModel.lineEnding) ||
value: '', lineEndings[1]; // Defaults to `\n`.
}, const baudRate =
{ baudRates.find((item) => item.value === this.monitorModel.baudRate) ||
label: 'New Line', baudRates[4]; // Defaults to `9600`.
value: '\n', return (
}, <div className="serial-monitor">
{ <div className="head">
label: 'Carriage Return', <div className="send">
value: '\r', <SerialMonitorSendInput
}, monitorConfig={this.monitorConnection.monitorConfig}
{ resolveFocus={this.onFocusResolved}
label: 'Both NL & CR', onSend={this.onSend}
value: '\r\n', />
}, </div>
]; <div className="config">
} <div className="select">
<ArduinoSelect
protected get baudRates(): OptionsType< maxMenuHeight={this.widgetHeight - 40}
SelectOption<MonitorConfig.BaudRate> options={lineEndings}
> { defaultValue={lineEnding}
const baudRates: Array<MonitorConfig.BaudRate> = [ onChange={this.onChangeLineEnding}
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, />
];
return baudRates.map((baudRate) => ({
label: baudRate + ' baud',
value: baudRate,
}));
}
protected render(): React.ReactNode {
const { baudRates, lineEndings } = this;
const lineEnding =
lineEndings.find(
(item) => item.value === this.monitorModel.lineEnding
) || lineEndings[1]; // Defaults to `\n`.
const baudRate =
baudRates.find(
(item) => item.value === this.monitorModel.baudRate
) || baudRates[4]; // Defaults to `9600`.
return (
<div className="serial-monitor">
<div className="head">
<div className="send">
<SerialMonitorSendInput
monitorConfig={this.monitorConnection.monitorConfig}
resolveFocus={this.onFocusResolved}
onSend={this.onSend}
/>
</div>
<div className="config">
<div className="select">
<ArduinoSelect
maxMenuHeight={this.widgetHeight - 40}
options={lineEndings}
defaultValue={lineEnding}
onChange={this.onChangeLineEnding}
/>
</div>
<div className="select">
<ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
defaultValue={baudRate}
onChange={this.onChangeBaudRate}
/>
</div>
</div>
</div>
<div className="body">
<SerialMonitorOutput
monitorModel={this.monitorModel}
monitorConnection={this.monitorConnection}
clearConsoleEvent={this.clearOutputEmitter.event}
/>
</div>
</div> </div>
); <div className="select">
} <ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
defaultValue={baudRate}
onChange={this.onChangeBaudRate}
/>
</div>
</div>
</div>
<div className="body">
<SerialMonitorOutput
monitorModel={this.monitorModel}
monitorConnection={this.monitorConnection}
clearConsoleEvent={this.clearOutputEmitter.event}
/>
</div>
</div>
);
}
protected readonly onSend = (value: string) => this.doSend(value); protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> { protected async doSend(value: string): Promise<void> {
this.monitorConnection.send(value); this.monitorConnection.send(value);
} }
protected readonly onChangeLineEnding = ( protected readonly onChangeLineEnding = (
option: SelectOption<MonitorModel.EOL> option: SelectOption<MonitorModel.EOL>
) => { ) => {
this.monitorModel.lineEnding = option.value; this.monitorModel.lineEnding = option.value;
}; };
protected readonly onChangeBaudRate = ( protected readonly onChangeBaudRate = (
option: SelectOption<MonitorConfig.BaudRate> option: SelectOption<MonitorConfig.BaudRate>
) => { ) => {
this.monitorModel.baudRate = option.value; this.monitorModel.baudRate = option.value;
}; };
} }
export namespace SerialMonitorSendInput { export namespace SerialMonitorSendInput {
export interface Props { export interface Props {
readonly monitorConfig?: MonitorConfig; readonly monitorConfig?: MonitorConfig;
readonly onSend: (text: string) => void; readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void; readonly resolveFocus: (element: HTMLElement | undefined) => void;
} }
export interface State { export interface State {
text: string; text: string;
} }
} }
export class SerialMonitorSendInput extends React.Component< export class SerialMonitorSendInput extends React.Component<
SerialMonitorSendInput.Props, SerialMonitorSendInput.Props,
SerialMonitorSendInput.State SerialMonitorSendInput.State
> { > {
constructor(props: Readonly<SerialMonitorSendInput.Props>) { constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props); super(props);
this.state = { text: '' }; this.state = { text: '' };
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this); this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this); this.onKeyDown = this.onKeyDown.bind(this);
} }
render(): React.ReactNode { render(): React.ReactNode {
return ( return (
<input <input
ref={this.setRef} ref={this.setRef}
type="text" type="text"
className={`theia-input ${ className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
this.props.monitorConfig ? '' : 'warning' placeholder={this.placeholder}
}`} value={this.state.text}
placeholder={this.placeholder} onChange={this.onChange}
value={this.state.text} onKeyDown={this.onKeyDown}
onChange={this.onChange} />
onKeyDown={this.onKeyDown} );
/> }
);
}
protected get placeholder(): string { protected get placeholder(): string {
const { monitorConfig } = this.props; const { monitorConfig } = this.props;
if (!monitorConfig) { if (!monitorConfig) {
return 'Not connected. Select a board and a port to connect automatically.'; return 'Not connected. Select a board and a port to connect automatically.';
}
const { board, port } = monitorConfig;
return `Message (${
isOSX ? '⌘' : 'Ctrl'
}+Enter to send message to '${Board.toString(board, {
useFqbn: false,
})}' on '${Port.toString(port)}')`;
} }
const { board, port } = monitorConfig;
return `Message (${
isOSX ? '⌘' : 'Ctrl'
}+Enter to send message to '${Board.toString(board, {
useFqbn: false,
})}' on '${Port.toString(port)}')`;
}
protected setRef = (element: HTMLElement | null) => { protected setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) { if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined); this.props.resolveFocus(element || undefined);
}
};
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ text: event.target.value });
} }
};
protected onSend(): void { protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.props.onSend(this.state.text); this.setState({ text: event.target.value });
this.setState({ text: '' }); }
}
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { protected onSend(): void {
const keyCode = KeyCode.createKeyCode(event.nativeEvent); this.props.onSend(this.state.text);
if (keyCode) { this.setState({ text: '' });
const { key, meta, ctrl } = keyCode; }
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
this.onSend(); protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
} const keyCode = KeyCode.createKeyCode(event.nativeEvent);
} if (keyCode) {
const { key, meta, ctrl } = keyCode;
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
this.onSend();
}
} }
}
} }
export namespace SerialMonitorOutput { export namespace SerialMonitorOutput {
export interface Props { export interface Props {
readonly monitorModel: MonitorModel; readonly monitorModel: MonitorModel;
readonly monitorConnection: MonitorConnection; readonly monitorConnection: MonitorConnection;
readonly clearConsoleEvent: Event<void>; readonly clearConsoleEvent: Event<void>;
} }
export interface State { export interface State {
content: string; content: string;
timestamp: boolean; timestamp: boolean;
} }
} }
export class SerialMonitorOutput extends React.Component< export class SerialMonitorOutput extends React.Component<
SerialMonitorOutput.Props, SerialMonitorOutput.Props,
SerialMonitorOutput.State SerialMonitorOutput.State
> { > {
/** /**
* Do not touch it. It is used to be able to "follow" the serial monitor log. * Do not touch it. It is used to be able to "follow" the serial monitor log.
*/ */
protected anchor: HTMLElement | null; protected anchor: HTMLElement | null;
protected toDisposeBeforeUnmount = new DisposableCollection(); protected toDisposeBeforeUnmount = new DisposableCollection();
constructor(props: Readonly<SerialMonitorOutput.Props>) { constructor(props: Readonly<SerialMonitorOutput.Props>) {
super(props); super(props);
this.state = { this.state = {
content: '', content: '',
timestamp: this.props.monitorModel.timestamp, timestamp: this.props.monitorModel.timestamp,
}; };
} }
render(): React.ReactNode { render(): React.ReactNode {
return ( return (
<React.Fragment> <React.Fragment>
<div style={{ whiteSpace: 'pre', fontFamily: 'monospace' }}> <div style={{ whiteSpace: 'pre', fontFamily: 'monospace' }}>
{this.state.content} {this.state.content}
</div> </div>
<div <div
style={{ float: 'left', clear: 'both' }} style={{ float: 'left', clear: 'both' }}
ref={(element) => { ref={(element) => {
this.anchor = element; this.anchor = element;
}} }}
/> />
</React.Fragment> </React.Fragment>
); );
} }
componentDidMount(): void { componentDidMount(): void {
this.scrollToBottom(); this.scrollToBottom();
this.toDisposeBeforeUnmount.pushAll([ this.toDisposeBeforeUnmount.pushAll([
this.props.monitorConnection.onRead(({ message }) => { this.props.monitorConnection.onRead(({ message }) => {
const rawLines = message.split('\n'); const rawLines = message.split('\n');
const lines: string[] = []; const lines: string[] = [];
const timestamp = () => const timestamp = () =>
this.state.timestamp this.state.timestamp
? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` ? `${dateFormat(new Date(), 'H:M:ss.l')} -> `
: ''; : '';
for (let i = 0; i < rawLines.length; i++) { for (let i = 0; i < rawLines.length; i++) {
if (i === 0 && this.state.content.length !== 0) { if (i === 0 && this.state.content.length !== 0) {
lines.push(rawLines[i]); lines.push(rawLines[i]);
} else { } else {
lines.push(timestamp() + rawLines[i]); lines.push(timestamp() + rawLines[i]);
} }
}
const content = this.state.content + lines.join('\n');
this.setState({ content });
}),
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
}),
]);
}
componentDidUpdate(): void {
this.scrollToBottom();
}
componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
protected scrollToBottom(): void {
if (this.props.monitorModel.autoscroll && this.anchor) {
this.anchor.scrollIntoView();
} }
const content = this.state.content + lines.join('\n');
this.setState({ content });
}),
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
}),
]);
}
componentDidUpdate(): void {
this.scrollToBottom();
}
componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
protected scrollToBottom(): void {
if (this.props.monitorModel.autoscroll && this.anchor) {
this.anchor.scrollIntoView();
} }
}
} }
export interface SelectOption<T> { export interface SelectOption<T> {
readonly label: string; readonly label: string;
readonly value: T; readonly value: T;
} }

View File

@@ -4,117 +4,117 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
NotificationServiceClient, NotificationServiceClient,
NotificationServiceServer, NotificationServiceServer,
} from '../common/protocol/notification-service'; } from '../common/protocol/notification-service';
import { import {
AttachedBoardsChangeEvent, AttachedBoardsChangeEvent,
BoardsPackage, BoardsPackage,
LibraryPackage, LibraryPackage,
Config, Config,
Sketch, Sketch,
} from '../common/protocol'; } from '../common/protocol';
@injectable() @injectable()
export class NotificationCenter export class NotificationCenter
implements NotificationServiceClient, FrontendApplicationContribution implements NotificationServiceClient, FrontendApplicationContribution
{ {
@inject(NotificationServiceServer) @inject(NotificationServiceServer)
protected readonly server: JsonRpcProxy<NotificationServiceServer>; protected readonly server: JsonRpcProxy<NotificationServiceServer>;
protected readonly indexUpdatedEmitter = new Emitter<void>(); protected readonly indexUpdatedEmitter = new Emitter<void>();
protected readonly daemonStartedEmitter = new Emitter<void>(); protected readonly daemonStartedEmitter = new Emitter<void>();
protected readonly daemonStoppedEmitter = new Emitter<void>(); protected readonly daemonStoppedEmitter = new Emitter<void>();
protected readonly configChangedEmitter = new Emitter<{ protected readonly configChangedEmitter = new Emitter<{
config: Config | undefined; config: Config | undefined;
}>(); }>();
protected readonly platformInstalledEmitter = new Emitter<{ protected readonly platformInstalledEmitter = new Emitter<{
item: BoardsPackage; item: BoardsPackage;
}>(); }>();
protected readonly platformUninstalledEmitter = new Emitter<{ protected readonly platformUninstalledEmitter = new Emitter<{
item: BoardsPackage; item: BoardsPackage;
}>(); }>();
protected readonly libraryInstalledEmitter = new Emitter<{ protected readonly libraryInstalledEmitter = new Emitter<{
item: LibraryPackage; item: LibraryPackage;
}>(); }>();
protected readonly libraryUninstalledEmitter = new Emitter<{ protected readonly libraryUninstalledEmitter = new Emitter<{
item: LibraryPackage; item: LibraryPackage;
}>(); }>();
protected readonly attachedBoardsChangedEmitter = protected readonly attachedBoardsChangedEmitter =
new Emitter<AttachedBoardsChangeEvent>(); new Emitter<AttachedBoardsChangeEvent>();
protected readonly recentSketchesChangedEmitter = new Emitter<{ protected readonly recentSketchesChangedEmitter = new Emitter<{
sketches: Sketch[]; sketches: Sketch[];
}>(); }>();
protected readonly toDispose = new DisposableCollection( protected readonly toDispose = new DisposableCollection(
this.indexUpdatedEmitter, this.indexUpdatedEmitter,
this.daemonStartedEmitter, this.daemonStartedEmitter,
this.daemonStoppedEmitter, this.daemonStoppedEmitter,
this.configChangedEmitter, this.configChangedEmitter,
this.platformInstalledEmitter, this.platformInstalledEmitter,
this.platformUninstalledEmitter, this.platformUninstalledEmitter,
this.libraryInstalledEmitter, this.libraryInstalledEmitter,
this.libraryUninstalledEmitter, this.libraryUninstalledEmitter,
this.attachedBoardsChangedEmitter this.attachedBoardsChangedEmitter
); );
readonly onIndexUpdated = this.indexUpdatedEmitter.event; readonly onIndexUpdated = this.indexUpdatedEmitter.event;
readonly onDaemonStarted = this.daemonStartedEmitter.event; readonly onDaemonStarted = this.daemonStartedEmitter.event;
readonly onDaemonStopped = this.daemonStoppedEmitter.event; readonly onDaemonStopped = this.daemonStoppedEmitter.event;
readonly onConfigChanged = this.configChangedEmitter.event; readonly onConfigChanged = this.configChangedEmitter.event;
readonly onPlatformInstalled = this.platformInstalledEmitter.event; readonly onPlatformInstalled = this.platformInstalledEmitter.event;
readonly onPlatformUninstalled = this.platformUninstalledEmitter.event; readonly onPlatformUninstalled = this.platformUninstalledEmitter.event;
readonly onLibraryInstalled = this.libraryInstalledEmitter.event; readonly onLibraryInstalled = this.libraryInstalledEmitter.event;
readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event; readonly onLibraryUninstalled = this.libraryUninstalledEmitter.event;
readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event; readonly onAttachedBoardsChanged = this.attachedBoardsChangedEmitter.event;
readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event; readonly onRecentSketchesChanged = this.recentSketchesChangedEmitter.event;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.server.setClient(this); this.server.setClient(this);
} }
onStop(): void { onStop(): void {
this.toDispose.dispose(); this.toDispose.dispose();
} }
notifyIndexUpdated(): void { notifyIndexUpdated(): void {
this.indexUpdatedEmitter.fire(); this.indexUpdatedEmitter.fire();
} }
notifyDaemonStarted(): void { notifyDaemonStarted(): void {
this.daemonStartedEmitter.fire(); this.daemonStartedEmitter.fire();
} }
notifyDaemonStopped(): void { notifyDaemonStopped(): void {
this.daemonStoppedEmitter.fire(); this.daemonStoppedEmitter.fire();
} }
notifyConfigChanged(event: { config: Config | undefined }): void { notifyConfigChanged(event: { config: Config | undefined }): void {
this.configChangedEmitter.fire(event); this.configChangedEmitter.fire(event);
} }
notifyPlatformInstalled(event: { item: BoardsPackage }): void { notifyPlatformInstalled(event: { item: BoardsPackage }): void {
this.platformInstalledEmitter.fire(event); this.platformInstalledEmitter.fire(event);
} }
notifyPlatformUninstalled(event: { item: BoardsPackage }): void { notifyPlatformUninstalled(event: { item: BoardsPackage }): void {
this.platformUninstalledEmitter.fire(event); this.platformUninstalledEmitter.fire(event);
} }
notifyLibraryInstalled(event: { item: LibraryPackage }): void { notifyLibraryInstalled(event: { item: LibraryPackage }): void {
this.libraryInstalledEmitter.fire(event); this.libraryInstalledEmitter.fire(event);
} }
notifyLibraryUninstalled(event: { item: LibraryPackage }): void { notifyLibraryUninstalled(event: { item: LibraryPackage }): void {
this.libraryUninstalledEmitter.fire(event); this.libraryUninstalledEmitter.fire(event);
} }
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.attachedBoardsChangedEmitter.fire(event); this.attachedBoardsChangedEmitter.fire(event);
} }
notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void { notifyRecentSketchesChanged(event: { sketches: Sketch[] }): void {
this.recentSketchesChangedEmitter.fire(event); this.recentSketchesChangedEmitter.fire(event);
} }
} }

View File

@@ -3,35 +3,34 @@ import { Emitter } from '@theia/core/lib/common/event';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution'; import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import { import {
ResponseService, ResponseService,
OutputMessage, OutputMessage,
ProgressMessage, ProgressMessage,
} from '../common/protocol/response-service'; } from '../common/protocol/response-service';
@injectable() @injectable()
export class ResponseServiceImpl implements ResponseService { export class ResponseServiceImpl implements ResponseService {
@inject(OutputContribution) @inject(OutputContribution)
protected outputContribution: OutputContribution; protected outputContribution: OutputContribution;
@inject(OutputChannelManager) @inject(OutputChannelManager)
protected outputChannelManager: OutputChannelManager; protected outputChannelManager: OutputChannelManager;
protected readonly progressDidChangeEmitter = protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
new Emitter<ProgressMessage>(); readonly onProgressDidChange = this.progressDidChangeEmitter.event;
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
appendToOutput(message: OutputMessage): void { appendToOutput(message: OutputMessage): void {
const { chunk } = message; const { chunk } = message;
const channel = this.outputChannelManager.getChannel('Arduino'); const channel = this.outputChannelManager.getChannel('Arduino');
channel.show({ preserveFocus: true }); channel.show({ preserveFocus: true });
channel.append(chunk); channel.append(chunk);
} }
clearArduinoChannel(): void { clearArduinoChannel(): void {
this.outputChannelManager.getChannel('Arduino').clear(); this.outputChannelManager.getChannel('Arduino').clear();
} }
reportProgress(progress: ProgressMessage): void { reportProgress(progress: ProgressMessage): void {
this.progressDidChangeEmitter.fire(progress); this.progressDidChangeEmitter.fire(progress);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { StorageService } from '@theia/core/lib/browser/storage-service'; import { StorageService } from '@theia/core/lib/browser/storage-service';
import { import {
Command, Command,
CommandContribution, CommandContribution,
CommandRegistry, CommandRegistry,
} from '@theia/core/lib/common/command'; } from '@theia/core/lib/common/command';
/** /**
@@ -11,27 +11,27 @@ import {
*/ */
@injectable() @injectable()
export class StorageWrapper implements CommandContribution { export class StorageWrapper implements CommandContribution {
@inject(StorageService) @inject(StorageService)
protected storageService: StorageService; protected storageService: StorageService;
registerCommands(commands: CommandRegistry): void { registerCommands(commands: CommandRegistry): void {
commands.registerCommand(StorageWrapper.Commands.GET_DATA, { commands.registerCommand(StorageWrapper.Commands.GET_DATA, {
execute: (key: string, defaultValue?: any) => execute: (key: string, defaultValue?: any) =>
this.storageService.getData(key, defaultValue), this.storageService.getData(key, defaultValue),
}); });
commands.registerCommand(StorageWrapper.Commands.SET_DATA, { commands.registerCommand(StorageWrapper.Commands.SET_DATA, {
execute: (key: string, value: any) => execute: (key: string, value: any) =>
this.storageService.setData(key, value), this.storageService.setData(key, value),
}); });
} }
} }
export namespace StorageWrapper { export namespace StorageWrapper {
export namespace Commands { export namespace Commands {
export const SET_DATA: Command = { export const SET_DATA: Command = {
id: 'arduino-store-wrapper-set', id: 'arduino-store-wrapper-set',
}; };
export const GET_DATA: Command = { export const GET_DATA: Command = {
id: 'arduino-store-wrapper-get', id: 'arduino-store-wrapper-get',
}; };
} }
} }

View File

@@ -4,12 +4,12 @@ import { CommandService } from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { OutputWidget } from '@theia/output/lib/browser/output-widget'; import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { import {
ConnectionStatusService, ConnectionStatusService,
ConnectionStatus, ConnectionStatus,
} from '@theia/core/lib/browser/connection-status-service'; } from '@theia/core/lib/browser/connection-status-service';
import { import {
ApplicationShell as TheiaApplicationShell, ApplicationShell as TheiaApplicationShell,
Widget, Widget,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { Sketch } from '../../../common/protocol'; import { Sketch } from '../../../common/protocol';
import { SaveAsSketch } from '../../contributions/save-as-sketch'; import { SaveAsSketch } from '../../contributions/save-as-sketch';
@@ -17,76 +17,75 @@ import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-ser
@injectable() @injectable()
export class ApplicationShell extends TheiaApplicationShell { export class ApplicationShell extends TheiaApplicationShell {
@inject(CommandService) @inject(CommandService)
protected readonly commandService: CommandService; protected readonly commandService: CommandService;
@inject(MessageService) @inject(MessageService)
protected readonly messageService: MessageService; protected readonly messageService: MessageService;
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl; protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(ConnectionStatusService) @inject(ConnectionStatusService)
protected readonly connectionStatusService: ConnectionStatusService; protected readonly connectionStatusService: ConnectionStatusService;
protected track(widget: Widget): void { protected track(widget: Widget): void {
super.track(widget); super.track(widget);
if (widget instanceof OutputWidget) { if (widget instanceof OutputWidget) {
widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700 widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700
}
if (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;
}
}
});
}
} }
if (widget instanceof EditorWidget) {
async addWidget( // Make the editor un-closeable asynchronously.
widget: Widget, this.sketchesServiceClient.currentSketch().then((sketch) => {
options: Readonly<TheiaApplicationShell.WidgetOptions> = {} if (sketch) {
): Promise<void> { if (Sketch.isInSketch(widget.editor.uri, sketch)) {
// By default, Theia open a widget **next** to the currently active in the target area. widget.title.closable = false;
// 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; });
const 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;
}
}
}
return super.addWidget(widget, { ...options, ref });
} }
}
async saveAll(): Promise<void> { async addWidget(
if ( widget: Widget,
this.connectionStatusService.currentStatus === options: Readonly<TheiaApplicationShell.WidgetOptions> = {}
ConnectionStatus.OFFLINE ): Promise<void> {
) { // By default, Theia open a widget **next** to the currently active in the target area.
this.messageService.error( // Instead of this logic, we want to open the new widget after the last of the target area.
'Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.' if (!widget.id) {
); console.error(
return; // Theia does not reject on failed save: https://github.com/eclipse-theia/theia/pull/8803 'Widgets added to the application shell must have a unique id property.'
} );
await super.saveAll(); return;
const options = { execOnlyIfTemp: true, openAfterMove: true };
await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
options
);
} }
let ref: Widget | undefined = options.ref;
const 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;
}
}
}
return super.addWidget(widget, { ...options, ref });
}
async saveAll(): Promise<void> {
if (
this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE
) {
this.messageService.error(
'Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.'
);
return; // Theia does not reject on failed save: https://github.com/eclipse-theia/theia/pull/8803
}
await super.saveAll();
const options = { execOnlyIfTemp: true, openAfterMove: true };
await this.commandService.executeCommand(
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
options
);
}
} }

View File

@@ -1,26 +1,26 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { import {
BrowserMainMenuFactory as TheiaBrowserMainMenuFactory, BrowserMainMenuFactory as TheiaBrowserMainMenuFactory,
MenuBarWidget, MenuBarWidget,
} from '@theia/core/lib/browser/menu/browser-menu-plugin'; } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager'; import { MainMenuManager } from '../../../common/main-menu-manager';
@injectable() @injectable()
export class BrowserMainMenuFactory export class BrowserMainMenuFactory
extends TheiaBrowserMainMenuFactory extends TheiaBrowserMainMenuFactory
implements MainMenuManager implements MainMenuManager
{ {
protected menuBar: MenuBarWidget | undefined; protected menuBar: MenuBarWidget | undefined;
createMenuBar(): MenuBarWidget { createMenuBar(): MenuBarWidget {
this.menuBar = super.createMenuBar(); this.menuBar = super.createMenuBar();
return this.menuBar; return this.menuBar;
} }
update() { update() {
if (this.menuBar) { if (this.menuBar) {
this.menuBar.clearMenus(); this.menuBar.clearMenus();
this.fillMenuBar(this.menuBar); this.fillMenuBar(this.menuBar);
}
} }
}
} }

View File

@@ -1,18 +1,18 @@
import '../../../../src/browser/style/browser-menu.css'; import '../../../../src/browser/style/browser-menu.css';
import { ContainerModule } from 'inversify'; import { ContainerModule } from 'inversify';
import { import {
BrowserMenuBarContribution, BrowserMenuBarContribution,
BrowserMainMenuFactory as TheiaBrowserMainMenuFactory, BrowserMainMenuFactory as TheiaBrowserMainMenuFactory,
} from '@theia/core/lib/browser/menu/browser-menu-plugin'; } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager'; import { MainMenuManager } from '../../../common/main-menu-manager';
import { ArduinoMenuContribution } from './browser-menu-plugin'; import { ArduinoMenuContribution } from './browser-menu-plugin';
import { BrowserMainMenuFactory } from './browser-main-menu-factory'; import { BrowserMainMenuFactory } from './browser-main-menu-factory';
export default new ContainerModule((bind, unbind, isBound, rebind) => { export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BrowserMainMenuFactory).toSelf().inSingletonScope(); bind(BrowserMainMenuFactory).toSelf().inSingletonScope();
bind(MainMenuManager).toService(BrowserMainMenuFactory); bind(MainMenuManager).toService(BrowserMainMenuFactory);
rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory); rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory);
rebind(BrowserMenuBarContribution) rebind(BrowserMenuBarContribution)
.to(ArduinoMenuContribution) .to(ArduinoMenuContribution)
.inSingletonScope(); .inSingletonScope();
}); });

View File

@@ -4,8 +4,8 @@ import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser
@injectable() @injectable()
export class ArduinoMenuContribution extends BrowserMenuBarContribution { export class ArduinoMenuContribution extends BrowserMenuBarContribution {
onStart(app: FrontendApplication): void { onStart(app: FrontendApplication): void {
const menu = this.factory.createMenuBar(); const menu = this.factory.createMenuBar();
app.shell.addWidget(menu, { area: 'top' }); app.shell.addWidget(menu, { area: 'top' });
} }
} }

View File

@@ -1,34 +1,35 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CommonFrontendContribution as TheiaCommonFrontendContribution, CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution'; import {
CommonFrontendContribution as TheiaCommonFrontendContribution,
CommonCommands,
} from '@theia/core/lib/browser/common-frontend-contribution';
@injectable() @injectable()
export class CommonFrontendContribution extends TheiaCommonFrontendContribution { export class CommonFrontendContribution extends TheiaCommonFrontendContribution {
registerMenus(registry: MenuModelRegistry): void {
registerMenus(registry: MenuModelRegistry): void { super.registerMenus(registry);
super.registerMenus(registry); for (const command of [
for (const command of [ CommonCommands.SAVE,
CommonCommands.SAVE, CommonCommands.SAVE_ALL,
CommonCommands.SAVE_ALL, CommonCommands.CUT,
CommonCommands.CUT, CommonCommands.COPY,
CommonCommands.COPY, CommonCommands.PASTE,
CommonCommands.PASTE, CommonCommands.COPY_PATH,
CommonCommands.COPY_PATH, CommonCommands.FIND,
CommonCommands.FIND, CommonCommands.REPLACE,
CommonCommands.REPLACE, CommonCommands.AUTO_SAVE,
CommonCommands.AUTO_SAVE, CommonCommands.OPEN_PREFERENCES,
CommonCommands.OPEN_PREFERENCES, CommonCommands.SELECT_ICON_THEME,
CommonCommands.SELECT_ICON_THEME, CommonCommands.SELECT_COLOR_THEME,
CommonCommands.SELECT_COLOR_THEME, CommonCommands.ABOUT_COMMAND,
CommonCommands.ABOUT_COMMAND, CommonCommands.CLOSE_TAB,
CommonCommands.CLOSE_TAB, CommonCommands.CLOSE_OTHER_TABS,
CommonCommands.CLOSE_OTHER_TABS, CommonCommands.CLOSE_ALL_TABS,
CommonCommands.CLOSE_ALL_TABS, CommonCommands.COLLAPSE_PANEL,
CommonCommands.COLLAPSE_PANEL, CommonCommands.SAVE_WITHOUT_FORMATTING, // Patched for https://github.com/eclipse-theia/theia/pull/8877
CommonCommands.SAVE_WITHOUT_FORMATTING // Patched for https://github.com/eclipse-theia/theia/pull/8877 ]) {
]) { registry.unregisterMenuAction(command);
registry.unregisterMenuAction(command);
}
} }
}
} }

View File

@@ -2,83 +2,81 @@ import { inject, injectable, postConstruct } from 'inversify';
import { Disposable } from '@theia/core/lib/common/disposable'; import { Disposable } from '@theia/core/lib/common/disposable';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar'; import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { import {
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService, FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution, ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
ConnectionStatus, ConnectionStatus,
} from '@theia/core/lib/browser/connection-status-service'; } from '@theia/core/lib/browser/connection-status-service';
import { ArduinoDaemon } from '../../../common/protocol'; import { ArduinoDaemon } from '../../../common/protocol';
import { NotificationCenter } from '../../notification-center'; import { NotificationCenter } from '../../notification-center';
@injectable() @injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService { export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(ArduinoDaemon) @inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon; protected readonly daemon: ArduinoDaemon;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected isRunning = false; protected isRunning = false;
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
this.schedulePing(); this.schedulePing();
try { try {
this.isRunning = await this.daemon.isRunning(); this.isRunning = await this.daemon.isRunning();
} catch {} } catch {}
this.notificationCenter.onDaemonStarted(() => (this.isRunning = true)); this.notificationCenter.onDaemonStarted(() => (this.isRunning = true));
this.notificationCenter.onDaemonStopped(() => (this.isRunning = false)); this.notificationCenter.onDaemonStopped(() => (this.isRunning = false));
this.wsConnectionProvider.onIncomingMessageActivity(() => { this.wsConnectionProvider.onIncomingMessageActivity(() => {
this.updateStatus(this.isRunning); this.updateStatus(this.isRunning);
this.schedulePing(); this.schedulePing();
}); });
} }
} }
@injectable() @injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution { export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
@inject(ArduinoDaemon) @inject(ArduinoDaemon)
protected readonly daemon: ArduinoDaemon; protected readonly daemon: ArduinoDaemon;
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected isRunning = false; protected isRunning = false;
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
try { try {
this.isRunning = await this.daemon.isRunning(); this.isRunning = await this.daemon.isRunning();
} catch {} } catch {}
this.notificationCenter.onDaemonStarted(() => (this.isRunning = true)); this.notificationCenter.onDaemonStarted(() => (this.isRunning = true));
this.notificationCenter.onDaemonStopped(() => (this.isRunning = false)); this.notificationCenter.onDaemonStopped(() => (this.isRunning = false));
}
protected onStateChange(state: ConnectionStatus): void {
if (!this.isRunning && state === ConnectionStatus.ONLINE) {
return;
} }
super.onStateChange(state);
}
protected onStateChange(state: ConnectionStatus): void { protected handleOffline(): void {
if (!this.isRunning && state === ConnectionStatus.ONLINE) { this.statusBar.setElement('connection-status', {
return; alignment: StatusBarAlignment.LEFT,
} text: this.isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline',
super.onStateChange(state); tooltip: this.isRunning
} ? 'Cannot connect to the backend.'
: 'Cannot connect to the CLI daemon.',
protected handleOffline(): void { priority: 5000,
this.statusBar.setElement('connection-status', { });
alignment: StatusBarAlignment.LEFT, this.toDisposeOnOnline.push(
text: this.isRunning ? 'Offline' : '$(bolt) CLI Daemon Offline', Disposable.create(() => this.statusBar.removeElement('connection-status'))
tooltip: this.isRunning );
? 'Cannot connect to the backend.' document.body.classList.add('theia-mod-offline');
: 'Cannot connect to the CLI daemon.', this.toDisposeOnOnline.push(
priority: 5000, Disposable.create(() =>
}); document.body.classList.remove('theia-mod-offline')
this.toDisposeOnOnline.push( )
Disposable.create(() => );
this.statusBar.removeElement('connection-status') }
)
);
document.body.classList.add('theia-mod-offline');
this.toDisposeOnOnline.push(
Disposable.create(() =>
document.body.classList.remove('theia-mod-offline')
)
);
}
} }

View File

@@ -8,32 +8,30 @@ import { ArduinoCommands } from '../../arduino-commands';
@injectable() @injectable()
export class FrontendApplication extends TheiaFrontendApplication { export class FrontendApplication extends TheiaFrontendApplication {
@inject(FileService) @inject(FileService)
protected readonly fileService: FileService; protected readonly fileService: FileService;
@inject(WorkspaceService) @inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService; protected readonly workspaceService: WorkspaceService;
@inject(CommandService) @inject(CommandService)
protected readonly commandService: CommandService; protected readonly commandService: CommandService;
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchesService: SketchesService; protected readonly sketchesService: SketchesService;
protected async initializeLayout(): Promise<void> { protected async initializeLayout(): Promise<void> {
await super.initializeLayout(); await super.initializeLayout();
const roots = await this.workspaceService.roots; const roots = await this.workspaceService.roots;
for (const root of roots) { for (const root of roots) {
const exists = await this.fileService.exists(root.resource); const exists = await this.fileService.exists(root.resource);
if (exists) { if (exists) {
this.sketchesService.markAsRecentlyOpened( this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu
root.resource.toString() await this.commandService.executeCommand(
); // no await, will get the notification later and rebuild the menu ArduinoCommands.OPEN_SKETCH_FILES.id,
await this.commandService.executeCommand( root.resource
ArduinoCommands.OPEN_SKETCH_FILES.id, );
root.resource }
);
}
}
} }
}
} }

View File

@@ -2,29 +2,29 @@ import { injectable } from 'inversify';
import { Command } from '@theia/core/lib/common/command'; import { Command } from '@theia/core/lib/common/command';
import { Keybinding } from '@theia/core/lib/common/keybinding'; import { Keybinding } from '@theia/core/lib/common/keybinding';
import { import {
KeybindingRegistry as TheiaKeybindingRegistry, KeybindingRegistry as TheiaKeybindingRegistry,
KeybindingScope, KeybindingScope,
} from '@theia/core/lib/browser/keybinding'; } from '@theia/core/lib/browser/keybinding';
@injectable() @injectable()
export class KeybindingRegistry extends TheiaKeybindingRegistry { export class KeybindingRegistry extends TheiaKeybindingRegistry {
// https://github.com/eclipse-theia/theia/issues/8209 // https://github.com/eclipse-theia/theia/issues/8209
unregisterKeybinding(key: string): void; unregisterKeybinding(key: string): void;
unregisterKeybinding(keybinding: Keybinding): void; unregisterKeybinding(keybinding: Keybinding): void;
unregisterKeybinding(command: Command): void; unregisterKeybinding(command: Command): void;
unregisterKeybinding(arg: string | Keybinding | Command): void { unregisterKeybinding(arg: string | Keybinding | Command): void {
const keymap = this.keymaps[KeybindingScope.DEFAULT]; const keymap = this.keymaps[KeybindingScope.DEFAULT];
const filter = Command.is(arg) const filter = Command.is(arg)
? ({ command }: Keybinding) => command === arg.id ? ({ command }: Keybinding) => command === arg.id
: ({ keybinding }: Keybinding) => : ({ keybinding }: Keybinding) =>
Keybinding.is(arg) Keybinding.is(arg)
? keybinding === arg.keybinding ? keybinding === arg.keybinding
: keybinding === arg; : keybinding === arg;
for (const binding of keymap.filter(filter)) { for (const binding of keymap.filter(filter)) {
const idx = keymap.indexOf(binding); const idx = keymap.indexOf(binding);
if (idx !== -1) { if (idx !== -1) {
keymap.splice(idx, 1); keymap.splice(idx, 1);
} }
}
} }
}
} }

View File

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

View File

@@ -9,36 +9,31 @@ import { ConfigService } from '../../../common/protocol/config-service';
@injectable() @injectable()
export class TabBarDecoratorService extends TheiaTabBarDecoratorService { export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(ILogger) @inject(ILogger)
protected readonly logger: ILogger; protected readonly logger: ILogger;
protected dataDirUri: URI | undefined; protected dataDirUri: URI | undefined;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.configService this.configService
.getConfiguration() .getConfiguration()
.then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri))) .then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
.catch((err) => .catch((err) =>
this.logger.error( this.logger.error(`Failed to determine the data directory: ${err}`)
`Failed to determine the data directory: ${err}` );
) }
);
} getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {
if (title.owner instanceof EditorWidget) {
getDecorations(title: Title<Widget>): WidgetDecoration.Data[] { const editor = title.owner.editor;
if (title.owner instanceof EditorWidget) { if (this.dataDirUri && this.dataDirUri.isEqualOrParent(editor.uri)) {
const editor = title.owner.editor; return [];
if ( }
this.dataDirUri &&
this.dataDirUri.isEqualOrParent(editor.uri)
) {
return [];
}
}
return super.getDecorations(title);
} }
return super.getDecorations(title);
}
} }

View File

@@ -2,62 +2,60 @@ import * as React from 'react';
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { LabelIcon } from '@theia/core/lib/browser/label-parser'; import { LabelIcon } from '@theia/core/lib/browser/label-parser';
import { import {
TabBarToolbar as TheiaTabBarToolbar, TabBarToolbar as TheiaTabBarToolbar,
TabBarToolbarItem, TabBarToolbarItem,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
@injectable() @injectable()
export class TabBarToolbar extends TheiaTabBarToolbar { export class TabBarToolbar extends TheiaTabBarToolbar {
/** /**
* Copied over from Theia. Added an ID to the parent of the toolbar item (`--container`). * 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. * CSS3 does not support parent selectors but we want to style the parent of the toolbar item.
*/ */
protected renderItem(item: TabBarToolbarItem): React.ReactNode { protected renderItem(item: TabBarToolbarItem): React.ReactNode {
let innerText = ''; let innerText = '';
const classNames = []; const classNames = [];
if (item.text) { if (item.text) {
for (const labelPart of this.labelParser.parse(item.text)) { for (const labelPart of this.labelParser.parse(item.text)) {
if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) { if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
const className = `fa fa-${labelPart.name}${ const className = `fa fa-${labelPart.name}${
labelPart.animation ? ' fa-' + labelPart.animation : '' labelPart.animation ? ' fa-' + labelPart.animation : ''
}`; }`;
classNames.push(...className.split(' ')); classNames.push(...className.split(' '));
} else { } else {
innerText = labelPart; 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>
);
} }
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

@@ -3,11 +3,11 @@ import { Saveable } from '@theia/core/lib/browser/saveable';
import { TabBarRenderer as TheiaTabBarRenderer } from '@theia/core/lib/browser/shell/tab-bars'; import { TabBarRenderer as TheiaTabBarRenderer } from '@theia/core/lib/browser/shell/tab-bars';
export class TabBarRenderer extends TheiaTabBarRenderer { export class TabBarRenderer extends TheiaTabBarRenderer {
createTabClass(data: TabBar.IRenderData<any>): string { createTabClass(data: TabBar.IRenderData<any>): string {
let className = super.createTabClass(data); let className = super.createTabClass(data);
if (!data.title.closable && Saveable.isDirty(data.title.owner)) { if (!data.title.closable && Saveable.isDirty(data.title.owner)) {
className += ' p-mod-closable'; className += ' p-mod-closable';
}
return className;
} }
return className;
}
} }

View File

@@ -10,138 +10,128 @@ import { SketchesService } from '../../../common/protocol';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl'; import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
import { DebugConfigurationModel } from './debug-configuration-model'; import { DebugConfigurationModel } from './debug-configuration-model';
import { import {
FileOperationError, FileOperationError,
FileOperationResult, FileOperationResult,
} from '@theia/filesystem/lib/common/files'; } from '@theia/filesystem/lib/common/files';
@injectable() @injectable()
export class DebugConfigurationManager extends TheiaDebugConfigurationManager { export class DebugConfigurationManager extends TheiaDebugConfigurationManager {
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchesService: SketchesService; protected readonly sketchesService: SketchesService;
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl; protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService; protected readonly appStateService: FrontendApplicationStateService;
protected onTempContentDidChangeEmitter = protected onTempContentDidChangeEmitter =
new Emitter<TheiaDebugConfigurationModel.JsonContent>(); new Emitter<TheiaDebugConfigurationModel.JsonContent>();
get onTempContentDidChange(): Event<TheiaDebugConfigurationModel.JsonContent> { get onTempContentDidChange(): Event<TheiaDebugConfigurationModel.JsonContent> {
return this.onTempContentDidChangeEmitter.event; return this.onTempContentDidChangeEmitter.event;
} }
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
super.init(); super.init();
this.appStateService.reachedState('ready').then(async () => { this.appStateService.reachedState('ready').then(async () => {
const tempContent = await this.getTempLaunchJsonContent(); const tempContent = await this.getTempLaunchJsonContent();
if (!tempContent) { if (!tempContent) {
// No active sketch. // No active sketch.
return; return;
} }
// Watch the file of the container folder. // Watch the file of the container folder.
this.fileService.watch( this.fileService.watch(
tempContent instanceof URI ? tempContent : tempContent.uri tempContent instanceof URI ? tempContent : tempContent.uri
); );
// Use the normalized temp folder name. We cannot compare Theia URIs here. // Use the normalized temp folder name. We cannot compare Theia URIs here.
// /var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-a0337d47f86b24a51df3dbcf2cc17925/launch.json // /var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-a0337d47f86b24a51df3dbcf2cc17925/launch.json
// /private/var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-A0337D47F86B24A51DF3DBCF2CC17925/launch.json // /private/var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-A0337D47F86B24A51DF3DBCF2CC17925/launch.json
const tempFolderName = ( const tempFolderName = (
tempContent instanceof URI tempContent instanceof URI ? tempContent : tempContent.uri.parent
? tempContent ).path.base.toLowerCase();
: tempContent.uri.parent this.fileService.onDidFilesChange((event) => {
).path.base.toLowerCase(); for (const { resource } of event.changes) {
this.fileService.onDidFilesChange((event) => { if (
for (const { resource } of event.changes) { resource.path.base === 'launch.json' &&
if ( resource.parent.path.base.toLowerCase() === tempFolderName
resource.path.base === 'launch.json' && ) {
resource.parent.path.base.toLowerCase() === this.getTempLaunchJsonContent().then((config) => {
tempFolderName if (config && !(config instanceof URI)) {
) { this.onTempContentDidChangeEmitter.fire(config);
this.getTempLaunchJsonContent().then((config) => { }
if (config && !(config instanceof URI)) {
this.onTempContentDidChangeEmitter.fire(config);
}
});
break;
}
}
}); });
this.updateModels(); break;
}); }
} }
});
this.updateModels();
});
}
protected updateModels = debounce(async () => { protected updateModels = debounce(async () => {
await this.appStateService.reachedState('ready'); await this.appStateService.reachedState('ready');
const roots = await this.workspaceService.roots; const roots = await this.workspaceService.roots;
const toDelete = new Set(this.models.keys()); const toDelete = new Set(this.models.keys());
for (const rootStat of roots) { for (const rootStat of roots) {
const key = rootStat.resource.toString(); const key = rootStat.resource.toString();
toDelete.delete(key); toDelete.delete(key);
if (!this.models.has(key)) { if (!this.models.has(key)) {
const tempContent = await this.getTempLaunchJsonContent(); const tempContent = await this.getTempLaunchJsonContent();
if (!tempContent) { if (!tempContent) {
continue; continue;
}
const configurations: DebugConfiguration[] =
tempContent instanceof URI
? []
: tempContent.configurations;
const uri =
tempContent instanceof URI ? undefined : tempContent.uri;
const model = new DebugConfigurationModel(
key,
this.preferences,
configurations,
uri,
this.onTempContentDidChange
);
model.onDidChange(() => this.updateCurrent());
model.onDispose(() => this.models.delete(key));
this.models.set(key, model);
}
}
for (const uri of toDelete) {
const model = this.models.get(uri);
if (model) {
model.dispose();
}
}
this.updateCurrent();
}, 500);
protected async getTempLaunchJsonContent(): Promise<
| (TheiaDebugConfigurationModel.JsonContent & { uri: URI })
| URI
| undefined
> {
const sketch = await this.sketchesServiceClient.currentSketch();
if (!sketch) {
return undefined;
}
const uri = await this.sketchesService.getIdeTempFolderUri(sketch);
const tempFolderUri = new URI(uri);
await this.fileService.createFolder(tempFolderUri);
try {
const uri = tempFolderUri.resolve('launch.json');
const { value } = await this.fileService.read(uri);
const configurations = DebugConfigurationModel.parse(
JSON.parse(value)
);
return { uri, configurations };
} catch (err) {
if (
err instanceof FileOperationError &&
err.fileOperationResult === FileOperationResult.FILE_NOT_FOUND
) {
return tempFolderUri;
}
console.error(
'Could not load debug configuration from IDE2 temp folder.',
err
);
throw err;
} }
const configurations: DebugConfiguration[] =
tempContent instanceof URI ? [] : tempContent.configurations;
const uri = tempContent instanceof URI ? undefined : tempContent.uri;
const model = new DebugConfigurationModel(
key,
this.preferences,
configurations,
uri,
this.onTempContentDidChange
);
model.onDidChange(() => this.updateCurrent());
model.onDispose(() => this.models.delete(key));
this.models.set(key, model);
}
} }
for (const uri of toDelete) {
const model = this.models.get(uri);
if (model) {
model.dispose();
}
}
this.updateCurrent();
}, 500);
protected async getTempLaunchJsonContent(): Promise<
(TheiaDebugConfigurationModel.JsonContent & { uri: URI }) | URI | undefined
> {
const sketch = await this.sketchesServiceClient.currentSketch();
if (!sketch) {
return undefined;
}
const uri = await this.sketchesService.getIdeTempFolderUri(sketch);
const tempFolderUri = new URI(uri);
await this.fileService.createFolder(tempFolderUri);
try {
const uri = tempFolderUri.resolve('launch.json');
const { value } = await this.fileService.read(uri);
const configurations = DebugConfigurationModel.parse(JSON.parse(value));
return { uri, configurations };
} catch (err) {
if (
err instanceof FileOperationError &&
err.fileOperationResult === FileOperationResult.FILE_NOT_FOUND
) {
return tempFolderUri;
}
console.error(
'Could not load debug configuration from IDE2 temp folder.',
err
);
throw err;
}
}
} }

View File

@@ -5,50 +5,50 @@ import { DebugConfiguration } from '@theia/debug/lib/common/debug-common';
import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model'; import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model';
export class DebugConfigurationModel extends TheiaDebugConfigurationModel { export class DebugConfigurationModel extends TheiaDebugConfigurationModel {
constructor( constructor(
readonly workspaceFolderUri: string, readonly workspaceFolderUri: string,
protected readonly preferences: PreferenceService, protected readonly preferences: PreferenceService,
protected readonly config: DebugConfiguration[], protected readonly config: DebugConfiguration[],
protected configUri: URI | undefined, protected configUri: URI | undefined,
protected readonly onConfigDidChange: Event<TheiaDebugConfigurationModel.JsonContent> protected readonly onConfigDidChange: Event<TheiaDebugConfigurationModel.JsonContent>
) { ) {
super(workspaceFolderUri, preferences); super(workspaceFolderUri, preferences);
this.toDispose.push( this.toDispose.push(
onConfigDidChange((content) => { onConfigDidChange((content) => {
const { uri, configurations } = content; const { uri, configurations } = content;
this.configUri = uri; this.configUri = uri;
this.config.length = 0; this.config.length = 0;
this.config.push(...configurations); this.config.push(...configurations);
this.reconcile();
})
);
this.reconcile(); this.reconcile();
} })
);
this.reconcile();
}
protected parseConfigurations(): TheiaDebugConfigurationModel.JsonContent { protected parseConfigurations(): TheiaDebugConfigurationModel.JsonContent {
return { return {
uri: this.configUri, uri: this.configUri,
configurations: this.config, configurations: this.config,
}; };
} }
} }
export namespace DebugConfigurationModel { export namespace DebugConfigurationModel {
export function parse(launchConfig: any): DebugConfiguration[] { export function parse(launchConfig: any): DebugConfiguration[] {
const configurations: DebugConfiguration[] = []; const configurations: DebugConfiguration[] = [];
if ( if (
launchConfig && launchConfig &&
typeof launchConfig === 'object' && typeof launchConfig === 'object' &&
'configurations' in launchConfig 'configurations' in launchConfig
) { ) {
if (Array.isArray(launchConfig.configurations)) { if (Array.isArray(launchConfig.configurations)) {
for (const configuration of launchConfig.configurations) { for (const configuration of launchConfig.configurations) {
if (DebugConfiguration.is(configuration)) { if (DebugConfiguration.is(configuration)) {
configurations.push(configuration); configurations.push(configuration);
} }
}
}
} }
return configurations; }
} }
return configurations;
}
} }

View File

@@ -1,15 +1,15 @@
import debounce from 'p-debounce'; import debounce from 'p-debounce';
import { import {
inject, inject,
injectable, injectable,
postConstruct, postConstruct,
interfaces, interfaces,
Container, Container,
} from 'inversify'; } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { MonacoConfigurationService } from '@theia/monaco/lib/browser/monaco-frontend-module'; import { MonacoConfigurationService } from '@theia/monaco/lib/browser/monaco-frontend-module';
import { INLINE_VALUE_DECORATION_KEY } from '@theia/debug/lib/browser/editor//debug-inline-value-decorator'; import { INLINE_VALUE_DECORATION_KEY } from '@theia/debug/lib/browser/editor//debug-inline-value-decorator';
@@ -22,95 +22,93 @@ import { createDebugHoverWidgetContainer } from './debug-hover-widget';
// TODO: Remove after https://github.com/eclipse-theia/theia/pull/9256/ // TODO: Remove after https://github.com/eclipse-theia/theia/pull/9256/
@injectable() @injectable()
export class DebugEditorModel extends TheiaDebugEditorModel { export class DebugEditorModel extends TheiaDebugEditorModel {
static createContainer( static createContainer(
parent: interfaces.Container, parent: interfaces.Container,
editor: DebugEditor editor: DebugEditor
): Container { ): Container {
const child = createDebugHoverWidgetContainer(parent, editor); const child = createDebugHoverWidgetContainer(parent, editor);
child.bind(DebugEditorModel).toSelf(); child.bind(DebugEditorModel).toSelf();
child.bind(DebugBreakpointWidget).toSelf(); child.bind(DebugBreakpointWidget).toSelf();
child.bind(DebugExceptionWidget).toSelf(); child.bind(DebugExceptionWidget).toSelf();
return child; return child;
} }
static createModel( static createModel(
parent: interfaces.Container, parent: interfaces.Container,
editor: DebugEditor editor: DebugEditor
): DebugEditorModel { ): DebugEditorModel {
return DebugEditorModel.createContainer(parent, editor).get( return DebugEditorModel.createContainer(parent, editor).get(
DebugEditorModel DebugEditorModel
); );
} }
@inject(MonacoConfigurationService) @inject(MonacoConfigurationService)
readonly configurationService: monaco.services.IConfigurationService; readonly configurationService: monaco.services.IConfigurationService;
protected readonly toDisposeOnRenderFrames = new DisposableCollection(); protected readonly toDisposeOnRenderFrames = new DisposableCollection();
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.toDispose.push(this.toDisposeOnRenderFrames); this.toDispose.push(this.toDisposeOnRenderFrames);
super.init(); super.init();
} }
protected async updateEditorHover(): Promise<void> { protected async updateEditorHover(): Promise<void> {
if (this.isCurrentEditorFrame(this.uri)) { if (this.isCurrentEditorFrame(this.uri)) {
const codeEditor = this.editor.getControl(); const codeEditor = this.editor.getControl();
codeEditor.updateOptions({ hover: { enabled: false } }); codeEditor.updateOptions({ hover: { enabled: false } });
this.toDisposeOnRenderFrames.push( this.toDisposeOnRenderFrames.push(
Disposable.create(() => { Disposable.create(() => {
const model = codeEditor.getModel()!; const model = codeEditor.getModel()!;
const overrides = { const overrides = {
resource: model.uri, resource: model.uri,
overrideIdentifier: ( overrideIdentifier: (model as any).getLanguageIdentifier().language,
model as any };
).getLanguageIdentifier().language, const { enabled, delay, sticky } =
}; this.configurationService._configuration.getValue(
const { enabled, delay, sticky } = 'editor.hover',
this.configurationService._configuration.getValue( overrides,
'editor.hover', undefined
overrides,
undefined
);
codeEditor.updateOptions({
hover: {
enabled,
delay,
sticky,
},
});
})
); );
} codeEditor.updateOptions({
hover: {
enabled,
delay,
sticky,
},
});
})
);
} }
}
private isCurrentEditorFrame(uri: URI): boolean { private isCurrentEditorFrame(uri: URI): boolean {
return ( return (
this.sessions.currentFrame?.source?.uri.toString() === this.sessions.currentFrame?.source?.uri.toString() === uri.toString()
uri.toString() );
); }
protected readonly renderFrames = debounce(async () => {
if (this.toDispose.disposed) {
return;
} }
this.toDisposeOnRenderFrames.dispose();
protected readonly renderFrames = debounce(async () => { this.toggleExceptionWidget();
if (this.toDispose.disposed) { const [newFrameDecorations, inlineValueDecorations] = await Promise.all([
return; this.createFrameDecorations(),
} this.createInlineValueDecorations(),
this.toDisposeOnRenderFrames.dispose(); ]);
const codeEditor = this.editor.getControl();
this.toggleExceptionWidget(); codeEditor.removeDecorations(INLINE_VALUE_DECORATION_KEY);
const [newFrameDecorations, inlineValueDecorations] = await Promise.all( codeEditor.setDecorations(
[this.createFrameDecorations(), this.createInlineValueDecorations()] INLINE_VALUE_DECORATION_KEY,
); inlineValueDecorations
const codeEditor = this.editor.getControl(); );
codeEditor.removeDecorations(INLINE_VALUE_DECORATION_KEY); this.frameDecorations = this.deltaDecorations(
codeEditor.setDecorations( this.frameDecorations,
INLINE_VALUE_DECORATION_KEY, newFrameDecorations
inlineValueDecorations );
); this.updateEditorHover();
this.frameDecorations = this.deltaDecorations( }, 100);
this.frameDecorations,
newFrameDecorations
);
this.updateEditorHover();
}, 100);
} }

View File

@@ -1,20 +1,20 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { import {
DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution, DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution,
DebugMenus, DebugMenus,
} from '@theia/debug/lib/browser/debug-frontend-application-contribution'; } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { unregisterSubmenu } from '../../menu/arduino-menus'; import { unregisterSubmenu } from '../../menu/arduino-menus';
@injectable() @injectable()
export class DebugFrontendApplicationContribution extends TheiaDebugFrontendApplicationContribution { export class DebugFrontendApplicationContribution extends TheiaDebugFrontendApplicationContribution {
constructor() { constructor() {
super(); super();
this.options.defaultWidgetOptions.rank = 4; this.options.defaultWidgetOptions.rank = 4;
} }
registerMenus(registry: MenuModelRegistry): void { registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry); super.registerMenus(registry);
unregisterSubmenu(DebugMenus.DEBUG, registry); unregisterSubmenu(DebugMenus.DEBUG, registry);
} }
} }

View File

@@ -1,21 +1,21 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { import {
ExpressionItem, ExpressionItem,
DebugVariable, DebugVariable,
} from '@theia/debug/lib/browser/console/debug-console-items'; } from '@theia/debug/lib/browser/console/debug-console-items';
import { DebugHoverSource as TheiaDebugHoverSource } from '@theia/debug/lib/browser/editor/debug-hover-source'; import { DebugHoverSource as TheiaDebugHoverSource } from '@theia/debug/lib/browser/editor/debug-hover-source';
// TODO: remove after https://github.com/eclipse-theia/theia/pull/9256/. // TODO: remove after https://github.com/eclipse-theia/theia/pull/9256/.
@injectable() @injectable()
export class DebugHoverSource extends TheiaDebugHoverSource { export class DebugHoverSource extends TheiaDebugHoverSource {
async evaluate2( async evaluate2(
expression: string expression: string
): Promise<ExpressionItem | DebugVariable | undefined> { ): Promise<ExpressionItem | DebugVariable | undefined> {
const evaluated = await this.doEvaluate(expression); const evaluated = await this.doEvaluate(expression);
const elements = evaluated && (await evaluated.getElements()); const elements = evaluated && (await evaluated.getElements());
this._expression = evaluated; this._expression = evaluated;
this.elements = elements ? [...elements] : []; this.elements = elements ? [...elements] : [];
this.fireDidChange(); this.fireDidChange();
return evaluated; return evaluated;
} }
} }

View File

@@ -7,116 +7,113 @@ import { DebugVariable } from '@theia/debug/lib/browser/console/debug-console-it
import { DebugExpressionProvider } from '@theia/debug/lib/browser/editor/debug-expression-provider'; import { DebugExpressionProvider } from '@theia/debug/lib/browser/editor/debug-expression-provider';
import { DebugHoverSource as TheiaDebugHoverSource } from '@theia/debug/lib/browser/editor/debug-hover-source'; import { DebugHoverSource as TheiaDebugHoverSource } from '@theia/debug/lib/browser/editor/debug-hover-source';
import { import {
DebugHoverWidget as TheiaDebugHoverWidget, DebugHoverWidget as TheiaDebugHoverWidget,
ShowDebugHoverOptions, ShowDebugHoverOptions,
} from '@theia/debug/lib/browser/editor/debug-hover-widget'; } from '@theia/debug/lib/browser/editor/debug-hover-widget';
import { DebugHoverSource } from './debug-hover-source'; import { DebugHoverSource } from './debug-hover-source';
export function createDebugHoverWidgetContainer( export function createDebugHoverWidgetContainer(
parent: interfaces.Container, parent: interfaces.Container,
editor: DebugEditor editor: DebugEditor
): Container { ): Container {
const child = SourceTreeWidget.createContainer(parent, { const child = SourceTreeWidget.createContainer(parent, {
virtualized: false, virtualized: false,
}); });
child.bind(DebugEditor).toConstantValue(editor); child.bind(DebugEditor).toConstantValue(editor);
child.bind(TheiaDebugHoverSource).toSelf(); child.bind(TheiaDebugHoverSource).toSelf();
child.bind(DebugHoverSource).toSelf(); child.bind(DebugHoverSource).toSelf();
child.rebind(TheiaDebugHoverSource).to(DebugHoverSource); child.rebind(TheiaDebugHoverSource).to(DebugHoverSource);
child.unbind(SourceTreeWidget); child.unbind(SourceTreeWidget);
child.bind(DebugExpressionProvider).toSelf(); child.bind(DebugExpressionProvider).toSelf();
child.bind(TheiaDebugHoverWidget).toSelf(); child.bind(TheiaDebugHoverWidget).toSelf();
child.bind(DebugHoverWidget).toSelf(); child.bind(DebugHoverWidget).toSelf();
child.rebind(TheiaDebugHoverWidget).to(DebugHoverWidget); child.rebind(TheiaDebugHoverWidget).to(DebugHoverWidget);
return child; return child;
} }
// TODO: remove patch after https://github.com/eclipse-theia/theia/pull/9256/ // TODO: remove patch after https://github.com/eclipse-theia/theia/pull/9256/
@injectable() @injectable()
export class DebugHoverWidget extends TheiaDebugHoverWidget { export class DebugHoverWidget extends TheiaDebugHoverWidget {
protected async doShow( protected async doShow(
options: ShowDebugHoverOptions | undefined = this.options options: ShowDebugHoverOptions | undefined = this.options
): Promise<void> { ): Promise<void> {
if (!this.isEditorFrame()) { if (!this.isEditorFrame()) {
this.hide(); this.hide();
return; return;
}
if (!options) {
this.hide();
return;
}
if (
this.options &&
this.options.selection.equalsRange(options.selection)
) {
return;
}
if (!this.isAttached) {
Widget.attach(this, this.contentNode);
}
this.options = options;
const matchingExpression = this.expressionProvider.get(
this.editor.getControl().getModel()!,
options.selection
);
if (!matchingExpression) {
this.hide();
return;
}
const toFocus = new DisposableCollection();
if (this.options.focus === true) {
toFocus.push(
this.model.onNodeRefreshed(() => {
toFocus.dispose();
this.activate();
})
);
}
const expression = await (
this.hoverSource as DebugHoverSource
).evaluate2(matchingExpression);
if (!expression || !expression.value) {
toFocus.dispose();
this.hide();
return;
}
this.contentNode.hidden = false;
['number', 'boolean', 'string'].forEach((token) =>
this.titleNode.classList.remove(token)
);
this.domNode.classList.remove('complex-value');
if (expression.hasElements) {
this.domNode.classList.add('complex-value');
} else {
this.contentNode.hidden = true;
if (
expression.type === 'number' ||
expression.type === 'boolean' ||
expression.type === 'string'
) {
this.titleNode.classList.add(expression.type);
} else if (!isNaN(+expression.value)) {
this.titleNode.classList.add('number');
} else if (DebugVariable.booleanRegex.test(expression.value)) {
this.titleNode.classList.add('boolean');
} else if (DebugVariable.stringRegex.test(expression.value)) {
this.titleNode.classList.add('string');
}
}
// super.show(); // Here we cannot call `super.show()` but have to call `show` on the `Widget` prototype.
Widget.prototype.show.call(this);
await new Promise<void>((resolve) => {
setTimeout(
() =>
window.requestAnimationFrame(() => {
this.editor.getControl().layoutContentWidget(this);
resolve();
}),
0
);
});
} }
if (!options) {
this.hide();
return;
}
if (this.options && this.options.selection.equalsRange(options.selection)) {
return;
}
if (!this.isAttached) {
Widget.attach(this, this.contentNode);
}
this.options = options;
const matchingExpression = this.expressionProvider.get(
this.editor.getControl().getModel()!,
options.selection
);
if (!matchingExpression) {
this.hide();
return;
}
const toFocus = new DisposableCollection();
if (this.options.focus === true) {
toFocus.push(
this.model.onNodeRefreshed(() => {
toFocus.dispose();
this.activate();
})
);
}
const expression = await (this.hoverSource as DebugHoverSource).evaluate2(
matchingExpression
);
if (!expression || !expression.value) {
toFocus.dispose();
this.hide();
return;
}
this.contentNode.hidden = false;
['number', 'boolean', 'string'].forEach((token) =>
this.titleNode.classList.remove(token)
);
this.domNode.classList.remove('complex-value');
if (expression.hasElements) {
this.domNode.classList.add('complex-value');
} else {
this.contentNode.hidden = true;
if (
expression.type === 'number' ||
expression.type === 'boolean' ||
expression.type === 'string'
) {
this.titleNode.classList.add(expression.type);
} else if (!isNaN(+expression.value)) {
this.titleNode.classList.add('number');
} else if (DebugVariable.booleanRegex.test(expression.value)) {
this.titleNode.classList.add('boolean');
} else if (DebugVariable.stringRegex.test(expression.value)) {
this.titleNode.classList.add('string');
}
}
// super.show(); // Here we cannot call `super.show()` but have to call `show` on the `Widget` prototype.
Widget.prototype.show.call(this);
await new Promise<void>((resolve) => {
setTimeout(
() =>
window.requestAnimationFrame(() => {
this.editor.getControl().layoutContentWidget(this);
resolve();
}),
0
);
});
}
} }

View File

@@ -6,54 +6,48 @@ import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/li
@injectable() @injectable()
export class DebugSessionManager extends TheiaDebugSessionManager { export class DebugSessionManager extends TheiaDebugSessionManager {
async start( async start(options: DebugSessionOptions): Promise<DebugSession | undefined> {
options: DebugSessionOptions return this.progressService.withProgress('Start...', 'debug', async () => {
): Promise<DebugSession | undefined> { try {
return this.progressService.withProgress( // Only save when dirty. To avoid saving temporary sketches.
'Start...', // This is a quick fix for not saving the editor when there are no dirty editors.
'debug', // // https://github.com/bcmi-labs/arduino-editor/pull/172#issuecomment-741831888
async () => { if (this.shell.canSaveAll()) {
try { await this.shell.saveAll();
// Only save when dirty. To avoid saving temporary sketches. }
// This is a quick fix for not saving the editor when there are no dirty editors. await this.fireWillStartDebugSession();
// // https://github.com/bcmi-labs/arduino-editor/pull/172#issuecomment-741831888 const resolved = await this.resolveConfiguration(options);
if (this.shell.canSaveAll()) {
await this.shell.saveAll();
}
await this.fireWillStartDebugSession();
const resolved = await this.resolveConfiguration(options);
// preLaunchTask isn't run in case of auto restart as well as postDebugTask // preLaunchTask isn't run in case of auto restart as well as postDebugTask
if (!options.configuration.__restart) { if (!options.configuration.__restart) {
const taskRun = await this.runTask( const taskRun = await this.runTask(
options.workspaceFolderUri, options.workspaceFolderUri,
resolved.configuration.preLaunchTask, resolved.configuration.preLaunchTask,
true true
); );
if (!taskRun) { if (!taskRun) {
return undefined; return undefined;
} }
} }
const sessionId = await this.debug.createDebugSession( const sessionId = await this.debug.createDebugSession(
resolved.configuration resolved.configuration
);
return this.doStart(sessionId, resolved);
} catch (e) {
if (DebugError.NotFound.is(e)) {
this.messageService.error(
`The debug session type "${e.data.type}" is not supported.`
);
return undefined;
}
this.messageService.error(
'There was an error starting the debug session, check the logs for more details.'
);
console.error('Error starting the debug session', e);
throw e;
}
}
); );
} return this.doStart(sessionId, resolved);
} catch (e) {
if (DebugError.NotFound.is(e)) {
this.messageService.error(
`The debug session type "${e.data.type}" is not supported.`
);
return undefined;
}
this.messageService.error(
'There was an error starting the debug session, check the logs for more details.'
);
console.error('Error starting the debug session', e);
throw e;
}
});
}
} }

View File

@@ -3,21 +3,19 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th
@injectable() @injectable()
export class EditorCommandContribution extends TheiaEditorCommandContribution { export class EditorCommandContribution extends TheiaEditorCommandContribution {
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
// Workaround for https://github.com/eclipse-theia/theia/issues/8722. // Workaround for https://github.com/eclipse-theia/theia/issues/8722.
this.editorPreferences.onPreferenceChanged( this.editorPreferences.onPreferenceChanged(
({ preferenceName, newValue, oldValue }) => { ({ preferenceName, newValue, oldValue }) => {
if (preferenceName === 'editor.autoSave') { if (preferenceName === 'editor.autoSave') {
const autoSaveWasOnBeforeChange = const autoSaveWasOnBeforeChange = !oldValue || oldValue === 'on';
!oldValue || oldValue === 'on'; const autoSaveIsOnAfterChange = !newValue || newValue === 'on';
const autoSaveIsOnAfterChange = if (!autoSaveWasOnBeforeChange && autoSaveIsOnAfterChange) {
!newValue || newValue === 'on'; this.shell.saveAll();
if (!autoSaveWasOnBeforeChange && autoSaveIsOnAfterChange) { }
this.shell.saveAll(); }
} }
} );
} }
);
}
} }

View File

@@ -5,18 +5,18 @@ import { StatusBarAlignment } from '@theia/core/lib/browser';
@injectable() @injectable()
export class EditorContribution extends TheiaEditorContribution { export class EditorContribution extends TheiaEditorContribution {
protected updateLanguageStatus(editor: TextEditor | undefined): void {} protected updateLanguageStatus(editor: TextEditor | undefined): void {}
protected setCursorPositionStatus(editor: TextEditor | undefined): void { protected setCursorPositionStatus(editor: TextEditor | undefined): void {
if (!editor) { if (!editor) {
this.statusBar.removeElement('editor-status-cursor-position'); this.statusBar.removeElement('editor-status-cursor-position');
return; return;
}
const { cursor } = editor;
this.statusBar.setElement('editor-status-cursor-position', {
text: `${cursor.line + 1}`,
alignment: StatusBarAlignment.LEFT,
priority: 100,
});
} }
const { cursor } = editor;
this.statusBar.setElement('editor-status-cursor-position', {
text: `${cursor.line + 1}`,
alignment: StatusBarAlignment.LEFT,
priority: 100,
});
}
} }

View File

@@ -8,33 +8,31 @@ import { SketchesService, Sketch } from '../../../common/protocol';
@injectable() @injectable()
export class EditorWidgetFactory extends TheiaEditorWidgetFactory { export class EditorWidgetFactory extends TheiaEditorWidgetFactory {
@inject(SketchesService) @inject(SketchesService)
protected readonly sketchesService: SketchesService; protected readonly sketchesService: SketchesService;
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl; protected readonly sketchesServiceClient: SketchesServiceClientImpl;
@inject(LabelProvider) @inject(LabelProvider)
protected readonly labelProvider: LabelProvider; protected readonly labelProvider: LabelProvider;
protected async createEditor(uri: URI): Promise<EditorWidget> { protected async createEditor(uri: URI): Promise<EditorWidget> {
const widget = await super.createEditor(uri); const widget = await super.createEditor(uri);
return this.maybeUpdateCaption(widget); return this.maybeUpdateCaption(widget);
} }
protected async maybeUpdateCaption( protected async maybeUpdateCaption(
widget: EditorWidget widget: EditorWidget
): Promise<EditorWidget> { ): Promise<EditorWidget> {
const sketch = await this.sketchesServiceClient.currentSketch(); const sketch = await this.sketchesServiceClient.currentSketch();
const { uri } = widget.editor; const { uri } = widget.editor;
if (sketch && Sketch.isInSketch(uri, sketch)) { if (sketch && Sketch.isInSketch(uri, sketch)) {
const isTemp = await this.sketchesService.isTemp(sketch); const isTemp = await this.sketchesService.isTemp(sketch);
if (isTemp) { if (isTemp) {
widget.title.caption = `Unsaved ${this.labelProvider.getName( widget.title.caption = `Unsaved ${this.labelProvider.getName(uri)}`;
uri }
)}`;
}
}
return widget;
} }
return widget;
}
} }

View File

@@ -1,18 +1,18 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core'; import { MenuModelRegistry } from '@theia/core';
import { import {
KeymapsFrontendContribution as TheiaKeymapsFrontendContribution, KeymapsFrontendContribution as TheiaKeymapsFrontendContribution,
KeymapsCommands, KeymapsCommands,
} from '@theia/keymaps/lib/browser/keymaps-frontend-contribution'; } from '@theia/keymaps/lib/browser/keymaps-frontend-contribution';
import { ArduinoMenus } from '../../menu/arduino-menus'; import { ArduinoMenus } from '../../menu/arduino-menus';
@injectable() @injectable()
export class KeymapsFrontendContribution extends TheiaKeymapsFrontendContribution { export class KeymapsFrontendContribution extends TheiaKeymapsFrontendContribution {
registerMenus(menus: MenuModelRegistry): void { registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(ArduinoMenus.FILE__ADVANCED_SUBMENU, { menus.registerMenuAction(ArduinoMenus.FILE__ADVANCED_SUBMENU, {
commandId: KeymapsCommands.OPEN_KEYMAPS.id, commandId: KeymapsCommands.OPEN_KEYMAPS.id,
label: 'Keyboard Shortcuts', label: 'Keyboard Shortcuts',
order: '1', order: '1',
}); });
} }
} }

View File

@@ -6,20 +6,20 @@ import { ProblemContribution as TheiaProblemContribution } from '@theia/markers/
@injectable() @injectable()
export class ProblemContribution extends TheiaProblemContribution { export class ProblemContribution extends TheiaProblemContribution {
async initializeLayout(app: FrontendApplication): Promise<void> { async initializeLayout(app: FrontendApplication): Promise<void> {
// NOOP // NOOP
} }
protected setStatusBarElement(problemStat: ProblemStat): void { protected setStatusBarElement(problemStat: ProblemStat): void {
// NOOP // NOOP
} }
registerKeybindings(keybindings: KeybindingRegistry): void { registerKeybindings(keybindings: KeybindingRegistry): void {
if (this.toggleCommand) { if (this.toggleCommand) {
keybindings.registerKeybinding({ keybindings.registerKeybinding({
command: this.toggleCommand.id, command: this.toggleCommand.id,
keybinding: 'ctrlcmd+alt+shift+m', keybinding: 'ctrlcmd+alt+shift+m',
}); });
}
} }
}
} }

View File

@@ -8,35 +8,33 @@ import { ConfigService } from '../../../common/protocol/config-service';
@injectable() @injectable()
export class ProblemManager extends TheiaProblemManager { export class ProblemManager extends TheiaProblemManager {
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(ILogger) @inject(ILogger)
protected readonly logger: ILogger; protected readonly logger: ILogger;
protected dataDirUri: URI | undefined; protected dataDirUri: URI | undefined;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
super.init(); super.init();
this.configService this.configService
.getConfiguration() .getConfiguration()
.then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri))) .then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
.catch((err) => .catch((err) =>
this.logger.error( this.logger.error(`Failed to determine the data directory: ${err}`)
`Failed to determine the data directory: ${err}` );
) }
);
} setMarkers(
uri: URI,
setMarkers( owner: string,
uri: URI, data: Diagnostic[]
owner: string, ): Marker<Diagnostic>[] {
data: Diagnostic[] if (this.dataDirUri && this.dataDirUri.isEqualOrParent(uri)) {
): Marker<Diagnostic>[] { return [];
if (this.dataDirUri && this.dataDirUri.isEqualOrParent(uri)) {
return [];
}
return super.setMarkers(uri, owner, data);
} }
return super.setMarkers(uri, owner, data);
}
} }

View File

@@ -5,46 +5,44 @@ import { NotificationCenterComponent as TheiaNotificationCenterComponent } from
const PerfectScrollbar = require('react-perfect-scrollbar'); const PerfectScrollbar = require('react-perfect-scrollbar');
export class NotificationCenterComponent extends TheiaNotificationCenterComponent { export class NotificationCenterComponent extends TheiaNotificationCenterComponent {
render(): React.ReactNode { render(): React.ReactNode {
const empty = this.state.notifications.length === 0; const empty = this.state.notifications.length === 0;
const title = empty ? 'NO NEW NOTIFICATIONS' : 'NOTIFICATIONS'; const title = empty ? 'NO NEW NOTIFICATIONS' : 'NOTIFICATIONS';
return ( return (
<div <div
className={`theia-notifications-container theia-notification-center ${ className={`theia-notifications-container theia-notification-center ${
this.state.visibilityState === 'center' ? 'open' : 'closed' this.state.visibilityState === 'center' ? 'open' : 'closed'
}`} }`}
> >
<div className="theia-notification-center-header"> <div className="theia-notification-center-header">
<div className="theia-notification-center-header-title"> <div className="theia-notification-center-header-title">{title}</div>
{title} <div className="theia-notification-center-header-actions">
</div> <ul className="theia-notification-actions">
<div className="theia-notification-center-header-actions"> <li
<ul className="theia-notification-actions"> className="collapse"
<li title="Hide Notification Center"
className="collapse" onClick={this.onHide}
title="Hide Notification Center" />
onClick={this.onHide} <li
/> className="clear-all"
<li title="Clear All"
className="clear-all" onClick={this.onClearAll}
title="Clear All" />
onClick={this.onClearAll} </ul>
/> </div>
</ul> </div>
</div> <PerfectScrollbar className="theia-notification-list-scroll-container">
</div> <div className="theia-notification-list">
<PerfectScrollbar className="theia-notification-list-scroll-container"> {this.state.notifications.map((notification) => (
<div className="theia-notification-list"> <NotificationComponent
{this.state.notifications.map((notification) => ( key={notification.messageId}
<NotificationComponent notification={notification}
key={notification.messageId} manager={this.props.manager}
notification={notification} />
manager={this.props.manager} ))}
/> </div>
))} </PerfectScrollbar>
</div> </div>
</PerfectScrollbar> );
</div> }
);
}
} }

View File

@@ -2,105 +2,96 @@ import * as React from 'react';
import { NotificationComponent as TheiaNotificationComponent } from '@theia/messages/lib/browser/notification-component'; import { NotificationComponent as TheiaNotificationComponent } from '@theia/messages/lib/browser/notification-component';
export class NotificationComponent extends TheiaNotificationComponent { export class NotificationComponent extends TheiaNotificationComponent {
render(): React.ReactNode { render(): React.ReactNode {
const { const { messageId, message, type, collapsed, expandable, source, actions } =
messageId, this.props.notification;
message, return (
type, <div key={messageId} className="theia-notification-list-item">
collapsed, <div
expandable, className={`theia-notification-list-item-content ${
source, collapsed ? 'collapsed' : ''
actions, }`}
} = this.props.notification; >
return ( <div className="theia-notification-list-item-content-main">
<div key={messageId} className="theia-notification-list-item"> <div
<div className={`theia-notification-icon theia-notification-icon-${type}`}
className={`theia-notification-list-item-content ${ />
collapsed ? 'collapsed' : '' <div className="theia-notification-message">
}`} <span
> dangerouslySetInnerHTML={{ __html: message }}
<div className="theia-notification-list-item-content-main"> onClick={this.onMessageClick}
<div />
className={`theia-notification-icon theia-notification-icon-${type}`}
/>
<div className="theia-notification-message">
<span
dangerouslySetInnerHTML={{ __html: message }}
onClick={this.onMessageClick}
/>
</div>
<ul className="theia-notification-actions">
{expandable && (
<li
className={
collapsed ? 'expand' : 'collapse'
}
title={collapsed ? 'Expand' : 'Collapse'}
data-message-id={messageId}
onClick={this.onToggleExpansion}
/>
)}
{!this.isProgress && (
<li
className="clear"
title="Clear"
data-message-id={messageId}
onClick={this.onClear}
/>
)}
</ul>
</div>
{(source || !!actions.length) && (
<div className="theia-notification-list-item-content-bottom">
<div className="theia-notification-source">
{source && <span>{source}</span>}
</div>
<div className="theia-notification-buttons">
{actions &&
actions.map((action, index) => (
<button
key={messageId + `-action-${index}`}
className="theia-button"
data-message-id={messageId}
data-action={action}
onClick={this.onAction}
>
{action}
</button>
))}
</div>
</div>
)}
</div>
{this.renderProgressBar()}
</div> </div>
); <ul className="theia-notification-actions">
} {expandable && (
<li
private renderProgressBar(): React.ReactNode { className={collapsed ? 'expand' : 'collapse'}
if (!this.isProgress) { title={collapsed ? 'Expand' : 'Collapse'}
return undefined; data-message-id={messageId}
} onClick={this.onToggleExpansion}
if (!Number.isNaN(this.props.notification.progress)) { />
return ( )}
<div className="theia-notification-item-progress"> {!this.isProgress && (
<div <li
className="theia-notification-item-progressbar" className="clear"
style={{ title="Clear"
width: `${this.props.notification.progress}%`, data-message-id={messageId}
}} onClick={this.onClear}
/> />
</div> )}
); </ul>
} </div>
return ( {(source || !!actions.length) && (
<div className="theia-progress-bar-container"> <div className="theia-notification-list-item-content-bottom">
<div className="theia-progress-bar" /> <div className="theia-notification-source">
{source && <span>{source}</span>}
</div>
<div className="theia-notification-buttons">
{actions &&
actions.map((action, index) => (
<button
key={messageId + `-action-${index}`}
className="theia-button"
data-message-id={messageId}
data-action={action}
onClick={this.onAction}
>
{action}
</button>
))}
</div>
</div> </div>
); )}
} </div>
{this.renderProgressBar()}
</div>
);
}
private get isProgress(): boolean { private renderProgressBar(): React.ReactNode {
return typeof this.props.notification.progress === 'number'; if (!this.isProgress) {
return undefined;
} }
if (!Number.isNaN(this.props.notification.progress)) {
return (
<div className="theia-notification-item-progress">
<div
className="theia-notification-item-progressbar"
style={{
width: `${this.props.notification.progress}%`,
}}
/>
</div>
);
}
return (
<div className="theia-progress-bar-container">
<div className="theia-progress-bar" />
</div>
);
}
private get isProgress(): boolean {
return typeof this.props.notification.progress === 'number';
}
} }

View File

@@ -3,23 +3,23 @@ import { NotificationComponent } from './notification-component';
import { NotificationToastsComponent as TheiaNotificationToastsComponent } from '@theia/messages/lib/browser/notification-toasts-component'; import { NotificationToastsComponent as TheiaNotificationToastsComponent } from '@theia/messages/lib/browser/notification-toasts-component';
export class NotificationToastsComponent extends TheiaNotificationToastsComponent { export class NotificationToastsComponent extends TheiaNotificationToastsComponent {
render(): React.ReactNode { render(): React.ReactNode {
return ( return (
<div <div
className={`theia-notifications-container theia-notification-toasts ${ className={`theia-notifications-container theia-notification-toasts ${
this.state.visibilityState === 'toasts' ? 'open' : 'closed' this.state.visibilityState === 'toasts' ? 'open' : 'closed'
}`} }`}
> >
<div className="theia-notification-list"> <div className="theia-notification-list">
{this.state.toasts.map((notification) => ( {this.state.toasts.map((notification) => (
<NotificationComponent <NotificationComponent
key={notification.messageId} key={notification.messageId}
notification={notification} notification={notification}
manager={this.props.manager} manager={this.props.manager}
/> />
))} ))}
</div> </div>
</div> </div>
); );
} }
} }

View File

@@ -1,50 +1,46 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { import {
ProgressMessage, ProgressMessage,
ProgressUpdate, ProgressUpdate,
} from '@theia/core/lib/common/message-service-protocol'; } from '@theia/core/lib/common/message-service-protocol';
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager'; import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
@injectable() @injectable()
export class NotificationManager extends TheiaNotificationManager { export class NotificationManager extends TheiaNotificationManager {
async reportProgress( async reportProgress(
messageId: string, messageId: string,
update: ProgressUpdate, update: ProgressUpdate,
originalMessage: ProgressMessage, originalMessage: ProgressMessage,
cancellationToken: CancellationToken cancellationToken: CancellationToken
): Promise<void> { ): Promise<void> {
const notification = this.find(messageId); const notification = this.find(messageId);
if (!notification) { if (!notification) {
return; return;
}
if (cancellationToken.isCancellationRequested) {
this.clear(messageId);
} else {
notification.message =
originalMessage.text && update.message
? `${originalMessage.text}: ${update.message}`
: originalMessage.text ||
update?.message ||
notification.message;
// Unlike in Theia, we allow resetting the progress monitor to NaN to enforce unknown progress.
const candidate = this.toPlainProgress(update);
notification.progress =
typeof candidate === 'number'
? candidate
: notification.progress;
}
this.fireUpdatedEvent();
} }
if (cancellationToken.isCancellationRequested) {
this.clear(messageId);
} else {
notification.message =
originalMessage.text && update.message
? `${originalMessage.text}: ${update.message}`
: originalMessage.text || update?.message || notification.message;
protected toPlainProgress(update: ProgressUpdate): number | undefined { // Unlike in Theia, we allow resetting the progress monitor to NaN to enforce unknown progress.
if (!update.work) { const candidate = this.toPlainProgress(update);
return undefined; notification.progress =
} typeof candidate === 'number' ? candidate : notification.progress;
if (Number.isNaN(update.work.done) || Number.isNaN(update.work.total)) {
return Number.NaN; // This should trigger the unknown monitor.
}
return Math.min((update.work.done / update.work.total) * 100, 100);
} }
this.fireUpdatedEvent();
}
protected toPlainProgress(update: ProgressUpdate): number | undefined {
if (!update.work) {
return undefined;
}
if (Number.isNaN(update.work.done) || Number.isNaN(update.work.total)) {
return Number.NaN; // This should trigger the unknown monitor.
}
return Math.min((update.work.done / update.work.total) * 100, 100);
}
} }

View File

@@ -7,16 +7,16 @@ import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/mess
@injectable() @injectable()
export class NotificationsRenderer extends TheiaNotificationsRenderer { export class NotificationsRenderer extends TheiaNotificationsRenderer {
protected render(): void { protected render(): void {
ReactDOM.render( ReactDOM.render(
<div> <div>
<NotificationToastsComponent <NotificationToastsComponent
manager={this.manager} manager={this.manager}
corePreferences={this.corePreferences} corePreferences={this.corePreferences}
/> />
<NotificationCenterComponent manager={this.manager} /> <NotificationCenterComponent manager={this.manager} />
</div>, </div>,
this.container this.container
); );
} }
} }

View File

@@ -1,101 +1,97 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { import {
Disposable, Disposable,
DisposableCollection, DisposableCollection,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl'; import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
type CancelablePromise = Promise<monaco.referenceSearch.ReferencesModel> & { type CancelablePromise = Promise<monaco.referenceSearch.ReferencesModel> & {
cancel: () => void; cancel: () => void;
}; };
interface EditorFactory { interface EditorFactory {
( (
override: monaco.editor.IEditorOverrideServices, override: monaco.editor.IEditorOverrideServices,
toDispose: DisposableCollection toDispose: DisposableCollection
): Promise<MonacoEditor>; ): Promise<MonacoEditor>;
} }
@injectable() @injectable()
export class MonacoEditorProvider extends TheiaMonacoEditorProvider { export class MonacoEditorProvider extends TheiaMonacoEditorProvider {
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl; protected readonly sketchesServiceClient: SketchesServiceClientImpl;
protected async doCreateEditor( protected async doCreateEditor(
uri: URI, uri: URI,
factory: EditorFactory factory: EditorFactory
): Promise<MonacoEditor> { ): Promise<MonacoEditor> {
const editor = await super.doCreateEditor(uri, factory); const editor = await super.doCreateEditor(uri, factory);
const toDispose = new DisposableCollection(); const toDispose = new DisposableCollection();
toDispose.push(this.installCustomReferencesController(editor)); toDispose.push(this.installCustomReferencesController(editor));
toDispose.push(editor.onDispose(() => toDispose.dispose())); toDispose.push(editor.onDispose(() => toDispose.dispose()));
return editor; return editor;
} }
private installCustomReferencesController( private installCustomReferencesController(editor: MonacoEditor): Disposable {
editor: MonacoEditor const control = editor.getControl();
): Disposable { const referencesController =
const control = editor.getControl(); control._contributions['editor.contrib.referencesController'];
const referencesController = const originalToggleWidget = referencesController.toggleWidget;
control._contributions['editor.contrib.referencesController']; const toDispose = new DisposableCollection();
const originalToggleWidget = referencesController.toggleWidget; const toDisposeBeforeToggleWidget = new DisposableCollection();
const toDispose = new DisposableCollection(); referencesController.toggleWidget = (
const toDisposeBeforeToggleWidget = new DisposableCollection(); range: monaco.Range,
referencesController.toggleWidget = ( modelPromise: CancelablePromise,
range: monaco.Range, peekMode: boolean
modelPromise: CancelablePromise, ) => {
peekMode: boolean toDisposeBeforeToggleWidget.dispose();
) => { originalToggleWidget.bind(referencesController)(
toDisposeBeforeToggleWidget.dispose(); range,
originalToggleWidget.bind(referencesController)( modelPromise,
range, peekMode
modelPromise, );
peekMode if (referencesController._widget) {
); if ('onDidClose' in referencesController._widget) {
if (referencesController._widget) { toDisposeBeforeToggleWidget.push(
if ('onDidClose' in referencesController._widget) { (referencesController._widget as any).onDidClose(() =>
toDisposeBeforeToggleWidget.push( toDisposeBeforeToggleWidget.dispose()
(referencesController._widget as any).onDidClose(() =>
toDisposeBeforeToggleWidget.dispose()
)
);
}
const preview = (referencesController._widget as any)
._preview as monaco.editor.ICodeEditor;
if (preview) {
toDisposeBeforeToggleWidget.push(
preview.onDidChangeModel(() =>
this.updateReadOnlyState(preview)
)
);
this.updateReadOnlyState(preview);
}
}
};
toDispose.push(
Disposable.create(() => toDisposeBeforeToggleWidget.dispose())
);
toDispose.push(
Disposable.create(
() => (referencesController.toggleWidget = originalToggleWidget)
) )
); );
return toDispose; }
} const preview = (referencesController._widget as any)
._preview as monaco.editor.ICodeEditor;
if (preview) {
toDisposeBeforeToggleWidget.push(
preview.onDidChangeModel(() => this.updateReadOnlyState(preview))
);
this.updateReadOnlyState(preview);
}
}
};
toDispose.push(
Disposable.create(() => toDisposeBeforeToggleWidget.dispose())
);
toDispose.push(
Disposable.create(
() => (referencesController.toggleWidget = originalToggleWidget)
)
);
return toDispose;
}
private updateReadOnlyState( private updateReadOnlyState(
editor: monaco.editor.ICodeEditor | undefined editor: monaco.editor.ICodeEditor | undefined
): void { ): void {
if (!editor) { if (!editor) {
return; return;
}
const model = editor.getModel();
if (!model) {
return;
}
const readOnly = this.sketchesServiceClient.isReadOnly(model.uri);
editor.updateOptions({ readOnly });
} }
const model = editor.getModel();
if (!model) {
return;
}
const readOnly = this.sketchesServiceClient.isReadOnly(model.uri);
editor.updateOptions({ readOnly });
}
} }

View File

@@ -3,7 +3,7 @@ import { MonacoStatusBarContribution as TheiaMonacoStatusBarContribution } from
@injectable() @injectable()
export class MonacoStatusBarContribution extends TheiaMonacoStatusBarContribution { export class MonacoStatusBarContribution extends TheiaMonacoStatusBarContribution {
protected setConfigTabSizeWidget() {} protected setConfigTabSizeWidget() {}
protected setLineEndingWidget() {} protected setLineEndingWidget() {}
} }

View File

@@ -10,73 +10,71 @@ import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-ser
@injectable() @injectable()
export class MonacoTextModelService extends TheiaMonacoTextModelService { export class MonacoTextModelService extends TheiaMonacoTextModelService {
@inject(SketchesServiceClientImpl) @inject(SketchesServiceClientImpl)
protected readonly sketchesServiceClient: SketchesServiceClientImpl; protected readonly sketchesServiceClient: SketchesServiceClientImpl;
protected async createModel( protected async createModel(resource: Resource): Promise<MonacoEditorModel> {
resource: Resource const factory = this.factories
): Promise<MonacoEditorModel> { .getContributions()
const factory = this.factories .find(({ scheme }) => resource.uri.scheme === scheme);
.getContributions() const readOnly = this.sketchesServiceClient.isReadOnly(resource.uri);
.find(({ scheme }) => resource.uri.scheme === scheme); return factory
const readOnly = this.sketchesServiceClient.isReadOnly(resource.uri); ? factory.createModel(resource)
return factory : new MaybeReadonlyMonacoEditorModel(
? factory.createModel(resource) resource,
: new MaybeReadonlyMonacoEditorModel( this.m2p,
resource, this.p2m,
this.m2p, this.logger,
this.p2m, undefined,
this.logger, readOnly
undefined, );
readOnly }
);
}
} }
// https://github.com/eclipse-theia/theia/pull/8491 // https://github.com/eclipse-theia/theia/pull/8491
class SilentMonacoEditorModel extends MonacoEditorModel { class SilentMonacoEditorModel extends MonacoEditorModel {
protected trace(loggable: Loggable): void { protected trace(loggable: Loggable): void {
if (this.logger) { if (this.logger) {
this.logger.trace((log: Log) => this.logger.trace((log: Log) =>
loggable((message, ...params) => loggable((message, ...params) =>
log(message, ...params, this.resource.uri.toString(true)) log(message, ...params, this.resource.uri.toString(true))
) )
); );
}
} }
}
} }
class MaybeReadonlyMonacoEditorModel extends SilentMonacoEditorModel { class MaybeReadonlyMonacoEditorModel extends SilentMonacoEditorModel {
constructor( constructor(
protected readonly resource: Resource, protected readonly resource: Resource,
protected readonly m2p: MonacoToProtocolConverter, protected readonly m2p: MonacoToProtocolConverter,
protected readonly p2m: ProtocolToMonacoConverter, protected readonly p2m: ProtocolToMonacoConverter,
protected readonly logger?: ILogger, protected readonly logger?: ILogger,
protected readonly editorPreferences?: EditorPreferences, protected readonly editorPreferences?: EditorPreferences,
protected readonly _readOnly?: boolean protected readonly _readOnly?: boolean
) { ) {
super(resource, m2p, p2m, logger, editorPreferences); super(resource, m2p, p2m, logger, editorPreferences);
} }
get readOnly(): boolean { get readOnly(): boolean {
if (typeof this._readOnly === 'boolean') { if (typeof this._readOnly === 'boolean') {
return this._readOnly; return this._readOnly;
}
return this.resource.saveContents === undefined;
} }
return this.resource.saveContents === undefined;
}
protected setDirty(dirty: boolean): void { protected setDirty(dirty: boolean): void {
if (this._readOnly === true) { if (this._readOnly === true) {
// NOOP // NOOP
return; return;
}
if (dirty === this._dirty) {
return;
}
this._dirty = dirty;
if (dirty === false) {
(this as any).updateSavedVersionId();
}
this.onDirtyChangedEmitter.fire(undefined);
} }
if (dirty === this._dirty) {
return;
}
this._dirty = dirty;
if (dirty === false) {
(this as any).updateSavedVersionId();
}
this.onDirtyChangedEmitter.fire(undefined);
}
} }

View File

@@ -11,35 +11,35 @@ import { WorkspacePreferences } from '@theia/workspace/lib/browser/workspace-pre
@injectable() @injectable()
export class FileNavigatorContribution extends TheiaFileNavigatorContribution { export class FileNavigatorContribution extends TheiaFileNavigatorContribution {
constructor( constructor(
@inject(FileNavigatorPreferences) @inject(FileNavigatorPreferences)
protected readonly fileNavigatorPreferences: FileNavigatorPreferences, protected readonly fileNavigatorPreferences: FileNavigatorPreferences,
@inject(OpenerService) protected readonly openerService: OpenerService, @inject(OpenerService) protected readonly openerService: OpenerService,
@inject(FileNavigatorFilter) @inject(FileNavigatorFilter)
protected readonly fileNavigatorFilter: FileNavigatorFilter, protected readonly fileNavigatorFilter: FileNavigatorFilter,
@inject(WorkspaceService) @inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService, protected readonly workspaceService: WorkspaceService,
@inject(WorkspacePreferences) @inject(WorkspacePreferences)
protected readonly workspacePreferences: WorkspacePreferences protected readonly workspacePreferences: WorkspacePreferences
) { ) {
super( super(
fileNavigatorPreferences, fileNavigatorPreferences,
openerService, openerService,
fileNavigatorFilter, fileNavigatorFilter,
workspaceService, workspaceService,
workspacePreferences workspacePreferences
); );
this.options.defaultWidgetOptions.rank = 1; this.options.defaultWidgetOptions.rank = 1;
} }
async initializeLayout(app: FrontendApplication): Promise<void> { async initializeLayout(app: FrontendApplication): Promise<void> {
// NOOP // NOOP
} }
registerKeybindings(registry: KeybindingRegistry): void { registerKeybindings(registry: KeybindingRegistry): void {
super.registerKeybindings(registry); super.registerKeybindings(registry);
[WorkspaceCommands.FILE_RENAME, WorkspaceCommands.FILE_DELETE].forEach( [WorkspaceCommands.FILE_RENAME, WorkspaceCommands.FILE_DELETE].forEach(
registry.unregisterKeybinding.bind(registry) registry.unregisterKeybinding.bind(registry)
); );
} }
} }

View File

@@ -8,12 +8,12 @@ import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@thei
*/ */
@injectable() @injectable()
export class NavigatorTabBarDecorator extends TheiaNavigatorTabBarDecorator { export class NavigatorTabBarDecorator extends TheiaNavigatorTabBarDecorator {
onStart(): void { onStart(): void {
// NOOP // NOOP
} }
decorate(): WidgetDecoration.Data[] { decorate(): WidgetDecoration.Data[] {
// Does not decorate anything. // Does not decorate anything.
return []; return [];
} }
} }

View File

@@ -4,15 +4,15 @@ import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/
@injectable() @injectable()
export class OutlineViewContribution extends TheiaOutlineViewContribution { export class OutlineViewContribution extends TheiaOutlineViewContribution {
constructor() { constructor() {
super(); super();
this.options.defaultWidgetOptions = { this.options.defaultWidgetOptions = {
area: 'left', area: 'left',
rank: 500, rank: 500,
}; };
} }
async initializeLayout(app: FrontendApplication): Promise<void> { async initializeLayout(app: FrontendApplication): Promise<void> {
// NOOP // NOOP
} }
} }

View File

@@ -5,55 +5,50 @@ import { OutputUri } from '@theia/output/lib/common/output-uri';
import { IReference } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { IReference } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { import {
OutputChannelManager as TheiaOutputChannelManager, OutputChannelManager as TheiaOutputChannelManager,
OutputChannel as TheiaOutputChannel, OutputChannel as TheiaOutputChannel,
} from '@theia/output/lib/common/output-channel'; } from '@theia/output/lib/common/output-channel';
@injectable() @injectable()
export class OutputChannelManager extends TheiaOutputChannelManager { export class OutputChannelManager extends TheiaOutputChannelManager {
getChannel(name: string): TheiaOutputChannel { getChannel(name: string): TheiaOutputChannel {
const existing = this.channels.get(name); const existing = this.channels.get(name);
if (existing) { if (existing) {
return existing; return existing;
}
// We have to register the resource first, because `textModelService#createModelReference` will require it
// right after creating the monaco.editor.ITextModel.
// All `append` and `appendLine` will be deferred until the underlying text-model instantiation.
let resource = this.resources.get(name);
if (!resource) {
const uri = OutputUri.create(name);
const editorModelRef = new Deferred<
IReference<MonacoEditorModel>
>();
resource = this.createResource({ uri, editorModelRef });
this.resources.set(name, resource);
this.textModelService
.createModelReference(uri)
.then((ref) => editorModelRef.resolve(ref));
}
const channel = new OutputChannel(resource, this.preferences);
this.channels.set(name, channel);
this.toDisposeOnChannelDeletion.set(
name,
this.registerListeners(channel)
);
this.channelAddedEmitter.fire(channel);
if (!this.selectedChannel) {
this.selectedChannel = channel;
}
return channel;
} }
// We have to register the resource first, because `textModelService#createModelReference` will require it
// right after creating the monaco.editor.ITextModel.
// All `append` and `appendLine` will be deferred until the underlying text-model instantiation.
let resource = this.resources.get(name);
if (!resource) {
const uri = OutputUri.create(name);
const editorModelRef = new Deferred<IReference<MonacoEditorModel>>();
resource = this.createResource({ uri, editorModelRef });
this.resources.set(name, resource);
this.textModelService
.createModelReference(uri)
.then((ref) => editorModelRef.resolve(ref));
}
const channel = new OutputChannel(resource, this.preferences);
this.channels.set(name, channel);
this.toDisposeOnChannelDeletion.set(name, this.registerListeners(channel));
this.channelAddedEmitter.fire(channel);
if (!this.selectedChannel) {
this.selectedChannel = channel;
}
return channel;
}
} }
export class OutputChannel extends TheiaOutputChannel { export class OutputChannel extends TheiaOutputChannel {
dispose(): void { dispose(): void {
super.dispose(); super.dispose();
if ((this as any).disposed) { if ((this as any).disposed) {
const textModifyQueue: PQueue = (this as any).textModifyQueue; const textModifyQueue: PQueue = (this as any).textModifyQueue;
textModifyQueue.pause(); textModifyQueue.pause();
textModifyQueue.clear(); textModifyQueue.clear();
}
} }
}
} }

Some files were not shown because too many files have changed in this diff Show More