From fe359deac7cf25dfb6336af1425d10c0e1aa1b95 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 9 Nov 2023 16:15:49 +0100 Subject: [PATCH 1/8] feat: show in tooltip if core is from sketchbook Closes #2270 Signed-off-by: Akos Kitta --- .../boards/boards-config-component.tsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/arduino-ide-extension/src/browser/boards/boards-config-component.tsx b/arduino-ide-extension/src/browser/boards/boards-config-component.tsx index acf2c7d4e..f14b8f390 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config-component.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config-component.tsx @@ -48,16 +48,17 @@ namespace BoardsConfigComponent { } } -export abstract class Item extends React.Component<{ +class Item extends React.Component<{ item: T; label: string; selected: boolean; onClick: (item: T) => void; missing?: boolean; details?: string; + title?: string | ((item: T) => string); }> { override render(): React.ReactNode { - const { selected, label, missing, details } = this.props; + const { selected, label, missing, details, item } = this.props; const classNames = ['item']; if (selected) { classNames.push('selected'); @@ -65,11 +66,15 @@ export abstract class Item extends React.Component<{ if (missing === true) { classNames.push('missing'); } + let title = this.props.title ?? `${label}${!details ? '' : details}`; + if (typeof title === 'function') { + title = title(item); + } return (
{label}
{!details ? '' :
{details}
} @@ -234,9 +239,20 @@ export class BoardsConfigComponent extends React.Component< distinctBoards.set(key, board); } } + const title = (board: Board.Detailed): string => { + const { details, manuallyInstalled } = board; + let label = board.name; + if (details) { + label += details; + } + if (manuallyInstalled) { + label += nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)'); + } + return label; + }; const boardsList = Array.from(distinctBoards.values()).map((board) => ( - + key={toKey(board)} item={board} label={board.name} @@ -244,6 +260,7 @@ export class BoardsConfigComponent extends React.Component< selected={board.selected} onClick={this.selectBoard} missing={board.missing} + title={title} /> )); From 20834318936117965cfa1bc8bbc7c2e302a3c23e Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 24 Oct 2023 16:21:59 +0200 Subject: [PATCH 2/8] feat: handle `v` prefix in CLI GH release name Ref: arduino/arduino-cli#2374 Signed-off-by: Akos Kitta --- arduino-ide-extension/scripts/generate-protocol.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/arduino-ide-extension/scripts/generate-protocol.js b/arduino-ide-extension/scripts/generate-protocol.js index 5b5301575..ec15850b3 100644 --- a/arduino-ide-extension/scripts/generate-protocol.js +++ b/arduino-ide-extension/scripts/generate-protocol.js @@ -6,6 +6,7 @@ const { mkdirSync, promises: fs } = require('node:fs'); const { exec } = require('./utils'); const glob = require('glob'); + const { SemVer, gte, valid: validSemVer } = require('semver'); const protoc = path.dirname(require('protoc/protoc')); const repository = await fs.mkdtemp(path.join(os.tmpdir(), 'arduino-cli-')); @@ -94,13 +95,12 @@ } */ const versionObject = JSON.parse(versionJson); - const version = versionObject.VersionString; - if ( - version && - !version.startsWith('nightly-') && - version !== '0.0.0-git' && - version !== 'git-snapshot' - ) { + let version = versionObject.VersionString; + if (validSemVer(version)) { + // https://github.com/arduino/arduino-cli/pull/2374 + if (gte(new SemVer(version, { loose: true }), new SemVer('0.35.0-rc.1'))) { + version = `v${version}`; + } console.log(`>>> Checking out tagged version: '${version}'...`); exec('git', ['-C', repository, 'fetch', '--all', '--tags'], { logStdout: true, From eed63c7d15ce10fe931a7c1d684ce56a7b2bfa6c Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 26 Oct 2023 16:20:50 +0200 Subject: [PATCH 3/8] chore: use `0.7.5` Arduino LS Signed-off-by: Akos Kitta --- arduino-ide-extension/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 866844ab8..8ce0e3d1a 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -175,7 +175,7 @@ "version": "2.4.1" }, "arduino-language-server": { - "version": "0.7.4" + "version": "0.7.5" }, "clangd": { "version": "14.0.0" From bf36741864b5716a1936dc4e6d7b99f83d185af4 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 6 Nov 2023 17:11:13 +0100 Subject: [PATCH 4/8] chore(deps): update to `electron@27.0.3` - Related change: https://github.com/arduino/arduino-ide/commit/153e34f11b9c16801eecba9d9f6f931c12b231b8 - Reported at: https://github.com/arduino/arduino-ide/pull/2267#issuecomment-1795180432 - External: https://forum.arduino.cc/t/ide-2-2-1-main-window-randomly-goes-blank/1166219 Signed-off-by: Akos Kitta --- electron-app/package.json | 4 ++-- yarn.lock | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/electron-app/package.json b/electron-app/package.json index 3254dd07e..9778a4128 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -28,8 +28,8 @@ "compression-webpack-plugin": "^9.0.0", "copy-webpack-plugin": "^8.1.1", "dateformat": "^5.0.3", - "electron": "^26.2.4", - "electron-builder": "^24.6.3", + "electron": "^27.0.3", + "electron-builder": "^24.6.4", "electron-notarize": "^1.1.1", "execa": "^7.1.1", "file-type": "^18.5.0", diff --git a/yarn.lock b/yarn.lock index 1c38ab984..2867d09d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5873,7 +5873,7 @@ ejs@^3.1.7, ejs@^3.1.8: dependencies: jake "^10.8.5" -electron-builder@^24.6.3: +electron-builder@^24.6.4: version "24.6.4" resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.6.4.tgz#c51271e49b9a02c9a3ec444f866b6008c4d98a1d" integrity sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA== @@ -5958,10 +5958,10 @@ electron-updater@^4.6.5: lodash.isequal "^4.5.0" semver "^7.3.5" -electron@^26.2.4: - version "26.3.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-26.3.0.tgz#3267773d170310384db76819cf6375bd98b3cc76" - integrity sha512-7ZpvSHu+jmqialSvywTZnOQZZGLqlyj+yV5HGDrEzFnMiFaXBRpbByHgoUhaExJ/8t/0xKQjKlMRAY65w+zNZQ== +electron@^27.0.3: + version "27.0.3" + resolved "https://registry.yarnpkg.com/electron/-/electron-27.0.3.tgz#dc843d95700b33d88e71b458082b66f37ca901c5" + integrity sha512-VaB9cI1se+mUtz366NP+zxFVnkHLbCBNO4wwouw3FuGyX/m7/Bv1I89JhWOBv78tC+n11ZYMrVD23Jf6EZgVcg== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18" From b00889ffcbc3cb8b501bfc6767a639b4eaa97fb7 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 21 Nov 2023 11:27:30 +0100 Subject: [PATCH 5/8] feat: new window inherits the custom board options A new startup task ensures setting any custom board menu selection in a new sketch window. Closes #2271 Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 3 + .../src/browser/boards/boards-data-store.ts | 237 +++++++-- .../contributions/boards-data-menu-updater.ts | 2 +- .../src/browser/contributions/ino-language.ts | 10 +- .../contributions/update-arduino-state.ts | 7 +- .../test/browser/boards-data-store.test.ts | 501 ++++++++++++++++++ 6 files changed, 700 insertions(+), 60 deletions(-) create mode 100644 arduino-ide-extension/src/test/browser/boards-data-store.test.ts diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 436dd8e86..5efc092c3 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -454,6 +454,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board. bind(BoardsDataStore).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(BoardsDataStore); + bind(CommandContribution).toService(BoardsDataStore); + bind(StartupTaskProvider).toService(BoardsDataStore); // to inherit the boards config options, programmer, etc in a new window + // Logger for the Arduino daemon bind(ILogger) .toDynamicValue((ctx) => { diff --git a/arduino-ide-extension/src/browser/boards/boards-data-store.ts b/arduino-ide-extension/src/browser/boards/boards-data-store.ts index 579f30b7a..e6e34abf0 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-store.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts @@ -1,21 +1,38 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { StorageService } from '@theia/core/lib/browser/storage-service'; +import type { + Command, + CommandContribution, + CommandRegistry, +} from '@theia/core/lib/common/command'; 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 } from '@theia/core/lib/common/objects'; +import { deepClone, deepFreeze } from '@theia/core/lib/common/objects'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import { BoardDetails, BoardsService, ConfigOption, Programmer, + isBoardIdentifierChangeEvent, } from '../../common/protocol'; import { notEmpty } from '../../common/utils'; +import type { + StartupTask, + StartupTaskProvider, +} from '../../electron-common/startup-task'; import { NotificationCenter } from '../notification-center'; +import { BoardsServiceProvider } from './boards-service-provider'; @injectable() -export class BoardsDataStore implements FrontendApplicationContribution { +export class BoardsDataStore + implements + FrontendApplicationContribution, + StartupTaskProvider, + CommandContribution +{ @inject(ILogger) @named('store') private readonly logger: ILogger; @@ -28,44 +45,110 @@ export class BoardsDataStore implements FrontendApplicationContribution { // In other words, store the data (such as the board configs) per sketch, not per IDE2 installation. https://github.com/arduino/arduino-ide/issues/2240 @inject(StorageService) private readonly storageService: StorageService; + @inject(BoardsServiceProvider) + private readonly boardsServiceProvider: BoardsServiceProvider; + @inject(FrontendApplicationStateService) + private readonly appStateService: FrontendApplicationStateService; - private readonly onChangedEmitter = new Emitter(); - private readonly toDispose = new DisposableCollection(this.onChangedEmitter); + private readonly onDidChangeEmitter = + new Emitter(); + private readonly toDispose = new DisposableCollection( + this.onDidChangeEmitter + ); + private _selectedBoardData: BoardsDataStoreChange | undefined; onStart(): void { - this.toDispose.push( + this.toDispose.pushAll([ + this.boardsServiceProvider.onBoardsConfigDidChange((event) => { + if (isBoardIdentifierChangeEvent(event)) { + this.updateSelectedBoardData(event.selectedBoard?.fqbn); + } + }), this.notificationCenter.onPlatformDidInstall(async ({ item }) => { - const dataDidChangePerFqbn: string[] = []; - for (const fqbn of item.boards + const boardsWithFqbn = item.boards .map(({ fqbn }) => fqbn) - .filter(notEmpty) - .filter((fqbn) => !!fqbn)) { + .filter(notEmpty); + const changes: BoardsDataStoreChange[] = []; + for (const fqbn of boardsWithFqbn) { const key = this.getStorageKey(fqbn); - let data = await this.storageService.getData(key); - if (!data || !data.length) { - const details = await this.getBoardDetailsSafe(fqbn); - if (details) { - data = details.configOptions; - if (data.length) { - await this.storageService.setData(key, data); - dataDidChangePerFqbn.push(fqbn); - } - } + const storedData = + await this.storageService.getData(key); + if (!storedData) { + // if not previously value is available for the board, do not update the cache + continue; + } + const details = await this.loadBoardDetails(fqbn); + if (details) { + const data = createDataStoreEntry(details); + await this.storageService.setData(key, data); + changes.push({ fqbn, data }); } } - if (dataDidChangePerFqbn.length) { - this.fireChanged(...dataDidChangePerFqbn); + if (changes.length) { + this.fireChanged(...changes); } - }) + }), + ]); + + Promise.all([ + this.boardsServiceProvider.ready, + this.appStateService.reachedState('ready'), + ]).then(() => + this.updateSelectedBoardData( + this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn + ) ); } + private async getSelectedBoardData( + fqbn: string | undefined + ): Promise { + if (!fqbn) { + return undefined; + } else { + const data = await this.getData(fqbn); + if (data === BoardsDataStore.Data.EMPTY) { + return undefined; + } + return { fqbn, data }; + } + } + + private async updateSelectedBoardData( + fqbn: string | undefined + ): Promise { + this._selectedBoardData = await this.getSelectedBoardData(fqbn); + } + onStop(): void { this.toDispose.dispose(); } - get onChanged(): Event { - return this.onChangedEmitter.event; + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(USE_INHERITED_DATA, { + execute: async (arg: unknown) => { + if (isBoardsDataStoreChange(arg)) { + await this.setData(arg); + this.fireChanged(arg); + } + }, + }); + } + + tasks(): StartupTask[] { + if (!this._selectedBoardData) { + return []; + } + return [ + { + command: USE_INHERITED_DATA.id, + args: [this._selectedBoardData], + }, + ]; + } + + get onDidChange(): Event { + return this.onDidChangeEmitter.event; } async appendConfigToFqbn( @@ -84,11 +167,11 @@ export class BoardsDataStore implements FrontendApplicationContribution { } const key = this.getStorageKey(fqbn); - let data = await this.storageService.getData< + const storedData = await this.storageService.getData< BoardsDataStore.Data | undefined >(key, undefined); - if (BoardsDataStore.Data.is(data)) { - return data; + if (BoardsDataStore.Data.is(storedData)) { + return storedData; } const boardDetails = await this.getBoardDetailsSafe(fqbn); @@ -96,10 +179,7 @@ export class BoardsDataStore implements FrontendApplicationContribution { return BoardsDataStore.Data.EMPTY; } - data = { - configOptions: boardDetails.configOptions, - programmers: boardDetails.programmers, - }; + const data = createDataStoreEntry(boardDetails); await this.storageService.setData(key, data); return data; } @@ -111,17 +191,15 @@ export class BoardsDataStore implements FrontendApplicationContribution { fqbn: string; selectedProgrammer: Programmer; }): Promise { - const data = deepClone(await this.getData(fqbn)); - const { programmers } = data; + const storedData = deepClone(await this.getData(fqbn)); + const { programmers } = storedData; if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) { return false; } - await this.setData({ - fqbn, - data: { ...data, selectedProgrammer }, - }); - this.fireChanged(fqbn); + const data = { ...storedData, selectedProgrammer }; + await this.setData({ fqbn, data }); + this.fireChanged({ fqbn, data }); return true; } @@ -153,17 +231,12 @@ export class BoardsDataStore implements FrontendApplicationContribution { return false; } await this.setData({ fqbn, data }); - this.fireChanged(fqbn); + this.fireChanged({ fqbn, data }); return true; } - protected async setData({ - fqbn, - data, - }: { - fqbn: string; - data: BoardsDataStore.Data; - }): Promise { + protected async setData(change: BoardsDataStoreChange): Promise { + const { fqbn, data } = change; const key = this.getStorageKey(fqbn); return this.storageService.setData(key, data); } @@ -176,7 +249,7 @@ export class BoardsDataStore implements FrontendApplicationContribution { fqbn: string ): Promise { try { - const details = this.boardsService.getBoardDetails({ fqbn }); + const details = await this.boardsService.getBoardDetails({ fqbn }); return details; } catch (err) { if ( @@ -197,8 +270,8 @@ export class BoardsDataStore implements FrontendApplicationContribution { } } - protected fireChanged(...fqbn: string[]): void { - this.onChangedEmitter.fire(fqbn); + protected fireChanged(...changes: BoardsDataStoreChange[]): void { + this.onDidChangeEmitter.fire({ changes }); } } @@ -209,11 +282,13 @@ export namespace BoardsDataStore { readonly selectedProgrammer?: Programmer; } export namespace Data { - export const EMPTY: Data = { + export const EMPTY: Data = deepFreeze({ configOptions: [], programmers: [], - }; - export function is(arg: any): arg is Data { + defaultProgrammerId: undefined, + }); + + export function is(arg: unknown): arg is Data { return ( !!arg && 'configOptions' in arg && @@ -224,3 +299,61 @@ export namespace BoardsDataStore { } } } + +export function isEmptyData(data: BoardsDataStore.Data): boolean { + return ( + Boolean(!data.configOptions.length) && + Boolean(!data.programmers.length) && + Boolean(!data.selectedProgrammer) + ); +} + +export function findDefaultProgrammer( + programmers: readonly Programmer[], + defaultProgrammerId: string | undefined | BoardsDataStore.Data +): Programmer | undefined { + if (!defaultProgrammerId) { + return undefined; + } + const id = + typeof defaultProgrammerId === 'string' + ? defaultProgrammerId + : defaultProgrammerId.defaultProgrammerId; + return programmers.find((p) => p.id === id); +} +function createDataStoreEntry(details: BoardDetails): BoardsDataStore.Data { + const configOptions = details.configOptions.slice(); + const programmers = details.programmers.slice(); + const selectedProgrammer = findDefaultProgrammer( + programmers, + details.defaultProgrammerId + ); + return { + configOptions, + programmers, + defaultProgrammerId: details.defaultProgrammerId, + selectedProgrammer, + }; +} + +export interface BoardsDataStoreChange { + readonly fqbn: string; + readonly data: BoardsDataStore.Data; +} + +function isBoardsDataStoreChange(arg: unknown): arg is BoardsDataStoreChange { + return ( + typeof arg === 'object' && + arg !== null && + typeof (arg).fqbn === 'string' && + BoardsDataStore.Data.is((arg).data) + ); +} + +export interface BoardsDataStoreChangeEvent { + readonly changes: readonly BoardsDataStoreChange[]; +} + +const USE_INHERITED_DATA: Command = { + id: 'arduino-use-inherited-boards-data', +}; diff --git a/arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts b/arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts index d9fe0ae7e..ea085f5ba 100644 --- a/arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts +++ b/arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts @@ -35,7 +35,7 @@ export class BoardsDataMenuUpdater extends Contribution { private readonly toDisposeOnBoardChange = new DisposableCollection(); override onStart(): void { - this.boardsDataStore.onChanged(() => + this.boardsDataStore.onDidChange(() => this.updateMenuActions( this.boardsServiceProvider.boardsConfig.selectedBoard ) diff --git a/arduino-ide-extension/src/browser/contributions/ino-language.ts b/arduino-ide-extension/src/browser/contributions/ino-language.ts index 096c27ed8..26c7487d1 100644 --- a/arduino-ide-extension/src/browser/contributions/ino-language.ts +++ b/arduino-ide-extension/src/browser/contributions/ino-language.ts @@ -90,7 +90,7 @@ export class InoLanguage extends SketchContribution { this.notificationCenter.onPlatformDidInstall(() => forceRestart()), this.notificationCenter.onPlatformDidUninstall(() => forceRestart()), this.notificationCenter.onDidReinitialize(() => forceRestart()), - this.boardDataStore.onChanged((dataChangePerFqbn) => { + this.boardDataStore.onDidChange((event) => { if (this.languageServerFqbn) { const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn); if (!sanitizeFqbn) { @@ -98,13 +98,13 @@ export class InoLanguage extends SketchContribution { `Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}` ); } - const matchingFqbn = dataChangePerFqbn.find( - (fqbn) => sanitizedFqbn === fqbn + const matchingChange = event.changes.find( + (change) => change.fqbn === sanitizedFqbn ); const { boardsConfig } = this.boardsServiceProvider; if ( - matchingFqbn && - boardsConfig.selectedBoard?.fqbn === matchingFqbn + matchingChange && + boardsConfig.selectedBoard?.fqbn === matchingChange.fqbn ) { start(boardsConfig.selectedBoard); } diff --git a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts index e83e46f51..ab4cdafb0 100644 --- a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts +++ b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts @@ -65,10 +65,13 @@ export class UpdateArduinoState extends SketchContribution { this.updateCompileSummary(args[0]); } }), - this.boardsDataStore.onChanged((fqbn) => { + this.boardsDataStore.onDidChange((event) => { const selectedFqbn = this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn; - if (selectedFqbn && fqbn.includes(selectedFqbn)) { + if ( + selectedFqbn && + event.changes.find((change) => change.fqbn === selectedFqbn) + ) { this.updateBoardDetails(selectedFqbn); } }), diff --git a/arduino-ide-extension/src/test/browser/boards-data-store.test.ts b/arduino-ide-extension/src/test/browser/boards-data-store.test.ts new file mode 100644 index 000000000..4f52c207d --- /dev/null +++ b/arduino-ide-extension/src/test/browser/boards-data-store.test.ts @@ -0,0 +1,501 @@ +import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; +const disableJSDOM = enableJSDOM(); + +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +FrontendApplicationConfigProvider.set({}); + +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { + LocalStorageService, + StorageService, +} from '@theia/core/lib/browser/storage-service'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { wait } from '@theia/core/lib/common/promise-util'; +import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; +import { Container, ContainerModule } from '@theia/core/shared/inversify'; +import { expect } from 'chai'; +import { BoardsDataStore } from '../../browser/boards/boards-data-store'; +import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider'; +import { NotificationCenter } from '../../browser/notification-center'; +import { + BoardDetails, + BoardsPackage, + BoardsService, + ConfigOption, + Programmer, +} from '../../common/protocol/boards-service'; +import { NotificationServiceServer } from '../../common/protocol/notification-service'; +import { ConsoleLogger, bindCommon } from '../common/common-test-bindings'; + +disableJSDOM(); + +describe('boards-data-store', function () { + this.slow(250); + + let toDisposeAfterEach: DisposableCollection; + let boardsServiceProvider: BoardsServiceProvider; + let boardsDataStore: BoardsDataStore; + let notificationCenter: NotificationCenter; + + beforeEach(async () => { + const container = createContainer(); + container.get( + FrontendApplicationStateService + ).state = 'ready'; + notificationCenter = container.get(NotificationCenter); + boardsServiceProvider = container.get( + BoardsServiceProvider + ); + toDisposeAfterEach = new DisposableCollection( + Disposable.create(() => boardsServiceProvider.onStop()) + ); + boardsServiceProvider.onStart(); + await boardsServiceProvider.ready; + boardsDataStore = container.get(BoardsDataStore); + boardsDataStore.onStart(); + }); + + afterEach(() => toDisposeAfterEach.dispose()); + + it('should load the board details when absent in local storage', async () => { + const storedData = await getStoredData(fqbn); + expect(storedData).to.be.undefined; + const data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should load from local storage if present', async () => { + const storedData: BoardsDataStore.Data = { + configOptions: [], + programmers: [edbg], + selectedProgrammer: edbg, + }; + await setStorageData(fqbn, storedData); + const data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal(storedData); + }); + + it('should update board details of selected board (selected with FQBN)', async () => { + const updated = boardsServiceProvider.updateConfig(board); + expect(updated).to.be.ok; + await wait(50); + + const selectedBoardData = boardsDataStore['_selectedBoardData']; + expect(selectedBoardData).to.be.deep.equal({ + fqbn, + data: { + configOptions: [configOption1], + programmers: [edbg, jlink], + }, + }); + }); + + it('should not update the board details of selected board when FQBN is missing', async () => { + const fqbn = undefined; + const name = 'ABC'; + const board = { name, fqbn }; + const updated = boardsServiceProvider.updateConfig(board); + expect(updated).to.ok; + await wait(50); + + const selectedBoardData = boardsDataStore['_selectedBoardData']; + expect(selectedBoardData).to.be.undefined; + }); + + it('should unset the the board details of selected board when no board was selected', async () => { + let updated = boardsServiceProvider.updateConfig(board); + expect(updated).to.ok; + await wait(50); + + let selectedBoardData = boardsDataStore['_selectedBoardData']; + expect(selectedBoardData).to.be.deep.equal({ + fqbn, + data: { + configOptions: [configOption1], + programmers: [edbg, jlink], + }, + }); + + updated = boardsServiceProvider.updateConfig('unset-board'); + expect(updated).to.be.true; + await wait(50); + + selectedBoardData = boardsDataStore['_selectedBoardData']; + expect(selectedBoardData).to.be.undefined; + }); + + it('should provide startup tasks when the data is available for the selected board', async () => { + const updated = boardsServiceProvider.updateConfig(board); + expect(updated).to.be.true; + await wait(50); + + const tasks = boardsDataStore.tasks(); + expect(tasks).to.be.deep.equal([ + { + command: 'arduino-use-inherited-boards-data', + args: [ + { + fqbn, + data: { + configOptions: [configOption1], + programmers: [edbg, jlink], + }, + }, + ], + }, + ]); + }); + + it('should not provide any startup tasks when no data is available for the selected board', async () => { + const tasks = boardsDataStore.tasks(); + expect(tasks).to.be.empty; + }); + + it('should select a programmer', async () => { + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectProgrammer({ + fqbn, + selectedProgrammer: edbg, + }); + expect(result).to.be.ok; + expect(didChangeCounter).to.be.equal(1); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + selectedProgrammer: edbg, + }); + }); + + it('should not select a programmer if it is absent', async () => { + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectProgrammer({ + fqbn, + selectedProgrammer: { id: 'p1', name: 'P1', platform: 'missing' }, + }); + expect(result).to.be.not.ok; + expect(didChangeCounter).to.be.equal(0); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should select a config option', async () => { + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectConfigOption({ + fqbn, + option: configOption1.option, + selectedValue: configOption1.values[1].value, + }); + expect(result).to.be.ok; + expect(didChangeCounter).to.be.equal(1); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [ + { + ...configOption1, + values: [ + { label: 'C1V1', selected: false, value: 'v1' }, + { label: 'C1V2', selected: true, value: 'v2' }, + ], + }, + ], + programmers: [edbg, jlink], + }); + }); + + it('should not select a config option if the option is absent', async () => { + const fqbn = 'a:b:c'; + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectConfigOption({ + fqbn, + option: 'missing', + selectedValue: configOption1.values[1].value, + }); + expect(result).to.be.not.ok; + expect(didChangeCounter).to.be.equal(0); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should not select a config option if the selected value is absent', async () => { + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectConfigOption({ + fqbn, + option: configOption1.option, + selectedValue: 'missing', + }); + expect(result).to.be.not.ok; + expect(didChangeCounter).to.be.equal(0); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should not update the board data on platform install if it was not cached', async () => { + let storedData = await getStoredData(fqbn); + expect(storedData).to.be.undefined; + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + notificationCenter.notifyPlatformDidInstall({ item: boardsPackage }); + await wait(50); + expect(didChangeCounter).to.be.equal(0); + + storedData = await getStoredData(fqbn); + expect(storedData).to.be.undefined; + }); + + it('should update the board data on platform install if the default empty value was cached', async () => { + let storedData = await getStoredData(fqbn); + expect(storedData).to.be.undefined; + + await setStorageData(fqbn, BoardsDataStore.Data.EMPTY); + storedData = await getStoredData(fqbn); + expect(storedData).to.be.deep.equal(BoardsDataStore.Data.EMPTY); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + notificationCenter.notifyPlatformDidInstall({ item: boardsPackage }); + await wait(50); + expect(didChangeCounter).to.be.equal(1); + + storedData = await getStoredData(fqbn); + expect(storedData).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should update the cached board data on platform install', async () => { + let storedData = await boardsDataStore.getData(fqbn); // caches the value + expect(storedData).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + // before the platform install event mock a different CLI `board details` output + toDisposeAfterEach.push( + mockBoardDetails([ + { + fqbn, + ...baseDetails, + configOptions: [configOption2], + }, + ]) + ); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + notificationCenter.notifyPlatformDidInstall({ item: boardsPackage }); + await wait(50); + expect(didChangeCounter).to.be.equal(1); + + storedData = await boardsDataStore.getData(fqbn); + expect(storedData).to.be.deep.equal({ + configOptions: [configOption2], + programmers: [edbg, jlink], + }); + }); + + function storageKey(fqbn: string): string { + return boardsDataStore['getStorageKey'](fqbn); + } + + function getStoredData(fqbn: string): Promise { + const key = storageKey(fqbn); + return boardsDataStore['storageService'].getData(key); + } + + function setStorageData( + fqbn: string, + data: BoardsDataStore.Data + ): Promise { + const key = storageKey(fqbn); + return boardsDataStore['storageService'].setData(key, data); + } + + function createContainer(): Container { + const container = new Container({ defaultScope: 'Singleton' }); + container.load( + new ContainerModule((bind, unbind, isBound, rebind) => { + bindCommon(bind); + bind(MessageService).toConstantValue({}); + bind(BoardsService).toConstantValue({ + getDetectedPorts() { + return {}; + }, + async getBoardDetails({ fqbn }) { + return boardDetailsMock().find((mock) => mock.fqbn === fqbn); + }, + }); + bind(NotificationCenter).toSelf().inSingletonScope(); + bind(NotificationServiceServer).toConstantValue(< + NotificationServiceServer + >{ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setClient(_) { + // nothing + }, + }); + bind(FrontendApplicationStateService).toSelf().inSingletonScope(); + bind(BoardsDataStore).toSelf().inSingletonScope(); + bind(LocalStorageService).toSelf().inSingletonScope(); + bind(WindowService).toConstantValue({}); + bind(StorageService).toService(LocalStorageService); + bind(BoardsServiceProvider).toSelf().inSingletonScope(); + // IDE2's test console logger does not support `Loggable` arg. + // Rebind logger to suppress `[Function (anonymous)]` messages in tests when the storage service is initialized without `window.localStorage`. + // https://github.com/eclipse-theia/theia/blob/04c8cf07843ea67402131132e033cdd54900c010/packages/core/src/browser/storage-service.ts#L60 + bind(MockLogger).toSelf().inSingletonScope(); + rebind(ConsoleLogger).toService(MockLogger); + }) + ); + return container; + } + + // Mocks the CLI's `board details` response + const jlink: Programmer = { + platform: 'Arduino SAMD (32-bits ARM Cortex-M0+) Boards', + id: 'jlink', + name: 'Segger J-Link', + }; + const edbg: Programmer = { + platform: 'Arduino SAMD (32-bits ARM Cortex-M0+) Boards', + id: 'edbg', + name: 'Atmel EDBG', + }; + + const configOption1: ConfigOption = { + label: 'C1', + option: 'c1', + values: [ + { label: 'C1V1', selected: true, value: 'v1' }, + { label: 'C1V2', selected: false, value: 'v2' }, + ], + }; + + const configOption2: ConfigOption = { + label: 'C2', + option: 'c2', + values: [ + { label: 'C2V1', selected: true, value: 'v1' }, + { label: 'C2V2', selected: false, value: 'v2' }, + ], + }; + + const baseDetails: Omit = { + VID: '1', + PID: '1', + buildProperties: [], + configOptions: [configOption1], + debuggingSupported: false, + programmers: [edbg, jlink], + requiredTools: [], + }; + + const fqbn = 'a:b:c'; + const name = 'ABC'; + const board = { fqbn, name }; + + const boardsPackage: BoardsPackage = { + id: 'a:b', + name: 'AB', + availableVersions: ['1.0.0'], + boards: [board], + description: 'boy', + summary: ':heart:', + author: 'mano', + types: [], + }; + + const defaultDetailsMocks: readonly BoardDetails[] = [ + { + fqbn, + ...baseDetails, + }, + ]; + let _currentDetailsMock = defaultDetailsMocks; + + function boardDetailsMock(): readonly BoardDetails[] { + return _currentDetailsMock; + } + function mockBoardDetails(newDetails: BoardDetails[]): Disposable { + _currentDetailsMock = newDetails; + return Disposable.create(resetDetailsMock); + } + function resetDetailsMock(): void { + _currentDetailsMock = defaultDetailsMocks; + } +}); From 615a684abdb6a804014d68dbb3f3af8e9d726d27 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 23 Nov 2023 10:22:33 +0100 Subject: [PATCH 6/8] fix: make hosted plugin support testable Hide the concrete implementation behind an interface so that tests can `require` it. Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 9 ++++--- .../src/browser/contributions/debug.ts | 25 ++++++++++++++++--- .../src/browser/contributions/ino-language.ts | 2 +- .../contributions/update-arduino-state.ts | 2 +- .../{ => hosted}/hosted-plugin-events.ts | 2 +- .../browser/hosted/hosted-plugin-support.ts | 14 +++++++++++ .../theia/monaco/monaco-theming-service.ts | 2 +- .../browser/theia/plugin-ext/hosted-plugin.ts | 6 ++++- .../browser/board-service-provider.test.ts | 7 ------ 9 files changed, 49 insertions(+), 20 deletions(-) rename arduino-ide-extension/src/browser/{ => hosted}/hosted-plugin-events.ts (97%) create mode 100644 arduino-ide-extension/src/browser/hosted/hosted-plugin-support.ts diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 5efc092c3..fa92aeafe 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -271,8 +271,8 @@ import { MonitorModel } from './monitor-model'; import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl'; import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager'; import { EditorManager } from './theia/editor/editor-manager'; -import { HostedPluginEvents } from './hosted-plugin-events'; -import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin'; +import { HostedPluginEvents } from './hosted/hosted-plugin-events'; +import { HostedPluginSupportImpl } from './theia/plugin-ext/hosted-plugin'; import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; import { Formatter, FormatterPath } from '../common/protocol/formatter'; import { Format } from './contributions/format'; @@ -985,8 +985,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { }) .inSingletonScope(); - bind(HostedPluginSupport).toSelf().inSingletonScope(); - rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport); + bind(HostedPluginSupportImpl).toSelf().inSingletonScope(); + bind(HostedPluginSupport).toService(HostedPluginSupportImpl); + rebind(TheiaHostedPluginSupport).toService(HostedPluginSupportImpl); bind(HostedPluginEvents).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(HostedPluginEvents); diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index 5dec4655c..8aebd0d82 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -1,8 +1,4 @@ import { inject, injectable } from '@theia/core/shared/inversify'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; -import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; -import { NotificationCenter } from '../notification-center'; import { Board, BoardIdentifier, @@ -12,6 +8,11 @@ import { Sketch, } from '../../common/protocol'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; +import { HostedPluginSupport } from '../hosted/hosted-plugin-support'; +import { ArduinoMenus } from '../menu/arduino-menus'; +import { NotificationCenter } from '../notification-center'; +import { CurrentSketch } from '../sketches-service-client-impl'; +import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { URI, Command, @@ -97,6 +98,22 @@ export class Debug extends SketchContribution { }); this.notificationCenter.onPlatformDidInstall(() => this.refreshState()); this.notificationCenter.onPlatformDidUninstall(() => this.refreshState()); + this.boardsDataStore.onDidChange((event) => { + const selectedFqbn = + this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn; + if (event.changes.find((change) => change.fqbn === selectedFqbn)) { + this.refreshState(); + } + }); + this.commandService.onDidExecuteCommand((event) => { + const { commandId, args } = event; + if ( + commandId === 'arduino.languageserver.notifyBuildDidComplete' && + isCompileSummary(args[0]) + ) { + this.refreshState(); + } + }); } override onReady(): void { diff --git a/arduino-ide-extension/src/browser/contributions/ino-language.ts b/arduino-ide-extension/src/browser/contributions/ino-language.ts index 26c7487d1..5f9a3f127 100644 --- a/arduino-ide-extension/src/browser/contributions/ino-language.ts +++ b/arduino-ide-extension/src/browser/contributions/ino-language.ts @@ -15,7 +15,7 @@ import { } from '../../common/protocol'; import { CurrentSketch } from '../sketches-service-client-impl'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; -import { HostedPluginEvents } from '../hosted-plugin-events'; +import { HostedPluginEvents } from '../hosted/hosted-plugin-events'; import { NotificationCenter } from '../notification-center'; import { SketchContribution, URI } from './contribution'; import { BoardsDataStore } from '../boards/boards-data-store'; diff --git a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts index ab4cdafb0..7f4cd71f1 100644 --- a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts +++ b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts @@ -1,7 +1,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable'; import URI from '@theia/core/lib/common/uri'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { HostedPluginSupport } from '../hosted/hosted-plugin-support'; import type { ArduinoState } from 'vscode-arduino-api'; import { BoardsService, diff --git a/arduino-ide-extension/src/browser/hosted-plugin-events.ts b/arduino-ide-extension/src/browser/hosted/hosted-plugin-events.ts similarity index 97% rename from arduino-ide-extension/src/browser/hosted-plugin-events.ts rename to arduino-ide-extension/src/browser/hosted/hosted-plugin-events.ts index ac7b1fe0d..35dbdbe4c 100644 --- a/arduino-ide-extension/src/browser/hosted-plugin-events.ts +++ b/arduino-ide-extension/src/browser/hosted/hosted-plugin-events.ts @@ -1,7 +1,7 @@ import { DisposableCollection, Emitter, Event } from '@theia/core'; import { FrontendApplicationContribution } from '@theia/core/lib/browser'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin'; +import { HostedPluginSupport } from './hosted-plugin-support'; /** * Frontend contribution to watch VS Code extension start/stop events from Theia. diff --git a/arduino-ide-extension/src/browser/hosted/hosted-plugin-support.ts b/arduino-ide-extension/src/browser/hosted/hosted-plugin-support.ts new file mode 100644 index 000000000..b0d11c86a --- /dev/null +++ b/arduino-ide-extension/src/browser/hosted/hosted-plugin-support.ts @@ -0,0 +1,14 @@ +import type { Event } from '@theia/core/lib/common/event'; + +/* +This implementation hides the default HostedPluginSupport implementation from Theia to be able to test it. +Otherwise, the default implementation fails at require time due to the `import.meta` in the Theia plugin worker code. +https://github.com/eclipse-theia/theia/blob/964f69ca3b3a5fb87ffa0177fb300b74ba0ca39f/packages/plugin-ext/src/hosted/browser/plugin-worker.ts#L30-L32 +*/ + +export const HostedPluginSupport = Symbol('HostedPluginSupport'); +export interface HostedPluginSupport { + readonly didStart: Promise; + readonly onDidLoad: Event; + readonly onDidCloseConnection: Event; +} diff --git a/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts b/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts index 40d703423..2604c3caf 100644 --- a/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts +++ b/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts @@ -20,7 +20,7 @@ import { } from '@theia/monaco/lib/browser/monaco-theming-service'; import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry'; import type { ThemeMix } from '@theia/monaco/lib/browser/textmate/monaco-theme-types'; -import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { HostedPluginSupport } from '../../hosted/hosted-plugin-support'; import { ArduinoThemes, compatibleBuiltInTheme } from '../core/theming'; import { WindowServiceExt } from '../core/window-service-ext'; diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts index 8491357a4..8edf46383 100644 --- a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts @@ -5,9 +5,13 @@ import { PluginContributions, HostedPluginSupport as TheiaHostedPluginSupport, } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { HostedPluginSupport } from '../../hosted/hosted-plugin-support'; @injectable() -export class HostedPluginSupport extends TheiaHostedPluginSupport { +export class HostedPluginSupportImpl + extends TheiaHostedPluginSupport + implements HostedPluginSupport +{ private readonly onDidLoadEmitter = new Emitter(); private readonly onDidCloseConnectionEmitter = new Emitter(); diff --git a/arduino-ide-extension/src/test/browser/board-service-provider.test.ts b/arduino-ide-extension/src/test/browser/board-service-provider.test.ts index 039111474..00d3eca5e 100644 --- a/arduino-ide-extension/src/test/browser/board-service-provider.test.ts +++ b/arduino-ide-extension/src/test/browser/board-service-provider.test.ts @@ -15,7 +15,6 @@ import { DisposableCollection, } from '@theia/core/lib/common/disposable'; import { MessageService } from '@theia/core/lib/common/message-service'; -import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { Container, ContainerModule } from '@theia/core/shared/inversify'; import { expect } from 'chai'; import { BoardsDataStore } from '../../browser/boards/boards-data-store'; @@ -31,7 +30,6 @@ import { PortIdentifierChangeEvent, } from '../../common/protocol/boards-service'; import { NotificationServiceServer } from '../../common/protocol/notification-service'; -import { bindCommon, ConsoleLogger } from '../common/common-test-bindings'; import { detectedPort, esp32S3DevModule, @@ -414,11 +412,6 @@ describe('board-service-provider', () => { bind(WindowService).toConstantValue({}); bind(StorageService).toService(LocalStorageService); bind(BoardsServiceProvider).toSelf().inSingletonScope(); - // IDE2's test console logger does not support `Loggable` arg. - // Rebind logger to suppress `[Function (anonymous)]` messages in tests when the storage service is initialized without `window.localStorage`. - // https://github.com/eclipse-theia/theia/blob/04c8cf07843ea67402131132e033cdd54900c010/packages/core/src/browser/storage-service.ts#L60 - bind(MockLogger).toSelf().inSingletonScope(); - rebind(ConsoleLogger).toService(MockLogger); }) ); return container; From e149eaa59b4e212e23813e0418105c4fdcddfcd7 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 23 Nov 2023 10:56:09 +0100 Subject: [PATCH 7/8] test: test Arduino state update for extensions Signed-off-by: Akos Kitta --- .../contributions/update-arduino-state.ts | 5 +- .../browser/board-service-provider.test.ts | 3 +- .../src/test/browser/browser-test-bindings.ts | 23 +- .../test/browser/update-arduino-state.test.ts | 670 ++++++++++++++++++ .../src/test/common/common-test-bindings.ts | 17 +- .../src/test/node/node-test-bindings.ts | 2 +- arduino-ide-extension/src/test/utils.ts | 7 +- 7 files changed, 716 insertions(+), 11 deletions(-) create mode 100644 arduino-ide-extension/src/test/browser/update-arduino-state.test.ts diff --git a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts index 7f4cd71f1..767fbbf8c 100644 --- a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts +++ b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts @@ -21,7 +21,10 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { CurrentSketch } from '../sketches-service-client-impl'; import { SketchContribution } from './contribution'; -interface UpdateStateParams { +/** + * (non-API) exported for tests + */ +export interface UpdateStateParams { readonly key: keyof T; readonly value: T[keyof T]; } diff --git a/arduino-ide-extension/src/test/browser/board-service-provider.test.ts b/arduino-ide-extension/src/test/browser/board-service-provider.test.ts index 00d3eca5e..54d3aa8ba 100644 --- a/arduino-ide-extension/src/test/browser/board-service-provider.test.ts +++ b/arduino-ide-extension/src/test/browser/board-service-provider.test.ts @@ -39,6 +39,7 @@ import { uno, unoSerialPort, } from '../common/fixtures'; +import { bindBrowser } from './browser-test-bindings'; disableJSDOM(); @@ -390,7 +391,7 @@ describe('board-service-provider', () => { const container = new Container({ defaultScope: 'Singleton' }); container.load( new ContainerModule((bind, unbind, isBound, rebind) => { - bindCommon(bind); + bindBrowser(bind, unbind, isBound, rebind); bind(MessageService).toConstantValue({}); bind(BoardsService).toConstantValue({ getDetectedPorts() { diff --git a/arduino-ide-extension/src/test/browser/browser-test-bindings.ts b/arduino-ide-extension/src/test/browser/browser-test-bindings.ts index 9165765f7..7b950833b 100644 --- a/arduino-ide-extension/src/test/browser/browser-test-bindings.ts +++ b/arduino-ide-extension/src/test/browser/browser-test-bindings.ts @@ -1,8 +1,25 @@ +import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { Container, ContainerModule } from '@theia/core/shared/inversify'; -import { bindCommon } from '../common/common-test-bindings'; +import { + Bind, + ConsoleLogger, + bindCommon, +} from '../common/common-test-bindings'; -export function createBaseContainer(): Container { +export function createBaseContainer(bind: Bind = bindBrowser): Container { const container = new Container({ defaultScope: 'Singleton' }); - container.load(new ContainerModule((bind) => bindCommon(bind))); + container.load(new ContainerModule(bind)); return container; } + +export const bindBrowser: Bind = function ( + ...args: Parameters +): ReturnType { + bindCommon(...args); + const [bind, , , rebind] = args; + // IDE2's test console logger does not support `Loggable` arg. + // Rebind logger to suppress `[Function (anonymous)]` messages in tests when the storage service is initialized without `window.localStorage`. + // https://github.com/eclipse-theia/theia/blob/04c8cf07843ea67402131132e033cdd54900c010/packages/core/src/browser/storage-service.ts#L60 + bind(MockLogger).toSelf().inSingletonScope(); + rebind(ConsoleLogger).toService(MockLogger); +}; diff --git a/arduino-ide-extension/src/test/browser/update-arduino-state.test.ts b/arduino-ide-extension/src/test/browser/update-arduino-state.test.ts new file mode 100644 index 000000000..c3ad66ce9 --- /dev/null +++ b/arduino-ide-extension/src/test/browser/update-arduino-state.test.ts @@ -0,0 +1,670 @@ +import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; +const disableJSDOM = enableJSDOM(); + +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +FrontendApplicationConfigProvider.set({}); + +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { OpenerService } from '@theia/core/lib/browser/opener-service'; +import { + LocalStorageService, + StorageService, +} from '@theia/core/lib/browser/storage-service'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { Emitter } from '@theia/core/lib/common/event'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { wait } from '@theia/core/lib/common/promise-util'; +import URI from '@theia/core/lib/common/uri'; +import { + Container, + ContainerModule, + injectable, +} from '@theia/core/shared/inversify'; +import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { expect } from 'chai'; +import type { + BoardDetails as ApiBoardDetails, + CompileSummary as ApiCompileSummary, + Port as ApiPort, +} from 'vscode-arduino-api'; +import { URI as CodeURI } from 'vscode-uri'; +import { ArduinoPreferences } from '../../browser/arduino-preferences'; +import { BoardsDataStore } from '../../browser/boards/boards-data-store'; +import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider'; +import { ConfigServiceClient } from '../../browser/config/config-service-client'; +import { CommandRegistry } from '../../browser/contributions/contribution'; +import { + UpdateArduinoState, + UpdateStateParams, +} from '../../browser/contributions/update-arduino-state'; +import { DialogService } from '../../browser/dialog-service'; +import { SettingsService } from '../../browser/dialogs/settings/settings'; +import { HostedPluginSupport } from '../../browser/hosted/hosted-plugin-support'; +import { NotificationCenter } from '../../browser/notification-center'; +import { + CurrentSketch, + SketchesServiceClientImpl, +} from '../../browser/sketches-service-client-impl'; +import { ApplicationConnectionStatusContribution } from '../../browser/theia/core/connection-status-service'; +import { OutputChannelManager } from '../../browser/theia/output/output-channel'; +import { WorkspaceService } from '../../browser/theia/workspace/workspace-service'; +import { MainMenuManager } from '../../common/main-menu-manager'; +import { + CompileSummary, + FileSystemExt, + SketchesService, +} from '../../common/protocol'; +import { + BoardDetails, + BoardsService, + Port, +} from '../../common/protocol/boards-service'; +import { NotificationServiceServer } from '../../common/protocol/notification-service'; +import { never } from '../utils'; +import { bindBrowser } from './browser-test-bindings'; + +disableJSDOM(); + +describe('update-arduino-state', function () { + this.slow(250); + + let toDisposeAfterEach: DisposableCollection; + let boardsServiceProvider: BoardsServiceProvider; + let notificationCenter: NotificationCenter; + let commandRegistry: CommandRegistry; + let updateArduinoState: UpdateArduinoState; + let stateUpdateParams: UpdateStateParams[]; + + let boardDetailsMocks: Record; + let dataStoreMocks: Record; + let currentSketchMock: CurrentSketch | undefined; + let sketchDirUriMock: URI | undefined; + let dataDirUriMock: URI | undefined; + let onCurrentSketchDidChangeEmitter: Emitter; + let onDataDirDidChangeEmitter: Emitter; + let onSketchDirDidChangeEmitter: Emitter; + let onDataStoreDidChangeEmitter: Emitter; + + beforeEach(async () => { + toDisposeAfterEach = new DisposableCollection(); + stateUpdateParams = []; + + // reset mocks + boardDetailsMocks = {}; + dataStoreMocks = {}; + currentSketchMock = undefined; + sketchDirUriMock = undefined; + dataDirUriMock = undefined; + onCurrentSketchDidChangeEmitter = new Emitter(); + onDataDirDidChangeEmitter = new Emitter(); + onSketchDirDidChangeEmitter = new Emitter(); + onDataStoreDidChangeEmitter = new Emitter(); + toDisposeAfterEach.pushAll([ + onCurrentSketchDidChangeEmitter, + onDataDirDidChangeEmitter, + onSketchDirDidChangeEmitter, + onDataStoreDidChangeEmitter, + ]); + + const container = createContainer(); + commandRegistry = container.get(CommandRegistry); + // This command is registered by vscode-arduino-api + commandRegistry.registerCommand( + { id: 'arduinoAPI.updateState' }, + { + execute: (params: UpdateStateParams) => stateUpdateParams.push(params), + } + ); + // This command is contributed by the vscode-arduino-tools VSIX + commandRegistry.registerCommand( + { id: 'arduino.languageserver.notifyBuildDidComplete' }, + { + execute: () => { + /* NOOP */ + }, + } + ); + container.get( + FrontendApplicationStateService + ).state = 'ready'; + boardsServiceProvider = container.get( + BoardsServiceProvider + ); + notificationCenter = container.get(NotificationCenter); + updateArduinoState = container.get(UpdateArduinoState); + toDisposeAfterEach.push( + Disposable.create(() => boardsServiceProvider.onStop()) + ); + boardsServiceProvider.onStart(); + await boardsServiceProvider.ready; + updateArduinoState.onStart(); + + await wait(50); + stateUpdateParams = []; + }); + + afterEach(() => { + toDisposeAfterEach.dispose(); + }); + + it('should automatically update the boards config (board+port) on ready', async () => { + const fqbn = 'a:b:c'; + const board = { fqbn, name: 'ABC' }; + const boardDetails = { + buildProperties: [], + configOptions: [], + debuggingSupported: false, + fqbn, + PID: '0', + VID: '0', + programmers: [], + requiredTools: [], + }; + boardDetailsMocks = { + 'a:b:c': boardDetails, + }; + const port = { address: 'COM1', protocol: 'serial' }; + boardsServiceProvider['_boardsConfig'] = { + selectedBoard: board, + selectedPort: port, + }; + boardsServiceProvider['_detectedPorts'] = { + [Port.keyOf(port)]: { + port: { + address: 'COM1', + addressLabel: 'COM1 Port', + protocol: 'serial', + protocolLabel: 'Serial', + }, + boards: [], + }, + }; + + updateArduinoState.onReady(); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { key: 'fqbn', value: 'a:b:c' }, + { + key: 'boardDetails', + value: { + buildProperties: {}, + configOptions: [], + fqbn: 'a:b:c', + programmers: [], + toolsDependencies: [], + } as ApiBoardDetails, + }, + { + key: 'port', + value: { + address: 'COM1', + protocol: 'serial', + protocolLabel: 'Serial', + hardwareId: '', + label: 'COM1 Port', + properties: {}, + } as ApiPort, + }, + ]); + }); + + it('should automatically update the sketch path on ready', async () => { + const uri = 'file:///path/to/my_sketch'; + currentSketchMock = { + name: 'my_sketch', + uri, + mainFileUri: 'file:///path/to/my_sketch/my_sketch.ino', + additionalFileUris: [], + otherSketchFileUris: [], + rootFolderFileUris: [], + }; + + updateArduinoState.onReady(); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'sketchPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'sketchPath', + value: CodeURI.parse(uri).fsPath, + }, + ]); + }); + + it("should automatically update the 'directories.data' path on ready", async () => { + const uri = 'file:///path/to/data/dir'; + dataDirUriMock = new URI(uri); + + stateUpdateParams = []; + updateArduinoState.onReady(); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'dataDirPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'dataDirPath', + value: CodeURI.parse(uri).fsPath, + }, + ]); + }); + + it("should automatically update the 'directories.user' path on ready", async () => { + const uri = 'file:///path/to/sketchbook'; + sketchDirUriMock = new URI(uri); + + updateArduinoState.onReady(); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'userDirPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'userDirPath', + value: CodeURI.parse(uri).fsPath, + }, + ]); + }); + + it('should update the boards config (board only) when did change', async () => { + const fqbn = 'a:b:c'; + const board = { fqbn, name: 'ABC' }; + const boardDetails = { + buildProperties: [], + configOptions: [], + debuggingSupported: false, + fqbn, + PID: '0', + VID: '0', + programmers: [], + requiredTools: [], + }; + boardDetailsMocks = { + 'a:b:c': boardDetails, + }; + boardsServiceProvider.updateConfig(board); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { key: 'fqbn', value: 'a:b:c' }, + { + key: 'boardDetails', + value: { + buildProperties: {}, + configOptions: [], + fqbn: 'a:b:c', + programmers: [], + toolsDependencies: [], + } as ApiBoardDetails, + }, + { key: 'port', value: undefined }, + ]); + }); + + it('should update the boards config (port only) when did change', async () => { + const port = { address: 'COM1', protocol: 'serial' }; + notificationCenter.notifyDetectedPortsDidChange({ + detectedPorts: { + [Port.keyOf(port)]: { + port: { + address: 'COM1', + addressLabel: 'COM1 Port', + protocol: 'serial', + protocolLabel: 'Serial', + }, + boards: [], + }, + }, + }); + boardsServiceProvider.updateConfig(port); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { key: 'fqbn', value: undefined }, + { key: 'boardDetails', value: undefined }, + { + key: 'port', + value: { + address: 'COM1', + protocol: 'serial', + protocolLabel: 'Serial', + hardwareId: '', + label: 'COM1 Port', + properties: {}, + } as ApiPort, + }, + ]); + }); + + it('should update the boards config (board+port) when did change', async () => { + const fqbn = 'a:b:c'; + const board = { fqbn, name: 'ABC' }; + const boardDetails = { + buildProperties: [], + configOptions: [], + debuggingSupported: false, + fqbn, + PID: '0', + VID: '0', + programmers: [], + requiredTools: [], + }; + boardDetailsMocks = { + 'a:b:c': boardDetails, + }; + const port = { address: 'COM1', protocol: 'serial' }; + boardsServiceProvider.updateConfig({ + selectedBoard: board, + selectedPort: port, + }); + notificationCenter.notifyDetectedPortsDidChange({ + detectedPorts: { + [Port.keyOf(port)]: { + port: { + address: 'COM1', + addressLabel: 'COM1 Port', + protocol: 'serial', + protocolLabel: 'Serial', + }, + boards: [], + }, + }, + }); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { key: 'fqbn', value: 'a:b:c' }, + { + key: 'boardDetails', + value: { + buildProperties: {}, + configOptions: [], + fqbn: 'a:b:c', + programmers: [], + toolsDependencies: [], + } as ApiBoardDetails, + }, + { + key: 'port', + value: { + address: 'COM1', + protocol: 'serial', + protocolLabel: 'Serial', + hardwareId: '', + label: 'COM1 Port', + properties: {}, + } as ApiPort, + }, + ]); + }); + + it('should update the compile summary after a verify', async () => { + const summary: CompileSummary = { + buildPath: '/path/to/build', + buildProperties: [], + executableSectionsSize: [], + usedLibraries: [], + boardPlatform: undefined, + buildPlatform: undefined, + buildOutputUri: 'file:///path/to/build', + }; + await commandRegistry.executeCommand( + 'arduino.languageserver.notifyBuildDidComplete', + summary + ); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'compileSummary' + ); + expect(params).to.be.deep.equal([ + { + key: 'compileSummary', + value: { + buildPath: '/path/to/build', + buildProperties: {}, + executableSectionsSize: [], + usedLibraries: [], + boardPlatform: undefined, + buildPlatform: undefined, + } as ApiCompileSummary, + }, + ]); + }); + + it('should update the current sketch when did change', async () => { + const uri = 'file:///path/to/my_sketch'; + const sketch = { + name: 'my_sketch', + uri, + mainFileUri: 'file:///path/to/my_sketch/my_sketch.ino', + additionalFileUris: [], + otherSketchFileUris: [], + rootFolderFileUris: [], + }; + onCurrentSketchDidChangeEmitter.fire(sketch); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'sketchPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'sketchPath', + value: CodeURI.parse(uri).fsPath, + }, + ]); + }); + + it("should update the 'directories.data' when did change", async () => { + const uri = new URI('file:///path/to/data/dir'); + onDataDirDidChangeEmitter.fire(uri); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'dataDirPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'dataDirPath', + value: CodeURI.parse(uri.toString()).fsPath, + }, + ]); + }); + + it("should update the 'directories.user' when did change", async () => { + const uri = new URI('file:///path/to/sketchbook'); + onSketchDirDidChangeEmitter.fire(uri); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'userDirPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'userDirPath', + value: CodeURI.parse(uri.toString()).fsPath, + }, + ]); + }); + + it('should not update the board details when data store did change but the selected board does not match', async () => { + onDataStoreDidChangeEmitter.fire(['a:b:c']); + await wait(50); + + expect(stateUpdateParams).to.be.empty; + }); + + it('should update the board details when the data store did change and the selected board matches', async () => { + const fqbn = 'a:b:c'; + const board = { fqbn, name: 'ABC' }; + const boardDetails = { + buildProperties: [], + configOptions: [], + debuggingSupported: false, + fqbn, + PID: '0', + VID: '0', + programmers: [], + requiredTools: [], + }; + boardDetailsMocks = { + 'a:b:c': boardDetails, + }; + boardsServiceProvider['_boardsConfig'] = { + selectedBoard: board, + selectedPort: undefined, + }; + + onDataStoreDidChangeEmitter.fire(['a:b:c']); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { + key: 'boardDetails', + value: { + buildProperties: {}, + configOptions: [], + fqbn: 'a:b:c', + programmers: [], + toolsDependencies: [], + } as ApiBoardDetails, + }, + ]); + }); + + function createContainer(): Container { + const container = new Container({ defaultScope: 'Singleton' }); + container.load( + new ContainerModule((bind, unbind, isBound, rebind) => { + bindBrowser(bind, unbind, isBound, rebind); + bind(MessageService).toConstantValue({}); + bind(BoardsService).toConstantValue({ + getDetectedPorts() { + return {}; + }, + async getBoardDetails({ fqbn }) { + return boardDetailsMocks[fqbn]; + }, + }); + bind(NotificationCenter).toSelf().inSingletonScope(); + bind(NotificationServiceServer).toConstantValue(< + NotificationServiceServer + >{ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setClient(_) { + // nothing + }, + }); + bind(FrontendApplicationStateService).toSelf().inSingletonScope(); + bind(BoardsDataStore).toConstantValue({ + async getData(fqbn) { + if (!fqbn) { + return BoardsDataStore.Data.EMPTY; + } + const data = dataStoreMocks[fqbn] ?? BoardsDataStore.Data.EMPTY; + return data; + }, + get onChanged() { + return onDataStoreDidChangeEmitter.event; + }, + }); + bind(LocalStorageService).toSelf().inSingletonScope(); + bind(WindowService).toConstantValue({}); + bind(StorageService).toService(LocalStorageService); + bind(BoardsServiceProvider).toSelf().inSingletonScope(); + bind(NoopHostedPluginSupport).toSelf().inSingletonScope(); + bind(HostedPluginSupport).toService(NoopHostedPluginSupport); + bind(UpdateArduinoState).toSelf().inSingletonScope(); + bind(FileService).toConstantValue({}); + bind(FileSystemExt).toConstantValue({}); + bind(ConfigServiceClient).toConstantValue({ + tryGetSketchDirUri() { + return sketchDirUriMock; + }, + tryGetDataDirUri() { + return dataDirUriMock; + }, + get onDidChangeSketchDirUri() { + return onSketchDirDidChangeEmitter.event; + }, + get onDidChangeDataDirUri() { + return onDataDirDidChangeEmitter.event; + }, + }); + bind(SketchesService).toConstantValue({}); + bind(OpenerService).toConstantValue({}); + bind(SketchesServiceClientImpl).toConstantValue(< + SketchesServiceClientImpl + >{ + tryGetCurrentSketch() { + return currentSketchMock; + }, + onCurrentSketchDidChange: onCurrentSketchDidChangeEmitter.event, + }); + bind(EditorManager).toConstantValue({}); + bind(OutputChannelManager).toConstantValue({}); + bind(EnvVariablesServer).toConstantValue({}); + bind(ApplicationConnectionStatusContribution).toConstantValue( + {} + ); + bind(WorkspaceService).toConstantValue({}); + bind(LabelProvider).toConstantValue({}); + bind(SettingsService).toConstantValue({}); + bind(ArduinoPreferences).toConstantValue({}); + bind(DialogService).toConstantValue({}); + bind(MainMenuManager).toConstantValue({}); + }) + ); + return container; + } +}); + +@injectable() +class NoopHostedPluginSupport implements HostedPluginSupport { + readonly didStart = Promise.resolve(); + readonly onDidCloseConnection = never(); + readonly onDidLoad = never(); +} diff --git a/arduino-ide-extension/src/test/common/common-test-bindings.ts b/arduino-ide-extension/src/test/common/common-test-bindings.ts index 1c1892dc2..f3e3f4373 100644 --- a/arduino-ide-extension/src/test/common/common-test-bindings.ts +++ b/arduino-ide-extension/src/test/common/common-test-bindings.ts @@ -9,14 +9,25 @@ import { LogLevel } from '@theia/core/lib/common/logger-protocol'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { injectable, interfaces } from '@theia/core/shared/inversify'; -export function bindCommon(bind: interfaces.Bind): interfaces.Bind { +export interface Bind { + ( + bind: interfaces.Bind, + unbind: interfaces.Unbind, + isBound: interfaces.IsBound, + rebind: interfaces.Rebind + ): void; +} + +export const bindCommon: Bind = function ( + ...args: Parameters +): ReturnType { + const [bind] = args; bind(ConsoleLogger).toSelf().inSingletonScope(); bind(ILogger).toService(ConsoleLogger); bind(CommandRegistry).toSelf().inSingletonScope(); bind(CommandService).toService(CommandRegistry); bindContributionProvider(bind, CommandContribution); - return bind; -} +}; @injectable() export class ConsoleLogger extends MockLogger { diff --git a/arduino-ide-extension/src/test/node/node-test-bindings.ts b/arduino-ide-extension/src/test/node/node-test-bindings.ts index 2b4c651f7..29ff09f52 100644 --- a/arduino-ide-extension/src/test/node/node-test-bindings.ts +++ b/arduino-ide-extension/src/test/node/node-test-bindings.ts @@ -222,7 +222,7 @@ export async function createBaseContainer( } const container = new Container({ defaultScope: 'Singleton' }); const module = new ContainerModule((bind, unbind, isBound, rebind) => { - bindCommon(bind); + bindCommon(bind, unbind, isBound, rebind); bind(CoreClientProvider).toSelf().inSingletonScope(); bind(CoreServiceImpl).toSelf().inSingletonScope(); bind(CoreService).toService(CoreServiceImpl); diff --git a/arduino-ide-extension/src/test/utils.ts b/arduino-ide-extension/src/test/utils.ts index 799081227..148649126 100644 --- a/arduino-ide-extension/src/test/utils.ts +++ b/arduino-ide-extension/src/test/utils.ts @@ -1,3 +1,6 @@ -export function tick(): Promise { - return new Promise((res) => setTimeout(res, 1)); +import { Emitter, Event } from '@theia/core/lib/common/event'; + +const neverEmitter = new Emitter(); +export function never(): Event { + return neverEmitter.event as Event; } From 35ca43ab038e23b15de674e951e835c2523eb2b1 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 8 Nov 2023 12:04:44 +0100 Subject: [PATCH 8/8] 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 --- .eslintrc.js | 2 +- arduino-ide-extension/package.json | 3 +- .../browser/arduino-ide-frontend-module.ts | 35 +- .../src/browser/boards/boards-data-store.ts | 53 +- .../contributions/auto-select-programmer.ts | 123 + .../src/browser/contributions/debug.ts | 358 ++- .../src/browser/contributions/ino-language.ts | 114 +- .../browser/sketches-service-client-impl.ts | 31 +- .../debug/debug-configuration-manager.ts | 53 +- .../debug/debug-configuration-widget.tsx | 57 + .../theia/debug/debug-session-manager.ts | 39 + .../theia/monaco/monaco-editor-provider.ts | 38 +- .../theia/monaco/monaco-text-model-service.ts | 2 +- arduino-ide-extension/src/common/nls.ts | 14 + .../src/common/protocol/boards-service.ts | 30 +- .../src/common/protocol/sketches-service.ts | 4 +- .../src/node/board-discovery.ts | 42 +- .../src/node/boards-service-impl.ts | 81 +- .../cc/arduino/cli/commands/v1/board_pb.d.ts | 9 +- .../cc/arduino/cli/commands/v1/board_pb.js | 94 +- .../cli/commands/v1/commands_grpc_pb.d.ts | 64 +- .../cli/commands/v1/commands_grpc_pb.js | 105 +- .../arduino/cli/commands/v1/commands_pb.d.ts | 1 + .../cc/arduino/cli/commands/v1/commands_pb.js | 2 + .../cc/arduino/cli/commands/v1/common_pb.d.ts | 17 + .../cc/arduino/cli/commands/v1/common_pb.js | 123 + .../cc/arduino/cli/commands/v1/core_pb.d.ts | 9 + .../cc/arduino/cli/commands/v1/core_pb.js | 96 +- .../arduino/cli/commands/v1/debug_grpc_pb.js | 1 + .../cc/arduino/cli/commands/v1/debug_pb.d.ts | 274 ++ .../cc/arduino/cli/commands/v1/debug_pb.js | 2221 +++++++++++++++++ .../arduino/cli/debug/v1/debug_grpc_pb.d.ts | 59 - .../cc/arduino/cli/debug/v1/debug_grpc_pb.js | 95 - .../cc/arduino/cli/debug/v1/debug_pb.d.ts | 154 -- .../cc/arduino/cli/debug/v1/debug_pb.js | 1233 --------- .../cli/monitor/v1/monitor_grpc_pb.d.ts | 42 - .../arduino/cli/monitor/v1/monitor_grpc_pb.js | 65 - .../cc/arduino/cli/monitor/v1/monitor_pb.d.ts | 131 - .../cc/arduino/cli/monitor/v1/monitor_pb.js | 819 ------ .../src/node/sketches-service-impl.ts | 4 +- .../node/theia/plugin-ext/plugin-reader.ts | 31 +- .../browser/auto-select-programmer.test.ts | 176 ++ .../test/browser/boards-data-store.test.ts | 220 +- .../src/test/browser/browser-test-bindings.ts | 89 +- .../src/test/browser/debug.test.ts | 379 +++ .../test/browser/update-arduino-state.test.ts | 140 +- electron-app/package.json | 2 +- i18n/en.json | 4 + 48 files changed, 4697 insertions(+), 3041 deletions(-) create mode 100644 arduino-ide-extension/src/browser/contributions/auto-select-programmer.ts create mode 100644 arduino-ide-extension/src/browser/theia/debug/debug-configuration-widget.tsx create mode 100644 arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts create mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/debug_grpc_pb.js create mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/debug_pb.d.ts create mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/debug_pb.js delete mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/debug/v1/debug_grpc_pb.d.ts delete mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/debug/v1/debug_grpc_pb.js delete mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/debug/v1/debug_pb.d.ts delete mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/debug/v1/debug_pb.js delete mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb.d.ts delete mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb.js delete mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb.d.ts delete mode 100644 arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb.js create mode 100644 arduino-ide-extension/src/test/browser/auto-select-programmer.test.ts create mode 100644 arduino-ide-extension/src/test/browser/debug.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index da16cbb76..4bb67da9e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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/*', ], diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 8ce0e3d1a..2042054a0 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -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", @@ -169,7 +170,7 @@ ], "arduino": { "arduino-cli": { - "version": "0.34.0" + "version": "0.35.0-rc.7" }, "arduino-fwuploader": { "version": "2.4.1" diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index fa92aeafe..f481dfc21 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -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'; @@ -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 @@ -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 @@ -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 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(); diff --git a/arduino-ide-extension/src/browser/boards/boards-data-store.ts b/arduino-ide-extension/src/browser/boards/boards-data-store.ts index e6e34abf0..e78f5b74b 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-store.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts @@ -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 { @@ -74,7 +77,7 @@ export class BoardsDataStore const storedData = await this.storageService.getData(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); @@ -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([ @@ -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; } @@ -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 = value; + if (mutable.value === selectedValue) { + mutable.selected = true; updated = true; } else { - (value as any).selected = false; + mutable.selected = false; } } if (!updated) { @@ -245,9 +256,7 @@ export class BoardsDataStore return `.arduinoIDE-configOptions-${fqbn}`; } - protected async getBoardDetailsSafe( - fqbn: string - ): Promise { + async loadBoardDetails(fqbn: string): Promise { try { const details = await this.boardsService.getBoardDetails({ fqbn }); return details; @@ -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((arg).configOptions) && + Array.isArray((arg).programmers) && + ((arg).selectedProgrammer === undefined || + isProgrammer((arg).selectedProgrammer)) && + ((arg).defaultProgrammerId === undefined || + typeof (arg).defaultProgrammerId === 'string') ); } } @@ -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) ); } @@ -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 { diff --git a/arduino-ide-extension/src/browser/contributions/auto-select-programmer.ts b/arduino-ide-extension/src/browser/contributions/auto-select-programmer.ts new file mode 100644 index 000000000..0bf8e277e --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/auto-select-programmer.ts @@ -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 { + 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; + loadBoardDetails: (fqbn: string) => MaybePromise; + selectProgrammer(options: { + fqbn: string; + selectedProgrammer: Programmer; + }): MaybePromise; +} + +export async function ensureProgrammerIsSelected( + params: EnsureProgrammerIsSelectedParams +): Promise { + 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; +} diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index 8aebd0d82..d1f205051 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -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(); - private onDisabledMessageDidChange = - this.disabledMessageDidChangeEmitter.event; + private _message?: string = noBoardSelected; // Initial pessimism. + private didChangeMessageEmitter = new Emitter(); + 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, + onDidChange: this.onDidChangeMessage as Event, }; 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 - .selectedBoard - ): Promise { - 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; + private async updateMessage(): Promise { + 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 startDebug( + private async isDebugEnabled( board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig .selectedBoard - ): Promise { - if (!board) { - return; + ): Promise { + const debugFqbn = await isDebugEnabled( + board, + (fqbn) => this.boardService.getBoardDetails({ fqbn }), + (fqbn) => this.boardsDataStore.getData(fqbn), + (fqbn) => this.boardsDataStore.appendConfigToFqbn(fqbn), + (params) => this.boardService.checkDebugEnabled(params) + ); + return debugFqbn; + } + + private async startDebug( + board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig + .selectedBoard, + sketch: + | CurrentSketch + | undefined = this.sketchServiceClient.tryGetCurrentSketch() + ): Promise { + 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 { + return this.commandService.executeCommand( + 'arduino.debug.start', + params + ); } get compileForDebug(): boolean { @@ -254,7 +258,7 @@ export class Debug extends SketchContribution { return value === 'true'; } - async toggleCompileForDebug(): Promise { + 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 { if (err instanceof Error) { try { @@ -277,6 +281,48 @@ export class Debug extends SketchContribution { } return false; } + + private async createStartDebugParams( + board: BoardIdentifier | undefined + ): Promise { + 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, + getData: (fqbn: string) => MaybePromise, + appendConfigToFqbn: (fqbn: string) => MaybePromise, + checkDebugEnabled: (params: CheckDebugEnabledParams) => MaybePromise +): Promise { + 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 + ); +} diff --git a/arduino-ide-extension/src/browser/contributions/ino-language.ts b/arduino-ide-extension/src/browser/contributions/ino-language.ts index 5f9a3f127..5d1fe4638 100644 --- a/arduino-ide-extension/src/browser/contributions/ino-language.ts +++ b/arduino-ide-extension/src/browser/contributions/ino-language.ts @@ -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( - '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 { + return this.commandService.executeCommand( + 'arduino.languageserver.start', + params + ); + } } diff --git a/arduino-ide-extension/src/browser/sketches-service-client-impl.ts b/arduino-ide-extension/src/browser/sketches-service-client-impl.ts index f0186454c..9b3cdac94 100644 --- a/arduino-ide-extension/src/browser/sketches-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/sketches-service-client-impl.ts @@ -67,6 +67,7 @@ export class SketchesServiceClientImpl ); private _currentSketch: CurrentSketch | undefined; + private _currentIdeTempFolderUri: URI | undefined; private currentSketchLoaded = new Deferred(); onStart(): void { @@ -74,7 +75,10 @@ export class SketchesServiceClientImpl this.watchSketchbookDir(sketchDirUri); const refreshCurrentSketch = async () => { const currentSketch = await this.loadCurrentSketch(); - this.useCurrentSketch(currentSketch); + const ideTempFolderUri = await this.getIdeTempFolderUriForSketch( + currentSketch + ); + this.useCurrentSketch(currentSketch, ideTempFolderUri); }; this.toDispose.push( this.configService.onDidChangeSketchDirUri((sketchDirUri) => { @@ -141,7 +145,10 @@ export class SketchesServiceClientImpl } if (!Sketch.sameAs(this._currentSketch, reloadedSketch)) { - this.useCurrentSketch(reloadedSketch, true); + const ideTempFolderUri = await this.getIdeTempFolderUriForSketch( + reloadedSketch + ); + this.useCurrentSketch(reloadedSketch, ideTempFolderUri, true); } return; } @@ -179,11 +186,23 @@ export class SketchesServiceClientImpl ]); } + private async getIdeTempFolderUriForSketch( + sketch: CurrentSketch + ): Promise { + if (CurrentSketch.isValid(sketch)) { + const uri = await this.sketchesService.getIdeTempFolderUri(sketch); + return new URI(uri); + } + return undefined; + } + private useCurrentSketch( currentSketch: CurrentSketch, + ideTempFolderUri: URI | undefined, reassignPromise = false ) { this._currentSketch = currentSketch; + this._currentIdeTempFolderUri = ideTempFolderUri; if (reassignPromise) { this.currentSketchLoaded = new Deferred(); } @@ -273,6 +292,14 @@ export class SketchesServiceClientImpl return false; } + if ( + this._currentIdeTempFolderUri && + this._currentIdeTempFolderUri.resolve('launch.json').toString() === + toCheck.toString() + ) { + return false; + } + const isCloudSketch = toCheck .toString() .includes(`${REMOTE_SKETCHBOOK_FOLDER}/${ARDUINO_CLOUD_FOLDER}`); diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts index f877e0e12..6e0210a41 100644 --- a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts +++ b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-manager.ts @@ -1,44 +1,44 @@ -import debounce from 'p-debounce'; -import { inject, injectable } from '@theia/core/shared/inversify'; -import URI from '@theia/core/lib/common/uri'; -import { Event, Emitter } from '@theia/core/lib/common/event'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { DebugConfiguration } from '@theia/debug/lib/common/debug-common'; -import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { Emitter, Event } from '@theia/core/lib/common/event'; +import URI from '@theia/core/lib/common/uri'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; +import { DebugConfigurationModel as TheiaDebugConfigurationModel } from '@theia/debug/lib/browser/debug-configuration-model'; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-common'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { + FileOperationError, + FileOperationResult, +} from '@theia/filesystem/lib/common/files'; +import debounce from 'p-debounce'; import { SketchesService } from '../../../common/protocol'; import { CurrentSketch, SketchesServiceClientImpl, } from '../../sketches-service-client-impl'; +import { maybeUpdateReadOnlyState } from '../monaco/monaco-editor-provider'; import { DebugConfigurationModel } from './debug-configuration-model'; -import { - FileOperationError, - FileOperationResult, -} from '@theia/filesystem/lib/common/files'; -import { FileService } from '@theia/filesystem/lib/browser/file-service'; @injectable() export class DebugConfigurationManager extends TheiaDebugConfigurationManager { @inject(SketchesService) - protected readonly sketchesService: SketchesService; - + private readonly sketchesService: SketchesService; @inject(SketchesServiceClientImpl) - protected readonly sketchesServiceClient: SketchesServiceClientImpl; - + private readonly sketchesServiceClient: SketchesServiceClientImpl; @inject(FrontendApplicationStateService) - protected readonly appStateService: FrontendApplicationStateService; - + private readonly appStateService: FrontendApplicationStateService; @inject(FileService) - protected readonly fileService: FileService; + private readonly fileService: FileService; - protected onTempContentDidChangeEmitter = + private onTempContentDidChangeEmitter = new Emitter(); get onTempContentDidChange(): Event { return this.onTempContentDidChangeEmitter.event; } protected override async doInit(): Promise { + this.watchLaunchConfigEditor(); this.appStateService.reachedState('ready').then(async () => { const tempContent = await this.getTempLaunchJsonContent(); if (!tempContent) { @@ -75,6 +75,19 @@ export class DebugConfigurationManager extends TheiaDebugConfigurationManager { return super.doInit(); } + /** + * Sets a listener on current sketch change, and maybe updates the readonly state of the editor showing the debug configuration. aka the `launch.json`. + */ + private watchLaunchConfigEditor(): Disposable { + return this.sketchesServiceClient.onCurrentSketchDidChange(() => { + for (const widget of this.editorManager.all) { + maybeUpdateReadOnlyState(widget, (uri) => + this.sketchesServiceClient.isReadOnly(uri) + ); + } + }); + } + protected override updateModels = debounce(async () => { await this.appStateService.reachedState('ready'); const roots = await this.workspaceService.roots; @@ -111,7 +124,7 @@ export class DebugConfigurationManager extends TheiaDebugConfigurationManager { this.updateCurrent(); }, 500); - protected async getTempLaunchJsonContent(): Promise< + private async getTempLaunchJsonContent(): Promise< (TheiaDebugConfigurationModel.JsonContent & { uri: URI }) | URI | undefined > { const sketch = await this.sketchesServiceClient.currentSketch(); diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-configuration-widget.tsx b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-widget.tsx new file mode 100644 index 000000000..7d05a4b68 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/debug/debug-configuration-widget.tsx @@ -0,0 +1,57 @@ +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { nls } from '@theia/core/lib/common/nls'; +import { injectable } from '@theia/core/shared/inversify'; +import React from '@theia/core/shared/react'; +import { DebugAction } from '@theia/debug/lib/browser/view/debug-action'; +import { DebugConfigurationSelect as TheiaDebugConfigurationSelect } from '@theia/debug/lib/browser/view/debug-configuration-select'; +import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget'; + +/** + * Patched to programmatically update the debug config