Skip to content

Commit

Permalink
feat: add yazi.toggle() to continue from the last hovered file (#230)
Browse files Browse the repository at this point in the history
This commit adds a new function `yazi.toggle()` that can be used to
pseudo resume the previous yazi session. An example mapping is added to
the README to show how to use it.

Note that this requires a very new yazi right now,
sxyazi/yazi#1305 from 2024-07-18

You also need to set `use_ya_for_events_reading` and
`use_yazi_client_id_flag` to true in your config.
  • Loading branch information
mikavilpas authored Jul 20, 2024
1 parent 305a6a5 commit dbddef0
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 12 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,24 @@ Using [lazy.nvim](https://lazy.folke.io/):
end,
desc = "Open the file manager in nvim's working directory" ,
},
{
'<c-up>',
function()
-- NOTE: requires a version of yazi that includes
-- https://github.com/sxyazi/yazi/pull/1305 from 2024-07-18
require('yazi').toggle()
end,
desc = "Resume the last yazi session",
},
},
---@type YaziConfig
opts = {
-- if you want to open yazi instead of netrw, see below for more info
open_for_directories = false,

-- enable these if you are using the latest version of yazi
-- use_ya_for_events_reading = true,
-- use_yazi_client_id_flag = true,
},
}
```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { startNeovimWithYa } from "./startNeovimWithYa"

describe("toggling yazi to pseudo-continue the previous session", () => {
beforeEach(() => {
cy.visit("http://localhost:5173")
})

function hoverAnotherFileToEnsureHoverEventIsReceivedInCI(file: string) {
// select another file (hacky)
cy.typeIntoTerminal("gg")

// select the desired file so that a new hover event is sent
cy.typeIntoTerminal(`/${file}{enter}`)
}

it("can highlight the buffer when hovered", () => {
startNeovimWithYa().then((dir) => {
// wait until text on the start screen is visible
cy.contains("If you see this text, Neovim is ready!")

// start yazi
cy.typeIntoTerminal("{upArrow}")

// select another file. This should send a hover event, which should be
// saved as the "last hovered file"

hoverAnotherFileToEnsureHoverEventIsReceivedInCI(
dir.contents["test.lua"].name,
)

// close yazi
cy.typeIntoTerminal("q")

// the hovered file should not be visible any longer
cy.contains(dir.contents["test.lua"].name).should("not.exist")

// start yazi again by toggling it
cy.typeIntoTerminal("{control+upArrow}")

// the previously hovered file should be visible again
cy.contains(dir.contents["test.lua"].name)
})
})

it("can start yazi even if no previous session exists", () => {
startNeovimWithYa().then((dir) => {
// wait until text on the start screen is visible
cy.contains("If you see this text, Neovim is ready!")

// toggle yazi
cy.typeIntoTerminal("{control+upArrow}")

// yazi should be visible, showing other files
cy.contains(dir.contents["test.lua"].name)
})
})
})
6 changes: 6 additions & 0 deletions integration-tests/test-environment/test-setup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ local plugins = {
require('yazi').yazi()
end,
},
{
'<c-up>',
function()
require('yazi').toggle()
end,
},
},
---@type YaziConfig
opts = {
Expand Down
42 changes: 39 additions & 3 deletions lua/yazi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ local utils = require('yazi.utils')
local configModule = require('yazi.config')
local event_handling = require('yazi.event_handling')
local Log = require('yazi.log')
local YaziProcess = require('yazi.yazi_process')
local YaziProcess = require('yazi.process.yazi_process')

local M = {}

M.version = '2.3.1' -- x-release-please-version

-- The last known state of yazi when it was closed
---@type YaziPreviousState
M.previous_state = {}

---@param config? YaziConfig?
---@param input_path? string
---@diagnostic disable-next-line: redefined-local
function M.yazi(config, input_path)
if utils.is_yazi_available() ~= true then
print('Please install yazi. Check the documentation for more information')
Expand Down Expand Up @@ -47,7 +50,7 @@ function M.yazi(config, input_path)
local yazi_process = YaziProcess:start(
config,
path,
function(exit_code, selected_files, events)
function(exit_code, selected_files, events, hovered_url)
if exit_code ~= 0 then
print(
"yazi.nvim: had trouble opening yazi. Run ':checkhealth yazi' for more information."
Expand Down Expand Up @@ -80,6 +83,13 @@ function M.yazi(config, input_path)
utils.on_yazi_exited(prev_win, prev_buf, win, config, selected_files, {
last_directory = event_info.last_directory or path:parent(),
})

if hovered_url then
-- currently we can't reliably get the hovered_url from ya due to
-- https://github.com/sxyazi/yazi/issues/1314 so let's try to at least
-- not corrupt the last working hovered state
M.previous_state.last_hovered = hovered_url
end
end
)

Expand All @@ -99,6 +109,32 @@ function M.yazi(config, input_path)
end)
end

-- Open yazi, continuing from the previously hovered file. If no previous file
-- was hovered, open yazi with the default path.
---@param config? YaziConfig?
function M.toggle(config)
assert(
(config and config.use_ya_for_events_reading)
or M.config.use_ya_for_events_reading,
'toggle requires setting `use_ya_for_events_reading`'
)
assert(
(config and config.use_yazi_client_id_flag)
or M.config.use_yazi_client_id_flag,
'toggle requires setting `use_yazi_client_id_flag`'
)

local path = M.previous_state and M.previous_state.last_hovered or nil
if path == nil then
Log:debug('No previous file hovered, opening yazi with default path')
else
Log:debug(
string.format('Opening yazi with previous file hovered: %s', path)
)
end
M.yazi(config, path)
end

M.config = configModule.default()

---@param opts YaziConfig?
Expand Down
11 changes: 9 additions & 2 deletions lua/yazi/process/ya_process.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ local highlight_hovered_buffer =
---@class (exact) YaProcess
---@field public events YaziEvent[] "The events that have been received from yazi"
---@field public new fun(config: YaziConfig, yazi_id: string): YaProcess
---@field public hovered_url? string "The path that is currently hovered over in this yazi. Only works if `use_yazi_client_id_flag` is set to true."
---@field private config YaziConfig
---@field private yazi_id? string "The YAZI_ID of the yazi process. Can be nil if this feature is not in use."
---@field private ya_process vim.SystemObj
Expand Down Expand Up @@ -116,12 +117,18 @@ function YaProcess:start()
data = vim.split(data, '\n', { plain = true, trimempty = true })

local parsed = utils.safe_parse_events(data)
Log:debug(string.format('Parsed events: %s', vim.inspect(parsed)))
-- Log:debug(string.format('Parsed events: %s', vim.inspect(parsed)))

for _, event in ipairs(parsed) do
if event.type == 'hover' then
---@cast event YaziHoverEvent
if event.yazi_id == self.yazi_id then
Log:debug(
string.format('Changing the last hovered_url to %s', event.url)
)
self.hovered_url = event.url
end
vim.schedule(function()
---@cast event YaziHoverEvent
highlight_hovered_buffer.highlight_hovered_buffer(
event.url,
self.config.highlight_groups
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
local YaProcess = require('yazi.process.ya_process')
local Log = require('yazi.log')
local utils = require('yazi.utils')
local YaziProcessApi = require('yazi.process.yazi_process_api')
local LegacyEventReadingFromEventFile =
require('yazi.process.legacy_events_from_file')

---@class YaziProcess
---@field private event_reader YaProcess | LegacyEventReadingFromEventFile "The process that reads events from yazi"
---@field public api YaziProcessApi
---@field public yazi_job_id integer
---@field private event_reader YaProcess | LegacyEventReadingFromEventFile "The process that reads events from yazi"
local YaziProcess = {}

---@diagnostic disable-next-line: inject-field
YaziProcess.__index = YaziProcess

---@param config YaziConfig
---@param path Path
---@param on_exit fun(code: integer, selected_files: string[], events: YaziEvent[])
---@param on_exit fun(code: integer, selected_files: string[], events: YaziEvent[], hovered_url: string | nil)
function YaziProcess:start(config, path, on_exit)
os.remove(config.chosen_file_path)

Expand All @@ -31,6 +33,7 @@ function YaziProcess:start(config, path, on_exit)
-- instance, so that we can communicate with it specifically, instead of
-- possibly multiple other yazis that are running on this computer.
local yazi_id = string.format('%.0f', vim.uv.hrtime())
self.api = YaziProcessApi.new(config, yazi_id)

self.event_reader = config.use_ya_for_events_reading == true
and YaProcess.new(config, yazi_id)
Expand All @@ -48,7 +51,7 @@ function YaziProcess:start(config, path, on_exit)
if utils.file_exists(config.chosen_file_path) == true then
chosen_files = vim.fn.readfile(config.chosen_file_path)
end
on_exit(code, chosen_files, events)
on_exit(code, chosen_files, events, self.event_reader.hovered_url)
end,
})

Expand Down
30 changes: 30 additions & 0 deletions lua/yazi/process/yazi_process_api.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---@class YaziProcessApi
---@field private config YaziConfig
---@field private yazi_id string
local YaziProcessApi = {}

---@param config YaziConfig
---@param yazi_id string
function YaziProcessApi.new(config, yazi_id)
local self = setmetatable({}, YaziProcessApi)
self.config = config
self.yazi_id = yazi_id
return self
end

---@param path string
function YaziProcessApi:cd(path)
if
self.config.use_ya_for_events_reading == true
and self.config.use_yazi_client_id_flag == true
then
-- ya pub --str "/" dds-cd 1760127405452166
assert(path, 'path is required')
vim.system(
{ 'ya', 'pub', '--str', path, 'dds-cd', self.yazi_id },
{ timeout = 1000 }
)
end
end

return YaziProcessApi
4 changes: 4 additions & 0 deletions lua/yazi/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@

---@alias YaziEvent YaziRenameEvent | YaziMoveEvent | YaziDeleteEvent | YaziTrashEvent | YaziChangeDirectoryEvent | YaziHoverEvent | YaziBulkEvent

---@class (exact) YaziPreviousState # describes the previous state of yazi when it was closed; the last known state
---@field public last_hovered? string # only works if you set use_ya_for_events_reading

---@class (exact) YaziClosedState # describes the state of yazi when it was closed; the last known state
---@field public last_directory Path # the last directory that yazi was in before it was closed

Expand Down Expand Up @@ -70,6 +73,7 @@
---@field public url string

---@class (exact) YaziHoverEvent "The event that is emitted when the user hovers over a file in yazi"
---@field public yazi_id string
---@field public type "hover"
---@field public url string

Expand Down
2 changes: 2 additions & 0 deletions lua/yazi/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ function M.parse_events(events_file_lines)
-- example of a hover event:
-- hover,0,1720375364822700,{"tab":0,"url":"/tmp/test-directory/test"}
local data_string = table.concat(parts, ',', 4, #parts)
local yazi_id = parts[3]
local json = vim.json.decode(data_string, {
luanil = {
array = true,
Expand All @@ -155,6 +156,7 @@ function M.parse_events(events_file_lines)

---@type YaziHoverEvent
local event = {
yazi_id = yazi_id,
type = type,
url = url or '',
}
Expand Down
8 changes: 4 additions & 4 deletions spec/yazi/yazi_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ local assert = require('luassert')
local mock = require('luassert.mock')
local match = require('luassert.match')
local spy = require('luassert.spy')
package.loaded['yazi.yazi_process'] =
package.loaded['yazi.process.yazi_process'] =
require('spec.yazi.helpers.fake_yazi_process')
local fake_yazi_process = require('spec.yazi.helpers.fake_yazi_process')
local yazi_process = require('yazi.yazi_process')
local yazi_process = require('yazi.process.yazi_process')

local plugin = require('yazi')

describe('opening a file', function()
after_each(function()
package.loaded['yazi.yazi_process'] = yazi_process
package.loaded['yazi.process.yazi_process'] = yazi_process
end)

before_each(function()
mock.revert(fake_yazi_process)
package.loaded['yazi.yazi_process'] = mock(fake_yazi_process)
package.loaded['yazi.process.yazi_process'] = mock(fake_yazi_process)
plugin.setup({
-- set_keymappings_function can only work with a real yazi process
set_keymappings_function = function() end,
Expand Down

0 comments on commit dbddef0

Please sign in to comment.