Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 06b2856

Browse files
author
Akos Kitta
committedNov 13, 2023
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`, Signed-off-by: Akos Kitta <[email protected]>
1 parent 503533d commit 06b2856

17 files changed

+544
-115
lines changed
 

‎.eslintrc.js

Lines changed: 1 addition & 1 deletion
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/src/browser/arduino-ide-frontend-module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,8 @@ 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 { SelectProgrammer } from './contributions/select-programmer';
365+
import { AutoSelectProgrammer } from './contributions/auto-select-programmer';
364366

365367
// Hack to fix copy/cut/paste issue after electron version update in Theia.
366368
// https://github.com/eclipse-theia/theia/issues/12487
@@ -753,6 +755,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
753755
Contribution.configure(bind, CreateCloudCopy);
754756
Contribution.configure(bind, UpdateArduinoState);
755757
Contribution.configure(bind, BoardsDataMenuUpdater);
758+
Contribution.configure(bind, SelectProgrammer);
759+
Contribution.configure(bind, AutoSelectProgrammer);
756760

757761
bindContributionProvider(bind, StartupTaskProvider);
758762
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window

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

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
44
import { Emitter, Event } from '@theia/core/lib/common/event';
55
import { ILogger } from '@theia/core/lib/common/logger';
66
import { deepClone } from '@theia/core/lib/common/objects';
7+
import type { Mutable } from '@theia/core/lib/common/types';
78
import { inject, injectable, named } from '@theia/core/shared/inversify';
89
import {
910
BoardDetails,
1011
BoardsService,
1112
ConfigOption,
13+
ConfigValue,
1214
Programmer,
15+
isProgrammer,
1316
} from '../../common/protocol';
1417
import { notEmpty } from '../../common/utils';
1518
import { NotificationCenter } from '../notification-center';
@@ -43,7 +46,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
4346
const key = this.getStorageKey(fqbn);
4447
let data = await this.storageService.getData<ConfigOption[]>(key);
4548
if (!data || !data.length) {
46-
const details = await this.getBoardDetailsSafe(fqbn);
49+
const details = await this.loadBoardDetails(fqbn);
4750
if (details) {
4851
data = details.configOptions;
4952
if (data.length) {
@@ -91,14 +94,15 @@ export class BoardsDataStore implements FrontendApplicationContribution {
9194
return data;
9295
}
9396

94-
const boardDetails = await this.getBoardDetailsSafe(fqbn);
97+
const boardDetails = await this.loadBoardDetails(fqbn);
9598
if (!boardDetails) {
9699
return BoardsDataStore.Data.EMPTY;
97100
}
98101

99102
data = {
100103
configOptions: boardDetails.configOptions,
101104
programmers: boardDetails.programmers,
105+
selectedProgrammer: boardDetails.programmers.find((p) => p.default),
102106
};
103107
await this.storageService.setData(key, data);
104108
return data;
@@ -142,11 +146,12 @@ export class BoardsDataStore implements FrontendApplicationContribution {
142146
}
143147
let updated = false;
144148
for (const value of configOption.values) {
145-
if (value.value === selectedValue) {
146-
(value as any).selected = true;
149+
const mutable: Mutable<ConfigValue> = value;
150+
if (mutable.value === selectedValue) {
151+
mutable.selected = true;
147152
updated = true;
148153
} else {
149-
(value as any).selected = false;
154+
mutable.selected = false;
150155
}
151156
}
152157
if (!updated) {
@@ -172,9 +177,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
172177
return `.arduinoIDE-configOptions-${fqbn}`;
173178
}
174179

175-
protected async getBoardDetailsSafe(
176-
fqbn: string
177-
): Promise<BoardDetails | undefined> {
180+
async loadBoardDetails(fqbn: string): Promise<BoardDetails | undefined> {
178181
try {
179182
const details = this.boardsService.getBoardDetails({ fqbn });
180183
return details;
@@ -213,14 +216,23 @@ export namespace BoardsDataStore {
213216
configOptions: [],
214217
programmers: [],
215218
};
216-
export function is(arg: any): arg is Data {
219+
export function is(arg: unknown): arg is Data {
217220
return (
218-
!!arg &&
219-
'configOptions' in arg &&
220-
Array.isArray(arg['configOptions']) &&
221-
'programmers' in arg &&
222-
Array.isArray(arg['programmers'])
221+
typeof arg === 'object' &&
222+
arg !== null &&
223+
Array.isArray((<Data>arg).configOptions) &&
224+
Array.isArray((<Data>arg).programmers) &&
225+
((<Data>arg).selectedProgrammer === undefined ||
226+
isProgrammer((<Data>arg).selectedProgrammer))
223227
);
224228
}
225229
}
226230
}
231+
232+
export function isEmptyData(data: BoardsDataStore.Data): boolean {
233+
return (
234+
Boolean(!data.configOptions.length) &&
235+
Boolean(!data.programmers.length) &&
236+
Boolean(!data.selectedProgrammer)
237+
);
238+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { inject, injectable } from '@theia/core/shared/inversify';
2+
import { isBoardIdentifierChangeEvent } from '../../common/protocol';
3+
import { BoardsDataStore, isEmptyData } from '../boards/boards-data-store';
4+
import { BoardsServiceProvider } from '../boards/boards-service-provider';
5+
import { Contribution } from './contribution';
6+
7+
/**
8+
* Before CLI 0.35.0-rc.3, there was no `programmer#default` property in the `board details` response.
9+
* This method does the programmer migration in the data store. If there is a programmer selected, it's a noop.
10+
* If no programmer is selected, it forcefully reloads the details from the CLI and updates it in the local storage.
11+
*/
12+
@injectable()
13+
export class AutoSelectProgrammer extends Contribution {
14+
@inject(BoardsServiceProvider)
15+
private readonly boardsServiceProvider: BoardsServiceProvider;
16+
@inject(BoardsDataStore)
17+
private readonly boardsDataStore: BoardsDataStore;
18+
19+
override onStart(): void {
20+
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
21+
if (isBoardIdentifierChangeEvent(event)) {
22+
this.ensureProgrammerIsSelected();
23+
}
24+
});
25+
}
26+
27+
override onReady(): void {
28+
this.boardsServiceProvider.ready.then(() =>
29+
this.ensureProgrammerIsSelected()
30+
);
31+
}
32+
33+
private async ensureProgrammerIsSelected(): Promise<void> {
34+
const fqbn = this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
35+
if (!fqbn) {
36+
return;
37+
}
38+
console.debug(`Ensuring a programmer is selected for ${fqbn}...`);
39+
const data = await this.boardsDataStore.getData(fqbn);
40+
if (isEmptyData(data)) {
41+
// For example, the platform is not installed.
42+
console.debug(`Skipping. No boards data is available for ${fqbn}.`);
43+
return;
44+
}
45+
if (data.selectedProgrammer) {
46+
console.debug(
47+
`A programmer is already selected for ${fqbn}: '${data.selectedProgrammer.id}'.`
48+
);
49+
return;
50+
}
51+
let programmer = data.programmers.find((p) => p.default);
52+
if (programmer) {
53+
// select the programmer if the default info is available
54+
await this.boardsDataStore.selectProgrammer({
55+
fqbn,
56+
selectedProgrammer: programmer,
57+
});
58+
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
59+
return;
60+
}
61+
console.debug(`Reloading board details for ${fqbn}...`);
62+
const reloadedData = await this.boardsDataStore.loadBoardDetails(fqbn);
63+
if (!reloadedData) {
64+
console.debug(`Skipping. No board details found for ${fqbn}.`);
65+
return;
66+
}
67+
if (!reloadedData.programmers.length) {
68+
console.debug(`Skipping. ${fqbn} does not have programmers.`);
69+
return;
70+
}
71+
programmer = reloadedData.programmers.find((p) => p.default);
72+
if (!programmer) {
73+
console.debug(
74+
`Skipping. Could not find a default programmer for ${fqbn}. Programmers were: `
75+
);
76+
return;
77+
}
78+
const result = await this.boardsDataStore.selectProgrammer({
79+
fqbn,
80+
selectedProgrammer: programmer,
81+
});
82+
if (result) {
83+
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
84+
} else {
85+
console.debug(
86+
`Could not select '${programmer.id}' programmer for ${fqbn}.`
87+
);
88+
}
89+
}
90+
}

