Skip to content

Commit 4d79caa

Browse files
committed
feat: expose Arduino state to VS Code extensions
- Update a shared state on fqbn, port, sketch path, and etc. changes. VS Code extensions can access it and listen on changes. - Force VISX activation order: API VSIX starts first. Signed-off-by: dankeboy36 <[email protected]>
1 parent d79bc0d commit 4d79caa

13 files changed

+569
-38
lines changed

Diff for: arduino-ide-extension/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@
104104
"temp": "^0.9.1",
105105
"temp-dir": "^2.0.0",
106106
"tree-kill": "^1.2.1",
107-
"util": "^0.12.5"
107+
"util": "^0.12.5",
108+
"vscode-arduino-api": "^0.1.2"
108109
},
109110
"devDependencies": {
110111
"@octokit/rest": "^18.12.0",

Diff for: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesy
354354
import { StylingParticipant } from '@theia/core/lib/browser/styling-service';
355355
import { MonacoEditorMenuContribution } from './theia/monaco/monaco-menu';
356356
import { MonacoEditorMenuContribution as TheiaMonacoEditorMenuContribution } from '@theia/monaco/lib/browser/monaco-menu';
357+
import { UpdateArduinoState } from './contributions/update-arduino-state';
357358

358359
// Hack to fix copy/cut/paste issue after electron version update in Theia.
359360
// https://github.com/eclipse-theia/theia/issues/12487
@@ -747,6 +748,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
747748
Contribution.configure(bind, Account);
748749
Contribution.configure(bind, CloudSketchbookContribution);
749750
Contribution.configure(bind, CreateCloudCopy);
751+
Contribution.configure(bind, UpdateArduinoState);
750752

751753
bindContributionProvider(bind, StartupTaskProvider);
752754
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
2+
import URI from '@theia/core/lib/common/uri';
3+
import { inject, injectable } from '@theia/core/shared/inversify';
4+
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
5+
import type { ArduinoState } from 'vscode-arduino-api';
6+
import {
7+
BoardsService,
8+
CompileSummary,
9+
Port,
10+
isCompileSummary,
11+
} from '../../common/protocol';
12+
import {
13+
toApiBoardDetails,
14+
toApiCompileSummary,
15+
toApiPort,
16+
} from '../../common/protocol/arduino-context-mapper';
17+
import type { BoardsConfig } from '../boards/boards-config';
18+
import { BoardsDataStore } from '../boards/boards-data-store';
19+
import { BoardsServiceProvider } from '../boards/boards-service-provider';
20+
import { CurrentSketch } from '../sketches-service-client-impl';
21+
import { SketchContribution } from './contribution';
22+
23+
interface UpdateStateParams<T extends ArduinoState> {
24+
readonly key: keyof T;
25+
readonly value: T[keyof T];
26+
}
27+
28+
/**
29+
* Contribution for updating the Arduino state, such as the FQBN, selected port, and sketch path changes via commands, so other VS Code extensions can access it.
30+
* See [`vscode-arduino-api`](https://github.com/dankeboy36/vscode-arduino-api#api) for more details.
31+
*/
32+
@injectable()
33+
export class UpdateArduinoState extends SketchContribution {
34+
@inject(BoardsService)
35+
private readonly boardsService: BoardsService;
36+
@inject(BoardsServiceProvider)
37+
private readonly boardsServiceProvider: BoardsServiceProvider;
38+
@inject(BoardsDataStore)
39+
private readonly boardsDataStore: BoardsDataStore;
40+
@inject(HostedPluginSupport)
41+
private readonly hostedPluginSupport: HostedPluginSupport;
42+
43+
private readonly toDispose = new DisposableCollection();
44+
45+
override onStart(): void {
46+
this.toDispose.pushAll([
47+
this.boardsServiceProvider.onBoardsConfigChanged((config) =>
48+
this.updateBoardsConfig(config)
49+
),
50+
this.sketchServiceClient.onCurrentSketchDidChange((sketch) =>
51+
this.updateSketchPath(sketch)
52+
),
53+
this.configService.onDidChangeDataDirUri((dataDirUri) =>
54+
this.updateDataDirPath(dataDirUri)
55+
),
56+
this.configService.onDidChangeSketchDirUri((userDirUri) =>
57+
this.updateUserDirPath(userDirUri)
58+
),
59+
this.commandService.onDidExecuteCommand(({ commandId, args }) => {
60+
if (
61+
commandId === 'arduino.languageserver.notifyBuildDidComplete' &&
62+
isCompileSummary(args[0])
63+
) {
64+
this.updateCompileSummary(args[0]);
65+
}
66+
}),
67+
this.boardsDataStore.onChanged((fqbn) => {
68+
const selectedFqbn =
69+
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
70+
if (selectedFqbn && fqbn.includes(selectedFqbn)) {
71+
this.updateBoardDetails(selectedFqbn);
72+
}
73+
}),
74+
]);
75+
}
76+
77+
override onReady(): void {
78+
this.boardsServiceProvider.reconciled.then(() => {
79+
this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig);
80+
});
81+
this.updateSketchPath(this.sketchServiceClient.tryGetCurrentSketch());
82+
this.updateUserDirPath(this.configService.tryGetSketchDirUri());
83+
this.updateDataDirPath(this.configService.tryGetDataDirUri());
84+
}
85+
86+
onStop(): void {
87+
this.toDispose.dispose();
88+
}
89+
90+
private async updateSketchPath(
91+
sketch: CurrentSketch | undefined
92+
): Promise<void> {
93+
const sketchPath = CurrentSketch.isValid(sketch)
94+
? new URI(sketch.uri).path.fsPath()
95+
: undefined;
96+
return this.updateState({ key: 'sketchPath', value: sketchPath });
97+
}
98+
99+
private async updateCompileSummary(
100+
compileSummary: CompileSummary
101+
): Promise<void> {
102+
const apiCompileSummary = toApiCompileSummary(compileSummary);
103+
return this.updateState({
104+
key: 'compileSummary',
105+
value: apiCompileSummary,
106+
});
107+
}
108+
109+
private async updateBoardsConfig(
110+
boardsConfig: BoardsConfig.Config
111+
): Promise<void> {
112+
const fqbn = boardsConfig.selectedBoard?.fqbn;
113+
const port = boardsConfig.selectedPort;
114+
await this.updateFqbn(fqbn);
115+
await this.updateBoardDetails(fqbn);
116+
await this.updatePort(port);
117+
}
118+
119+
private async updateFqbn(fqbn: string | undefined): Promise<void> {
120+
await this.updateState({ key: 'fqbn', value: fqbn });
121+
}
122+
123+
private async updateBoardDetails(fqbn: string | undefined): Promise<void> {
124+
const unset = () =>
125+
this.updateState({ key: 'boardDetails', value: undefined });
126+
if (!fqbn) {
127+
return unset();
128+
}
129+
const [details, persistedData] = await Promise.all([
130+
this.boardsService.getBoardDetails({ fqbn }),
131+
this.boardsDataStore.getData(fqbn),
132+
]);
133+
if (!details) {
134+
return unset();
135+
}
136+
const apiBoardDetails = toApiBoardDetails({
137+
...details,
138+
configOptions:
139+
BoardsDataStore.Data.EMPTY === persistedData
140+
? details.configOptions
141+
: persistedData.configOptions.slice(),
142+
});
143+
return this.updateState({
144+
key: 'boardDetails',
145+
value: apiBoardDetails,
146+
});
147+
}
148+
149+
private async updatePort(port: Port | undefined): Promise<void> {
150+
const apiPort = port && toApiPort(port);
151+
return this.updateState({ key: 'port', value: apiPort });
152+
}
153+
154+
private async updateUserDirPath(userDirUri: URI | undefined): Promise<void> {
155+
const userDirPath = userDirUri?.path.fsPath();
156+
return this.updateState({
157+
key: 'userDirPath',
158+
value: userDirPath,
159+
});
160+
}
161+
162+
private async updateDataDirPath(dataDirUri: URI | undefined): Promise<void> {
163+
const dataDirPath = dataDirUri?.path.fsPath();
164+
return this.updateState({
165+
key: 'dataDirPath',
166+
value: dataDirPath,
167+
});
168+
}
169+
170+
private async updateState<T extends ArduinoState>(
171+
params: UpdateStateParams<T>
172+
): Promise<void> {
173+
await this.hostedPluginSupport.didStart;
174+
return this.commandService.executeCommand(
175+
'arduinoAPI.updateState',
176+
params
177+
);
178+
}
179+
}

