diff options
author | Gregory Anders <8965202+gpanders@users.noreply.github.com> | 2022-05-31 14:00:11 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-31 14:00:11 -0600 |
commit | c632f64e247c672e425f609bb47a9ab0517a4c31 (patch) | |
tree | 9409d824661880a0abdc289b5933c930d4eafae4 | |
parent | e6652821bd32e4ff8d62a0b67fc2041a5f41e252 (diff) | |
parent | 046b4ed461cb78b8b302a6403cc7ea64ad6b6085 (diff) | |
download | rneovim-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.txt | 125 | ||||
-rw-r--r-- | runtime/lua/vim/_editor.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/fs.lua | 205 | ||||
-rwxr-xr-x | scripts/gen_vimdoc.py | 3 | ||||
-rw-r--r-- | test/functional/lua/fs_spec.lua | 101 |
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) |