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`.
This commit is contained in:
Yochem van Rosmalen
2026-01-22 13:22:35 +01:00
committed by Lewis Russell
parent 9988d7142d
commit f7041625f1
11 changed files with 31 additions and 41 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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