From 52c8d9e4fe22e37ada6c474a64103e793a0253bd Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 11:16:12 +0200 Subject: [PATCH 01/10] Error handling getting ghc version --- package.json | 11 ++++++ src/extension.ts | 45 +++++++++++++++--------- src/hlsBinaries.ts | 85 ++++++++++++++++++++++++++++++++-------------- src/utils.ts | 50 ++++++++++++++++++++++++++- 4 files changed, 148 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index dee56c39..8919baae 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,17 @@ "default": "off", "description": "Traces the communication between VS Code and the language server." }, + "haskell.trace.client": { + "scope": "resource", + "type": "string", + "enum": [ + "off", + "error", + "debug" + ], + "default": "error", + "description": "Traces the communication between VS Code and the language server." + }, "haskell.logFile": { "scope": "resource", "type": "string", diff --git a/src/extension.ts b/src/extension.ts index fee1b27b..9aa4d575 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ import { ExecutableOptions, LanguageClient, LanguageClientOptions, + Logger, RevealOutputChannelOn, ServerOptions, TransportKind, @@ -23,7 +24,7 @@ import { CommandNames } from './commands/constants'; import { ImportIdentifier } from './commands/importIdentifier'; import { DocsBrowser } from './docsBrowser'; import { downloadHaskellLanguageServer } from './hlsBinaries'; -import { executableExists } from './utils'; +import { executableExists, ExtensionLogger } from './utils'; // The current map of documents & folders to language servers. // It may be null to indicate that we are in the process of launching a server, @@ -45,7 +46,11 @@ export async function activate(context: ExtensionContext) { for (const folder of event.removed) { const client = clients.get(folder.uri.toString()); if (client) { - clients.delete(folder.uri.toString()); + const uri = folder.uri.toString(); + client.info('Deleting folder for clients: ${uri}'); + clients.delete(uri); + client.info; + client.info('Stopping the client'); client.stop(); } } @@ -54,10 +59,13 @@ export async function activate(context: ExtensionContext) { // Register editor commands for HIE, but only register the commands once at activation. const restartCmd = commands.registerCommand(CommandNames.RestartServerCommandName, async () => { for (const langClient of clients.values()) { + langClient?.info('Stopping the client'); await langClient?.stop(); + langClient?.info('Starting the client'); langClient?.start(); } }); + context.subscriptions.push(restartCmd); context.subscriptions.push(ImportIdentifier.registerCommand()); @@ -70,18 +78,18 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(openOnHackageDisposable); } -function findManualExecutable(uri: Uri, folder?: WorkspaceFolder): string | null { +function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null { let exePath = workspace.getConfiguration('haskell', uri).serverExecutablePath; if (exePath === '') { return null; } - + logger.info('Trying to find the server executable in: ${exePath}'); // Substitute path variables with their corresponding locations. exePath = exePath.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir); if (folder) { exePath = exePath.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path); } - + logger.info('Location after path variables subsitution: ${exePath}'); if (!executableExists(exePath)) { throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and is not on the PATH`); } @@ -89,11 +97,12 @@ function findManualExecutable(uri: Uri, folder?: WorkspaceFolder): string | null } /** Searches the PATH for whatever is set in serverVariant */ -function findLocalServer(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder): string | null { +function findLocalServer(context: ExtensionContext, logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null { const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server']; - + logger.info('Searching for server executables ${exes} in PATH'); for (const exe of exes) { if (executableExists(exe)) { + logger.info('Found server executable in PATH: ${exe}'); return exe; } } @@ -120,6 +129,9 @@ async function activeServer(context: ExtensionContext, document: TextDocument) { async function activateServerForFolder(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder) { const clientsKey = folder ? folder.uri.toString() : uri.toString(); + // Set a unique name per workspace folder (useful for multi-root workspaces). + const langName = 'Haskell' + (folder ? ` (${folder.name})` : ''); + const outputChannel: OutputChannel = window.createOutputChannel(langName); // If the client already has an LSP server for this uri/folder, then don't start a new one. if (clients.has(clientsKey)) { @@ -129,21 +141,25 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold clients.set(clientsKey, null); const logLevel = workspace.getConfiguration('haskell', uri).trace.server; + const clientLogLevel = workspace.getConfiguration('haskell', uri).trace.client; const logFile = workspace.getConfiguration('haskell', uri).logFile; + const logger: Logger = new ExtensionLogger('client', clientLogLevel, outputChannel); + let serverExecutable; try { // Try and find local installations first - serverExecutable = findManualExecutable(uri, folder) ?? findLocalServer(context, uri, folder); + serverExecutable = findManualExecutable(logger, uri, folder) ?? findLocalServer(context, logger, uri, folder); if (serverExecutable === null) { // If not, then try to download haskell-language-server binaries if it's selected - serverExecutable = await downloadHaskellLanguageServer(context, uri, folder); + serverExecutable = await downloadHaskellLanguageServer(context, logger, uri, folder); if (!serverExecutable) { return; } } } catch (e) { if (e instanceof Error) { + logger.error('Error getting the server executable: ${e.message}'); window.showErrorMessage(e.message); } return; @@ -173,13 +189,9 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold debug: { command: serverExecutable, transport: TransportKind.stdio, args, options: exeOptions }, }; - // Set a unique name per workspace folder (useful for multi-root workspaces). - const langName = 'Haskell' + (folder ? ` (${folder.name})` : ''); - const outputChannel: OutputChannel = window.createOutputChannel(langName); - outputChannel.appendLine('[client] run command: "' + serverExecutable + ' ' + args.join(' ') + '"'); - outputChannel.appendLine('[client] debug command: "' + serverExecutable + ' ' + args.join(' ') + '"'); - - outputChannel.appendLine(`[client] server cwd: ${exeOptions.cwd}`); + logger.info('run command: "' + serverExecutable + ' ' + args.join(' ') + '"'); + logger.info('debug command: "' + serverExecutable + ' ' + args.join(' ') + '"'); + logger.info(`server cwd: ${exeOptions.cwd}`); const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*'; const clientOptions: LanguageClientOptions = { @@ -213,6 +225,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold langClient.registerProposedFeatures(); // Finally start the client and add it to the list of clients. + logger.info('Starting language client'); langClient.start(); clients.set(clientsKey, langClient); } diff --git a/src/hlsBinaries.ts b/src/hlsBinaries.ts index 150fa4de..8fae7350 100644 --- a/src/hlsBinaries.ts +++ b/src/hlsBinaries.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import * as url from 'url'; import { promisify } from 'util'; import { env, ExtensionContext, ProgressLocation, Uri, window, workspace, WorkspaceFolder } from 'vscode'; +import { Logger } from 'vscode-languageclient'; import { downloadFile, executableExists, httpsGetSilently } from './utils'; import * as validate from './validation'; @@ -97,33 +98,59 @@ class NoBinariesError extends Error { * if needed. Returns null if there was an error in either downloading the wrapper or * in working out the ghc version */ -async function getProjectGhcVersion(context: ExtensionContext, dir: string, release: IRelease): Promise { +async function getProjectGhcVersion( + context: ExtensionContext, + logger: Logger, + dir: string, + release: IRelease +): Promise { + const title: string = 'Working out the project GHC version. This might take a while...'; + logger.info(title); const callWrapper = (wrapper: string) => { return window.withProgress( { - location: ProgressLocation.Window, - title: 'Working out the project GHC version. This might take a while...', + location: ProgressLocation.Notification, + title: title, + cancellable: true, }, - async () => { + async (progress, token) => { return new Promise((resolve, reject) => { + const command: string = wrapper + ' --project-ghc-version'; + logger.info('Executing `${command}` in cwd ${dir} to get the project ghc version'); + token.onCancellationRequested(() => { + logger.warn('User canceled the executon of `${command}`'); + }); // Need to set the encoding to 'utf8' in order to get back a string - child_process.exec( - wrapper + ' --project-ghc-version', - { encoding: 'utf8', cwd: dir }, - (err, stdout, stderr) => { - if (err) { - const regex = /Cradle requires (.+) but couldn't find it/; - const res = regex.exec(stderr); - if (res) { - throw new MissingToolError(res[1]); + // We execute the command in a shell for windows, to allow use cmd or bat scripts + let childProcess = child_process + .execFile( + command, + { encoding: 'utf8', cwd: dir, shell: getGithubOS() == 'Windows' }, + (err, stdout, stderr) => { + if (err) { + logger.error('Error executing `${command}` with error code ${err.code}'); + logger.error('stderr: ${stderr}'); + logger.error('stdout: ${stdout}'); + const regex = /Cradle requires (.+) but couldn't find it/; + const res = regex.exec(stderr); + if (res) { + throw new MissingToolError(res[1]); + } + throw Error( + `${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}` + ); } - throw Error( - `${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}` - ); + resolve(stdout.trim()); } - resolve(stdout.trim()); - } - ); + ) + .on('close', (code, signal) => { + logger.info('Execution of `${command}` closed with code ${err.code} and signal ${signal}'); + }) + .on('error', (err) => { + logger.error('Error execution `${command}`: name = ${err.name}, message = ${err.message}'); + throw err; + }); + token.onCancellationRequested((_) => childProcess.kill()); }); } ); @@ -250,10 +277,12 @@ async function getLatestReleaseMetadata(context: ExtensionContext): Promise { // Make sure to create this before getProjectGhcVersion + logger.info('Downloading haskell-language-server'); if (!fs.existsSync(context.globalStoragePath)) { fs.mkdirSync(context.globalStoragePath); } @@ -265,7 +294,7 @@ export async function downloadHaskellLanguageServer( return null; } - // Fetch the latest release from GitHub or from cache + logger.info('Fetching the latest release from GitHub or from cache'); const release = await getLatestReleaseMetadata(context); if (!release) { let message = "Couldn't find any pre-built haskell-language-server binaries"; @@ -276,12 +305,12 @@ export async function downloadHaskellLanguageServer( window.showErrorMessage(message); return null; } - - // Figure out the ghc version to use or advertise an installation link for missing components + logger.info('The latest release is ${release.tag_name}'); + logger.info('Figure out the ghc version to use or advertise an installation link for missing components'); const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath); let ghcVersion: string; try { - ghcVersion = await getProjectGhcVersion(context, dir, release); + ghcVersion = await getProjectGhcVersion(context, logger, dir, release); } catch (error) { if (error instanceof MissingToolError) { const link = error.installLink(); @@ -304,8 +333,10 @@ export async function downloadHaskellLanguageServer( // When searching for binaries, use startsWith because the compression may differ // between .zip and .gz const assetName = `haskell-language-server-${githubOS}-${ghcVersion}${exeExt}`; + logger.info('Search for binary ${assetName} in release assests'); const asset = release?.assets.find((x) => x.name.startsWith(assetName)); if (!asset) { + logger.error('No binary ${assetName} found in the release assets: ' + release?.assets.map((value) => value.name)); window.showInformationMessage(new NoBinariesError(release.tag_name, ghcVersion).message); return null; } @@ -314,12 +345,14 @@ export async function downloadHaskellLanguageServer( const binaryDest = path.join(context.globalStoragePath, serverName); const title = `Downloading haskell-language-server ${release.tag_name} for GHC ${ghcVersion}`; + logger.info(title); await downloadFile(title, asset.browser_download_url, binaryDest); if (ghcVersion.startsWith('9.')) { - window.showWarningMessage( + let warning = 'Currently, HLS supports GHC 9 only partially. ' + - 'See [issue #297](https://github.com/haskell/haskell-language-server/issues/297) for more detail.' - ); + 'See [issue #297](https://github.com/haskell/haskell-language-server/issues/297) for more detail.'; + logger.warn(warning); + window.showWarningMessage(warning); } return binaryDest; } diff --git a/src/utils.ts b/src/utils.ts index 8d141ce2..e9a2b8d1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,10 +7,58 @@ import * as https from 'https'; import { extname } from 'path'; import * as url from 'url'; import { promisify } from 'util'; -import { ProgressLocation, window } from 'vscode'; +import { OutputChannel, ProgressLocation, window } from 'vscode'; +import { Logger } from 'vscode-languageclient'; import * as yazul from 'yauzl'; import { createGunzip } from 'zlib'; +enum LogLevel { + Off, + Error, + Warn, + Info, +} +export class ExtensionLogger implements Logger { + public readonly name: string; + public readonly level: LogLevel; + public readonly channel: OutputChannel; + + constructor(name: string, level: string, channel: OutputChannel) { + this.name = name; + this.level = this.getLogLevel(level); + this.channel = channel; + } + warn(message: string): void { + this.logLevel(LogLevel.Warn, message); + } + info(message: string): void { + this.logLevel(LogLevel.Info, message); + } + + error(message: string) { + this.logLevel(LogLevel.Error, message); + } + + log(msg: string) { + this.channel.appendLine(msg); + } + + private logLevel(level: LogLevel, msg: string) { + if (level <= this.level) this.log('[${name}][${level}] ${msg}'); + } + + private getLogLevel(level: string) { + switch (level) { + case 'off': + return LogLevel.Off; + case 'error': + return LogLevel.Error; + default: + return LogLevel.Info; + } + } +} + /** When making http requests to github.com, use this header otherwise * the server will close the request */ From 38bbf98a4475d011464a356a0c8b515ed9fbe023 Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 11:19:46 +0200 Subject: [PATCH 02/10] Add start and stop lsp server --- package.json | 10 ++++++++++ src/commands/constants.ts | 2 ++ src/extension.ts | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/package.json b/package.json index 8919baae..dd267905 100644 --- a/package.json +++ b/package.json @@ -323,6 +323,16 @@ "command": "haskell.commands.restartServer", "title": "Haskell: Restart Haskell LSP server", "description": "Restart the Haskell LSP server" + }, + { + "command": "haskell.commands.startServer", + "title": "Haskell: Start Haskell LSP server", + "description": "Start the Haskell LSP server" + }, + { + "command": "haskell.commands.stopServer", + "title": "Haskell: Stop Haskell LSP server", + "description": "Stop the Haskell LSP server" } ] }, diff --git a/src/commands/constants.ts b/src/commands/constants.ts index e91017a6..0d2e3e9a 100644 --- a/src/commands/constants.ts +++ b/src/commands/constants.ts @@ -1,4 +1,6 @@ export namespace CommandNames { export const ImportIdentifierCommandName = 'haskell.commands.importIdentifier'; export const RestartServerCommandName = 'haskell.commands.restartServer'; + export const StartServerCommandName = 'haskell.commands.startServer'; + export const StopServerCommandName = 'haskell.commands.stopServer'; } diff --git a/src/extension.ts b/src/extension.ts index 9aa4d575..4333ad88 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -68,6 +68,26 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(restartCmd); + const stopCmd = commands.registerCommand(CommandNames.StopServerCommandName, async () => { + for (const langClient of clients.values()) { + langClient?.info('Stopping the client'); + await langClient?.stop(); + langClient?.info('Client stopped'); + } + }); + + context.subscriptions.push(stopCmd); + + const startCmd = commands.registerCommand(CommandNames.StartServerCommandName, async () => { + for (const langClient of clients.values()) { + langClient?.info('Starting the client'); + langClient?.start(); + langClient?.info('Client started'); + } + }); + + context.subscriptions.push(startCmd); + context.subscriptions.push(ImportIdentifier.registerCommand()); // Set up the documentation browser. From b799ead8bfbe740465595db1eb240ccbd11535dc Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 11:53:11 +0200 Subject: [PATCH 03/10] Several corrections --- package-lock.json | 16 +++------------- src/extension.ts | 17 ++++++++--------- src/hlsBinaries.ts | 32 +++++++++++++++++--------------- src/utils.ts | 13 ++++++++----- 4 files changed, 36 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92042865..6e3e5044 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "haskell", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3235,8 +3235,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "optional": true + "dev": true }, "npm-run-path": { "version": "2.0.2", @@ -5069,7 +5068,6 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, - "optional": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -5088,7 +5086,6 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, - "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -5121,7 +5118,6 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, - "optional": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -5134,7 +5130,6 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, - "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -5186,7 +5181,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -5196,7 +5190,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, - "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -5208,7 +5201,6 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, - "optional": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -5257,8 +5249,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "optional": true + "dev": true }, "string_decoder": { "version": "1.1.1", @@ -5275,7 +5266,6 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, - "optional": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" diff --git a/src/extension.ts b/src/extension.ts index 4333ad88..16ad4e01 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -47,9 +47,8 @@ export async function activate(context: ExtensionContext) { const client = clients.get(folder.uri.toString()); if (client) { const uri = folder.uri.toString(); - client.info('Deleting folder for clients: ${uri}'); + client.info(`Deleting folder for clients: ${uri}`); clients.delete(uri); - client.info; client.info('Stopping the client'); client.stop(); } @@ -103,15 +102,15 @@ function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder if (exePath === '') { return null; } - logger.info('Trying to find the server executable in: ${exePath}'); + logger.info(`Trying to find the server executable in: ${exePath}`); // Substitute path variables with their corresponding locations. exePath = exePath.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir); if (folder) { exePath = exePath.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path); } - logger.info('Location after path variables subsitution: ${exePath}'); + logger.info(`Location after path variables subsitution: ${exePath}`); if (!executableExists(exePath)) { - throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and is not on the PATH`); + throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and it is not on the PATH`); } return exePath; } @@ -119,10 +118,10 @@ function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder /** Searches the PATH for whatever is set in serverVariant */ function findLocalServer(context: ExtensionContext, logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null { const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server']; - logger.info('Searching for server executables ${exes} in PATH'); + logger.info(`Searching for server executables ${exes.join(' ')} in PATH`); for (const exe of exes) { if (executableExists(exe)) { - logger.info('Found server executable in PATH: ${exe}'); + logger.info(`Found server executable in PATH: ${exe}`); return exe; } } @@ -209,8 +208,8 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold debug: { command: serverExecutable, transport: TransportKind.stdio, args, options: exeOptions }, }; - logger.info('run command: "' + serverExecutable + ' ' + args.join(' ') + '"'); - logger.info('debug command: "' + serverExecutable + ' ' + args.join(' ') + '"'); + logger.info(`run command: ${serverExecutable} ${args.join(' ')}`); + logger.info(`debug command: ${serverExecutable} ${args.join(' ')}`); logger.info(`server cwd: ${exeOptions.cwd}`); const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*'; diff --git a/src/hlsBinaries.ts b/src/hlsBinaries.ts index 8fae7350..ebbb6a87 100644 --- a/src/hlsBinaries.ts +++ b/src/hlsBinaries.ts @@ -110,27 +110,27 @@ async function getProjectGhcVersion( return window.withProgress( { location: ProgressLocation.Notification, - title: title, + title: `${title}`, cancellable: true, }, async (progress, token) => { return new Promise((resolve, reject) => { const command: string = wrapper + ' --project-ghc-version'; - logger.info('Executing `${command}` in cwd ${dir} to get the project ghc version'); + logger.info(`Executing '${command}' in cwd ${dir} to get the project ghc version`); token.onCancellationRequested(() => { - logger.warn('User canceled the executon of `${command}`'); + logger.warn(`User canceled the executon of '${command}'`); }); // Need to set the encoding to 'utf8' in order to get back a string // We execute the command in a shell for windows, to allow use cmd or bat scripts - let childProcess = child_process + const childProcess = child_process .execFile( command, - { encoding: 'utf8', cwd: dir, shell: getGithubOS() == 'Windows' }, + { encoding: 'utf8', cwd: dir, shell: getGithubOS() === 'Windows' }, (err, stdout, stderr) => { if (err) { - logger.error('Error executing `${command}` with error code ${err.code}'); - logger.error('stderr: ${stderr}'); - logger.error('stdout: ${stdout}'); + logger.error(`Error executing '${command}' with error code ${err.code}`); + logger.error(`stderr: ${stderr}`); + logger.error(`stdout: ${stdout}`); const regex = /Cradle requires (.+) but couldn't find it/; const res = regex.exec(stderr); if (res) { @@ -144,10 +144,10 @@ async function getProjectGhcVersion( } ) .on('close', (code, signal) => { - logger.info('Execution of `${command}` closed with code ${err.code} and signal ${signal}'); + logger.info(`Execution of '${command}' closed with code ${code} and signal ${signal}`); }) .on('error', (err) => { - logger.error('Error execution `${command}`: name = ${err.name}, message = ${err.message}'); + logger.error(`Error executing '${command}': name = ${err.name}, message = ${err.message}`); throw err; }); token.onCancellationRequested((_) => childProcess.kill()); @@ -305,8 +305,8 @@ export async function downloadHaskellLanguageServer( window.showErrorMessage(message); return null; } - logger.info('The latest release is ${release.tag_name}'); - logger.info('Figure out the ghc version to use or advertise an installation link for missing components'); + logger.info(`The latest release is ${release.tag_name}`); + logger.info(`Figure out the ghc version to use or advertise an installation link for missing components`); const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath); let ghcVersion: string; try { @@ -333,10 +333,12 @@ export async function downloadHaskellLanguageServer( // When searching for binaries, use startsWith because the compression may differ // between .zip and .gz const assetName = `haskell-language-server-${githubOS}-${ghcVersion}${exeExt}`; - logger.info('Search for binary ${assetName} in release assests'); + logger.info(`Search for binary ${assetName} in release assests`); const asset = release?.assets.find((x) => x.name.startsWith(assetName)); if (!asset) { - logger.error('No binary ${assetName} found in the release assets: ' + release?.assets.map((value) => value.name)); + logger.error( + `No binary ${assetName} found in the release assets: ${release?.assets.map((value) => value.name).join(',')}` + ); window.showInformationMessage(new NoBinariesError(release.tag_name, ghcVersion).message); return null; } @@ -348,7 +350,7 @@ export async function downloadHaskellLanguageServer( logger.info(title); await downloadFile(title, asset.browser_download_url, binaryDest); if (ghcVersion.startsWith('9.')) { - let warning = + const warning = 'Currently, HLS supports GHC 9 only partially. ' + 'See [issue #297](https://github.com/haskell/haskell-language-server/issues/297) for more detail.'; logger.warn(warning); diff --git a/src/utils.ts b/src/utils.ts index e9a2b8d1..856b03df 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,23 +28,26 @@ export class ExtensionLogger implements Logger { this.level = this.getLogLevel(level); this.channel = channel; } - warn(message: string): void { + public warn(message: string): void { this.logLevel(LogLevel.Warn, message); } - info(message: string): void { + + public info(message: string): void { this.logLevel(LogLevel.Info, message); } - error(message: string) { + public error(message: string) { this.logLevel(LogLevel.Error, message); } - log(msg: string) { + public log(msg: string) { this.channel.appendLine(msg); } private logLevel(level: LogLevel, msg: string) { - if (level <= this.level) this.log('[${name}][${level}] ${msg}'); + if (level <= this.level) { + this.log(`[${this.name}][${level}] ${msg}`); + } } private getLogLevel(level: string) { From 12b3a71f0944c842c48428be8c8d2fb3b58489ef Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 13:39:05 +0200 Subject: [PATCH 04/10] Show log level in upper case --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 856b03df..5c8a9b9f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -46,7 +46,7 @@ export class ExtensionLogger implements Logger { private logLevel(level: LogLevel, msg: string) { if (level <= this.level) { - this.log(`[${this.name}][${level}] ${msg}`); + this.log(`[${this.name}][${LogLevel[level].toUpperCase()}] ${msg}`); } } From 8078c342c49f99874efe2b772aea7cbeaff6a8c9 Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 13:39:49 +0200 Subject: [PATCH 05/10] Improve logging --- src/extension.ts | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 16ad4e01..90e4bc4b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -49,7 +49,7 @@ export async function activate(context: ExtensionContext) { const uri = folder.uri.toString(); client.info(`Deleting folder for clients: ${uri}`); clients.delete(uri); - client.info('Stopping the client'); + client.info('Stopping the server'); client.stop(); } } @@ -58,9 +58,9 @@ export async function activate(context: ExtensionContext) { // Register editor commands for HIE, but only register the commands once at activation. const restartCmd = commands.registerCommand(CommandNames.RestartServerCommandName, async () => { for (const langClient of clients.values()) { - langClient?.info('Stopping the client'); + langClient?.info('Stopping the server'); await langClient?.stop(); - langClient?.info('Starting the client'); + langClient?.info('Starting the server'); langClient?.start(); } }); @@ -69,9 +69,9 @@ export async function activate(context: ExtensionContext) { const stopCmd = commands.registerCommand(CommandNames.StopServerCommandName, async () => { for (const langClient of clients.values()) { - langClient?.info('Stopping the client'); + langClient?.info('Stopping the server'); await langClient?.stop(); - langClient?.info('Client stopped'); + langClient?.info('Server stopped'); } }); @@ -79,9 +79,9 @@ export async function activate(context: ExtensionContext) { const startCmd = commands.registerCommand(CommandNames.StartServerCommandName, async () => { for (const langClient of clients.values()) { - langClient?.info('Starting the client'); + langClient?.info('Starting the server'); langClient?.start(); - langClient?.info('Client started'); + langClient?.info('Server started'); } }); @@ -118,10 +118,10 @@ function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder /** Searches the PATH for whatever is set in serverVariant */ function findLocalServer(context: ExtensionContext, logger: Logger, uri: Uri, folder?: WorkspaceFolder): string | null { const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server']; - logger.info(`Searching for server executables ${exes.join(' ')} in PATH`); + logger.info(`Searching for server executables ${exes.join(',')} in $PATH`); for (const exe of exes) { if (executableExists(exe)) { - logger.info(`Found server executable in PATH: ${exe}`); + logger.info(`Found server executable in $PATH: ${exe}`); return exe; } } @@ -178,7 +178,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold } } catch (e) { if (e instanceof Error) { - logger.error('Error getting the server executable: ${e.message}'); + logger.error(`Error getting the server executable: ${e.message}`); window.showErrorMessage(e.message); } return; @@ -197,6 +197,12 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold // If we're operating on a standalone file (i.e. not in a folder) then we need // to launch the server in a reasonable current directory. Otherwise the cradle // guessing logic in hie-bios will be wrong! + if (folder) { + logger.info(`Activating the language server in the workspace folder: ${folder?.uri.fsPath}`); + } else { + logger.info(`Activating the language server in the parent dir of the file: ${uri.fsPath}`); + } + const exeOptions: ExecutableOptions = { cwd: folder ? undefined : path.dirname(uri.fsPath), }; @@ -210,9 +216,12 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold logger.info(`run command: ${serverExecutable} ${args.join(' ')}`); logger.info(`debug command: ${serverExecutable} ${args.join(' ')}`); - logger.info(`server cwd: ${exeOptions.cwd}`); + if (exeOptions.cwd) { + logger.info(`server cwd: ${exeOptions.cwd}`); + } const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*'; + logger.info(`document selector patten: ${pat}`); const clientOptions: LanguageClientOptions = { // Use the document selector to only notify the LSP on files inside the folder // path for the specific workspace. @@ -244,7 +253,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold langClient.registerProposedFeatures(); // Finally start the client and add it to the list of clients. - logger.info('Starting language client'); + logger.info('Starting language server'); langClient.start(); clients.set(clientsKey, langClient); } From 7631a51c86fe48487bc3f3bb2efbf76b102366ef Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 13:41:05 +0200 Subject: [PATCH 06/10] Use reject instead throw exception --- src/hlsBinaries.ts | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/hlsBinaries.ts b/src/hlsBinaries.ts index ebbb6a87..45cfa038 100644 --- a/src/hlsBinaries.ts +++ b/src/hlsBinaries.ts @@ -116,12 +116,12 @@ async function getProjectGhcVersion( async (progress, token) => { return new Promise((resolve, reject) => { const command: string = wrapper + ' --project-ghc-version'; - logger.info(`Executing '${command}' in cwd ${dir} to get the project ghc version`); + logger.info(`Executing '${command}' in cwd '${dir}' to get the project or file ghc version`); token.onCancellationRequested(() => { - logger.warn(`User canceled the executon of '${command}'`); + logger.warn(`User canceled the execution of '${command}'`); }); // Need to set the encoding to 'utf8' in order to get back a string - // We execute the command in a shell for windows, to allow use cmd or bat scripts + // We execute the command in a shell for windows, to allow use .cmd or .bat scripts const childProcess = child_process .execFile( command, @@ -130,25 +130,33 @@ async function getProjectGhcVersion( if (err) { logger.error(`Error executing '${command}' with error code ${err.code}`); logger.error(`stderr: ${stderr}`); - logger.error(`stdout: ${stdout}`); + if (stdout) { + logger.error(`stdout: ${stdout}`); + } const regex = /Cradle requires (.+) but couldn't find it/; const res = regex.exec(stderr); if (res) { - throw new MissingToolError(res[1]); + reject(new MissingToolError(res[1])); } - throw Error( - `${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}` + reject( + Error(`${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`) ); + } else { + logger.info(`The GHC version for the project or file: ${stdout?.trim()}`); + resolve(stdout?.trim()); } - resolve(stdout.trim()); } ) - .on('close', (code, signal) => { - logger.info(`Execution of '${command}' closed with code ${code} and signal ${signal}`); + .on('exit', (code, signal) => { + const msg = + `Execution of '${command}' terminated with code ${code}` + (signal ? `and signal ${signal}` : ''); + logger.info(msg); }) .on('error', (err) => { - logger.error(`Error executing '${command}': name = ${err.name}, message = ${err.message}`); - throw err; + if (err) { + logger.error(`Error executing '${command}': name = ${err.name}, message = ${err.message}`); + reject(err); + } }); token.onCancellationRequested((_) => childProcess.kill()); }); @@ -306,7 +314,7 @@ export async function downloadHaskellLanguageServer( return null; } logger.info(`The latest release is ${release.tag_name}`); - logger.info(`Figure out the ghc version to use or advertise an installation link for missing components`); + logger.info('Figure out the ghc version to use or advertise an installation link for missing components'); const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath); let ghcVersion: string; try { From 1abcf514e9ef360e5e11b01f44fc1b835f0bae2c Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 13:48:45 +0200 Subject: [PATCH 07/10] Update Changelog with 1.5.1 --- Changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Changelog.md b/Changelog.md index 63a6271f..8e297b0a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,13 @@ - Add tracking of cabal files to work together with the incoming cabal formatter plugin +### 1.5.1 + +- Add much more logging in the client side, configured with `haskell.trace.client` +- Fix error handling of `working out project ghc` (See #421) + - And dont use a shell to spawn the subprocess in non windows systems +- Add commands `Start Haskell LSP server` and `Stop Haskell LSP server` + ### 1.5.0 - Emit warning about limited support for ghc-9.x on hls executable download From 737f2bc2e3c082855788b2a7c0af861065ab1608 Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 13:54:16 +0200 Subject: [PATCH 08/10] Complete Changelog for 1.5.1 --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index 8e297b0a..42e911e1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ - Add much more logging in the client side, configured with `haskell.trace.client` - Fix error handling of `working out project ghc` (See #421) - And dont use a shell to spawn the subprocess in non windows systems + - Show the progress as a cancellable notification - Add commands `Start Haskell LSP server` and `Stop Haskell LSP server` ### 1.5.0 From e799472f35cd8f8ed3f62924427e6cdef6735e75 Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 14:00:41 +0200 Subject: [PATCH 09/10] Bump up version to 1.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dd267905..2a739ecc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "haskell", "displayName": "Haskell", "description": "Haskell language support powered by the Haskell Language Server", - "version": "1.5.0", + "version": "1.5.1", "license": "MIT", "publisher": "haskell", "engines": { From 61d8e3f9229417050bd04e5c6c4df96e27aea0f6 Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 4 Aug 2021 14:25:47 +0200 Subject: [PATCH 10/10] Separate args to avoid spaces issues --- package-lock.json | 2 +- src/hlsBinaries.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e3e5044..9ba2c18b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "haskell", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/hlsBinaries.ts b/src/hlsBinaries.ts index 45cfa038..3e0fbedf 100644 --- a/src/hlsBinaries.ts +++ b/src/hlsBinaries.ts @@ -115,7 +115,8 @@ async function getProjectGhcVersion( }, async (progress, token) => { return new Promise((resolve, reject) => { - const command: string = wrapper + ' --project-ghc-version'; + const args = ['--project-ghc-version']; + const command: string = wrapper + args.join(' '); logger.info(`Executing '${command}' in cwd '${dir}' to get the project or file ghc version`); token.onCancellationRequested(() => { logger.warn(`User canceled the execution of '${command}'`); @@ -124,7 +125,8 @@ async function getProjectGhcVersion( // We execute the command in a shell for windows, to allow use .cmd or .bat scripts const childProcess = child_process .execFile( - command, + wrapper, + args, { encoding: 'utf8', cwd: dir, shell: getGithubOS() === 'Windows' }, (err, stdout, stderr) => { if (err) {