Skip to content

feat: new window inherits the custom board options #2289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
204 changes: 153 additions & 51 deletions arduino-ide-extension/src/browser/boards/boards-data-store.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string[]>();
private readonly toDispose = new DisposableCollection(this.onChangedEmitter);
private readonly onDidChangeEmitter =
new Emitter<BoardsDataStoreChangeEvent>();
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<ConfigOption[]>(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<BoardsDataStore.Data>(key);
if (!storedData) {
// if not previously value is available for the board, do not update the cache
continue;
}
const details = await this.getBoardDetailsSafe(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<BoardsDataStoreChange | undefined> {
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<void> {
this._selectedBoardData = await this.getSelectedBoardData(fqbn);
}

onStop(): void {
this.toDispose.dispose();
}

get onChanged(): Event<string[]> {
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<BoardsDataStoreChangeEvent> {
return this.onDidChangeEmitter.event;
}

async appendConfigToFqbn(
Expand All @@ -84,22 +167,19 @@ 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);
if (!boardDetails) {
return BoardsDataStore.Data.EMPTY;
}

data = {
configOptions: boardDetails.configOptions,
programmers: boardDetails.programmers,
};
const data = createDataStoreEntry(boardDetails);
await this.storageService.setData(key, data);
return data;
}
Expand All @@ -111,17 +191,15 @@ export class BoardsDataStore implements FrontendApplicationContribution {
fqbn: string;
selectedProgrammer: Programmer;
}): Promise<boolean> {
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;
}

Expand Down Expand Up @@ -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<void> {
protected async setData(change: BoardsDataStoreChange): Promise<void> {
const { fqbn, data } = change;
const key = this.getStorageKey(fqbn);
return this.storageService.setData(key, data);
}
Expand All @@ -176,7 +249,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
fqbn: string
): Promise<BoardDetails | undefined> {
try {
const details = this.boardsService.getBoardDetails({ fqbn });
const details = await this.boardsService.getBoardDetails({ fqbn });
return details;
} catch (err) {
if (
Expand All @@ -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 });
}
}

Expand All @@ -209,10 +282,10 @@ 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 {
return (
!!arg &&
Expand All @@ -224,3 +297,32 @@ export namespace BoardsDataStore {
}
}
}

function createDataStoreEntry(details: BoardDetails): BoardsDataStore.Data {
return {
configOptions: details.configOptions.slice(),
programmers: details.programmers.slice(),
};
}

export interface BoardsDataStoreChange {
readonly fqbn: string;
readonly data: BoardsDataStore.Data;
}

function isBoardsDataStoreChange(arg: unknown): arg is BoardsDataStoreChange {
return (
typeof arg === 'object' &&
arg !== null &&
typeof (<BoardsDataStoreChange>arg).fqbn === 'string' &&
BoardsDataStore.Data.is((<BoardsDataStoreChange>arg).data)
);
}

export interface BoardsDataStoreChangeEvent {
readonly changes: readonly BoardsDataStoreChange[];
}

const USE_INHERITED_DATA: Command = {
id: 'arduino-use-inherited-boards-data',
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
10 changes: 5 additions & 5 deletions arduino-ide-extension/src/browser/contributions/ino-language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,21 @@ 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) {
throw new Error(
`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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}),
Expand Down
Loading