Skip to content

Introduce the StatusBarItem #1237

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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
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-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
Expand Down
28 changes: 22 additions & 6 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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',
{
Expand All @@ -23,5 +40,4 @@ export default [
],
},
},
...tseslint.configs.recommended,
];
);
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion src/commands/constants.ts
Original file line number Diff line number Diff line change
@@ -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';
141 changes: 141 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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 !== '');
}
25 changes: 13 additions & 12 deletions src/docsBrowser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { dirname } from 'path';
import {
CancellationToken,
Expand Down Expand Up @@ -51,7 +50,7 @@ async function showDocumentation({
const bytes = await workspace.fs.readFile(Uri.parse(localPath));

const addBase = `
<base href="${panel.webview.asWebviewUri(Uri.parse(documentationDirectory))}/">
<base href="${panel.webview.asWebviewUri(Uri.parse(documentationDirectory)).toString()}/">
`;

panel.webview.html = `
Expand All @@ -63,8 +62,10 @@ async function showDocumentation({
</body>
</html>
`;
} catch (e: any) {
await window.showErrorMessage(e);
} catch (e) {
if (e instanceof Error) {
await window.showErrorMessage(e.message);
}
}
return panel;
}
Expand All @@ -87,8 +88,10 @@ async function openDocumentationOnHackage({
if (inWebView) {
await commands.executeCommand('workbench.action.closeActiveEditor');
}
} catch (e: any) {
await window.showErrorMessage(e);
} catch (e) {
if (e instanceof Error) {
await window.showErrorMessage(e.message);
}
}
}

Expand Down Expand Up @@ -154,11 +157,9 @@ function processLink(ms: MarkdownString | MarkedString): string | MarkdownString
cmd = 'command:haskell.showDocumentation?' + encoded;
}
return `[${title}](${cmd})`;
} else if (title === 'Source') {
hackageUri = `https://hackage.haskell.org/package/${packageName}/docs/src/${fileAndAnchor.replace(
/-/gi,
'.',
)}`;
} else if (title === 'Source' && typeof fileAndAnchor === 'string') {
const moduleLocation = fileAndAnchor.replace(/-/gi, '.');
hackageUri = `https://hackage.haskell.org/package/${packageName}/docs/src/${moduleLocation}`;
const encoded = encodeURIComponent(JSON.stringify({ title, localPath, hackageUri }));
let cmd: string;
if (openSourceInHackage) {
Expand All @@ -174,7 +175,7 @@ function processLink(ms: MarkdownString | MarkedString): string | MarkdownString
);
}
if (typeof ms === 'string') {
return transform(ms as string);
return transform(ms);
} else if (ms instanceof MarkdownString) {
const mstr = new MarkdownString(transform(ms.value));
mstr.isTrusted = true;
Expand Down
2 changes: 0 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export class MissingToolError extends HlsError {
prettyTool = 'GHCup';
break;
case 'haskell-language-server':
prettyTool = 'HLS';
break;
case 'hls':
prettyTool = 'HLS';
break;
Expand Down
Loading