Skip to content

Commit 35ca43a

Browse files
author
Akos Kitta
committed
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 <[email protected]>
1 parent e149eaa commit 35ca43a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+4697
-3041
lines changed

.eslintrc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module.exports = {
1818
'electron-app/src-gen/*',
1919
'electron-app/gen-webpack*.js',
2020
'!electron-app/webpack.config.js',
21-
'plugins/*',
21+
'electron-app/plugins/*',
2222
'arduino-ide-extension/src/node/cli-protocol',
2323
'**/lib/*',
2424
],

arduino-ide-extension/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"deepmerge": "^4.2.2",
7070
"drivelist": "^9.2.4",
7171
"electron-updater": "^4.6.5",
72+
"fast-deep-equal": "^3.1.3",
7273
"fast-json-stable-stringify": "^2.1.0",
7374
"fast-safe-stringify": "^2.1.1",
7475
"filename-reserved-regex": "^2.0.0",
@@ -169,7 +170,7 @@
169170
],
170171
"arduino": {
171172
"arduino-cli": {
172-
"version": "0.34.0"
173+
"version": "0.35.0-rc.7"
173174
},
174175
"arduino-fwuploader": {
175176
"version": "2.4.1"

arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import '../../src/browser/style/index.css';
2-
import { ContainerModule } from '@theia/core/shared/inversify';
2+
import { Container, ContainerModule } from '@theia/core/shared/inversify';
33
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
44
import { CommandContribution } from '@theia/core/lib/common/command';
55
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
@@ -361,6 +361,16 @@ import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } fro
361361
import { SelectionService } from '@theia/core/lib/common/selection-service';
362362
import { CommandService } from '@theia/core/lib/common/command';
363363
import { CorePreferences } from '@theia/core/lib/browser/core-preferences';
364+
import { AutoSelectProgrammer } from './contributions/auto-select-programmer';
365+
import { HostedPluginSupport } from './hosted/hosted-plugin-support';
366+
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
367+
import { DebugSessionManager } from './theia/debug/debug-session-manager';
368+
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
369+
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
370+
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
371+
import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widget';
372+
import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
373+
import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
364374

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

760771
bindContributionProvider(bind, StartupTaskProvider);
761772
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@@ -857,6 +868,28 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
857868
// To be able to use a `launch.json` from outside of the workspace.
858869
bind(DebugConfigurationManager).toSelf().inSingletonScope();
859870
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
871+
// To update the currently selected debug config <select> option when starting a debug session.
872+
bind(DebugSessionManager).toSelf().inSingletonScope();
873+
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
874+
// Customized debug widget with its customized config <select> to update it programmatically.
875+
bind(WidgetFactory)
876+
.toDynamicValue(({ container }) => ({
877+
id: DebugWidget.ID,
878+
createWidget: () => {
879+
const child = new Container({ defaultScope: 'Singleton' });
880+
child.parent = container;
881+
child.bind(DebugViewModel).toSelf();
882+
child.bind(DebugToolBar).toSelf();
883+
child.bind(DebugSessionWidget).toSelf();
884+
child.bind(DebugConfigurationWidget).toSelf(); // with the patched select
885+
child // use the customized one in the Theia DI
886+
.bind(TheiaDebugConfigurationWidget)
887+
.toService(DebugConfigurationWidget);
888+
child.bind(DebugWidget).toSelf();
889+
return child.get(DebugWidget);
890+
},
891+
}))
892+
.inSingletonScope();
860893

861894
// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
862895
bind(WidgetManager).toSelf().inSingletonScope();

arduino-ide-extension/src/browser/boards/boards-data-store.ts

+34-19
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
1010
import { Emitter, Event } from '@theia/core/lib/common/event';
1111
import { ILogger } from '@theia/core/lib/common/logger';
1212
import { deepClone, deepFreeze } from '@theia/core/lib/common/objects';
13+
import type { Mutable } from '@theia/core/lib/common/types';
1314
import { inject, injectable, named } from '@theia/core/shared/inversify';
1415
import {
1516
BoardDetails,
1617
BoardsService,
1718
ConfigOption,
19+
ConfigValue,
1820
Programmer,
1921
isBoardIdentifierChangeEvent,
22+
isProgrammer,
2023
} from '../../common/protocol';
2124
import { notEmpty } from '../../common/utils';
2225
import type {
@@ -74,7 +77,7 @@ export class BoardsDataStore
7477
const storedData =
7578
await this.storageService.getData<BoardsDataStore.Data>(key);
7679
if (!storedData) {
77-
// if not previously value is available for the board, do not update the cache
80+
// if no previously value is available for the board, do not update the cache
7881
continue;
7982
}
8083
const details = await this.loadBoardDetails(fqbn);
@@ -88,6 +91,13 @@ export class BoardsDataStore
8891
this.fireChanged(...changes);
8992
}
9093
}),
94+
this.onDidChange((event) => {
95+
const selectedFqbn =
96+
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
97+
if (event.changes.find((change) => change.fqbn === selectedFqbn)) {
98+
this.updateSelectedBoardData(selectedFqbn);
99+
}
100+
}),
91101
]);
92102

