commit f7041625f1abe4e9bfd2461fef23d1d74dc75c29
parent 9988d7142d65f1bd732480d7d942ccd16669c5b2
Author: Yochem van Rosmalen <git@yochem.nl>
Date: Thu, 22 Jan 2026 13:22:35 +0100
refactor(lua): use vim.fs instead of fnamemodify
Although powerful -- especially with chained modifiers --, the
readability (and therefore maintainability) of `fnamemodify()` and its
modifiers is often worse than a function name, giving less context and
having to rely on `:h filename-modifiers`. However, it is used plenty in
the Lua stdlib:
- 16x for the basename: `fnamemodify(path, ':t')`
- 7x for the parents: `fnamemodify(path, ':h')`
- 7x for the stem (filename w/o extension): `fnamemodify(path, ':r')`
- 6x for the absolute path: `fnamemodify(path, ':p')`
- 2x for the suffix: `fnamemodify(path, ':e')`
- 2x relative to the home directory: `fnamemodify(path, ':~')`
- 1x relative to the cwd: `fnamemodify(path, ':.')`
The `fs` module in the stdlib provides a cleaner interface for most of
these path operations: `vim.fs.basename` instead of `':t'`,
`vim.fs.dirname` instead of `':h'`, `vim.fs.abspath` instead of `':p'`.
This commit refactors the runtime to use these instead of fnamemodify.
Not all fnamemodify calls are removed; some have intrinsic differences
in behavior with the `vim.fs` replacement or do not yet have a
replacement in the Lua module, i.e. `:~`, `:.`, `:e` and `:r`.
Diffstat:
11 files changed, 31 insertions(+), 41 deletions(-)
diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua
@@ -262,7 +262,7 @@ local function get_path(name, sect)
-- find any that match the specified name
--- @param v string
local namematches = vim.tbl_filter(function(v)
- local tail = fn.fnamemodify(v, ':t')
+ local tail = vim.fs.basename(v)
return tail:find(name, 1, true) ~= nil
end, results) or {}
local sectmatches = {}
@@ -364,7 +364,7 @@ end
--- @return string name
--- @return string sect
local function parse_path(path)
- local tail = fn.fnamemodify(path, ':t')
+ local tail = vim.fs.basename(path)
if
path:match('%.[glx]z$')
or path:match('%.bz2$')
diff --git a/runtime/lua/nvim/spellfile.lua b/runtime/lua/nvim/spellfile.lua
@@ -119,6 +119,7 @@ local function ensure_target_dir()
return dirs[1]
end
+ -- vim.fs.relpath does not prepend '~/' while fnamemodify does
dir = vim.fn.fnamemodify(dir, ':~')
error(('cannot find a writable spell/ dir in runtimepath, and %s is not usable'):format(dir))
end
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
@@ -2863,15 +2863,6 @@ local function normalize_path(path, as_pattern)
return normal
end
-local abspath = function(x)
- return fn.fnamemodify(x, ':p')
-end
-if fn.has('win32') == 1 then
- abspath = function(x)
- return (fn.fnamemodify(x, ':p'):gsub('\\', '/'))
- end
-end
-
--- @class vim.filetype.add.filetypes
--- @inlinedoc
--- @field pattern? vim.filetype.mapping
@@ -3178,7 +3169,7 @@ function M.match(args)
if name then
name = normalize_path(name)
- local path = abspath(name)
+ local path = vim.fs.abspath(name)
do -- First check for the simple case where the full path exists as a key
local ft, on_detect = dispatch(filename[path], path, bufnr)
if ft then
@@ -3186,7 +3177,7 @@ function M.match(args)
end
end
- local tail = fn.fnamemodify(name, ':t')
+ local tail = vim.fs.basename(name)
do -- Next check against just the file name
local ft, on_detect = dispatch(filename[tail], path, bufnr)
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
@@ -18,6 +18,7 @@
-- `if line =~ '^\s*unwind_protect\>'` => `if matchregex(line, [[\c^\s*unwind_protect\>]])`
local fn = vim.fn
+local fs = vim.fs
local M = {}
@@ -395,7 +396,7 @@ end
--- @type vim.filetype.mapfn
function M.dat(path, bufnr)
- local file_name = fn.fnamemodify(path, ':t'):lower()
+ local file_name = fs.basename(path):lower()
-- Innovation data processing
if findany(file_name, { '^upstream%.dat$', '^upstream%..*%.dat$', '^.*%.upstream%.dat$' }) then
return 'upstreamdat'
@@ -423,7 +424,7 @@ end
-- to non-dep3patch files, such as README and other text files.
--- @type vim.filetype.mapfn
function M.dep3patch(path, bufnr)
- local file_name = fn.fnamemodify(path, ':t')
+ local file_name = fs.basename(path)
if file_name == 'series' then
return
end
@@ -562,7 +563,7 @@ function M.dsp(path, bufnr)
end
-- Test the filename
- local file_name = fn.fnamemodify(path, ':t')
+ local file_name = fs.basename(path)
if file_name:find('^[mM]akefile.*$') then
return 'make'
end
@@ -773,7 +774,7 @@ end
--- @return boolean
local function is_hare_module(dir, depth)
depth = math.max(depth, 0)
- for name, _ in vim.fs.dir(dir, { depth = depth + 1 }) do
+ for name, _ in fs.dir(dir, { depth = depth + 1 }) do
if name:find('%.ha$') then
return true
end
@@ -784,7 +785,7 @@ end
--- @type vim.filetype.mapfn
function M.haredoc(path, _)
if vim.g.filetype_haredoc then
- if is_hare_module(vim.fs.dirname(path), vim.g.haredoc_search_depth or 1) then
+ if is_hare_module(fs.dirname(path), vim.g.haredoc_search_depth or 1) then
return 'haredoc'
end
end
@@ -1064,8 +1065,8 @@ end
--- – files in POSIX M4
--- @type vim.filetype.mapfn
function M.m4(path, bufnr)
- local fname = fn.fnamemodify(path, ':t')
- path = fn.fnamemodify(path, ':p:h')
+ local fname = fs.basename(path)
+ path = fs.dirname(fs.abspath(path))
if fname:find('html%.m4$') then
return 'htmlm4'
@@ -1121,7 +1122,7 @@ function M.make(path, bufnr)
vim.b.make_flavor = nil
-- 1. filename
- local file_name = fn.fnamemodify(path, ':t')
+ local file_name = fs.basename(path)
if file_name == 'BSDmakefile' then
vim.b.make_flavor = 'bsd'
return 'make'
@@ -1187,7 +1188,7 @@ end
--- @param path string
--- @return string?
function M.me(path)
- local filename = fn.fnamemodify(path, ':t'):lower()
+ local filename = fs.basename(path):lower()
if filename ~= 'read.me' and filename ~= 'click.me' then
return 'nroff'
end
@@ -1297,7 +1298,7 @@ end
--- (Slow test) If a file contains a 'use' statement then it is almost certainly a Perl file.
--- @type vim.filetype.mapfn
function M.perl(path, bufnr)
- local dir_name = vim.fs.dirname(path)
+ local dir_name = fs.dirname(path)
if fn.fnamemodify(path, '%:e') == 't' and (dir_name == 't' or dir_name == 'xt') then
return 'perl'
end
@@ -1529,7 +1530,7 @@ function M.rules(path)
return 'hog'
end
--- @cast config_lines -string
- local dir = fn.fnamemodify(path, ':h')
+ local dir = fs.dirname(path)
for _, line in ipairs(config_lines) do
local match = line:match(udev_rules_pattern)
if match then
diff --git a/runtime/lua/vim/health/health.lua b/runtime/lua/vim/health/health.lua
@@ -215,7 +215,7 @@ local function check_rplugin_manifest()
local contents = vim.fn.join(vim.fn.readfile(script))
if vim.regex([[\<\%(from\|import\)\s\+neovim\>]]):match_str(contents) then
if vim.regex([[[\/]__init__\.py$]]):match_str(script) then
- script = vim.fn.tr(vim.fn.fnamemodify(script, ':h'), '\\', '/')
+ script = vim.fs.normalize(vim.fs.dirname(script))
end
if not existing_rplugins[script] then
local msg = vim.fn.printf('"%s" is not registered.', vim.fs.basename(path))
diff --git a/runtime/lua/vim/loader.lua b/runtime/lua/vim/loader.lua
@@ -429,7 +429,7 @@ function M.enable(enable)
M.enabled = enable
if enable then
- vim.fn.mkdir(vim.fn.fnamemodify(M.path, ':p'), 'p')
+ vim.fn.mkdir(vim.fs.abspath(M.path), 'p')
_G.loadfile = loadfile_cached
-- add Lua loader
table.insert(loaders, 2, loader_cached)
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
@@ -91,6 +91,7 @@ local function check_active_clients()
else
dirs_info = string.format(
'- Root directory: %s',
+ -- vim.fs.relpath does not prepend '~/' while fnamemodify does
client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~')
) or nil
end
diff --git a/runtime/lua/vim/provider/health.lua b/runtime/lua/vim/provider/health.lua
@@ -535,18 +535,12 @@ local function version_info(python)
return { python_version, 'unable to load neovim Python module', pypi_version, nvim_path }
end
- -- Assuming that multiple versions of a package are installed, sort them
- -- numerically in descending order.
+ -- Assuming that multiple versions of a package are installed as
+ -- `<semver>/<metapath>`, sort them on semantic version in descending order.
local function compare(metapath1, metapath2)
- local a = vim.fn.matchstr(vim.fn.fnamemodify(metapath1, ':p:h:t'), [[[0-9.]\+]])
- local b = vim.fn.matchstr(vim.fn.fnamemodify(metapath2, ':p:h:t'), [[[0-9.]\+]])
- if a == b then
- return 0
- elseif a > b then
- return 1
- else
- return -1
- end
+ local dir1 = vim.fs.basename(vim.fs.dirname(vim.fs.abspath(metapath1)))
+ local dir2 = vim.fs.basename(vim.fs.dirname(vim.fs.abspath(metapath2)))
+ return vim.version.cmp(dir1, dir2)
end
-- Try to get neovim.VERSION (added in 0.1.11dev).
@@ -576,6 +570,7 @@ local function version_info(python)
end
end
+ -- vim.fs.relpath does not prepend '~/' while fnamemodify does
local nvim_path_base = vim.fn.fnamemodify(nvim_path, [[:~:h]])
local version_status = 'unknown; ' .. nvim_path_base
if not is_bad_response(nvim_version) and not is_bad_response(pypi_version) then
diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua
@@ -40,8 +40,9 @@ end
local function guess_query_lang(buf)
local filename = api.nvim_buf_get_name(buf)
if filename ~= '' then
- local resolved_filename = vim.F.npcall(vim.fn.fnamemodify, filename, ':p:h:t')
- return resolved_filename and vim.treesitter.language.get_lang(resolved_filename)
+ -- get <lang> from /path/<lang>/<query_type>.scm
+ local resolved = vim.fs.basename(vim.fs.dirname(vim.fs.abspath(filename)))
+ return vim.treesitter.language.get_lang(resolved)
end
end
diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua
@@ -378,7 +378,7 @@ function M.inspect_tree(opts)
local opts_title = opts.title
if not opts_title then
local bufname = api.nvim_buf_get_name(buf)
- title = string.format('Syntax tree for %s', vim.fn.fnamemodify(bufname, ':.'))
+ title = ('Syntax tree for %s'):format(vim.fs.relpath('.', bufname))
elseif type(opts_title) == 'function' then
title = opts_title(buf)
end
diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua
@@ -101,7 +101,7 @@ function M.check()
end)
for _, query in ipairs(queries) do
- local dir = vim.fn.fnamemodify(query.path, ':h')
+ local dir = vim.fs.dirname(query.path)
health.ok(string.format('%-15s %-15s %s', lang, query.type, dir))
end
end