‎arduino-ide-extension/src/browser/contributions/debug.ts

Lines changed: 120 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,81 @@
1+
import { Emitter, Event } from '@theia/core/lib/common/event';
2+
import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry';
3+
import { nls } from '@theia/core/lib/common/nls';
14
import { inject, injectable } from '@theia/core/shared/inversify';
2-
import { Event, Emitter } from '@theia/core/lib/common/event';
35
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
4-
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
5-
import { NotificationCenter } from '../notification-center';
6+
import { SelectManually } from '../../common/nls';
67
import {
78
Board,
89
BoardIdentifier,
910
BoardsService,
1011
ExecutableService,
12+
SketchRef,
1113
isBoardIdentifierChangeEvent,
12-
Sketch,
14+
isProgrammer,
1315
} from '../../common/protocol';
16+
import { BoardsDataStore } from '../boards/boards-data-store';
1417
import { BoardsServiceProvider } from '../boards/boards-service-provider';
18+
import { ArduinoMenus } from '../menu/arduino-menus';
19+
import { NotificationCenter } from '../notification-center';
20+
import { CurrentSketch } from '../sketches-service-client-impl';
21+
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
1522
import {
16-
URI,
1723
Command,
1824
CommandRegistry,
1925
SketchContribution,
2026
TabBarToolbarRegistry,
27+
URI,
2128
} from './contribution';
22-
import { MenuModelRegistry, nls } from '@theia/core/lib/common';
23-
import { CurrentSketch } from '../sketches-service-client-impl';
24-
import { ArduinoMenus } from '../menu/arduino-menus';
2529

2630
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
2731

