From 171a5bf65608ebd335eece431918e11ffe876006 Mon Sep 17 00:00:00 2001 From: dathegreat Date: Fri, 24 Apr 2026 12:52:08 -0600 Subject: [PATCH 01/11] languages/haskell.nix: fix invalid keys in the haskell-tools configuration These keys presented the error `unrecognized configs in vim.g.haskell_tools: { "hls.filetypes", "hls.root_dir", "tools.hover.enable", "hls.enable" }` on attaching to any haskell file. --- docs/manual/release-notes/rl-0.9.md | 7 +++ modules/plugins/languages/haskell.nix | 87 ++++++++++++++++++--------- 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/docs/manual/release-notes/rl-0.9.md b/docs/manual/release-notes/rl-0.9.md index ffdccb8e..870b3d32 100644 --- a/docs/manual/release-notes/rl-0.9.md +++ b/docs/manual/release-notes/rl-0.9.md @@ -201,6 +201,13 @@ - Updated nix language plugin to use pkgs.nixfmt instead of pkgs.nixfmt-rfc-style +[dathegreat](https://github.com/dathegreat): + +- Fixed invalid keys in the haskell-tools configuration +- Changed the default haskell formatter to + [fourmolu](https://github.com/fourmolu/fourmolu), matching the + [haskell-tools default](https://github.com/mrcjkb/haskell-tools.nvim/blob/9ea030aa67f3875753e70e1eb59701f7020479a0/lua/haskell-tools/config/internal.lua#L131) + [alfarel](https://github.com/alfarelcynthesis): [obsidian.nvim]: https://github.com/obsidian-nvim/obsidian.nvim diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index 32f45151..e4831b01 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -4,14 +4,19 @@ pkgs, ... }: let - inherit (builtins) isList attrNames; - inherit (lib.types) either package enum listOf str; + inherit (builtins) attrNames; + inherit + (lib.types) + either + package + enum + listOf + str + ; inherit (lib.options) mkEnableOption mkOption literalExpression; - inherit (lib.strings) optionalString; inherit (lib.modules) mkIf mkMerge; inherit (lib.nvim.types) mkGrammarOption; inherit (lib.nvim.dag) entryAfter; - inherit (lib.nvim.lua) toLuaObject; inherit (lib.meta) getExe'; inherit (lib.generators) mkLuaInline; inherit (pkgs) haskellPackages; @@ -22,8 +27,14 @@ servers = { hls = { enable = false; - cmd = [(getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") "--lsp"]; - filetypes = ["haskell" "lhaskell"]; + cmd = [ + (getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") + "--lsp" + ]; + filetypes = [ + "haskell" + "lhaskell" + ]; on_attach = mkLuaInline /* @@ -116,31 +127,49 @@ in { (mkIf (cfg.dap.enable || cfg.lsp.enable) { vim = { startPlugins = ["haskell-tools-nvim"]; - luaConfigRC.haskell-tools-nvim = - entryAfter - ["lsp-servers"] - '' - vim.g.haskell_tools = { - ${optionalString cfg.lsp.enable '' - -- LSP - tools = { - hover = { - enable = true, + luaConfigRC.haskell-tools-nvim = entryAfter ["lsp-servers"] '' + vim.g.haskell_tools = { + tools = { + hover = { + stylize_markdown = false, + auto_focus = false, + }, + }, + hls = { + auto_attach = true, + cmd = {"${getExe' haskellPackages.haskell-language-server "haskell-language-server-wrapper"}", "--lsp"}, + on_attach = function(client, bufnr) + local ht = require("haskell-tools") + local opts = { noremap = true, silent = true, buffer = bufnr } + vim.keymap.set('n', 'cl', vim.lsp.codelens.run, opts) + vim.keymap.set('n', 'hs', ht.hoogle.hoogle_signature, opts) + vim.keymap.set('n', 'ea', ht.lsp.buf_eval_all, opts) + vim.keymap.set('n', 'rr', function() + vim.cmd('Haskell repl toggle') + end, opts) + vim.keymap.set('n', 'rf', function() + vim.cmd('Haskell repl toggle ' .. vim.api.nvim_buf_get_name(0)) + end, opts) + vim.keymap.set('n', 'rq', function() + vim.cmd('Haskell repl quit') + end, opts) + end, + settings = function(project_root) + local ht = require("haskell-tools") + return ht.lsp.load_hls_settings(project_root) + end, + default_settings = { + haskell = { + formattingProvider = "fourmolu", + cabalFormattingProvider = "cabal-fmt", }, }, - hls = ${toLuaObject servers.hls}, - ''} - ${optionalString cfg.dap.enable '' - dap = { - cmd = ${ - if isList cfg.dap.package - then toLuaObject cfg.dap.package - else ''{"${cfg.dap.package}/bin/haskell-debug-adapter"}'' - }, - }, - ''} - } - ''; + }, + dap = { + cmd = {"${getExe' haskellPackages.haskell-debug-adapter "haskell-debug-adapter"}"}, + }, + } + ''; }; }) ]); From ff84f1c0ff098c411e005d436063f15a241d167c Mon Sep 17 00:00:00 2001 From: dathegreat Date: Fri, 24 Apr 2026 14:04:49 -0600 Subject: [PATCH 02/11] languages/haskell.nix: revert list expansion --- modules/plugins/languages/haskell.nix | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index e4831b01..8fd3dd61 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -5,14 +5,7 @@ ... }: let inherit (builtins) attrNames; - inherit - (lib.types) - either - package - enum - listOf - str - ; + inherit (lib.types) either package enum listOf str; inherit (lib.options) mkEnableOption mkOption literalExpression; inherit (lib.modules) mkIf mkMerge; inherit (lib.nvim.types) mkGrammarOption; @@ -27,14 +20,8 @@ servers = { hls = { enable = false; - cmd = [ - (getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") - "--lsp" - ]; - filetypes = [ - "haskell" - "lhaskell" - ]; + cmd = [(getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") "--lsp"]; + filetypes = ["haskell" "lhaskell"]; on_attach = mkLuaInline /* From ca21d4359024f8868ee70fd5f8ca2d5eb4635adb Mon Sep 17 00:00:00 2001 From: dathegreat Date: Fri, 24 Apr 2026 17:02:47 -0600 Subject: [PATCH 03/11] languages/haskell.nix: move haskell-tools config into `languages.haskell.extensions` and hls presets into `lsp/presets/hls.nix` --- modules/plugins/languages/haskell.nix | 87 ++++++++++--------------- modules/plugins/lsp/presets/default.nix | 1 + modules/plugins/lsp/presets/hls.nix | 42 ++++++++++++ 3 files changed, 79 insertions(+), 51 deletions(-) create mode 100644 modules/plugins/lsp/presets/hls.nix diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index 8fd3dd61..c4546640 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -4,61 +4,23 @@ pkgs, ... }: let - inherit (builtins) attrNames; + inherit (builtins) attrNames isList; + inherit (lib) genAttrs; inherit (lib.types) either package enum listOf str; inherit (lib.options) mkEnableOption mkOption literalExpression; + inherit (lib.strings) optionalString; inherit (lib.modules) mkIf mkMerge; inherit (lib.nvim.types) mkGrammarOption; + inherit (lib.nvim.lua) toLuaObject; inherit (lib.nvim.dag) entryAfter; inherit (lib.meta) getExe'; - inherit (lib.generators) mkLuaInline; inherit (pkgs) haskellPackages; cfg = config.vim.languages.haskell; defaultServers = ["hls"]; servers = { - hls = { - enable = false; - cmd = [(getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") "--lsp"]; - filetypes = ["haskell" "lhaskell"]; - on_attach = - mkLuaInline - /* - lua - */ - '' - function(client, bufnr) - local ht = require("haskell-tools") - local opts = { noremap = true, silent = true, buffer = bufnr } - vim.keymap.set('n', 'cl', vim.lsp.codelens.run, opts) - vim.keymap.set('n', 'hs', ht.hoogle.hoogle_signature, opts) - vim.keymap.set('n', 'ea', ht.lsp.buf_eval_all, opts) - vim.keymap.set('n', 'rr', ht.repl.toggle, opts) - vim.keymap.set('n', 'rf', function() - ht.repl.toggle(vim.api.nvim_buf_get_name(0)) - end, opts) - vim.keymap.set('n', 'rq', ht.repl.quit, opts) - end - ''; - root_dir = - mkLuaInline - /* - lua - */ - '' - function(bufnr, on_dir) - local fname = vim.api.nvim_buf_get_name(bufnr) - on_dir(util.root_pattern('hie.yaml', 'stack.yaml', 'cabal.project', '*.cabal', 'package.yaml')(fname)) - end - ''; - settings = { - haskell = { - formattingProvider = "ormolu"; - cabalFormattingProvider = "cabal-fmt"; - }; - }; - }; + hls = {}; }; in { options.vim.languages.haskell = { @@ -86,6 +48,11 @@ in { default = defaultServers; description = "Haskell LSP server to use"; }; + formattingProvider = mkOption { + type = enum ["ormolu" "fourmolu" "stylish-haskell" "brittany" "floskell" "none"]; + default = "ormolu"; + description = "Formatter used by HLS"; + }; }; dap = { @@ -101,6 +68,12 @@ in { description = "Haskell DAP package or command to run the Haskell DAP"; }; }; + + extensions = { + haskell-tools = { + enable = mkEnableOption "haskell-tools.nvim"; + }; + }; }; config = mkIf cfg.enable (mkMerge [ @@ -111,7 +84,17 @@ in { }; }) - (mkIf (cfg.dap.enable || cfg.lsp.enable) { + (mkIf (cfg.lsp.enable && !cfg.extensions.haskell-tools.enable) { + vim.lsp = { + presets = genAttrs cfg.lsp.servers (_: {enable = true;}); + servers = genAttrs cfg.lsp.servers (_: { + filetypes = ["haskell" "lhaskell"]; + settings.haskell.formattingProvider = cfg.lsp.formattingProvider; + }); + }; + }) + + (mkIf cfg.extensions.haskell-tools.enable { vim = { startPlugins = ["haskell-tools-nvim"]; luaConfigRC.haskell-tools-nvim = entryAfter ["lsp-servers"] '' @@ -141,20 +124,22 @@ in { vim.cmd('Haskell repl quit') end, opts) end, - settings = function(project_root) - local ht = require("haskell-tools") - return ht.lsp.load_hls_settings(project_root) - end, - default_settings = { + settings = { haskell = { - formattingProvider = "fourmolu", + formattingProvider = "${cfg.lsp.formattingProvider}", cabalFormattingProvider = "cabal-fmt", }, }, }, + ${optionalString cfg.dap.enable '' dap = { - cmd = {"${getExe' haskellPackages.haskell-debug-adapter "haskell-debug-adapter"}"}, + cmd = ${ + if isList cfg.dap.package + then toLuaObject cfg.dap.package + else ''{"${cfg.dap.package}/bin/haskell-debug-adapter"}'' }, + }, + ''} } ''; }; diff --git a/modules/plugins/lsp/presets/default.nix b/modules/plugins/lsp/presets/default.nix index f5f71c35..98dee73d 100644 --- a/modules/plugins/lsp/presets/default.nix +++ b/modules/plugins/lsp/presets/default.nix @@ -22,6 +22,7 @@ ./gopls.nix ./harper.nix ./helm-ls.nix + ./hls.nix ./intelephense.nix ./jdt-language-server.nix ./jinja-lsp.nix diff --git a/modules/plugins/lsp/presets/hls.nix b/modules/plugins/lsp/presets/hls.nix new file mode 100644 index 00000000..11f4dd44 --- /dev/null +++ b/modules/plugins/lsp/presets/hls.nix @@ -0,0 +1,42 @@ +{ + config, + lib, + pkgs, + ... +}: let + inherit (lib.meta) getExe'; + inherit (lib.modules) mkDefault mkIf; + inherit (lib.nvim.types) mkLspPresetEnableOption; + inherit (lib.generators) mkLuaInline; + + cfg = config.vim.lsp.presets.hls; +in { + options.vim.lsp.presets.hls = { + enable = mkLspPresetEnableOption "hls" "Haskell" []; + }; + + config = mkIf cfg.enable { + vim.lsp.servers.hls = { + enable = true; + cmd = [(getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") "--lsp"]; + filetypes = ["haskell" "lhaskell"]; + root_dir = + mkLuaInline + /* + lua + */ + '' + function(bufnr, on_dir) + local fname = vim.api.nvim_buf_get_name(bufnr) + on_dir(util.root_pattern('hie.yaml', 'stack.yaml', 'cabal.project', '*.cabal', 'package.yaml')(fname)) + end + ''; + settings = { + haskell = { + formattingProvider = mkDefault "ormolu"; + cabalFormattingProvider = "cabal-fmt"; + }; + }; + }; + }; +} From a97aa7d72485f94ad19cf35a61da436276bb86fc Mon Sep 17 00:00:00 2001 From: dathegreat Date: Mon, 27 Apr 2026 10:16:08 -0600 Subject: [PATCH 04/11] languages/haskell.nix: address PR comments - Rename hls -> haskell-language-server to match the nixpkgs name - Remove unneeded injection - Only set lsp filetypes in the language module, not the preset - Remove unneeded `mkDefault` - Use `root_markers` instead of yucky inline Lua --- docs/manual/release-notes/rl-0.9.md | 6 +-- modules/plugins/languages/haskell.nix | 6 ++- modules/plugins/lsp/presets/default.nix | 2 +- .../lsp/presets/haskell-language-server.nix | 30 +++++++++++++ modules/plugins/lsp/presets/hls.nix | 42 ------------------- 5 files changed, 38 insertions(+), 48 deletions(-) create mode 100644 modules/plugins/lsp/presets/haskell-language-server.nix delete mode 100644 modules/plugins/lsp/presets/hls.nix diff --git a/docs/manual/release-notes/rl-0.9.md b/docs/manual/release-notes/rl-0.9.md index 870b3d32..15685e8b 100644 --- a/docs/manual/release-notes/rl-0.9.md +++ b/docs/manual/release-notes/rl-0.9.md @@ -204,9 +204,9 @@ [dathegreat](https://github.com/dathegreat): - Fixed invalid keys in the haskell-tools configuration -- Changed the default haskell formatter to - [fourmolu](https://github.com/fourmolu/fourmolu), matching the - [haskell-tools default](https://github.com/mrcjkb/haskell-tools.nvim/blob/9ea030aa67f3875753e70e1eb59701f7020479a0/lua/haskell-tools/config/internal.lua#L131) +- Split haskell configuration into `lsp/presets/haskell-language-server.nix` and + `languages/haskell.nix` +- Made the haskell LSP and formatter configurable [alfarel](https://github.com/alfarelcynthesis): diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index c4546640..90cc2d38 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -18,9 +18,9 @@ cfg = config.vim.languages.haskell; - defaultServers = ["hls"]; + defaultServers = ["haskell-language-server"]; servers = { - hls = {}; + haskell-language-server = {}; }; in { options.vim.languages.haskell = { @@ -84,6 +84,8 @@ in { }; }) + # haskell-tools prefers to manage the lsp directly, + # so we only configure the lsp ourselves if haskell-tools is disabled (mkIf (cfg.lsp.enable && !cfg.extensions.haskell-tools.enable) { vim.lsp = { presets = genAttrs cfg.lsp.servers (_: {enable = true;}); diff --git a/modules/plugins/lsp/presets/default.nix b/modules/plugins/lsp/presets/default.nix index 98dee73d..8c43b61e 100644 --- a/modules/plugins/lsp/presets/default.nix +++ b/modules/plugins/lsp/presets/default.nix @@ -22,7 +22,7 @@ ./gopls.nix ./harper.nix ./helm-ls.nix - ./hls.nix + ./haskell-language-server.nix ./intelephense.nix ./jdt-language-server.nix ./jinja-lsp.nix diff --git a/modules/plugins/lsp/presets/haskell-language-server.nix b/modules/plugins/lsp/presets/haskell-language-server.nix new file mode 100644 index 00000000..a9ed0980 --- /dev/null +++ b/modules/plugins/lsp/presets/haskell-language-server.nix @@ -0,0 +1,30 @@ +{ + config, + lib, + pkgs, + ... +}: let + inherit (lib.meta) getExe'; + inherit (lib.modules) mkIf; + inherit (lib.nvim.types) mkLspPresetEnableOption; + + cfg = config.vim.lsp.presets.haskell-language-server; +in { + options.vim.lsp.presets.haskell-language-server = { + enable = mkLspPresetEnableOption "haskell-language-server" "Haskell" []; + }; + + config = mkIf cfg.enable { + vim.lsp.servers.haskell-language-server = { + enable = true; + cmd = [(getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") "--lsp"]; + root_markers = ["hie.yaml" "stack.yaml" "cabal.project" "*.cabal" "package.yaml"]; + settings = { + haskell = { + formattingProvider = "ormolu"; + cabalFormattingProvider = "cabal-fmt"; + }; + }; + }; + }; +} diff --git a/modules/plugins/lsp/presets/hls.nix b/modules/plugins/lsp/presets/hls.nix deleted file mode 100644 index 11f4dd44..00000000 --- a/modules/plugins/lsp/presets/hls.nix +++ /dev/null @@ -1,42 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: let - inherit (lib.meta) getExe'; - inherit (lib.modules) mkDefault mkIf; - inherit (lib.nvim.types) mkLspPresetEnableOption; - inherit (lib.generators) mkLuaInline; - - cfg = config.vim.lsp.presets.hls; -in { - options.vim.lsp.presets.hls = { - enable = mkLspPresetEnableOption "hls" "Haskell" []; - }; - - config = mkIf cfg.enable { - vim.lsp.servers.hls = { - enable = true; - cmd = [(getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") "--lsp"]; - filetypes = ["haskell" "lhaskell"]; - root_dir = - mkLuaInline - /* - lua - */ - '' - function(bufnr, on_dir) - local fname = vim.api.nvim_buf_get_name(bufnr) - on_dir(util.root_pattern('hie.yaml', 'stack.yaml', 'cabal.project', '*.cabal', 'package.yaml')(fname)) - end - ''; - settings = { - haskell = { - formattingProvider = mkDefault "ormolu"; - cabalFormattingProvider = "cabal-fmt"; - }; - }; - }; - }; -} From f15c3f377a35a2869d056c3c176c1b2cc2c6ad61 Mon Sep 17 00:00:00 2001 From: dathegreat Date: Mon, 27 Apr 2026 11:54:15 -0600 Subject: [PATCH 05/11] lsp/presets/haskell-language-server.nix: Add cabal-fmt as an extra package if lsp is enabled Otherwise, formatting a .cabal file throws an error that cabal-fmt can't be found. This is especially annoying if saving => formatting, because each save throws an error which must be escaped. --- modules/plugins/languages/haskell.nix | 1 + modules/plugins/lsp/presets/haskell-language-server.nix | 1 + 2 files changed, 2 insertions(+) diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index 90cc2d38..2ea05fd8 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -98,6 +98,7 @@ in { (mkIf cfg.extensions.haskell-tools.enable { vim = { + extraPackages = [haskellPackages.cabal-fmt]; startPlugins = ["haskell-tools-nvim"]; luaConfigRC.haskell-tools-nvim = entryAfter ["lsp-servers"] '' vim.g.haskell_tools = { diff --git a/modules/plugins/lsp/presets/haskell-language-server.nix b/modules/plugins/lsp/presets/haskell-language-server.nix index a9ed0980..cd63e086 100644 --- a/modules/plugins/lsp/presets/haskell-language-server.nix +++ b/modules/plugins/lsp/presets/haskell-language-server.nix @@ -15,6 +15,7 @@ in { }; config = mkIf cfg.enable { + vim.extraPackages = [pkgs.haskellPackages.cabal-fmt]; vim.lsp.servers.haskell-language-server = { enable = true; cmd = [(getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") "--lsp"]; From b2d6a252cea5d738e017daa68ccaa39243ab0e59 Mon Sep 17 00:00:00 2001 From: dathegreat Date: Thu, 7 May 2026 14:54:27 -0600 Subject: [PATCH 06/11] languages/haskell.nix: Address PR comments - Move formatting responsibility to conform-nvim, instead of HLS - Wrap formatter binaries separately now that we are not using the HLS-bundled libraries - Add helpful assertion message to enforce the "not HLS if haskell-tools" invariant. - Add helpful assertion message to enforce the "not DAP if not haskell-tools" invariant. --- modules/plugins/languages/haskell.nix | 80 +++++++++++++++---- .../lsp/presets/haskell-language-server.nix | 19 ++++- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index 2ea05fd8..a9974f5f 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -10,17 +10,24 @@ inherit (lib.options) mkEnableOption mkOption literalExpression; inherit (lib.strings) optionalString; inherit (lib.modules) mkIf mkMerge; - inherit (lib.nvim.types) mkGrammarOption; + inherit (lib.nvim.types) mkGrammarOption deprecatedSingleOrListOf; inherit (lib.nvim.lua) toLuaObject; inherit (lib.nvim.dag) entryAfter; - inherit (lib.meta) getExe'; + inherit (lib.nvim.attrsets) mapListToAttrs; + inherit (lib.meta) getExe getExe'; inherit (pkgs) haskellPackages; cfg = config.vim.languages.haskell; defaultServers = ["haskell-language-server"]; - servers = { - haskell-language-server = {}; + servers = ["haskell-language-server"]; + + defaultFormat = ["ormolu"]; + formats = { + ormolu = {command = getExe haskellPackages.ormolu;}; + fourmolu = {command = getExe haskellPackages.fourmolu;}; + stylish-haskell = {command = getExe haskellPackages.stylish-haskell;}; + floskell = {command = getExe haskellPackages.floskell;}; }; in { options.vim.languages.haskell = { @@ -44,14 +51,23 @@ in { defaultText = literalExpression "config.vim.lsp.enable"; }; servers = mkOption { - type = listOf (enum (attrNames servers)); + type = listOf (enum servers); default = defaultServers; description = "Haskell LSP server to use"; }; - formattingProvider = mkOption { - type = enum ["ormolu" "fourmolu" "stylish-haskell" "brittany" "floskell" "none"]; - default = "ormolu"; - description = "Formatter used by HLS"; + }; + + format = { + enable = + mkEnableOption "Haskell formatting" + // { + default = config.vim.languages.enableFormat; + defaultText = literalExpression "config.vim.languages.enableFormat"; + }; + type = mkOption { + type = deprecatedSingleOrListOf "vim.languages.haskell.format.type" (enum (attrNames formats)); + default = defaultFormat; + description = "Haskell formatter to use"; }; }; @@ -77,6 +93,27 @@ in { }; config = mkIf cfg.enable (mkMerge [ + { + assertions = [ + { + assertion = !(cfg.lsp.enable && cfg.extensions.haskell-tools.enable); + message = '' + vim.languages.haskell: haskell-tools.nvim manages the LSP directly and + is incompatible with vim.languages.haskell.lsp.enable. Disable one or + the other. See https://github.com/mrcjkb/haskell-tools.nvim/blob/fe9ed6e6adfa6311e06c84569d8536190f172030/doc/haskell-tools.txt#L22 + ''; + } + { + assertion = !(cfg.dap.enable && !cfg.extensions.haskell-tools.enable); + message = '' + vim.languages.haskell: DAP support requires haskell-tools.nvim, which + handles adapter registration and launch configuration discovery. + Enable vim.languages.haskell.extensions.haskell-tools to use DAP. + ''; + } + ]; + } + (mkIf cfg.treesitter.enable { vim.treesitter = { enable = true; @@ -84,18 +121,33 @@ in { }; }) - # haskell-tools prefers to manage the lsp directly, - # so we only configure the lsp ourselves if haskell-tools is disabled - (mkIf (cfg.lsp.enable && !cfg.extensions.haskell-tools.enable) { + (mkIf cfg.lsp.enable { vim.lsp = { presets = genAttrs cfg.lsp.servers (_: {enable = true;}); servers = genAttrs cfg.lsp.servers (_: { filetypes = ["haskell" "lhaskell"]; - settings.haskell.formattingProvider = cfg.lsp.formattingProvider; }); }; }) + (mkIf cfg.format.enable { + vim.formatter.conform-nvim = { + enable = true; + setupOpts = { + formatters_by_ft = { + haskell = cfg.format.type; + lhaskell = cfg.format.type; + }; + formatters = + mapListToAttrs (name: { + inherit name; + value = formats.${name}; + }) + cfg.format.type; + }; + }; + }) + (mkIf cfg.extensions.haskell-tools.enable { vim = { extraPackages = [haskellPackages.cabal-fmt]; @@ -129,7 +181,7 @@ in { end, settings = { haskell = { - formattingProvider = "${cfg.lsp.formattingProvider}", + formattingProvider = "none", cabalFormattingProvider = "cabal-fmt", }, }, diff --git a/modules/plugins/lsp/presets/haskell-language-server.nix b/modules/plugins/lsp/presets/haskell-language-server.nix index cd63e086..0ae1598e 100644 --- a/modules/plugins/lsp/presets/haskell-language-server.nix +++ b/modules/plugins/lsp/presets/haskell-language-server.nix @@ -15,14 +15,27 @@ in { }; config = mkIf cfg.enable { - vim.extraPackages = [pkgs.haskellPackages.cabal-fmt]; vim.lsp.servers.haskell-language-server = { enable = true; - cmd = [(getExe' pkgs.haskellPackages.haskell-language-server "haskell-language-server-wrapper") "--lsp"]; + cmd = [ + (getExe' (pkgs.symlinkJoin { + name = "haskell-language-server-wrapper"; + paths = [pkgs.haskellPackages.haskell-language-server]; + meta.mainProgram = "haskell-language-server-wrapper"; + buildInputs = [pkgs.makeBinaryWrapper]; + postBuild = '' + wrapProgram $out/bin/haskell-language-server-wrapper \ + --prefix PATH : ${pkgs.haskellPackages.cabal-fmt}/bin + ''; + }) "haskell-language-server-wrapper") + "--lsp" + ]; root_markers = ["hie.yaml" "stack.yaml" "cabal.project" "*.cabal" "package.yaml"]; settings = { haskell = { - formattingProvider = "ormolu"; + # formatting is handled by conform-nvim; disable HLS's built-in formatter + formattingProvider = "none"; + # cabal-fmt is an external tool; it is wrapped into the LSP binary's PATH above cabalFormattingProvider = "cabal-fmt"; }; }; From 29d37d9692c128a6a3bdf96ce3db7a92ccdf16b4 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Thu, 7 May 2026 23:44:41 +0200 Subject: [PATCH 07/11] languages/haskell: haskell-tools setupOpts refactor --- modules/plugins/languages/haskell.nix | 111 ++++++++++++++------------ 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index a9974f5f..6df47a44 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -4,17 +4,15 @@ pkgs, ... }: let - inherit (builtins) attrNames isList; + inherit (builtins) attrNames; inherit (lib) genAttrs; - inherit (lib.types) either package enum listOf str; + inherit (lib.types) either package enum listOf str nullOr; inherit (lib.options) mkEnableOption mkOption literalExpression; - inherit (lib.strings) optionalString; inherit (lib.modules) mkIf mkMerge; - inherit (lib.nvim.types) mkGrammarOption deprecatedSingleOrListOf; - inherit (lib.nvim.lua) toLuaObject; - inherit (lib.nvim.dag) entryAfter; + inherit (lib.nvim.types) mkGrammarOption deprecatedSingleOrListOf mkPluginSetupOption luaInline; inherit (lib.nvim.attrsets) mapListToAttrs; - inherit (lib.meta) getExe getExe'; + inherit (lib.meta) getExe; + inherit (lib.generators) mkLuaInline; inherit (pkgs) haskellPackages; cfg = config.vim.languages.haskell; @@ -88,6 +86,59 @@ in { extensions = { haskell-tools = { enable = mkEnableOption "haskell-tools.nvim"; + + setupOpts = mkPluginSetupOption "haskell-tools.nvim" { + hls = { + cmd = mkOption { + type = nullOr (listOf str); + default = [ + "${pkgs.haskellPackages.haskell-language-server}/bin/haskell-language-server" + "--lsp" + ]; + description = '' + Command for haskell-language-server. + + > [!NOTE] + > + > Since HLS is very sensitive about the GHC version, there's a + > very high chance that the default HLS we use is not + > compatible with your project. It is highly recommended to set + > this option to `null` and install HLS separately in a + > `devShell`. + ''; + }; + on_attach = mkOption { + type = nullOr luaInline; + description = "Function to run when HLS is attached"; + default = mkLuaInline '' + function(client, bufnr) + local ht = require("haskell-tools") + local opts = { noremap = true, silent = true, buffer = bufnr } + vim.keymap.set('n', 'cl', vim.lsp.codelens.run, opts) + vim.keymap.set('n', 'hs', ht.hoogle.hoogle_signature, opts) + vim.keymap.set('n', 'ea', ht.lsp.buf_eval_all, opts) + vim.keymap.set('n', 'rr', function() + vim.cmd('Haskell repl toggle') + end, opts) + vim.keymap.set('n', 'rf', function() + vim.cmd('Haskell repl toggle ' .. vim.api.nvim_buf_get_name(0)) + end, opts) + vim.keymap.set('n', 'rq', function() + vim.cmd('Haskell repl quit') + end, opts) + end + ''; + }; + }; + + dap = { + cmd = mkOption { + type = nullOr (listOf str); + default = null; + description = "Debug adapter command"; + }; + }; + }; }; }; }; @@ -152,51 +203,7 @@ in { vim = { extraPackages = [haskellPackages.cabal-fmt]; startPlugins = ["haskell-tools-nvim"]; - luaConfigRC.haskell-tools-nvim = entryAfter ["lsp-servers"] '' - vim.g.haskell_tools = { - tools = { - hover = { - stylize_markdown = false, - auto_focus = false, - }, - }, - hls = { - auto_attach = true, - cmd = {"${getExe' haskellPackages.haskell-language-server "haskell-language-server-wrapper"}", "--lsp"}, - on_attach = function(client, bufnr) - local ht = require("haskell-tools") - local opts = { noremap = true, silent = true, buffer = bufnr } - vim.keymap.set('n', 'cl', vim.lsp.codelens.run, opts) - vim.keymap.set('n', 'hs', ht.hoogle.hoogle_signature, opts) - vim.keymap.set('n', 'ea', ht.lsp.buf_eval_all, opts) - vim.keymap.set('n', 'rr', function() - vim.cmd('Haskell repl toggle') - end, opts) - vim.keymap.set('n', 'rf', function() - vim.cmd('Haskell repl toggle ' .. vim.api.nvim_buf_get_name(0)) - end, opts) - vim.keymap.set('n', 'rq', function() - vim.cmd('Haskell repl quit') - end, opts) - end, - settings = { - haskell = { - formattingProvider = "none", - cabalFormattingProvider = "cabal-fmt", - }, - }, - }, - ${optionalString cfg.dap.enable '' - dap = { - cmd = ${ - if isList cfg.dap.package - then toLuaObject cfg.dap.package - else ''{"${cfg.dap.package}/bin/haskell-debug-adapter"}'' - }, - }, - ''} - } - ''; + globals.haskell_tools = cfg.extensions.haskell-tools.setupOpts; }; }) ]); From 63876fb1f24c22ec9b9726987378137d80a43372 Mon Sep 17 00:00:00 2001 From: "D.A. Marcyes" <91547826+dathegreat@users.noreply.github.com> Date: Fri, 8 May 2026 09:33:31 -0600 Subject: [PATCH 08/11] presets/haskell-language-server.nix: Remove redundant `formattingProvider = "none"` Conform will ignore the LSP formatter anyway. Co-authored-by: Ching Pei Yang <59727193+horriblename@users.noreply.github.com> --- modules/plugins/lsp/presets/haskell-language-server.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/plugins/lsp/presets/haskell-language-server.nix b/modules/plugins/lsp/presets/haskell-language-server.nix index 0ae1598e..572ed13f 100644 --- a/modules/plugins/lsp/presets/haskell-language-server.nix +++ b/modules/plugins/lsp/presets/haskell-language-server.nix @@ -33,8 +33,6 @@ in { root_markers = ["hie.yaml" "stack.yaml" "cabal.project" "*.cabal" "package.yaml"]; settings = { haskell = { - # formatting is handled by conform-nvim; disable HLS's built-in formatter - formattingProvider = "none"; # cabal-fmt is an external tool; it is wrapped into the LSP binary's PATH above cabalFormattingProvider = "cabal-fmt"; }; From dfaa9b4c5a708de3155b4f748466a1c2dde9f635 Mon Sep 17 00:00:00 2001 From: dathegreat Date: Fri, 8 May 2026 10:59:37 -0600 Subject: [PATCH 09/11] languages/haskell.nix: Add haskell-tools keymaps to the extension opts They were previously set in plain lua, which made them difficult / impossible to override --- modules/plugins/languages/haskell.nix | 68 +++++++++++++++++++-------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index 6df47a44..f3bb54e9 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -1,6 +1,7 @@ { config, lib, + options, pkgs, ... }: let @@ -9,6 +10,9 @@ inherit (lib.types) either package enum listOf str nullOr; inherit (lib.options) mkEnableOption mkOption literalExpression; inherit (lib.modules) mkIf mkMerge; + inherit (config.vim.lib) mkMappingOption; + inherit (lib.nvim.lua) toLuaObject; + inherit (lib.nvim.binds) addDescriptionsToMappings; inherit (lib.nvim.types) mkGrammarOption deprecatedSingleOrListOf mkPluginSetupOption luaInline; inherit (lib.nvim.attrsets) mapListToAttrs; inherit (lib.meta) getExe; @@ -87,6 +91,15 @@ in { haskell-tools = { enable = mkEnableOption "haskell-tools.nvim"; + mappings = { + codeLensRun = mkMappingOption "Run code lens [haskell-tools.nvim]" "cl"; + hoogleSignature = mkMappingOption "Hoogle signature [haskell-tools.nvim]" "hs"; + evalAll = mkMappingOption "Evaluate all [haskell-tools.nvim]" "ea"; + replToggle = mkMappingOption "Toggle REPL [haskell-tools.nvim]" "rr"; + replToggleFile = mkMappingOption "Toggle REPL for current file [haskell-tools.nvim]" "rf"; + replQuit = mkMappingOption "Quit REPL [haskell-tools.nvim]" "rq"; + }; + setupOpts = mkPluginSetupOption "haskell-tools.nvim" { hls = { cmd = mkOption { @@ -109,25 +122,9 @@ in { }; on_attach = mkOption { type = nullOr luaInline; - description = "Function to run when HLS is attached"; - default = mkLuaInline '' - function(client, bufnr) - local ht = require("haskell-tools") - local opts = { noremap = true, silent = true, buffer = bufnr } - vim.keymap.set('n', 'cl', vim.lsp.codelens.run, opts) - vim.keymap.set('n', 'hs', ht.hoogle.hoogle_signature, opts) - vim.keymap.set('n', 'ea', ht.lsp.buf_eval_all, opts) - vim.keymap.set('n', 'rr', function() - vim.cmd('Haskell repl toggle') - end, opts) - vim.keymap.set('n', 'rf', function() - vim.cmd('Haskell repl toggle ' .. vim.api.nvim_buf_get_name(0)) - end, opts) - vim.keymap.set('n', 'rq', function() - vim.cmd('Haskell repl quit') - end, opts) - end - ''; + description = "Function to run when HLS is attached. When null, mappings from the mappings option are used."; + default = null; + defaultText = literalExpression "Generated from vim.languages.haskell.extensions.haskell-tools.mappings"; }; }; @@ -203,7 +200,38 @@ in { vim = { extraPackages = [haskellPackages.cabal-fmt]; startPlugins = ["haskell-tools-nvim"]; - globals.haskell_tools = cfg.extensions.haskell-tools.setupOpts; + globals.haskell_tools = let + htCfg = cfg.extensions.haskell-tools; + keymapDefinitions = options.vim.languages.haskell.extensions.haskell-tools.mappings; + mappings = addDescriptionsToMappings htCfg.mappings keymapDefinitions; + mkBinding = binding: action: + if binding.value != null + then "vim.keymap.set('n', ${toLuaObject binding.value}, ${action}, {buffer=bufnr, noremap=true, silent=true, desc=${toLuaObject binding.description}})" + else ""; + generatedOnAttach = mkLuaInline '' + function(client, bufnr) + local ht = require("haskell-tools") + ${mkBinding mappings.codeLensRun "vim.lsp.codelens.run"} + ${mkBinding mappings.hoogleSignature "ht.hoogle.hoogle_signature"} + ${mkBinding mappings.evalAll "ht.lsp.buf_eval_all"} + ${mkBinding mappings.replToggle "function() vim.cmd('Haskell repl toggle') end"} + ${mkBinding mappings.replToggleFile "function() vim.cmd('Haskell repl toggle ' .. vim.api.nvim_buf_get_name(0)) end"} + ${mkBinding mappings.replQuit "function() vim.cmd('Haskell repl quit') end"} + end + ''; + effectiveOnAttach = + if htCfg.setupOpts.hls.on_attach != null + then htCfg.setupOpts.hls.on_attach + else generatedOnAttach; + in + htCfg.setupOpts + // { + hls = + htCfg.setupOpts.hls + // { + on_attach = effectiveOnAttach; + }; + }; }; }) ]); From 8a0073182f7b845e74e951af318a166177162d62 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Sun, 10 May 2026 23:19:39 +0200 Subject: [PATCH 10/11] languages/haskell: simplify attach function --- modules/plugins/languages/haskell.nix | 55 ++++++++++++--------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/modules/plugins/languages/haskell.nix b/modules/plugins/languages/haskell.nix index f3bb54e9..737b511d 100644 --- a/modules/plugins/languages/haskell.nix +++ b/modules/plugins/languages/haskell.nix @@ -200,38 +200,31 @@ in { vim = { extraPackages = [haskellPackages.cabal-fmt]; startPlugins = ["haskell-tools-nvim"]; - globals.haskell_tools = let - htCfg = cfg.extensions.haskell-tools; - keymapDefinitions = options.vim.languages.haskell.extensions.haskell-tools.mappings; - mappings = addDescriptionsToMappings htCfg.mappings keymapDefinitions; - mkBinding = binding: action: - if binding.value != null - then "vim.keymap.set('n', ${toLuaObject binding.value}, ${action}, {buffer=bufnr, noremap=true, silent=true, desc=${toLuaObject binding.description}})" - else ""; - generatedOnAttach = mkLuaInline '' - function(client, bufnr) - local ht = require("haskell-tools") - ${mkBinding mappings.codeLensRun "vim.lsp.codelens.run"} - ${mkBinding mappings.hoogleSignature "ht.hoogle.hoogle_signature"} - ${mkBinding mappings.evalAll "ht.lsp.buf_eval_all"} - ${mkBinding mappings.replToggle "function() vim.cmd('Haskell repl toggle') end"} - ${mkBinding mappings.replToggleFile "function() vim.cmd('Haskell repl toggle ' .. vim.api.nvim_buf_get_name(0)) end"} - ${mkBinding mappings.replQuit "function() vim.cmd('Haskell repl quit') end"} - end - ''; - effectiveOnAttach = - if htCfg.setupOpts.hls.on_attach != null - then htCfg.setupOpts.hls.on_attach - else generatedOnAttach; - in - htCfg.setupOpts - // { - hls = - htCfg.setupOpts.hls - // { - on_attach = effectiveOnAttach; - }; + globals.haskell_tools = cfg.extensions.haskell-tools.setupOpts; + languages.haskell.extensions.haskell-tools.setupOpts = { + hls = { + on_attach = let + htCfg = cfg.extensions.haskell-tools; + keymapDefinitions = options.vim.languages.haskell.extensions.haskell-tools.mappings; + mappings = addDescriptionsToMappings htCfg.mappings keymapDefinitions; + mkBinding = binding: action: + if binding.value != null + then "vim.keymap.set('n', ${toLuaObject binding.value}, ${action}, {buffer=bufnr, noremap=true, silent=true, desc=${toLuaObject binding.description}})" + else ""; + in + mkLuaInline '' + function(client, bufnr) + local ht = require("haskell-tools") + ${mkBinding mappings.codeLensRun "vim.lsp.codelens.run"} + ${mkBinding mappings.hoogleSignature "ht.hoogle.hoogle_signature"} + ${mkBinding mappings.evalAll "ht.lsp.buf_eval_all"} + ${mkBinding mappings.replToggle "function() vim.cmd('Haskell repl toggle') end"} + ${mkBinding mappings.replToggleFile "function() vim.cmd('Haskell repl toggle ' .. vim.api.nvim_buf_get_name(0)) end"} + ${mkBinding mappings.replQuit "function() vim.cmd('Haskell repl quit') end"} + end + ''; }; + }; }; }) ]); From 2890e99c72570801c62c807882d97fd3a1114e48 Mon Sep 17 00:00:00 2001 From: Ching Pei Yang Date: Sun, 10 May 2026 23:30:12 +0200 Subject: [PATCH 11/11] doc: add breaking change in haskell LSP --- docs/manual/release-notes/rl-0.9.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/manual/release-notes/rl-0.9.md b/docs/manual/release-notes/rl-0.9.md index 15685e8b..805bacf1 100644 --- a/docs/manual/release-notes/rl-0.9.md +++ b/docs/manual/release-notes/rl-0.9.md @@ -39,6 +39,9 @@ `setupOpts.strategies.chat.adapter`, rename them to `setupOpts.interactions.chat.adapter`. +- Haskell LSP now defaults to haskell-language-server, haskell-tools based LSP + support is moved to `vim.languages.haskell.extensions.haskell-tools` + [Snoweuph](https://github.com/snoweuph) - Remove `mind.nvim`. This plugin doesn't exist anymore. The original author