mrcjkb/haskell-tools.nvim

github github
lspneovim-0.5
stars 489
issues 18
subscribers 4
forks 20
CREATED

2022-10-15

UPDATED

4 days ago


Neovim Lua Haskell Nix

GPL2 License Issues Build Status LuaRocks

All Contributors

[!NOTE]

:link: Quick Links

:grey_question: Do I need haskell-tools.nvim

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.

:pencil: Prerequisites

Required

  • neovim >= 0.10

Optional

:inbox_tray: Installation

This plugin is available on LuaRocks:

:Rocks install haskell-tools.nvim

Example using lazy.nvim:

{
  'mrcjkb/haskell-tools.nvim',
  version = '^4', -- 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.

:zap: 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 client for haskell-language-server manually, 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]

[!IMPORTANT]

  • Do not set vim.g.haskell_tools in after/ftplugin/haskell.lua, as the file is sourced after the plugin is initialized.

:star2: Features

codeLens

  • 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.

evalAll

  • 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()

hoogleSig

  • Hole-driven development powered by Hoogle

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

hoogleHole

  • GHCi repl

Start a GHCi repl for the current project / buffer.

  • Automagically detects the appropriate command (cabal repl, stack ghci or ghci) 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.

repl

  • Open project/package files for the current buffer

commands

  • Hover actions

Inspired by rust-tools.nvim, this plugin adds the following hover actions (if available):

  • Hoogle search (no dedicated <Plug> mapping because there can be multiple signatures to search for).
  • Open documentation in browser (<Plug>HaskellHoverActionDocs).
  • Open source in browser (<Plug>HaskellHoverActionSource).
  • Go to definition (<Plug>HaskellHoverActionDefinition).
  • Go to type definition (<Plug>HaskellHoverActionTypeDefinition).
  • Find references (<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.

[!NOTE]

Hover actions are disabled if Neovim is not built with luajit.

hoverActions

  • 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 autocmds 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-discover haskell-debug-adapter configurations

If the nvim-dap plugin is installed, haskell-tools.nvim will automatically discover haskell-debug-adapter configurations.

dap

[!NOTE]

haskell-debug-adapter is an experimental design and implementation of a debug adapter for Haskell.

  • Planned

For planned features, refer to the issues.

:gear: Advanced configuration

To modify the default configuration, set vim.g.haskell_tools.

vim.g.haskell_tools = {
  ---@type ToolsOpts
  tools = {
    -- ...
  },
  ---@type HaskellLspClientOpts
  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 HTDapOpts
  dap = {
    -- ...
  },
}

[!TIP]

vim.g.haskell_tools can also be a function that returns a table.

How to dynamically load different haskell-language-server settings per project

By 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,
  },
}

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.

Launch haskell-language-server on Cabal files

Since 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.

Set up iron.nvim to use haskell-tools.nvim

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,
      },
    },
  },
}

Create haskell-debug-adapter launch configurations

There are two ways this plugin will detect haskell-debug-adapter launch configurations:

  1. Automatically, by parsing Cabal or Stack project files.
  2. 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

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()

Hoogle

local ht = require('haskell-tools')
--- Run a hoogle signature search for the value under the cursor
ht.hoogle.hoogle_signature()

Repl

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()

Project

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)

Tags

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-tools will automate generating project and package tags, if fast-tags is detected.

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)

[!NOTE]

haskell-tools.nvim will discover DAP launch configurations automatically, if nivm-dap is installed and the debug adapter server is executable. There is typically no need to call this function manually.

Telescope extension

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.

:stethoscope: Troubleshooting

For a health check, run :checkhealth haskell-tools

LSP features not working

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 here.

Minimal config

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". or nix 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 as hoogle, fast-tags or ghci might break on non-Unix-like operating systems.

Logs

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

:link: Recommendations

Here are some other plugins I recommend for Haskell (and nix) development in neovim:

Contributors ✨

Thanks goes to these wonderful people (emoji key):

This project follows the all-contributors specification. Contributions of any kind welcome!