Skip to content

Commit

Permalink
feat: use new debug -I -P CLI output
Browse files Browse the repository at this point in the history
 - 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>
  • Loading branch information
Akos Kitta authored and kittaakos committed Dec 13, 2023
1 parent 42bf1a0 commit 73b6dc4
Show file tree
Hide file tree
Showing 48 changed files with 4,697 additions and 3,041 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
'electron-app/src-gen/*',
'electron-app/gen-webpack*.js',
'!electron-app/webpack.config.js',
'plugins/*',
'electron-app/plugins/*',
'arduino-ide-extension/src/node/cli-protocol',
'**/lib/*',
],
Expand Down
3 changes: 2 additions & 1 deletion arduino-ide-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"deepmerge": "^4.2.2",
"drivelist": "^9.2.4",
"electron-updater": "^4.6.5",
"fast-deep-equal": "^3.1.3",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1",
"filename-reserved-regex": "^2.0.0",
Expand Down Expand Up @@ -169,7 +170,7 @@
],
"arduino": {
"arduino-cli": {
"version": "0.34.0"
"version": "0.35.0-rc.7"
},
"arduino-fwuploader": {
"version": "2.4.1"
Expand Down
35 changes: 34 additions & 1 deletion arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
Expand Down Expand Up @@ -361,6 +361,16 @@ import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } fro
import { SelectionService } from '@theia/core/lib/common/selection-service';
import { CommandService } from '@theia/core/lib/common/command';
import { CorePreferences } from '@theia/core/lib/browser/core-preferences';
import { AutoSelectProgrammer } from './contributions/auto-select-programmer';
import { HostedPluginSupport } from './hosted/hosted-plugin-support';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widget';
import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';

// Hack to fix copy/cut/paste issue after electron version update in Theia.
// https://github.com/eclipse-theia/theia/issues/12487
Expand Down Expand Up @@ -756,6 +766,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, CreateCloudCopy);
Contribution.configure(bind, UpdateArduinoState);
Contribution.configure(bind, BoardsDataMenuUpdater);
Contribution.configure(bind, AutoSelectProgrammer);

bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
Expand Down Expand Up @@ -857,6 +868,28 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// To be able to use a `launch.json` from outside of the workspace.
bind(DebugConfigurationManager).toSelf().inSingletonScope();
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
// To update the currently selected debug config <select> option when starting a debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
// Customized debug widget with its customized config <select> to update it programmatically.
bind(WidgetFactory)
.toDynamicValue(({ container }) => ({
id: DebugWidget.ID,
createWidget: () => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = container;
child.bind(DebugViewModel).toSelf();
child.bind(DebugToolBar).toSelf();
child.bind(DebugSessionWidget).toSelf();
child.bind(DebugConfigurationWidget).toSelf(); // with the patched select
child // use the customized one in the Theia DI
.bind(TheiaDebugConfigurationWidget)
.toService(DebugConfigurationWidget);
child.bind(DebugWidget).toSelf();
return child.get(DebugWidget);
},
}))
.inSingletonScope();

// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
bind(WidgetManager).toSelf().inSingletonScope();
Expand Down
53 changes: 34 additions & 19 deletions arduino-ide-extension/src/browser/boards/boards-data-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone, deepFreeze } from '@theia/core/lib/common/objects';
import type { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import {
BoardDetails,
BoardsService,
ConfigOption,
ConfigValue,
Programmer,
isBoardIdentifierChangeEvent,
isProgrammer,
} from '../../common/protocol';
import { notEmpty } from '../../common/utils';
import type {
Expand Down Expand Up @@ -74,7 +77,7 @@ export class BoardsDataStore
const storedData =
await this.storageService.getData<BoardsDataStore.Data>(key);
if (!storedData) {
// if not previously value is available for the board, do not update the cache
// if no previously value is available for the board, do not update the cache
continue;
}
const details = await this.loadBoardDetails(fqbn);
Expand All @@ -88,6 +91,13 @@ export class BoardsDataStore
this.fireChanged(...changes);
}
}),
this.onDidChange((event) => {
const selectedFqbn =
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
if (event.changes.find((change) => change.fqbn === selectedFqbn)) {
this.updateSelectedBoardData(selectedFqbn);
}
}),
]);

Promise.all([
Expand Down Expand Up @@ -174,7 +184,7 @@ export class BoardsDataStore
return storedData;
}

const boardDetails = await this.getBoardDetailsSafe(fqbn);
const boardDetails = await this.loadBoardDetails(fqbn);
if (!boardDetails) {
return BoardsDataStore.Data.EMPTY;
}
Expand Down Expand Up @@ -220,11 +230,12 @@ export class BoardsDataStore
}
let updated = false;
for (const value of configOption.values) {
if (value.value === selectedValue) {
(value as any).selected = true;
const mutable: Mutable<ConfigValue> = value;
if (mutable.value === selectedValue) {
mutable.selected = true;
updated = true;
} else {
(value as any).selected = false;
mutable.selected = false;
}
}
if (!updated) {
Expand All @@ -245,9 +256,7 @@ export class BoardsDataStore
return `.arduinoIDE-configOptions-${fqbn}`;
}

protected async getBoardDetailsSafe(
fqbn: string
): Promise<BoardDetails | undefined> {
async loadBoardDetails(fqbn: string): Promise<BoardDetails | undefined> {
try {
const details = await this.boardsService.getBoardDetails({ fqbn });
return details;
Expand Down Expand Up @@ -280,21 +289,24 @@ export namespace BoardsDataStore {
readonly configOptions: ConfigOption[];
readonly programmers: Programmer[];
readonly selectedProgrammer?: Programmer;
readonly defaultProgrammerId?: string;
}
export namespace Data {
export const EMPTY: Data = deepFreeze({
configOptions: [],
programmers: [],
defaultProgrammerId: undefined,
});

export function is(arg: unknown): arg is Data {
return (
!!arg &&
'configOptions' in arg &&
Array.isArray(arg['configOptions']) &&
'programmers' in arg &&
Array.isArray(arg['programmers'])
typeof arg === 'object' &&
arg !== null &&
Array.isArray((<Data>arg).configOptions) &&
Array.isArray((<Data>arg).programmers) &&
((<Data>arg).selectedProgrammer === undefined ||
isProgrammer((<Data>arg).selectedProgrammer)) &&
((<Data>arg).defaultProgrammerId === undefined ||
typeof (<Data>arg).defaultProgrammerId === 'string')
);
}
}
Expand All @@ -304,7 +316,8 @@ export function isEmptyData(data: BoardsDataStore.Data): boolean {
return (
Boolean(!data.configOptions.length) &&
Boolean(!data.programmers.length) &&
Boolean(!data.selectedProgrammer)
Boolean(!data.selectedProgrammer) &&
Boolean(!data.defaultProgrammerId)
);
}

Expand All @@ -324,16 +337,18 @@ export function findDefaultProgrammer(
function createDataStoreEntry(details: BoardDetails): BoardsDataStore.Data {
const configOptions = details.configOptions.slice();
const programmers = details.programmers.slice();
const { defaultProgrammerId } = details;
const selectedProgrammer = findDefaultProgrammer(
programmers,
details.defaultProgrammerId
defaultProgrammerId
);
return {
const data = {
configOptions,
programmers,
defaultProgrammerId: details.defaultProgrammerId,
selectedProgrammer,
...(selectedProgrammer ? { selectedProgrammer } : {}),
...(defaultProgrammerId ? { defaultProgrammerId } : {}),
};
return data;
}

export interface BoardsDataStoreChange {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 73b6dc4

Please sign in to comment.