Skip to content

Commit 64c040c

Browse files
committed
IntelliSense for "platformio.ini" // Resolve platformio#3167
1 parent c80151d commit 64c040c

File tree

3 files changed

+338
-11
lines changed

3 files changed

+338
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
**Requires VSCode 1.65 or above**
66

77
- Project Management
8+
* IntelliSense for [platformio.ini](https://docs.platformio.org/en/latest/projectconf/index.html) configuration file
9+
- Auto-completion for configuration options
10+
- Auto-completion for choice-based option values
11+
- Hover over the option and get a quick documentation
12+
- Realtime serial port auto-completion for port-related options
13+
- Quickly jump to the development platform or library in the PlatormIO Registry
814
* Native integration of [PlatformIO Unit Testing](https://docs.platformio.org/en/latest/advanced/unit-testing/index.html) with VSCode Testing UI
915
* New port switcher to override upload, monitor, or testing port (issue [#545](https://github.com/platformio/platformio-vscode-ide/issues/545))
1016
* Advanced project configuring progress with logging and canceling features

src/project/config.js

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/**
2+
* Copyright (c) 2017-present PlatformIO <contact@platformio.org>
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
9+
import * as pioNodeHelpers from 'platformio-node-helpers';
10+
11+
import { disposeSubscriptions, listCoreSerialPorts } from '../utils';
12+
import vscode from 'vscode';
13+
14+
export class ProjectConfigLanguageProvider {
15+
static DOCUMENT_SELECTOR = { language: 'ini' };
16+
SCOPE_PLATFORMIO = 'platformio';
17+
SCOPE_ENV = 'env';
18+
19+
constructor(projectDir) {
20+
this.projectDir = projectDir;
21+
this.subscriptions = [
22+
vscode.languages.registerHoverProvider(
23+
ProjectConfigLanguageProvider.DOCUMENT_SELECTOR,
24+
{
25+
provideHover: async (document, position) =>
26+
await this.provideHover(document, position),
27+
}
28+
),
29+
vscode.languages.registerCompletionItemProvider(
30+
ProjectConfigLanguageProvider.DOCUMENT_SELECTOR,
31+
{
32+
provideCompletionItems: async (document, position, token, context) =>
33+
await this.provideCompletionItems(document, position, token, context),
34+
}
35+
),
36+
];
37+
// if (vscode.languages.registerInlineCompletionItemProvider) {
38+
// this.subscriptions.push(
39+
// vscode.languages.registerInlineCompletionItemProvider(
40+
// ProjectConfigLanguageProvider.DOCUMENT_SELECTOR,
41+
// {
42+
// provideInlineCompletionItems: async (document, position) =>
43+
// await this.provideCompletionItems(document, position, true),
44+
// }
45+
// )
46+
// );
47+
// }
48+
this._options = undefined;
49+
this._ports = undefined;
50+
}
51+
52+
dispose() {
53+
disposeSubscriptions(this.subscriptions);
54+
}
55+
56+
async getOptions() {
57+
if (this._options) {
58+
return this._options;
59+
}
60+
const script = `
61+
import json
62+
try:
63+
from platformio.public import get_config_options_schema
64+
except ImportError:
65+
from platformio.project.options import get_config_options_schema
66+
67+
print(json.dumps(get_config_options_schema()))
68+
`;
69+
const output = await pioNodeHelpers.core.getCorePythonCommandOutput(
70+
['-c', script],
71+
{ projectDir: this.projectDir }
72+
);
73+
this._options = JSON.parse(output.trim());
74+
return this._options;
75+
}
76+
77+
renderOptionDocs(option) {
78+
const attrs = [
79+
['Name', option.name],
80+
['Group', option.group],
81+
['Type', option.type],
82+
['Multiple', option.multiple ? 'yes' : 'no'],
83+
];
84+
if (option.sysenvvar) {
85+
attrs.push(['EnvironmentVariable', option.sysenvvar]);
86+
}
87+
if (option.type === 'choice') {
88+
attrs.push(['Choices', option.choices.join(', ')]);
89+
}
90+
if (option.min !== undefined) {
91+
attrs.push(['Minimum', option.min]);
92+
}
93+
if (option.max !== undefined) {
94+
attrs.push(['Maximum', option.max]);
95+
}
96+
if (option.default !== null || option.type === 'boolean') {
97+
let value = option.default;
98+
if (option.type === 'boolean') {
99+
value = option.default ? 'yes' : 'no';
100+
} else if (option.multiple && Array.isArray(option.default)) {
101+
value = option.default.join(', ');
102+
}
103+
attrs.push(['Default', value]);
104+
}
105+
const docs = new vscode.MarkdownString();
106+
docs.appendCodeblock(
107+
attrs.map(([name, value]) => `${name} = ${value}`).join('\n'),
108+
'ini'
109+
);
110+
docs.appendMarkdown(`
111+
${option.description}
112+
113+
[View documentation](https://docs.platformio.org/en/latest/projectconf/sections/${option.scope}/options/${option.group}/${option.name}.html?utm_source=vscode&utm_medium=completion)
114+
`);
115+
return docs;
116+
}
117+
118+
getScopeAt(document, position) {
119+
const text = document.getText(
120+
new vscode.Range(new vscode.Position(0, 0), position)
121+
);
122+
for (const line of text.split('\n').reverse()) {
123+
if (line.startsWith('[platformio]')) {
124+
return this.SCOPE_PLATFORMIO;
125+
} else if (line.startsWith('[env]') || line.startsWith('[env:')) {
126+
return this.SCOPE_ENV;
127+
}
128+
}
129+
return undefined;
130+
}
131+
132+
async getOptionAt(document, position) {
133+
for (let lineNum = position.line; lineNum > 0; lineNum--) {
134+
const line = document.lineAt(lineNum).text;
135+
if (line.startsWith(' ') || line.startsWith('\t')) {
136+
continue;
137+
}
138+
const optionName = line.split('=')[0].trim();
139+
return (await this.getOptions()).find((option) => option.name === optionName);
140+
}
141+
}
142+
143+
isOptionValueLocation(document, position) {
144+
const line = document.lineAt(position.line).text;
145+
const sepPos = line.indexOf('=');
146+
return (
147+
line.startsWith(' ') ||
148+
line.startsWith('\t') ||
149+
(sepPos > 0 && position.character > sepPos)
150+
);
151+
}
152+
153+
async provideHover(document, position) {
154+
const word = document.getText(document.getWordRangeAtPosition(position));
155+
const option = (await this.getOptions()).find((option) => option.name === word);
156+
if (option) {
157+
return new vscode.Hover(this.renderOptionDocs(option));
158+
}
159+
return this.providePackageHover(document, position);
160+
}
161+
162+
async providePackageHover(document, position) {
163+
const line = document.lineAt(position.line).text;
164+
let rawValue = undefined;
165+
if (line.startsWith(' ') || line.startsWith('\t')) {
166+
rawValue = line;
167+
} else if (line.includes('=')) {
168+
rawValue = line.split('=', 2)[1];
169+
}
170+
if (!rawValue) {
171+
return;
172+
}
173+
const pkgRegExp = /^(([a-z\d_\-]+)\/)?([a-z\d\_\- ]+)/i;
174+
const matches = pkgRegExp.exec(rawValue.trim());
175+
if (!matches) {
176+
return;
177+
}
178+
179+
const option = await this.getOptionAt(document, position);
180+
if (!['platform', 'lib_deps'].includes(option.name)) {
181+
return;
182+
}
183+
184+
const pkgOwner = matches[2];
185+
const pkgName = matches[3];
186+
const pkgUrlParts = ['https://registry.platformio.org'];
187+
if (pkgOwner) {
188+
pkgUrlParts.push(option.name === 'platform' ? 'platforms' : 'libraries');
189+
pkgUrlParts.push(pkgOwner.trim(), encodeURIComponent(pkgName.trim()));
190+
} else {
191+
const qs = new URLSearchParams();
192+
qs.set('t', option.group);
193+
qs.set('q', `name:"${pkgName.trim()}"`);
194+
pkgUrlParts.push(`search?${qs.toString()}`);
195+
}
196+
197+
return new vscode.Hover(
198+
new vscode.MarkdownString(
199+
`[Open in PlatformIO Registry](${pkgUrlParts.join('/')})`
200+
)
201+
);
202+
}
203+
204+
async provideCompletionItems(document, position, token, context, isInline = false) {
205+
if (token.isCancellationRequested) {
206+
return;
207+
}
208+
return await (this.isOptionValueLocation(document, position)
209+
? this.provideCompletionValues(document, position, isInline)
210+
: this.provideCompletionOptions(document, position, isInline));
211+
}
212+
213+
async provideCompletionOptions(document, position, isInline = false) {
214+
const scope = this.getScopeAt(document, position);
215+
if (!scope) {
216+
return;
217+
}
218+
const options = await this.getOptions();
219+
return options
220+
.filter((option) => option.scope === scope)
221+
.map((option) => {
222+
if (isInline) {
223+
return new vscode.InlineCompletionItem(option.name);
224+
}
225+
const item = new vscode.CompletionItem(
226+
option.name,
227+
vscode.CompletionItemKind.Field
228+
);
229+
item.documentation = this.renderOptionDocs(option);
230+
return item;
231+
});
232+
}
233+
234+
async provideCompletionValues(document, position) {
235+
const option = await this.getOptionAt(document, position);
236+
if (!option) {
237+
return;
238+
}
239+
switch (option.name) {
240+
case 'upload_port':
241+
case 'monitor_port':
242+
case 'test_port':
243+
return await this.provideCompletionPorts();
244+
245+
case 'upload_speed':
246+
case 'monitor_speed':
247+
case 'test_speed':
248+
return await this.provideCompletionBaudrates(option);
249+
}
250+
return this.provideTypedCompletionValues(option);
251+
}
252+
253+
async provideTypedCompletionValues(option) {
254+
const values = [];
255+
let defaultValue = option.default;
256+
switch (option.type) {
257+
case 'boolean':
258+
values.push('yes', 'no');
259+
defaultValue = option.default ? 'yes' : 'no';
260+
break;
261+
case 'choice':
262+
option.choices.forEach((item) => values.push(item));
263+
break;
264+
265+
case 'integer range':
266+
for (let i = option.min; i <= option.max; i++) {
267+
values.push(i);
268+
}
269+
break;
270+
}
271+
return values.map((value) => {
272+
const item = new vscode.CompletionItem(
273+
value.toString(),
274+
vscode.CompletionItemKind.EnumMember
275+
);
276+
item.preselect = defaultValue === value;
277+
return item;
278+
});
279+
}
280+
281+
createCustomCompletionValueItem() {
282+
const item = new vscode.CompletionItem('Custom', vscode.CompletionItemKind.Value);
283+
item.insertText = '';
284+
item.sortText = 'Z';
285+
return item;
286+
}
287+
288+
async provideCompletionPorts() {
289+
if (!this._ports) {
290+
this._ports = await listCoreSerialPorts();
291+
setTimeout(() => (this._ports = undefined), 3000);
292+
}
293+
const items = (this._ports || []).map((port) => {
294+
const item = new vscode.CompletionItem(
295+
port.port,
296+
vscode.CompletionItemKind.Value
297+
);
298+
item.detail = port.description;
299+
item.documentation = port.hwid;
300+
return item;
301+
});
302+
items.push(this.createCustomCompletionValueItem());
303+
return items;
304+
}
305+
306+
async provideCompletionBaudrates(option) {
307+
const values = [
308+
600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 230400,
309+
];
310+
const items = values.map((value, index) => {
311+
const item = new vscode.CompletionItem(
312+
value.toString(),
313+
vscode.CompletionItemKind.Value
314+
);
315+
item.sortText = String.fromCharCode(index + 65);
316+
item.preselect = option.default === value;
317+
return item;
318+
});
319+
items.push(this.createCustomCompletionValueItem());
320+
return items;
321+
}
322+
}

src/project/manager.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as pioNodeHelpers from 'platformio-node-helpers';
1010
import * as projectHelpers from './helpers';
1111

1212
import { disposeSubscriptions, notifyError } from '../utils';
13+
import { ProjectConfigLanguageProvider } from './config';
1314
import ProjectTaskManager from './tasks';
1415
import ProjectTestManager from './tests';
1516
import { STATUS_BAR_PRIORITY_START } from '../constants';
@@ -20,7 +21,6 @@ import vscode from 'vscode';
2021
export default class ProjectManager {
2122
constructor() {
2223
this._taskManager = undefined;
23-
this._testManager = undefined;
2424
this._sbEnvSwitcher = undefined;
2525
this._logOutputChannel = vscode.window.createOutputChannel(
2626
'PlatformIO: Project Configuration'
@@ -103,18 +103,16 @@ export default class ProjectManager {
103103
this._taskManager.runTask(task)
104104
),
105105
];
106+
this.internalSubscriptions = [];
106107

107108
this.registerEnvSwitcher();
108109
// switch to the first project in a workspace on start-up
109110
this.switchToProject(this.findActiveProjectDir());
110111
}
111112

112113
dispose() {
113-
for (const manager of [this._taskManager, this._testManager]) {
114-
if (manager) {
115-
this.subscriptions.push(manager);
116-
}
117-
}
114+
this.disposeInternals();
115+
disposeSubscriptions(this.internalSubscriptions);
118116
disposeSubscriptions(this.subscriptions);
119117
}
120118

@@ -188,13 +186,14 @@ export default class ProjectManager {
188186
currentProjectDir !== projectDir ||
189187
currentEnvName !== observer.getActiveEnvName()
190188
) {
189+
disposeSubscriptions(this.internalSubscriptions);
191190
this._pool.switch(projectDir);
192-
193-
[this._taskManager, this._testManager].forEach((manager) =>
194-
manager ? manager.dispose() : undefined
195-
);
196191
this._taskManager = new ProjectTaskManager(projectDir, observer);
197-
this._testManager = new ProjectTestManager(projectDir);
192+
this.internalSubscriptions.push(
193+
this._taskManager,
194+
new ProjectConfigLanguageProvider(projectDir),
195+
new ProjectTestManager(projectDir)
196+
);
198197

199198
// open "platformio.ini" if no visible editors
200199
if (

0 commit comments

Comments
 (0)