Skip to content

Commit 0459c30

Browse files
committed
Introduce config type bundling configuration
Includes the arguments passed to the server, the logging configuration, working directory, anything that is supposed to stay constant during the execution of a single HLS instance. Note, it is not promised to stay constant over restarts.
1 parent 1c9df16 commit 0459c30

File tree

5 files changed

+161
-80
lines changed

5 files changed

+161
-80
lines changed

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default [
77
{ languageOptions: { globals: globals.node } },
88
{
99
...pluginJs.configs.recommended,
10+
...tseslint.configs.recommendedTypeChecked,
1011
rules: {
1112
'@typescript-eslint/no-explicit-any': 'off',
1213
'@typescript-eslint/no-unused-vars': [

src/config.ts

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

src/extension.ts

Lines changed: 25 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,4 @@
1-
import * as path from 'path';
2-
import {
3-
commands,
4-
env,
5-
ExtensionContext,
6-
OutputChannel,
7-
TextDocument,
8-
Uri,
9-
window,
10-
workspace,
11-
WorkspaceFolder,
12-
} from 'vscode';
1+
import { commands, env, ExtensionContext, TextDocument, Uri, window, workspace, WorkspaceFolder } from 'vscode';
132
import {
143
ExecutableOptions,
154
LanguageClient,
@@ -22,7 +11,8 @@ import { RestartServerCommandName, StartServerCommandName, StopServerCommandName
2211
import * as DocsBrowser from './docsBrowser';
2312
import { HlsError, MissingToolError, NoMatchingHls } from './errors';
2413
import { callAsync, findHaskellLanguageServer, IEnvVars } from './hlsBinaries';
25-
import { addPathToProcessPath, comparePVP, expandHomeDir, ExtensionLogger } from './utils';
14+
import { addPathToProcessPath, comparePVP } from './utils';
15+
import { initConfig, initLoggerFromConfig, logConfig } from './config';
2616

2717
// The current map of documents & folders to language servers.
2818
// It may be null to indicate that we are in the process of launching a server,
@@ -112,47 +102,25 @@ async function activeServer(context: ExtensionContext, document: TextDocument) {
112102

113103
async function activateServerForFolder(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder) {
114104
const clientsKey = folder ? folder.uri.toString() : uri.toString();
115-
// Set a unique name per workspace folder (useful for multi-root workspaces).
116-
const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
117-
118105
// If the client already has an LSP server for this uri/folder, then don't start a new one.
119106
if (clients.has(clientsKey)) {
120107
return;
121108
}
122-
123-
const currentWorkingDir = folder ? folder.uri.fsPath : path.dirname(uri.fsPath);
124-
125109
// Set the key to null to prevent multiple servers being launched at once
126110
clients.set(clientsKey, null);
127111

128-
const logLevel = workspace.getConfiguration('haskell', uri).trace.server;
129-
const clientLogLevel = workspace.getConfiguration('haskell', uri).trace.client;
130-
const logFile: string = workspace.getConfiguration('haskell', uri).logFile;
112+
const config = initConfig(workspace.getConfiguration('haskell', uri), uri, folder);
113+
const logger: Logger = initLoggerFromConfig(config);
131114

132-
const outputChannel: OutputChannel = window.createOutputChannel(langName);
133-
134-
const logFilePath = logFile !== '' ? path.resolve(currentWorkingDir, expandHomeDir(logFile)) : undefined;
135-
const logger: Logger = new ExtensionLogger('client', clientLogLevel, outputChannel, logFilePath);
136-
if (logFilePath) {
137-
logger.info(`Writing client log to file ${logFilePath}`);
138-
}
139-
logger.log('Environment variables:');
140-
Object.entries(process.env).forEach(([key, value]: [string, string | undefined]) => {
141-
// only list environment variables that we actually care about.
142-
// this makes it safe for users to just paste the logs to whoever,
143-
// and avoids leaking secrets.
144-
if (['PATH'].includes(key)) {
145-
logger.log(` ${key}: ${value}`);
146-
}
147-
});
115+
logConfig(logger, config);
148116

149117
let serverExecutable: string;
150118
let addInternalServerPath: string | undefined; // if we download HLS, add that bin dir to PATH
151119
try {
152120
[serverExecutable, addInternalServerPath] = await findHaskellLanguageServer(
153121
context,
154122
logger,
155-
currentWorkingDir,
123+
config.workingDir,
156124
folder,
157125
);
158126
if (!serverExecutable) {
@@ -190,31 +158,10 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
190158
return;
191159
}
192160

193-
let args: string[] = ['--lsp'];
194-
195-
if (logLevel === 'messages') {
196-
args = args.concat(['-d']);
197-
}
198-
199-
if (logFile !== '') {
200-
args = args.concat(['-l', logFile]);
201-
}
202-
203-
const extraArgs: string = workspace.getConfiguration('haskell', uri).serverExtraArgs;
204-
if (extraArgs !== '') {
205-
args = args.concat(extraArgs.split(' '));
206-
}
207-
208-
const cabalFileSupport: 'automatic' | 'enable' | 'disable' = workspace.getConfiguration(
209-
'haskell',
210-
uri,
211-
).supportCabalFiles;
212-
logger.info(`Support for '.cabal' files: ${cabalFileSupport}`);
213-
214161
// If we're operating on a standalone file (i.e. not in a folder) then we need
215162
// to launch the server in a reasonable current directory. Otherwise the cradle
216163
// guessing logic in hie-bios will be wrong!
217-
let cwdMsg = `Activating the language server in working dir: ${currentWorkingDir}`;
164+
let cwdMsg = `Activating the language server in working dir: ${config.workingDir}`;
218165
if (folder) {
219166
cwdMsg += ' (the workspace folder)';
220167
} else {
@@ -231,22 +178,19 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
231178
};
232179
}
233180
const exeOptions: ExecutableOptions = {
234-
cwd: folder ? folder.uri.fsPath : path.dirname(uri.fsPath),
181+
cwd: config.workingDir,
235182
env: { ...process.env, ...serverEnvironment },
236183
};
237184

238-
// We don't want empty strings in our args
239-
args = args.map((x) => x.trim()).filter((x) => x !== '');
240-
241185
// For our intents and purposes, the server should be launched the same way in
242186
// both debug and run mode.
243187
const serverOptions: ServerOptions = {
244-
run: { command: serverExecutable, args, options: exeOptions },
245-
debug: { command: serverExecutable, args, options: exeOptions },
188+
run: { command: serverExecutable, args: config.serverArgs, options: exeOptions },
189+
debug: { command: serverExecutable, args: config.serverArgs, options: exeOptions },
246190
};
247191

248-
logger.info(`run command: ${serverExecutable} ${args.join(' ')}`);
249-
logger.info(`debug command: ${serverExecutable} ${args.join(' ')}`);
192+
logger.info(`run command: ${serverExecutable} ${config.serverArgs.join(' ')}`);
193+
logger.info(`debug command: ${serverExecutable} ${config.serverArgs.join(' ')}`);
250194
if (exeOptions.cwd) {
251195
logger.info(`server cwd: ${exeOptions.cwd}`);
252196
}
@@ -268,13 +212,19 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
268212

269213
const documentSelector = [...haskellDocumentSelector];
270214

215+
const cabalFileSupport: 'automatic' | 'enable' | 'disable' = workspace.getConfiguration(
216+
'haskell',
217+
uri,
218+
).supportCabalFiles;
219+
logger.info(`Support for '.cabal' files: ${cabalFileSupport}`);
220+
271221
switch (cabalFileSupport) {
272222
case 'automatic':
273223
const hlsVersion = await callAsync(
274224
serverExecutable,
275225
['--numeric-version'],
276226
logger,
277-
currentWorkingDir,
227+
config.workingDir,
278228
undefined /* this command is very fast, don't show anything */,
279229
false,
280230
serverEnvironment,
@@ -301,10 +251,10 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
301251
// Synchronize the setting section 'haskell' to the server.
302252
configurationSection: 'haskell',
303253
},
304-
diagnosticCollectionName: langName,
254+
diagnosticCollectionName: config.langName,
305255
revealOutputChannelOn: RevealOutputChannelOn.Never,
306-
outputChannel,
307-
outputChannelName: langName,
256+
outputChannel: config.outputChannel,
257+
outputChannelName: config.langName,
308258
middleware: {
309259
provideHover: DocsBrowser.hoverLinksMiddlewareHook,
310260
provideCompletionItem: DocsBrowser.completionLinksMiddlewareHook,
@@ -314,15 +264,15 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
314264
};
315265

316266
// Create the LSP client.
317-
const langClient = new LanguageClient('haskell', langName, serverOptions, clientOptions);
267+
const langClient = new LanguageClient('haskell', config.langName, serverOptions, clientOptions);
318268

319269
// Register ClientCapabilities for stuff like window/progress
320270
langClient.registerProposedFeatures();
321271

322272
// Finally start the client and add it to the list of clients.
323273
logger.info('Starting language server');
324-
langClient.start();
325274
clients.set(clientsKey, langClient);
275+
await langClient.start();
326276
}
327277

328278
/*

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Logger } from 'vscode-languageclient';
77
import * as which from 'which';
88

99
// Used for environment variables later on
10-
export interface IEnvVars {
10+
export type IEnvVars = {
1111
[key: string]: string;
1212
}
1313

tsconfig.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
{
22
"compilerOptions": {
3-
"module": "commonjs",
4-
"moduleResolution": "node",
5-
"target": "es6",
3+
"module": "CommonJS",
4+
"target": "es2022",
65
"outDir": "out",
7-
"lib": ["es6"],
6+
"lib": ["es2022"],
87
"sourceMap": true,
98
"rootDir": ".",
109
"noUnusedLocals": true,

0 commit comments

Comments
 (0)