Skip to content

Commit c565890

Browse files
committed
feat: spellcheck picker, all misspelled words in buffer
1 parent 1e5933e commit c565890

8 files changed

Lines changed: 150 additions & 1 deletion

File tree

OPTIONS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,6 +1165,10 @@ Filetypes
11651165

11661166
Neovim's menus
11671167

1168+
#### spellcheck
1169+
1170+
Misspelled words in buffer
1171+
11681172
#### spell_suggest
11691173

11701174
Spelling suggestions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ Alternatively, resuming work on a specific picker:
301301
| `keymaps` | key mappings |
302302
| `filetypes` | filetypes |
303303
| `menus` | menus |
304+
| `spellcheck` | misspelled words in buffer |
304305
| `spell_suggest` | spelling suggestions |
305306
| `packadd` | :packadd <package> |
306307

doc/fzf-lua-opts.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*fzf-lua-opts.txt* For Neovim >= 0.8.0 Last change: 2025 March 13
1+
*fzf-lua-opts.txt* For Neovim >= 0.8.0 Last change: 2025 May 13
22

33
==============================================================================
44
Table of Contents *fzf-lua-opts-table-of-contents*
@@ -1605,6 +1605,12 @@ Neovim's menus
16051605

16061606

16071607

1608+
spellcheck *fzf-lua-opts-spellcheck*
1609+
1610+
Misspelled words in buffer
1611+
1612+
1613+
16081614
spell_suggest *fzf-lua-opts-spell_suggest*
16091615

16101616
Spelling suggestions

lua/fzf-lua/actions.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,12 @@ M.spell_apply = function(selected, opts)
664664
end
665665
end
666666

667+
M.spell_suggest = function(selected, opts)
668+
if not selected[1] then return false end
669+
M.file_edit(selected, opts)
670+
FzfLua.spell_suggest({ no_resume = true })
671+
end
672+
667673
M.set_filetype = function(selected)
668674
vim.bo.filetype = selected[1]:match("[^" .. utils.nbsp .. "]+$")
669675
end

lua/fzf-lua/config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,7 @@ M._action_to_helpstr = {
982982
[actions.nvim_opt_edit_local] = "nvim-opt-edit-local",
983983
[actions.nvim_opt_edit_global] = "nvim-opt-edit-global",
984984
[actions.spell_apply] = "spell-apply",
985+
[actions.spell_suggest] = "spell-suggest",
985986
[actions.set_filetype] = "set-filetype",
986987
[actions.packadd] = "packadd",
987988
[actions.help] = "help-open",

lua/fzf-lua/defaults.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,33 @@ M.defaults.treesitter = {
728728
},
729729
}
730730

