@@ -226,7 +226,7 @@ export async function findHaskellLanguageServer(
226
226
227
227
if ( ! manageHLS ) {
228
228
// plugin needs initialization
229
- const promptMessage = 'How do you want the extension to manage/discover HLS?' ;
229
+ const promptMessage = 'How do you want the extension to manage/discover HLS and the relevant toolchain ?' ;
230
230
231
231
const decision =
232
232
( await window . showInformationMessage ( promptMessage , 'system ghcup (recommended)' , 'internal ghcup' , 'PATH' ) ) ||
@@ -249,39 +249,64 @@ export async function findHaskellLanguageServer(
249
249
// we manage HLS, make sure ghcup is installed/available
250
250
await getGHCup ( context , logger ) ;
251
251
252
- // get a preliminary hls wrapper for finding project GHC version,
253
- // later we may install a different HLS that supports the given GHC
254
- let wrapper = await getLatestWrapperFromGHCup ( context , logger ) . then ( ( e ) =>
255
- ! e
256
- ? callGHCup ( context , logger , [ 'install' , 'hls' ] , 'Installing latest HLS' , true ) . then ( ( ) =>
257
- callGHCup (
258
- context ,
259
- logger ,
260
- [ 'whereis' , 'hls' ] ,
261
- undefined ,
262
- false ,
263
- ( err , stdout , _stderr , resolve , reject ) => {
264
- err ? reject ( "Couldn't find latest HLS" ) : resolve ( stdout ?. trim ( ) ) ;
265
- }
266
- )
267
- )
268
- : e
269
- ) ;
252
+ // get a preliminary toolchain for finding the correct project GHC version (we need HLS and cabal/stack and ghc as fallback),
253
+ // later we may install a different toolchain that's more project-specific
254
+ const installGHC = ! executableExists ( 'ghc' ) ;
255
+ let latestHLS = await getLatestToolFromGHCup ( context , logger , 'hls' ) ;
256
+ let latestCabal = await getLatestToolFromGHCup ( context , logger , 'cabal' ) ;
257
+ let latestStack = await getLatestToolFromGHCup ( context , logger , 'stack' ) ;
258
+ let recGHC = await getLatestAvailableToolFromGHCup ( context , logger , 'ghc' , 'recommended' ) ;
259
+ // TODO: this should be obsolete for ghcup-0.1.17.6
260
+ // and we can drop the use of `-b`.
261
+ let symHLSPath = installGHC
262
+ ? path . join ( storagePath , `hls-${ latestHLS } _cabal-${ latestCabal } -stack-${ latestStack } ` )
263
+ : path . join ( storagePath , `hls-${ latestHLS } _ghc-${ recGHC } -cabal-${ latestCabal } -stack-${ latestStack } ` ) ;
264
+
265
+ const latestToolchainBindir = await callGHCup (
266
+ context ,
267
+ logger ,
268
+ [ 'run'
269
+ , '--hls' , latestHLS ? latestHLS : 'latest'
270
+ , '--cabal' , latestCabal ? latestCabal : 'latest'
271
+ , '--stack' , latestStack ? latestStack : 'latest'
272
+ , ...( installGHC ? [ '--ghc' , 'recommended' ] : [ ] )
273
+ , '-b' , symHLSPath
274
+ , '--install'
275
+ ] ,
276
+ 'Installing latest toolchain for bootstrap' ,
277
+ true ,
278
+ ( err , stdout , _stderr , resolve , reject ) => {
279
+ err ? reject ( "Couldn't install latest toolchain" ) : resolve ( stdout ?. trim ( ) ) ;
280
+ }
281
+ ) ;
270
282
271
283
// now figure out the project GHC version and the latest supported HLS version
272
284
// we need for it (e.g. this might in fact be a downgrade for old GHCs)
273
- const installableHls = await getLatestHLS ( context , logger , workingDir , wrapper ) ;
285
+ const [ installableHls , projectGhc ] = await getLatestHLS ( context , logger , workingDir , latestToolchainBindir ) ;
286
+
287
+ latestHLS = await getLatestToolFromGHCup ( context , logger , 'hls' )
288
+ latestCabal = await getLatestToolFromGHCup ( context , logger , 'cabal' ) ;
289
+ latestStack = await getLatestToolFromGHCup ( context , logger , 'stack' ) ;
274
290
275
291
// now install said version in an isolated symlink directory
276
- const symHLSPath = path . join ( storagePath , 'hls' , installableHls ) ;
277
- wrapper = path . join ( symHLSPath , `haskell-language-server-wrapper${ exeExt } ` ) ;
292
+
293
+ // TODO: this should be obsolete for ghcup-0.1.17.6
294
+ // and we can drop the use of `-b`.
295
+ symHLSPath = path . join ( storagePath , `hls-${ installableHls } _ghc-${ projectGhc } _cabal-${ latestCabal } -stack-${ latestStack } ` ) ;
296
+
297
+ const wrapper = path . join ( symHLSPath , `haskell-language-server-wrapper${ exeExt } ` ) ;
278
298
// Check if we have a working symlink, so we can avoid another popup
279
299
if ( ! fs . existsSync ( wrapper ) ) {
280
300
await callGHCup (
281
301
context ,
282
302
logger ,
283
- [ 'run' , '--hls' , installableHls , '-b' , symHLSPath , '-i' ] ,
284
- `Installing HLS ${ installableHls } ` ,
303
+ [ 'run'
304
+ , '--hls' , installableHls
305
+ , '--ghc' , projectGhc
306
+ , '--cabal' , `${ latestCabal } `
307
+ , '--stack' , `${ latestStack } `
308
+ , '-b' , symHLSPath , '-i' ] ,
309
+ `Installing project specific toolchain: HLS-${ installableHls } , GHC-${ projectGhc } , cabal-${ latestCabal } , stack-${ latestStack } ` ,
285
310
true
286
311
) ;
287
312
}
@@ -340,13 +365,13 @@ async function getLatestHLS(
340
365
context : ExtensionContext ,
341
366
logger : Logger ,
342
367
workingDir : string ,
343
- wrapper ? : string
344
- ) : Promise < string > {
368
+ toolchainBindir : string
369
+ ) : Promise < [ string , string ] > {
345
370
const storagePath : string = await getStoragePath ( context ) ;
346
371
347
372
// get project GHC version, but fallback to system ghc if necessary.
348
- const projectGhc = wrapper
349
- ? await getProjectGHCVersion ( wrapper , workingDir , logger )
373
+ const projectGhc = toolchainBindir
374
+ ? await getProjectGHCVersion ( toolchainBindir , workingDir , logger )
350
375
: await callAsync ( `ghc${ exeExt } ` , [ '--numeric-version' ] , storagePath , logger , undefined , false ) ;
351
376
const noMatchingHLS = `No HLS version was found for supporting GHC ${ projectGhc } .` ;
352
377
@@ -368,7 +393,7 @@ async function getLatestHLS(
368
393
window . showErrorMessage ( noMatchingHLS ) ;
369
394
throw new Error ( noMatchingHLS ) ;
370
395
} else {
371
- return latest [ 0 ] ;
396
+ return [ latest [ 0 ] , projectGhc ] ;
372
397
}
373
398
}
374
399
@@ -380,21 +405,27 @@ async function getLatestHLS(
380
405
* @param logger Logger for feedback.
381
406
* @returns The GHC version, or fail with an `Error`.
382
407
*/
383
- export async function getProjectGHCVersion ( wrapper : string , workingDir : string , logger : Logger ) : Promise < string > {
408
+ export async function getProjectGHCVersion ( toolchainBindir : string , workingDir : string , logger : Logger ) : Promise < string > {
384
409
const title = 'Working out the project GHC version. This might take a while...' ;
385
410
logger . info ( title ) ;
411
+
386
412
const args = [ '--project-ghc-version' ] ;
387
413
414
+ const newPath = addPathToProcessPath ( toolchainBindir ) ;
415
+ const environmentNew : IEnvVars = {
416
+ PATH : newPath ,
417
+ } ;
418
+
388
419
return callAsync (
389
- wrapper ,
420
+ 'haskell-language-server- wrapper' ,
390
421
args ,
391
422
workingDir ,
392
423
logger ,
393
424
title ,
394
425
false ,
395
- undefined ,
426
+ environmentNew ,
396
427
( err , stdout , stderr , resolve , reject ) => {
397
- const command : string = wrapper + ' ' + args . join ( ' ' ) ;
428
+ const command : string = 'haskell-language-server- wrapper' + ' ' + args . join ( ' ' ) ;
398
429
if ( err ) {
399
430
logger . error ( `Error executing '${ command } ' with error code ${ err . code } ` ) ;
400
431
logger . error ( `stderr: ${ stderr } ` ) ;
@@ -413,7 +444,7 @@ export async function getProjectGHCVersion(wrapper: string, workingDir: string,
413
444
}
414
445
reject ( new MissingToolError ( 'unknown' ) ) ;
415
446
}
416
- reject ( Error ( `${ wrapper } --project-ghc-version exited with exit code ${ err . code } :\n${ stdout } \n${ stderr } ` ) ) ;
447
+ reject ( Error ( `haskell-language-server --project-ghc-version exited with exit code ${ err . code } :\n${ stdout } \n${ stderr } ` ) ) ;
417
448
} else {
418
449
logger . info ( `The GHC version for the project or file: ${ stdout ?. trim ( ) } ` ) ;
419
450
resolve ( stdout ?. trim ( ) ) ;
@@ -541,25 +572,57 @@ export function addPathToProcessPath(extraPath: string): string {
541
572
return PATH . join ( pathSep ) ;
542
573
}
543
574
544
- async function getLatestWrapperFromGHCup ( context : ExtensionContext , logger : Logger ) : Promise < string | null > {
545
- const hlsVersions = await callGHCup (
575
+ // the tool might be installed or not
576
+ async function getLatestToolFromGHCup ( context : ExtensionContext , logger : Logger , tool : string ) : Promise < string > {
577
+ // these might be custom/stray/compiled, so we try first
578
+ const installedVersions = await callGHCup (
546
579
context ,
547
580
logger ,
548
- [ 'list' , '-t' , 'hls' , '-c' , 'installed' , '-r' ] ,
581
+ [ 'list' , '-t' , tool , '-c' , 'installed' , '-r' ] ,
549
582
undefined ,
550
583
false
551
584
) ;
552
- const installed = hlsVersions . split ( / \r ? \n / ) . pop ( ) ;
553
- if ( installed ) {
554
- const latestHlsVersion = installed . split ( ' ' ) [ 1 ] ;
585
+ const latestInstalled = installedVersions . split ( / \r ? \n / ) . pop ( ) ;
586
+ if ( latestInstalled ) {
587
+ const latestInstalledVersion = latestInstalled . split ( / \s + / ) [ 1 ] ;
555
588
556
- let bin = await callGHCup ( context , logger , [ 'whereis' , 'hls' , `${ latestHlsVersion } ` ] , undefined , false ) ;
557
- return bin ;
589
+ let bin = await callGHCup ( context , logger , [ 'whereis' , tool , `${ latestInstalledVersion } ` ] , undefined , false ) ;
590
+ const storagePath : string = await getStoragePath ( context ) ;
591
+ const ver = await callAsync ( `${ bin } ` , [ '--numeric-version' ] , storagePath , logger , undefined , false )
592
+ if ( ver ) {
593
+ return ver ;
594
+ } else {
595
+ throw new Error ( `Could not figure out version of ${ bin } ` ) ;
596
+ }
597
+ }
598
+
599
+ return getLatestAvailableToolFromGHCup ( context , logger , tool ) ;
600
+ }
601
+
602
+ async function getLatestAvailableToolFromGHCup ( context : ExtensionContext , logger : Logger , tool : string , tag ?: string , criteria ?: string ) : Promise < string > {
603
+ // fall back to installable versions
604
+ const availableVersions = await callGHCup (
605
+ context ,
606
+ logger ,
607
+ [ 'list' , '-t' , tool , '-c' , criteria ? criteria : 'available' , '-r' ] ,
608
+ undefined ,
609
+ false
610
+ ) . then ( s => s . split ( / \r ? \n / ) ) ;
611
+
612
+ let latestAvailable : string | null = null ;
613
+ availableVersions . forEach ( ( ver ) => {
614
+ if ( ver . split ( / \s + / ) [ 2 ] . split ( ',' ) . includes ( tag ? tag : 'latest' ) ) {
615
+ latestAvailable = ver . split ( / \s + / ) [ 1 ] ;
616
+ }
617
+ } ) ;
618
+ if ( ! latestAvailable ) {
619
+ throw new Error ( `Unable to find ${ tag ? tag : 'latest' } tool ${ tool } ` )
558
620
} else {
559
- return null ;
621
+ return latestAvailable ;
560
622
}
561
623
}
562
624
625
+
563
626
// complements getLatestHLSfromMetadata, by checking possibly locally compiled
564
627
// HLS in ghcup
565
628
// If 'targetGhc' is omitted, picks the latest 'haskell-language-server-wrapper',
@@ -585,7 +648,7 @@ async function getHLSesFromGHCup(
585
648
. catch ( ( ) => false ) ;
586
649
} ) ;
587
650
588
- const installed = hlsVersions . split ( / \r ? \n / ) . map ( ( e ) => e . split ( ' ' ) [ 1 ] ) ;
651
+ const installed = hlsVersions . split ( / \r ? \n / ) . map ( ( e ) => e . split ( / \s + / ) [ 1 ] ) ;
589
652
if ( installed ?. length ) {
590
653
const myMap = new Map < string , string [ ] > ( ) ;
591
654
installed . forEach ( ( hls ) => {
0 commit comments