Skip to content

Commit e19373f

Browse files
authored
Introduce the StatusBarItem
Merge pull request #1237 from fendor/enhance/refactor This refactors much more than just what is necessary for the `StatusBarItem`. My apologies!
2 parents 1c9df16 + 6ca6a85 commit e19373f

File tree

16 files changed

+1172
-854
lines changed

16 files changed

+1172
-854
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
fail-fast: false
1313
matrix:
1414
os: [macos-latest, ubuntu-latest, windows-latest]
15-
ghc: [8.10.7, 9.4.8, 9.6.4, 9.8.2]
15+
ghc: [8.10.7, 9.6.7, 9.8.4, 9.12.2]
1616
runs-on: ${{ matrix.os }}
1717
steps:
1818
- name: Checkout

eslint.config.mjs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
import globals from 'globals';
2-
import pluginJs from '@eslint/js';
2+
import eslint from '@eslint/js';
33
import tseslint from 'typescript-eslint';
44

5-
export default [
5+
export default tseslint.config(
66
{ files: ['**/*.{js,mjs,cjs,ts}'] },
7-
{ languageOptions: { globals: globals.node } },
87
{
9-
...pluginJs.configs.recommended,
8+
languageOptions: {
9+
globals: globals.node,
10+
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
11+
},
12+
},
13+
{
14+
// disables type checking for this file only
15+
files: ['eslint.config.mjs'],
16+
extends: [tseslint.configs.disableTypeChecked],
17+
},
18+
eslint.configs.recommended,
19+
tseslint.configs.recommendedTypeChecked,
20+
{
1021
rules: {
22+
// turn off these lints as we access workspaceConfiguration fields.
23+
// So far, there was no bug found with these unsafe accesses.
24+
'@typescript-eslint/no-unsafe-assignment': 'off',
25+
'@typescript-eslint/no-unsafe-member-access': 'off',
26+
// Sometimes, the 'any' just saves too much time.
1127
'@typescript-eslint/no-explicit-any': 'off',
28+
'@typescript-eslint/no-floating-promises': 'error',
1229
'@typescript-eslint/no-unused-vars': [
1330
'error',
1431
{
@@ -23,5 +40,4 @@ export default [
2340
],
2441
},
2542
},
26-
...tseslint.configs.recommended,
27-
];
43+
);

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,9 +1316,9 @@
13161316
},
13171317
"commands": [
13181318
{
1319-
"command": "haskell.commands.importIdentifier",
1320-
"title": "Haskell: Import identifier",
1321-
"description": "Imports a function or type based on a Hoogle search"
1319+
"command": "haskell.commands.restartExtension",
1320+
"title": "Haskell: Restart vscode-haskell extension",
1321+
"description": "Restart the vscode-haskell extension. Reloads configuration."
13221322
},
13231323
{
13241324
"command": "haskell.commands.restartServer",

src/commands/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export const ImportIdentifierCommandName = 'haskell.commands.importIdentifier';
1+
export const RestartExtensionCommandName = 'haskell.commands.restartExtension';
22
export const RestartServerCommandName = 'haskell.commands.restartServer';
33
export const StartServerCommandName = 'haskell.commands.startServer';
44
export const StopServerCommandName = 'haskell.commands.stopServer';
5+
export const OpenLogsCommandName = 'haskell.commands.openLogs';
6+
export const ShowExtensionVersions = 'haskell.commands.showVersions';

src/config.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { OutputChannel, Uri, window, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
2+
import { expandHomeDir, IEnvVars } from './utils';
3+
import * as path from 'path';
4+
import { Logger } from 'vscode-languageclient';
5+
import { ExtensionLogger } from './logger';
6+
import { GHCupConfig } from './ghcup';
7+
8+
export type LogLevel = 'off' | 'messages' | 'verbose';
9+
export type ClientLogLevel = 'off' | 'error' | 'info' | 'debug';
10+
11+
export type Config = {
12+
/**
13+
* Unique name per workspace folder (useful for multi-root workspaces).
14+
*/
15+
langName: string;
16+
logLevel: LogLevel;
17+
clientLogLevel: ClientLogLevel;
18+
logFilePath?: string;
19+
workingDir: string;
20+
outputChannel: OutputChannel;
21+
serverArgs: string[];
22+
serverEnvironment: IEnvVars;
23+
ghcupConfig: GHCupConfig;
24+
};
25+
26+
export function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, folder?: WorkspaceFolder): Config {
27+
// Set a unique name per workspace folder (useful for multi-root workspaces).
28+
const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
29+
const currentWorkingDir = folder ? folder.uri.fsPath : path.dirname(uri.fsPath);
30+
31+
const logLevel = getLogLevel(workspaceConfig);
32+
const clientLogLevel = getClientLogLevel(workspaceConfig);
33+
34+
const logFile = getLogFile(workspaceConfig);
35+
const logFilePath = resolveLogFilePath(logFile, currentWorkingDir);
36+
37+
const outputChannel: OutputChannel = window.createOutputChannel(langName);
38+
const serverArgs = getServerArgs(workspaceConfig, logLevel, logFilePath);
39+
40+
return {
41+
langName: langName,
42+
logLevel: logLevel,
43+
clientLogLevel: clientLogLevel,
44+
logFilePath: logFilePath,
45+
workingDir: currentWorkingDir,
46+
outputChannel: outputChannel,
47+
serverArgs: serverArgs,
48+
serverEnvironment: workspaceConfig.serverEnvironment,
49+
ghcupConfig: {
50+
metadataUrl: workspaceConfig.metadataURL as string,
51+
upgradeGHCup: workspaceConfig.get('upgradeGHCup') as boolean,
52+
executablePath: workspaceConfig.get('ghcupExecutablePath') as string,
53+
},
54+
};
55+
}
56+
57+
export function initLoggerFromConfig(config: Config): ExtensionLogger {
58+
return new ExtensionLogger('client', config.clientLogLevel, config.outputChannel, config.logFilePath);
59+
}
60+
61+
export function logConfig(logger: Logger, config: Config) {
62+
if (config.logFilePath) {
63+
logger.info(`Writing client log to file ${config.logFilePath}`);
64+
}
65+
logger.log('Environment variables:');
66+
Object.entries(process.env).forEach(([key, value]: [string, string | undefined]) => {
67+
// only list environment variables that we actually care about.
68+
// this makes it safe for users to just paste the logs to whoever,
69+
// and avoids leaking secrets.
70+
if (['PATH'].includes(key)) {
71+
logger.log(` ${key}: ${value}`);
72+
}
73+
});
74+
}
75+
76+
function getLogFile(workspaceConfig: WorkspaceConfiguration) {
77+
const logFile_: unknown = workspaceConfig.logFile;
78+
let logFile: string | undefined;
79+
if (typeof logFile_ === 'string') {
80+
logFile = logFile_ !== '' ? logFile_ : undefined;
81+
}
82+
return logFile;
83+
}
84+
85+
function getClientLogLevel(workspaceConfig: WorkspaceConfiguration): ClientLogLevel {
86+
const clientLogLevel_: unknown = workspaceConfig.trace.client;
87+
let clientLogLevel;
88+
if (typeof clientLogLevel_ === 'string') {
89+
switch (clientLogLevel_) {
90+
case 'off':
91+
case 'error':
92+
case 'info':
93+
case 'debug':
94+
clientLogLevel = clientLogLevel_;
95+
break;
96+
default:
97+
throw new Error("Option \"haskell.trace.client\" is expected to be one of 'off', 'error', 'info', 'debug'.");
98+
}
99+
} else {
100+
throw new Error('Option "haskell.trace.client" is expected to be a string');
101+
}
102+
return clientLogLevel;
103+
}
104+
105+
function getLogLevel(workspaceConfig: WorkspaceConfiguration): LogLevel {
106+
const logLevel_: unknown = workspaceConfig.trace.server;
107+
let logLevel;
108+
if (typeof logLevel_ === 'string') {
109+
switch (logLevel_) {
110+
case 'off':
111+
case 'messages':
112+
case 'verbose':
113+
logLevel = logLevel_;
114+
break;
115+
default:
116+
throw new Error("Option \"haskell.trace.server\" is expected to be one of 'off', 'messages', 'verbose'.");
117+
}
118+
} else {
119+
throw new Error('Option "haskell.trace.server" is expected to be a string');
120+
}
121+
return logLevel;
122+
}
123+
124+
function resolveLogFilePath(logFile: string | undefined, currentWorkingDir: string): string | undefined {
125+
return logFile !== undefined ? path.resolve(currentWorkingDir, expandHomeDir(logFile)) : undefined;
126+
}
127+
128+
function getServerArgs(workspaceConfig: WorkspaceConfiguration, logLevel: LogLevel, logFilePath?: string): string[] {
129+
const serverArgs = ['--lsp']
130+
.concat(logLevel === 'messages' ? ['-d'] : [])
131+
.concat(logFilePath !== undefined ? ['-l', logFilePath] : []);
132+
133+
const rawExtraArgs: unknown = workspaceConfig.serverExtraArgs;
134+
if (typeof rawExtraArgs === 'string' && rawExtraArgs !== '') {
135+
const e = rawExtraArgs.split(' ');
136+
serverArgs.push(...e);
137+
}
138+
139+
// We don't want empty strings in our args
140+
return serverArgs.map((x) => x.trim()).filter((x) => x !== '');
141+
}

src/docsBrowser.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import { dirname } from 'path';
32
import {
43
CancellationToken,
@@ -51,7 +50,7 @@ async function showDocumentation({
5150
const bytes = await workspace.fs.readFile(Uri.parse(localPath));
5251

5352
const addBase = `
54-
<base href="${panel.webview.asWebviewUri(Uri.parse(documentationDirectory))}/">
53+
<base href="${panel.webview.asWebviewUri(Uri.parse(documentationDirectory)).toString()}/">
5554
`;
5655

5756
panel.webview.html = `
@@ -63,8 +62,10 @@ async function showDocumentation({
6362
</body>
6463
</html>
6564
`;
66-
} catch (e: any) {
67-
await window.showErrorMessage(e);
65+
} catch (e) {
66+
if (e instanceof Error) {
67+
await window.showErrorMessage(e.message);
68+
}
6869
}
6970
return panel;
7071
}
@@ -87,8 +88,10 @@ async function openDocumentationOnHackage({
8788
if (inWebView) {
8889
await commands.executeCommand('workbench.action.closeActiveEditor');
8990
}
90-
} catch (e: any) {
91-
await window.showErrorMessage(e);
91+
} catch (e) {
92+
if (e instanceof Error) {
93+
await window.showErrorMessage(e.message);
94+
}
9295
}
9396
}
9497

@@ -154,11 +157,9 @@ function processLink(ms: MarkdownString | MarkedString): string | MarkdownString
154157
cmd = 'command:haskell.showDocumentation?' + encoded;
155158
}
156159
return `[${title}](${cmd})`;
157-
} else if (title === 'Source') {
158-
hackageUri = `https://hackage.haskell.org/package/${packageName}/docs/src/${fileAndAnchor.replace(
159-
/-/gi,
160-
'.',
161-
)}`;
160+
} else if (title === 'Source' && typeof fileAndAnchor === 'string') {
161+
const moduleLocation = fileAndAnchor.replace(/-/gi, '.');
162+
hackageUri = `https://hackage.haskell.org/package/${packageName}/docs/src/${moduleLocation}`;
162163
const encoded = encodeURIComponent(JSON.stringify({ title, localPath, hackageUri }));
163164
let cmd: string;
164165
if (openSourceInHackage) {
@@ -174,7 +175,7 @@ function processLink(ms: MarkdownString | MarkedString): string | MarkdownString
174175
);
175176
}
176177
if (typeof ms === 'string') {
177-
return transform(ms as string);
178+
return transform(ms);
178179
} else if (ms instanceof MarkdownString) {
179180
const mstr = new MarkdownString(transform(ms.value));
180181
mstr.isTrusted = true;

src/errors.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ export class MissingToolError extends HlsError {
2020
prettyTool = 'GHCup';
2121
break;
2222
case 'haskell-language-server':
23-
prettyTool = 'HLS';
24-
break;
2523
case 'hls':
2624
prettyTool = 'HLS';
2725
break;

0 commit comments

Comments
 (0)