mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-09 10:28:32 +00:00
443 lines
13 KiB
TypeScript
443 lines
13 KiB
TypeScript
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<string | undefined>;
|
|
}
|
|
|
|
@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<string | undefined>();
|
|
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<void>,
|
|
};
|
|
|
|
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<void> {
|
|
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<string> {
|
|
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<StartDebugResult> {
|
|
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<StartDebugResult | undefined> {
|
|
return this.commandService.executeCommand<StartDebugResult>(
|
|
'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<boolean> {
|
|
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<StartDebugParams | undefined> {
|
|
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<BoardDetails | undefined>,
|
|
getData: (fqbn: string) => MaybePromise<BoardsDataStore.Data>,
|
|
appendConfigToFqbn: (fqbn: string) => MaybePromise<string | undefined>,
|
|
checkDebugEnabled: (params: CheckDebugEnabledParams) => MaybePromise<string>
|
|
): Promise<string> {
|
|
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
|
|
);
|
|
}
|