Skip to content

Commit

Permalink
feat!: files renamed in yazi are kept in sync in nvim
Browse files Browse the repository at this point in the history
BREAKING CHANGE: this plugin now requires yazi 0.2.4 or newer.
  • Loading branch information
mikavilpas committed Apr 5, 2024
1 parent 8697649 commit bd57653
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 13 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,5 +46,3 @@ Using lazy.nvim:
},
}
```

You can run `:checkhealth yazi` to see if the plugin is installed and working.
19 changes: 17 additions & 2 deletions lua/yazi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lua/yazi/health.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions lua/yazi/types.lua
Original file line number Diff line number Diff line change
@@ -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
96 changes: 96 additions & 0 deletions lua/yazi/utils.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local fn = vim.fn
local iterators = require('plenary.iterators')

local M = {}

Expand Down Expand Up @@ -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
9 changes: 5 additions & 4 deletions tests/yazi/example_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
7 changes: 4 additions & 3 deletions tests/yazi/open_dir_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
25 changes: 25 additions & 0 deletions tests/yazi/read_events_spec.lua
Original file line number Diff line number Diff line change
@@ -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)
104 changes: 104 additions & 0 deletions tests/yazi/rename_spec.lua
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit bd57653

Please sign in to comment.