mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 19:38:20 +00:00
Merge pull request #18583 from gpanders/path-root
feat(fs): add vim.fs module
This commit is contained in:
@@ -2147,4 +2147,129 @@ set({mode}, {lhs}, {rhs}, {opts}) *vim.keymap.set()*
|
|||||||
See also: ~
|
See also: ~
|
||||||
|nvim_set_keymap()|
|
|nvim_set_keymap()|
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
Lua module: fs *lua-fs*
|
||||||
|
|
||||||
|
basename({file}) *vim.fs.basename()*
|
||||||
|
Return the basename of the given file or directory
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
{file} (string) File or directory
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
(string) Basename of {file}
|
||||||
|
|
||||||
|
dir({path}) *vim.fs.dir()*
|
||||||
|
Return an iterator over the files and directories located in
|
||||||
|
{path}
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
{path} (string) An absolute or relative path to the
|
||||||
|
directory to iterate over. The path is first
|
||||||
|
normalized |vim.fs.normalize()|.
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
Iterator over files and directories in {path}. Each
|
||||||
|
iteration yields two values: name and type. Each "name" is
|
||||||
|
the basename of the file or directory relative to {path}.
|
||||||
|
Type is one of "file" or "directory".
|
||||||
|
|
||||||
|
dirname({file}) *vim.fs.dirname()*
|
||||||
|
Return the parent directory of the given file or directory
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
{file} (string) File or directory
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
(string) Parent directory of {file}
|
||||||
|
|
||||||
|
find({names}, {opts}) *vim.fs.find()*
|
||||||
|
Find files or directories in the given path.
|
||||||
|
|
||||||
|
Finds any files or directories given in {names} starting from
|
||||||
|
{path}. If {upward} is "true" then the search traverses upward
|
||||||
|
through parent directories; otherwise, the search traverses
|
||||||
|
downward. Note that downward searches are recursive and may
|
||||||
|
search through many directories! If {stop} is non-nil, then
|
||||||
|
the search stops when the directory given in {stop} is
|
||||||
|
reached. The search terminates when {limit} (default 1)
|
||||||
|
matches are found. The search can be narrowed to find only
|
||||||
|
files or or only directories by specifying {type} to be "file"
|
||||||
|
or "directory", respectively.
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
{names} (string|table) Names of the files and directories
|
||||||
|
to find. Must be base names, paths and globs are
|
||||||
|
not supported.
|
||||||
|
{opts} (table) Optional keyword arguments:
|
||||||
|
• path (string): Path to begin searching from. If
|
||||||
|
omitted, the current working directory is used.
|
||||||
|
• upward (boolean, default false): If true,
|
||||||
|
search upward through parent directories.
|
||||||
|
Otherwise, search through child directories
|
||||||
|
(recursively).
|
||||||
|
• stop (string): Stop searching when this
|
||||||
|
directory is reached. The directory itself is
|
||||||
|
not searched.
|
||||||
|
• type (string): Find only files ("file") or
|
||||||
|
directories ("directory"). If omitted, both
|
||||||
|
files and directories that match {name} are
|
||||||
|
included.
|
||||||
|
• limit (number, default 1): Stop the search
|
||||||
|
after finding this many matches. Use
|
||||||
|
`math.huge` to place no limit on the number of
|
||||||
|
matches.
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
(table) The paths of all matching files or directories
|
||||||
|
|
||||||
|
normalize({path}) *vim.fs.normalize()*
|
||||||
|
Normalize a path to a standard format. A tilde (~) character
|
||||||
|
at the beginning of the path is expanded to the user's home
|
||||||
|
directory and any backslash (\) characters are converted to
|
||||||
|
forward slashes (/). Environment variables are also expanded.
|
||||||
|
|
||||||
|
Example: >
|
||||||
|
|
||||||
|
vim.fs.normalize('C:\Users\jdoe')
|
||||||
|
=> 'C:/Users/jdoe'
|
||||||
|
|
||||||
|
vim.fs.normalize('~/src/neovim')
|
||||||
|
=> '/home/jdoe/src/neovim'
|
||||||
|
|
||||||
|
vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')
|
||||||
|
=> '/Users/jdoe/.config/nvim/init.vim'
|
||||||
|
<
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
{path} (string) Path to normalize
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
(string) Normalized path
|
||||||
|
|
||||||
|
parents({start}) *vim.fs.parents()*
|
||||||
|
Iterate over all the parents of the given file or directory.
|
||||||
|
|
||||||
|
Example: >
|
||||||
|
|
||||||
|
local root_dir
|
||||||
|
for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do
|
||||||
|
if vim.fn.isdirectory(dir .. "/.git") == 1 then
|
||||||
|
root_dir = dir
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if root_dir then
|
||||||
|
print("Found git repository at", root_dir)
|
||||||
|
end
|
||||||
|
<
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
{start} (string) Initial file or directory.
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
(function) Iterator
|
||||||
|
|
||||||
vim:tw=78:ts=8:ft=help:norl:
|
vim:tw=78:ts=8:ft=help:norl:
|
||||||
|
@@ -50,6 +50,7 @@ for k, v in pairs({
|
|||||||
keymap = true,
|
keymap = true,
|
||||||
ui = true,
|
ui = true,
|
||||||
health = true,
|
health = true,
|
||||||
|
fs = true,
|
||||||
}) do
|
}) do
|
||||||
vim._submodules[k] = v
|
vim._submodules[k] = v
|
||||||
end
|
end
|
||||||
|
205
runtime/lua/vim/fs.lua
Normal file
205
runtime/lua/vim/fs.lua
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
local M = {}
|
||||||
|
|
||||||
|
--- Iterate over all the parents of the given file or directory.
|
||||||
|
---
|
||||||
|
--- Example:
|
||||||
|
--- <pre>
|
||||||
|
--- local root_dir
|
||||||
|
--- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do
|
||||||
|
--- if vim.fn.isdirectory(dir .. "/.git") == 1 then
|
||||||
|
--- root_dir = dir
|
||||||
|
--- break
|
||||||
|
--- end
|
||||||
|
--- end
|
||||||
|
---
|
||||||
|
--- if root_dir then
|
||||||
|
--- print("Found git repository at", root_dir)
|
||||||
|
--- end
|
||||||
|
--- </pre>
|
||||||
|
---
|
||||||
|
---@param start (string) Initial file or directory.
|
||||||
|
---@return (function) Iterator
|
||||||
|
function M.parents(start)
|
||||||
|
return function(_, dir)
|
||||||
|
local parent = M.dirname(dir)
|
||||||
|
if parent == dir then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return parent
|
||||||
|
end,
|
||||||
|
nil,
|
||||||
|
start
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Return the parent directory of the given file or directory
|
||||||
|
---
|
||||||
|
---@param file (string) File or directory
|
||||||
|
---@return (string) Parent directory of {file}
|
||||||
|
function M.dirname(file)
|
||||||
|
return vim.fn.fnamemodify(file, ':h')
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Return the basename of the given file or directory
|
||||||
|
---
|
||||||
|
---@param file (string) File or directory
|
||||||
|
---@return (string) Basename of {file}
|
||||||
|
function M.basename(file)
|
||||||
|
return vim.fn.fnamemodify(file, ':t')
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Return an iterator over the files and directories located in {path}
|
||||||
|
---
|
||||||
|
---@param path (string) An absolute or relative path to the directory to iterate
|
||||||
|
--- over. The path is first normalized |vim.fs.normalize()|.
|
||||||
|
---@return Iterator over files and directories in {path}. Each iteration yields
|
||||||
|
--- two values: name and type. Each "name" is the basename of the file or
|
||||||
|
--- directory relative to {path}. Type is one of "file" or "directory".
|
||||||
|
function M.dir(path)
|
||||||
|
return function(fs)
|
||||||
|
return vim.loop.fs_scandir_next(fs)
|
||||||
|
end, vim.loop.fs_scandir(M.normalize(path))
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Find files or directories in the given path.
|
||||||
|
---
|
||||||
|
--- Finds any files or directories given in {names} starting from {path}. If
|
||||||
|
--- {upward} is "true" then the search traverses upward through parent
|
||||||
|
--- directories; otherwise, the search traverses downward. Note that downward
|
||||||
|
--- searches are recursive and may search through many directories! If {stop}
|
||||||
|
--- is non-nil, then the search stops when the directory given in {stop} is
|
||||||
|
--- reached. The search terminates when {limit} (default 1) matches are found.
|
||||||
|
--- The search can be narrowed to find only files or or only directories by
|
||||||
|
--- specifying {type} to be "file" or "directory", respectively.
|
||||||
|
---
|
||||||
|
---@param names (string|table) Names of the files and directories to find. Must
|
||||||
|
--- be base names, paths and globs are not supported.
|
||||||
|
---@param opts (table) Optional keyword arguments:
|
||||||
|
--- - path (string): Path to begin searching from. If
|
||||||
|
--- omitted, the current working directory is used.
|
||||||
|
--- - upward (boolean, default false): If true, search
|
||||||
|
--- upward through parent directories. Otherwise,
|
||||||
|
--- search through child directories
|
||||||
|
--- (recursively).
|
||||||
|
--- - stop (string): Stop searching when this directory is
|
||||||
|
--- reached. The directory itself is not searched.
|
||||||
|
--- - type (string): Find only files ("file") or
|
||||||
|
--- directories ("directory"). If omitted, both
|
||||||
|
--- files and directories that match {name} are
|
||||||
|
--- included.
|
||||||
|
--- - limit (number, default 1): Stop the search after
|
||||||
|
--- finding this many matches. Use `math.huge` to
|
||||||
|
--- place no limit on the number of matches.
|
||||||
|
---@return (table) The paths of all matching files or directories
|
||||||
|
function M.find(names, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
vim.validate({
|
||||||
|
names = { names, { 's', 't' } },
|
||||||
|
path = { opts.path, 's', true },
|
||||||
|
upward = { opts.upward, 'b', true },
|
||||||
|
stop = { opts.stop, 's', true },
|
||||||
|
type = { opts.type, 's', true },
|
||||||
|
limit = { opts.limit, 'n', true },
|
||||||
|
})
|
||||||
|
|
||||||
|
names = type(names) == 'string' and { names } or names
|
||||||
|
|
||||||
|
local path = opts.path or vim.loop.cwd()
|
||||||
|
local stop = opts.stop
|
||||||
|
local limit = opts.limit or 1
|
||||||
|
|
||||||
|
local matches = {}
|
||||||
|
|
||||||
|
---@private
|
||||||
|
local function add(match)
|
||||||
|
matches[#matches + 1] = match
|
||||||
|
if #matches == limit then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.upward then
|
||||||
|
---@private
|
||||||
|
local function test(p)
|
||||||
|
local t = {}
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
local f = p .. '/' .. name
|
||||||
|
local stat = vim.loop.fs_stat(f)
|
||||||
|
if stat and (not opts.type or opts.type == stat.type) then
|
||||||
|
t[#t + 1] = f
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, match in ipairs(test(path)) do
|
||||||
|
if add(match) then
|
||||||
|
return matches
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for parent in M.parents(path) do
|
||||||
|
if stop and parent == stop then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, match in ipairs(test(parent)) do
|
||||||
|
if add(match) then
|
||||||
|
return matches
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local dirs = { path }
|
||||||
|
while #dirs > 0 do
|
||||||
|
local dir = table.remove(dirs, 1)
|
||||||
|
if stop and dir == stop then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
for other, type in M.dir(dir) do
|
||||||
|
local f = dir .. '/' .. other
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
if name == other and (not opts.type or opts.type == type) then
|
||||||
|
if add(f) then
|
||||||
|
return matches
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if type == 'directory' then
|
||||||
|
dirs[#dirs + 1] = f
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return matches
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Normalize a path to a standard format. A tilde (~) character at the
|
||||||
|
--- beginning of the path is expanded to the user's home directory and any
|
||||||
|
--- backslash (\\) characters are converted to forward slashes (/). Environment
|
||||||
|
--- variables are also expanded.
|
||||||
|
---
|
||||||
|
--- Example:
|
||||||
|
--- <pre>
|
||||||
|
--- vim.fs.normalize('C:\\Users\\jdoe')
|
||||||
|
--- => 'C:/Users/jdoe'
|
||||||
|
---
|
||||||
|
--- vim.fs.normalize('~/src/neovim')
|
||||||
|
--- => '/home/jdoe/src/neovim'
|
||||||
|
---
|
||||||
|
--- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')
|
||||||
|
--- => '/Users/jdoe/.config/nvim/init.vim'
|
||||||
|
--- </pre>
|
||||||
|
---
|
||||||
|
---@param path (string) Path to normalize
|
||||||
|
---@return (string) Normalized path
|
||||||
|
function M.normalize(path)
|
||||||
|
vim.validate({ path = { path, 's' } })
|
||||||
|
return (path:gsub('^~/', vim.env.HOME .. '/'):gsub('%$([%w_]+)', vim.env):gsub('\\', '/'))
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
@@ -134,6 +134,7 @@ CONFIG = {
|
|||||||
'ui.lua',
|
'ui.lua',
|
||||||
'filetype.lua',
|
'filetype.lua',
|
||||||
'keymap.lua',
|
'keymap.lua',
|
||||||
|
'fs.lua',
|
||||||
],
|
],
|
||||||
'files': [
|
'files': [
|
||||||
'runtime/lua/vim/_editor.lua',
|
'runtime/lua/vim/_editor.lua',
|
||||||
@@ -142,6 +143,7 @@ CONFIG = {
|
|||||||
'runtime/lua/vim/ui.lua',
|
'runtime/lua/vim/ui.lua',
|
||||||
'runtime/lua/vim/filetype.lua',
|
'runtime/lua/vim/filetype.lua',
|
||||||
'runtime/lua/vim/keymap.lua',
|
'runtime/lua/vim/keymap.lua',
|
||||||
|
'runtime/lua/vim/fs.lua',
|
||||||
],
|
],
|
||||||
'file_patterns': '*.lua',
|
'file_patterns': '*.lua',
|
||||||
'fn_name_prefix': '',
|
'fn_name_prefix': '',
|
||||||
@@ -167,6 +169,7 @@ CONFIG = {
|
|||||||
'ui': 'vim.ui',
|
'ui': 'vim.ui',
|
||||||
'filetype': 'vim.filetype',
|
'filetype': 'vim.filetype',
|
||||||
'keymap': 'vim.keymap',
|
'keymap': 'vim.keymap',
|
||||||
|
'fs': 'vim.fs',
|
||||||
},
|
},
|
||||||
'append_only': [
|
'append_only': [
|
||||||
'shared.lua',
|
'shared.lua',
|
||||||
|
101
test/functional/lua/fs_spec.lua
Normal file
101
test/functional/lua/fs_spec.lua
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
local helpers = require('test.functional.helpers')(after_each)
|
||||||
|
|
||||||
|
local clear = helpers.clear
|
||||||
|
local exec_lua = helpers.exec_lua
|
||||||
|
local eq = helpers.eq
|
||||||
|
local mkdir_p = helpers.mkdir_p
|
||||||
|
local rmdir = helpers.rmdir
|
||||||
|
local nvim_dir = helpers.nvim_dir
|
||||||
|
local test_build_dir = helpers.test_build_dir
|
||||||
|
local iswin = helpers.iswin
|
||||||
|
local nvim_prog = helpers.nvim_prog
|
||||||
|
|
||||||
|
local nvim_prog_basename = iswin() and 'nvim.exe' or 'nvim'
|
||||||
|
|
||||||
|
before_each(clear)
|
||||||
|
|
||||||
|
describe('vim.fs', function()
|
||||||
|
describe('parents()', function()
|
||||||
|
it('works', function()
|
||||||
|
local test_dir = nvim_dir .. '/test'
|
||||||
|
mkdir_p(test_dir)
|
||||||
|
local dirs = exec_lua([[
|
||||||
|
local test_dir, test_build_dir = ...
|
||||||
|
local dirs = {}
|
||||||
|
for dir in vim.fs.parents(test_dir .. "/foo.txt") do
|
||||||
|
dirs[#dirs + 1] = dir
|
||||||
|
if dir == test_build_dir then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return dirs
|
||||||
|
]], test_dir, test_build_dir)
|
||||||
|
eq({test_dir, nvim_dir, test_build_dir}, dirs)
|
||||||
|
rmdir(test_dir)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('dirname()', function()
|
||||||
|
it('works', function()
|
||||||
|
eq(test_build_dir, exec_lua([[
|
||||||
|
local nvim_dir = ...
|
||||||
|
return vim.fs.dirname(nvim_dir)
|
||||||
|
]], nvim_dir))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('basename()', function()
|
||||||
|
it('works', function()
|
||||||
|
eq(nvim_prog_basename, exec_lua([[
|
||||||
|
local nvim_prog = ...
|
||||||
|
return vim.fs.basename(nvim_prog)
|
||||||
|
]], nvim_prog))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('dir()', function()
|
||||||
|
it('works', function()
|
||||||
|
eq(true, exec_lua([[
|
||||||
|
local dir, nvim = ...
|
||||||
|
for name, type in vim.fs.dir(dir) do
|
||||||
|
if name == nvim and type == 'file' then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
]], nvim_dir, nvim_prog_basename))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('find()', function()
|
||||||
|
it('works', function()
|
||||||
|
eq({test_build_dir}, exec_lua([[
|
||||||
|
local dir = ...
|
||||||
|
return vim.fs.find('build', { path = dir, upward = true, type = 'directory' })
|
||||||
|
]], nvim_dir))
|
||||||
|
eq({nvim_prog}, exec_lua([[
|
||||||
|
local dir, nvim = ...
|
||||||
|
return vim.fs.find(nvim, { path = dir, type = 'file' })
|
||||||
|
]], test_build_dir, nvim_prog_basename))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('normalize()', function()
|
||||||
|
it('works with backward slashes', function()
|
||||||
|
eq('C:/Users/jdoe', exec_lua [[ return vim.fs.normalize('C:\\Users\\jdoe') ]])
|
||||||
|
end)
|
||||||
|
it('works with ~', function()
|
||||||
|
if iswin() then
|
||||||
|
pending([[$HOME does not exist on Windows ¯\_(ツ)_/¯]])
|
||||||
|
end
|
||||||
|
eq(os.getenv('HOME') .. '/src/foo', exec_lua [[ return vim.fs.normalize('~/src/foo') ]])
|
||||||
|
end)
|
||||||
|
it('works with environment variables', function()
|
||||||
|
local xdg_config_home = test_build_dir .. '/.config'
|
||||||
|
eq(xdg_config_home .. '/nvim', exec_lua([[
|
||||||
|
vim.env.XDG_CONFIG_HOME = ...
|
||||||
|
return vim.fs.normalize('$XDG_CONFIG_HOME/nvim')
|
||||||
|
]], xdg_config_home))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
Reference in New Issue
Block a user