93103
Promise.all([
@@ -174,7 +184,7 @@ export class BoardsDataStore
174184
return storedData;
175185
}
176186

177-
const boardDetails = await this.getBoardDetailsSafe(fqbn);
187+
const boardDetails = await this.loadBoardDetails(fqbn);
178188
if (!boardDetails) {
179189
return BoardsDataStore.Data.EMPTY;
180190
}
@@ -220,11 +230,12 @@ export class BoardsDataStore
220230
}
221231
let updated = false;
222232
for (const value of configOption.values) {
223-
if (value.value === selectedValue) {
224-
(value as any).selected = true;
233+
const mutable: Mutable<ConfigValue> = value;
234+
if (mutable.value === selectedValue) {
235+
mutable.selected = true;
225236
updated = true;
226237
} else {
227-
(value as any).selected = false;
238+
mutable.selected = false;
228239
}
229240
}
230241
if (!updated) {
@@ -245,9 +256,7 @@ export class BoardsDataStore
245256
return `.arduinoIDE-configOptions-${fqbn}`;
246257
}
247258

248-
protected async getBoardDetailsSafe(
249-
fqbn: string
250-
): Promise<BoardDetails | undefined> {
259+
async loadBoardDetails(fqbn: string): Promise<BoardDetails | undefined> {
251260
try {
252261
const details = await this.boardsService.getBoardDetails({ fqbn });
253262
return details;
@@ -280,21 +289,24 @@ export namespace BoardsDataStore {
280289
readonly configOptions: ConfigOption[];
281290
readonly programmers: Programmer[];
282291
readonly selectedProgrammer?: Programmer;
292+
readonly defaultProgrammerId?: string;
283293
}
284294
export namespace Data {
285295
export const EMPTY: Data = deepFreeze({
286296
configOptions: [],
287297
programmers: [],
288-
defaultProgrammerId: undefined,
289298
});
290299

291300
export function is(arg: unknown): arg is Data {
292301
return (
293-
!!arg &&
294-
'configOptions' in arg &&
295-
Array.isArray(arg['configOptions']) &&
296-
'programmers' in arg &&
297-
Array.isArray(arg['programmers'])
302+
typeof arg === 'object' &&
303+
arg !== null &&
304+
Array.isArray((<Data>arg).configOptions) &&
305+
Array.isArray((<Data>arg).programmers) &&
306+
((<Data>arg).selectedProgrammer === undefined ||
307+
isProgrammer((<Data>arg).selectedProgrammer)) &&
308+
((<Data>arg).defaultProgrammerId === undefined ||
309+
typeof (<Data>arg).defaultProgrammerId === 'string')
298310
);
299311
}
300312
}
@@ -304,7 +316,8 @@ export function isEmptyData(data: BoardsDataStore.Data): boolean {
304316
return (
305317
Boolean(!data.configOptions.length) &&
306318
Boolean(!data.programmers.length) &&
307-
Boolean(!data.selectedProgrammer)
319+
Boolean(!data.selectedProgrammer) &&
320+
Boolean(!data.defaultProgrammerId)
308321
);
309322
}
310323

@@ -324,16 +337,18 @@ export function findDefaultProgrammer(
324337
function createDataStoreEntry(details: BoardDetails): BoardsDataStore.Data {
325338
const configOptions = details.configOptions.slice();
326339
const programmers = details.programmers.slice();
340+
const { defaultProgrammerId } = details;
327341
const selectedProgrammer = findDefaultProgrammer(
328342
programmers,
329-
details.defaultProgrammerId
343+
defaultProgrammerId
330344
);
331-
return {
345+
const data = {
332346
configOptions,
333347
programmers,
334-
defaultProgrammerId: details.defaultProgrammerId,
335-
selectedProgrammer,
348+
...(selectedProgrammer ? { selectedProgrammer } : {}),
349+
...(defaultProgrammerId ? { defaultProgrammerId } : {}),
336350
};
351+
return data;
337352
}
338353

339354
export interface BoardsDataStoreChange {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { MaybePromise } from '@theia/core/lib/common/types';
2+
import { inject, injectable } from '@theia/core/shared/inversify';
3+
import {
4+
BoardDetails,
5+
Programmer,
6+
isBoardIdentifierChangeEvent,
7+
} from '../../common/protocol';
8+
import {
9+
BoardsDataStore,
10+
findDefaultProgrammer,
11+
isEmptyData,
12+
} from '../boards/boards-data-store';
13+
import { BoardsServiceProvider } from '../boards/boards-service-provider';
14+
import { Contribution } from './contribution';
15+
16+
/**
17+
* Before CLI 0.35.0-rc.3, there was no `programmer#default` property in the `board details` response.
18+
* This method does the programmer migration in the data store. If there is a programmer selected, it's a noop.
19+
* If no programmer is selected, it forcefully reloads the details from the CLI and updates it in the local storage.
20+
*/
21+
@injectable()
22+
export class AutoSelectProgrammer extends Contribution {
23+
@inject(BoardsServiceProvider)
24+
private readonly boardsServiceProvider: BoardsServiceProvider;
25+
@inject(BoardsDataStore)
26+
private readonly boardsDataStore: BoardsDataStore;
27+
28+
override onStart(): void {
29+
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
30+
if (isBoardIdentifierChangeEvent(event)) {
31+
this.ensureProgrammerIsSelected();
32+
}
33+
});
34+
}
35+
36+
override onReady(): void {
37+
this.boardsServiceProvider.ready.then(() =>
38+
this.ensureProgrammerIsSelected()
39+
);
40+
}
41+
42+
private async ensureProgrammerIsSelected(): Promise<boolean> {
43+
return ensureProgrammerIsSelected({
44+
fqbn: this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
45+
getData: (fqbn) => this.boardsDataStore.getData(fqbn),
46+
loadBoardDetails: (fqbn) => this.boardsDataStore.loadBoardDetails(fqbn),
47+
selectProgrammer: (arg) => this.boardsDataStore.selectProgrammer(arg),
48+
});
49+
}
50+
}
51+
52+
interface EnsureProgrammerIsSelectedParams {
53+
fqbn: string | undefined;
54+
getData: (fqbn: string | undefined) => MaybePromise<BoardsDataStore.Data>;
55+
loadBoardDetails: (fqbn: string) => MaybePromise<BoardDetails | undefined>;
56+
selectProgrammer(options: {
57+
fqbn: string;
58+
selectedProgrammer: Programmer;
59+
}): MaybePromise<boolean>;
60+
}
61+
62+
export async function ensureProgrammerIsSelected(
63+
params: EnsureProgrammerIsSelectedParams
64+
): Promise<boolean> {
65+
const { fqbn, getData, loadBoardDetails, selectProgrammer } = params;
66+
if (!fqbn) {
67+
return false;
68+
}
69+
console.debug(`Ensuring a programmer is selected for ${fqbn}...`);
70+
const data = await getData(fqbn);
71+
if (isEmptyData(data)) {
72+
// For example, the platform is not installed.
73+
console.debug(`Skipping. No boards data is available for ${fqbn}.`);
74+
return false;
75+
}
76+
if (data.selectedProgrammer) {
77+
console.debug(
78+
`A programmer is already selected for ${fqbn}: '${data.selectedProgrammer.id}'.`
79+
);
80+
return true;
81+
}
82+
let programmer = findDefaultProgrammer(data.programmers, data);
83+
if (programmer) {
84+
// select the programmer if the default info is available
85+
const result = await selectProgrammer({
86+
fqbn,
87+
selectedProgrammer: programmer,
88+
});
89+
if (result) {
90+
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
91+
return result;
92+
}
93+
}
94+
console.debug(`Reloading board details for ${fqbn}...`);
95+
const reloadedData = await loadBoardDetails(fqbn);
96+
if (!reloadedData) {
97+
console.debug(`Skipping. No board details found for ${fqbn}.`);
98+
return false;
99+
}
100+
if (!reloadedData.programmers.length) {
101+
console.debug(`Skipping. ${fqbn} does not have programmers.`);
102+
return false;
103+
}
104+
programmer = findDefaultProgrammer(reloadedData.programmers, reloadedData);
105+
if (!programmer) {
106+
console.debug(
107+
`Skipping. Could not find a default programmer for ${fqbn}. Programmers were: `
108+
);
109+
return false;
110+
}
111+
const result = await selectProgrammer({
112+
fqbn,
113+
selectedProgrammer: programmer,
114+
});
115+
if (result) {
116+
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
117+
} else {
118+
console.debug(
119+
`Could not select '${programmer.id}' programmer for ${fqbn}.`
120+
);
121+
}
122+
return result;
123+
}

0 commit comments

Comments
 (0)