Skip to content

Commit f90eaea

Browse files
committed
Introduce named type for Hls Binary locations
1 parent 0459c30 commit f90eaea

File tree

4 files changed

+104
-67
lines changed

4 files changed

+104
-67
lines changed

src/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OutputChannel, Uri, window, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
2-
import { expandHomeDir, ExtensionLogger } from './utils';
2+
import { expandHomeDir, ExtensionLogger, IEnvVars } from './utils';
33
import path = require('path');
44
import { Logger } from 'vscode-languageclient';
55

@@ -17,6 +17,7 @@ export type Config = {
1717
workingDir: string;
1818
outputChannel: OutputChannel;
1919
serverArgs: string[];
20+
serverEnvironment: IEnvVars;
2021
};
2122

2223
export function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, folder?: WorkspaceFolder): Config {
@@ -41,6 +42,7 @@ export function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, fo
4142
workingDir: currentWorkingDir,
4243
outputChannel: outputChannel,
4344
serverArgs: serverArgs,
45+
serverEnvironment: workspaceConfig.serverEnvironment,
4446
};
4547
}
4648

src/extension.ts

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
import { RestartServerCommandName, StartServerCommandName, StopServerCommandName } from './commands/constants';
1111
import * as DocsBrowser from './docsBrowser';
1212
import { HlsError, MissingToolError, NoMatchingHls } from './errors';
13-
import { callAsync, findHaskellLanguageServer, IEnvVars } from './hlsBinaries';
13+
import { callAsync, findHaskellLanguageServer, HlsExecutable, IEnvVars } from './hlsBinaries';
1414
import { addPathToProcessPath, comparePVP } from './utils';
15-
import { initConfig, initLoggerFromConfig, logConfig } from './config';
15+
import { Config, initConfig, initLoggerFromConfig, logConfig } from './config';
1616

1717
// The current map of documents & folders to language servers.
1818
// It may be null to indicate that we are in the process of launching a server,
@@ -114,18 +114,9 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
114114

115115
logConfig(logger, config);
116116

