refactor(spellfile): config() interface, docs #36481

Problem:
- Exposing the raw config as table is a pattern not seen anywhere else
  in the Nvim codebase.
- Old spellfile.vim docs still available, no new documentation

Solution:
- Exposing a `config()` function that both acts as "getter" and "setter"
  is a much more common idiom (e.g. vim.lsp, vim.diagnostic).
- Add new documentation and link old docs to |spellfile.lua| instead of
  |spellfile.vim|.
This commit is contained in:
Yochem van Rosmalen
2025-11-10 06:51:39 +01:00
committed by GitHub
parent cf347110c1
commit 9bdb011a50
11 changed files with 160 additions and 93 deletions

View File

@@ -6107,7 +6107,7 @@ A jump table for the options with a short description can be found at |Q_op|.
encoding is used, Vim doesn't check it.
How the related spell files are found is explained here: |spell-load|.
If the |spellfile.vim| plugin is active and you use a language name
If the |spellfile.lua| plugin is active and you use a language name
for which Vim cannot find the .spl file in 'runtimepath' the plugin
will ask you if you want to download the file.

View File

@@ -16,7 +16,7 @@ loaded by default while others are not loaded until requested by |:packadd|.
==============================================================================
Standard plugins ~
*standard-plugin-list*
*standard-plugin-list*
Help-link Loaded Short description ~
|difftool| No Compares two directories or files side-by-side
|editorconfig| Yes Detect and interpret editorconfig
@@ -37,7 +37,7 @@ Help-link Loaded Short description ~
|pi_tar.txt| Yes Tar file explorer
|pi_tutor.txt| Yes Interactive tutorial
|pi_zip.txt| Yes Zip archive explorer
|spellfile.vim| Yes Install spellfile if missing
|spellfile.lua| Yes Install spellfile if missing
|tohtml| Yes Convert buffer to html, syntax included
|undotree| No Interactive textual undotree
@@ -166,6 +166,54 @@ trim_trailing_whitespace *editorconfig.trim_trailing_whitespace*
buffer is written.
==============================================================================
Builtin plugin: spellfile *spellfile.lua*
Asks the user to download missing spellfiles. The spellfile is written to
`stdpath('data') .. 'site/spell'` or the first writable directory in the
'runtimepath'.
The plugin can be disabled by setting `g:loaded_spellfile_plugin = 1`.
*nvim.spellfile.Opts*
A table with the following fields:
Fields: ~
• {url} (`string`) The base URL from where the spellfiles are
downloaded. Uses `g:spellfile_URL` if it's set,
otherwise https://ftp.nluug.nl/pub/vim/runtime/spell.
• {timeout_ms} (`integer`, default: 15000) Number of milliseconds after
which the |vim.net.request()| times out.
config({opts}) *spellfile.config()*
Configure spellfile download options. For example: >lua
require('nvim.spellfile').config({ url = '...' })
<
Parameters: ~
• {opts} (`nvim.spellfile.Opts?`) When omitted or `nil`, retrieve the
current configuration. Otherwise, a configuration table.
Return: ~
(`nvim.spellfile.Opts?`) Current config if {opts} is omitted.
get({lang}) *spellfile.get()*
Download spellfiles for language {lang} if available.
Parameters: ~
• {lang} (`string`) Language code.
Return: ~
(`table?`) A table with the following fields:
• {files} (`string[]`)
• {key} (`string`)
• {lang} (`string`)
• {encoding} (`string`)
• {dir} (`string`)
==============================================================================
Builtin plugin: tohtml *tohtml*

View File

