diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e8edf075..727c8bb1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
- ghc: [8.10.7, 9.4.8, 9.6.4, 9.8.2]
+ ghc: [8.10.7, 9.6.7, 9.8.4, 9.12.2]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 32aa338c..5c88b3b4 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,14 +1,31 @@
import globals from 'globals';
-import pluginJs from '@eslint/js';
+import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
-export default [
+export default tseslint.config(
{ files: ['**/*.{js,mjs,cjs,ts}'] },
- { languageOptions: { globals: globals.node } },
{
- ...pluginJs.configs.recommended,
+ languageOptions: {
+ globals: globals.node,
+ parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
+ },
+ },
+ {
+ // disables type checking for this file only
+ files: ['eslint.config.mjs'],
+ extends: [tseslint.configs.disableTypeChecked],
+ },
+ eslint.configs.recommended,
+ tseslint.configs.recommendedTypeChecked,
+ {
rules: {
+ // turn off these lints as we access workspaceConfiguration fields.
+ // So far, there was no bug found with these unsafe accesses.
+ '@typescript-eslint/no-unsafe-assignment': 'off',
+ '@typescript-eslint/no-unsafe-member-access': 'off',
+ // Sometimes, the 'any' just saves too much time.
'@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
@@ -23,5 +40,4 @@ export default [
],
},
},
- ...tseslint.configs.recommended,
-];
+);
diff --git a/package.json b/package.json
index 96fad689..a286f987 100644
--- a/package.json
+++ b/package.json
@@ -1310,9 +1310,9 @@
},
"commands": [
{
- "command": "haskell.commands.importIdentifier",
- "title": "Haskell: Import identifier",
- "description": "Imports a function or type based on a Hoogle search"
+ "command": "haskell.commands.restartExtension",
+ "title": "Haskell: Restart vscode-haskell extension",
+ "description": "Restart the vscode-haskell extension. Reloads configuration."
},
{
"command": "haskell.commands.restartServer",
diff --git a/src/commands/constants.ts b/src/commands/constants.ts
index 660ec8a3..55806a19 100644
--- a/src/commands/constants.ts
+++ b/src/commands/constants.ts
@@ -1,4 +1,6 @@
-export const ImportIdentifierCommandName = 'haskell.commands.importIdentifier';
+export const RestartExtensionCommandName = 'haskell.commands.restartExtension';
export const RestartServerCommandName = 'haskell.commands.restartServer';
export const StartServerCommandName = 'haskell.commands.startServer';
export const StopServerCommandName = 'haskell.commands.stopServer';
+export const OpenLogsCommandName = 'haskell.commands.openLogs';
+export const ShowExtensionVersions = 'haskell.commands.showVersions';
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 00000000..914a51fe
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,141 @@
+import { OutputChannel, Uri, window, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
+import { expandHomeDir, IEnvVars } from './utils';
+import * as path from 'path';
+import { Logger } from 'vscode-languageclient';
+import { ExtensionLogger } from './logger';
+import { GHCupConfig } from './ghcup';
+
+export type LogLevel = 'off' | 'messages' | 'verbose';
+export type ClientLogLevel = 'off' | 'error' | 'info' | 'debug';
+
+export type Config = {
+ /**
+ * Unique name per workspace folder (useful for multi-root workspaces).
+ */
+ langName: string;
+ logLevel: LogLevel;
+ clientLogLevel: ClientLogLevel;
+ logFilePath?: string;
+ workingDir: string;
+ outputChannel: OutputChannel;
+ serverArgs: string[];
+ serverEnvironment: IEnvVars;
+ ghcupConfig: GHCupConfig;
+};
+
+export function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, folder?: WorkspaceFolder): Config {
+ // Set a unique name per workspace folder (useful for multi-root workspaces).
+ const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
+ const currentWorkingDir = folder ? folder.uri.fsPath : path.dirname(uri.fsPath);
+
+ const logLevel = getLogLevel(workspaceConfig);
+ const clientLogLevel = getClientLogLevel(workspaceConfig);
+
+ const logFile = getLogFile(workspaceConfig);
+ const logFilePath = resolveLogFilePath(logFile, currentWorkingDir);
+
+ const outputChannel: OutputChannel = window.createOutputChannel(langName);
+ const serverArgs = getServerArgs(workspaceConfig, logLevel, logFilePath);
+
+ return {
+ langName: langName,
+ logLevel: logLevel,
+ clientLogLevel: clientLogLevel,
+ logFilePath: logFilePath,
+ workingDir: currentWorkingDir,
+ outputChannel: outputChannel,
+ serverArgs: serverArgs,
+ serverEnvironment: workspaceConfig.serverEnvironment,
+ ghcupConfig: {
+ metadataUrl: workspaceConfig.metadataURL as string,
+ upgradeGHCup: workspaceConfig.get('upgradeGHCup') as boolean,
+ executablePath: workspaceConfig.get('ghcupExecutablePath') as string,
+ },
+ };
+}
+
+export function initLoggerFromConfig(config: Config): ExtensionLogger {
+ return new ExtensionLogger('client', config.clientLogLevel, config.outputChannel, config.logFilePath);
+}
+
+export function logConfig(logger: Logger, config: Config) {
+ if (config.logFilePath) {
+ logger.info(`Writing client log to file ${config.logFilePath}`);
+ }
+ logger.log('Environment variables:');
+ Object.entries(process.env).forEach(([key, value]: [string, string | undefined]) => {
+ // only list environment variables that we actually care about.
+ // this makes it safe for users to just paste the logs to whoever,
+ // and avoids leaking secrets.
+ if (['PATH'].includes(key)) {
+ logger.log(` ${key}: ${value}`);
+ }
+ });
+}
+
+function getLogFile(workspaceConfig: WorkspaceConfiguration) {
+ const logFile_: unknown = workspaceConfig.logFile;
+ let logFile: string | undefined;
+ if (typeof logFile_ === 'string') {
+ logFile = logFile_ !== '' ? logFile_ : undefined;
+ }
+ return logFile;
+}
+
+function getClientLogLevel(workspaceConfig: WorkspaceConfiguration): ClientLogLevel {
+ const clientLogLevel_: unknown = workspaceConfig.trace.client;
+ let clientLogLevel;
+ if (typeof clientLogLevel_ === 'string') {
+ switch (clientLogLevel_) {
+ case 'off':
+ case 'error':
+ case 'info':
+ case 'debug':
+ clientLogLevel = clientLogLevel_;
+ break;
+ default:
+ throw new Error("Option \"haskell.trace.client\" is expected to be one of 'off', 'error', 'info', 'debug'.");
+ }
+ } else {
+ throw new Error('Option "haskell.trace.client" is expected to be a string');
+ }
+ return clientLogLevel;
+}
+
+function getLogLevel(workspaceConfig: WorkspaceConfiguration): LogLevel {
+ const logLevel_: unknown = workspaceConfig.trace.server;
+ let logLevel;
+ if (typeof logLevel_ === 'string') {
+ switch (logLevel_) {
+ case 'off':
+ case 'messages':
+ case 'verbose':
+ logLevel = logLevel_;
+ break;
+ default:
+ throw new Error("Option \"haskell.trace.server\" is expected to be one of 'off', 'messages', 'verbose'.");
+ }
+ } else {
+ throw new Error('Option "haskell.trace.server" is expected to be a string');
+ }
+ return logLevel;
+}
+
+function resolveLogFilePath(logFile: string | undefined, currentWorkingDir: string): string | undefined {
+ return logFile !== undefined ? path.resolve(currentWorkingDir, expandHomeDir(logFile)) : undefined;
+}
+
+function getServerArgs(workspaceConfig: WorkspaceConfiguration, logLevel: LogLevel, logFilePath?: string): string[] {
+ const serverArgs = ['--lsp']
+ .concat(logLevel === 'messages' ? ['-d'] : [])
+ .concat(logFilePath !== undefined ? ['-l', logFilePath] : []);
+
+ const rawExtraArgs: unknown = workspaceConfig.serverExtraArgs;
+ if (typeof rawExtraArgs === 'string' && rawExtraArgs !== '') {
+ const e = rawExtraArgs.split(' ');
+ serverArgs.push(...e);
+ }
+
+ // We don't want empty strings in our args
+ return serverArgs.map((x) => x.trim()).filter((x) => x !== '');
+}
diff --git a/src/docsBrowser.ts b/src/docsBrowser.ts
index 0d9ec779..aed1e196 100644
--- a/src/docsBrowser.ts
+++ b/src/docsBrowser.ts
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
import { dirname } from 'path';
import {
CancellationToken,
@@ -51,7 +50,7 @@ async function showDocumentation({
const bytes = await workspace.fs.readFile(Uri.parse(localPath));
const addBase = `
-
+
`;
panel.webview.html = `
@@ -63,8 +62,10 @@ async function showDocumentation({