Skip to content

Commit

Permalink
feat(watcher): implement watcher to detect and invoke on file change
Browse files Browse the repository at this point in the history
  • Loading branch information
pysan3 committed Jan 29, 2024
1 parent 8fb9050 commit de8a39d
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 9 deletions.
79 changes: 77 additions & 2 deletions lua/pathlib/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ local fs = vim.fs
local utils = require("pathlib.utils")
local const = require("pathlib.const")
local errs = require("pathlib.utils.errors")
local watcher = require("pathlib.utils.watcher")

---@alias PathlibString string # Specific annotation for result of `tostring(Path)`
---@alias PathlibPointer string # A unique id to each path object

---@class PathlibPath
---@field nuv uv
---@field git_state PathlibGitState
---@field error_msg string?
---@field _raw_paths PathlibStrList
---@field _drive_name string # Drive name for Windows path. ("C:", "D:")
---@field __windows_panic boolean # Windows paths shouldn't be passed to this type, but when it is.
---@field _drive_name string # Drive name for Windows path. ("C:", "D:", "\\127.0.0.1")
---@field __windows_panic boolean # Set to true when passed path might be a windows path. PathlibWindows ignores this.
---@field __fs_event_callbacks? table<string, PathlibWatcherCallback> # List of functions called when a fs_event is triggered.
---@field __string_cache string? # Cache result of `tostring(self)`.
---@field __parent_cache PathlibPath? # Cache reference to parent object.
local Path = setmetatable({
mytype = const.path_module_enum.PathlibPath,
sep_str = "/",
const = const,
}, {
__call = function(cls, ...)
return cls.new(cls, ...)
Expand Down Expand Up @@ -854,6 +859,76 @@ function Path:glob(pattern)
end
end

---Register fs_event watcher for `self`.
---@param func_name string? # Name of the callback to check existence. If nil, returns whether any callback exists.
---@return boolean exists
function Path:has_watcher(func_name)
if not self.__fs_event_callbacks then
return false
end
if not func_name then
for _, _ in pairs(self.__fs_event_callbacks) do
return true
end
return false
end
return not not self.__fs_event_callbacks[func_name]
end

---Register fs_event watcher for `self`.
---@param func_name string # Name of the callback to prevent register same callback multiple time
---@param callback PathlibWatcherCallback # Callback passed to `luv.fs_event_start`
---@return boolean succeess
function Path:register_watcher(func_name, callback)
self.__fs_event_callbacks = self.__fs_event_callbacks or {}
self.__fs_event_callbacks[func_name] = callback
local suc, err_msg = watcher.register(self)
if suc ~= nil then
return true
else
self.error_msg = err_msg
return false
end
end

---Unregister fs_event watcher for `self`.
---@param func_name string # Name of the callback registered with `self:register(func_name, ...)`
---@return boolean succeess
function Path:unregister_watcher(func_name)
if not self.__fs_event_callbacks then
return true
end
self.__fs_event_callbacks[func_name] = nil
for _, _ in pairs(self.__fs_event_callbacks) do
return true -- still has other callbacks
end
local suc, err_msg = watcher.unregister(self)
if suc ~= nil then
return true
else
self.error_msg = err_msg
return false
end
end

---Register fs_event watcher for `self`.
---@param func_name string? # Name of the callback to check existence. If nil, calls all watchers.
---@param args PathlibWatcherArgs
function Path:execute_watchers(func_name, args)
if not self.__fs_event_callbacks then
return
end
if func_name then
if self.__fs_event_callbacks[func_name] then
pcall(self.__fs_event_callbacks[func_name], self, args)
end
else
for _, func in pairs(self.__fs_event_callbacks) do
pcall(func, self, args)
end
end
end

---@alias PathlibAbsPath PathlibPath
---@alias PathlibRelPath PathlibPath

Expand Down
32 changes: 31 additions & 1 deletion lua/pathlib/git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ local utils = require("pathlib.utils")
---@class PathlibGitState
---@field is_ready boolean # true if the git status is up to date.
---@field ignored boolean?
---@field git_root PathlibPath?
---@field state PathlibGitStatus?

---@class PathlibGit
local M = {
Expand Down Expand Up @@ -237,18 +239,23 @@ function M.fill_git_ignore(paths, git_root)
end

---Fill in all `path.git_state` inside a single git repo. Use `M.fill_git_state_batch` when git root is unknown.
---You may want to pass absolute paths for better performance.
---@param paths PathlibPath[] # List of paths to check git ignored or not. Overwrites `path.git_ignored`.
---@param git_root PathlibPath # The git root dir.
function M.fill_git_state_in_root(paths, git_root)
local status, _ = M.status(git_root, true)
for _, path in ipairs(paths) do
path:inplace_absolute()
path.git_state.is_ready = true
path.git_state.git_root = git_root
path.git_state.state = status[path:tostring()]
end
M.fill_git_ignore(paths, git_root)
end

---Fill in all `path.git_state` by asking git cli.
---@param paths PathlibPath[] # List of paths to check git ignored or not. Overwrites `path.git_ignored`.
function M.fill_git_state_batch(paths)
function M.fill_git_state(paths)
---@type table<PathlibString, PathlibPath[] | { root: PathlibPath }>
local check_list = {} -- sort paths by their git roots
for _, path in ipairs(paths) do
Expand All @@ -265,4 +272,27 @@ function M.fill_git_state_batch(paths)
end
end

---Callback to update `PathlibPath.git_state` on fs_event
---@param path PathlibPath
---@param args PathlibWatcherArgs
function M.update_git_state_callback(path, args)
if path:is_dir(true) then
return
end
path.git_state.is_ready = false
if not path.git_state.git_root then
path.git_state.git_root = M.find_root(path)
end
local cmd = { "git", "-C", path.git_state.git_root:tostring(), "check-ignore", path:tostring() }
local suc, result = utils.execute_command(cmd)
if suc then
path.git_state.ignored = #result > 0
end
if not path.git_state.ignored then
-- TODO: request status update cycle via debounce
local status, _ = M.status(path.git_state.git_root, false)
path.git_state.state = status[path:tostring()]
end
end

return M
60 changes: 60 additions & 0 deletions lua/pathlib/utils/debug.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
local d = {}

---@param str string
function d.flatten(str)
return table.concat(vim.split(str:gsub("\n", " "), " +", { trimempty = true, plain = false }), " ")
end

---@param t table
function d.tbl(t)
return d.flatten(vim.inspect(t))
end

function d.p(...)
return vim.print(d.ps(...))
end

function d.ps(...)
return (d.tbl({ ... }):sub(3, -3):gsub([[^['"](.*)["']$]], "%1"):gsub([[\t]], " "))
end

function d.pt(...)
local result = {}
for index, value in ipairs({ ... }) do
result[index] = tostring(value)
end
return unpack(result)
end

function d.pp(path, ...)
return d.p(tostring(path), ...)
end

d.f = string.format

function d.pf(s, ...)
return d.p(d.f(s, ...))
end

function d.ptr(x)
return d.f("%p", x)
end

---@param state PathlibGitState
function d.g(state)
return d.tbl(vim.tbl_deep_extend("force", state, { git_root = state.git_root:tostring() }))
end

---@param dir PathlibPath
function d.rmdir(dir)
for _p in dir:iterdir() do
if _p:is_dir(true) then
d.rmdir(_p)
_p:rmdir()
end
_p:unlink()
end
dir:rmdir()
end

return d
9 changes: 5 additions & 4 deletions lua/pathlib/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ end
---@return boolean success
---@return string[] result_lines # Each line of the output from the command.
function M.execute_command(cmd, input)
local result = vim.fn.systemlist(cmd, input)
if vim.v.shell_error ~= 0 or (#result > 0 and vim.startswith(result[1], "fatal:")) then
return false, {}
-- TODO: execute_command cannot be called inside async task
local result = vim.system(cmd, { stdin = input }):wait()
if result.code == 0 then
return true, vim.split(result.stdout or "", "\n", { plain = true, trimempty = false })
else
return true, result
return false, {}
end
end

Expand Down
6 changes: 6 additions & 0 deletions lua/pathlib/utils/paths.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@ function M.normalize(path, iswin, opts)
return (path:gsub("([^/])/+$", "%1"))
end

---@param path PathlibPath
---@return PathlibPointer
function M.path_pointer(path)
return string.format("%p", path)
end

return M
Loading

0 comments on commit de8a39d

Please sign in to comment.