diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 7dd6fc1b9..89b199ebe 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -72,6 +72,7 @@ import { ConfigServicePath, } from '../common/protocol/config-service'; import { MonitorWidget } from './serial/monitor/monitor-widget'; +import { DecodeWidget } from './serial/decode/decode-widget'; import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution'; import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator'; import { TabBarDecoratorService } from './theia/core/tab-bar-decorator'; @@ -321,6 +322,7 @@ import { InterfaceScale } from './contributions/interface-scale'; import { OpenHandler } from '@theia/core/lib/browser/opener-service'; import { NewCloudSketch } from './contributions/new-cloud-sketch'; import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget'; +import { DecodeViewContribution } from './serial/decode/decode-view'; import { WindowTitleUpdater } from './theia/core/window-title-updater'; import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater'; import { ThemeServiceWithDB } from './theia/core/theming'; @@ -480,6 +482,21 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .inSingletonScope(); bind(CoreErrorHandler).toSelf().inSingletonScope(); + // Decode box + bind(DecodeWidget).toSelf(); + bindViewContribution(bind, DecodeViewContribution); + bind(TabBarToolbarContribution).toService(DecodeViewContribution); + bind(WidgetFactory).toDynamicValue((context) => ({ + id: DecodeWidget.ID, + createWidget: () => { + return new DecodeWidget( + context.container.get(ConfigService), + context.container.get(BoardsServiceProvider), + context.container.get(SketchesServiceClientImpl) + ); + }, + })); + // Serial monitor bind(MonitorWidget).toSelf(); bind(FrontendApplicationContribution).toService(MonitorModel); diff --git a/arduino-ide-extension/src/browser/serial/decode/decode-output.tsx b/arduino-ide-extension/src/browser/serial/decode/decode-output.tsx new file mode 100644 index 000000000..0e142076a --- /dev/null +++ b/arduino-ide-extension/src/browser/serial/decode/decode-output.tsx @@ -0,0 +1,150 @@ +import * as React from '@theia/core/shared/react'; +import { Event } from '@theia/core/lib/common/event'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { spawnCommand } from '../../../node/exec-util'; + +export type Line = { message: string; lineLen: number }; +export type Element = { + address: string; + function: string; + path: { + value: string; + isLink: boolean; + } + lineNumber: string +}; + +export class DecodeOutput extends React.Component< + DecodeOutput.Props, + DecodeOutput.State +> { + /** + * Do not touch it. It is used to be able to "follow" the serial monitor log. + */ + protected toDisposeBeforeUnmount = new DisposableCollection(); + + constructor(props: Readonly) { + super(props); + this.state = { + elements: [], + }; + } + + // If a string of .. or . is found, replaces it with "*" + changeVersionToAny = (path: string) => { + const regex = new RegExp(/(\d\.\d\.\d)|(\d\.\d)/g); + const found = path.match(regex); + if(found) { + return path.replace(found[0], "*") + } + return path + } + + isClientPath = async (path:string): Promise => { + return await spawnCommand("cd", [ + path + ], (err) => err) + .then((data) => true) + .catch(err => false) + } + + openFinder = async (path:string) => { + await spawnCommand("open", [ + path + ]); + } + + retrievePath = (dirPath:string) => { + return dirPath.substring(0,dirPath.lastIndexOf("/")+1); + } + + decodeText = async (value: string) => { + const lines = value.split("\n"); + + // Remove the extra newline at the end + lines.pop(); + const elements : Array = []; + for(let i=0;i + {this.state.elements.map((element) => ( +
+ {element.address} + {element.function} + at + { element.path.isLink ? await this.openFinder(this.retrievePath(element.path.value))}>{element.path.value} : {element.path.value} } + line + {element.lineNumber} +
+ ))} + + ); + } + + override shouldComponentUpdate(): boolean { + return true; + } + + override componentDidMount(): void { + this.toDisposeBeforeUnmount.pushAll([ + this.props.clearConsoleEvent(() => + this.setState({ elements: [] }) + ), + ]); + } + + override componentWillUnmount(): void { + // TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout? + this.toDisposeBeforeUnmount.dispose(); + } +} + +export namespace DecodeOutput { + export interface Props { + readonly clearConsoleEvent: Event; + readonly height: number; + } + + export interface State { + elements: Element[]; + } + + export interface SelectOption { + readonly label: string; + readonly value: T; + } + + export const MAX_CHARACTERS = 1_000_000; +} diff --git a/arduino-ide-extension/src/browser/serial/decode/decode-send-input.tsx b/arduino-ide-extension/src/browser/serial/decode/decode-send-input.tsx new file mode 100644 index 000000000..fd59277e8 --- /dev/null +++ b/arduino-ide-extension/src/browser/serial/decode/decode-send-input.tsx @@ -0,0 +1,135 @@ +import * as React from '@theia/core/shared/react'; +import { Key, KeyCode } from '@theia/core/lib/browser/keys'; +import { DisposableCollection } from '@theia/core/lib/common'; + +class HistoryList { + private readonly items: string[] = []; + private index = -1; + + constructor(private readonly size = 100) {} + + push(val: string): void { + if (val !== this.items[this.items.length - 1]) { + this.items.push(val); + } + while (this.items.length > this.size) { + this.items.shift(); + } + this.index = -1; + } + + previous(): string { + if (this.index === -1) { + this.index = this.items.length - 1; + return this.items[this.index]; + } + if (this.hasPrevious) { + return this.items[--this.index]; + } + return this.items[this.index]; + } + + private get hasPrevious(): boolean { + return this.index >= 1; + } + + next(): string { + if (this.index === this.items.length - 1) { + this.index = -1; + return ''; + } + if (this.hasNext) { + return this.items[++this.index]; + } + return ''; + } + + private get hasNext(): boolean { + return this.index >= 0 && this.index !== this.items.length - 1; + } +} + +export namespace DecodeSendInput { + export interface Props { + readonly onSend: (text: string) => void; + readonly resolveFocus: (element: HTMLElement | undefined) => void; + } + export interface State { + text: string; + history: HistoryList; + } +} + +export class DecodeSendInput extends React.Component< + DecodeSendInput.Props, + DecodeSendInput.State +> { + protected toDisposeBeforeUnmount = new DisposableCollection(); + + constructor(props: Readonly) { + super(props); + this.state = { text: '', history: new HistoryList() }; + this.onChange = this.onChange.bind(this); + this.onSend = this.onSend.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + } + + override componentWillUnmount(): void { + // TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout? + this.toDisposeBeforeUnmount.dispose(); + } + + override render(): React.ReactNode { + return ( + + ); + } + + protected get placeholder(): string { + + return 'Enter backtrace text to decode. (Ex: Backtrace: 0x40086e7c:0x3ffb4ff0...)'; + } + + protected setRef = (element: HTMLElement | null): void => { + if (this.props.resolveFocus) { + this.props.resolveFocus(element || undefined); + } + }; + + protected onChange(event: React.ChangeEvent): void { + this.setState({ text: event.target.value }); + } + + protected onSend(): void { + this.props.onSend(this.state.text); + this.setState({ text: '' }); + } + + protected onKeyDown(event: React.KeyboardEvent): void { + const keyCode = KeyCode.createKeyCode(event.nativeEvent); + if (keyCode) { + const { key } = keyCode; + if (key === Key.ENTER) { + const { text } = this.state; + this.onSend(); + if (text) { + this.state.history.push(text); + } + } else if (key === Key.ARROW_UP) { + this.setState({ text: this.state.history.previous() }); + } else if (key === Key.ARROW_DOWN) { + this.setState({ text: this.state.history.next() }); + } else if (key === Key.ESCAPE) { + this.setState({ text: '' }); + } + } + } +} diff --git a/arduino-ide-extension/src/browser/serial/decode/decode-view.tsx b/arduino-ide-extension/src/browser/serial/decode/decode-view.tsx new file mode 100644 index 000000000..fb084b590 --- /dev/null +++ b/arduino-ide-extension/src/browser/serial/decode/decode-view.tsx @@ -0,0 +1,110 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { AbstractViewContribution, codicon } from '@theia/core/lib/browser'; +import { DecodeWidget } from './decode-widget'; +import { MenuModelRegistry, Command, CommandRegistry } from '@theia/core'; +import { + TabBarToolbarContribution, + TabBarToolbarRegistry, +} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { ArduinoToolbar } from '../../toolbar/arduino-toolbar'; +import { ArduinoMenus } from '../../menu/arduino-menus'; + +export namespace DebugBox { + export namespace Commands { + export const CLEAR_OUTPUT = Command.toLocalizedCommand( + { + id: 'debug-box-clear-output', + label: 'Clear Output', + iconClass: codicon('clear-all'), + }, + 'vscode/output.contribution/clearOutput.label' + ); + } +} + +@injectable() +export class DecodeViewContribution + extends AbstractViewContribution + implements TabBarToolbarContribution +{ + static readonly TOGGLE_DECODE_BOX = DecodeWidget.ID + ':toggle'; + static readonly TOGGLE_DECODE_BOX_TOOLBAR = + DecodeWidget.ID + ':toggle-toolbar'; + static readonly RESET_DECODE_BOX = DecodeWidget.ID + ':reset'; + + constructor() { + super({ + widgetId: DecodeWidget.ID, + widgetName: DecodeWidget.LABEL, + defaultWidgetOptions: { + area: 'bottom', + }, + toggleCommandId: DecodeViewContribution.TOGGLE_DECODE_BOX, + toggleKeybinding: 'CtrlCmd+Shift+D', + }); + } + + override registerMenus(menus: MenuModelRegistry): void { + if (this.toggleCommand) { + menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { + commandId: this.toggleCommand.id, + label: DecodeWidget.LABEL, + order: '6', + }); + } + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: DebugBox.Commands.CLEAR_OUTPUT.id, + command: DebugBox.Commands.CLEAR_OUTPUT.id, + tooltip: 'Clear Output' + }); + } + + override registerCommands(commands: CommandRegistry): void { + commands.registerCommand(DebugBox.Commands.CLEAR_OUTPUT, { + isEnabled: (widget) => widget instanceof DecodeWidget, + isVisible: (widget) => widget instanceof DecodeWidget, + execute: (widget) => { + if (widget instanceof DecodeWidget) { + widget.clearConsole(); + } + }, + }); + if (this.toggleCommand) { + commands.registerCommand(this.toggleCommand, { + execute: () => this.toggle(), + }); + commands.registerCommand( + { id: DecodeViewContribution.TOGGLE_DECODE_BOX_TOOLBAR }, + { + isVisible: (widget) => + ArduinoToolbar.is(widget) && widget.side === 'right', + execute: () => this.toggle(), + } + ); + } + commands.registerCommand( + { id: DecodeViewContribution.RESET_DECODE_BOX }, + { execute: () => this.reset() } + ); + } + + protected async toggle(): Promise { + const widget = this.tryGetWidget(); + if (widget) { + widget.dispose(); + } else { + await this.openView({ activate: true, reveal: true }); + } + } + + protected async reset(): Promise { + const widget = this.tryGetWidget(); + if (widget) { + widget.dispose(); + await this.openView({ activate: true, reveal: true }); + } + } +} diff --git a/arduino-ide-extension/src/browser/serial/decode/decode-widget.tsx b/arduino-ide-extension/src/browser/serial/decode/decode-widget.tsx new file mode 100644 index 000000000..8449bbe78 --- /dev/null +++ b/arduino-ide-extension/src/browser/serial/decode/decode-widget.tsx @@ -0,0 +1,170 @@ +import * as React from '@theia/core/shared/react'; +import { injectable, inject } from '@theia/core/shared/inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import URI from '@theia/core/lib/common/uri'; +import { + ReactWidget, + Message, + Widget, + MessageLoop, +} from '@theia/core/lib/browser/widgets'; +import { DecodeSendInput } from './decode-send-input'; +import { DecodeOutput } from './decode-output'; +import { spawnCommand } from '../../../node/exec-util'; +import { ConfigService } from '../../../common/protocol'; +import { CurrentSketch, SketchesServiceClientImpl } from '../../sketches-service-client-impl'; +import { BoardsServiceProvider } from '../../boards/boards-service-provider'; + +@injectable() +export class DecodeWidget extends ReactWidget { + + static readonly LABEL = 'Decode Box'; + static readonly ID = 'decode-box'; + protected widgetHeight: number; + protected text: string; + private decodeOutputElement: React.RefObject; + + /** + * Do not touch or use it. It is for setting the focus on the `input` after the widget activation. + */ + protected focusNode: HTMLElement | undefined; + /** + * Guard against re-rendering the view after the close was requested. + * See: https://github.com/eclipse-theia/theia/issues/6704 + */ + protected closing = false; + protected readonly clearOutputEmitter = new Emitter(); + + constructor( + @inject(ConfigService) + protected readonly configService: ConfigService, + + @inject(BoardsServiceProvider) + protected readonly boardsServiceProvider: BoardsServiceProvider, + + @inject(SketchesServiceClientImpl) + protected readonly sketchServiceClient: SketchesServiceClientImpl, + ) { + super(); + this.id = DecodeWidget.ID; + this.title.label = DecodeWidget.LABEL; + this.title.iconClass = 'monitor-tab-icon'; + this.title.closable = true; + this.scrollOptions = undefined; + this.toDispose.push(this.clearOutputEmitter); + this.decodeOutputElement = React.createRef(); + } + + protected override onBeforeAttach(msg: Message): void { + this.update(); + } + + clearConsole(): void { + this.clearOutputEmitter.fire(undefined); + this.update(); + } + + override dispose(): void { + super.dispose(); + } + + protected override onCloseRequest(msg: Message): void { + this.closing = true; + super.onCloseRequest(msg); + } + + protected override 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 override onResize(msg: Widget.ResizeMessage): void { + super.onResize(msg); + this.widgetHeight = msg.height; + this.update(); + } + + protected override 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 render(): React.ReactNode { + + return ( +
+
+
+ +
+
+
+ +
+
+ ); + } + + protected readonly onSend = (value: string) => this.doSend(value); + + protected async doSend(value: string) { + const configPath = await this.configService.getConfiguration() + .then(({config}) => (new URI(config?.dataDirUri)).path); + const boards = this.boardsServiceProvider.boardsConfig + const fqbn = boards.selectedBoard?.fqbn; + if(!fqbn) { + return + } + const selectedBoard = fqbn.split(':')[1]; + const currentSketch = await this.sketchServiceClient.currentSketch(); + if (!CurrentSketch.isValid(currentSketch)) { + return; + } + const sketchUri = (new URI(currentSketch.uri)).path; + const elfPath = `${sketchUri}/build/${fqbn.split(':').join('.')}/${currentSketch.name}.ino.elf`; + + // * enters an unkown foldername, in this case the version of gcc + const xtensaPath= `${configPath}/packages/${selectedBoard}/tools/xtensa-${selectedBoard}-elf-gcc/\*/bin/xtensa-${selectedBoard}-elf-addr2line`; + const regex = new RegExp(/0x4(\d|[a-f]|[A-F]){7}/g); + const arrAddresses = value.match(regex); + if(!arrAddresses) { + return this.decodeOutputElement.current.decodeText('Provided format can not be decoded!'); + } + let decodeResult = ''; + for(let i=0;i