@@ -73,6 +73,7 @@ class MissingToolError extends Error {
73
73
* @param callback Upon process termination, execute this callback. If given, must resolve promise.
74
74
* @returns Stdout of the process invocation, trimmed off newlines, or whatever the `callback` resolved to.
75
75
*/
76
+
76
77
async function callAsync (
77
78
binary : string ,
78
79
args : string [ ] ,
@@ -85,7 +86,7 @@ async function callAsync(
85
86
error : ExecException | null ,
86
87
stdout : string ,
87
88
stderr : string ,
88
- resolve : ( value : string | PromiseLike < string > ) => void ,
89
+ resolve : ( value : string | PromiseLike < string > ) => void ,
89
90
reject : ( reason ?: any ) => void
90
91
) => void
91
92
) : Promise < string > {
@@ -191,7 +192,7 @@ export async function findHaskellLanguageServer(
191
192
logger : Logger ,
192
193
workingDir : string ,
193
194
folder ?: WorkspaceFolder
194
- ) : Promise < [ string , string ] > {
195
+ ) : Promise < string > {
195
196
// we manage HLS, make sure ghcup is installed/available
196
197
await getGHCup ( context , logger ) ;
197
198
@@ -205,61 +206,87 @@ export async function findHaskellLanguageServer(
205
206
}
206
207
207
208
const manageHLS = workspace . getConfiguration ( 'haskell' ) . get ( 'manageHLS' ) as boolean ;
208
- const wrapper = findHLSinPATH ( context , logger , folder ) ;
209
- const [ installableHls , projectGhc ] = await getLatestSuitableHLS (
210
- context ,
211
- logger ,
212
- workingDir ,
213
- ( wrapper === null ) ? undefined : wrapper
214
- ) ;
215
209
216
210
if ( ! manageHLS ) {
211
+ const wrapper = findHLSinPATH ( context , logger , folder ) ;
217
212
if ( ! wrapper ) {
218
213
const msg = 'Could not find a HLS binary! Consider installing HLS via ghcup or set "haskell.manageHLS" to true' ;
219
214
window . showErrorMessage ( msg ) ;
220
215
throw new Error ( msg ) ;
221
216
} else {
222
- return [ wrapper , projectGhc ] ;
217
+ return wrapper ;
223
218
}
224
- }
225
-
226
- const symHLSPath = path . join ( storagePath , 'hls' , installableHls ) ;
227
-
228
- // check if the found existing wrapper is suitable
229
- if ( wrapper !== undefined && wrapper !== null ) {
230
- // version of active hls wrapper
231
- const setVersion = await callAsync ( wrapper , [ '--numeric-version' ] , storagePath , logger ) ;
232
-
233
- // is the currently set hls wrapper matching the required version?
234
- if ( comparePVP ( setVersion , installableHls ) !== 0 ) {
235
- return [ wrapper , projectGhc ] ;
219
+ } else {
220
+ // permissively check if we have HLS installed
221
+ // this is just to avoid a popup
222
+ let wrapper = await callGHCup ( context , logger ,
223
+ [ 'whereis' , 'hls' ] ,
224
+ undefined ,
225
+ false ,
226
+ ( err , stdout , _stderr , resolve , _reject ) => { err ? resolve ( '' ) : resolve ( stdout ?. trim ( ) ) }
227
+ ) ;
228
+ if ( wrapper === '' ) {
229
+ // install recommended HLS... even if this doesn't support the project GHC, because
230
+ // we need a HLS to find the correct project GHC in the first place
231
+ await callGHCup ( context , logger ,
232
+ [ 'install' , 'hls' ] ,
233
+ 'Installing latest HLS' ,
234
+ true
235
+ ) ;
236
+ // get path to just installed HLS
237
+ wrapper = await callGHCup ( context , logger ,
238
+ [ 'whereis' , 'hls' ] ,
239
+ undefined ,
240
+ false
241
+ ) ;
242
+ }
243
+ // now figure out the project GHC version and the latest supported HLS version
244
+ // we need for it (e.g. this might in fact be a downgrade for old GHCs)
245
+ const installableHls = await getLatestSuitableHLS (
246
+ context ,
247
+ logger ,
248
+ workingDir ,
249
+ ( wrapper === null ) ? undefined : wrapper
250
+ ) ;
251
+
252
+ // now install said version in an isolated symlink directory
253
+ const symHLSPath = path . join ( storagePath , 'hls' , installableHls ) ;
254
+ wrapper = path . join ( symHLSPath , `haskell-language-server-wrapper${ exeExt } ` ) ;
255
+ // Check if we have a working symlink, so we can avoid another popup
256
+ if ( ! fs . existsSync ( wrapper ) ) {
257
+ await callGHCup ( context , logger ,
258
+ [ 'run' , '--hls' , installableHls , '-b' , symHLSPath , '-i' ] ,
259
+ `Installing HLS ${ installableHls } ` ,
260
+ true
261
+ ) ;
236
262
}
263
+ return wrapper ;
237
264
}
238
-
239
- await callGHCup ( context , logger ,
240
- [ 'run' , '--hls' , installableHls , '-b' , symHLSPath , '-i' ] ,
241
- `Installing HLS ${ installableHls } ` ,
242
- true
243
- ) ;
244
- return [ path . join ( symHLSPath , `haskell-language-server-wrapper${ exeExt } ` ) , projectGhc ] ;
245
265
}
246
266
247
267
async function callGHCup (
248
268
context : ExtensionContext ,
249
269
logger : Logger ,
250
270
args : string [ ] ,
251
271
title ?: string ,
252
- cancellable ?: boolean
272
+ cancellable ?: boolean ,
273
+ callback ?: (
274
+ error : ExecException | null ,
275
+ stdout : string ,
276
+ stderr : string ,
277
+ resolve : ( value : string | PromiseLike < string > ) => void ,
278
+ reject : ( reason ?: any ) => void
279
+ ) => void
253
280
) : Promise < string > {
254
281
255
282
const storagePath : string = await getStoragePath ( context ) ;
256
283
const ghcup = ( systemGHCup === true ) ? `ghcup${ exeExt } ` : path . join ( storagePath , `ghcup${ exeExt } ` ) ;
257
284
if ( systemGHCup ) {
258
- return await callAsync ( 'ghcup' , [ '--no-verbose' ] . concat ( args ) , storagePath , logger , title , cancellable ) ;
285
+ return await callAsync ( 'ghcup' , [ '--no-verbose' ] . concat ( args ) , storagePath , logger , title , cancellable , undefined , callback ) ;
259
286
} else {
260
287
return await callAsync ( ghcup , [ '--no-verbose' ] . concat ( args ) , storagePath , logger , title , cancellable , {
261
288
GHCUP_INSTALL_BASE_PREFIX : storagePath ,
262
- } ) ;
289
+ } , callback ) ;
263
290
}
264
291
}
265
292
@@ -268,7 +295,7 @@ async function getLatestSuitableHLS(
268
295
logger : Logger ,
269
296
workingDir : string ,
270
297
wrapper ?: string
271
- ) : Promise < [ string , string ] > {
298
+ ) : Promise < string > {
272
299
const storagePath : string = await getStoragePath ( context ) ;
273
300
274
301
// get latest hls version
@@ -282,7 +309,6 @@ async function getLatestSuitableHLS(
282
309
const latestHlsVersion = hlsVersions . split ( / \r ? \n / ) . pop ( ) ! . split ( ' ' ) [ 1 ] ;
283
310
284
311
// get project GHC version
285
- // TODO: we may run this function twice on startup (e.g. in extension.ts)
286
312
const projectGhc =
287
313
wrapper === undefined
288
314
? await callAsync ( `ghc${ exeExt } ` , [ '--numeric-version' ] , storagePath , logger , undefined , false )
@@ -293,25 +319,9 @@ async function getLatestSuitableHLS(
293
319
projectGhc !== null ? await getLatestHLSforGHC ( context , storagePath , projectGhc , logger ) : null ;
294
320
const installableHls = latestMetadataHls !== null ? latestMetadataHls : latestHlsVersion ;
295
321
296
- return [ installableHls , projectGhc ] ;
322
+ return installableHls ;
297
323
}
298
324
299
- // also serves as sanity check
300
- export async function validateHLSToolchain (
301
- wrapper : string ,
302
- ghc : string ,
303
- workingDir : string ,
304
- logger : Logger
305
- ) : Promise < void > {
306
- const wrapperDir = path . dirname ( wrapper ) ;
307
- const hlsExe = path . join ( wrapperDir , `haskell-language-server-${ ghc } ${ exeExt } ` ) ;
308
- const hlsVer = await callAsync ( wrapper , [ '--numeric-version' ] , workingDir , logger ) ;
309
- if ( ! executableExists ( hlsExe ) ) {
310
- const msg = `Couldn't find ${ hlsExe } . Your project ghc version ${ ghc } may not be supported! Consider building HLS from source, e.g.: ghcup compile hls --jobs 8 --ghc ${ ghc } ${ hlsVer } ` ;
311
- window . showErrorMessage ( msg ) ;
312
- throw new Error ( msg ) ;
313
- }
314
- }
315
325
316
326
/**
317
327
* Obtain the project ghc version from the HLS - Wrapper.
@@ -361,30 +371,30 @@ export async function getProjectGHCVersion(
361
371
*/
362
372
export async function getGHCup ( context : ExtensionContext , logger : Logger ) : Promise < string | undefined > {
363
373
logger . info ( 'Checking for ghcup installation' ) ;
364
- const localGHCup = [ 'ghcup' ] . find ( executableExists ) ;
374
+ const localGHCup = [ 'ghcup' ] . find ( executableExists ) ;
365
375
366
376
if ( systemGHCup === null ) {
367
- if ( localGHCup !== undefined ) {
377
+ if ( localGHCup !== undefined ) {
368
378
const promptMessage =
369
379
'Detected system ghcup. Do you want VSCode to use it instead of an internal ghcup?' ;
370
380
371
381
systemGHCup = await window . showInformationMessage ( promptMessage , 'Yes' , 'No' ) . then ( b => b === 'Yes' ) ;
372
- logger . info ( `set useSystemGHCup to ${ systemGHCup } ` ) ;
382
+ logger . info ( `set useSystemGHCup to ${ systemGHCup } ` ) ;
373
383
374
- } else { // no local ghcup, disable
375
- systemGHCup = false ;
376
- }
377
- }
378
- // set config globally
379
- workspace . getConfiguration ( 'haskell' ) . update ( 'useSystemGHCup' , systemGHCup , ConfigurationTarget . Global ) ;
384
+ } else { // no local ghcup, disable
385
+ systemGHCup = false ;
386
+ }
387
+ }
388
+ // set config globally
389
+ workspace . getConfiguration ( 'haskell' ) . update ( 'useSystemGHCup' , systemGHCup , ConfigurationTarget . Global ) ;
380
390
381
391
if ( systemGHCup === true ) {
382
- if ( localGHCup === undefined ) {
392
+ if ( localGHCup === undefined ) {
383
393
const msg = 'Could not find a system ghcup installation, please follow instructions at https://www.haskell.org/ghcup/' ;
384
394
window . showErrorMessage ( msg ) ;
385
395
throw new Error ( msg ) ;
386
- }
387
- logger . info ( `found system ghcup at ${ localGHCup } ` ) ;
396
+ }
397
+ logger . info ( `found system ghcup at ${ localGHCup } ` ) ;
388
398
return localGHCup ;
389
399
}
390
400
0 commit comments