1+ import * as cp from 'child_process' ;
12import * as path from 'path' ;
2- import { promises as fs } from 'fs' ;
3+ import * as os from 'os' ;
4+ import { promises as fs , constants } from 'fs' ;
35import { spawnSync } from 'child_process' ;
46import deepEqual from 'deep-equal' ;
57import WebRequest from 'web-request' ;
@@ -9,11 +11,52 @@ import vscode, { ExtensionContext } from 'vscode';
911import { LanguageClient , CloseAction , ErrorAction , InitializeError , Message , RevealOutputChannelOn } from 'vscode-languageclient' ;
1012import { DidCompleteBuildNotification , DidCompleteBuildParams } from './protocol' ;
1113
12- interface LanguageServerConfig {
14+ interface LanguageServerExecutables {
1315 readonly lsPath : string ;
14- readonly cliDaemonAddr : string ;
15- readonly cliDaemonInstance : string ;
1616 readonly clangdPath : string ;
17+ readonly cliPath : string ;
18+ }
19+ namespace LanguageServerExecutables {
20+ export function fromDir ( dirPath : string ) : LanguageServerExecutables {
21+ const appRootPath = fromAppRootPath ( ) ;
22+ return appendExeOnWindows ( {
23+ cliPath : path . join ( dirPath , appRootPath , 'arduino-cli' ) ,
24+ lsPath : path . join ( dirPath , appRootPath , 'arduino-language-server' ) ,
25+ clangdPath : path . join ( dirPath , appRootPath , 'clangd' ) ,
26+ } ) ;
27+ }
28+ export async function validate ( executables : LanguageServerExecutables ) : Promise < void > {
29+ await Promise . all ( Object . values ( executables ) . map ( canExecute ) ) ;
30+ }
31+ export async function canExecute ( pathToExecutable : string ) : Promise < void > {
32+ return fs . access ( pathToExecutable , constants . X_OK ) ;
33+ }
34+ function appendExeOnWindows ( executables : LanguageServerExecutables ) : LanguageServerExecutables {
35+ if ( process . platform === 'win32' ) {
36+ const exe = '.exe' ;
37+ return {
38+ cliPath : executables . cliPath + exe ,
39+ lsPath : executables . lsPath + exe ,
40+ clangdPath : executables . clangdPath + exe ,
41+ } ;
42+ }
43+ return executables ;
44+ }
45+ function fromAppRootPath ( ) : string {
46+ const defaultPath = path . join ( 'app' , 'node_modules' , 'arduino-ide-extension' , 'build' ) ;
47+ switch ( process . platform ) {
48+ case 'win32' :
49+ case 'linux' :
50+ return path . join ( 'resources' , defaultPath ) ;
51+ case 'darwin' :
52+ return path . join ( 'Contents' , 'Resources' , defaultPath ) ;
53+ default :
54+ throw new Error ( `Unsupported platform: ${ process . platform } ` ) ;
55+ }
56+ }
57+ }
58+
59+ interface LanguageServerConfig {
1760 readonly board : {
1861 readonly fqbn : string ;
1962 readonly name ?: string ;
@@ -67,28 +110,55 @@ const languageServerStartMutex = new Mutex();
67110export let languageServerIsRunning = false ; // TODO: use later for `start`, `stop`, and `restart` language server.
68111
69112let ide2Path : string | undefined ;
113+ let executables : LanguageServerExecutables | undefined ;
114+
115+ function useIde2Path ( ide2PathToUse : string | undefined = vscode . workspace . getConfiguration ( 'arduinoTools' ) . get ( 'ide2Path' ) ) : string | undefined {
116+ ide2Path = ide2PathToUse ;
117+ executables = findExecutables ( ) ;
118+ if ( executables ) {
119+ vscode . window . showInformationMessage ( `Executables: ${ JSON . stringify ( executables ) } ` ) ;
120+ }
121+ return ide2Path ;
122+ }
123+ function findExecutables ( ) : LanguageServerExecutables | undefined {
124+ if ( ! ide2Path ) {
125+ return undefined ;
126+ }
127+ return LanguageServerExecutables . fromDir ( ide2Path ) ;
128+ }
129+
130+ interface Platform {
131+ readonly boards : Board [ ] ;
132+ }
133+ interface Board {
134+ readonly name : string ;
135+ readonly fqbn ?: string ;
136+ }
137+ namespace Board {
138+ export function installed ( board : Board ) : board is Board & { fqbn : string } {
139+ return ! ! board . fqbn ;
140+ }
141+ }
70142
71143export function activate ( context : ExtensionContext ) {
72- ide2Path = vscode . workspace . getConfiguration ( 'vscode-arduino-tools' ) . get ( 'arduinoTools.ide2Path' ) ;
144+ useIde2Path ( ) ;
73145 vscode . window . showInformationMessage ( 'ide2Path: ' + ide2Path ) ;
74146 vscode . workspace . onDidChangeConfiguration ( event => {
75147 if ( event . affectsConfiguration ( 'arduinoTools.ide2Path' ) ) {
76- ide2Path = vscode . workspace . getConfiguration ( 'vscode-arduino-tools' ) . get ( 'arduinoTools.ide2Path' ) ;
77- vscode . window . showInformationMessage ( 'ide2Path: ' + ide2Path ) ;
148+ useIde2Path ( ) ;
78149 }
79150 } ) ;
80- console . log ( 'hello' ) ;
81151 context . subscriptions . push (
82- vscode . commands . registerCommand ( 'arduino.ide2Path' , ( ) => {
83- ide2Path = vscode . workspace . getConfiguration ( 'vscode-arduino-tools' ) . get ( 'arduinoTools.ide2Path' ) ;
84- vscode . window . showInformationMessage ( 'ide2Path: ' + ide2Path ) ;
85- } ) ,
86- vscode . commands . registerCommand ( 'arduino.languageserver.start' , async ( config : LanguageServerConfig ) => {
152+ vscode . commands . registerCommand ( 'arduino.languageserver.start' , async ( ) => {
87153 const unlock = await languageServerStartMutex . acquire ( ) ;
88154 try {
89- const started = await startLanguageServer ( context , config ) ;
90- languageServerIsRunning = started ;
91- return languageServerIsRunning ? config . board . fqbn : undefined ;
155+ const fqbn = await selectFqbn ( ) ;
156+ if ( fqbn ) {
157+ const started = await startLanguageServer ( context , { board : { fqbn } } ) ;
158+ languageServerIsRunning = started ;
159+ return languageServerIsRunning ? fqbn : undefined ;
160+ }
161+ return false ;
92162 } catch ( err ) {
93163 console . error ( 'Failed to start the language server.' , err ) ;
94164 languageServerIsRunning = false ;
@@ -118,10 +188,51 @@ export function activate(context: ExtensionContext) {
118188 } else {
119189 vscode . window . showWarningMessage ( 'Language server is not running.' ) ;
120190 }
121- } )
191+ } ) ,
122192 ) ;
123193}
124194
195+ async function selectFqbn ( ) : Promise < string | undefined > {
196+ if ( executables ) {
197+ const boards = await installedBoards ( ) ;
198+ const fqbn = await vscode . window . showQuickPick ( boards . map ( ( { fqbn} ) => fqbn ) ) ;
199+ return fqbn ;
200+ }
201+ return undefined ;
202+ }
203+ async function coreList ( ) : Promise < Platform [ ] > {
204+ const raw = await cliExec ( [ 'core' , 'list' , '--format' , 'json' ] ) ;
205+ return JSON . parse ( raw ) as Platform [ ] ;
206+ }
207+ async function installedBoards ( ) : Promise < ( Board & { fqbn : string } ) [ ] > {
208+ const platforms = await coreList ( ) ;
209+ return platforms . map ( ( { boards} ) => boards ) . reduce ( ( acc , curr ) => {
210+ acc . push ( ...curr ) ;
211+ return acc ;
212+ } , [ ] as Board [ ] ) . filter ( Board . installed ) ;
213+ }
214+
215+ async function cliExec ( args : string [ ] = [ ] ) : Promise < string > {
216+ if ( ! executables ) {
217+ throw new Error ( "Could not find the Arduino executables. Did you set the 'ide2Path' correctly?" ) ;
218+ }
219+ const out : Buffer [ ] = [ ] ;
220+ const err : Buffer [ ] = [ ] ;
221+ return new Promise ( ( resolve , reject ) => {
222+ const child = cp . spawn ( `"${ executables ?. cliPath } "` , args , { shell : true } ) ;
223+ child . stdout . on ( 'data' , ( data ) => out . push ( data ) ) ;
224+ child . stderr . on ( 'data' , ( data ) => err . push ( data ) ) ;
225+ child . on ( 'error' , reject ) ;
226+ child . on ( 'exit' , ( code ) => {
227+ if ( code === 0 ) {
228+ return resolve ( Buffer . concat ( out ) . toString ( 'utf-8' ) ) ;
229+ } else {
230+ return reject ( Buffer . concat ( err ) . toString ( 'utf-8' ) ) ;
231+ }
232+ } ) ;
233+ } ) ;
234+ } ;
235+
125236async function startDebug ( _ : ExtensionContext , config : DebugConfig ) : Promise < boolean > {
126237 let info : DebugInfo | undefined = undefined ;
127238 let rawStdout : string | undefined = undefined ;
@@ -200,9 +311,13 @@ async function stopLanguageServer(context: ExtensionContext): Promise<void> {
200311
201312async function startLanguageServer ( context : ExtensionContext , config : LanguageServerConfig ) : Promise < boolean > {
202313 await stopLanguageServer ( context ) ;
314+ if ( ! executables ) {
315+ vscode . window . showErrorMessage ( "Failed to start the language server. Could not find the Arduino executables. Did you set the 'ide2Path' correctly?" ) ;
316+ return false ;
317+ }
203318 if ( ! languageClient || ! deepEqual ( latestConfig , config ) ) {
204319 latestConfig = config ;
205- languageClient = await buildLanguageClient ( config ) ;
320+ languageClient = await buildLanguageClient ( Object . assign ( config , executables ) ) ;
206321 crashCount = 0 ;
207322 }
208323
@@ -212,9 +327,9 @@ async function startLanguageServer(context: ExtensionContext, config: LanguageSe
212327 return true ;
213328}
214329
215- async function buildLanguageClient ( config : LanguageServerConfig ) : Promise < LanguageClient > {
216- const { lsPath : command , clangdPath, cliDaemonAddr , cliDaemonInstance , board, flags, env, log } = config ;
217- const args = [ '-clangd ' , clangdPath , '-cli-daemon-addr ' , cliDaemonAddr , '-cli-daemon-instance ' , cliDaemonInstance , '-fqbn' , board . fqbn , '-skip-libraries-discovery-on-rebuild' ] ;
330+ async function buildLanguageClient ( config : LanguageServerConfig & LanguageServerExecutables ) : Promise < LanguageClient > {
331+ const { lsPath : command , clangdPath, board, flags, env, log } = config ;
332+ const args = [ '-cli ' , config . cliPath , '-cli-config ' , path . join ( os . homedir ( ) , '.arduinoIDE/arduino -cli.yaml' ) , '-clangd ', clangdPath , '-fqbn' , board . fqbn ?? 'arduino:avr:uno' , '-skip-libraries-discovery-on-rebuild' ] ;
218333 if ( board . name ) {
219334 args . push ( '-board-name' , board . name ) ;
220335 }
0 commit comments