mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-14 04:39:28 +00:00
feat: use new debug -I -P CLI output
- Can pick a programmer if missing, - Can auto-select a programmer on app start, - Can edit the `launch.json`, - Adjust board discovery to new gRPC API. From now on, it's a client read stream, not a duplex. - Allow `.cxx` and `.cc` file extensions. (Closes #2265) - Drop `debuggingSupported` from `BoardDetails`. - Dedicated service endpoint for checking the debugger. Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import type { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
BoardDetails,
|
||||
Programmer,
|
||||
isBoardIdentifierChangeEvent,
|
||||
} from '../../common/protocol';
|
||||
import {
|
||||
BoardsDataStore,
|
||||
findDefaultProgrammer,
|
||||
isEmptyData,
|
||||
} from '../boards/boards-data-store';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
/**
|
||||
* Before CLI 0.35.0-rc.3, there was no `programmer#default` property in the `board details` response.
|
||||
* This method does the programmer migration in the data store. If there is a programmer selected, it's a noop.
|
||||
* If no programmer is selected, it forcefully reloads the details from the CLI and updates it in the local storage.
|
||||
*/
|
||||
@injectable()
|
||||
export class AutoSelectProgrammer extends Contribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
@inject(BoardsDataStore)
|
||||
private readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
override onStart(): void {
|
||||
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
|
||||
if (isBoardIdentifierChangeEvent(event)) {
|
||||
this.ensureProgrammerIsSelected();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.boardsServiceProvider.ready.then(() =>
|
||||
this.ensureProgrammerIsSelected()
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureProgrammerIsSelected(): Promise<boolean> {
|
||||
return ensureProgrammerIsSelected({
|
||||
fqbn: this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
|
||||
getData: (fqbn) => this.boardsDataStore.getData(fqbn),
|
||||
loadBoardDetails: (fqbn) => this.boardsDataStore.loadBoardDetails(fqbn),
|
||||
selectProgrammer: (arg) => this.boardsDataStore.selectProgrammer(arg),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface EnsureProgrammerIsSelectedParams {
|
||||
fqbn: string | undefined;
|
||||
getData: (fqbn: string | undefined) => MaybePromise<BoardsDataStore.Data>;
|
||||
loadBoardDetails: (fqbn: string) => MaybePromise<BoardDetails | undefined>;
|
||||
selectProgrammer(options: {
|
||||
fqbn: string;
|
||||
selectedProgrammer: Programmer;
|
||||
}): MaybePromise<boolean>;
|
||||
}
|
||||
|
||||
export async function ensureProgrammerIsSelected(
|
||||
params: EnsureProgrammerIsSelectedParams
|
||||
): Promise<boolean> {
|
||||
const { fqbn, getData, loadBoardDetails, selectProgrammer } = params;
|
||||
if (!fqbn) {
|
||||
return false;
|
||||
}
|
||||
console.debug(`Ensuring a programmer is selected for ${fqbn}...`);
|
||||
const data = await getData(fqbn);
|
||||
if (isEmptyData(data)) {
|
||||
// For example, the platform is not installed.
|
||||
console.debug(`Skipping. No boards data is available for ${fqbn}.`);
|
||||
return false;
|
||||
}
|
||||
if (data.selectedProgrammer) {
|
||||
console.debug(
|
||||
`A programmer is already selected for ${fqbn}: '${data.selectedProgrammer.id}'.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
let programmer = findDefaultProgrammer(data.programmers, data);
|
||||
if (programmer) {
|
||||
// select the programmer if the default info is available
|
||||
const result = await selectProgrammer({
|
||||
fqbn,
|
||||
selectedProgrammer: programmer,
|
||||
});
|
||||
if (result) {
|
||||
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
console.debug(`Reloading board details for ${fqbn}...`);
|
||||
const reloadedData = await loadBoardDetails(fqbn);
|
||||
if (!reloadedData) {
|
||||
console.debug(`Skipping. No board details found for ${fqbn}.`);
|
||||
return false;
|
||||
}
|
||||
if (!reloadedData.programmers.length) {
|
||||
console.debug(`Skipping. ${fqbn} does not have programmers.`);
|
||||
return false;
|
||||
}
|
||||
programmer = findDefaultProgrammer(reloadedData.programmers, reloadedData);
|
||||
if (!programmer) {
|
||||
console.debug(
|
||||
`Skipping. Could not find a default programmer for ${fqbn}. Programmers were: `
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const result = await selectProgrammer({
|
||||
fqbn,
|
||||
selectedProgrammer: programmer,
|
||||
});
|
||||
if (result) {
|
||||
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
|
||||
} else {
|
||||
console.debug(
|
||||
`Could not select '${programmer.id}' programmer for ${fqbn}.`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
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 {
|
||||
Board,
|
||||
BoardDetails,
|
||||
BoardIdentifier,
|
||||
BoardsService,
|
||||
CheckDebugEnabledParams,
|
||||
ExecutableService,
|
||||
SketchRef,
|
||||
isBoardIdentifierChangeEvent,
|
||||
Sketch,
|
||||
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';
|
||||
@@ -14,95 +22,119 @@ import { NotificationCenter } from '../notification-center';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
SketchContribution,
|
||||
TabBarToolbarRegistry,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { MenuModelRegistry, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
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;
|
||||
|
||||
@injectable()
|
||||
export class Debug extends SketchContribution {
|
||||
@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 reason why it's disabled.
|
||||
* If `undefined`, debugging is enabled. Otherwise, the human-readable reason why it's disabled.
|
||||
*/
|
||||
private _disabledMessages?: string = nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
); // Initial pessimism.
|
||||
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
||||
private onDisabledMessageDidChange =
|
||||
this.disabledMessageDidChangeEmitter.event;
|
||||
private _message?: string = noBoardSelected; // Initial pessimism.
|
||||
private didChangeMessageEmitter = new Emitter<string | undefined>();
|
||||
private onDidChangeMessage = this.didChangeMessageEmitter.event;
|
||||
|
||||
private get disabledMessage(): string | undefined {
|
||||
return this._disabledMessages;
|
||||
private get message(): string | undefined {
|
||||
return this._message;
|
||||
}
|
||||
private set disabledMessage(message: string | undefined) {
|
||||
this._disabledMessages = message;
|
||||
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
|
||||
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.disabledMessage
|
||||
this.message
|
||||
? nls.localize(
|
||||
'arduino/debug/debugWithMessage',
|
||||
'Debug - {0}',
|
||||
this.disabledMessage
|
||||
this.message
|
||||
)
|
||||
: Debug.Commands.START_DEBUGGING.label
|
||||
}`,
|
||||
priority: 3,
|
||||
onDidChange: this.onDisabledMessageDidChange as Event<void>,
|
||||
onDidChange: this.onDidChangeMessage as Event<void>,
|
||||
};
|
||||
|
||||
override onStart(): void {
|
||||
this.onDisabledMessageDidChange(
|
||||
this.onDidChangeMessage(
|
||||
() =>
|
||||
(this.debugToolbarItem.tooltip = `${
|
||||
this.disabledMessage
|
||||
this.message
|
||||
? nls.localize(
|
||||
'arduino/debug/debugWithMessage',
|
||||
'Debug - {0}',
|
||||
this.disabledMessage
|
||||
this.message
|
||||
)
|
||||
: Debug.Commands.START_DEBUGGING.label
|
||||
}`)
|
||||
);
|
||||
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
|
||||
if (isBoardIdentifierChangeEvent(event)) {
|
||||
this.refreshState(event.selectedBoard);
|
||||
this.updateMessage();
|
||||
}
|
||||
});
|
||||
this.notificationCenter.onPlatformDidInstall(() => this.refreshState());
|
||||
this.notificationCenter.onPlatformDidUninstall(() => this.refreshState());
|
||||
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.refreshState();
|
||||
this.updateMessage();
|
||||
}
|
||||
});
|
||||
this.commandService.onDidExecuteCommand((event) => {
|
||||
@@ -111,13 +143,13 @@ export class Debug extends SketchContribution {
|
||||
commandId === 'arduino.languageserver.notifyBuildDidComplete' &&
|
||||
isCompileSummary(args[0])
|
||||
) {
|
||||
this.refreshState();
|
||||
this.updateMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.boardsServiceProvider.ready.then(() => this.refreshState());
|
||||
this.boardsServiceProvider.ready.then(() => this.updateMessage());
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
@@ -125,7 +157,7 @@ export class Debug extends SketchContribution {
|
||||
execute: () => this.startDebug(),
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: () => !this.disabledMessage,
|
||||
isEnabled: () => !this.message,
|
||||
});
|
||||
registry.registerCommand(Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG, {
|
||||
execute: () => this.toggleCompileForDebug(),
|
||||
@@ -148,94 +180,56 @@ export class Debug extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshState(
|
||||
board: Board | undefined = this.boardsServiceProvider.boardsConfig
|
||||
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<void> {
|
||||
if (!board) {
|
||||
this.disabledMessage = nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const fqbn = board.fqbn;
|
||||
if (!fqbn) {
|
||||
this.disabledMessage = nls.localize(
|
||||
'arduino/debug/noPlatformInstalledFor',
|
||||
"Platform is not installed for '{0}'",
|
||||
board.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
const details = await this.boardService.getBoardDetails({ fqbn });
|
||||
if (!details) {
|
||||
this.disabledMessage = nls.localize(
|
||||
'arduino/debug/noPlatformInstalledFor',
|
||||
"Platform is not installed for '{0}'",
|
||||
board.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { debuggingSupported } = details;
|
||||
if (!debuggingSupported) {
|
||||
this.disabledMessage = nls.localize(
|
||||
'arduino/debug/debuggingNotSupported',
|
||||
"Debugging is not supported by '{0}'",
|
||||
board.name
|
||||
);
|
||||
} else {
|
||||
this.disabledMessage = undefined;
|
||||
}
|
||||
): 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
|
||||
): Promise<void> {
|
||||
if (!board) {
|
||||
return;
|
||||
.selectedBoard,
|
||||
sketch:
|
||||
| CurrentSketch
|
||||
| undefined = this.sketchServiceClient.tryGetCurrentSketch()
|
||||
): Promise<StartDebugResult> {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
const { name, fqbn } = board;
|
||||
if (!fqbn) {
|
||||
return;
|
||||
const params = await this.createStartDebugParams(board);
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
await this.hostedPluginSupport.didStart;
|
||||
const [sketch, executables] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.executableService.list(),
|
||||
]);
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const ideTempFolderUri = await this.sketchesService.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,
|
||||
};
|
||||
try {
|
||||
await this.commandService.executeCommand('arduino.debug.start', config);
|
||||
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(
|
||||
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?",
|
||||
sketch.name
|
||||
),
|
||||
sketchIsNotCompiled(sketch.name),
|
||||
yes
|
||||
);
|
||||
if (answer === yes) {
|
||||
@@ -247,6 +241,16 @@ export class Debug extends SketchContribution {
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async debug(
|
||||
params: StartDebugParams
|
||||
): Promise<StartDebugResult | undefined> {
|
||||
return this.commandService.executeCommand<StartDebugResult>(
|
||||
'arduino.debug.start',
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
get compileForDebug(): boolean {
|
||||
@@ -254,7 +258,7 @@ export class Debug extends SketchContribution {
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
async toggleCompileForDebug(): Promise<void> {
|
||||
private toggleCompileForDebug(): void {
|
||||
const oldState = this.compileForDebug;
|
||||
const newState = !oldState;
|
||||
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
|
||||
@@ -263,7 +267,7 @@ export class Debug extends SketchContribution {
|
||||
|
||||
private async isSketchNotVerifiedError(
|
||||
err: unknown,
|
||||
sketch: Sketch
|
||||
sketch: SketchRef
|
||||
): Promise<boolean> {
|
||||
if (err instanceof Error) {
|
||||
try {
|
||||
@@ -277,6 +281,48 @@ export class Debug extends SketchContribution {
|
||||
}
|
||||
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 {
|
||||
@@ -301,3 +347,89 @@ export namespace Debug {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (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}`
|
||||
);
|
||||
}
|
||||
if (!data.selectedProgrammer) {
|
||||
throw new Error(noProgrammerSelectedFor(board.name));
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
/**
|
||||
* (non-API)
|
||||
*/
|
||||
export function noProgrammerSelectedFor(boardName: string): string {
|
||||
return nls.localize(
|
||||
'arduino/debug/noProgrammerSelectedFor',
|
||||
"No programmer selected for '{0}'",
|
||||
boardName
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,26 +20,83 @@ import { NotificationCenter } from '../notification-center';
|
||||
import { SketchContribution, URI } from './contribution';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
|
||||
interface DaemonAddress {
|
||||
/**
|
||||
* The host where the Arduino CLI daemon is available.
|
||||
*/
|
||||
readonly hostname: string;
|
||||
/**
|
||||
* The port where the Arduino CLI daemon is listening.
|
||||
*/
|
||||
readonly port: number;
|
||||
/**
|
||||
* The [id](https://arduino.github.io/arduino-cli/latest/rpc/commands/#instance) of the initialized core Arduino client instance.
|
||||
*/
|
||||
readonly instance: number;
|
||||
}
|
||||
|
||||
interface StartLanguageServerParams {
|
||||
/**
|
||||
* Absolute filesystem path to the Arduino Language Server executable.
|
||||
*/
|
||||
readonly lsPath: string;
|
||||
/**
|
||||
* The hostname and the port for the gRPC channel connecting to the Arduino CLI daemon.
|
||||
* The `instance` number is for the initialized core Arduino client.
|
||||
*/
|
||||
readonly daemonAddress: DaemonAddress;
|
||||
/**
|
||||
* Absolute filesystem path to [`clangd`](https://clangd.llvm.org/).
|
||||
*/
|
||||
readonly clangdPath: string;
|
||||
/**
|
||||
* The board is relevant to start a specific "flavor" of the language.
|
||||
*/
|
||||
readonly board: { fqbn: string; name?: string };
|
||||
/**
|
||||
* `true` if the LS should generate the log files into the default location. The default location is the `cwd` of the process.
|
||||
* It's very often the same as the workspace root of the IDE, aka the sketch folder.
|
||||
* When it is a string, it is the absolute filesystem path to the folder to generate the log files.
|
||||
* If `string`, but the path is inaccessible, the log files will be generated into the default location.
|
||||
*/
|
||||
readonly log?: boolean | string;
|
||||
/**
|
||||
* Optional `env` for the language server process.
|
||||
*/
|
||||
readonly env?: NodeJS.ProcessEnv;
|
||||
/**
|
||||
* Additional flags for the Arduino Language server process.
|
||||
*/
|
||||
readonly flags?: readonly string[];
|
||||
/**
|
||||
* Set to `true`, to enable `Diagnostics`.
|
||||
*/
|
||||
readonly realTimeDiagnostics?: boolean;
|
||||
/**
|
||||
* If `true`, the logging is not forwarded to the _Output_ view via the language client.
|
||||
*/
|
||||
readonly silentOutput?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The FQBN the language server runs with or `undefined` if it could not start.
|
||||
*/
|
||||
type StartLanguageServerResult = string | undefined;
|
||||
|
||||
@injectable()
|
||||
export class InoLanguage extends SketchContribution {
|
||||
@inject(HostedPluginEvents)
|
||||
private readonly hostedPluginEvents: HostedPluginEvents;
|
||||
|
||||
@inject(ExecutableService)
|
||||
private readonly executableService: ExecutableService;
|
||||
|
||||
@inject(ArduinoDaemon)
|
||||
private readonly daemon: ArduinoDaemon;
|
||||
|
||||
@inject(BoardsService)
|
||||
private readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
private readonly boardDataStore: BoardsDataStore;
|
||||
|
||||
@@ -129,6 +186,10 @@ export class InoLanguage extends SketchContribution {
|
||||
if (!port) {
|
||||
return;
|
||||
}
|
||||
const portNumber = Number.parseInt(port, 10); // TODO: IDE2 APIs should provide a number and not string
|
||||
if (Number.isNaN(portNumber)) {
|
||||
return;
|
||||
}
|
||||
const release = await this.languageServerStartMutex.acquire();
|
||||
const toDisposeOnRelease = new DisposableCollection();
|
||||
try {
|
||||
@@ -197,22 +258,22 @@ export class InoLanguage extends SketchContribution {
|
||||
);
|
||||
toDisposeOnRelease.push(Disposable.create(() => clearTimeout(timer)));
|
||||
}),
|
||||
this.commandService.executeCommand<string>(
|
||||
'arduino.languageserver.start',
|
||||
{
|
||||
lsPath,
|
||||
cliDaemonAddr: `localhost:${port}`,
|
||||
clangdPath,
|
||||
log: currentSketchPath ? currentSketchPath : log,
|
||||
cliDaemonInstance: '1',
|
||||
board: {
|
||||
fqbn: fqbnWithConfig,
|
||||
name: name ? `"${name}"` : undefined,
|
||||
},
|
||||
realTimeDiagnostics,
|
||||
silentOutput: true,
|
||||
}
|
||||
),
|
||||
this.start({
|
||||
lsPath,
|
||||
daemonAddress: {
|
||||
hostname: 'localhost',
|
||||
port: portNumber,
|
||||
instance: 1, // TODO: get it from the backend
|
||||
},
|
||||
clangdPath,
|
||||
log: currentSketchPath ? currentSketchPath : log,
|
||||
board: {
|
||||
fqbn: fqbnWithConfig,
|
||||
name,
|
||||
},
|
||||
realTimeDiagnostics,
|
||||
silentOutput: true,
|
||||
}),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
|
||||
@@ -222,4 +283,13 @@ export class InoLanguage extends SketchContribution {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
private async start(
|
||||
params: StartLanguageServerParams
|
||||
): Promise<StartLanguageServerResult | undefined> {
|
||||
return this.commandService.executeCommand<StartLanguageServerResult>(
|
||||
'arduino.languageserver.start',
|
||||
params
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user