32+
interface StartDebugParams {
33+
/**
34+
* Absolute filesystem path to the Arduino CLI executable.
35+
*/
36+
readonly cliPath: string;
37+
/**
38+
* The the board to debug.
39+
*/
40+
readonly board: Readonly<{ fqbn: string; name?: string }>;
41+
/**
42+
* Absolute filesystem path of the sketch to debug.
43+
*/
44+
readonly sketchPath: string;
45+
/**
46+
* Location where the `launch.json` will be created on the fly before starting every debug session.
47+
* If not defined, it falls back to `sketchPath/.vscode/launch.json`.
48+
*/
49+
readonly launchConfigsDirPath?: string;
50+
/**
51+
* Absolute path to the `arduino-cli.yaml` file. If not specified, it falls back to `~/.arduinoIDE/arduino-cli.yaml`.
52+
*/
53+
readonly cliConfigPath?: string;
54+
/**
55+
* Programmer for the debugging.
56+
*/
57+
readonly programmer?: string;
58+
/**
59+
* Custom progress message to use when getting the debug information from the CLI.
60+
*/
61+
readonly message?: string;
62+
}
63+
type StartDebugResult = boolean;
64+
2865
@injectable()
2966
export class Debug extends SketchContribution {
3067
@inject(HostedPluginSupport)
3168
private readonly hostedPluginSupport: HostedPluginSupport;
32-
3369
@inject(NotificationCenter)
3470
private readonly notificationCenter: NotificationCenter;
35-
3671
@inject(ExecutableService)
3772
private readonly executableService: ExecutableService;
38-
3973
@inject(BoardsService)
4074
private readonly boardService: BoardsService;
41-
4275
@inject(BoardsServiceProvider)
4376
private readonly boardsServiceProvider: BoardsServiceProvider;
77+
@inject(BoardsDataStore)
78+
private readonly boardsDataStore: BoardsDataStore;
4479

4580
/**
4681
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
@@ -175,44 +210,37 @@ export class Debug extends SketchContribution {
175210
private async startDebug(
176211
board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig
177212
.selectedBoard
178-
): Promise<void> {
179-
if (!board) {
180-
return;
181-
}
182-
const { name, fqbn } = board;
183-
if (!fqbn) {
184-
return;
213+
): Promise<StartDebugResult> {
214+
const params = await this.createStartDebugParams(board);
215+
if (!params) {
216+
return false;
185217
}
186218
await this.hostedPluginSupport.didStart;
187-
const [sketch, executables] = await Promise.all([
188-
this.sketchServiceClient.currentSketch(),
189-
this.executableService.list(),
190-
]);
191-
if (!CurrentSketch.isValid(sketch)) {
192-
return;
193-
}
194-
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
195-
sketch
196-
);
197-
const [cliPath, sketchPath, configPath] = await Promise.all([
198-
this.fileService.fsPath(new URI(executables.cliUri)),
199-
this.fileService.fsPath(new URI(sketch.uri)),
200-
this.fileService.fsPath(new URI(ideTempFolderUri)),
201-
]);
202-
const config = {
203-
cliPath,
204-
board: {
205-
fqbn,
206-
name,
207-
},
208-
sketchPath,
209-
configPath,
210-
};
211219
try {
212-
await this.commandService.executeCommand('arduino.debug.start', config);
220+
const result = await this.debug(params);
221+
return Boolean(result);
213222
} catch (err) {
214-
if (await this.isSketchNotVerifiedError(err, sketch)) {
215-
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
223+
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
224+
const sketchUri = await this.fileSystemExt.getUri(params.sketchPath);
225+
const sketch = SketchRef.fromUri(sketchUri);
226+
if (err instanceof Error && /missing programmer/gi.test(err.message)) {
227+
const answer = await this.messageService.warn(
228+
nls.localize(
229+
'arduino/debug/programmerNotSelected',
230+
'The debugger requires a programmer. Do you want to select a programmer? You can select it manually from the Tools > Programmer menu.'
231+
),
232+
SelectManually,
233+
yes
234+
);
235+
if (answer === yes) {
236+
const result = await this.commandService.executeCommand(
237+
'arduino-select-programmer'
238+
);
239+
if (isProgrammer(result)) {
240+
return this.startDebug();
241+
}
242+
}
243+
} else if (await this.isSketchNotVerifiedError(err, sketch)) {
216244
const answer = await this.messageService.error(
217245
nls.localize(
218246
'arduino/debug/sketchIsNotCompiled',
@@ -230,6 +258,16 @@ export class Debug extends SketchContribution {
230258
);
231259
}
232260
}
261+
return false;
262+
}
263+
264+
private async debug(
265+
params: StartDebugParams
266+
): Promise<StartDebugResult | undefined> {
267+
return this.commandService.executeCommand<StartDebugResult>(
268+
'arduino.debug.start',
269+
params
270+
);
233271
}
234272

235273
get compileForDebug(): boolean {
@@ -246,7 +284,7 @@ export class Debug extends SketchContribution {
246284

247285
private async isSketchNotVerifiedError(
248286
err: unknown,
249-
sketch: Sketch
287+
sketch: SketchRef
250288
): Promise<boolean> {
251289
if (err instanceof Error) {
252290
try {
@@ -260,6 +298,41 @@ export class Debug extends SketchContribution {
260298
}
261299
return false;
262300
}
301+
302+
private async createStartDebugParams(
303+
board: BoardIdentifier | undefined
304+
): Promise<StartDebugParams | undefined> {
305+
if (!board || !board.fqbn) {
306+
return undefined;
307+
}
308+
const [sketch, executables, boardsData] = await Promise.all([
309+
this.sketchServiceClient.currentSketch(),
310+
this.executableService.list(),
311+
this.boardsDataStore.getData(board.fqbn),
312+
]);
313+
if (!CurrentSketch.isValid(sketch)) {
314+
return;
315+
}
316+
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
317+
sketch
318+
);
319+
const [cliPath, sketchPath, launchConfigsDirPath] = await Promise.all([
320+
this.fileService.fsPath(new URI(executables.cliUri)),
321+
this.fileService.fsPath(new URI(sketch.uri)),
322+
this.fileService.fsPath(new URI(ideTempFolderUri)),
323+
]);
324+
return {
325+
board: { fqbn: board.fqbn, name: board.name },
326+
cliPath,
327+
sketchPath,
328+
launchConfigsDirPath,
329+
programmer: boardsData.selectedProgrammer?.id,
330+
message: nls.localize(
331+
'arduino/debug/getDebugInfo',
332+
'Getting debug info...'
333+
),
334+
};
335+
}
263336
}
264337
export namespace Debug {
265338
export namespace Commands {

‎arduino-ide-extension/src/browser/contributions/ino-language.ts

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,83 @@ import { NotificationCenter } from '../notification-center';
2020
import { SketchContribution, URI } from './contribution';
2121
import { BoardsDataStore } from '../boards/boards-data-store';
2222

23+
interface DaemonAddress {
24+
/**
25+
* The host where the Arduino CLI daemon is available.
26+
*/
27+
readonly hostname: string;
28+
/**
29+
* The port where the Arduino CLI daemon is listening.
30+
*/
31+
readonly port: number;
32+
/**
33+
* The [id](https://arduino.github.io/arduino-cli/latest/rpc/commands/#instance) of the initialized core Arduino client instance.
34+
*/
35+
readonly instance: number;
36+
}
37+
38+
interface StartLanguageServerParams {
39+
/**
40+
* Absolute filesystem path to the Arduino Language Server executable.
41+
*/
42+
readonly lsPath: string;
43+
/**
44+
* The hostname and the port for the gRPC channel connecting to the Arduino CLI daemon.
45+
* The `instance` number is for the initialized core Arduino client.
46+
*/
47+
readonly daemonAddress: DaemonAddress;
48+
/**
49+
* Absolute filesystem path to [`clangd`](https://clangd.llvm.org/).
50+
*/
51+
readonly clangdPath: string;
52+
/**
53+
* The board is relevant to start a specific "flavor" of the language.
54+
*/
55+
readonly board: { fqbn: string; name?: string };
56+
/**
57+
* `true` if the LS should generate the log files into the default location. The default location is the `cwd` of the process.
58+
* It's very often the same as the workspace root of the IDE, aka the sketch folder.
59+
* When it is a string, it is the absolute filesystem path to the folder to generate the log files.
60+
* If `string`, but the path is inaccessible, the log files will be generated into the default location.
61+
*/
62+
readonly log?: boolean | string;
63+
/**
64+
* Optional `env` for the language server process.
65+
*/
66+
readonly env?: NodeJS.ProcessEnv;
67+
/**
68+
* Additional flags for the Arduino Language server process.
69+
*/
70+
readonly flags?: readonly string[];
71+
/**
72+
* Set to `true`, to enable `Diagnostics`.
73+
*/
74+
readonly realTimeDiagnostics?: boolean;
75+
/**
76+
* If `true`, the logging is not forwarded to the _Output_ view via the language client.
77+
*/
78+
readonly silentOutput?: boolean;
79+
}
80+
81+
/**
82+
* The FQBN the language server runs with or `undefined` if it could not start.
83+
*/
84+
type StartLanguageServerResult = string | undefined;
85+
2386
@injectable()
2487
export class InoLanguage extends SketchContribution {
2588
@inject(HostedPluginEvents)
2689
private readonly hostedPluginEvents: HostedPluginEvents;
27-
2890
@inject(ExecutableService)
2991
private readonly executableService: ExecutableService;
30-
3192
@inject(ArduinoDaemon)
3293
private readonly daemon: ArduinoDaemon;
33-
3494
@inject(BoardsService)
3595
private readonly boardsService: BoardsService;
36-
3796
@inject(BoardsServiceProvider)
3897
private readonly boardsServiceProvider: BoardsServiceProvider;
39-
4098
@inject(NotificationCenter)
4199
private readonly notificationCenter: NotificationCenter;
42-
43100
@inject(BoardsDataStore)
44101
private readonly boardDataStore: BoardsDataStore;
45102

@@ -129,6 +186,10 @@ export class InoLanguage extends SketchContribution {
129186
if (!port) {
130187
return;
131188
}
189+
const portNumber = Number.parseInt(port, 10); // TODO: IDE2 APIs should provide a number and not string
190+
if (Number.isNaN(portNumber)) {
191+
return;
192+
}
132193
const release = await this.languageServerStartMutex.acquire();
133194
const toDisposeOnRelease = new DisposableCollection();
134195
try {
@@ -197,22 +258,22 @@ export class InoLanguage extends SketchContribution {
197258
);
198259
toDisposeOnRelease.push(Disposable.create(() => clearTimeout(timer)));
199260
}),
200-
this.commandService.executeCommand<string>(
201-
'arduino.languageserver.start',
202-
{
203-
lsPath,
204-
cliDaemonAddr: `localhost:${port}`,
205-
clangdPath,
206-
log: currentSketchPath ? currentSketchPath : log,
207-
cliDaemonInstance: '1',
208-
board: {
209-
fqbn: fqbnWithConfig,
210-
name: name ? `"${name}"` : undefined,
211-
},
212-
realTimeDiagnostics,
213-
silentOutput: true,
214-
}
215-
),
261+
this.start({
262+
lsPath,
263+
daemonAddress: {
264+
hostname: 'localhost',
265+
port: portNumber,
266+
instance: 1, // TODO: get it from the backend
267+
},
268+
clangdPath,
269+
log: currentSketchPath ? currentSketchPath : log,
270+
board: {
271+
fqbn: fqbnWithConfig,
272+
name,
273+
},
274+
realTimeDiagnostics,
275+
silentOutput: true,
276+
}),
216277
]);
217278
} catch (e) {
218279
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
@@ -222,4 +283,13 @@ export class InoLanguage extends SketchContribution {
222283
release();
223284
}
224285
}
286+
287+
private async start(
288+
params: StartLanguageServerParams
289+
): Promise<StartLanguageServerResult | undefined> {
290+
return this.commandService.executeCommand<StartLanguageServerResult>(
291+
'arduino.languageserver.start',
292+
params
293+
);
294+
}
225295
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { nls } from '@theia/core/lib/common/nls';
2+
import {
3+
QuickPickItem,
4+
QuickPickService,
5+
} from '@theia/core/lib/common/quick-pick-service';
6+
import { inject, injectable } from '@theia/core/shared/inversify';
7+
import { Programmer } from '../../common/protocol';
8+
import { BoardsDataStore } from '../boards/boards-data-store';
9+
import { BoardsServiceProvider } from '../boards/boards-service-provider';
10+
import { CommandRegistry, Contribution } from './contribution';
11+
12+
class ProgrammerQuickPickItem implements QuickPickItem {
13+
constructor(
14+
readonly programmer: Readonly<Programmer>,
15+
readonly label = programmer.name,
16+
readonly description = programmer.id
17+
) {}
18+
}
19+
20+
@injectable()
21+
export class SelectProgrammer extends Contribution {
22+
@inject(BoardsServiceProvider)
23+
private readonly boardsServiceProvider: BoardsServiceProvider;
24+
@inject(BoardsDataStore)
25+
private readonly boardsDataStore: BoardsDataStore;
26+
@inject(QuickPickService)
27+
private readonly quickPickService: QuickPickService;
28+
29+
override registerCommands(registry: CommandRegistry): void {
30+
registry.registerCommand(SelectProgrammer.Commands.SELECT_PROGRAMMER, {
31+
execute: () =>
32+
this.selectProgrammer(
33+
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
34+
),
35+
});
36+
}
37+
38+
private async pickProgrammer(
39+
fqbn: string | undefined
40+
): Promise<Programmer | undefined> {
41+
const { programmers, selectedProgrammer } =
42+
await this.boardsDataStore.getData(fqbn);
43+
if (!programmers.length) {
44+
return undefined;
45+
}
46+
const items = programmers.map((p) => new ProgrammerQuickPickItem(p));
47+
const activeItem = items.find(
48+
(i) => i.programmer.id === selectedProgrammer?.id
49+
);
50+
const selected = await this.quickPickService.show(items, {
51+
activeItem,
52+
placeholder: nls.localize(
53+
'arduino/quickSelectProgrammer',
54+
'Type the programmer. Press Enter to confirm or Escape to cancel.'
55+
),
56+
matchOnDescription: true,
57+
});
58+
return selected?.programmer;
59+
}
60+
61+
private async selectProgrammer(
62+
fqbn: string | undefined
63+
): Promise<Programmer | undefined> {
64+
if (!fqbn) {
65+
return undefined;
66+
}
67+
const programmer = await this.pickProgrammer(fqbn);
68+
if (programmer) {
69+
const ok = await this.boardsDataStore.selectProgrammer({
70+
fqbn,
71+
selectedProgrammer: programmer,
72+
});
73+
if (ok) {
74+
return programmer;
75+
}
76+
}
77+
return undefined;
78+
}
79+
}
80+
81+
export namespace SelectProgrammer {
82+
export namespace Commands {
83+
export const SELECT_PROGRAMMER = {
84+
id: 'arduino-select-programmer',
85+
};
86+
}
87+
}

