From a5605c6f36456b8f7cec7d962f4225fab4d7f019 Mon Sep 17 00:00:00 2001 From: fendor Date: Sat, 5 Apr 2025 15:48:28 +0200 Subject: [PATCH 1/2] Add option to enable/disable `.cabal` file support Support for `.cabal` files was implemented in HLS 2.0.0.0. But the vscode-haskell extension does not enable this Language Server support, because it would break for older HLS binaries, as these were unable to handle file updates for `.cabal` files. To work-around, we had a pre-release of the vscode-haskell plugin, which enabled support for `.cabal` plugins in an opt-in fashion. It's been a while, and we want to enable language server support for `.cabal` files unconditionally. There are still HLS binaries around that can't handle `.cabal` files, but it much less. For the unfortunate souls left behind with GHC versions such as 8.6.5, we add a new vscode-haskell option: haskell.supportCabalFiles This option is `true` by default, but if set to `false` it will unconditionally **disable** sending File Notifications for `.cabal` files, which allows us to keep working for older HLS binaries. This change would be difficult to test, and incur an, in my opinion, unreasonable price on our CI times. I manually tested the changes, by using HLS 1.8.0.0 (last released binary for GHC 8.6.5) on a project with `haskell.supportCabalFiles` set to `true` and `false` respectively. HLS seemed to behave correctly, showing Diagnostics for Haskell files, but not show any logs for `.cabal` files. The only potential change in behaviour is that vscode-haskell now also activates when a `.cabal` file has been opened. This may be overly eager to some, but since no Haskell files are typechecked, the impact on memory usage is negligible in my opinion. --- package.json | 6 ++++++ src/extension.ts | 20 +++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 159b5186..fd4e94c8 100644 --- a/package.json +++ b/package.json @@ -234,6 +234,12 @@ "Prefer a multiple component session, if the build tool supports it. At the moment, only `cabal` supports multiple components session loading. If the `cabal` version does not support loading multiple components at once, we gracefully fall back to \"singleComponent\" mode." ] }, + "haskell.supportCabalFiles": { + "scope": "resource", + "default": true, + "type": "boolean", + "description": "Enable Language Server support for `.cabal` files. Requires Haskell Language Server version >= 2.0.0.0." + }, "haskell.maxCompletions": { "scope": "resource", "default": 40, diff --git a/src/extension.ts b/src/extension.ts index 111b72ed..2e576ab6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -205,6 +205,9 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold args = args.concat(extraArgs.split(' ')); } + const cabalFileSupport: boolean = workspace.getConfiguration('haskell', uri).supportCabalFiles; + logger.info(`Support for '.cabal' files enabled: ${cabalFileSupport ? 'yes' : 'no'}`); + // 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! @@ -253,14 +256,21 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*'; logger.log(`document selector patten: ${pat}`); + + + const cabalDocumentSelector = cabalFileSupport ? [{ scheme: 'file', language: 'cabal', pattern: pat }] : []; + const haskellDocumentSelector = [ + { scheme: 'file', language: 'haskell', pattern: pat }, + { scheme: 'file', language: 'literate haskell', pattern: pat }, + ]; + const clientOptions: LanguageClientOptions = { // Use the document selector to only notify the LSP on files inside the folder // path for the specific workspace. - documentSelector: [ - { scheme: 'file', language: 'haskell', pattern: pat }, - { scheme: 'file', language: 'literate haskell', pattern: pat }, - { scheme: 'file', language: 'cabal', pattern: pat }, - ], + documentSelector: + [ ... haskellDocumentSelector + , ... cabalDocumentSelector + ], synchronize: { // Synchronize the setting section 'haskell' to the server. configurationSection: 'haskell', From fba2f31833596b9873794ca43dd00f159d662972 Mon Sep 17 00:00:00 2001 From: fendor Date: Sun, 6 Apr 2025 17:51:51 +0200 Subject: [PATCH 2/2] Introduce 'automatic' mode for 'supportCabalFiles' option Add option to automatically determine whether the Haskell Language Server binary we are about to launch supports '.cabal' files. If not, i.e. the HLS binary is older than '1.9.0.0', then we do not send file notifications for '.cabal' files. Otherwise, we do send these notifications. The user can explicitly overwrite this option in order to avoid the version check. --- package.json | 16 +++++++++++++--- src/extension.ts | 46 ++++++++++++++++++++++++++++++++++++---------- src/hlsBinaries.ts | 2 +- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index fd4e94c8..6f045f6f 100644 --- a/package.json +++ b/package.json @@ -236,9 +236,19 @@ }, "haskell.supportCabalFiles": { "scope": "resource", - "default": true, - "type": "boolean", - "description": "Enable Language Server support for `.cabal` files. Requires Haskell Language Server version >= 2.0.0.0." + "default": "automatic", + "type": "string", + "enum": [ + "enable", + "disable", + "automatic" + ], + "description": "Enable Language Server support for `.cabal` files. Requires Haskell Language Server version >= 1.9.0.0.", + "enumDescriptions": [ + "Enable Language Server support for `.cabal` files", + "Disable Language Server support for `.cabal` files", + "Enable Language Server support for `.cabal` files if the HLS version supports it." + ] }, "haskell.maxCompletions": { "scope": "resource", diff --git a/src/extension.ts b/src/extension.ts index 2e576ab6..480ba77a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,8 +21,8 @@ import { import { RestartServerCommandName, StartServerCommandName, StopServerCommandName } from './commands/constants'; import * as DocsBrowser from './docsBrowser'; import { HlsError, MissingToolError, NoMatchingHls } from './errors'; -import { findHaskellLanguageServer, IEnvVars } from './hlsBinaries'; -import { addPathToProcessPath, expandHomeDir, ExtensionLogger } from './utils'; +import { callAsync, findHaskellLanguageServer, IEnvVars } from './hlsBinaries'; +import { addPathToProcessPath, comparePVP, expandHomeDir, 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, @@ -205,8 +205,11 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold args = args.concat(extraArgs.split(' ')); } - const cabalFileSupport: boolean = workspace.getConfiguration('haskell', uri).supportCabalFiles; - logger.info(`Support for '.cabal' files enabled: ${cabalFileSupport ? 'yes' : 'no'}`); + const cabalFileSupport: 'automatic' | 'enable' | 'disable' = workspace.getConfiguration( + 'haskell', + uri, + ).supportCabalFiles; + logger.info(`Support for '.cabal' files: ${cabalFileSupport}`); // 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 @@ -257,20 +260,43 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*'; logger.log(`document selector patten: ${pat}`); - - const cabalDocumentSelector = cabalFileSupport ? [{ scheme: 'file', language: 'cabal', pattern: pat }] : []; + const cabalDocumentSelector = { scheme: 'file', language: 'cabal', pattern: pat }; const haskellDocumentSelector = [ { scheme: 'file', language: 'haskell', pattern: pat }, { scheme: 'file', language: 'literate haskell', pattern: pat }, ]; + const documentSelector = [...haskellDocumentSelector]; + + switch (cabalFileSupport) { + case 'automatic': + const hlsVersion = await callAsync( + serverExecutable, + ['--numeric-version'], + logger, + currentWorkingDir, + undefined /* this command is very fast, don't show anything */, + false, + serverEnvironment, + ); + if (comparePVP(hlsVersion, '1.9.0.0') >= 0) { + // If hlsVersion is >= '1.9.0.0' + documentSelector.push(cabalDocumentSelector); + } + break; + case 'enable': + documentSelector.push(cabalDocumentSelector); + break; + case 'disable': + break; + default: + break; + } + const clientOptions: LanguageClientOptions = { // Use the document selector to only notify the LSP on files inside the folder // path for the specific workspace. - documentSelector: - [ ... haskellDocumentSelector - , ... cabalDocumentSelector - ], + documentSelector: [...documentSelector], synchronize: { // Synchronize the setting section 'haskell' to the server. configurationSection: 'haskell', diff --git a/src/hlsBinaries.ts b/src/hlsBinaries.ts index 91a42f59..65ea68c7 100644 --- a/src/hlsBinaries.ts +++ b/src/hlsBinaries.ts @@ -58,7 +58,7 @@ type ProcessCallback = ( * @param callback Upon process termination, execute this callback. If given, must resolve promise. On error, stderr and stdout are logged regardless of whether the callback has been specified. * @returns Stdout of the process invocation, trimmed off newlines, or whatever the `callback` resolved to. */ -async function callAsync( +export async function callAsync( binary: string, args: string[], logger: Logger,