Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 42d017e

Browse files
dankeboy36kittaakos
authored andcommitted
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 <dankeboy36@gmail.com>
1 parent c66b720 commit 42d017e

File tree

13 files changed

+569
-38
lines changed

13 files changed

+569
-38
lines changed

‎arduino-ide-extension/package.json‎

Lines changed: 2 additions & 1 deletion
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",

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

Lines changed: 2 additions & 0 deletions
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
Lines changed: 179 additions & 0 deletions
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+
}

‎arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts‎

Lines changed: 40 additions & 7 deletions
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+
}
Lines changed: 126 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /