From 70c6084304cb6c44569195ef78f71376b575deb8 Mon Sep 17 00:00:00 2001 From: Makaze Date: Tue, 16 Apr 2024 12:53:04 -0400 Subject: [PATCH 1/3] Add file watcher. Needs docs --- doc/watch.txt | 16 +++++++++++++++- lua/watch.lua | 43 +++++++++++++++++++++++++++++++++++++++++-- plugin/watch.lua | 25 +++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/doc/watch.txt b/doc/watch.txt index df7851c..3f91b71 100644 --- a/doc/watch.txt +++ b/doc/watch.txt @@ -2,7 +2,7 @@ Author: Makaze License: GPLv3.0 -Version: 0.2.0 +Version: 0.3.0 ================================================================================ INTRODUCTION *watch* @@ -35,6 +35,7 @@ Features: ~ [x] Scrollable output [x] Pause watching when in the background [x] Option to open in a configurable split window + [x] Watch for file modifications instead of just timing Planned: ~ [ ] ANSI color support @@ -67,6 +68,19 @@ COMMANDS *watch-commands* Parameters: ~ {command...} (string) Shell command to stop watching. +:WatchFile {command...} {refresh_rate*} *WatchFile* + Starts a new watcher for the currently open file. Behaves like |WatchStart|, + but only runs the command if the file has been modified. Refresh rate must + be a minimum of 1000 ms. Refresh rate values lower than 1000 will be + increased to 1000 ms. + + Parameters: ~ + {command...} (string) Shell command to watch. + {refresh_rate} (integer) Time between refreshes in milliseconds. Will + automically increase to a minimum of 1000. + Defaults to 1000. + + ================================================================================ CONFIGURATION *watch-config* diff --git a/lua/watch.lua b/lua/watch.lua index 612d79c..e15e6af 100644 --- a/lua/watch.lua +++ b/lua/watch.lua @@ -4,6 +4,8 @@ --- @field refresh_rate integer The refresh rate for the watcher in milliseconds. --- @field bufnr integer The buffer number attached to the watcher. --- @field timer function The timer object attached to the watcher. +--- @field file string|nil The filename to watch (if applicable). +--- @field last_updated integer The time since the file was last checked. Used when watching files. local Watch = {} @@ -43,6 +45,21 @@ local function get_buf_by_name(name) end) end +--- Get the time a file was last updated, or `nil` if <= the result of last check. +--- +--- @param path string The absolute file path. +--- @param last_check integer The unix timestamp to check against. Defaults to `0`. +--- @return integer|nil time +local function file_updated(path, last_check) + last_check = last_check or 0 + local stat = uv.fs_stat(path) + if stat and stat.type == "file" and stat.mtime.sec > last_check then + return stat.mtime.sec + else + return nil + end +end + --- @type watch.Watcher[] --- --- Global list of watchers and associated data. @@ -119,6 +136,17 @@ Watch.update = function(command, bufnr) return end + local W = Watch.watchers[command] + if W.file then + local update = file_updated(W.file, W.last_updated) + + if update then + Watch.watchers[command].last_updated = update + else + return + end + end + -- Execute your command and capture its output -- Use vim.system for async local code = vim.system( @@ -166,9 +194,10 @@ end --- Starts continually reloading a buffer's contents with a shell command. If the command is aleady being watched, then opens that buffer in the current window. --- --- @param command string Shell command. ---- @param refresh_rate? integer Time between reloads in milliseconds. Defaults to `watch.config.refresh_rate`. +--- @param refresh_rate? integer Time between reloads in milliseconds. Defaults to `watch.config.refresh_rate`. If `file` is provided, then it is increased to a minimum of 1000 ms. --- @param bufnr? integer Buffer number to update. Defaults to a new buffer. -Watch.start = function(command, refresh_rate, bufnr) +--- @param file? string The absolute path of the file to watch. Defaults to `nil`. +Watch.start = function(command, refresh_rate, bufnr, file) -- Check if command is nil if not command or not string.len(command) then vim.notify("[watch] Error: Empty command passed", vim.log.levels.ERROR) @@ -203,6 +232,14 @@ Watch.start = function(command, refresh_rate, bufnr) refresh_rate = Watch.config.refresh_rate end + -- Minimum 1000 ms if file check + if file and refresh_rate < 1000 then + vim.notify( + "[watch] File watchers require refresh_rate >= 1000. Increasing to 1000 ms." + ) + refresh_rate = 1000 + end + -- Create a split based on configurations local split = Watch.config.split or {} if split and split.enabled then @@ -255,6 +292,8 @@ Watch.start = function(command, refresh_rate, bufnr) bufnr = bufnr, refresh_rate = refresh_rate, timer = timer, + file = file, + last_updated = 0, } Watch.watchers[command] = watcher diff --git a/plugin/watch.lua b/plugin/watch.lua index b9a9fec..11dd22e 100644 --- a/plugin/watch.lua +++ b/plugin/watch.lua @@ -27,6 +27,31 @@ command("WatchStart", function(cmd) require("watch").start(table.concat(new_cmd, " "), refresh_rate, nil) end, "+") +--- Starts a new watcher for the currently open file. Behaves like a normal watcher, but only runs the command if the file has been modified. +--- +--- @param cmd table The watched command. Final argument is the refresh rate in milliseconds, or nil if not a number. +command("WatchFile", function(cmd) + local args = cmd.fargs + + -- Add the last argument to command if not a number + local refresh_rate = tonumber(args[#args]) + local from = 1 + local to = refresh_rate and #args - 1 or #args + + -- Get the command(s) from the arguments + local new_cmd = {} + for i = from, to do + table.insert(new_cmd, args[i]) + end + + require("watch").start( + table.concat(new_cmd, " "), + refresh_rate, + nil, + vim.fn.expand("%:p") + ) +end, "+") + --- Stops a watcher. --- --- `WARNING:` If `watch.config.close_on_stop` is set to `true`, then affected buffers will also be deleted. From e8ca3f96270755267f8edcffafd24994f152ad45 Mon Sep 17 00:00:00 2001 From: Makaze Date: Tue, 16 Apr 2024 23:24:12 -0400 Subject: [PATCH 2/3] Fix file expansion and add documentation --- README.md | 21 +++++++++++++++++++-- doc/watch.txt | 19 +++++++++++++------ lua/watch.lua | 9 +++++++-- plugin/watch.lua | 15 +++++++-------- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3d76d46..5e266ec 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ started Neovim. - [x] Scrollable output - [x] Pause watching when in the background - [x] Option to open in a configurable split window +- [x] Option to watch for file changes #### Planned: - [ ] ANSI color support @@ -53,7 +54,7 @@ Using [lazy.nvim](https://github.com/nvim-telescope/telescope.nvim): ```lua { "Makaze/watch.nvim", - cmd = { "WatchStart", "WatchStop" }, + cmd = { "WatchStart", "WatchStop", "WatchFile" }, } ``` @@ -95,7 +96,7 @@ watch.setup({ # Example Usage -You can use the Lua API or call the commands from the commandline. To watch the command `tree -cdC` every 500 milliseconds: +You can use the Lua API or call the commands from the commandline. To watch the command `tree -cdC` every 500 milliseconds and to watch the file `error.log` for changes: ### Lua API @@ -115,6 +116,14 @@ watch.stop({ file = "tree -cdC" }) -- Stop watching `tree -cdC` watch.stop() -- Stop all watchers ``` +##### Watch a File for Changes + +```lua +local watch = require("watch") +watch.start("cat %s", 3000, nil, "errog.log") -- Specify 3000 ms refresh +watch.start("cat %s", nil, nil, "errog.log") -- Default to 1000 ms refresh +``` + ### Ex Commands ##### Start @@ -131,6 +140,14 @@ watch.stop() -- Stop all watchers :WatchStop " Stop all watchers ``` +##### Watch a File for Changes + +```vim +" With error.log open and focused +:WatchFile cat 3000 " Specify 3000 ms refresh on the currently open file +:WatchFile cat " Default to 1000 ms refresh on the currently open file +``` + # Documentation For examples and technical documentation about commands and the Lua API see `:help watch`. diff --git a/doc/watch.txt b/doc/watch.txt index 3f91b71..f306d34 100644 --- a/doc/watch.txt +++ b/doc/watch.txt @@ -35,7 +35,7 @@ Features: ~ [x] Scrollable output [x] Pause watching when in the background [x] Option to open in a configurable split window - [x] Watch for file modifications instead of just timing + [x] Option to watch for file changes Planned: ~ [ ] ANSI color support @@ -70,12 +70,14 @@ COMMANDS *watch-commands* :WatchFile {command...} {refresh_rate*} *WatchFile* Starts a new watcher for the currently open file. Behaves like |WatchStart|, - but only runs the command if the file has been modified. Refresh rate must - be a minimum of 1000 ms. Refresh rate values lower than 1000 will be - increased to 1000 ms. + but only runs the command if the file has been modified. + + `NOTE:` Refresh rate values lower than 1000 will be increased to 1000 ms. Parameters: ~ - {command...} (string) Shell command to watch. + {command...} (string) Shell command to watch. Use `%s` inside the + command to insert the absolute path of the + current file. {refresh_rate} (integer) Time between refreshes in milliseconds. Will automically increase to a minimum of 1000. Defaults to 1000. @@ -132,7 +134,7 @@ watch.setup({opts*}) *watch.setup()* delete the buffer when calling |watch.stop()|. Default false. -watch.start({command}, {refresh_rate*}, {bufnr*}) *watch.start()* +watch.start({command}, {refresh_rate*}, {bufnr*}, {file*}) *watch.start()* Starts continually reloading a buffer's contents with a shell command. If the command is aleady being watched, opens that buffer in the current window. @@ -143,6 +145,11 @@ watch.start({command}, {refresh_rate*}, {bufnr*}) *watch.start()* milliseconds. Defaults to `500`. {bufnr} (integer) (optional) The buffer number to load to. Defaults to a new buffer. + {file} (string) (optional) The absolute path of a file to + watch. If given, the command will be run + when the file is modified on the disk, + checking at an interval of {refresh_rate}. + Defaults to no file (timer only). watch.stop({event*}) *watch.stop()* Stops watching the specified command and detaches from the buffer. If no diff --git a/lua/watch.lua b/lua/watch.lua index e15e6af..bf78477 100644 --- a/lua/watch.lua +++ b/lua/watch.lua @@ -193,7 +193,7 @@ end --- Starts continually reloading a buffer's contents with a shell command. If the command is aleady being watched, then opens that buffer in the current window. --- ---- @param command string Shell command. +--- @param command string Shell command to watch. If `file` is given, then `%s` will expand to the filename. --- @param refresh_rate? integer Time between reloads in milliseconds. Defaults to `watch.config.refresh_rate`. If `file` is provided, then it is increased to a minimum of 1000 ms. --- @param bufnr? integer Buffer number to update. Defaults to a new buffer. --- @param file? string The absolute path of the file to watch. Defaults to `nil`. @@ -213,6 +213,11 @@ Watch.start = function(command, refresh_rate, bufnr, file) return end + -- Expand %s to the filename + if file then + command = string.gsub(command, "%%s", file) + end + -- Open the buffer if already running if Watch.watchers[command] then bufnr = Watch.watchers[command].bufnr @@ -312,7 +317,7 @@ end --- --- `WARNING:` If `watch.config.close_on_stop` is set to `true`, then affected buffers will also be deleted. --- ---- @param event? string|table The command name to stop. If string, then uses the string. If table, then uses `event.file`. +--- @param event? string|table The shell command to stop. If string, then uses the string. If table, then uses `event.file`. Watch.stop = function(event) -- Get the current buffer if it is a watcher local bufname = nil diff --git a/plugin/watch.lua b/plugin/watch.lua index 11dd22e..a095956 100644 --- a/plugin/watch.lua +++ b/plugin/watch.lua @@ -9,7 +9,7 @@ end --- Starts a new watcher. --- ---- @param cmd table The watched command. Final argument is the refresh rate in milliseconds, or nil if not a number. +--- @param cmd table The watched command. Refresh rate is the last argument given in milliseconds, or defaults to config if not a number. command("WatchStart", function(cmd) local args = cmd.fargs @@ -29,7 +29,7 @@ end, "+") --- Starts a new watcher for the currently open file. Behaves like a normal watcher, but only runs the command if the file has been modified. --- ---- @param cmd table The watched command. Final argument is the refresh rate in milliseconds, or nil if not a number. +--- @param cmd table The watched command. Use `%s` inside the command to insert the absolute path of the current file. Refresh rate is the last argument given in milliseconds, or defaults to config if not a number (minimum 1000 for file watchers). command("WatchFile", function(cmd) local args = cmd.fargs @@ -44,12 +44,11 @@ command("WatchFile", function(cmd) table.insert(new_cmd, args[i]) end - require("watch").start( - table.concat(new_cmd, " "), - refresh_rate, - nil, - vim.fn.expand("%:p") - ) + local cmd_string = table.concat(new_cmd, " ") + + local file = vim.fn.expand("%:p") + + require("watch").start(cmd_string, refresh_rate, nil, file) end, "+") --- Stops a watcher. From f7360f8073b56403952e164f2e8018a2c8f3e861 Mon Sep 17 00:00:00 2001 From: Makaze Date: Tue, 16 Apr 2024 23:29:16 -0400 Subject: [PATCH 3/3] Wording fixes --- README.md | 1 + doc/watch.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e266ec..bdf5ad1 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ watch.stop() -- Stop all watchers ```lua local watch = require("watch") +-- Use `%s` inside the command to insert the absolute path of the current file. watch.start("cat %s", 3000, nil, "errog.log") -- Specify 3000 ms refresh watch.start("cat %s", nil, nil, "errog.log") -- Default to 1000 ms refresh ``` diff --git a/doc/watch.txt b/doc/watch.txt index f306d34..0a06dba 100644 --- a/doc/watch.txt +++ b/doc/watch.txt @@ -145,7 +145,7 @@ watch.start({command}, {refresh_rate*}, {bufnr*}, {file*}) *watch.start()* milliseconds. Defaults to `500`. {bufnr} (integer) (optional) The buffer number to load to. Defaults to a new buffer. - {file} (string) (optional) The absolute path of a file to + {file} (string) (optional) The path of a file to watch. If given, the command will be run when the file is modified on the disk, checking at an interval of {refresh_rate}.