@@ -313,10 +313,6 @@ Only the first file is loaded, the one that is first in 'runtimepath'. If
this succeeds then additionally files with the name LL.EEE.add.spl are loaded.
All the ones that are found are used.
If no spell file is found the |SpellFileMissing| autocommand event is
triggered. This may trigger the |spellfile.vim| plugin to offer you
downloading the spell file.
Additionally, the files related to the names in 'spellfile' are loaded. These
are the files that |zg| and |zw| add good and wrong words to.
@@ -640,48 +636,11 @@ Comment lines with the name of the .spl file are used as a header above the
words that were generated from that .spl file.
SPELL FILE MISSING *spell-SpellFileMissing* *spellfile.vim*
SPELL FILE MISSING *spell-SpellFileMissing*
If the spell file for the language you are using is not available, you will
get an error message. But if the "spellfile.vim" plugin is active it will
offer you to download the spell file. Just follow the instructions, it will
ask you where to write the file (there must be a writable directory in
'runtimepath' for this).
If a spell file is missing, the user is asked whether to download it. See
|spellfile.lua|.
The plugin has a default place where to look for spell files, on the Vim ftp
server. The protocol used is TLS (`https://`) for security. If you want to
use another location or another protocol, set the g:spellfile_URL variable to
the directory that holds the spell files. You can use `http://` or `ftp://`,
but you are taking a security risk then. The |netrw| plugin is used for
getting the file, look there for the specific syntax of the URL. Example: >
let g:spellfile_URL = 'https://ftp.nluug.nl/vim/runtime/spell'
You may need to escape special characters.
The plugin will only ask about downloading a language once. If you want to
try again anyway restart Vim, or set g:spellfile_URL to another value (e.g.,
prepend a space).
To avoid using the "spellfile.vim" plugin do this in your vimrc file: >
let loaded_spellfile_plugin = 1
Instead of using the plugin you can define a |SpellFileMissing| autocommand to
handle the missing file yourself. You can use it like this: >
:au SpellFileMissing * call Download_spell_file(expand('<amatch>'))
Thus the <amatch> item contains the name of the language. Another important
value is 'encoding', since every encoding has its own spell file. With two
exceptions:
- For ISO-8859-15 (latin9) the name "latin1" is used (the encodings only
differ in characters not used in dictionary words).
- The name "ascii" may also be used for some languages where the words use
only ASCII letters for most of the words.
The default "spellfile.vim" plugin uses this autocommand, if you define your
autocommand afterwards you may want to use ":au! SpellFileMissing" to overrule
it. If you define your autocommand before the plugin is loaded it will notice
this and not do anything.
*E797*
Note that the SpellFileMissing autocommand must not change or destroy the
buffer the user was editing.

View File

@@ -97,6 +97,8 @@ Defaults *defaults* *nvim-defaults*
- |man.lua| plugin is enabled, so |:Man| is available by default.
- |matchit| plugin is enabled. To disable it in your config: >vim
:let loaded_matchit = 1
- |spellfile.lua| plugin is enabled, spellfiles are installed by default if
missing.
- |g:vimsyn_embed| defaults to "l" to enable Lua highlighting
@@ -741,6 +743,7 @@ Editor:
- *cscope* support was removed in favour of plugin-based solutions such as:
https://github.com/dhananjaylatkar/cscope_maps.nvim
- *popup-window* : Use |floating-windows| instead.
- *spellfile.vim* : Replaced by |spellfile.lua|.
- *textprop* : Use |extmarks| instead.
Eval:

View File