‎arduino-ide-extension/src/browser/sketches-service-client-impl.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,18 @@ export class SketchesServiceClientImpl
6767
);
6868

6969
private _currentSketch: CurrentSketch | undefined;
70+
private _currentIdeTempFolderUri: URI | undefined;
7071
private currentSketchLoaded = new Deferred<CurrentSketch>();
7172

7273
onStart(): void {
7374
const sketchDirUri = this.configService.tryGetSketchDirUri();
7475
this.watchSketchbookDir(sketchDirUri);
7576
const refreshCurrentSketch = async () => {
7677
const currentSketch = await this.loadCurrentSketch();
77-
this.useCurrentSketch(currentSketch);
78+
const ideTempFolderUri = await this.getIdeTempFolderUriForSketch(
79+
currentSketch
80+
);
81+
this.useCurrentSketch(currentSketch, ideTempFolderUri);
7882
};
7983
this.toDispose.push(
8084
this.configService.onDidChangeSketchDirUri((sketchDirUri) => {
@@ -141,7 +145,10 @@ export class SketchesServiceClientImpl
141145
}
142146

143147
if (!Sketch.sameAs(this._currentSketch, reloadedSketch)) {
144-
this.useCurrentSketch(reloadedSketch, true);
148+
const ideTempFolderUri = await this.getIdeTempFolderUriForSketch(
149+
reloadedSketch
150+
);
151+
this.useCurrentSketch(reloadedSketch, ideTempFolderUri, true);
145152
}
146153
return;
147154
}
@@ -179,11 +186,23 @@ export class SketchesServiceClientImpl
179186
]);
180187
}
181188

