Skip to content

Commit 6382e2d

Browse files
committed
Turn ghcup into a class
1 parent e111eca commit 6382e2d

File tree

4 files changed

+137
-107
lines changed

4 files changed

+137
-107
lines changed

src/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { expandHomeDir, IEnvVars } from './utils';
33
import * as path from 'path';
44
import { Logger } from 'vscode-languageclient';
55
import { ExtensionLogger } from './logger';
6+
import { GHCupConfig } from './ghcup';
67

78
export type LogLevel = 'off' | 'messages' | 'verbose';
89
export type ClientLogLevel = 'off' | 'error' | 'info' | 'debug';
@@ -19,6 +20,7 @@ export type Config = {
1920
outputChannel: OutputChannel;
2021
serverArgs: string[];
2122
serverEnvironment: IEnvVars;
23+
ghcupConfig: GHCupConfig;
2224
};
2325

2426
export function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, folder?: WorkspaceFolder): Config {
@@ -44,6 +46,11 @@ export function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, fo
4446
outputChannel: outputChannel,
4547
serverArgs: serverArgs,
4648
serverEnvironment: workspaceConfig.serverEnvironment,
49+
ghcupConfig: {
50+
metadataUrl: workspaceConfig.metadataURL as string,
51+
upgradeGHCup: workspaceConfig.get('upgradeGHCup') as boolean,
52+
executablePath: workspaceConfig.get('ghcupExecutablePath') as string,
53+
},
4754
};
4855
}
4956

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
118118

