DEV Community

lbobylev
lbobylev

Posted on Edited on

Setting Up Haskell Development in Neovim

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
Enter fullscreen mode Exit fullscreen mode

Restart your shell, then install Hoogle and generate the local Hoogle database:

cabal update
cabal install hoogle
hoogle generate
Enter fullscreen mode Exit fullscreen mode

Verify the installation:

ghc --version
cabal --version
haskell-language-server-wrapper --version
hoogle --version
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

It provides Telescope search commands for the current Haskell package:

:Telescope ht package_files
:Telescope ht package_grep
Enter fullscreen mode Exit fullscreen mode

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...
    },
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

haskell-debug-adapter 0.0.42.0 may fail to build with hie-bios 0.20 or later because of an API incompatibility. If Cabal reports a TargetWithContext type error, constrain hie-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
    },
}
Enter fullscreen mode Exit fullscreen mode

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.

Haskell DAP

Formatting

Install formatters

cabal install fourmolu stylish-haskell
fourmolu --version
stylish-haskell --version
Enter fullscreen mode Exit fullscreen mode
{
    "stevearc/conform.nvim",
    opts = {
        formatters_by_ft = {
            -- Other formatters
            haskell = { "stylish_haskell", "fourmolu" },
        },
    },
    -- Rest of the configuration
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Hover actions

Haskell hover actions

File REPL

Haskell file REPL

Top comments (0)