[!NOTE]
- Just works. No need to call
setup!- No dependency on
lspconfig.- Lazy initialization by design.
If you are starting out with Haskell, nvim-lspconfig.hls
is probably enough for you.
It provides the lowest common denominator of LSP support.
This plugin is for those who would like additional features
that are specific to Haskell tooling.
neovim >= 0.11[!NOTE]
For versions that are compatible with older Neovim versions, see previous major version bumps in the changelog.
haskell-language-server
(recommended).telescope.nvim.hoogle
installation (recommended for better hoogle search performance).fast-tags
(for automatic tag generation as a fallback for
vim.lsp.tagfunc).haskell-debug-adapter
and nvim-dap.This plugin is available on LuaRocks:
:Rocks install haskell-tools.nvim
Example using lazy.nvim:
{
'mrcjkb/haskell-tools.nvim',
version = '^7', -- Recommended
lazy = false, -- This plugin is already lazy
}
[!TIP]
It is suggested to pin to tagged releases if you would like to avoid breaking changes.
To manually generate documentation, use :helptags ALL.
[!NOTE]
For NixOS users with flakes enabled, this project provides outputs in the form of a package and an overlay; use it as you wish in your NixOS or home-manager configuration. It is also available in
nixpkgs.
This plugin automatically configures the haskell-language-server builtin LSP
client and integrates with other haskell tools.
See the Features section for more info.
[!WARNING]
Do not call the
nvim-lspconfig.hlssetup or set up the lsp client forhaskell-language-servermanually, as doing so may cause conflicts.
This is a filetype plugin that works out of the box,
so there is no need to call a setup function or configure anything
to get this plugin working.
You will most likely want to add some keymaps.
Most keymaps are only useful in haskell and/or cabal files,
so I suggest you define them in ~/.config/nvim/after/ftplugin/haskell.lua[^1]
and/or ~/.config/nvim/after/ftplugin/cabal.lua[^1].
[^1]: See :help base-directories
Some suggestions:
-- ~/.config/nvim/after/ftplugin/haskell.lua
local ht = require('haskell-tools')
local bufnr = vim.api.nvim_get_current_buf()
local opts = { noremap = true, silent = true, buffer = bufnr, }
-- haskell-language-server relies heavily on codeLenses,
-- so auto-refresh (see advanced configuration) is enabled by default
vim.keymap.set('n', '<space>cl', vim.lsp.codelens.run, opts)
-- Hoogle search for the type signature of the definition under the cursor
vim.keymap.set('n', '<space>hs', ht.hoogle.hoogle_signature, opts)
-- Evaluate all code snippets
vim.keymap.set('n', '<space>ea', ht.lsp.buf_eval_all, opts)
-- Toggle a GHCi repl for the current package
vim.keymap.set('n', '<leader>rr', ht.repl.toggle, opts)
-- Toggle a GHCi repl for the current buffer
vim.keymap.set('n', '<leader>rf', function()
ht.repl.toggle(vim.api.nvim_buf_get_name(0))
end, opts)
vim.keymap.set('n', '<leader>rq', ht.repl.quit, opts)
[!TIP]
- For more LSP related keymaps, see the
nvim-lspconfigsuggestions.- If using a local
hoogleinstallation, follow these instructions to generate a database.- See the Advanced configuration section for more configuration options.
[!IMPORTANT]
- Do not set
vim.g.haskell_toolsinafter/ftplugin/haskell.lua, as the file is sourced after the plugin is initialized.
haskell-language-server client.haskell-language-server settings per project
from JSON files.haskell-language-server heavily relies on. Can be disabled.
haskell-language-server can evaluate code snippets using code lenses.
haskell-tools.nvim provides a require('haskell-tools').lsp.buf_eval_all()
shortcut to evaluate all of them at once.

Hoogle-search for signature
Search for the type signature under the cursor.
Falls back to the word under the cursor if the type signature cannot be determined.
Telescope keymaps:
<CR> to copy the selected entry (<name> :: <signature>) to the clipboard.<C-b> to open the selected entry's Hackage URL in a browser.<C-r> to replace the word under the cursor with the selected entry.require('haskell-tools').hoogle.hoogle_signature()

With the <C-r> keymap,
the Hoogle search telescope integration can be used to fill holes.