117-
let serverExecutable: string;
118-
let addInternalServerPath: string | undefined; // if we download HLS, add that bin dir to PATH
117+
let hlsExecutable: HlsExecutable;
119118
try {
120-
[serverExecutable, addInternalServerPath] = await findHaskellLanguageServer(
121-
context,
122-
logger,
123-
config.workingDir,
124-
folder,
125-
);
126-
if (!serverExecutable) {
127-
return;
128-
}
119+
hlsExecutable = await findHaskellLanguageServer(context, logger, config.workingDir, folder);
129120
} catch (e) {
130121
if (e instanceof MissingToolError) {
131122
const link = e.installLink();
@@ -169,14 +160,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
169160
}
170161
logger.info(cwdMsg);
171162

172-
let serverEnvironment: IEnvVars = await workspace.getConfiguration('haskell', uri).serverEnvironment;
173-
if (addInternalServerPath !== undefined) {
174-
const newPath = await addPathToProcessPath(addInternalServerPath);
175-
serverEnvironment = {
176-
...serverEnvironment,
177-
...{ PATH: newPath },
178-
};
179-
}
163+
const serverEnvironment: IEnvVars = initServerEnvironment(config, hlsExecutable);
180164
const exeOptions: ExecutableOptions = {
181165
cwd: config.workingDir,
182166
env: { ...process.env, ...serverEnvironment },
@@ -185,12 +169,12 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
185169
// For our intents and purposes, the server should be launched the same way in
186170
// both debug and run mode.
187171
const serverOptions: ServerOptions = {
188-
run: { command: serverExecutable, args: config.serverArgs, options: exeOptions },
189-
debug: { command: serverExecutable, args: config.serverArgs, options: exeOptions },
172+
run: { command: hlsExecutable.location, args: config.serverArgs, options: exeOptions },
173+
debug: { command: hlsExecutable.location, args: config.serverArgs, options: exeOptions },
190174
};
191175

192-
logger.info(`run command: ${serverExecutable} ${config.serverArgs.join(' ')}`);
193-
logger.info(`debug command: ${serverExecutable} ${config.serverArgs.join(' ')}`);
176+
logger.info(`run command: ${hlsExecutable.location} ${config.serverArgs.join(' ')}`);
177+
logger.info(`debug command: ${hlsExecutable.location} ${config.serverArgs.join(' ')}`);
194178
if (exeOptions.cwd) {
195179
logger.info(`server cwd: ${exeOptions.cwd}`);
196180
}
@@ -221,7 +205,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
221205
switch (cabalFileSupport) {
222206
case 'automatic':
223207
const hlsVersion = await callAsync(
224-
serverExecutable,
208+
hlsExecutable.location,
225209
['--numeric-version'],
226210
logger,
227211
config.workingDir,
@@ -275,6 +259,18 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
275259
await langClient.start();
276260
}
277261

262+
function initServerEnvironment(config: Config, hlsExecutable: HlsExecutable) {
263+
let serverEnvironment: IEnvVars = config.serverEnvironment;
264+
if (hlsExecutable.tag === 'ghcup') {
265+
const newPath = addPathToProcessPath(hlsExecutable.binaryDirectory);
266+
serverEnvironment = {
267+
...serverEnvironment,
268+
...{ PATH: newPath },
269+
};
270+
}
271+
return serverEnvironment;
272+
}
273+
278274
/*
279275
* Deactivate each of the LSP servers.
280276
*/

src/hlsBinaries.ts

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ type ToolConfig = Map<Tool, string>;
2828
type ManageHLS = 'GHCup' | 'PATH';
2929
let manageHLS = workspace.getConfiguration('haskell').get('manageHLS') as ManageHLS;
3030

31+
export type Context = {
32+
manageHls: ManageHLS;
33+
serverResolved?: HlsExecutable;
34+
};
35+
3136
// On Windows the executable needs to be stored somewhere with an .exe extension
3237
const exeExt = process.platform === 'win32' ? '.exe' : '';
3338

@@ -163,6 +168,27 @@ async function findHLSinPATH(_context: ExtensionContext, logger: Logger): Promis
163168
throw new MissingToolError('hls');
164169
}
165170

171+
export type HlsExecutable = HlsOnPath | HlsViaVSCodeConfig | HlsViaGhcup;
172+
173+
export type HlsOnPath = {
174+
location: string;
175+
tag: 'path';
176+
};
177+
178+
export type HlsViaVSCodeConfig = {
179+
location: string;
180+
tag: 'config';
181+
};
182+
183+
export type HlsViaGhcup = {
184+
location: string;
185+
/**
186+
* if we download HLS, add that bin dir to PATH
187+
*/
188+
binaryDirectory: string;
189+
tag: 'ghcup';
190+
};
191+
166192
/**
167193
* Downloads the latest haskell-language-server binaries via GHCup.
168194
* Makes sure that either `ghcup` is available locally, otherwise installs
@@ -181,12 +207,15 @@ export async function findHaskellLanguageServer(
181207
logger: Logger,
182208
workingDir: string,
183209
folder?: WorkspaceFolder,
184-
): Promise<[string, string | undefined]> {
210+
): Promise<HlsExecutable> {
185211
logger.info('Finding haskell-language-server');
186212

187213
if (workspace.getConfiguration('haskell').get('serverExecutablePath') as string) {
188214
const exe = await findServerExecutable(logger, folder);
189-
return [exe, undefined];
215+
return {
216+
location: exe,
217+
tag: 'config',
218+
};
190219
}
191220

192221
const storagePath: string = await getStoragePath(context);
@@ -196,47 +225,24 @@ export async function findHaskellLanguageServer(
196225
}
197226

198227
// first plugin initialization
199-
if (manageHLS !== 'GHCup' && (!context.globalState.get('pluginInitialized') as boolean | null)) {
200-
const promptMessage = `How do you want the extension to manage/discover HLS and the relevant toolchain?
201-
202-
Choose "Automatically" if you're in doubt.
203-
`;
204-
205-
const popup = window.showInformationMessage(
206-
promptMessage,
207-
{ modal: true },
208-
'Automatically via GHCup',
209-
'Manually via PATH',
210-
);
211-
212-
const decision = (await popup) || null;
213-
if (decision === 'Automatically via GHCup') {
214-
manageHLS = 'GHCup';
215-
} else if (decision === 'Manually via PATH') {
216-
manageHLS = 'PATH';
217-
} else {
218-
window.showWarningMessage(
219-
"Choosing default PATH method for HLS discovery. You can change this via 'haskell.manageHLS' in the settings.",
220-
);
221-
manageHLS = 'PATH';
222-
}
223-
workspace.getConfiguration('haskell').update('manageHLS', manageHLS, ConfigurationTarget.Global);
224-
context.globalState.update('pluginInitialized', true);
225-
}
228+
manageHLS = await promptUserForManagingHls(context, manageHLS);
226229

227230
if (manageHLS === 'PATH') {
228231
const exe = await findHLSinPATH(context, logger);
229-
return [exe, undefined];
232+
return {
233+
location: exe,
234+
tag: 'path',
235+
};
230236
} else {
231237
// we manage HLS, make sure ghcup is installed/available
232238
await upgradeGHCup(context, logger);
233239

234240
// boring init
235-
let latestHLS: string | undefined | null;
241+
let latestHLS: string | undefined;
236242
let latestCabal: string | undefined | null;
237243
let latestStack: string | undefined | null;
238244
let recGHC: string | undefined | null = 'recommended';
239-
let projectHls: string | undefined | null;
245+
let projectHls: string | undefined;
240246
let projectGhc: string | undefined | null;
241247

242248
// support explicit toolchain config
@@ -358,7 +364,7 @@ export async function findHaskellLanguageServer(
358364

359365
// more download popups
360366
if (promptBeforeDownloads) {
361-
const hlsInstalled = projectHls ? await toolInstalled(context, logger, 'hls', projectHls) : undefined;
367+
const hlsInstalled = await toolInstalled(context, logger, 'hls', projectHls);
362368
const ghcInstalled = projectGhc ? await toolInstalled(context, logger, 'ghc', projectGhc) : undefined;
363369
const toInstall: InstalledTool[] = [hlsInstalled, ghcInstalled].filter(
364370
(tool) => tool && !tool.installed,
@@ -398,7 +404,7 @@ export async function findHaskellLanguageServer(
398404
logger,
399405
[
400406
'run',
401-
...(projectHls ? ['--hls', projectHls] : []),
407+
...['--hls', projectHls],
402408
...(latestCabal ? ['--cabal', latestCabal] : []),
403409
...(latestStack ? ['--stack', latestStack] : []),
404410
...(projectGhc ? ['--ghc', projectGhc] : []),
@@ -416,12 +422,45 @@ export async function findHaskellLanguageServer(
416422
true,
417423
);
418424

419-
if (projectHls) {
420-
return [path.join(hlsBinDir, `haskell-language-server-wrapper${exeExt}`), hlsBinDir];
425+
return {
426+
binaryDirectory: hlsBinDir,
427+
location: path.join(hlsBinDir, `haskell-language-server-wrapper${exeExt}`),
428+
tag: 'ghcup',
429+
};
430+
}
431+
}
432+
433+
async function promptUserForManagingHls(context: ExtensionContext, manageHlsSetting: ManageHLS): Promise<ManageHLS> {
434+
if (manageHlsSetting !== 'GHCup' && (!context.globalState.get('pluginInitialized') as boolean | null)) {
435+
const promptMessage = `How do you want the extension to manage/discover HLS and the relevant toolchain?
436+
437+
Choose "Automatically" if you're in doubt.
438+
`;
439+
440+
const popup = window.showInformationMessage(
441+
promptMessage,
442+
{ modal: true },
443+
'Automatically via GHCup',
444+
'Manually via PATH',
445+
);
446+
447+
const decision = (await popup) || null;
448+
let howToManage: ManageHLS;
449+
if (decision === 'Automatically via GHCup') {
450+
howToManage = 'GHCup';
451+
} else if (decision === 'Manually via PATH') {
452+
howToManage = 'PATH';
421453
} else {
422-
const exe = await findHLSinPATH(context, logger);
423-
return [exe, hlsBinDir];
454+
window.showWarningMessage(
455+
"Choosing default PATH method for HLS discovery. You can change this via 'haskell.manageHLS' in the settings.",
456+
);
457+
howToManage = 'PATH';
424458
}
459+
workspace.getConfiguration('haskell').update('manageHLS', howToManage, ConfigurationTarget.Global);
460+
context.globalState.update('pluginInitialized', true);
461+
return howToManage;
462+
} else {
463+
return manageHlsSetting;
425464
}
426465
}
427466

src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,9 @@ export function resolvePATHPlaceHolders(path: string) {
201201
}
202202

203203
// also honours serverEnvironment.PATH
204-
export async function addPathToProcessPath(extraPath: string): Promise<string> {
204+
export function addPathToProcessPath(extraPath: string): string {
205205
const pathSep = process.platform === 'win32' ? ';' : ':';
206-
const serverEnvironment: IEnvVars = (await workspace.getConfiguration('haskell').get('serverEnvironment')) || {};
206+
const serverEnvironment: IEnvVars = workspace.getConfiguration('haskell').get('serverEnvironment') || {};
207207
const path: string[] = serverEnvironment.PATH
208208
? serverEnvironment.PATH.split(pathSep).map((p) => resolvePATHPlaceHolders(p))
209209
: (process.env.PATH?.split(pathSep) ?? []);

0 commit comments

Comments
 (0)