@@ -1,22 +1,56 @@
--- @brief
--- Asks the user to download missing spellfiles. The spellfile is written to
--- `stdpath('data') .. 'site/spell'` or the first writable directory in the
--- 'runtimepath'.
---
--- The plugin can be disabled by setting `g:loaded_spellfile_plugin = 1`.
local M = {}
--- @class vim.spellfile.Config
--- @class nvim.spellfile.Info
--- @inlinedoc
--- @field files string[]
--- @field key string
--- @field lang string
--- @field encoding string
--- @field dir string
--- A table with the following fields:
--- @class nvim.spellfile.Opts
---
--- The base URL from where the spellfiles are downloaded. Uses `g:spellfile_URL`
--- if it's set, otherwise https://ftp.nluug.nl/pub/vim/runtime/spell.
--- @field url string
---
--- Number of milliseconds after which the [vim.net.request()] times out.
--- (default: 15000)
--- @field timeout_ms integer
---@class vim.spellfile.Info
---@field files string[]
---@field key string
---@field lang string
---@field encoding string
---@field dir string
---@type vim.spellfile.Config
M.config = {
url = 'https://ftp.nluug.nl/pub/vim/runtime/spell',
--- @type nvim.spellfile.Opts
local config = {
url = vim.g.spellfile_URL or 'https://ftp.nluug.nl/pub/vim/runtime/spell',
timeout_ms = 15000,
}
--- Configure spellfile download options. For example:
--- ```lua
--- require('nvim.spellfile').config({ url = '...' })
--- ```
--- @param opts nvim.spellfile.Opts? When omitted or `nil`, retrieve the
--- current configuration. Otherwise, a configuration table.
--- @return nvim.spellfile.Opts? : Current config if {opts} is omitted.
function M.config(opts)
vim.validate('opts', opts, 'table', true)
if not opts then
return vim.deepcopy(config, true)
end
for k, v in
pairs(opts --[[@as table<any,any>]])
do
config[k] = v
end
end
--- TODO(justinmk): add on_done/on_err callbacks to download(), instead of exposing this?
---@type table<string, boolean>
M._done = {}
@@ -26,6 +60,8 @@ local function rtp_list()
return vim.opt.rtp:get()
end
---@param msg string
---@param level vim.log.levels?
local function notify(msg, level)
vim.notify(msg, level or vim.log.levels.INFO)
end
@@ -37,15 +73,20 @@ local function normalize_lang(lang)
return (l:match('^[^,%s]+') or l)
end
---@param path string
---@return boolean
local function file_ok(path)
local s = vim.uv.fs_stat(path)
return s and s.type == 'file' and (s.size or 0) > 0
return s ~= nil and s.type == 'file' and (s.size or 0) > 0
end
---@param dir string
---@return boolean
local function can_use_dir(dir)
return not not (vim.fn.isdirectory(dir) == 1 and vim.uv.fs_access(dir, 'W'))
end
---@return string[]
local function writable_spell_dirs_from_rtp()
local dirs = {}
for _, dir in ipairs(rtp_list()) do
@@ -57,6 +98,7 @@ local function writable_spell_dirs_from_rtp()
return dirs
end
---@return string?
local function ensure_target_dir()
local dir = vim.fs.abspath(vim.fs.joinpath(vim.fn.stdpath('data'), 'site/spell'))
if vim.fn.isdirectory(dir) == 0 and pcall(vim.fn.mkdir, dir, 'p') then
@@ -88,13 +130,15 @@ end
---
--- Treats status==0 as success if file exists.
---
--- @param url string
--- @param outpath string
--- @return boolean ok, integer|nil status, string|nil err
local function fetch_file_sync(url, outpath, timeout_ms)
local function fetch_file_sync(url, outpath)
local done, err, res = false, nil, nil
vim.net.request(url, { outpath = outpath }, function(e, r)
err, res, done = e, r, true
end)
vim.wait(timeout_ms or M.config.timeout_ms, function()
vim.wait(config.timeout_ms, function()
return done
end, 50, false)
@@ -103,6 +147,8 @@ local function fetch_file_sync(url, outpath, timeout_ms)
return not not ok, (status ~= 0 and status or nil), err
end
---@param lang string
---@return nvim.spellfile.Info
local function parse(lang)
local code = normalize_lang(lang)
local enc = 'utf-8'
@@ -128,7 +174,7 @@ local function parse(lang)
}
end
---@param info vim.spellfile.Info
---@param info nvim.spellfile.Info
local function download(info)
local dir = info.dir or ensure_target_dir()
if not dir then
@@ -143,10 +189,10 @@ local function download(info)
local spl_ascii = string.format('%s.ascii.spl', lang)
local sug_name = string.format('%s.%s.sug', lang, enc)
local url_utf8 = M.config.url .. '/' .. spl_utf8
local url_utf8 = config.url .. '/' .. spl_utf8
local out_utf8 = vim.fs.joinpath(dir, spl_utf8)
notify('Downloading ' .. spl_utf8 .. '')
local ok, st, err = fetch_file_sync(url_utf8, out_utf8, M.config.timeout_ms)
local ok, st, err = fetch_file_sync(url_utf8, out_utf8)
if not ok then
notify(
('Could not get %s (status %s): trying %s …'):format(
@@ -155,9 +201,9 @@ local function download(info)
spl_ascii
)
)
local url_ascii = M.config.url .. '/' .. spl_ascii
local url_ascii = config.url .. '/' .. spl_ascii
local out_ascii = vim.fs.joinpath(dir, spl_ascii)
local ok2, st2, err2 = fetch_file_sync(url_ascii, out_ascii, M.config.timeout_ms)
local ok2, st2, err2 = fetch_file_sync(url_ascii, out_ascii)
if not ok2 then
notify(
('No spell file available for %s (utf8:%s ascii:%s) — %s'):format(
@@ -182,10 +228,10 @@ local function download(info)
reload_spell_silent()
if not file_ok(vim.fs.joinpath(dir, sug_name)) then
local url_sug = M.config.url .. '/' .. sug_name
local url_sug = config.url .. '/' .. sug_name
local out_sug = vim.fs.joinpath(dir, sug_name)
notify('Downloading ' .. sug_name .. '')
local ok3, st3, err3 = fetch_file_sync(url_sug, out_sug, M.config.timeout_ms)
local ok3, st3, err3 = fetch_file_sync(url_sug, out_sug)
if ok3 then
notify('Saved ' .. sug_name .. ' to ' .. out_sug)
else
@@ -211,7 +257,10 @@ local function download(info)
M._done[info.key] = true
end
function M.load_file(lang)
--- Download spellfiles for language {lang} if available.
--- @param lang string Language code.
--- @return nvim.spellfile.Info?
function M.get(lang)
local info = parse(lang)
if #info.files == 0 then
return
@@ -221,14 +270,18 @@ function M.load_file(lang)
return
end
local answer = vim.fn.input(
string.format('No spell file found for %s (%s). Download? [y/N] ', info.lang, info.encoding)
local prompt = ('No spell file found for %s (%s). Download? [y/N] '):format(
info.lang,
info.encoding
)
if (answer or ''):lower() ~= 'y' then
return
end
download(info)
vim.ui.input({ prompt = prompt }, function(input)
-- properly clear the message window
vim.api.nvim_echo({ { ' ' } }, false, { kind = 'empty' })
if not input or input:lower() ~= 'y' then
return
end
download(info)
end)
return info
end

View File

@@ -6527,7 +6527,7 @@ vim.bo.spf = vim.bo.spellfile
--- encoding is used, Vim doesn't check it.
--- How the related spell files are found is explained here: `spell-load`.
---
--- If the `spellfile.vim` plugin is active and you use a language name
--- If the `spellfile.lua` plugin is active and you use a language name
--- for which Vim cannot find the .spl file in 'runtimepath' the plugin
--- will ask you if you want to download the file.
---

View File

@@ -1,16 +1,12 @@
if vim.g.loaded_spellfile_plugin ~= nil then
return
end
vim.g.loaded_spellfile_plugin = true
--- Downloads missing .spl file.
---
--- @param args { bufnr: integer, match: string }
local function on_spellfile_missing(args)
local spellfile = require('nvim.spellfile')
spellfile.load_file(args.match)
end
vim.api.nvim_create_autocmd('SpellFileMissing', {
group = vim.api.nvim_create_augroup('nvim_spellfile', { clear = true }),
pattern = '*',
group = vim.api.nvim_create_augroup('nvim.spellfile', {}),
desc = 'Download missing spell files when setting spelllang',
callback = on_spellfile_missing,
callback = function(args)
require('nvim.spellfile').get(args.match)
end,
})

View File

@@ -415,6 +415,7 @@ local config = {
section_order = {
'difftool.lua',
'editorconfig.lua',
'spellfile.lua',
'tohtml.lua',
'undotree.lua',
},
@@ -423,6 +424,7 @@ local config = {
'runtime/lua/tohtml.lua',
'runtime/pack/dist/opt/nvim.undotree/lua/undotree.lua',
'runtime/pack/dist/opt/nvim.difftool/lua/difftool.lua',
'runtime/lua/nvim/spellfile.lua',
},
fn_xform = function(fun)
if fun.module == 'editorconfig' then
@@ -430,11 +432,17 @@ local config = {
fun.table = true
fun.name = vim.split(fun.name, '.', { plain = true })[2] or fun.name
end
if vim.startswith(fun.module, 'nvim.') then
fun.module = fun.module:sub(#'nvim.' + 1)
end
end,
section_fmt = function(name)
return 'Builtin plugin: ' .. name:lower()
end,
helptag_fmt = function(name)
if name:lower() == 'spellfile' then
name = 'spellfile.lua'
end
return name:lower()
end,
},

View File

@@ -8547,7 +8547,7 @@ local options = {
encoding is used, Vim doesn't check it.
How the related spell files are found is explained here: |spell-load|.
If the |spellfile.vim| plugin is active and you use a language name
If the |spellfile.lua| plugin is active and you use a language name
for which Vim cannot find the .spl file in 'runtimepath' the plugin
will ask you if you want to download the file.

View File

@@ -1610,7 +1610,7 @@ static void spell_load_lang(char *lang)
// Plugins aren't loaded yet, so nvim/spellfile.lua cannot handle this case.
char autocmd_buf[512] = { 0 };
snprintf(autocmd_buf, sizeof(autocmd_buf),
"autocmd VimEnter * call v:lua.require'nvim.spellfile'.load_file('%s')|set spell",
"autocmd VimEnter * call v:lua.require'nvim.spellfile'.get('%s')|set spell",
lang);
do_cmdline_cmd(autocmd_buf);
} else {

View File

@@ -42,7 +42,7 @@ describe('nvim.spellfile', function()
local requests = 0
vim.net.request = function(...) requests = requests + 1 end
s.load_file('en_gb')
s.get('en_gb')
return { prompted = prompted, requests = requests }
]],
@@ -88,7 +88,7 @@ describe('nvim.spellfile', function()
end
end
s.load_file('en_gb')
s.get('en_gb')
local spl = vim.fs.joinpath(data_root, 'site/spell/en_gb.utf-8.spl')
local sug = vim.fs.joinpath(data_root, 'site/spell/en_gb.utf-8.sug')
@@ -136,7 +136,7 @@ describe('nvim.spellfile', function()
vim.net.request = function(_, _, cb) cb(nil, { status = 404 }) end
local info = s.load_file('zz')
local info = s.get('zz')
local done = s._done[info.key] == true
return { warns = warns, done = done, did_reload = did_reload }