mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +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: ~ | ||||
|                     |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: | ||||
|   | ||||
| @@ -50,6 +50,7 @@ for k, v in pairs({ | ||||
|   keymap = true, | ||||
|   ui = true, | ||||
|   health = true, | ||||
|   fs = true, | ||||
| }) do | ||||
|   vim._submodules[k] = v | ||||
| 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', | ||||
|             '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', | ||||
|   | ||||
							
								
								
									
										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
	 Gregory Anders
					Gregory Anders