@@ -7,16 +7,9 @@ import * as path from 'path';
7
7
import { match } from 'ts-pattern' ;
8
8
import * as url from 'url' ;
9
9
import { promisify } from 'util' ;
10
- import {
11
- ConfigurationTarget ,
12
- ExtensionContext ,
13
- ProgressLocation ,
14
- Uri ,
15
- window ,
16
- workspace ,
17
- WorkspaceFolder ,
18
- } from 'vscode' ;
10
+ import { ConfigurationTarget , ExtensionContext , ProgressLocation , window , workspace , WorkspaceFolder } from 'vscode' ;
19
11
import { Logger } from 'vscode-languageclient' ;
12
+ import { HlsError , MissingToolError , NoMatchingHls } from './errors' ;
20
13
import {
21
14
addPathToProcessPath ,
22
15
executableExists ,
@@ -39,52 +32,6 @@ let manageHLS = workspace.getConfiguration('haskell').get('manageHLS') as Manage
39
32
// On Windows the executable needs to be stored somewhere with an .exe extension
40
33
const exeExt = process . platform === 'win32' ? '.exe' : '' ;
41
34
42
- export class MissingToolError extends Error {
43
- public readonly tool : string ;
44
- constructor ( tool : string ) {
45
- let prettyTool : string ;
46
- switch ( tool . toLowerCase ( ) ) {
47
- case 'stack' :
48
- prettyTool = 'Stack' ;
49
- break ;
50
- case 'cabal' :
51
- prettyTool = 'Cabal' ;
52
- break ;
53
- case 'ghc' :
54
- prettyTool = 'GHC' ;
55
- break ;
56
- case 'ghcup' :
57
- prettyTool = 'GHCup' ;
58
- break ;
59
- case 'haskell-language-server' :
60
- prettyTool = 'HLS' ;
61
- break ;
62
- case 'hls' :
63
- prettyTool = 'HLS' ;
64
- break ;
65
- default :
66
- prettyTool = tool ;
67
- break ;
68
- }
69
- super ( `Project requires ${ prettyTool } but it isn't installed` ) ;
70
- this . tool = prettyTool ;
71
- }
72
-
73
- public installLink ( ) : Uri | null {
74
- switch ( this . tool ) {
75
- case 'Stack' :
76
- return Uri . parse ( 'https://docs.haskellstack.org/en/stable/install_and_upgrade/' ) ;
77
- case 'GHCup' :
78
- case 'Cabal' :
79
- case 'HLS' :
80
- case 'GHC' :
81
- return Uri . parse ( 'https://www.haskell.org/ghcup/' ) ;
82
- default :
83
- return null ;
84
- }
85
- }
86
- }
87
-
88
35
/**
89
36
* Call a process asynchronously.
90
37
* While doing so, update the windows with progress information.
@@ -324,19 +271,19 @@ export async function findHaskellLanguageServer(
324
271
if ( promptBeforeDownloads ) {
325
272
const hlsInstalled = latestHLS
326
273
? await toolInstalled ( context , logger , 'hls' , latestHLS )
327
- : ( [ true , 'hls' , '' ] as [ boolean , Tool , string ] ) ;
274
+ : new InstalledTool ( 'hls' ) ;
328
275
const cabalInstalled = latestCabal
329
276
? await toolInstalled ( context , logger , 'cabal' , latestCabal )
330
- : ( [ true , 'cabal' , '' ] as [ boolean , Tool , string ] ) ;
277
+ : new InstalledTool ( 'cabal' ) ;
331
278
const stackInstalled = latestStack
332
279
? await toolInstalled ( context , logger , 'stack' , latestStack )
333
- : ( [ true , 'stack' , '' ] as [ boolean , Tool , string ] ) ;
280
+ : new InstalledTool ( 'stack' ) ;
334
281
const ghcInstalled = ( await executableExists ( 'ghc' ) )
335
- ? ( [ true , 'ghc' , '' ] as [ boolean , Tool , string ] )
282
+ ? new InstalledTool ( 'ghc' )
336
283
: await toolInstalled ( context , logger , 'ghc' , recGHC ! ) ;
337
284
const toInstall = [ hlsInstalled , cabalInstalled , stackInstalled , ghcInstalled ]
338
- . filter ( ( [ b , t , v ] ) => ! b )
339
- . map ( ( [ _ , t , v ] ) => ` ${ t } - ${ v } ` ) ;
285
+ . filter ( ( tool ) => ! tool . installed )
286
+ . map ( ( tool ) => tool . nameWithVersion ) ;
340
287
if ( toInstall . length > 0 ) {
341
288
const decision = await window . showInformationMessage (
342
289
`Need to download ${ toInstall . join ( ', ' ) } , continue?` ,
@@ -348,15 +295,15 @@ export async function findHaskellLanguageServer(
348
295
} else if ( decision === "Yes, don't ask again" ) {
349
296
workspace . getConfiguration ( 'haskell' ) . update ( 'promptBeforeDownloads' , false ) ;
350
297
} else {
351
- [ hlsInstalled , cabalInstalled , stackInstalled , ghcInstalled ] . forEach ( ( [ b , t ] ) => {
352
- if ( ! b ) {
353
- if ( t === 'hls' ) {
298
+ [ hlsInstalled , cabalInstalled , stackInstalled , ghcInstalled ] . forEach ( ( tool ) => {
299
+ if ( ! tool . installed ) {
300
+ if ( tool . name === 'hls' ) {
354
301
throw new MissingToolError ( 'hls' ) ;
355
- } else if ( t === 'cabal' ) {
302
+ } else if ( tool . name === 'cabal' ) {
356
303
latestCabal = null ;
357
- } else if ( t === 'stack' ) {
304
+ } else if ( tool . name === 'stack' ) {
358
305
latestStack = null ;
359
- } else if ( t === 'ghc' ) {
306
+ } else if ( tool . name === 'ghc' ) {
360
307
recGHC = null ;
361
308
}
362
309
}
@@ -400,11 +347,13 @@ export async function findHaskellLanguageServer(
400
347
if ( promptBeforeDownloads ) {
401
348
const hlsInstalled = projectHls
402
349
? await toolInstalled ( context , logger , 'hls' , projectHls )
403
- : ( [ true , 'hls' , '' ] as [ boolean , Tool , string ] ) ;
350
+ : new InstalledTool ( 'hls' ) ;
404
351
const ghcInstalled = projectGhc
405
352
? await toolInstalled ( context , logger , 'ghc' , projectGhc )
406
- : ( [ true , 'ghc' , '' ] as [ boolean , Tool , string ] ) ;
407
- const toInstall = [ hlsInstalled , ghcInstalled ] . filter ( ( [ b , t , v ] ) => ! b ) . map ( ( [ _ , t , v ] ) => `${ t } -${ v } ` ) ;
353
+ : new InstalledTool ( 'ghc' ) ;
354
+ const toInstall = [ hlsInstalled , ghcInstalled ]
355
+ . filter ( ( tool ) => ! tool . installed )
356
+ . map ( ( tool ) => tool . nameWithVersion ) ;
408
357
if ( toInstall . length > 0 ) {
409
358
const decision = await window . showInformationMessage (
410
359
`Need to download ${ toInstall . join ( ', ' ) } , continue?` ,
@@ -417,11 +366,11 @@ export async function findHaskellLanguageServer(
417
366
} else if ( decision === "Yes, don't ask again" ) {
418
367
workspace . getConfiguration ( 'haskell' ) . update ( 'promptBeforeDownloads' , false ) ;
419
368
} else {
420
- [ hlsInstalled , ghcInstalled ] . forEach ( ( [ b , t ] ) => {
421
- if ( ! b ) {
422
- if ( t === 'hls' ) {
369
+ [ hlsInstalled , ghcInstalled ] . forEach ( ( tool ) => {
370
+ if ( ! tool . installed ) {
371
+ if ( tool . name === 'hls' ) {
423
372
throw new MissingToolError ( 'hls' ) ;
424
- } else if ( t === 'ghc' ) {
373
+ } else if ( tool . name === 'ghc' ) {
425
374
projectGhc = null ;
426
375
}
427
376
}
@@ -487,7 +436,7 @@ async function callGHCup(
487
436
callback
488
437
) ;
489
438
} else {
490
- throw new Error ( `Internal error: tried to call ghcup while haskell.manageHLS is set to ${ manageHLS } . Aborting!` ) ;
439
+ throw new HlsError ( `Internal error: tried to call ghcup while haskell.manageHLS is set to ${ manageHLS } . Aborting!` ) ;
491
440
}
492
441
}
493
442
@@ -496,7 +445,7 @@ async function getLatestProjectHLS(
496
445
logger : Logger ,
497
446
workingDir : string ,
498
447
toolchainBindir : string
499
- ) : Promise < [ string , string | null ] > {
448
+ ) : Promise < [ string , string ] > {
500
449
// get project GHC version, but fallback to system ghc if necessary.
501
450
const projectGhc = toolchainBindir
502
451
? await getProjectGHCVersion ( toolchainBindir , workingDir , logger ) . catch ( async ( e ) => {
@@ -507,7 +456,6 @@ async function getLatestProjectHLS(
507
456
return await callAsync ( `ghc${ exeExt } ` , [ '--numeric-version' ] , logger , undefined , undefined , false ) ;
508
457
} )
509
458
: await callAsync ( `ghc${ exeExt } ` , [ '--numeric-version' ] , logger , undefined , undefined , false ) ;
510
- const noMatchingHLS = `No HLS version was found for supporting GHC ${ projectGhc } .` ;
511
459
512
460
// first we get supported GHC versions from available HLS bindists (whether installed or not)
513
461
const metadataMap = ( await getHLSesfromMetadata ( context , logger ) ) || new Map < string , string [ ] > ( ) ;
@@ -524,7 +472,7 @@ async function getLatestProjectHLS(
524
472
. pop ( ) ;
525
473
526
474
if ( ! latest ) {
527
- throw new Error ( noMatchingHLS ) ;
475
+ throw new NoMatchingHls ( projectGhc ) ;
528
476
} else {
529
477
return [ latest [ 0 ] , projectGhc ] ;
530
478
}
@@ -774,11 +722,11 @@ async function toolInstalled(
774
722
logger : Logger ,
775
723
tool : Tool ,
776
724
version : string
777
- ) : Promise < [ boolean , Tool , string ] > {
725
+ ) : Promise < InstalledTool > {
778
726
const b = await callGHCup ( context , logger , [ 'whereis' , tool , version ] , undefined , false )
779
727
. then ( ( x ) => true )
780
728
. catch ( ( x ) => false ) ;
781
- return [ b , tool , version ] ;
729
+ return new InstalledTool ( tool , version , b ) ;
782
730
}
783
731
784
732
/**
@@ -896,3 +844,26 @@ async function getReleaseMetadata(
896
844
}
897
845
}
898
846
}
847
+
848
+ /**
849
+ * Tracks the name, version and installation state of tools we need.
850
+ */
851
+ class InstalledTool {
852
+ /**
853
+ * "<name>-<version>" of the installed Tool.
854
+ */
855
+ readonly nameWithVersion : string = '' ;
856
+
857
+ /**
858
+ * Initialize an installed tool entry.
859
+ *
860
+ * If optional parameters are omitted, we assume the tool is installed.
861
+ *
862
+ * @param name Name of the tool.
863
+ * @param version Version of the tool, expected to be either SemVer or PVP versioned.
864
+ * @param installed Is this tool currently installed?
865
+ */
866
+ public constructor ( readonly name : string , readonly version : string = '' , readonly installed : boolean = true ) {
867
+ this . nameWithVersion = `${ name } -${ version } ` ;
868
+ }
869
+ }
0 commit comments