@@ -7,16 +7,9 @@ import * as path from 'path';
77import { match } from 'ts-pattern' ;
88import * as url from 'url' ;
99import { 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' ;
1911import { Logger } from 'vscode-languageclient' ;
12+ import { HlsError , MissingToolError , NoMatchingHls } from './errors' ;
2013import {
2114 addPathToProcessPath ,
2215 executableExists ,
@@ -39,52 +32,6 @@ let manageHLS = workspace.getConfiguration('haskell').get('manageHLS') as Manage
3932// On Windows the executable needs to be stored somewhere with an .exe extension
4033const exeExt = process . platform === 'win32' ? '.exe' : '' ;
4134
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-
8835/**
8936 * Call a process asynchronously.
9037 * While doing so, update the windows with progress information.
@@ -324,19 +271,19 @@ export async function findHaskellLanguageServer(
324271 if ( promptBeforeDownloads ) {
325272 const hlsInstalled = latestHLS
326273 ? await toolInstalled ( context , logger , 'hls' , latestHLS )
327- : ( [ true , 'hls' , '' ] as [ boolean , Tool , string ] ) ;
274+ : new InstalledTool ( 'hls' ) ;
328275 const cabalInstalled = latestCabal
329276 ? await toolInstalled ( context , logger , 'cabal' , latestCabal )
330- : ( [ true , 'cabal' , '' ] as [ boolean , Tool , string ] ) ;
277+ : new InstalledTool ( 'cabal' ) ;
331278 const stackInstalled = latestStack
332279 ? await toolInstalled ( context , logger , 'stack' , latestStack )
333- : ( [ true , 'stack' , '' ] as [ boolean , Tool , string ] ) ;
280+ : new InstalledTool ( 'stack' ) ;
334281 const ghcInstalled = ( await executableExists ( 'ghc' ) )
335- ? ( [ true , 'ghc' , '' ] as [ boolean , Tool , string ] )
282+ ? new InstalledTool ( 'ghc' )
336283 : await toolInstalled ( context , logger , 'ghc' , recGHC ! ) ;
337284 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 ) ;
340287 if ( toInstall . length > 0 ) {
341288 const decision = await window . showInformationMessage (
342289 `Need to download ${ toInstall . join ( ', ' ) } , continue?` ,
@@ -348,15 +295,15 @@ export async function findHaskellLanguageServer(
348295 } else if ( decision === "Yes, don't ask again" ) {
349296 workspace . getConfiguration ( 'haskell' ) . update ( 'promptBeforeDownloads' , false ) ;
350297 } 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' ) {
354301 throw new MissingToolError ( 'hls' ) ;
355- } else if ( t === 'cabal' ) {
302+ } else if ( tool . name === 'cabal' ) {
356303 latestCabal = null ;
357- } else if ( t === 'stack' ) {
304+ } else if ( tool . name === 'stack' ) {
358305 latestStack = null ;
359- } else if ( t === 'ghc' ) {
306+ } else if ( tool . name === 'ghc' ) {
360307 recGHC = null ;
361308 }
362309 }
@@ -400,11 +347,13 @@ export async function findHaskellLanguageServer(
400347 if ( promptBeforeDownloads ) {
401348 const hlsInstalled = projectHls
402349 ? await toolInstalled ( context , logger , 'hls' , projectHls )
403- : ( [ true , 'hls' , '' ] as [ boolean , Tool , string ] ) ;
350+ : new InstalledTool ( 'hls' ) ;
404351 const ghcInstalled = projectGhc
405352 ? 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 ) ;
408357 if ( toInstall . length > 0 ) {
409358 const decision = await window . showInformationMessage (
410359 `Need to download ${ toInstall . join ( ', ' ) } , continue?` ,
@@ -417,11 +366,11 @@ export async function findHaskellLanguageServer(
417366 } else if ( decision === "Yes, don't ask again" ) {
418367 workspace . getConfiguration ( 'haskell' ) . update ( 'promptBeforeDownloads' , false ) ;
419368 } 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' ) {
423372 throw new MissingToolError ( 'hls' ) ;
424- } else if ( t === 'ghc' ) {
373+ } else if ( tool . name === 'ghc' ) {
425374 projectGhc = null ;
426375 }
427376 }
@@ -487,7 +436,7 @@ async function callGHCup(
487436 callback
488437 ) ;
489438 } 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!` ) ;
491440 }
492441}
493442
@@ -496,7 +445,7 @@ async function getLatestProjectHLS(
496445 logger : Logger ,
497446 workingDir : string ,
498447 toolchainBindir : string
499- ) : Promise < [ string , string | null ] > {
448+ ) : Promise < [ string , string ] > {
500449 // get project GHC version, but fallback to system ghc if necessary.
501450 const projectGhc = toolchainBindir
502451 ? await getProjectGHCVersion ( toolchainBindir , workingDir , logger ) . catch ( async ( e ) => {
@@ -507,7 +456,6 @@ async function getLatestProjectHLS(
507456 return await callAsync ( `ghc${ exeExt } ` , [ '--numeric-version' ] , logger , undefined , undefined , false ) ;
508457 } )
509458 : await callAsync ( `ghc${ exeExt } ` , [ '--numeric-version' ] , logger , undefined , undefined , false ) ;
510- const noMatchingHLS = `No HLS version was found for supporting GHC ${ projectGhc } .` ;
511459
512460 // first we get supported GHC versions from available HLS bindists (whether installed or not)
513461 const metadataMap = ( await getHLSesfromMetadata ( context , logger ) ) || new Map < string , string [ ] > ( ) ;
@@ -524,7 +472,7 @@ async function getLatestProjectHLS(
524472 . pop ( ) ;
525473
526474 if ( ! latest ) {
527- throw new Error ( noMatchingHLS ) ;
475+ throw new NoMatchingHls ( projectGhc ) ;
528476 } else {
529477 return [ latest [ 0 ] , projectGhc ] ;
530478 }
@@ -774,11 +722,11 @@ async function toolInstalled(
774722 logger : Logger ,
775723 tool : Tool ,
776724 version : string
777- ) : Promise < [ boolean , Tool , string ] > {
725+ ) : Promise < InstalledTool > {
778726 const b = await callGHCup ( context , logger , [ 'whereis' , tool , version ] , undefined , false )
779727 . then ( ( x ) => true )
780728 . catch ( ( x ) => false ) ;
781- return [ b , tool , version ] ;
729+ return new InstalledTool ( tool , version , b ) ;
782730}
783731
784732/**
@@ -896,3 +844,26 @@ async function getReleaseMetadata(
896844 }
897845 }
898846}
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