189+
private async getIdeTempFolderUriForSketch(
190+
sketch: CurrentSketch
191+
): Promise<URI | undefined> {
192+
if (CurrentSketch.isValid(sketch)) {
193+
const uri = await this.sketchesService.getIdeTempFolderUri(sketch);
194+
return new URI(uri);
195+
}
196+
return undefined;
197+
}
198+
182199
private useCurrentSketch(
183200
currentSketch: CurrentSketch,
201+
ideTempFolderUri: URI | undefined,
184202
reassignPromise = false
185203
) {
186204
this._currentSketch = currentSketch;
205+
this._currentIdeTempFolderUri = ideTempFolderUri;
187206
if (reassignPromise) {
188207
this.currentSketchLoaded = new Deferred();
189208
}
@@ -273,6 +292,14 @@ export class SketchesServiceClientImpl
273292
return false;
274293
}
275294

295+
if (
296+
this._currentIdeTempFolderUri &&
297+
this._currentIdeTempFolderUri.resolve('launch.json').toString() ===
298+
toCheck.toString()
299+
) {
300+
return false;
301+
}
302+
276303
const isCloudSketch = toCheck
277304
.toString()
278305
.includes(`${REMOTE_SKETCHBOOK_FOLDER}/${ARDUINO_CLOUD_FOLDER}`);

