feat(stdlib): vim.fs.ext() returns file extension #36997

Problem:
Checking the extension of a file is done often, e.g. in Nvim's codebase
for differentiating Lua and Vimscript files in the runtime. The current
way to do this in Lua is (1) a Lua pattern match, which has pitfalls
such as not considering filenames starting with a dot, or (2)
fnamemodify() which is both hard to discover and hard to use / read if
not very familiar with the possible modifiers.

vim.fs.ext() returns the file extension including the leading dot of
the extension. Similar to the "file extension" implementation of many
other stdlibs (including fnamemodify(file, ":e")), a leading dot
doesn't indicate the start of the extension. E.g.: the .git folder in a
repository doesn't have the extension .git, but it simply has no
extension, similar to a folder named git or any other filename without
dot(s).
This commit is contained in:
Yochem van Rosmalen
2026-03-20 10:08:00 +01:00
committed by GitHub
parent 1244fe157f
commit 72a63346d8
7 changed files with 57 additions and 5 deletions

View File

@@ -1038,7 +1038,7 @@ enable({name}, {enable}) *vim.lsp.enable()*
config to activate: >lua
vim.lsp.config('lua_ls', {
root_dir = function(bufnr, on_dir)
if not vim.fn.bufname(bufnr):match('%.txt$') then
if vim.fs.ext(vim.fn.bufname(bufnr)) ~= 'txt' then
on_dir(vim.fn.getcwd())
end
end

View File

@@ -2537,6 +2537,28 @@ vim.fs.dirname({file}) *vim.fs.dirname()*
Return: ~
(`string?`) Parent directory of {file}
vim.fs.ext({file}, {opts}) *vim.fs.ext()*
Return the file's last extension, if any.
Similar to |fnamemodify()| with the |::e| modifier. The extension does not
include a leading period.
Examples: >lua
vim.fs.ext('archive.tar.gz') -- 'gz'
vim.fs.ext('~/.git') -- ''
vim.fs.ext('plugin/myplug.lua') -- 'lua'
<
Attributes: ~
Since: 0.12.0
Parameters: ~
• {file} (`string`) Path
• {opts} (`table?`) Reserved for future use
Return: ~
(`string`) Extension of {file}
vim.fs.find({names}, {opts}) *vim.fs.find()*
Find files or directories (or other items as specified by `opts.type`) in
the given path.
@@ -2744,7 +2766,7 @@ vim.fs.root({source}, {marker}) *vim.fs.root()*
-- Find the parent directory containing any file with a .csproj extension
vim.fs.root(0, function(name, path)
return name:match('%.csproj$') ~= nil
return vim.fs.ext(name) == 'csproj'
end)
-- Find the first ancestor directory containing EITHER "stylua.toml" or ".luarc.json"; if

View File

@@ -354,6 +354,7 @@ LUA
• |vim.json.encode()| has an `sort_keys` option.
• |Range:is_empty()| to check if a |vim.Range| is empty.
• |vim.json.decode()| has an `skip_comments` option.
• |vim.fs.ext()| returns the last extension of a file.
OPTIONS

View File

@@ -409,7 +409,7 @@ end
---
--- -- Find the parent directory containing any file with a .csproj extension
--- vim.fs.root(0, function(name, path)
--- return name:match('%.csproj$') ~= nil
--- return vim.fs.ext(name) == 'csproj'
--- end)
---
--- -- Find the first ancestor directory containing EITHER "stylua.toml" or ".luarc.json"; if
@@ -835,4 +835,27 @@ function M.relpath(base, target, opts)
return vim.startswith(target, base) and target:sub(#base + 1) or nil
end
--- Return the file's last extension, if any.
---
--- Similar to |fnamemodify()| with the |::e| modifier. The extension does not include a leading
--- period.
---
--- Examples:
---
--- ```lua
--- vim.fs.ext('archive.tar.gz') -- 'gz'
--- vim.fs.ext('~/.git') -- ''
--- vim.fs.ext('plugin/myplug.lua') -- 'lua'
--- ```
---
---@since 14
---@param file string Path
---@param opts table? Reserved for future use
---@return string Extension of {file}
function M.ext(file, opts)
vim.validate('file', file, 'string')
vim.validate('opts', opts, 'table', true)
return vim.fn.fnamemodify(file, ':e')
end
return M

View File

@@ -311,7 +311,7 @@ function M.error(msg, ...)
end
local path2name = function(path)
if path:match('%.lua$') then
if vim.fs.ext(path) == 'lua' then
-- Lua: transform "../lua/vim/lsp/health.lua" into "vim.lsp"
-- Get full path, make sure all slashes are '/'

View File

@@ -585,7 +585,7 @@ end
--- ```lua
--- vim.lsp.config('lua_ls', {
--- root_dir = function(bufnr, on_dir)
--- if not vim.fn.bufname(bufnr):match('%.txt$') then
--- if vim.fs.ext(vim.fn.bufname(bufnr)) ~= 'txt' then
--- on_dir(vim.fn.getcwd())
--- end
--- end

View File

@@ -753,4 +753,10 @@ describe('vim.fs', function()
assert_rm_symlinked_dir({ recursive = true, force = true })
end)
end)
describe('ext()', function()
it('works', function()
-- See test/functional/vimscript/fnamemodify_spec.lua
end)
end)
end)