diff --git a/README.md b/README.md index 8d7c2cab..8cbac21a 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,15 @@ So far I have done some maintenance work: - test: add simple testing setup for future development - feat: can optionally open yazi instead of netrw for directories - feat: health check for yazi +- feat: files renamed in yazi are kept in sync with open buffers If you'd like to collaborate, contact me via GitHub issues. ## Installation +> **Note:** This plugin requires a recent version of yazi. +> You can run `:checkhealth yazi` to see if a compatible version is installed and working. + Using lazy.nvim: ```lua @@ -42,5 +46,3 @@ Using lazy.nvim: }, } ``` - -You can run `:checkhealth yazi` to see if the plugin is installed and working. diff --git a/lua/yazi.lua b/lua/yazi.lua index 2797e806..160637b2 100644 --- a/lua/yazi.lua +++ b/lua/yazi.lua @@ -8,6 +8,7 @@ local M = {} M.yazi_loaded = false local output_path = '/tmp/yazi_filechosen' +local yazi_nvim_events_path = '/tmp/yazi.nvim.events.txt' --- :Yazi entry point ---@param path string? defaults to the current file or the working directory @@ -24,7 +25,11 @@ function M.yazi(path) local win, buffer = window.open_floating_window() os.remove(output_path) - local cmd = string.format('yazi "%s" --chooser-file "%s"', path, output_path) + local cmd = string.format( + 'yazi "%s" --local-events "rename" --chooser-file "%s" > /tmp/yazi.nvim.events.txt', + path, + output_path + ) if M.yazi_loaded == false then -- ensure that the buffer is closed on exit @@ -38,6 +43,7 @@ function M.yazi(path) M.yazi_loaded = false vim.cmd('silent! :checktime') + -- open the file that was chosen if vim.api.nvim_win_is_valid(prev_win) then -- NOTE the types for nvim_ apis are inaccurate so we need to typecast ---@cast win integer @@ -50,14 +56,23 @@ function M.yazi(path) end end + ---@cast buffer integer if - ---@cast buffer integer vim.api.nvim_buf_is_valid(buffer) and vim.api.nvim_buf_is_loaded(buffer) then vim.api.nvim_buf_delete(buffer, { force = true }) end end + + -- process events emitted from yazi + local rename_events = utils.read_events_file(yazi_nvim_events_path) + local renames = + utils.get_buffers_that_need_renaming_after_yazi_exited(rename_events) + + for _, event in ipairs(renames) do + vim.api.nvim_buf_set_name(event.buffer, event.to) + end end, }) end diff --git a/lua/yazi/health.lua b/lua/yazi/health.lua index 190b83fe..bb4d4c4c 100644 --- a/lua/yazi/health.lua +++ b/lua/yazi/health.lua @@ -21,9 +21,9 @@ return { end local checker = require('vim.version') - if not checker.ge(semver, '0.1.5') then + if not checker.ge(semver, '0.2.4') then return vim.health.warn( - 'yazi version is too old, please upgrade to 0.1.5 or newer' + 'yazi version is too old, please upgrade to 0.2.4 or newer' ) end diff --git a/lua/yazi/types.lua b/lua/yazi/types.lua index 564cdc8d..d17043e1 100644 --- a/lua/yazi/types.lua +++ b/lua/yazi/types.lua @@ -1,2 +1,16 @@ ---@class YaziConfig ---@field public open_for_directories boolean + +---@class YaziRenameEvent +---@field public type "rename" +---@field public timestamp string +---@field public id string +---@field public data YaziEventDataRename + +---@class YaziEventDataRename +---@field public from string +---@field public to string + +---@class YaziBufferRenameInstruction +---@field buffer integer the existing buffer number that needs renaming +---@field to string the new file name that the buffer should point to diff --git a/lua/yazi/utils.lua b/lua/yazi/utils.lua index 206b6b36..7fa61831 100644 --- a/lua/yazi/utils.lua +++ b/lua/yazi/utils.lua @@ -1,4 +1,5 @@ local fn = vim.fn +local iterators = require('plenary.iterators') local M = {} @@ -33,4 +34,99 @@ function M.selected_file_path(path) return path end +-- Returns parsed events from the yazi events file +---@param events_file_lines string[] +---@return YaziRenameEvent[] +function M.parse_events(events_file_lines) + ---@type string[] + local events = {} + + for _, line in ipairs(events_file_lines) do + local parts = vim.split(line, ',') + local type = parts[1] + + if type == 'rename' then + -- example of a rename event: + + -- rename,1712242143209837,1712242143209837,{"tab":0,"from":"/Users/mikavilpas/git/yazi/LICENSE","to":"/Users/mikavilpas/git/yazi/LICENSE2"} + local timestamp = parts[2] + local id = parts[3] + local data_string = table.concat(parts, ',', 4, #parts) + + ---@type YaziRenameEvent + local event = { + type = type, + timestamp = timestamp, + id = id, + data = vim.fn.json_decode(data_string), + } + table.insert(events, event) + end + end + + return events +end + +---@param path string +---@return YaziRenameEvent[] +function M.read_events_file(path) + local success, events_file_lines = pcall(vim.fn.readfile, path) + os.remove(path) + if not success then + return {} + end + + -- selene: allow(shadowing) + ---@diagnostic disable-next-line: redefined-local + local success, events = pcall(M.parse_events, events_file_lines) + if not success then + return {} + end + + return events +end + +---@param rename_events YaziRenameEvent[] +---@return YaziBufferRenameInstruction[] +function M.get_buffers_that_need_renaming_after_yazi_exited(rename_events) + local buffers = iterators + .iter(vim.api.nvim_list_bufs()) + :filter(function(buffer) + if vim.api.nvim_buf_get_name(buffer) == '' then + return false + end + + return true + end) + :map(function(buffer) + -- the buffer is found if + -- * the buffer name matches the original name + -- * or the buffer's file is under a directory that was renamed (also nested directories) + for _, event in ipairs(rename_events) do + local buffer_name = vim.api.nvim_buf_get_name(buffer) + + if event.data.from == buffer_name then + ---@type YaziBufferRenameInstruction + return { + buffer = buffer, + to = event.data.to, + } + end + + local starts_with = buffer_name:sub(1, #event.data.from) + == event.data.from + if starts_with then + ---@type YaziBufferRenameInstruction + return { + buffer = buffer, + to = event.data.to .. buffer_name:sub(#event.data.from + 1), + } + end + end + end) + :tolist() + + return buffers +end + return M diff --git a/tests/yazi/example_spec.lua b/tests/yazi/example_spec.lua index ddc70d15..dade2e1d 100644 --- a/tests/yazi/example_spec.lua +++ b/tests/yazi/example_spec.lua @@ -23,7 +23,7 @@ describe('opening a file', function() plugin.yazi() assert.stub(api_mock.termopen).was_called_with( - 'yazi "/tmp/test-file.txt" --chooser-file "/tmp/yazi_filechosen"', + 'yazi "/tmp/test-file.txt" --local-events "rename" --chooser-file "/tmp/yazi_filechosen" > /tmp/yazi.nvim.events.txt', match.is_table() ) end) @@ -33,9 +33,10 @@ describe('opening a file', function() plugin.yazi() - assert - .stub(api_mock.termopen) - .was_called_with('yazi "/tmp/" --chooser-file "/tmp/yazi_filechosen"', match.is_table()) + assert.stub(api_mock.termopen).was_called_with( + 'yazi "/tmp/" --local-events "rename" --chooser-file "/tmp/yazi_filechosen" > /tmp/yazi.nvim.events.txt', + match.is_table() + ) end) describe("when a file is selected in yazi's chooser", function() diff --git a/tests/yazi/open_dir_spec.lua b/tests/yazi/open_dir_spec.lua index ecf6fd6c..0c95b892 100644 --- a/tests/yazi/open_dir_spec.lua +++ b/tests/yazi/open_dir_spec.lua @@ -23,8 +23,9 @@ describe('when the user set open_for_directories = true', function() -- instead of netrw opening, yazi should open vim.api.nvim_command('edit /') - assert - .stub(api_mock.termopen) - .was_called_with('yazi "/" --chooser-file "/tmp/yazi_filechosen"', match.is_table()) + assert.stub(api_mock.termopen).was_called_with( + 'yazi "/" --local-events "rename" --chooser-file "/tmp/yazi_filechosen" > /tmp/yazi.nvim.events.txt', + match.is_table() + ) end) end) diff --git a/tests/yazi/read_events_spec.lua b/tests/yazi/read_events_spec.lua new file mode 100644 index 00000000..bd3f3fef --- /dev/null +++ b/tests/yazi/read_events_spec.lua @@ -0,0 +1,25 @@ +local assert = require('luassert') +local utils = require('yazi.utils') + +describe('parsing yazi event file events', function() + it('can parse rename events', function() + local data = { + 'rename,1712242143209837,1712242143209837,{"tab":0,"from":"/Users/mikavilpas/git/yazi/file","to":"/Users/mikavilpas/git/yazi/file2"}', + } + + local events = utils.parse_events(data) + + assert.are.same(events, { + { + type = 'rename', + timestamp = '1712242143209837', + id = '1712242143209837', + data = { + tab = 0, + from = '/Users/mikavilpas/git/yazi/file', + to = '/Users/mikavilpas/git/yazi/file2', + }, + }, + }) + end) +end) diff --git a/tests/yazi/rename_spec.lua b/tests/yazi/rename_spec.lua new file mode 100644 index 00000000..69e2c70c --- /dev/null +++ b/tests/yazi/rename_spec.lua @@ -0,0 +1,104 @@ +local assert = require('luassert') +local utils = require('yazi.utils') + +describe('get_buffers_that_need_renaming_after_yazi_exited', function() + before_each(function() + -- clear all buffers + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + vim.api.nvim_buf_delete(buf, { force = true }) + end + end) + + it('can detect renames to files whose names match exactly', function() + ---@type YaziRenameEvent[] + local rename_events = { + { + type = 'rename', + timestamp = '1712242143209837', + id = '1712242143209837', + data = { + from = '/my-tmp/file1', + to = '/my-tmp/file2', + }, + }, + { + type = 'rename', + timestamp = '1712242143209837', + id = '1712242143209837', + data = { + from = '/my-tmp/file3', + to = '/my-tmp/file4', + }, + }, + } + + -- simulate the buffers being opened + vim.fn.bufadd('/my-tmp/file1') + vim.fn.bufadd('/my-tmp/file3') + + local renames = + utils.get_buffers_that_need_renaming_after_yazi_exited(rename_events) + + assert.is_equal(#renames, 2) + + local result1 = renames[1] + assert.is_equal('/my-tmp/file2', result1.to) + assert.is_number(result1.buffer) + + local result2 = renames[2] + assert.is_equal('/my-tmp/file4', result2.to) + assert.is_number(result2.buffer) + end) + + it( + 'can detect renames to buffers open in a directory that was renamed', + function() + ---@type YaziRenameEvent[] + local rename_events = { + { + type = 'rename', + timestamp = '1712242143209837', + id = '1712242143209837', + data = { + from = '/my-tmp/dir1', + to = '/my-tmp/dir2', + }, + }, + } + + -- simulate the buffer being opened + vim.fn.bufadd('/my-tmp/dir1/file') + + local renames = + utils.get_buffers_that_need_renaming_after_yazi_exited(rename_events) + + assert.is_equal(#renames, 1) + + local result1 = renames[1] + assert.is_equal('/my-tmp/dir2/file', result1.to) + end + ) + + it("doesn't rename a buffer that was not renamed in yazi", function() + ---@type YaziRenameEvent[] + local rename_events = { + { + type = 'rename', + timestamp = '1712242143209837', + id = '1712242143209837', + data = { + from = '/my-tmp/not-opened-file', + to = '/my-tmp/not-opened-file-renamed', + }, + }, + } + + -- simulate the buffer being opened + vim.fn.bufadd('/my-tmp/dir1/file') + + local renames = + utils.get_buffers_that_need_renaming_after_yazi_exited(rename_events) + + assert.is_equal(#renames, 0) + end) +end)