‎arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,44 @@
1-
import debounce from 'p-debounce';
2-
import { inject, injectable } from '@theia/core/shared/inversify';
3-
import URI from '@theia/core/lib/common/uri';
4-
import { Event, Emitter } from '@theia/core/lib/common/event';
51
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
6-
import { DebugConfiguration } from '@theia/debug/lib/common/debug-common';
7-
import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model';
2+
import { Disposable } from '@theia/core/lib/common/disposable';
3+
import { Emitter, Event } from '@theia/core/lib/common/event';
4+
import URI from '@theia/core/lib/common/uri';
5+
import { inject, injectable } from '@theia/core/shared/inversify';
86
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
7+
import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model';
8+
import { DebugConfiguration } from '@theia/debug/lib/common/debug-common';
9+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
10+
import {
11+
FileOperationError,
12+
FileOperationResult,
13+
} from '@theia/filesystem/lib/common/files';
14+
import debounce from 'p-debounce';
915
import { SketchesService } from '../../../common/protocol';
1016
import {
1117
CurrentSketch,
1218
SketchesServiceClientImpl,
1319
} from '../../sketches-service-client-impl';
20+
import { maybeUpdateReadOnlyState } from '../monaco/monaco-editor-provider';
1421
import { DebugConfigurationModel } from './debug-configuration-model';
15-
import {
16-
FileOperationError,
17-
FileOperationResult,
18-
} from '@theia/filesystem/lib/common/files';
19-
import { FileService } from '@theia/filesystem/lib/browser/file-service';
2022