119119
let hlsExecutable: HlsExecutable;
120120
try {
121-
hlsExecutable = await findHaskellLanguageServer(context, logger, config.workingDir, folder);
121+
hlsExecutable = await findHaskellLanguageServer(context, logger, config.ghcupConfig, config.workingDir, folder);
122122
} catch (e) {
123123
if (e instanceof MissingToolError) {
124124
const link = e.installLink();

src/ghcup.ts

Lines changed: 102 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,122 @@
11
import * as path from 'path';
22
import * as os from 'os';
33
import * as process from 'process';
4-
import { workspace, WorkspaceFolder } from 'vscode';
4+
import { WorkspaceFolder } from 'vscode';
55
import { Logger } from 'vscode-languageclient';
66
import { MissingToolError } from './errors';
7-
import { resolvePathPlaceHolders, executableExists, callAsync, ProcessCallback } from './utils';
7+
import { resolvePathPlaceHolders, executableExists, callAsync, ProcessCallback, IEnvVars } from './utils';
88
import { match } from 'ts-pattern';
99

1010
export type Tool = 'hls' | 'ghc' | 'cabal' | 'stack';
1111

1212
export type ToolConfig = Map<Tool, string>;
1313

14-
export async function callGHCup(
15-
logger: Logger,
16-
args: string[],
17-
title?: string,
18-
cancellable?: boolean,
19-
callback?: ProcessCallback,
20-
): Promise<string> {
21-
const metadataUrl = workspace.getConfiguration('haskell').metadataURL;
22-
const ghcup = findGHCup(logger);
23-
return await callAsync(
24-
ghcup,
25-
['--no-verbose'].concat(metadataUrl ? ['-s', metadataUrl] : []).concat(args),
26-
logger,
27-
undefined,
28-
title,
29-
cancellable,
30-
{
31-
// omit colourful output because the logs are uglier
32-
NO_COLOR: '1',
33-
},
34-
callback,
35-
);
14+
export function initDefaultGHCup(config: GHCupConfig, logger: Logger, folder?: WorkspaceFolder): GHCup {
15+
const ghcupLoc = findGHCup(logger, config.executablePath, folder);
16+
return new GHCup(logger, ghcupLoc, config, {
17+
// omit colourful output because the logs are uglier
18+
NO_COLOR: '1',
19+
});
3620
}
3721

38-
export async function upgradeGHCup(logger: Logger): Promise<void> {
39-
const upgrade = workspace.getConfiguration('haskell').get('upgradeGHCup') as boolean;
40-
if (upgrade) {
41-
await callGHCup(logger, ['upgrade'], 'Upgrading ghcup', true);
22+
export type GHCupConfig = {
23+
metadataUrl?: string;
24+
upgradeGHCup: boolean;
25+
executablePath?: string;
26+
};
27+
28+
export class GHCup {
29+
constructor(
30+
readonly logger: Logger,
31+
readonly location: string,
32+
readonly config: GHCupConfig,
33+
readonly environment: IEnvVars,
34+
) {}
35+
36+
/**
37+
* Most generic way to run the `ghcup` binary.
38+
* @param args Arguments to run the `ghcup` binary with.
39+
* @param title Displayed to the user for long-running tasks.
40+
* @param cancellable Whether this invocation can be cancelled by the user.
41+
* @param callback Handle success or failures.
42+
* @returns The output of the `ghcup` invocation. If no {@link callback} is given, this is the stdout. Otherwise, whatever {@link callback} produces.
43+
*/
44+
public async call(
45+
args: string[],
46+
title?: string,
47+
cancellable?: boolean,
48+
callback?: ProcessCallback,
49+
): Promise<string> {
50+
const metadataUrl = this.config.metadataUrl; // ;
51+
return await callAsync(
52+
this.location,
53+
['--no-verbose'].concat(metadataUrl ? ['-s', metadataUrl] : []).concat(args),
54+
this.logger,
55+
undefined,
56+
title,
57+
cancellable,
58+
this.environment,
59+
callback,
60+
);
61+
}
62+
63+
/**
64+
* Upgrade the `ghcup` binary unless this option was disabled by the user.
65+
*/
66+
public async upgrade(): Promise<void> {
67+
const upgrade = this.config.upgradeGHCup; // workspace.getConfiguration('haskell').get('upgradeGHCup') as boolean;
68+
if (upgrade) {
69+
await this.call(['upgrade'], 'Upgrading ghcup', true);
70+
}
71+
}
72+
73+
/**
74+
* Find the latest version of a {@link Tool} that we can find in GHCup.
75+
* Prefer already installed versions, but fall back to all available versions, if there aren't any.
76+
* @param tool Tool you want to know the latest version of.
77+
* @returns The latest installed or generally available version of the {@link tool}
78+
*/
79+
public async getLatestVersion(tool: Tool): Promise<string> {
80+
// these might be custom/stray/compiled, so we try first
81+
const installedVersions = await this.call(['list', '-t', tool, '-c', 'installed', '-r'], undefined, false);
82+
const latestInstalled = installedVersions.split(/\r?\n/).pop();
83+
if (latestInstalled) {
84+
return latestInstalled.split(/\s+/)[1];
85+
}
86+
87+
return this.getLatestAvailableVersion(tool);
88+
}
89+
90+
/**
91+
* Find the latest available version that we can find in GHCup with a certain {@link tag}.
92+
* Corresponds to the `ghcup list -t <tool> -c available -r` command.
93+
* The tag can be used to further filter the list of versions, for example you can provide
94+
* @param tool Tool you want to know the latest version of.
95+
* @param tag The tag to filter the available versions with. By default `"latest"`.
96+
* @returns The latest available version filtered by {@link tag}.
97+
*/
98+
public async getLatestAvailableVersion(tool: Tool, tag: string = 'latest'): Promise<string> {
99+
// fall back to installable versions
100+
const availableVersions = await this.call(['list', '-t', tool, '-c', 'available', '-r'], undefined, false).then(
101+
(s) => s.split(/\r?\n/),
102+
);
103+
104+
let latestAvailable: string | null = null;
105+
availableVersions.forEach((ver) => {
106+
if (ver.split(/\s+/)[2].split(',').includes(tag)) {
107+
latestAvailable = ver.split(/\s+/)[1];
108+
}
109+
});
110+
if (!latestAvailable) {
111+
throw new Error(`Unable to find ${tag} tool ${tool}`);
112+
} else {
113+
return latestAvailable;
114+
}
42115
}
43116
}
44117

45-
export function findGHCup(logger: Logger, folder?: WorkspaceFolder): string {
118+
function findGHCup(logger: Logger, exePath?: string, folder?: WorkspaceFolder): string {
46119
logger.info('Checking for ghcup installation');
47-
let exePath = workspace.getConfiguration('haskell').get('ghcupExecutablePath') as string;
48120
if (exePath) {
49121
logger.info(`Trying to find the ghcup executable in: ${exePath}`);
50122
exePath = resolvePathPlaceHolders(exePath, folder);
@@ -97,47 +169,3 @@ export function findGHCup(logger: Logger, folder?: WorkspaceFolder): string {
97169
}
98170
}
99171
}
100-
101-
// the tool might be installed or not
102-
export async function getLatestToolFromGHCup(logger: Logger, tool: Tool): Promise<string> {
103-
// these might be custom/stray/compiled, so we try first
104-
const installedVersions = await callGHCup(logger, ['list', '-t', tool, '-c', 'installed', '-r'], undefined, false);
105-
const latestInstalled = installedVersions.split(/\r?\n/).pop();
106-
if (latestInstalled) {
107-
return latestInstalled.split(/\s+/)[1];
108-
}
109-
110-
return getLatestAvailableToolFromGHCup(logger, tool);
111-
}
112-
113-
export async function getLatestAvailableToolFromGHCup(
114-
logger: Logger,
115-
tool: Tool,
116-
tag?: string,
117-
criteria?: string,
118-
): Promise<string> {
119-
// fall back to installable versions
120-
const availableVersions = await callGHCup(
121-
logger,
122-
['list', '-t', tool, '-c', criteria ? criteria : 'available', '-r'],
123-
undefined,
124-
false,
125-
).then((s) => s.split(/\r?\n/));
126-
127-
let latestAvailable: string | null = null;
128-
availableVersions.forEach((ver) => {
129-
if (
130-
ver
131-
.split(/\s+/)[2]
132-
.split(',')
133-
.includes(tag ? tag : 'latest')
134-
) {
135-
latestAvailable = ver.split(/\s+/)[1];
136-
}
137-
});
138-
if (!latestAvailable) {
139-
throw new Error(`Unable to find ${tag ? tag : 'latest'} tool ${tool}`);
140-
} else {
141-
return latestAvailable;
142-
}
143-
}

0 commit comments

Comments
 (0)