731+
M.defaults.spellcheck = {
732+
previewer = M._default_previewer_fn,
733+
file_icons = false,
734+
color_icons = false,
735+
word_separator = "[%s%p]",
736+
fzf_opts = {
737+
["--multi"] = true,
738+
["--tabstop"] = "4",
739+
["--delimiter"] = "[:]",
740+
["--with-nth"] = "2..",
741+
},
742+
line_field_index = "{2}",
743+
_actions = function()
744+
return M.globals.actions.buffers or M.globals.actions.files
745+
end,
746+
actions = {
747+
["ctrl-s"] = { fn = actions.spell_suggest, header = "spell suggest" }
748+
},
749+
_cached_hls = { "buf_name", "buf_nr", "buf_linenr", "path_colnr" },
750+
_fmt = {
751+
to = false,
752+
from = function(s, _)
753+
return s:gsub("\t\t", ": ")
754+
end
755+
},
756+
}
757+
731758
M.defaults.tags = {
732759
previewer = { _ctor = previewers.builtin.tags },
733760
input_prompt = "[tags] Grep For> ",

lua/fzf-lua/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ do
239239
lines = { "fzf-lua.providers.buffers", "lines" },
240240
blines = { "fzf-lua.providers.buffers", "blines" },
241241
treesitter = { "fzf-lua.providers.buffers", "treesitter" },
242+
spellcheck = { "fzf-lua.providers.buffers", "spellcheck" },
242243
helptags = { "fzf-lua.providers.helptags", "helptags" },
243244
manpages = { "fzf-lua.providers.manpages", "manpages" },
244245
-- backward compat

lua/fzf-lua/providers/buffers.lua

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,4 +622,107 @@ M.treesitter = function(opts)
622622
core.fzf_exec(contents, opts)
623623
end
624624

625+
M.spellcheck = function(opts)
626+
opts = config.normalize_opts(opts, "spellcheck")
627+
if not opts then return end
628+
629+
if #vim.bo.spelllang == 0 then
630+
utils.info("Spell language not set, use ':setl spl=...' to enable spell checking.")
631+
return
632+
end
633+
634+
-- Default to current buffer
635+
opts._bufnr = tonumber(opts.bufnr) or vim.api.nvim_get_current_buf()
636+
opts._bufname = path.basename(vim.api.nvim_buf_get_name(opts._bufnr))
637+
if not opts._bufname or #opts._bufname == 0 then
638+
opts._bufname = utils.nvim_buf_get_name(opts._bufnr)
639+
end
640+
641+
if utils.mode_is_visual() then
642+
local _, sel = utils.get_visual_selection()
643+
opts.start_line = opts.start_line or sel.start.line
644+
opts.end_line = opts.end_line or sel["end"].line
645+
end
646+
647+
local contents = function(cb)
648+
coroutine.wrap(function()
649+
local co = coroutine.running()
650+
local data = {}
651+
652+
-- Use vim.schedule to avoid
653+
-- E5560: vimL function must not be called in a lua loop callback
654+
vim.schedule(function()
655+
local bufnr = opts._bufnr
656+
local filepath = vim.api.nvim_buf_get_name(bufnr)
657+
if vim.api.nvim_buf_is_loaded(bufnr) then
658+
data = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
659+
elseif vim.fn.filereadable(filepath) ~= 0 then
660+
data = vim.fn.readfile(filepath, "")
661+
end
662+
coroutine.resume(co)
663+
end)
664+
665+
-- wait for vim.schedule
666+
coroutine.yield()
667+
668+
local offset = 0
669+
local start_line = opts.start_line or 1
670+
local end_line = opts.end_line or #data
671+
local lines = end_line - start_line + 1
672+
673+
if opts.start == "cursor" then
674+
-- start display from current line and wrap from bottom
675+
offset = core.CTX().cursor[1] - start_line
676+
end
677+
678+
for i = 1, lines do
679+
local lnum = i + offset
680+
if lnum > lines then
681+
lnum = lnum % lines
682+
end
683+
lnum = lnum + start_line - 1
684+
685+
local line, from, to = data[lnum], 1, nil
686+
repeat
687+
local word_separator = opts.word_separator or "[%s%p]"
688+
local function trim(s)
689+
return s:gsub("^" .. word_separator .. "+", ""):gsub(word_separator .. "+$", "")
690+
end
691+
from, to = string.find(line, "%w+", from)
692+
local word = from and string.sub(line, from, to)
693+
local prefix = from and string.sub(line, from - 1, from - 1) or ""
694+
local postfix = to and string.sub(line, to + 1, to + 1) or ""
695+
local valid_word = word
696+
and (#prefix == 0 or prefix:match("^" .. word_separator))
697+
and (#postfix == 0 or postfix:match(word_separator .. "$"))
698+
if valid_word then
699+
local _, lead = word:find("^" .. word_separator .. "+")
700+
local spell = vim.spell.check(trim(word))[1]
701+
if spell then
702+
cb(string.format("[%s]%s%s:%s:%s\t\t%s",
703+
utils.ansi_codes[opts.hls.buf_nr](tostring(opts._bufnr)),
704+
utils.nbsp,
705+
utils.ansi_codes[opts.hls.buf_name](opts._bufname),
706+
utils.ansi_codes[opts.hls.buf_linenr](tostring(lnum)),
707+
utils.ansi_codes[opts.hls.path_colnr](tostring(from + (lead or 0))),
708+
trim(word)
709+
), function(err)
710+
coroutine.resume(co)
711+
if err then cb(nil) end
712+
end)
713+
coroutine.yield()
714+
end
715+
end
716+
if from then from = to + 1 end
717+
until not from
718+
end
719+
cb(nil)
720+
end)()
721+
end
722+
723+
opts = core.set_header(opts, opts.headers or { "actions" })
724+
725+
core.fzf_exec(contents, opts)
726+
end
727+
625728
return M

0 commit comments

Comments
 (0)