Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanups #605

Merged
merged 3 commits into from
Apr 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
os: [macos-11, ubuntu-latest, windows-latest]
ghc: [9.0.1, 8.10.4]
ghc: [9.0.2, 8.10.4]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ export class NoMatchingHls extends Error {
super(`HLS does not support GHC ${ghcProjVersion} yet.`);
}
public docLink(): Uri {
return Uri.parse('https://haskell-language-server.readthedocs.io/en/latest/supported-versions.html');
return Uri.parse('https://haskell-language-server.readthedocs.io/en/latest/supported-versions.html');
}
}
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold

let serverEnvironment: IEnvVars = await workspace.getConfiguration('haskell', uri).serverEnvironment;
if (addInternalServerPath !== undefined) {
const newPath = await addPathToProcessPath(addInternalServerPath, logger);
const newPath = await addPathToProcessPath(addInternalServerPath);
serverEnvironment = {
...serverEnvironment,
...{ PATH: newPath },
Expand Down
111 changes: 59 additions & 52 deletions src/hlsBinaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { stat } from 'fs/promises';
import * as https from 'https';
import * as path from 'path';
import { match } from 'ts-pattern';
import * as url from 'url';
import { promisify } from 'util';
import { ConfigurationTarget, ExtensionContext, ProgressLocation, window, workspace, WorkspaceFolder } from 'vscode';
import { Logger } from 'vscode-languageclient';
Expand Down Expand Up @@ -68,7 +67,7 @@ async function callAsync(
envAdd?: IEnvVars,
callback?: ProcessCallback
): Promise<string> {
let newEnv: IEnvVars = await resolveServerEnvironmentPATH(
let newEnv: IEnvVars = resolveServerEnvironmentPATH(
workspace.getConfiguration('haskell').get('serverEnvironment') || {}
);
newEnv = { ...(process.env as IEnvVars), ...newEnv, ...(envAdd || {}) };
Expand Down Expand Up @@ -135,15 +134,14 @@ async function callAsync(
/** Gets serverExecutablePath and fails if it's not set.
*/
async function findServerExecutable(
context: ExtensionContext,
logger: Logger,
folder?: WorkspaceFolder
): Promise<string> {
let exePath = workspace.getConfiguration('haskell').get('serverExecutablePath') as string;
logger.info(`Trying to find the server executable in: ${exePath}`);
exePath = resolvePathPlaceHolders(exePath, folder);
logger.log(`Location after path variables substitution: ${exePath}`);
if (await executableExists(exePath)) {
if (executableExists(exePath)) {
return exePath;
} else {
const msg = `Could not find a HLS binary at ${exePath}! Consider installing HLS via ghcup or change "haskell.manageHLS" in your settings.`;
Expand All @@ -153,13 +151,13 @@ async function findServerExecutable(

/** Searches the PATH. Fails if nothing is found.
*/
async function findHLSinPATH(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): Promise<string> {
async function findHLSinPATH(_context: ExtensionContext, logger: Logger): Promise<string> {
// try PATH
const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server'];
logger.info(`Searching for server executables ${exes.join(',')} in $PATH`);
logger.info(`$PATH environment variable: ${process.env.PATH}`);
for (const exe of exes) {
if (await executableExists(exe)) {
if (executableExists(exe)) {
logger.info(`Found server executable in $PATH: ${exe}`);
return exe;
}
Expand Down Expand Up @@ -189,7 +187,7 @@ export async function findHaskellLanguageServer(
logger.info('Finding haskell-language-server');

if (workspace.getConfiguration('haskell').get('serverExecutablePath') as string) {
const exe = await findServerExecutable(context, logger, folder);
const exe = await findServerExecutable(logger, folder);
return [exe, undefined];
}

Expand Down Expand Up @@ -226,7 +224,7 @@ export async function findHaskellLanguageServer(
}

if (manageHLS === 'PATH') {
const exe = await findHLSinPATH(context, logger, folder);
const exe = await findHLSinPATH(context, logger);
return [exe, undefined];
} else {
// we manage HLS, make sure ghcup is installed/available
Expand Down Expand Up @@ -267,40 +265,42 @@ export async function findHaskellLanguageServer(
latestStack = await getLatestToolFromGHCup(context, logger, 'stack');
}
if (recGHC === undefined) {
recGHC = !(await executableExists('ghc'))
recGHC = !executableExists('ghc')
? await getLatestAvailableToolFromGHCup(context, logger, 'ghc', 'recommended')
: null;
}

// download popups
const promptBeforeDownloads = workspace.getConfiguration('haskell').get('promptBeforeDownloads') as boolean;
if (promptBeforeDownloads) {
const hlsInstalled = latestHLS
? await toolInstalled(context, logger, 'hls', latestHLS)
: undefined;
const cabalInstalled = latestCabal
? await toolInstalled(context, logger, 'cabal', latestCabal)
: undefined;
const stackInstalled = latestStack
? await toolInstalled(context, logger, 'stack', latestStack)
: undefined;
const hlsInstalled = latestHLS ? await toolInstalled(context, logger, 'hls', latestHLS) : undefined;
const cabalInstalled = latestCabal ? await toolInstalled(context, logger, 'cabal', latestCabal) : undefined;
const stackInstalled = latestStack ? await toolInstalled(context, logger, 'stack', latestStack) : undefined;
const ghcInstalled = executableExists('ghc')
? new InstalledTool('ghc', await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false))
// if recGHC is null, that means user disabled automatic handling,
: (recGHC !== null ? await toolInstalled(context, logger, 'ghc', recGHC) : undefined);
const toInstall: InstalledTool[] = [hlsInstalled, cabalInstalled, stackInstalled, ghcInstalled]
.filter((tool) => tool && !tool.installed) as InstalledTool[];
? new InstalledTool(
'ghc',
await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false)
)
: // if recGHC is null, that means user disabled automatic handling,
recGHC !== null
? await toolInstalled(context, logger, 'ghc', recGHC)
: undefined;
const toInstall: InstalledTool[] = [hlsInstalled, cabalInstalled, stackInstalled, ghcInstalled].filter(
(tool) => tool && !tool.installed
) as InstalledTool[];
if (toInstall.length > 0) {
const decision = await window.showInformationMessage(
`Need to download ${toInstall.map(t => t.nameWithVersion).join(', ')}, continue?`,
`Need to download ${toInstall.map((t) => t.nameWithVersion).join(', ')}, continue?`,
'Yes',
'No',
"Yes, don't ask again"
);
if (decision === 'Yes') {
logger.info(`User accepted download for ${toInstall.map(t => t.nameWithVersion).join(', ')}.`);
logger.info(`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')}.`);
} else if (decision === "Yes, don't ask again") {
logger.info(`User accepted download for ${toInstall.map(t => t.nameWithVersion).join(', ')} and won't be asked again.`);
logger.info(
`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')} and won't be asked again.`
);
workspace.getConfiguration('haskell').update('promptBeforeDownloads', false);
} else {
toInstall.forEach((tool) => {
Expand Down Expand Up @@ -353,26 +353,25 @@ export async function findHaskellLanguageServer(

// more download popups
if (promptBeforeDownloads) {
const hlsInstalled = projectHls
? await toolInstalled(context, logger, 'hls', projectHls)
: undefined;
const ghcInstalled = projectGhc
? await toolInstalled(context, logger, 'ghc', projectGhc)
: undefined;
const toInstall: InstalledTool[] = [hlsInstalled, ghcInstalled]
.filter((tool) => tool && !tool.installed) as InstalledTool[];
const hlsInstalled = projectHls ? await toolInstalled(context, logger, 'hls', projectHls) : undefined;
const ghcInstalled = projectGhc ? await toolInstalled(context, logger, 'ghc', projectGhc) : undefined;
const toInstall: InstalledTool[] = [hlsInstalled, ghcInstalled].filter(
(tool) => tool && !tool.installed
) as InstalledTool[];
if (toInstall.length > 0) {
const decision = await window.showInformationMessage(
`Need to download ${toInstall.map(t => t.nameWithVersion).join(', ')}, continue?`,
`Need to download ${toInstall.map((t) => t.nameWithVersion).join(', ')}, continue?`,
{ modal: true },
'Yes',
'No',
"Yes, don't ask again"
);
if (decision === 'Yes') {
logger.info(`User accepted download for ${toInstall.map(t => t.nameWithVersion).join(', ')}.`);
logger.info(`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')}.`);
} else if (decision === "Yes, don't ask again") {
logger.info(`User accepted download for ${toInstall.map(t => t.nameWithVersion).join(', ')} and won't be asked again.`);
logger.info(
`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')} and won't be asked again.`
);
workspace.getConfiguration('haskell').update('promptBeforeDownloads', false);
} else {
toInstall.forEach((tool) => {
Expand Down Expand Up @@ -400,14 +399,22 @@ export async function findHaskellLanguageServer(
...(projectGhc ? ['--ghc', projectGhc] : []),
'--install',
],
`Installing project specific toolchain: ${[['hls', projectHls], ['GHC', projectGhc], ['cabal', latestCabal], ['stack', latestStack]].filter(t => t[1]).map(t => `${t[0]}-${t[1]}`).join(', ')}`,
`Installing project specific toolchain: ${[
['hls', projectHls],
['GHC', projectGhc],
['cabal', latestCabal],
['stack', latestStack],
]
.filter((t) => t[1])
.map((t) => `${t[0]}-${t[1]}`)
.join(', ')}`,
true
);

if (projectHls) {
return [path.join(hlsBinDir, `haskell-language-server-wrapper${exeExt}`), hlsBinDir];
} else {
const exe = await findHLSinPATH(context, logger, folder);
const exe = await findHLSinPATH(context, logger);
return [exe, hlsBinDir];
}
}
Expand Down Expand Up @@ -470,8 +477,8 @@ async function getLatestProjectHLS(
const merged = new Map<string, string[]>([...metadataMap, ...ghcupMap]); // right-biased
// now sort and get the latest suitable version
const latest = [...merged]
.filter(([k, v]) => v.some((x) => x === projectGhc))
.sort(([k1, v1], [k2, v2]) => comparePVP(k1, k2))
.filter(([_k, v]) => v.some((x) => x === projectGhc))
.sort(([k1, _v1], [k2, _v2]) => comparePVP(k1, k2))
.pop();

if (!latest) {
Expand All @@ -484,7 +491,7 @@ async function getLatestProjectHLS(
/**
* Obtain the project ghc version from the HLS - Wrapper (which must be in PATH now).
* Also, serves as a sanity check.
* @param wrapper Path to the Haskell-Language-Server wrapper
* @param toolchainBindir Path to the toolchainn bin directory (added to PATH)
* @param workingDir Directory to run the process, usually the root of the workspace.
* @param logger Logger for feedback.
* @returns The GHC version, or fail with an `Error`.
Expand All @@ -499,7 +506,7 @@ export async function getProjectGHCVersion(

const args = ['--project-ghc-version'];

const newPath = await addPathToProcessPath(toolchainBindir, logger);
const newPath = await addPathToProcessPath(toolchainBindir);
const environmentNew: IEnvVars = {
PATH: newPath,
};
Expand Down Expand Up @@ -550,14 +557,14 @@ export async function upgradeGHCup(context: ExtensionContext, logger: Logger): P
}
}

export async function findGHCup(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): Promise<string> {
export async function findGHCup(_context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): Promise<string> {
logger.info('Checking for ghcup installation');
let exePath = workspace.getConfiguration('haskell').get('ghcupExecutablePath') as string;
if (exePath) {
logger.info(`Trying to find the ghcup executable in: ${exePath}`);
exePath = resolvePathPlaceHolders(exePath, folder);
logger.log(`Location after path variables substitution: ${exePath}`);
if (await executableExists(exePath)) {
if (executableExists(exePath)) {
return exePath;
} else {
throw new Error(`Could not find a ghcup binary at ${exePath}!`);
Expand Down Expand Up @@ -684,8 +691,8 @@ async function toolInstalled(
version: string
): Promise<InstalledTool> {
const b = await callGHCup(context, logger, ['whereis', tool, version], undefined, false)
.then((x) => true)
.catch((x) => false);
.then((_x) => true)
.catch((_x) => false);
return new InstalledTool(tool, version, b);
}

Expand Down Expand Up @@ -737,7 +744,7 @@ export type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;
*/
async function getHLSesfromMetadata(context: ExtensionContext, logger: Logger): Promise<Map<string, string[]> | null> {
const storagePath: string = await getStoragePath(context);
const metadata = await getReleaseMetadata(context, storagePath, logger).catch((e) => null);
const metadata = await getReleaseMetadata(context, storagePath, logger).catch((_e) => null);
if (!metadata) {
window.showErrorMessage('Could not get release metadata');
return null;
Expand Down Expand Up @@ -803,23 +810,23 @@ export function findSupportedHlsPerGhc(
/**
* Download GHCUP metadata.
*
* @param context Extension context.
* @param _context Extension context.
* @param storagePath Path to put in binary files and caches.
* @param logger Logger for feedback.
* @returns Metadata of releases, or null if the cache can not be found.
*/
async function getReleaseMetadata(
context: ExtensionContext,
_context: ExtensionContext,
storagePath: string,
logger: Logger
): Promise<ReleaseMetadata | null> {
const releasesUrl = workspace.getConfiguration('haskell').releasesURL
? url.parse(workspace.getConfiguration('haskell').releasesURL)
? new URL(workspace.getConfiguration('haskell').releasesURL)
: undefined;
const opts: https.RequestOptions = releasesUrl
? {
host: releasesUrl.host,
path: releasesUrl.path,
path: releasesUrl.pathname,
}
: {
host: 'raw.githubusercontent.com',
Expand Down
11 changes: 5 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as http from 'http';
import * as https from 'https';
import * as os from 'os';
import { extname } from 'path';
import * as url from 'url';
import { promisify } from 'util';
import { OutputChannel, ProgressLocation, window, workspace, WorkspaceFolder } from 'vscode';
import { Logger } from 'vscode-languageclient';
Expand Down Expand Up @@ -215,10 +214,10 @@ export async function downloadFile(titleMsg: string, src: string, dest: string):
},
async (progress) => {
const p = new Promise<void>((resolve, reject) => {
const srcUrl = url.parse(src);
const srcUrl = new URL(src);
const opts: https.RequestOptions = {
host: srcUrl.host,
path: srcUrl.path,
path: srcUrl.pathname,
protocol: srcUrl.protocol,
port: srcUrl.port,
headers: userAgentHeader,
Expand All @@ -230,9 +229,9 @@ export async function downloadFile(titleMsg: string, src: string, dest: string):

// Decompress it if it's a gzip or zip
const needsGunzip =
res.headers['content-type'] === 'application/gzip' || extname(srcUrl.path ?? '') === '.gz';
res.headers['content-type'] === 'application/gzip' || extname(srcUrl.pathname ?? '') === '.gz';
const needsUnzip =
res.headers['content-type'] === 'application/zip' || extname(srcUrl.path ?? '') === '.zip';
res.headers['content-type'] === 'application/zip' || extname(srcUrl.pathname ?? '') === '.zip';
if (needsGunzip) {
const gunzip = createGunzip();
gunzip.on('error', reject);
Expand Down Expand Up @@ -360,7 +359,7 @@ export function resolvePATHPlaceHolders(path: string) {
}

// also honours serverEnvironment.PATH
export async function addPathToProcessPath(extraPath: string, logger: Logger): Promise<string> {
export async function addPathToProcessPath(extraPath: string): Promise<string> {
const pathSep = process.platform === 'win32' ? ';' : ':';
const serverEnvironment: IEnvVars = (await workspace.getConfiguration('haskell').get('serverEnvironment')) || {};
const path: string[] = serverEnvironment.PATH
Expand Down