feat(defaults): store spellfile in stdpath('data') #33048

Problem:
First rtp directory is unpredictable and not in line with XDG
base spec.

Solution:
Use stdpath('data')/spell as directory if 'spellfile' is not set.

Co-authored-by: zeertzjq <zeertzjq@outlook.com>
Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
This commit is contained in:
Yochem van Rosmalen
2025-04-04 14:21:57 +02:00
committed by GitHub
parent 4983fa45fc
commit b10cb0296a
8 changed files with 76 additions and 59 deletions

View File

@@ -177,7 +177,8 @@ CHANGED FEATURES *news-changed*
These existing features changed their behavior. These existing features changed their behavior.
todo 'spellfile' location defaults to `stdpath("data").."/spell/"` instead of the
first writable directoy in 'runtimepath'.
============================================================================== ==============================================================================
REMOVED FEATURES *news-removed* REMOVED FEATURES *news-removed*

View File

@@ -5755,11 +5755,10 @@ A jump table for the options with a short description can be found at |Q_op|.
It may also be a comma-separated list of names. A count before the It may also be a comma-separated list of names. A count before the
|zg| and |zw| commands can be used to access each. This allows using |zg| and |zw| commands can be used to access each. This allows using
a personal word list file and a project word list file. a personal word list file and a project word list file.
When a word is added while this option is empty Vim will set it for When a word is added while this option is empty Nvim will use
you: Using the first directory in 'runtimepath' that is writable. If (and auto-create) `stdpath('data')/spell/`. For the file name the
there is no "spell" directory yet it will be created. For the file first language name that appears in 'spelllang' is used, ignoring the
name the first language name that appears in 'spelllang' is used, region.
ignoring the region.
The resulting ".spl" file will be used for spell checking, it does not The resulting ".spl" file will be used for spell checking, it does not
have to appear in 'spelllang'. have to appear in 'spelllang'.
Normally one file is used for all regions, but you can add the region Normally one file is used for all regions, but you can add the region

View File

@@ -79,6 +79,7 @@ Defaults *defaults* *nvim-defaults*
- 'showcmd' is enabled - 'showcmd' is enabled
- 'sidescroll' defaults to 1 - 'sidescroll' defaults to 1
- 'smarttab' is enabled - 'smarttab' is enabled
- 'spellfile' defaults to `stdpath("data").."/spell/"`
- 'startofline' is disabled - 'startofline' is disabled
- 'switchbuf' defaults to "uselast" - 'switchbuf' defaults to "uselast"
- 'tabpagemax' defaults to 50 - 'tabpagemax' defaults to 50

View File

@@ -6123,11 +6123,10 @@ vim.bo.spc = vim.bo.spellcapcheck
--- It may also be a comma-separated list of names. A count before the --- It may also be a comma-separated list of names. A count before the
--- `zg` and `zw` commands can be used to access each. This allows using --- `zg` and `zw` commands can be used to access each. This allows using
--- a personal word list file and a project word list file. --- a personal word list file and a project word list file.
--- When a word is added while this option is empty Vim will set it for --- When a word is added while this option is empty Nvim will use
--- you: Using the first directory in 'runtimepath' that is writable. If --- (and auto-create) `stdpath('data')/spell/`. For the file name the
--- there is no "spell" directory yet it will be created. For the file --- first language name that appears in 'spelllang' is used, ignoring the
--- name the first language name that appears in 'spelllang' is used, --- region.
--- ignoring the region.
--- The resulting ".spl" file will be used for spell checking, it does not --- The resulting ".spl" file will be used for spell checking, it does not
--- have to appear in 'spelllang'. --- have to appear in 'spelllang'.
--- Normally one file is used for all regions, but you can add the region --- Normally one file is used for all regions, but you can add the region

View File

