It is assumed that Neovim, lazy.nvim, and Telescope are already configured.
Install Haskell tools
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
ghcup install ghc
ghcup set ghc recommended
ghcup install cabal
ghcup install hls
ghcup install stack
Restart your shell, then install Hoogle and generate the local Hoogle database:
cabal update
cabal install hoogle
hoogle generate
Verify the installation:
ghc --version
cabal --version
haskell-language-server-wrapper --version
hoogle --version
haskell-tools.nvim uses haskell-language-server-wrapper for LSP and the local Hoogle database generated by the hoogle generate command.
Configure haskell-tools.nvim
{
"mrcjkb/haskell-tools.nvim",
version = "^10",
lazy = false,
init = function()
vim.g.haskell_tools = {
tools = {
hoogle = {
mode = "telescope-local",
},
repl = {
handler = "builtin",
},
hover = {
auto_focus = true,
},
},
}
end,
}
haskell-tools.nvim starts HLS by itself. Do not configure HLS separately through nvim-lspconfig or Mason.
haskell-tools.nvim currently requires Neovim 0.12 or later. If you use Neovim 0.11 or an earlier release, install a compatible older version of the plugin instead. The version = "^10" constraint should only be used after confirming that version 10 supports your Neovim version.
Optional Telescope Haskell Tools extension
After configuring Telescope, load the ht extension:
telescope.setup(...)
telescope.load_extension("ht")
It provides Telescope search commands for the current Haskell package:
:Telescope ht package_files
:Telescope ht package_grep
Add Treesitter support for Haskell
Add Haskell to your existing Tree-sitter configuration:
return {
{
"nvim-treesitter/nvim-treesitter",
-- ...
config = function()
local ts = require("nvim-treesitter")
ts.setup()
ts.install({
-- other languages...
"haskell",
"haskell_persistent",
})
-- configure indentation for Haskell filetypes
vim.api.nvim_create_autocmd("FileType", {
pattern = {
-- other filetypes...
"haskell",
},
callback = function()
pcall(vim.treesitter.start)
end,
})
-- rest of your config...
end,
-- rest of your plugin spec...
},
}
Add Haskell key mappings
The mappings are buffer-local and only apply to Haskell files.
-- ftplugin/haskell.lua
local ht = require("haskell-tools")
local map = function(mode, key, action, desc)
vim.keymap.set(mode, key, action, {
noremap = true,
silent = true,
buffer = 0,
desc = desc,
})
end
local toggle_file_repl = function()
ht.repl.toggle(vim.api.nvim_buf_get_name(0))
end
map("n", "gd", vim.lsp.buf.definition, "Go to definition")
map("n", "gr", vim.lsp.buf.references, "Find references")
map("n", "<leader>lh", "<cmd>Haskell hover<CR>", "Hover actions")
map("n", "<leader>hs", ht.hoogle.hoogle_signature, "Hoogle signature")
map("n", "<leader>hc", vim.lsp.codelens.run, "Run CodeLens")
map("n", "<leader>he", ht.lsp.buf_eval_all, "Evaluate snippets")
map("n", "<leader>hp", ht.repl.toggle, "Toggle project REPL")
map("n", "<leader>hf", toggle_file_repl, "Toggle file REPL")
map("n", "<leader>hq", ht.repl.quit, "Quit REPL")
Debugging
Install DAP
ghci-dap must be built with the same GHC version used by the project. Otherwise GHC cannot load the projectβs interface files and fails with mismatched interface file versions. Verify compatibility before debugging with ghci-dap --version, and use the binary built by the projectβs selected build tool and GHC version.
# cabal
cabal install ghci-dap haskell-debug-adapter
# stack, run from the project root
stack install haskell-dap ghci-dap haskell-debug-adapter
haskell-debug-adapter --version
ghci-dap --version
haskell-debug-adapter0.0.42.0 may fail to build withhie-bios0.20 or later because of an API incompatibility. If Cabal reports aTargetWithContexttype error, constrainhie-bios:cabal install ghci-dap haskell-debug-adapter \ --constraint='hie-bios < 0.20'
Plugins configuration
-- lua/plugins/dap.lua
return {
{
"mfussenegger/nvim-dap",
cmd = { "DapContinue", "DapToggleBreakpoint", "DapStepInto" },
},
{
"rcarriga/nvim-dap-ui",
ft = { "haskell" },
dependencies = { "mfussenegger/nvim-dap", 'nvim-neotest/nvim-nio' },
config = function()
local function map(lhs, rhs, desc)
vim.keymap.set("n", lhs, rhs, { noremap = true, silent = true, desc = desc })
end
local dap = require 'dap'
local dapui = require 'dapui'
local widgets = require 'dap.ui.widgets'
dapui.setup {
layouts = {
{
elements = {
-- { id = "repl", size = 0.30 },
{ id = "scopes", size = 0.25 },
{ id = "breakpoints", size = 0.25 },
{ id = "stacks", size = 0.25 },
{ id = "watches", size = 0.25 },
},
position = "right",
size = 50
},
{
elements = {
{ id = "repl", size = 1 }
},
position = "bottom",
size = 20
},
{
elements = {
{ id = "console", size = 1 }
},
position = "bottom",
size = 25
},
},
}
dap.listeners.before.attach.dapui_config = function()
dapui.open()
end
dap.listeners.before.launch.dapui_config = function()
dapui.open()
end
dap.listeners.before.event_terminated.dapui_config = function()
-- dapui.close()
end
dap.listeners.before.event_exited.dapui_config = function()
-- dapui.close()
end
local set_conditional_breakpoint = function()
dap.set_breakpoint(vim.fn.input('Breakpoint condition: '))
end
local set_log_point = function()
dap.set_breakpoint(nil, nil, vim.fn.input('Log point message: '))
end
map('<leader>du', function() dapui.toggle { reset = true } end, 'Toggle dap ui')
map('<leader>dU', function() dapui.toggle { reset = true, layout = 1 } end, 'Toggle dap ui')
map("<leader>db", dap.toggle_breakpoint, "Set breakpoint")
map("<leader>dB", set_conditional_breakpoint, "Set conditional breakpoint")
map("<leader>dL", set_log_point, "Set log point")
map('<leader>dC', dap.clear_breakpoints, "Clear breakpoints")
map("<leader>dc", dap.continue, "Continue")
map("<leader>dn", dap.step_over, "Step over")
map("<leader>di", dap.step_into, "Step into")
map("<leader>do", dap.step_out, "Step out")
map('<leader>dd', dap.disconnect, "Disconnect")
map('<leader>dt', dap.terminate, "Terminate")
map("<leader>dr", dap.repl.toggle, "Open REPL")
map("<leader>dl", dap.run_last, "Run last")
map('<leader>dv', widgets.hover, "Variables")
map('<leader>ds', function() widgets.centered_float(widgets.scopes) end, "Scopes")
end
},
}
haskell-tools.nvim automatically discovers haskell-debug-adapter launch configurations from Cabal and Stack projects when nvim-dap is installed and haskell-debug-adapter is executable in PATH. No manual dap.adapters.ghc configuration is required.
Formatting
Install formatters
cabal install fourmolu stylish-haskell
fourmolu --version
stylish-haskell --version
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
-- Other formatters
haskell = { "stylish_haskell", "fourmolu" },
},
},
-- Rest of the configuration
}
stylish_haskell runs first to organize imports; fourmolu then formats the complete Haskell source file.
Install and verify
:Lazy sync
:TSInstall haskell haskell_persistent
:checkhealth haskell-tools



Top comments (0)