aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregory Anders <8965202+gpanders@users.noreply.github.com>2022-05-31 14:00:11 -0600
committerGitHub <noreply@github.com>2022-05-31 14:00:11 -0600
commitc632f64e247c672e425f609bb47a9ab0517a4c31 (patch)
tree9409d824661880a0abdc289b5933c930d4eafae4
parente6652821bd32e4ff8d62a0b67fc2041a5f41e252 (diff)
parent046b4ed461cb78b8b302a6403cc7ea64ad6b6085 (diff)
downloadrneovim-c632f64e247c672e425f609bb47a9ab0517a4c31.tar.gz
rneovim-c632f64e247c672e425f609bb47a9ab0517a4c31.tar.bz2
rneovim-c632f64e247c672e425f609bb47a9ab0517a4c31.zip
Merge pull request #18583 from gpanders/path-root
feat(fs): add vim.fs module
-rw-r--r--runtime/doc/lua.txt125
-rw-r--r--runtime/lua/vim/_editor.lua1
-rw-r--r--runtime/lua/vim/fs.lua205
-rwxr-xr-xscripts/gen_vimdoc.py3
-rw-r--r--test/functional/lua/fs_spec.lua101
5 files changed, 435 insertions, 0 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index dd1843ade3..8a14f80856 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -2147,4 +2147,129 @@ set({mode}, {lhs}, {rhs}, {opts}) *vim.keymap.set()*
See also: ~
|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:
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index c8a0aa8260..453aa6ac81 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -50,6 +50,7 @@ for k, v in pairs({
keymap = true,
ui = true,
health = true,
+ fs = true,
}) do
vim._submodules[k] = v
end
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
new file mode 100644
index 0000000000..9bf38f7bc3
--- /dev/null
+++ b/runtime/lua/vim/fs.lua
@@ -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
diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py
index 755749cef6..790c2ba52d 100755
--- a/scripts/gen_vimdoc.py
+++ b/scripts/gen_vimdoc.py
@@ -134,6 +134,7 @@ CONFIG = {
'ui.lua',
'filetype.lua',
'keymap.lua',
+ 'fs.lua',
],
'files': [
'runtime/lua/vim/_editor.lua',
@@ -142,6 +143,7 @@ CONFIG = {
'runtime/lua/vim/ui.lua',
'runtime/lua/vim/filetype.lua',
'runtime/lua/vim/keymap.lua',
+ 'runtime/lua/vim/fs.lua',
],
'file_patterns': '*.lua',
'fn_name_prefix': '',
@@ -167,6 +169,7 @@ CONFIG = {
'ui': 'vim.ui',
'filetype': 'vim.filetype',
'keymap': 'vim.keymap',
+ 'fs': 'vim.fs',
},
'append_only': [
'shared.lua',
diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua
new file mode 100644
index 0000000000..2bcc84db0f
--- /dev/null
+++ b/test/functional/lua/fs_spec.lua
@@ -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)