Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ This is a plugin to display spelling errors as diagnostics. Some language server
A simpler solution, therefore, is to use Neovim's existing spellchecking and diagnostics features. This is done by iterating through the flagged words in a buffer and passing them to `vim.diagnostic.set()`. Neovim is fast enough that this is ~instantaneous for most files. See below for installation and configuration instructions.

## Installation

I recommend using [Lazy.nvim](https://github.com/folke/lazy.nvim):

```lua
{
"ravibrock/spellwarn.nvim",
Expand All @@ -23,7 +25,9 @@ I recommend using [Lazy.nvim](https://github.com/folke/lazy.nvim):
Note that this uses Neovim's built-in spellchecking. This requires putting `vim.opt.spell = true` and `vim.opt.spelllang = [YOUR LANGUAGE HERE]` somewhere in your Neovim config if you haven't already. You may also want to add the word "spellwarn" to your Neovim dictionary. This can be done by putting the cursor onto "spellwarn" and hitting `zg`.

## Configuration

Pass any of the following options to `require("spellwarn").setup()`:

```lua
{
event = { -- event(s) to refresh diagnostics on
Expand All @@ -33,40 +37,60 @@ Pass any of the following options to `require("spellwarn").setup()`:
"TextChangedI",
"TextChangedP",
},

enable = true, -- enable diagnostics on startup

max_file_size = nil, -- maximum file size to check in lines (nil for no limit)

suggest = false, -- show spelling suggestions in diagnostic message
num_suggest = 3, -- number of suggestions shown in diagnostic message

-- function to do any custom processing of the diagnostics table before passing it to vim.diagnostic.set
func_preprocess = function(bufnr, diag_tbl)
return diag_tbl
end,

bt_config = { -- buffer types to run on
[""] = true,
},
bt_default = false, -- default for types not in bt_config.

ft_config = { -- spellcheck method: "cursor", "iter", or boolean
alpha = false,
help = false,
lazy = false,
alpha = false,
help = false,
lazy = false,
lspinfo = false,
mason = false,
mason = false,
},
ft_default = true, -- default option for unspecified filetypes
max_file_size = nil, -- maximum file size to check in lines (nil for no limit)

diagnostic_opts = { severity_sort = true }, -- options for diagnostic display
severity = { -- severity for each spelling error type (false to disable diagnostics for that type)
spellbad = "WARN",
spellcap = "HINT",
spelllocal = "HINT",
spellrare = "INFO",
spellbad = { level = "WARN", prefix = "Unknown Word: ", suffix = "" },
spellcap = { level = "HINT", prefix = "Missing capital: ", suffix = "" },
spelllocal = { level = "HINT", prefix = "Word Localization: ", suffix = "" },
spellrare = { level = "INFO", prefix = "Rare Word: ", suffix = "" },
},
suggest = false, -- show spelling suggestions in diagnostic message (works best with window-style message)
num_suggest = 3, -- number of spelling suggestions shown in diagnostic message
prefix = "possible misspelling(s): ", -- prefix for each diagnostic message
diagnostic_opts = { severity_sort = true }, -- options for diagnostic display
}
```

Most options are overwritten (e.g. passing `ft_config = { python = false }` will mean that `alpha`, `mason`, etc. are set to true) but `severity` and `diagnostic_opts` are merged, so that (for example) passing `{ spellbad = "HINT" }` won't cause `spellcap` to be nil. You can pass any of `cursor`, `iter`, `treesitter`, `false`, or `true` as options to `ft_config`. The default method is `cursor`, which iterates through the buffer with `]s`. There is also `iter`, which uses Treesitter (if available) and the Lua API. Finally, `false` disables Spellwarn for that filetype and `true` uses the default (`cursor`). The `suggest` option adds spelling suggestions to the diagnostic message, it does not allow auto-complete. `num_suggest` specifies the number of suggestions to show in the diagnostic message. **If you have `suggest` set to `true`, you need to also have `num_suggest >= 1` or else you will have an error.**

*Note: `iter` doesn't show `spellcap` errors, but works well other than that. I recommend it.*

## Usage

The plugin should be good to go after installation with the provided snippet. It has sensible defaults. Run `:Spellwarn enable`, `:Spellwarn disable`, or `:Spellwarn toggle` to enable/disable/toggle during runtime (though this will *not* override `max_file_size`, `ft_config`, or `ft_default`). You can also add keybindings for any of these (one possible usecase would be disabling by default with the `enable` key of the configuration table and then only enabling when needed). To disable diagnostics on a specific line, add `spellwarn:disable-next-line` to the line immediately above or `spellwarn:disable-line` to a comment at the end of the line. To disable diagnostics in a file, add a comment with `spellwarn:disable` to the *first or second* line of the file.

`Spellwarn qflist` will show the quickfix window, populated with the list of spelling mistakes in the current buffer.

## Lua API

The Lua API matches the arguments for the `Spellwarn` command:
- `require("spellwarn").disable()` to disable
- `require("spellwarn").enable()` to enable
- `require("spellwarn").toggle()` to toggle
- `require("spellwarn").qflist()` to show the quickfix window

## Contributing

PRs and issues welcome! Please consult `CONTRIBUTING.md` for style guidelines.
57 changes: 37 additions & 20 deletions doc/spellwarn.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
*spellwarn.txt* For NVIM v0.8.0 Last change: 2025 September 03
*spellwarn.txt* For NVIM v0.8.0 Last change: 2026 March 13

==============================================================================
Table of Contents *spellwarn-table-of-contents*
Expand Down Expand Up @@ -31,7 +31,7 @@ configuration instructions.

INSTALLATION *spellwarn-spellwarn.nvim-installation*

I recommend using Lazy.nvim <https://github.com/folke/lazy.nvim>
I recommend using Lazy.nvim <https://github.com/folke/lazy.nvim>:

>lua
{
Expand All @@ -41,7 +41,7 @@ I recommend using Lazy.nvim <https://github.com/folke/lazy.nvim>
}
<

Notethat this uses Neovim’s built-in spellchecking. This requires putting
Note that this uses Neovim’s built-in spellchecking. This requires putting
`vim.opt.spell = true` and `vim.opt.spelllang = [YOUR LANGUAGE HERE]` somewhere
in your Neovim config if you haven’t already. You may also want to add the
word "spellwarn" to your Neovim dictionary. This can be done by putting the
Expand All @@ -50,7 +50,7 @@ cursor onto "spellwarn" and hitting `zg`.

CONFIGURATION *spellwarn-spellwarn.nvim-configuration*

Pass any of the following options to `require("spellwarn").setup()`
Pass any of the following options to `require("spellwarn").setup()`:

>lua
{
Expand All @@ -61,30 +61,43 @@ Pass any of the following options to `require("spellwarn").setup()`
"TextChangedI",
"TextChangedP",
},

enable = true, -- enable diagnostics on startup

max_file_size = nil, -- maximum file size to check in lines (nil for no limit)

suggest = false, -- show spelling suggestions in diagnostic message
num_suggest = 3, -- number of suggestions shown in diagnostic message

-- function to do any custom processing of the diagnostics table before passing it to vim.diagnostic.set
func_preprocess = function(bufnr, diag_tbl)
return diag_tbl
end,

bt_config = { -- buffer types to run on
[""] = true,
},
bt_default = false, -- default for types not in bt_config.

ft_config = { -- spellcheck method: "cursor", "iter", or boolean
alpha = false,
help = false,
lazy = false,
alpha = false,
help = false,
lazy = false,
lspinfo = false,
mason = false,
mason = false,
},
ft_default = true, -- default option for unspecified filetypes
max_file_size = nil, -- maximum file size to check in lines (nil for no limit)

diagnostic_opts = { severity_sort = true }, -- options for diagnostic display
severity = { -- severity for each spelling error type (false to disable diagnostics for that type)
spellbad = "WARN",
spellcap = "HINT",
spelllocal = "HINT",
spellrare = "INFO",
spellbad = { level = "WARN", prefix = "Unknown Word: ", suffix = "" },
spellcap = { level = "HINT", prefix = "Missing capital: ", suffix = "" },
spelllocal = { level = "HINT", prefix = "Word Localization: ", suffix = "" },
spellrare = { level = "INFO", prefix = "Rare Word: ", suffix = "" },
},
suggest = false, -- show spelling suggestions in diagnostic message (works best with window-style message)
num_suggest = 3, -- number of spelling suggestions shown in diagnostic message
prefix = "possible misspelling(s): ", -- prefix for each diagnostic message
diagnostic_opts = { severity_sort = true }, -- options for diagnostic display
}
<

Mostoptions are overwritten (e.g. passing `ft_config = { python = false }`
Most options are overwritten (e.g. passing `ft_config = { python = false }`
will mean that `alpha`, `mason`, etc. are set to true) but `severity` and
`diagnostic_opts` are merged, so that (for example) passing `{ spellbad =
"HINT" }` won’t cause `spellcap` to be nil. You can pass any of `cursor`,
Expand Down Expand Up @@ -115,12 +128,16 @@ when needed). To disable diagnostics on a specific line, add
diagnostics in a file, add a comment with `spellwarn:disable` to the _first or
second_ line of the file.

`Spellwarn qflist` will show the quickfix window, populated with the list of
spelling mistakes in the current buffer.


LUA API *spellwarn-spellwarn.nvim-lua-api*

The Lua API matches the arguments for the `Spellwarn` command: -
`require("spellwarn").disable()` to disable - `require("spellwarn").enable()`
to enable - `require("spellwarn").toggle()` to toggle
to enable - `require("spellwarn").toggle()` to toggle -
`require("spellwarn").qflist()` to show the quickfix window


CONTRIBUTING *spellwarn-spellwarn.nvim-contributing*
Expand Down
89 changes: 68 additions & 21 deletions lua/spellwarn/diagnostics.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
local M = {}
local namespace = vim.api.nvim_create_namespace("Spellwarn")

local flag_text_changed = true -- Prevent updating when nothing was changed (CursorHold navigation, etc)

local function get_bufs_loaded()
local bufs_loaded = {}
for i, buf_hndl in ipairs(vim.api.nvim_list_bufs()) do
Expand All @@ -16,14 +18,9 @@ function M.update_diagnostics(opts, bufnr)
if opts.max_file_size and vim.api.nvim_buf_line_count(bufnr) > opts.max_file_size then
return
end
local ft = vim.fn.getbufvar(bufnr, "&filetype")
if opts.ft_config[ft] == false or (opts.ft_config[ft] == nil and opts.ft_default == false) then
vim.diagnostic.reset(namespace, bufnr)
return
end
local diags = {}
for _, error in pairs(require("spellwarn.spelling").get_spelling_errors_main(opts, bufnr) or {}) do
local msg = opts.prefix .. error.word
local msg = error.word
if opts.suggest and opts.num_suggest > 0 then
local suggestions = vim.fn.spellsuggest(error.word, opts.num_suggest)
local addition = "\nSuggestions:\n"
Expand All @@ -43,33 +40,81 @@ function M.update_diagnostics(opts, bufnr)
diags[#diags + 1] = {
col = error.col - 1, -- 0-indexed
lnum = error.lnum - 1, -- 0-indexed
message = msg,
severity = vim.diagnostic.severity[opts.severity[error.type]],
source = "spellwarn",
message = opts.severity[error.type].prefix .. msg .. opts.severity[error.type].suffix,
severity = vim.diagnostic.severity[opts.severity[error.type].level],
source = "SpellWarn",
}
end
end
end
-- TODO: Add suffix diagnostics with type of spelling error the way that LSP diagnostics do

-- Pre-process, if a function is set in opts to do anything.
diags = opts.func_preprocess(bufnr, diags) or {}

vim.diagnostic.set(namespace, bufnr, diags, opts.diagnostic_opts)
end

local function can_update(opts, bufnr)
local winid = vim.api.nvim_get_current_win()
if winid then
if not vim.wo[winid].spell then
return false
end
end

-- Allow the buffer type, or file type, to cancel the attempt to process the buffer.
local is_ok = true

-- Buffer type check.
local buftype = vim.api.nvim_get_option_value("buftype", { buf = bufnr })
if opts.bt_config[buftype] ~= nil then
is_ok = opts.bt_config[buftype]
else
is_ok = opts.bt_default
end

if not is_ok then
return false
end
-- Still OK to proceed.

-- File type check.
local ft = vim.fn.getbufvar(bufnr, "&filetype")
if opts.ft_config[ft] ~= nil then
is_ok = opts.ft_config[ft]
else
is_ok = opts.ft_default
end

return is_ok
end

function M.setup(opts)
local group_name = "Spellwarn"
function M.enable()
vim.api.nvim_create_augroup("Spellwarn", {})
vim.api.nvim_create_augroup(group_name, {})
-- BufEnter to trigger an initial pass through.
vim.api.nvim_create_autocmd({ "BufEnter", "TextChanged", "TextChangedI" }, {
group = group_name,
callback = function()
flag_text_changed = true
end,
})

vim.api.nvim_create_autocmd(opts.event, {
group = "Spellwarn",
group = group_name,
callback = function()
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.fn.bufnr("%")
if winid then
if not vim.wo[winid].spell then
vim.diagnostic.reset(namespace, bufnr) -- ensure old are cleared if spell is toggled to off.
return
end
if not flag_text_changed then
return false
end

M.update_diagnostics(opts, bufnr)
local bufnr = vim.fn.bufnr("%")
if can_update(opts, bufnr) then
flag_text_changed = false
M.update_diagnostics(opts, bufnr)
else
vim.diagnostic.reset(namespace, bufnr)
end
end,
desc = "Update Spellwarn diagnostics",
})
Expand Down Expand Up @@ -103,13 +148,15 @@ function M.setup(opts)
M.disable()
elseif arg == "toggle" then
M.toggle()
elseif arg == "qflist" then
require("spellwarn.qflist").qflist(opts)
else
vim.api.nvim_echo({ { "Invalid argument: " .. arg .. "\n" } }, true, { err = true })
end
end, {
nargs = 1,
complete = function()
return { "disable", "enable", "toggle" }
return { "disable", "enable", "toggle", "qflist" }
end,
})

Expand Down
Loading