Start a GHCi repl for the current project / buffer.
cabal repl, stack ghci
or ghci) for your project.toggleterm.nvim.iron.nvim
(see advanced configuration).

This plugin adds the following hover actions (if available),
via the :Haskell hover command:
<Plug> mapping
because there can be multiple signatures to search for).<Plug>HaskellHoverActionDocs).<Plug>HaskellHoverActionSource).<Plug>HaskellHoverActionDefinition).<Plug>HaskellHoverActionTypeDefinition).<Plug>HaskellHoverActionReferences).You can invoke them by switching to the hover window and entering <CR>
on the respective line, or with a keymap for the respective <Plug> mapping.
If you do not wish to create a separate keymap for each <Plug> mapping,
you can also create a keymap for <Plug>HaskellHoverAction, which accepts
a <count> prefix as the (1-based) index of the hover action to invoke.
For example, if you set the following keymap:
vim.keymap.set('n', '<space>a', '<Plug>HaskellHoverAction')
you can invoke the third hover action with 3<space>a.
Additionally, the default behaviour of stylizing markdown is disabled. And the hover buffer's filetype is set to markdown, so that nvim-treesitter users can benefit from syntax highlighting of code snippets.
To override Neovim's built-in hover keymap with the haskell-tools hover actions,
you can add the following, e.g. in ftplugin/haskell.lua:
vim.keymap.set('n', 'K', function() vim.cmd.Haskell { 'hover' } end)

On attaching, Neovim's LSP client will set up tagfunc
to query the language server for locations to jump to.
If no location is found, it will fall back to a tags file.
If fast-tags is installed,
this plugin will set up autocmds to automatically generate tags:
This feature can be tweaked or disabled in the advanced configuration.
haskell-debug-adapter configurationsIf the nvim-dap plugin is installed,
haskell-tools.nvim will automatically discover haskell-debug-adapter
configurations.