Diff for: arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts

+40-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { Emitter, Event, JsonRpcProxy } from '@theia/core';
1+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
2+
import { Emitter, Event } from '@theia/core/lib/common/event';
23
import { injectable, interfaces } from '@theia/core/shared/inversify';
3-
import { HostedPluginServer } from '@theia/plugin-ext/lib/common/plugin-protocol';
4-
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
4+
import {
5+
PluginContributions,
6+
HostedPluginSupport as TheiaHostedPluginSupport,
7+
} from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
58

69
@injectable()
710
export class HostedPluginSupport extends TheiaHostedPluginSupport {
@@ -10,7 +13,7 @@ export class HostedPluginSupport extends TheiaHostedPluginSupport {
1013

1114
override onStart(container: interfaces.Container): void {
1215
super.onStart(container);
13-
this.hostedPluginServer.onDidCloseConnection(() =>
16+
this['server'].onDidCloseConnection(() =>
1417
this.onDidCloseConnectionEmitter.fire()
1518
);
1619
}
@@ -28,8 +31,38 @@ export class HostedPluginSupport extends TheiaHostedPluginSupport {
2831
return this.onDidCloseConnectionEmitter.event;
2932
}
3033

31-
private get hostedPluginServer(): JsonRpcProxy<HostedPluginServer> {
32-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33-
return (this as any).server;
34+
protected override startPlugins(
35+
contributionsByHost: Map<string, PluginContributions[]>,
36+
toDisconnect: DisposableCollection
37+
): Promise<void> {
38+
reorderPlugins(contributionsByHost);
39+
return super.startPlugins(contributionsByHost, toDisconnect);
3440
}
3541
}
42+
43+
/**
44+
* Force the `vscode-arduino-ide` API to activate before any Arduino IDE tool VSIX.
45+
*
46+
* Arduino IDE tool VISXs are not forced to declare the `vscode-arduino-api` as a `extensionDependencies`,
47+
* but the API must activate before any tools. This in place sorting helps to bypass Theia's plugin resolution
48+
* without forcing tools developers to add `vscode-arduino-api` to the `extensionDependencies`.
49+
*/
50+
function reorderPlugins(
51+
contributionsByHost: Map<string, PluginContributions[]>
52+
): void {
53+
for (const [, contributions] of contributionsByHost) {
54+
const apiPluginIndex = contributions.findIndex(isArduinoAPI);
55+
if (apiPluginIndex >= 0) {
56+
const apiPlugin = contributions[apiPluginIndex];
57+
contributions.splice(apiPluginIndex, 1);
58+
contributions.unshift(apiPlugin);
59+
}
60+
}
61+
}
62+
63+
function isArduinoAPI(pluginContribution: PluginContributions): boolean {
64+
return (
65+
pluginContribution.plugin.metadata.model.id ===
66+
'dankeboy36.vscode-arduino-api'
67+
);
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type {
2+
Port as APIPort,
3+
BoardDetails as ApiBoardDetails,
4+
BuildProperties as ApiBuildProperties,
5+
CompileSummary as ApiCompileSummary,
6+
ConfigOption as ApiConfigOption,
7+
ConfigValue as ApiConfigValue,
8+
Tool as ApiTool,
9+
} from 'vscode-arduino-api';
10+
import type {
11+
BoardDetails,
12+
CompileSummary,
13+
ConfigOption,
14+
ConfigValue,
15+
Port,
16+
Tool,
17+
} from '../protocol';
18+
19+
export function toApiCompileSummary(
20+
compileSummary: CompileSummary
21+
): ApiCompileSummary {
22+
const {
23+
buildPath,
24+
buildProperties,
25+
boardPlatform,
26+
buildPlatform,
27+
executableSectionsSize,
28+
usedLibraries,
29+
} = compileSummary;
30+
return {
31+
buildPath,
32+
buildProperties: toApiBuildProperties(buildProperties),
33+
executableSectionsSize: executableSectionsSize,
34+
boardPlatform,
35+
buildPlatform,
36+
usedLibraries,
37+
};
38+
}
39+
40+
export function toApiPort(port: Port): APIPort | undefined {
41+
const {
42+
hardwareId = '',
43+
properties = {},
44+
address,
45+
protocol,
46+
protocolLabel,
47+
addressLabel: label,
48+
} = port;
49+
return {
50+
label,
51+
address,
52+
hardwareId,
53+
properties,
54+
protocol,
55+
protocolLabel,
56+
};
57+
}
58+
59+
export function toApiBoardDetails(boardDetails: BoardDetails): ApiBoardDetails {
60+
const { fqbn, programmers, configOptions, requiredTools } = boardDetails;
61+
return {
62+
buildProperties: toApiBuildProperties(boardDetails.buildProperties),
63+
configOptions: configOptions.map(toApiConfigOption),
64+
fqbn,
65+
programmers,
66+
toolsDependencies: requiredTools.map(toApiTool),
67+
};
68+
}
69+
70+
function toApiConfigOption(configOption: ConfigOption): ApiConfigOption {
71+
const { label, values, option } = configOption;
72+
return {
73+
optionLabel: label,
74+
option,
75+
values: values.map(toApiConfigValue),
76+
};
77+
}
78+
79+
function toApiConfigValue(configValue: ConfigValue): ApiConfigValue {
80+
const { label, selected, value } = configValue;
81+
return {
82+
selected,
83+
value,
84+
valueLabel: label,
85+
};
86+
}
87+
88+
function toApiTool(toolDependency: Tool): ApiTool {
89+
const { name, packager, version } = toolDependency;
90+
return {
91+
name,
92+
packager,
93+
version,
94+
};
95+
}
96+
97+
const propertySep = '=';
98+
99+
function parseProperty(
100+
property: string
101+
): [key: string, value: string] | undefined {
102+
const segments = property.split(propertySep);
103+
if (segments.length < 2) {
104+
console.warn(`Could not parse build property: ${property}.`);
105+
return undefined;
106+
}
107+
108+
const [key, ...rest] = segments;
109+
if (!key) {
110+
console.warn(`Could not determine property key from raw: ${property}.`);
111+
return undefined;
112+
}
113+
const value = rest.join(propertySep);
114+
return [key, value];
115+
}
116+
117+
export function toApiBuildProperties(properties: string[]): ApiBuildProperties {
118+
return properties.reduce((acc, curr) => {
119+
const entry = parseProperty(curr);
120+
if (entry) {
121+
const [key, value] = entry;
122+
acc[key] = value;
123+
}
124+
return acc;
125+
}, <Record<string, string>>{});
126+
}

0 commit comments

Comments
 (0)