Quick Links
- Installation
- Quick Setup
- Features
- Advanced configuration
- Troubleshooting
- Recommendations
- Contributing
Prerequisites
Required
neovim >= 0.8
plenary.nvim
Optional
haskell-language-server
(recommended).telescope.nvim
.- A local
hoogle
installation (recommended for better hoogle search performance). fast-tags
(for automatic tag generation as a fallback forvim.lsp.tagfunc
).haskell-debug-adapter
andnvim-dap
.
Installation
This plugin is available on LuaRocks.
If you use a plugin manager that does not support LuaRocks, you have to declare the dependencies yourself.
Example using packer.nvim
:
use {
'mrcjkb/haskell-tools.nvim',
requires = {
'nvim-lua/plenary.nvim',
'nvim-telescope/telescope.nvim', -- optional
},
branch = '1.x.x', -- recommended
}
Note
It is suggested to use the stable branch 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
.
Quick Setup
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.hls
setup or set up the lsp manually, as doing so may cause conflicts.
To get started quickly with the default setup, add the following to ~/.config/nvim/ftplugin/haskell.lua
1:
local ht = require('haskell-tools')
local def_opts = { noremap = true, silent = true, }
ht.start_or_attach {
hls = {
on_attach = function(client, bufnr)
local opts = vim.tbl_extend('keep', def_opts, { buffer = bufnr, })
-- haskell-language-server relies heavily on codeLenses,
-- so auto-refresh (see advanced configuration) is enabled by default
vim.keymap.set('n', '<space>ca', vim.lsp.codelens.run, opts)
vim.keymap.set('n', '<space>hs', ht.hoogle.hoogle_signature, opts)
vim.keymap.set('n', '<space>ea', ht.lsp.buf_eval_all, opts)
end,
},
}
-- Suggested keymaps that do not depend on haskell-language-server:
local bufnr = vim.api.nvim_get_current_buf()
-- set buffer = bufnr in ftplugin/haskell.lua
local opts = { noremap = true, silent = true, buffer = bufnr }
-- 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, def_opts)
vim.keymap.set('n', '<leader>rq', ht.repl.quit, opts)
-- Detect nvim-dap launch configurations
-- (requires nvim-dap and haskell-debug-adapter)
ht.dap.discover_configurations(bufnr)
Note
- For more LSP related keymaps, see the
nvim-lspconfig
suggestions. - If using a local
hoogle
installation, follow these instructions to generate a database. - If you prefer, you can instead call
require('haskell-tools').setup {}
with the same options asstart_or_attach()
in your Neovim config. In this case,haskell-tools.nvim
will set up filetype autocommands for you.
Features
- Set up a
haskell-language-server
client. - Dynamically load
haskell-language-server
settings per project from JSON files. - Clean shutdown of language server on exit to prevent corrupted files (see ghc #14533).
- Automatically adds capabilities for the following plugins, if loaded:
- cmp-nvim-lsp (provides completion sources for nvim-cmp).
- nvim-lsp-selection-range (Adds haskell-specific expand selection support).
- Automatically refreshes code lenses by default,
which
haskell-language-server
heavily relies on. Can be disabled.
- Evaluate all code snippets at once
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()
- Hole-driven development powered by Hoogle
With the <C-r>
keymap,
the Hoogle search telescope integration can be used to fill holes.
- GHCi repl
Start a GHCi repl for the current project / buffer.
- Automagically detects the appropriate command (
cabal new-repl
,stack ghci
orghci
) for your project. - Choose between a builtin handler or
toggleterm.nvim
. - Dynamically create a repl command for
iron.nvim
(see advanced configuration). - Interact with the repl from within Haskell files using a lua API.
- Open project/package files for the current buffer
- Hover actions
Inspired by rust-tools.nvim, this plugin adds the following hover actions (if available):
- Hoogle search.
- Open documentation in browser.
- Open source in browser.
- Go to definition.
- Go to type definition.
- Find references.
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.
- Automatically generate tags
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 autocmd
s to automatically generate tags:
- For the whole project, when starting a session.
- For the current (sub)package, when writing a file.
This feature can be tweaked or disabled in the advanced configuration.
- Auto-detect
haskell-debug-adapter
configurations
If the nvim-dap
plugin is installed,
you can use haskell-tools.nvim
to auto-detect haskell-debug-adapter
configurations.
Note
haskell-debug-adapter
is an experimental design and implementation of
a debug adapter for Haskell.
- Planned
For planned features, refer to the issues.
Advanced configuration
To modify the default configs, call
-- defaults
require('haskell-tools').start_or_attach {
tools = { -- haskell-tools options
codeLens = {
-- Whether to automatically display/refresh codeLenses
-- (explicitly set to false to disable)
autoRefresh = true,
},
hoogle = {
-- 'auto': Choose a mode automatically, based on what is available.
-- 'telescope-local': Force use of a local installation.
-- 'telescope-web': The online version (depends on curl).
-- 'browser': Open hoogle search in the default browser.
mode = 'auto',
},
hover = {
-- Whether to disable haskell-tools hover
-- and use the builtin lsp's default handler
disable = false,
-- Set to nil to disable
border = {
{ 'โญ', 'FloatBorder' },
{ 'โ', 'FloatBorder' },
{ 'โฎ', 'FloatBorder' },
{ 'โ', 'FloatBorder' },
{ 'โฏ', 'FloatBorder' },
{ 'โ', 'FloatBorder' },
{ 'โฐ', 'FloatBorder' },
{ 'โ', 'FloatBorder' },
},
-- Stylize markdown (the builtin lsp's default behaviour).
-- Setting this option to false sets the file type to markdown and enables
-- Treesitter syntax highligting for Haskell snippets
-- if nvim-treesitter is installed
stylize_markdown = false,
-- Whether to automatically switch to the hover window
auto_focus = false,
},
definition = {
-- Configure vim.lsp.definition to fall back to hoogle search
-- (does not affect vim.lsp.tagfunc)
hoogle_signature_fallback = false,
},
repl = {
-- 'builtin': Use the simple builtin repl
-- 'toggleterm': Use akinsho/toggleterm.nvim
handler = 'builtin',
-- Which backend to prefer if both stack and cabal files are present
prefer = vim.fn.executable('stack') and 'stack' or 'cabal',
builtin = {
create_repl_window = function(view)
-- create_repl_split | create_repl_vsplit | create_repl_tabnew | create_repl_cur_win
return view.create_repl_split { size = vim.o.lines / 3 }
end
},
-- Can be overriden to either `true` or `false`.
-- The default behaviour depends on the handler.
auto_focus = nil,
},
-- Set up autocmds to generate tags (using fast-tags)
-- e.g. so that `vim.lsp.tagfunc` can fall back to Haskell tags
tags = {
enable = vim.fn.executable('fast-tags') == 1,
-- Events to trigger package tag generation
package_events = { 'BufWritePost' },
},
dap = {
cmd = { 'haskell-debug-adapter' },
},
},
hls = { -- LSP client options
-- ...
default_settings = {
haskell = { -- haskell-language-server options
formattingProvider = 'ormolu',
-- Setting this to true could have a performance impact on large mono repos.
checkProject = true,
-- ...
}
}
}
}
- The full list of defaults can be found here.
- To view all available language server settings
(including those not set by this plugin), run
haskell-language-server generate-default-config
. - For detailed descriptions of the configs, look at the haskell-language-server documentation.
haskell-language-server
settings per project
How to dynamically load different By default, this plugin will look for a hls.json
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.
You can change this behaviour with the hls.settings
config:
local ht = require('haskell-tools')
ht.start_or_attach {
-- ...
hls = {
settings = function(project_root)
return ht.lsp.load_hls_settings(project_root, {
settings_file_pattern = 'hls.json'
})
end,
},
}
How to disable individual code lenses
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 files
Launch Since version 1.9.0.0
, haskell-language-server
can launch on Cabal files.
You can either attach the LSP client in a ~/.config/nvim/ftplugin/cabal.lua
file1, or call haskell-tools.setup()
.
iron.nvim
to use haskell-tools.nvim
Set up Depends 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 configurations
Create There are two ways this plugin will detect haskell-debug-adapter
launch configurations:
- Automatically, by parsing Cabal or Stack project files.
- By loading a
launch.json
file in the project root.
Available functions and commands
For a complete overview, enter :help haskell-tools
in Neovim.
LSP
:HlsStart
- Start the LSP client.:HlsStop
- Stop the LSP client.:HlsRestart
- Restart the LSP client.
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
ht.lsp.load_hls_settings(project_root)
-- Evaluate all code snippets in comments
ht.lsp.buf_eval_all()
Hoogle
local ht = require('haskell-tools')
-- Run a hoogle signature search for the value under the cursor
ht.hoogle.hoogle_signature()
Repl
local ht = require('haskell-tools')
-- Toggle a GHCi repl for the current project
ht.repl.toggle()
-- Toggle a GHCi repl for `file` (must be a Haskell file)
ht.repl.toggle(file)
-- Quit the repl
ht.repl.quit()
-- Paste a command to the repl from register `reg`. (`reg` defaults to '"')
ht.repl.paste(reg)
-- Query the repl for the type of register `reg`. (`reg` 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`. (`reg` 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
ht.repl.load_file(file)
-- Reload the repl
ht.repl.reload()
Project
:HsProjectFile
- Open the project file for the current buffer (cabal.project or stack.yaml).:HsPackageYaml
- Open the package.yaml file for the current buffer.:HsPackageCabal
- 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: Optional telescope.nvim `find_files` options
ht.project.telescope_package_files(opts)
-- Live grep within the current (sub)package
---@param opts: Optional telescope.nvim `live_grep` options
ht.project.telescope_package_grep(opts)
Tags
The following functions depend on fast-tags
.
local ht = require('haskell-tools')
-- Generate tags for the whole project
---@param path: An optional file path, defaults to the current buffer
---@param opts: Optional options:
---@param opts.refresh: 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: An optional file path, defaults to the current buffer
ht.tags.generate_package_tags(path)
Telescope extension
If telescope.nvim
is installed,
haskell-tools.nvim
will register the ht
extenstion
with the following commands:
: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')
DAP
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)
Troubleshooting
For a health check, run :checkhealth haskell-tools
LSP features not working
Check which versions of hls
and GHC you are using
(haskell-language-server-wrapper --version
).
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 here.
Minimal config
To troubleshoot this plugin with a minimal config in a temporary directory, you can try minimal.lua.
mkdir -p /tmp/minimal/
# The first start will install the plugins into the temporary directory
NVIM_DATA_MINIMAL=/tmp/minimal nvim -u minimal.lua
# Quit Neovim and start it up again with the plugins loaded
NVIM_DATA_MINIMAL=/tmp/minimal nvim -u minimal.lua
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 this has not been tested.
Features that rely on external tools, such as hoogle
,
fast-tags
or ghci
are likely to break on non-Unix-like operating systems.
Logs
To enable debug logging, set the log level to DEBUG
2:
require('haskell-tools').start_or_attach {
tools = { -- haskell-tools options
log = {
level = vim.log.levels.DEBUG,
},
},
}
You can also temporarily set the log level by calling
: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()
:lua require('haskell-tools').log.nvim_open_hls_logfile()
Recommendations
Here are some other plugins I recommend for Haskell (and nix) development in neovim:
- neotest-haskell: Interact with tests in neovim.
- haskell-snippets.nvim Collection of Haskell snippets for LuaSnip.
- telescope_hoogle: Live Hoogle search.
- telescope-manix: Nix search.
- nvim-lint: As a fallback in case there are problems with haskell-language-server (e.g. in large mono repos).
- nvim-treesitter: For syntax highlighting, and much more.
- nvim-treesitter-textobjects: For TreeSitter-based textobjects.
Contributors โจ
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
Footnotes
-
See
:help vim.log.levels
:โฉ