Skip to content

Commit 5292056

Browse files
authored
Merge pull request #579 from fendor/enhancement/tool-class
Add Tool class and print stacktraces
2 parents 81e7709 + 77416e0 commit 5292056

File tree

3 files changed

+120
-82
lines changed

3 files changed

+120
-82
lines changed

src/errors.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Uri } from 'vscode';
2+
3+
// tslint:disable max-classes-per-file
4+
export class HlsError extends Error {}
5+
6+
export class MissingToolError extends HlsError {
7+
public readonly tool: string;
8+
constructor(tool: string) {
9+
let prettyTool: string;
10+
switch (tool.toLowerCase()) {
11+
case 'stack':
12+
prettyTool = 'Stack';
13+
break;
14+
case 'cabal':
15+
prettyTool = 'Cabal';
16+
break;
17+
case 'ghc':
18+
prettyTool = 'GHC';
19+
break;
20+
case 'ghcup':
21+
prettyTool = 'GHCup';
22+
break;
23+
case 'haskell-language-server':
24+
prettyTool = 'HLS';
25+
break;
26+
case 'hls':
27+
prettyTool = 'HLS';
28+
break;
29+
default:
30+
prettyTool = tool;
31+
break;
32+
}
33+
super(`Project requires ${prettyTool} but it isn't installed`);
34+
this.tool = prettyTool;
35+
}
36+
37+
public installLink(): Uri | null {
38+
switch (this.tool) {
39+
case 'Stack':
40+
return Uri.parse('https://docs.haskellstack.org/en/stable/install_and_upgrade/');
41+
case 'GHCup':
42+
case 'Cabal':
43+
case 'HLS':
44+
case 'GHC':
45+
return Uri.parse('https://www.haskell.org/ghcup/');
46+
default:
47+
return null;
48+
}
49+
}
50+
}
51+
52+
export class NoMatchingHls extends Error {
53+
constructor(readonly ghcProjVersion: string) {
54+
const noMatchingHLS = `No HLS version was found for supporting GHC ${ghcProjVersion}.`;
55+
super(noMatchingHLS);
56+
}
57+
}

src/extension.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
import { CommandNames } from './commands/constants';
2424
import { ImportIdentifier } from './commands/importIdentifier';
2525
import { DocsBrowser } from './docsBrowser';
26-
import { findHaskellLanguageServer, IEnvVars, MissingToolError } from './hlsBinaries';
26+
import { HlsError, MissingToolError } from './errors';
27+
import { findHaskellLanguageServer, IEnvVars } from './hlsBinaries';
2728
import { addPathToProcessPath, expandHomeDir, ExtensionLogger } from './utils';
2829

2930
// The current map of documents & folders to language servers.
@@ -172,8 +173,17 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
172173
} else {
173174
await window.showErrorMessage(e.message);
174175
}
176+
} else if (e instanceof HlsError) {
177+
logger.error(`General HlsError: ${e.message}`);
178+
if (e.stack) {
179+
logger.error(`${e.stack}`);
180+
}
181+
window.showErrorMessage(e.message);
175182
} else if (e instanceof Error) {
176-
logger.error(`Error getting the server executable: ${e.message}`);
183+
logger.error(`Internal Error: ${e.message}`);
184+
if (e.stack) {
185+
logger.error(`${e.stack}`);
186+
}
177187
window.showErrorMessage(e.message);
178188
}
179189
return;

src/hlsBinaries.ts

+51-80
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,9 @@ import * as path from 'path';
77
import { match } from 'ts-pattern';
88
import * as url from 'url';
99
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';
1911
import { Logger } from 'vscode-languageclient';
12+
import { HlsError, MissingToolError, NoMatchingHls } from './errors';
2013
import {
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
4033
const 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

Comments
 (0)