[!NOTE]
haskell-debug-adapteris an experimental design and implementation of a debug adapter for Haskell.
For planned features, refer to the issues.
To modify the default configuration, set vim.g.haskell_tools.
:help haskell-tools.config for a detailed
documentation of all available configuration options.
You may need to run :helptags ALL if the documentation has not been installed.HTDefaultConfig).haskell-language-server settings
(including those not set by this plugin), run haskell-language-server generate-default-config.haskell-language-server documentation.vim.g.haskell_tools = {
---@type haskell-tools.tools.Opts
tools = {
-- ...
},
---@type haskell-tools.lsp.ClientOpts
---You can also configure these via `:h vim.lsp.config`,
--- with the "haskell-tools" key.
hls = {
---@param client number The LSP client ID.
---@param bufnr number The buffer number
---@param ht HaskellTools = require('haskell-tools')
on_attach = function(client, bufnr, ht)
-- Set keybindings, etc. here.
end,
-- ...
},
---@type haskell-tools.dap.Opts
dap = {
-- ...
},
}
[!TIP]
vim.g.haskell_toolscan also be a function that returns a table.
haskell-language-server settings per projectBy default, this plugin will look for a hls.json[^2] file
in the project root directory, and attempt to load it.
If the file does not exist, or it can't be decoded,
the hls.default_settings will be used.
[^2]: haskell-language-server can generate such a file with the generate-default-config CLI argument.
You can change this behaviour with the hls.settings config:
vim.g.haskell_tools = {
-- ...
hls = {
---@param project_root string Path to the project root
settings = function(project_root)
local ht = require('haskell-tools')
return ht.lsp.load_hls_settings(project_root, {
settings_file_pattern = 'hls.json'
})
end,
},
}
Some code lenses might be more interesting than others.
For example, the importLens could be annoying if you prefer to import
everything or use a custom prelude.
Individual code lenses can be turned off by disabling them in the respective
plugin configurations:
hls = {
settings = {
haskell = {
plugin = {
class = { -- missing class methods
codeLensOn = false,
},
importLens = { -- make import lists fully explicit
codeLensOn = false,
},
refineImports = { -- refine imports
codeLensOn = false,
},
tactics = { -- wingman
codeLensOn = false,
},
moduleName = { -- fix module names
globalOn = false,
},
eval = { -- evaluate code snippets
globalOn = false,
},
['ghcide-type-lenses'] = { -- show/add missing type signatures
globalOn = false,
},
},
},
},
},
[!NOTE]
Alternatively, you can dynamically enable/disable different code lenses per project.
haskell-language-server on Cabal filesSince version 1.9.0.0, haskell-language-server can launch on Cabal files,
but it does not support all features that it has for Haskell files.
You can add cabal-specific keymaps, etc. in ~/.config/nvim/after/ftplugin/cabal.lua.
iron.nvim to use haskell-tools.nvimDepends on iron.nvim/#300.
local iron = require("iron.core")
iron.setup {
config = {
repl_definition = {
haskell = {
command = function(meta)
local file = vim.api.nvim_buf_get_name(meta.current_bufnr)
-- call `require` in case iron is set up before haskell-tools
return require('haskell-tools').repl.mk_repl_cmd(file)
end,
},
},
},
}
haskell-debug-adapter launch configurationsThere are two ways this plugin will detect haskell-debug-adapter launch configurations:
launch.json
file in the project root.For a complete overview, enter :help haskell-tools in Neovim.
| Command | Description |
|---|---|
:Hls start |
Start the LSP client |
:Hls stop |
Stop the LSP client |
:Hls restart |
Restart the LSP client |
:Hls evalAll |
Evaluate all code snippets |
local ht = require('haskell-tools')
--- Start or attach the LSP client.
ht.lsp.start()
--- Stop the LSP client.
ht.lsp.stop()
--- Restart the LSP client.
ht.lsp.restart()
--- Callback for dynamically loading haskell-language-server settings
--- Falls back to the `hls.default_settings` if no file is found
--- or one is found, but it cannot be read or decoded.
--- @param project_root string? The project root
ht.lsp.load_hls_settings(project_root)
--- Evaluate all code snippets in comments
ht.lsp.buf_eval_all()
local ht = require('haskell-tools')
--- Run a hoogle signature search for the value under the cursor
ht.hoogle.hoogle_signature()
| Command | Description | Arguments |
|---|---|---|
:Haskell repl toggle {file?} |
Toggle a GHCi repl | filepath (optional) |
:Haskell repl quit |
Quit the current repl | |
:Haskell repl load {file?} |
Load a file into the current repl | filepath (optional) |
:Haskell repl reload |
Reload the current repl | |
:Haskell repl paste_type {register?} |
Query the repl for the type of {register} | |
:Haskell repl cword_type |
Query the repl for the type of the word under the cursor | |
:Haskell repl paste_info {register?} |
Query the repl for info on {register} | |
:Haskell repl cword_info {register?} |
Query the repl for info on the word under the cursor |
local ht = require('haskell-tools')
--- Toggle a GHCi repl for the current project
ht.repl.toggle()
--- Toggle a GHCi repl for `file`
--- @param file string Path to a Haskell file
ht.repl.toggle(file)
--- Quit the repl
ht.repl.quit()
--- Paste a command to the repl from register `reg`.
--- @param reg string? Register to paste from (:h registers), defaults to '"'.
ht.repl.paste(reg)
--- Query the repl for the type of register `reg`, and paste it to the repl.
--- @param reg string? Register to paste from (:h registers), defaults to '"'.
ht.repl.paste_type(reg)
--- Query the repl for the type of word under the cursor
ht.repl.cword_type()
--- Query the repl for info on register `reg`.
--- @param reg string? Register to paste from (:h registers), defaults to '"'.
ht.repl.paste_info(reg)
--- Query the repl for info on the word under the cursor
ht.repl.cword_info()
--- Load a file into the repl
--- @param file string The absolute file path
ht.repl.load_file(file)
--- Reload the repl
ht.repl.reload()
| Command | Description |
|---|---|
:Haskell projectFile |
Open the project file for the current buffer (cabal.project or stack.yaml) |
:Haskell packageYaml |
Open the package.yaml file for the current buffer |
:Haskell packageCabal |
Open the *.cabal file for the current buffer |
local ht = require('haskell-tools')
--- Open the project file for the current buffer (cabal.project or stack.yaml)
ht.project.open_project_file()
--- Open the package.yaml file for the current buffer
ht.project.open_package_yaml()
--- Open the *.cabal file for the current buffer
ht.project.open_package_cabal()
--- Search for files within the current (sub)package
--- @param opts table Optional telescope.nvim `find_files` options
ht.project.telescope_package_files(opts)
--- Live grep within the current (sub)package
--- @param opts table Optional telescope.nvim `live_grep` options
ht.project.telescope_package_grep(opts)
| Command | Description |
|---|---|
:Haskell definition |
vim.lsp.buf.definition(), but fall back to a Hoogle search if no definition is found |
The following functions depend on fast-tags.
local ht = require('haskell-tools')
-- Generate tags for the whole project
---@param path string? An optional file path, defaults to the current buffer
---@param opts table Optional options:
--- opts.refresh boolean
--- - Whether to refresh tags if they have already been generated for a project
ht.tags.generate_project_tags(path, opts)
-- Generate tags for the whole project
---@param path string? An optional file path, defaults to the current buffer
ht.tags.generate_package_tags(path)
[!NOTE]
By default,
haskell-toolswill automate generating project and package tags, iffast-tagsis detected.
local ht = require('haskell-tools')
---@param bufnr integer The buffer number
---@param opts table? Optional
---@param opts.autodetect: (boolean)
--- Whether to auto-detect launch configurations
---@param opts.settings_file_pattern: (string)
--- File name or pattern to search for. Defaults to 'launch.json'
ht.dap.discover_configurations(bufnr, opts)
[!NOTE]
haskell-tools.nvimwill discover DAP launch configurations automatically, ifnivm-dapis installed and the debug adapter server is executable. There is typically no need to call this function manually.
If telescope.nvim is installed,
haskell-tools.nvim will register the ht extension
with the following commands:
| Command | Description |
|---|---|
:Telescope ht package_files |
Search for files within the current (sub)package |
:Telescope ht package_hsfiles |
Search for Haskell files within the current (sub)package |
:Telescope ht package_grep |
Live grep within the current (sub)package |
:Telescope ht package_hsgrep |
Live grep Haskell files within the current (sub)package |
:Telescope ht hoogle_signature |
Run a Hoogle search for the type signature under the cursor |
To load the extension, call
require('telescope').load_extension('ht')
[!IMPORTANT]
If you lazy-load this plugin, make sure it is loaded before registering the Telescope extension.
For a health check, run :checkhealth haskell-tools
If hls is unable to show diagnostics, or shows an error diagnostic
at the top of your file, you should first check if you can compile
your project with cabal or stack.
If there are compile errors, open the files that cannot be compiled,
and hls should be able to show the error diagnostics for those files.
Check which versions of hls and GHC you are using
(e.g. by calling haskell-language-server-wrapper --probe-tools
or haskell-language-server --probe-tools).
Sometimes, certain features take some time to be implemented
for the latest GHC versions.
You can see how well a specific GHC version is supported in the hls docs.
To troubleshoot this plugin with a minimal config in a temporary directory, you can try minimal.lua.
nvim -u minimal.lua
[!NOTE]
If you use Nix, you can run
nix run "github:mrcjkb/haskell-tools.nvim#nvim-minimal-stable". ornix run "github:mrcjkb/haskell-tools.nvim#nvim-minimal-nightly".
If you cannot reproduce your issue with a minimal config,
it may be caused by another plugin.
In this case, add additional plugins and their configurations to minimal.lua,
until you can reproduce it.
[!NOTE]
This plugin is only tested on Linux. It should work on MacOS, and basic features should also work on Windows (since version
1.9.5), but I have no way to test this myself. Features that rely on external tools, such ashoogle,fast-tagsorghcimight break on non-Unix-like operating systems.
To enable debug logging, set the log level to DEBUG[^3]:
[^3]: See :help vim.log.levels:
vim.g.haskell_tools = {
tools = { -- haskell-tools options
log = {
level = vim.log.levels.DEBUG,
},
},
}
You can also temporarily set the log level by calling
| Command | Argument |
|---|---|
:Haskell log setLevel |
One of debug error warn info trace off |
or
:lua require('haskell-tools').log.set_level(vim.log.levels.DEBUG)
You can find the log files by calling
-- haskell-tools.nvim log
:lua =require('haskell-tools').log.get_logfile()
-- haskell-language-server logs
:lua =require('haskell-tools').log.get_hls_logfile()
or open them by calling
:lua require('haskell-tools').log.nvim_open_logfile() -- or :Haskell log openLog
:lua require('haskell-tools').log.nvim_open_hls_logfile() -- or :Haskell log openHlsLog
Here are some other plugins I recommend for Haskell (and nix) development in neovim:
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!