99import * as pioNodeHelpers from 'platformio-node-helpers' ;
1010
1111import { disposeSubscriptions , listCoreSerialPorts } from '../utils' ;
12+ import path from 'path' ;
1213import vscode from 'vscode' ;
1314
1415export class ProjectConfigLanguageProvider {
1516 static DOCUMENT_SELECTOR = { language : 'ini' } ;
1617 SCOPE_PLATFORMIO = 'platformio' ;
1718 SCOPE_ENV = 'env' ;
1819
19- constructor ( projectDir ) {
20- this . projectDir = projectDir ;
20+ constructor ( ) {
21+ this . diagnosticCollection =
22+ vscode . languages . createDiagnosticCollection ( 'PlatformIO' ) ;
2123 this . subscriptions = [
24+ this . diagnosticCollection ,
2225 vscode . languages . registerHoverProvider (
2326 ProjectConfigLanguageProvider . DOCUMENT_SELECTOR ,
2427 {
@@ -33,6 +36,12 @@ export class ProjectConfigLanguageProvider {
3336 await this . provideCompletionItems ( document , position , token , context ) ,
3437 }
3538 ) ,
39+ vscode . workspace . onDidOpenTextDocument ( ( document ) =>
40+ this . lintConfig ( document . uri )
41+ ) ,
42+ vscode . workspace . onDidSaveTextDocument ( ( document ) =>
43+ this . lintConfig ( document . uri )
44+ ) ,
3645 ] ;
3746 // if (vscode.languages.registerInlineCompletionItemProvider) {
3847 // this.subscriptions.push(
@@ -45,17 +54,27 @@ export class ProjectConfigLanguageProvider {
4554 // )
4655 // );
4756 // }
48- this . _options = undefined ;
57+ this . _optionsCache = new Map ( ) ;
4958 this . _ports = undefined ;
59+
60+ // vscode.window.visibleTextEditors.forEach((editor) =>
61+ // this.lintConfig(editor.document)
62+ // );
5063 }
5164
5265 dispose ( ) {
5366 disposeSubscriptions ( this . subscriptions ) ;
67+ this . _optionsCache . clear ( ) ;
68+ this . diagnosticCollection . clear ( ) ;
5469 }
5570
56- async getOptions ( ) {
57- if ( this . _options ) {
58- return this . _options ;
71+ /**
72+ * Completion
73+ */
74+ async getOptions ( document ) {
75+ const configPath = document . uri . fsPath ;
76+ if ( this . _optionsCache . has ( configPath ) ) {
77+ return this . _optionsCache . get ( configPath ) ;
5978 }
6079 const script = `
6180import json
@@ -65,10 +84,10 @@ print(json.dumps(get_config_options_schema()))
6584 ` ;
6685 const output = await pioNodeHelpers . core . getCorePythonCommandOutput (
6786 [ '-c' , script ] ,
68- { projectDir : this . projectDir }
87+ { projectDir : path . dirname ( configPath ) }
6988 ) ;
70- this . _options = JSON . parse ( output . trim ( ) ) ;
71- return this . _options ;
89+ this . _optionsCache . set ( configPath , JSON . parse ( output ) ) ;
90+ return this . _optionsCache . get ( configPath ) ;
7291 }
7392
7493 renderOptionDocs ( option ) {
@@ -133,7 +152,9 @@ ${option.description}
133152 continue ;
134153 }
135154 const optionName = line . split ( '=' ) [ 0 ] . trim ( ) ;
136- return ( await this . getOptions ( ) ) . find ( ( option ) => option . name === optionName ) ;
155+ return ( await this . getOptions ( document ) ) . find (
156+ ( option ) => option . name === optionName
157+ ) ;
137158 }
138159 }
139160
@@ -149,7 +170,9 @@ ${option.description}
149170
150171 async provideHover ( document , position ) {
151172 const word = document . getText ( document . getWordRangeAtPosition ( position ) ) ;
152- const option = ( await this . getOptions ( ) ) . find ( ( option ) => option . name === word ) ;
173+ const option = ( await this . getOptions ( document ) ) . find (
174+ ( option ) => option . name === word
175+ ) ;
153176 if ( option ) {
154177 return new vscode . Hover ( this . renderOptionDocs ( option ) ) ;
155178 }
@@ -212,7 +235,7 @@ ${option.description}
212235 if ( ! scope ) {
213236 return ;
214237 }
215- const options = await this . getOptions ( ) ;
238+ const options = await this . getOptions ( document ) ;
216239 return options
217240 . filter ( ( option ) => option . scope === scope )
218241 . map ( ( option ) => {
@@ -316,4 +339,114 @@ ${option.description}
316339 items . push ( this . createCustomCompletionValueItem ( ) ) ;
317340 return items ;
318341 }
342+
343+ /**
344+ * Linting
345+ */
346+ async lintConfig ( uri ) {
347+ // ignore non-platformio.ini docs
348+ if ( path . basename ( uri . fsPath ) !== 'platformio.ini' ) {
349+ return ;
350+ }
351+ const script = `
352+ import configparser
353+ import glob
354+ import json
355+
356+ from platformio import fs
357+ from platformio.project import exception
358+ from platformio.public import ProjectConfig
359+
360+
361+ # remove this code for PIO Core 6.1.8+
362+ class TmpProjectConfig(ProjectConfig):
363+ def read(self, path, parse_extra=True):
364+ if path in self._parsed:
365+ return
366+ self._parsed.append(path)
367+ try:
368+ self._parser.read(path, "utf-8")
369+ except configparser.Error as exc:
370+ raise exception.InvalidProjectConfError(path, str(exc)) from exc
371+ if not parse_extra:
372+ return
373+ # load extra configs
374+ for pattern in self.get("platformio", "extra_configs", []):
375+ if pattern.startswith("~"):
376+ pattern = fs.expanduser(pattern)
377+ for item in glob.glob(pattern, recursive=True):
378+ self.read(item)
379+
380+
381+ errors = []
382+ warnings = []
383+
384+ try:
385+ config = TmpProjectConfig()
386+ config.validate(silent=True)
387+ warnings = config.warnings
388+ config.as_tuple()
389+ except Exception as exc:
390+ if exc.__cause__:
391+ exc = exc.__cause__
392+ item = {"type": exc.__class__.__name__, "message": str(exc)}
393+ for attr in ("lineno", "source"):
394+ if hasattr(exc, attr):
395+ item[attr] = getattr(exc, attr)
396+ errors.append(item)
397+ if item["type"] == "ParsingError" and hasattr(exc, "errors"):
398+ for lineno, line in getattr(exc, "errors"):
399+ errors.append(
400+ {
401+ "type": item["type"],
402+ "message": f"Parsing error: {line}",
403+ "lineno": lineno,
404+ "source": item["source"]
405+ }
406+ )
407+
408+ print(json.dumps(dict(errors=errors, warnings=warnings)))
409+ ` ;
410+ this . diagnosticCollection . clear ( ) ;
411+ const projectDir = path . dirname ( uri . fsPath ) ;
412+ const output = await pioNodeHelpers . core . getCorePythonCommandOutput (
413+ [ '-c' , script ] ,
414+ { projectDir }
415+ ) ;
416+ const { errors, warnings } = JSON . parse ( output ) ;
417+ this . diagnosticCollection . set (
418+ uri ,
419+ warnings . map (
420+ ( msg ) =>
421+ new vscode . Diagnostic (
422+ new vscode . Range ( 0 , 0 , 0 , 0 ) ,
423+ msg ,
424+ vscode . DiagnosticSeverity . Warning
425+ )
426+ )
427+ ) ;
428+ const uriDiagnostics = new Map ( ) ;
429+ errors . forEach ( ( data ) => {
430+ const sourceUri = data . source
431+ ? vscode . Uri . file (
432+ path . isAbsolute ( data . source )
433+ ? data . source
434+ : path . join ( projectDir , data . source )
435+ )
436+ : uri ;
437+ const diagnostics = uriDiagnostics . get ( sourceUri . fsPath ) || [ ] ;
438+ diagnostics . push (
439+ new vscode . Diagnostic (
440+ new vscode . Range ( data ?. lineno - 1 || 0 , 0 , data ?. lineno || 0 , 0 ) ,
441+ data . message ,
442+ vscode . DiagnosticSeverity . Error
443+ )
444+ ) ;
445+ uriDiagnostics . set ( sourceUri . fsPath , diagnostics ) ;
446+ } ) ;
447+ uriDiagnostics . forEach ( ( diagnostics , fsPath ) =>
448+ this . diagnosticCollection . set ( vscode . Uri . file ( fsPath ) , diagnostics )
449+ ) ;
450+ return ! errors . length ;
451+ }
319452}
0 commit comments