2123
@injectable()
2224
export class DebugConfigurationManager extends TheiaDebugConfigurationManager {
2325
@inject(SketchesService)
24-
protected readonly sketchesService: SketchesService;
25-
26+
private readonly sketchesService: SketchesService;
2627
@inject(SketchesServiceClientImpl)
27-
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
28-
28+
private readonly sketchesServiceClient: SketchesServiceClientImpl;
2929
@inject(FrontendApplicationStateService)
30-
protected readonly appStateService: FrontendApplicationStateService;
31-
30+
private readonly appStateService: FrontendApplicationStateService;
3231
@inject(FileService)
33-
protected readonly fileService: FileService;
32+
private readonly fileService: FileService;
3433

35-
protected onTempContentDidChangeEmitter =
34+
private onTempContentDidChangeEmitter =
3635
new Emitter<TheiaDebugConfigurationModel.JsonContent>();
3736
get onTempContentDidChange(): Event<TheiaDebugConfigurationModel.JsonContent> {
3837
return this.onTempContentDidChangeEmitter.event;
3938
}
4039

4140
protected override async doInit(): Promise<void> {
41+
this.watchLaunchConfigEditor();
4242
this.appStateService.reachedState('ready').then(async () => {
4343
const tempContent = await this.getTempLaunchJsonContent();
4444
if (!tempContent) {
@@ -75,6 +75,19 @@ export class DebugConfigurationManager extends TheiaDebugConfigurationManager {
7575
return super.doInit();
7676
}
7777

78+
/**
79+
* Sets a listener on current sketch change, and maybe updates the readonly state of the editor showing the debug configuration. aka the `launch.json`.
80+
*/
81+
private watchLaunchConfigEditor(): Disposable {
82+
return this.sketchesServiceClient.onCurrentSketchDidChange(() => {
83+
for (const widget of this.editorManager.all) {
84+
maybeUpdateReadOnlyState(widget, (uri) =>
85+
this.sketchesServiceClient.isReadOnly(uri)
86+
);
87+
}
88+
});
89+
}
90+
7891
protected override updateModels = debounce(async () => {
7992
await this.appStateService.reachedState('ready');
8093
const roots = await this.workspaceService.roots;
@@ -111,7 +124,7 @@ export class DebugConfigurationManager extends TheiaDebugConfigurationManager {
111124
this.updateCurrent();
112125
}, 500);
113126

114-
protected async getTempLaunchJsonContent(): Promise<
127+
private async getTempLaunchJsonContent(): Promise<
115128
(TheiaDebugConfigurationModel.JsonContent & { uri: URI }) | URI | undefined
116129
> {
117130
const sketch = await this.sketchesServiceClient.currentSketch();

‎arduino-ide-extension/src/browser/theia/monaco/monaco-editor-provider.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import { inject, injectable } from '@theia/core/shared/inversify';
2-
import URI from '@theia/core/lib/common/uri';
1+
import { LOCKED_CLASS, lock } from '@theia/core/lib/browser/widgets/widget';
32
import {
43
Disposable,
54
DisposableCollection,
65
} from '@theia/core/lib/common/disposable';
6+
import URI from '@theia/core/lib/common/uri';
7+
import { Title, Widget } from '@theia/core/shared/@phosphor/widgets';
8+
import { inject, injectable } from '@theia/core/shared/inversify';
9+
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
10+
import * as monaco from '@theia/monaco-editor-core';
11+
import type { ReferencesModel } from '@theia/monaco-editor-core/esm/vs/editor/contrib/gotoSymbol/browser/referencesModel';
712
import {
813
EditorServiceOverrides,
914
MonacoEditor,
1015
} from '@theia/monaco/lib/browser/monaco-editor';
1116
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
1217
import { SketchesServiceClientImpl } from '../../sketches-service-client-impl';
13-
import * as monaco from '@theia/monaco-editor-core';
14-
import type { ReferencesModel } from '@theia/monaco-editor-core/esm/vs/editor/contrib/gotoSymbol/browser/referencesModel';
1518

1619
type CancelablePromise = Promise<ReferencesModel> & {
1720
cancel: () => void;
@@ -101,3 +104,30 @@ export class MonacoEditorProvider extends TheiaMonacoEditorProvider {
101104
editor.updateOptions({ readOnly });
102105
}
103106
}
107+
108+
// Theia cannot dynamically set an editor to writable once it was readonly.
109+
export function maybeUpdateReadOnlyState(
110+
widget: EditorWidget,
111+
isReadOnly: (uri: string | URI | monaco.Uri) => boolean
112+
): void {
113+
const editor = widget.editor;
114+
if (!(editor instanceof MonacoEditor)) {
115+
return;
116+
}
117+
const model = editor.document;
118+
const oldReadOnly = model.readOnly;
119+
const resource = model['resource'];
120+
const newReadOnly = Boolean(resource.isReadonly) || isReadOnly(resource.uri);
121+
if (oldReadOnly !== newReadOnly) {
122+
editor.getControl().updateOptions({ readOnly: newReadOnly });
123+
if (newReadOnly) {
124+
lock(widget.title);
125+
} else {
126+
unlock(widget.title);
127+
}
128+
}
129+
}
130+
131+
function unlock(title: Title<Widget>): void {
132+
title.className = title.className.replace(LOCKED_CLASS, '').trim();
133+
}

‎arduino-ide-extension/src/browser/theia/monaco/monaco-text-model-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class MaybeReadonlyMonacoEditorModel extends SilentMonacoEditorModel {
7777
}
7878
this._dirty = dirty;
7979
if (dirty === false) {
80-
(this as any).updateSavedVersionId();
80+
this['updateSavedVersionId']();
8181
}
8282
this.onDirtyChangedEmitter.fire(undefined);
8383
}

‎arduino-ide-extension/src/common/nls.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export const InstallManually = nls.localize(
2020
'arduino/common/installManually',
2121
'Install Manually'
2222
);
23+
export const SelectManually = nls.localize(
24+
'arduino/common/selectManually',
25+
'Select Manually'
26+
);
2327

2428
export const serialMonitorWidgetLabel = nls.localize(
2529
'arduino/common/serialMonitor',

‎arduino-ide-extension/src/common/protocol/boards-service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ export interface Programmer {
406406
readonly name: string;
407407
readonly platform: string;
408408
readonly id: string;
409+
readonly default?: boolean;
409410
}
410411
export namespace Programmer {
411412
export function equals(
@@ -425,6 +426,20 @@ export namespace Programmer {
425426
);
426427
}
427428
}
429+
export function isProgrammer(arg: unknown): arg is Programmer {
430+
return (
431+
typeof arg === 'object' &&
432+
arg !== null &&
433+
(<Programmer>arg).id !== undefined &&
434+
typeof (<Programmer>arg).id === 'string' &&
435+
(<Programmer>arg).name !== undefined &&
436+
typeof (<Programmer>arg).name === 'string' &&
437+
(<Programmer>arg).platform !== undefined &&
438+
typeof (<Programmer>arg).platform === 'string' &&
439+
((<Programmer>arg).default === undefined ||
440+
typeof (<Programmer>arg).default === 'boolean')
441+
);
442+
}
428443

429444
export namespace Board {
430445
export function is(board: any): board is Board {

‎arduino-ide-extension/src/common/protocol/sketches-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export interface SketchesService {
121121
* Hence, IDE2 has to provide multiple build paths on Windows. This hack will be obsolete when the CLI can provide error codes:
122122
* https://github.com/arduino/arduino-cli/issues/1762.
123123
*/
124-
tempBuildPath(sketch: Sketch): Promise<string[]>;
124+
tempBuildPath(sketch: SketchRef): Promise<string[]>;
125125
}
126126

127127
export interface SketchRef {

‎arduino-ide-extension/src/node/sketches-service-impl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,12 +555,12 @@ export class SketchesServiceImpl
555555
return destinationUri;
556556
}
557557

558-
async getIdeTempFolderUri(sketch: Sketch): Promise<string> {
558+
async getIdeTempFolderUri(sketch: SketchRef): Promise<string> {
559559
const genBuildPath = await this.getIdeTempFolderPath(sketch);
560560
return FileUri.create(genBuildPath).toString();
561561
}
562562

563-
private async getIdeTempFolderPath(sketch: Sketch): Promise<string> {
563+
private async getIdeTempFolderPath(sketch: SketchRef): Promise<string> {
564564
const sketchPath = FileUri.fsPath(sketch.uri);
565565
await fs.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
566566
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');

‎electron-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@
196196
"theiaPlugins": {
197197
"vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix",
198198
"vscode-arduino-api": "https://github.com/dankeboy36/vscode-arduino-api/releases/download/0.1.2/vscode-arduino-api-0.1.2.vsix",
199-
"vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.8.vsix",
199+
"vscode-arduino-tools": "https://github.com/arduino/vscode-arduino-tools/raw/cli-0.35.0-rc.1/build-artifacts/vscode-arduino-tools-0.1.0-beta.1.vsix",
200200
"vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix",
201201
"vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix",
202202
"cortex-debug": "https://downloads.arduino.cc/marus25.cortex-debug/marus25.cortex-debug-1.5.1.vsix",

‎i18n/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
"processing": "Processing",
147147
"recommended": "Recommended",
148148
"retired": "Retired",
149+
"selectManually": "Select Manually",
149150
"selectedOn": "on {0}",
150151
"serialMonitor": "Serial Monitor",
151152
"type": "Type",
@@ -209,8 +210,10 @@
209210
"debug": {
210211
"debugWithMessage": "Debug - {0}",
211212
"debuggingNotSupported": "Debugging is not supported by '{0}'",
213+
"getDebugInfo": "Getting debug info...",
212214
"noPlatformInstalledFor": "Platform is not installed for '{0}'",
213215
"optimizeForDebugging": "Optimize for Debugging",
216+
"programmerNotSelected": "The debugger requires a programmer. Do you want to select a programmer? You can select it manually from the Tools > Programmer menu.",
214217
"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?"
215218
},
216219
"developer": {
@@ -413,6 +416,7 @@
413416
"deprecationMessage": "Deprecated. Use 'window.zoomLevel' instead."
414417
}
415418
},
419+
"quickSelectProgrammer": "Type the programmer. Press Enter to confirm or Escape to cancel.",
416420
"renameCloudSketch": {
417421
"renameSketchTitle": "New name of the Cloud Sketch"
418422
},

0 commit comments

Comments
 (0)
Please sign in to comment.