@@ -8158,11 +8158,10 @@ local options = {
It may also be a comma-separated list of names. A count before the It may also be a comma-separated list of names. A count before the
|zg| and |zw| commands can be used to access each. This allows using |zg| and |zw| commands can be used to access each. This allows using
a personal word list file and a project word list file. a personal word list file and a project word list file.
When a word is added while this option is empty Vim will set it for When a word is added while this option is empty Nvim will use
you: Using the first directory in 'runtimepath' that is writable. If (and auto-create) `stdpath('data')/spell/`. For the file name the
there is no "spell" directory yet it will be created. For the file first language name that appears in 'spelllang' is used, ignoring the
name the first language name that appears in 'spelllang' is used, region.
ignoring the region.
The resulting ".spl" file will be used for spell checking, it does not The resulting ".spl" file will be used for spell checking, it does not
have to appear in 'spelllang'. have to appear in 'spelllang'.
Normally one file is used for all regions, but you can add the region Normally one file is used for all regions, but you can add the region

View File

@@ -262,6 +262,7 @@
#include "nvim/os/input.h" #include "nvim/os/input.h"
#include "nvim/os/os.h" #include "nvim/os/os.h"
#include "nvim/os/os_defs.h" #include "nvim/os/os_defs.h"
#include "nvim/os/stdpaths_defs.h"
#include "nvim/os/time.h" #include "nvim/os/time.h"
#include "nvim/os/time_defs.h" #include "nvim/os/time_defs.h"
#include "nvim/path.h" #include "nvim/path.h"
@@ -5547,9 +5548,11 @@ void spell_add_word(char *word, int len, SpellAddType what, int idx, bool undo)
} }
// Initialize 'spellfile' for the current buffer. // Initialize 'spellfile' for the current buffer.
//
// If the location does not exist, create it. Defaults to
// stdpath("data") + "/spell/{spelllang}.{encoding}.add".
static void init_spellfile(void) static void init_spellfile(void)
{ {
int l;
char *lend; char *lend;
bool aspath = false; bool aspath = false;
char *lstart = curbuf->b_s.b_p_spl; char *lstart = curbuf->b_s.b_p_spl;
@@ -5558,8 +5561,6 @@ static void init_spellfile(void)
return; return;
} }
char *buf = xmalloc(MAXPATHL);
// Find the end of the language name. Exclude the region. If there // Find the end of the language name. Exclude the region. If there
// is a path separator remember the start of the tail. // is a path separator remember the start of the tail.
for (lend = curwin->w_s->b_p_spl; *lend != NUL for (lend = curwin->w_s->b_p_spl; *lend != NUL
@@ -5570,49 +5571,40 @@ static void init_spellfile(void)
} }
} }
// Loop over all entries in 'runtimepath'. Use the first one where we char *buf = xmalloc(MAXPATHL);
// are allowed to write. size_t buf_len = MAXPATHL;
char *rtp = p_rtp;
while (*rtp != NUL) {
if (aspath) {
// Use directory of an entry with path, e.g., for
// "/dir/lg.utf-8.spl" use "/dir".
xmemcpyz(buf, curbuf->b_s.b_p_spl, (size_t)(lstart - curbuf->b_s.b_p_spl - 1));
} else {
// Copy the path from 'runtimepath' to buf[].
copy_option_part(&rtp, buf, MAXPATHL, ",");
}
if (os_file_is_writable(buf) == 2) {
// Use the first language name from 'spelllang' and the
// encoding used in the first loaded .spl file.
if (aspath) {
xmemcpyz(buf, curbuf->b_s.b_p_spl, (size_t)(lend - curbuf->b_s.b_p_spl));
} else {
// Create the "spell" directory if it doesn't exist yet.
l = (int)strlen(buf);
vim_snprintf(buf + l, MAXPATHL - (size_t)l, "/spell");
if (os_file_is_writable(buf) != 2) {
os_mkdir(buf, 0755);
}
l = (int)strlen(buf); if (!aspath) {
vim_snprintf(buf + l, MAXPATHL - (size_t)l, char *xdg_path = get_xdg_home(kXDGDataHome);
"/%.*s", (int)(lend - lstart), lstart); xstrlcpy(buf, xdg_path, buf_len);
} xfree(xdg_path);
l = (int)strlen(buf);
char *fname = LANGP_ENTRY(curwin->w_s->b_langp, 0) xstrlcat(buf, "/spell", buf_len);
->lp_slang->sl_fname;
vim_snprintf(buf + l, MAXPATHL - (size_t)l, ".%s.add", char *failed_dir;
((fname != NULL if (os_mkdir_recurse(buf, 0755, &failed_dir, NULL) != 0) {
&& strstr(path_tail(fname), ".ascii.") != NULL) xfree(buf);
? "ascii" xfree(failed_dir);
: spell_enc())); return;
set_option_value_give_err(kOptSpellfile, CSTR_AS_OPTVAL(buf), OPT_LOCAL);
break;
} }
aspath = false; } else {
if ((size_t)(lend - curbuf->b_s.b_p_spl) >= buf_len) {
xfree(buf);
return;
}
xmemcpyz(buf, curbuf->b_s.b_p_spl, (size_t)(lend - curbuf->b_s.b_p_spl));
} }
// Append spelllang
vim_snprintf(buf + strlen(buf), buf_len - strlen(buf), "/%.*s", (int)(lend - lstart), lstart);
// Append ".ascii.add" or ".{enc}.add"
char *fname = LANGP_ENTRY(curwin->w_s->b_langp, 0)->lp_slang->sl_fname;
const char *enc_suffix =
(fname != NULL && strstr(path_tail(fname), ".ascii.") != NULL) ? "ascii" : spell_enc();
vim_snprintf(buf + strlen(buf), buf_len - strlen(buf), ".%s.add", enc_suffix);
set_option_value_give_err(kOptSpellfile, CSTR_AS_OPTVAL(buf), OPT_LOCAL);
xfree(buf); xfree(buf);
} }

