import { Emitter, Event } from '@theia/core/lib/common/event'; import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry'; import { nls } from '@theia/core/lib/common/nls'; import { MaybePromise } from '@theia/core/lib/common/types'; import { inject, injectable } from '@theia/core/shared/inversify'; import { noBoardSelected } from '../../common/nls'; import { BoardDetails, BoardIdentifier, BoardsService, CheckDebugEnabledParams, ExecutableService, SketchRef, isBoardIdentifierChangeEvent, isCompileSummary, } from '../../common/protocol'; import { BoardsDataStore } from '../boards/boards-data-store'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { HostedPluginSupport } from '../hosted/hosted-plugin-support'; import { ArduinoMenus } from '../menu/arduino-menus'; import { NotificationCenter } from '../notification-center'; import { CurrentSketch } from '../sketches-service-client-impl'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { Command, CommandRegistry, SketchContribution, TabBarToolbarRegistry, URI, } from './contribution'; const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug'; interface StartDebugParams { /** * Absolute filesystem path to the Arduino CLI executable. */ readonly cliPath: string; /** * The the board to debug. */ readonly board: Readonly<{ fqbn: string; name?: string }>; /** * Absolute filesystem path of the sketch to debug. */ readonly sketchPath: string; /** * Location where the `launch.json` will be created on the fly before starting every debug session. * If not defined, it falls back to `sketchPath/.vscode/launch.json`. */ readonly launchConfigsDirPath?: string; /** * Absolute path to the `arduino-cli.yaml` file. If not specified, it falls back to `~/.arduinoIDE/arduino-cli.yaml`. */ readonly cliConfigPath?: string; /** * Programmer for the debugging. */ readonly programmer?: string; /** * Custom progress title to use when getting the debug information from the CLI. */ readonly title?: string; } type StartDebugResult = boolean; export const DebugDisabledStatusMessageSource = Symbol( 'DebugDisabledStatusMessageSource' ); export interface DebugDisabledStatusMessageSource { /** * `undefined` if debugging is enabled (for the currently selected board + programmer + config options). * Otherwise, it's the human readable message why it's disabled. */ get message(): string | undefined; /** * Emits an event when {@link message} changes. */ get onDidChangeMessage(): Event; } @injectable() export class Debug extends SketchContribution implements DebugDisabledStatusMessageSource { @inject(HostedPluginSupport) private readonly hostedPluginSupport: HostedPluginSupport; @inject(NotificationCenter) private readonly notificationCenter: NotificationCenter; @inject(ExecutableService) private readonly executableService: ExecutableService; @inject(BoardsService) private readonly boardService: BoardsService; @inject(BoardsServiceProvider) private readonly boardsServiceProvider: BoardsServiceProvider; @inject(BoardsDataStore) private readonly boardsDataStore: BoardsDataStore; /** * If `undefined`, debugging is enabled. Otherwise, the human-readable reason why it's disabled. */ private _message?: string = noBoardSelected; // Initial pessimism. private readonly didChangeMessageEmitter = new Emitter(); readonly onDidChangeMessage = this.didChangeMessageEmitter.event; get message(): string | undefined { return this._message; } private set message(message: string | undefined) { this._message = message; this.didChangeMessageEmitter.fire(this._message); } private readonly debugToolbarItem = { id: Debug.Commands.START_DEBUGGING.id, command: Debug.Commands.START_DEBUGGING.id, tooltip: `${ this.message ? nls.localize( 'arduino/debug/debugWithMessage', 'Debug - {0}', this.message ) : Debug.Commands.START_DEBUGGING.label }`, priority: 3, onDidChange: this.onDidChangeMessage as Event, }; override onStart(): void { this.onDidChangeMessage( () => (this.debugToolbarItem.tooltip = `${ this.message ? nls.localize( 'arduino/debug/debugWithMessage', 'Debug - {0}', this.message ) : Debug.Commands.START_DEBUGGING.label }`) ); this.boardsServiceProvider.onBoardsConfigDidChange((event) => { if (isBoardIdentifierChangeEvent(event)) { this.updateMessage(); } }); this.notificationCenter.onPlatformDidInstall(() => this.updateMessage()); this.notificationCenter.onPlatformDidUninstall(() => this.updateMessage()); this.boardsDataStore.onDidChange((event) => { const selectedFqbn = this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn; if (event.changes.find((change) => change.fqbn === selectedFqbn)) { this.updateMessage(); } }); this.commandService.onDidExecuteCommand((event) => { const { commandId, args } = event; if ( commandId === 'arduino.languageserver.notifyBuildDidComplete' && isCompileSummary(args[0]) ) { this.updateMessage(); } }); } override onReady(): void { this.boardsServiceProvider.ready.then(() => this.updateMessage()); } override registerCommands(registry: CommandRegistry): void { registry.registerCommand(Debug.Commands.START_DEBUGGING, { execute: () => this.startDebug(), isVisible: (widget) => ArduinoToolbar.is(widget) && widget.side === 'left', isEnabled: () => !this.message, }); registry.registerCommand(Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG, { execute: () => this.toggleCompileForDebug(), isToggled: () => this.compileForDebug, }); registry.registerCommand(Debug.Commands.IS_OPTIMIZE_FOR_DEBUG, { execute: () => this.compileForDebug, }); } override registerToolbarItems(registry: TabBarToolbarRegistry): void { registry.registerItem(this.debugToolbarItem); } override registerMenus(registry: MenuModelRegistry): void { registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { commandId: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.id, label: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.label, order: '5', }); } private async updateMessage(): Promise { try { await this.isDebugEnabled(); this.message = undefined; } catch (err) { let message = String(err); if (err instanceof Error) { message = err.message; } this.message = message; } } private async isDebugEnabled( board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig .selectedBoard ): Promise { const debugFqbn = await isDebugEnabled( board, (fqbn) => this.boardService.getBoardDetails({ fqbn }), (fqbn) => this.boardsDataStore.getData(fqbn), (fqbn) => this.boardsDataStore.appendConfigToFqbn(fqbn), (params) => this.boardService.checkDebugEnabled(params) ); return debugFqbn; } private async startDebug( board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig .selectedBoard, sketch: | CurrentSketch | undefined = this.sketchServiceClient.tryGetCurrentSketch() ): Promise { if (!CurrentSketch.isValid(sketch)) { return false; } const params = await this.createStartDebugParams(board); if (!params) { return false; } await this.hostedPluginSupport.didStart; try { const result = await this.debug(params); return Boolean(result); } catch (err) { if (await this.isSketchNotVerifiedError(err, sketch)) { const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); const answer = await this.messageService.error( sketchIsNotCompiled(sketch.name), yes ); if (answer === yes) { this.commandService.executeCommand('arduino-verify-sketch'); } } else { this.messageService.error( err instanceof Error ? err.message : String(err) ); } } return false; } private async debug( params: StartDebugParams ): Promise { return this.commandService.executeCommand( 'arduino.debug.start', params ); } get compileForDebug(): boolean { const value = window.localStorage.getItem(COMPILE_FOR_DEBUG_KEY); return value === 'true'; } private toggleCompileForDebug(): void { const oldState = this.compileForDebug; const newState = !oldState; window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState)); this.menuManager.update(); } private async isSketchNotVerifiedError( err: unknown, sketch: SketchRef ): Promise { if (err instanceof Error) { try { const buildPaths = await this.sketchesService.getBuildPath(sketch); return buildPaths.some((tempBuildPath) => err.message.includes(tempBuildPath) ); } catch { return false; } } return false; } private async createStartDebugParams( board: BoardIdentifier | undefined ): Promise { if (!board || !board.fqbn) { return undefined; } let debugFqbn: string | undefined = undefined; try { debugFqbn = await this.isDebugEnabled(board); } catch {} if (!debugFqbn) { return undefined; } const [sketch, executables, boardsData] = await Promise.all([ this.sketchServiceClient.currentSketch(), this.executableService.list(), this.boardsDataStore.getData(board.fqbn), ]); if (!CurrentSketch.isValid(sketch)) { return undefined; } const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri( sketch ); const [cliPath, sketchPath, launchConfigsDirPath] = await Promise.all([ this.fileService.fsPath(new URI(executables.cliUri)), this.fileService.fsPath(new URI(sketch.uri)), this.fileService.fsPath(new URI(ideTempFolderUri)), ]); return { board: { fqbn: debugFqbn, name: board.name }, cliPath, sketchPath, launchConfigsDirPath, programmer: boardsData.selectedProgrammer?.id, title: nls.localize( 'arduino/debug/getDebugInfo', 'Getting debug info...' ), }; } } export namespace Debug { export namespace Commands { export const START_DEBUGGING = Command.toLocalizedCommand( { id: 'arduino-start-debug', label: 'Start Debugging', category: 'Arduino', }, 'vscode/debug.contribution/startDebuggingHelp' ); export const TOGGLE_OPTIMIZE_FOR_DEBUG = Command.toLocalizedCommand( { id: 'arduino-toggle-optimize-for-debug', label: 'Optimize for Debugging', category: 'Arduino', }, 'arduino/debug/optimizeForDebugging' ); export const IS_OPTIMIZE_FOR_DEBUG: Command = { id: 'arduino-is-optimize-for-debug', }; } } /** * Resolves with the FQBN to use for the `debug --info --programmer p --fqbn $FQBN` command. Otherwise, rejects. * * (non-API) */ export async function isDebugEnabled( board: BoardIdentifier | undefined, getDetails: (fqbn: string) => MaybePromise, getData: (fqbn: string) => MaybePromise, appendConfigToFqbn: (fqbn: string) => MaybePromise, checkDebugEnabled: (params: CheckDebugEnabledParams) => MaybePromise ): Promise { if (!board) { throw new Error(noBoardSelected); } const { fqbn } = board; if (!fqbn) { throw new Error(noPlatformInstalledFor(board.name)); } const [details, data, fqbnWithConfig] = await Promise.all([ getDetails(fqbn), getData(fqbn), appendConfigToFqbn(fqbn), ]); if (!details) { throw new Error(noPlatformInstalledFor(board.name)); } if (!fqbnWithConfig) { throw new Error( `Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}` ); } const params = { fqbn: fqbnWithConfig, programmer: data.selectedProgrammer?.id, }; try { const debugFqbn = await checkDebugEnabled(params); return debugFqbn; } catch (err) { throw new Error(debuggingNotSupported(board.name)); } } /** * (non-API) */ export function sketchIsNotCompiled(sketchName: string): string { return nls.localize( 'arduino/debug/sketchIsNotCompiled', "Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?", sketchName ); } /** * (non-API) */ export function noPlatformInstalledFor(boardName: string): string { return nls.localize( 'arduino/debug/noPlatformInstalledFor', "Platform is not installed for '{0}'", boardName ); } /** * (non-API) */ export function debuggingNotSupported(boardName: string): string { return nls.localize( 'arduino/debug/debuggingNotSupported', "Debugging is not supported by '{0}'", boardName ); }