ghci-quickfix 📝
This is a GHC plugin that will write diagnostics to a file during compilation,
which can then be used with vim/nvim's quickfix feature. By default, the file
is errors.err in the project root directory but this can be customized (see plugin options).
NOTE: If you're using this plugin via
repl-alliance, you need to
explicitly enable it by passing --fplugin-opt ReplAlliance:--quickfix to GHC
or by setting the environment variable GHCI_QUICKFIX_ENABLED=true.
Usage
This plugin is intended to be used with GHCi or adjacent utilities such as
ghcid and ghciwatch as a development tool, not as a package dependency.
Stack Projects
To use with a stack project (you may need to add ghci-quickfix to your
extra-deps first):
stack repl my-project --package ghci-quickfix --ghci-options='-fplugin GhciQuickfix'
Cabal Projects
To use with a cabal project (you may need to run cabal update first):
cabal repl my-project --build-depends ghci-quickfix --repl-options='-fplugin GhciQuickfix'
Vim/Neovim Integration
After starting your REPL with the plugin enabled, you can load errors in Vim:
:cf errors.err
The errors.err argument can be omitted since it is the default file.
Or to update without jumping to the first error:
:cg errors.err
Navigate between error locations using :cn (next) and :cp (previous).
To get highlighting for errors and warnings in the editor, you can turn the
quickfix entries into diagnostics. The following snippet can be added to your
init.lua file to accomplish this.
-- [[ Quickfix to diagnostics ]]
-- Configure errorformat to distinguish between warning and error type.
vim.o.errorformat = '%f:%l:%c: %tarning: %m,%f:%l:%c: %trror: %m,' .. vim.o.errorformat
-- Create namespace for quickfix diagnostics
local qf_ns = vim.api.nvim_create_namespace('quickfix_diagnostics')
-- Configure diagnostic display for quickfix namespace
vim.diagnostic.config({
underline = true,
virtual_text = false,
signs = true,
update_in_insert = false,
}, qf_ns)
-- Helper function to create diagnostic entry from quickfix item
local function create_diagnostic_from_qf_item(item)
local severity = vim.diagnostic.severity.ERROR
-- Determine severity based on type field
if item.type == 'W' or item.type == 'w' then
severity = vim.diagnostic.severity.WARN
end
-- Handle column range - if no end_col, highlight to end of line
local col_start = (item.col or 1) - 1
local col_end = nil
local end_lnum = nil
if item.end_col and item.end_col > 0 and item.end_lnum then
col_end = item.end_col
end_lnum = item.end_lnum - 1
end
return {
lnum = item.lnum - 1,
col = col_start,
end_lnum = end_lnum,
end_col = col_end,
severity = severity,
message = item.text or '',
source = 'quickfix',
}
end
-- Function to convert quickfix entries to diagnostics and apply to all buffers
local function quickfix_to_diagnostics()
-- Clear all quickfix diagnostics from all buffers
vim.diagnostic.reset(qf_ns)
local qf_list = vim.fn.getqflist()
local diagnostics_by_buf = {}
for _, item in ipairs(qf_list) do
if item.bufnr > 0 and item.lnum > 0 then
if not diagnostics_by_buf[item.bufnr] then
diagnostics_by_buf[item.bufnr] = {}
end
table.insert(diagnostics_by_buf[item.bufnr], create_diagnostic_from_qf_item(item))
end
end
-- Set diagnostics for each buffer
for bufnr, diagnostics in pairs(diagnostics_by_buf) do
vim.diagnostic.set(qf_ns, bufnr, diagnostics, {})
end
end
-- Function to apply diagnostics for a specific buffer
local function apply_quickfix_diagnostics_for_buffer(bufnr)
local qf_list = vim.fn.getqflist()
local diagnostics = {}
for _, item in ipairs(qf_list) do
if item.bufnr == bufnr and item.lnum > 0 then
table.insert(diagnostics, create_diagnostic_from_qf_item(item))
end
end
vim.diagnostic.set(qf_ns, bufnr, diagnostics, {})
end
-- Auto-convert quickfix entries to diagnostics when loading from file
vim.api.nvim_create_autocmd('QuickFixCmdPost', {
pattern = '[cg]file,[cg]getfile',
callback = function()
quickfix_to_diagnostics()
end,
})
-- Reapply diagnostics when a buffer is read (handles newly opened buffers)
vim.api.nvim_create_autocmd('BufReadPost', {
pattern = '*',
callback = function(args)
local qf_list = vim.fn.getqflist()
if #qf_list == 0 then
return
end
-- Only apply diagnostics for this specific buffer if it has quickfix entries
local bufnr = args.buf
for _, item in ipairs(qf_list) do
if item.bufnr == bufnr then
apply_quickfix_diagnostics_for_buffer(bufnr)
return
end
end
end,
})
Plugin Options
Plugin options are passed using the --fplugin-opt flag. For example:
-fplugin GhciQuickfix -fplugin-opt GhciQuickfix:--quickfix-file=my-errors.err
Available Options
-
--quickfix-file=<path>
Specify the output file path for diagnostics.
Default: errors.err
Alternative: Set environment variable GHCI_QUICKFIX_FILE=<path>
-
--quickfix-include-parser-errors
Include parser errors in the quickfix file.
Default: Parser errors are excluded (HLint typically reports them)
Alternative: Set environment variable GHCI_QUICKFIX_INCLUDE_PARSER_ERRORS=true
-
--quickfix-path-replace=<needle>:<replace>
Replace text in file paths in the quickfix output.
Example: --quickfix-path-replace=/home/user:/Users/user
Can be specified multiple times for multiple replacements.
Useful for containerized or remote development environments.
Alternative: Set environment variable GHCI_QUICKFIX_PATH_REPLACE=<needle>:<replace>
-
--quickfix
Explicitly enable the plugin when using pluginOffByDefault (e.g., with repl-alliance).
Alternative: Set environment variable GHCI_QUICKFIX_ENABLED=true
The plugin generates quickfix entries in GCC-style format:
filename.hs:line:col: severity: message
This format is automatically recognized by Vim's quickfix system.
Compatibility
This plugin aims to support the 4 latest GHC major releases (i.e. 9.6.* through 9.12.*).
Check the cabal file for the currently supported versions.