View File

@@ -14,7 +14,7 @@ local testdir = 'Xtest-functional-spell-spellfile.d'
describe('spellfile', function() describe('spellfile', function()
before_each(function() before_each(function()
clear() clear({ env = { XDG_DATA_HOME = testdir .. '/xdg_data' } })
rmdir(testdir) rmdir(testdir)
mkdir(testdir) mkdir(testdir)
mkdir(testdir .. '/spell') mkdir(testdir .. '/spell')
@@ -117,4 +117,29 @@ describe('spellfile', function()
local fname = fn.fnamemodify(testdir .. '/spell/spell.add', ':p') local fname = fn.fnamemodify(testdir .. '/spell/spell.add', ':p')
api.nvim_set_option_value('spellfile', fname, {}) api.nvim_set_option_value('spellfile', fname, {})
end) end)
describe('default location', function()
it("is stdpath('data')/spell/en.utf-8.add", function()
n.command('set spell')
n.insert('abc')
n.feed('zg')
eq(
t.fix_slashes(fn.stdpath('data') .. '/spell/en.utf-8.add'),
t.fix_slashes(api.nvim_get_option_value('spellfile', {}))
)
end)
it("is not set if stdpath('data') is not writable", function()
n.command('set spell')
fn.writefile({ '' }, testdir .. '/xdg_data')
n.insert('abc')
eq("Vim(normal):E764: Option 'spellfile' is not set", exc_exec('normal! zg'))
end)
it("is not set if 'spelllang' is not set", function()
n.command('set spell spelllang=')
n.insert('abc')
eq("Vim(normal):E764: Option 'spellfile' is not set", exc_exec('normal! zg'))
end)
end)
end) end)

View File

@@ -1123,6 +1123,7 @@ endfunc
" When 'spellfile' is not set, adding a new good word will automatically set " When 'spellfile' is not set, adding a new good word will automatically set
" the 'spellfile' " the 'spellfile'
func Test_init_spellfile() func Test_init_spellfile()
throw 'Skipped: Nvim defaults spellfile to stdpath("data")/spell/'
let save_rtp = &rtp let save_rtp = &rtp
let save_encoding = &encoding let save_encoding = &encoding
call mkdir('Xrtp/spell', 'pR') call mkdir('Xrtp/spell', 'pR')