fs.lua 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. local M = {}
  2. --- Iterate over all the parents of the given file or directory.
  3. ---
  4. --- Example:
  5. --- <pre>
  6. --- local root_dir
  7. --- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do
  8. --- if vim.fn.isdirectory(dir .. "/.git") == 1 then
  9. --- root_dir = dir
  10. --- break
  11. --- end
  12. --- end
  13. ---
  14. --- if root_dir then
  15. --- print("Found git repository at", root_dir)
  16. --- end
  17. --- </pre>
  18. ---
  19. ---@param start (string) Initial file or directory.
  20. ---@return (function) Iterator
  21. function M.parents(start)
  22. return function(_, dir)
  23. local parent = M.dirname(dir)
  24. if parent == dir then
  25. return nil
  26. end
  27. return parent
  28. end,
  29. nil,
  30. start
  31. end
  32. --- Return the parent directory of the given file or directory
  33. ---
  34. ---@param file (string) File or directory
  35. ---@return (string) Parent directory of {file}
  36. function M.dirname(file)
  37. if file == nil then
  38. return nil
  39. end
  40. return vim.fn.fnamemodify(file, ':h')
  41. end
  42. --- Return the basename of the given file or directory
  43. ---
  44. ---@param file (string) File or directory
  45. ---@return (string) Basename of {file}
  46. function M.basename(file)
  47. return vim.fn.fnamemodify(file, ':t')
  48. end
  49. --- Return an iterator over the files and directories located in {path}
  50. ---
  51. ---@param path (string) An absolute or relative path to the directory to iterate
  52. --- over. The path is first normalized |vim.fs.normalize()|.
  53. ---@return Iterator over files and directories in {path}. Each iteration yields
  54. --- two values: name and type. Each "name" is the basename of the file or
  55. --- directory relative to {path}. Type is one of "file" or "directory".
  56. function M.dir(path)
  57. return function(fs)
  58. return vim.loop.fs_scandir_next(fs)
  59. end,
  60. vim.loop.fs_scandir(M.normalize(path))
  61. end
  62. --- Find files or directories in the given path.
  63. ---
  64. --- Finds any files or directories given in {names} starting from {path}. If
  65. --- {upward} is "true" then the search traverses upward through parent
  66. --- directories; otherwise, the search traverses downward. Note that downward
  67. --- searches are recursive and may search through many directories! If {stop}
  68. --- is non-nil, then the search stops when the directory given in {stop} is
  69. --- reached. The search terminates when {limit} (default 1) matches are found.
  70. --- The search can be narrowed to find only files or or only directories by
  71. --- specifying {type} to be "file" or "directory", respectively.
  72. ---
  73. ---@param names (string|table|fun(name: string): boolean) Names of the files
  74. --- and directories to find.
  75. --- Must be base names, paths and globs are not supported.
  76. --- If a function it is called per file and dir within the
  77. --- traversed directories to test if they match.
  78. ---@param opts (table) Optional keyword arguments:
  79. --- - path (string): Path to begin searching from. If
  80. --- omitted, the current working directory is used.
  81. --- - upward (boolean, default false): If true, search
  82. --- upward through parent directories. Otherwise,
  83. --- search through child directories
  84. --- (recursively).
  85. --- - stop (string): Stop searching when this directory is
  86. --- reached. The directory itself is not searched.
  87. --- - type (string): Find only files ("file") or
  88. --- directories ("directory"). If omitted, both
  89. --- files and directories that match {name} are
  90. --- included.
  91. --- - limit (number, default 1): Stop the search after
  92. --- finding this many matches. Use `math.huge` to
  93. --- place no limit on the number of matches.
  94. ---@return (table) The paths of all matching files or directories
  95. function M.find(names, opts)
  96. opts = opts or {}
  97. vim.validate({
  98. names = { names, { 's', 't', 'f' } },
  99. path = { opts.path, 's', true },
  100. upward = { opts.upward, 'b', true },
  101. stop = { opts.stop, 's', true },
  102. type = { opts.type, 's', true },
  103. limit = { opts.limit, 'n', true },
  104. })
  105. names = type(names) == 'string' and { names } or names
  106. local path = opts.path or vim.loop.cwd()
  107. local stop = opts.stop
  108. local limit = opts.limit or 1
  109. local matches = {}
  110. ---@private
  111. local function add(match)
  112. matches[#matches + 1] = match
  113. if #matches == limit then
  114. return true
  115. end
  116. end
  117. if opts.upward then
  118. local test
  119. if type(names) == 'function' then
  120. test = function(p)
  121. local t = {}
  122. for name, type in M.dir(p) do
  123. if names(name) and (not opts.type or opts.type == type) then
  124. table.insert(t, p .. '/' .. name)
  125. end
  126. end
  127. return t
  128. end
  129. else
  130. test = function(p)
  131. local t = {}
  132. for _, name in ipairs(names) do
  133. local f = p .. '/' .. name
  134. local stat = vim.loop.fs_stat(f)
  135. if stat and (not opts.type or opts.type == stat.type) then
  136. t[#t + 1] = f
  137. end
  138. end
  139. return t
  140. end
  141. end
  142. for _, match in ipairs(test(path)) do
  143. if add(match) then
  144. return matches
  145. end
  146. end
  147. for parent in M.parents(path) do
  148. if stop and parent == stop then
  149. break
  150. end
  151. for _, match in ipairs(test(parent)) do
  152. if add(match) then
  153. return matches
  154. end
  155. end
  156. end
  157. else
  158. local dirs = { path }
  159. while #dirs > 0 do
  160. local dir = table.remove(dirs, 1)
  161. if stop and dir == stop then
  162. break
  163. end
  164. for other, type_ in M.dir(dir) do
  165. local f = dir .. '/' .. other
  166. if type(names) == 'function' then
  167. if names(other) and (not opts.type or opts.type == type_) then
  168. if add(f) then
  169. return matches
  170. end
  171. end
  172. else
  173. for _, name in ipairs(names) do
  174. if name == other and (not opts.type or opts.type == type_) then
  175. if add(f) then
  176. return matches
  177. end
  178. end
  179. end
  180. end
  181. if type_ == 'directory' then
  182. dirs[#dirs + 1] = f
  183. end
  184. end
  185. end
  186. end
  187. return matches
  188. end
  189. --- Normalize a path to a standard format. A tilde (~) character at the
  190. --- beginning of the path is expanded to the user's home directory and any
  191. --- backslash (\\) characters are converted to forward slashes (/). Environment
  192. --- variables are also expanded.
  193. ---
  194. --- Example:
  195. --- <pre>
  196. --- vim.fs.normalize('C:\\Users\\jdoe')
  197. --- => 'C:/Users/jdoe'
  198. ---
  199. --- vim.fs.normalize('~/src/neovim')
  200. --- => '/home/jdoe/src/neovim'
  201. ---
  202. --- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')
  203. --- => '/Users/jdoe/.config/nvim/init.vim'
  204. --- </pre>
  205. ---
  206. ---@param path (string) Path to normalize
  207. ---@return (string) Normalized path
  208. function M.normalize(path)
  209. vim.validate({ path = { path, 's' } })
  210. return (path:gsub('^~/', vim.env.HOME .. '/'):gsub('%$([%w_]+)', vim.env):gsub('\\', '/'))
  211. end
  212. return M