From 19bd29e80594cfa8963f88fe2ab09e5421ac8379 Mon Sep 17 00:00:00 2001 From: Mika Vilpas Date: Sun, 7 Jul 2024 21:28:51 +0300 Subject: [PATCH] feat: highlight the currently hovered file in yazi (opt-in) For now, this change is opt-in (disabled by default). This change makes it possible to highlight the currently hovered file with a style of the user's own choosing. To enable it, you need to do the following steps in your configuration: - set `use_ya_for_events_reading = true` - set these new settings in your configuration: ```lua ---@type LazySpec { "mikavilpas/yazi.nvim", -- ...other settings you might already have ---@type YaziConfig opts = { -- add these: use_ya_for_events_reading = true, highlight_groups = { hovered_buffer_background = { bg = "#363a4f" }, }, -- ...other settings you might already have } } ``` For now, the color needs to be configured manually. In the future we may have a good default color. If you use catppuccin, you can find all the colors in the palette here: . I used the `Surface 0` color from my catppuccin macchiato palette. --- README.md | 6 + .../client/testEnvironmentTypes.ts | 1 + integration-tests/cypress.config.ts | 5 + .../hover-highlights.cy.ts | 124 ++++++++++++++++++ integration-tests/cypress/support/commands.ts | 18 ++- ..._yazi_config_to_use_ya_as_event_reader.lua | 3 + .../disposable_highlight.lua | 46 +++++++ .../highlight_hovered_buffer.lua | 65 +++++++++ lua/yazi/config.lua | 4 + lua/yazi/process/ya_process.lua | 39 ++++-- lua/yazi/types.lua | 10 +- lua/yazi/utils.lua | 47 ++++++- lua/yazi/yazi_process.lua | 2 +- spec/yazi/yazi_visible_buffer_spec.lua | 41 ++++++ 14 files changed, 389 insertions(+), 22 deletions(-) create mode 100644 integration-tests/cypress/e2e/using-ya-to-read-events/hover-highlights.cy.ts create mode 100644 lua/yazi/buffer_highlighting/disposable_highlight.lua create mode 100644 lua/yazi/buffer_highlighting/highlight_hovered_buffer.lua create mode 100644 spec/yazi/yazi_visible_buffer_spec.lua diff --git a/README.md b/README.md index 54d40602..e33d777d 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,12 @@ You can optionally configure yazi.nvim by setting any of the options below. -- https://github.com/mikavilpas/yazi.nvim/pull/152 use_ya_for_events_reading = false, + -- an upcoming optional feature + highlight_groups = { + -- NOTE: this only works if `use_ya_for_events_reading` is enabled, etc. + hovered_buffer_background = nil, + }, + -- the floating window scaling factor. 1 means 100%, 0.9 means 90%, etc. floating_window_scaling_factor = 0.9, diff --git a/integration-tests/client/testEnvironmentTypes.ts b/integration-tests/client/testEnvironmentTypes.ts index 2cb78f32..5f0117c3 100644 --- a/integration-tests/client/testEnvironmentTypes.ts +++ b/integration-tests/client/testEnvironmentTypes.ts @@ -54,6 +54,7 @@ export type TestDirectory = { ["initial-file.txt"]: FileEntry ["test.lua"]: FileEntry ["file.txt"]: FileEntry + ["modify_yazi_config_to_use_ya_as_event_reader.lua"]: FileEntry ["subdirectory/sub.txt"]: FileEntry ["routes/posts.$postId/route.tsx"]: FileEntry ["routes/posts.$postId/adjacent-file.tsx"]: FileEntry diff --git a/integration-tests/cypress.config.ts b/integration-tests/cypress.config.ts index 38c04493..2209eed3 100644 --- a/integration-tests/cypress.config.ts +++ b/integration-tests/cypress.config.ts @@ -87,6 +87,11 @@ export default defineConfig({ stem: "sub", extension: ".txt", }, + "modify_yazi_config_to_use_ya_as_event_reader.lua": { + name: "modify_yazi_config_to_use_ya_as_event_reader.lua", + stem: "modify_yazi_config_to_use_ya_as_event_reader", + extension: ".lua", + }, "routes/posts.$postId/adjacent-file.tsx": { name: "adjacent-file.tsx", stem: "adjacent-file", diff --git a/integration-tests/cypress/e2e/using-ya-to-read-events/hover-highlights.cy.ts b/integration-tests/cypress/e2e/using-ya-to-read-events/hover-highlights.cy.ts new file mode 100644 index 00000000..6ccae388 --- /dev/null +++ b/integration-tests/cypress/e2e/using-ya-to-read-events/hover-highlights.cy.ts @@ -0,0 +1,124 @@ +import { startNeovimWithYa } from "./startNeovimWithYa" + +describe("highlighting the buffer with 'hover' events", () => { + beforeEach(() => { + cy.visit("http://localhost:5173") + }) + + const backgroundColors = { + normal: "rgb(36, 39, 58)", + hovered: "rgb(73, 77, 100)", + } as const + + // NOTE: when opening the file, the cursor is placed at the beginning of + // the file. This causes the web terminal to render multiple elements for the + // same text, and this can cause issues when matching colors, as in the DOM + // there are multiple colors. Work around this by matching a substring of the + // text instead of the whole text. + + 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!") + .children() + .should("have.css", "background-color", backgroundColors.normal) + + // start yazi + cy.typeIntoTerminal("{upArrow}") + + // yazi is shown and adjacent files should be visible now + // + // the current file (initial-file.txt) is highlighted by default when + // opening yazi. This should have sent the 'hover' event and caused the + // Neovim window to be shown with a different background color + cy.contains("If you see this text, Neovim is ready!").should( + "have.css", + "background-color", + backgroundColors.hovered, + ) + + // close yazi - the highlight should be removed and we should see the + // same color as before + cy.typeIntoTerminal("q") + cy.contains("Neovim is ready!").should( + "have.css", + "background-color", + backgroundColors.normal, + ) + }) + }) + + it("can remove the highlight when the cursor is moved away", () => { + startNeovimWithYa().then((dir) => { + // wait until text on the start screen is visible + cy.contains("Neovim is ready!").should( + "have.css", + "background-color", + backgroundColors.normal, + ) + + // start yazi + cy.typeIntoTerminal("{upArrow}") + + // yazi is shown and adjacent files should be visible now + cy.contains(dir.contents["test.lua"].name) + + // the current file (initial-file.txt) is highlighted by default when + // opening yazi. This should have sent the 'hover' event and caused the + // Neovim window to be shown with a different background color + cy.contains("Neovim is ready!").should( + "have.css", + "background-color", + backgroundColors.hovered, + ) + + // hover another file - the highlight should be removed + cy.typeIntoTerminal(`/^${dir.contents["test.lua"].name}{enter}`) + + cy.contains("Neovim is ready!").should( + "have.css", + "background-color", + backgroundColors.normal, + ) + }) + }) + + it("can move the highlight to another buffer when hovering over it", () => { + startNeovimWithYa().then((dir) => { + // wait until text on the start screen is visible + cy.contains("If you see this text, Neovim is ready!") + .children() + .should("have.css", "background-color", backgroundColors.normal) + + // open an adjacent file and wait for it to be displayed + cy.typeIntoTerminal( + `:vsplit ${dir.rootPath}/${dir.contents["test.lua"].name}{enter}`, + { delay: 1 }, + ) + cy.contains("how to initialize the test environment") + + // start yazi - the initial file should be highlighted + cy.typeIntoTerminal("{upArrow}") + cy.contains("how to initialize the test environment").should( + "have.css", + "background-color", + backgroundColors.hovered, + ) + + // select the other file - the highlight should move to it + cy.typeIntoTerminal(`/^${dir.contents["initial-file.txt"].name}{enter}`, { + delay: 1, + }) + cy.contains("how to initialize the test environment").should( + "have.css", + "background-color", + backgroundColors.normal, + ) + cy.contains("If you see this text, Neovim is ready!").should( + "have.css", + "background-color", + backgroundColors.hovered, + ) + }) + }) +}) diff --git a/integration-tests/cypress/support/commands.ts b/integration-tests/cypress/support/commands.ts index a7372707..bfe2298b 100644 --- a/integration-tests/cypress/support/commands.ts +++ b/integration-tests/cypress/support/commands.ts @@ -54,17 +54,23 @@ Cypress.Commands.add("startNeovim", (startArguments?: StartNeovimArguments) => { }) }) -Cypress.Commands.add("typeIntoTerminal", (text: string) => { - // the syntax for keys is described here: - // https://docs.cypress.io/api/commands/type - cy.get("#app").type(text) -}) +Cypress.Commands.add( + "typeIntoTerminal", + (text: string, options?: Partial) => { + // the syntax for keys is described here: + // https://docs.cypress.io/api/commands/type + cy.get("#app").type(text, options) + }, +) declare global { namespace Cypress { interface Chainable { startNeovim(args?: StartNeovimArguments): Chainable - typeIntoTerminal(text: string): Chainable + typeIntoTerminal( + text: string, + options?: Partial, + ): Chainable task(event: "createTempDir"): Chainable } } diff --git a/integration-tests/test-environment/config-modifications/modify_yazi_config_to_use_ya_as_event_reader.lua b/integration-tests/test-environment/config-modifications/modify_yazi_config_to_use_ya_as_event_reader.lua index 8ff77a47..0871fc83 100644 --- a/integration-tests/test-environment/config-modifications/modify_yazi_config_to_use_ya_as_event_reader.lua +++ b/integration-tests/test-environment/config-modifications/modify_yazi_config_to_use_ya_as_event_reader.lua @@ -4,5 +4,8 @@ require('yazi').setup( ---@type YaziConfig { use_ya_for_events_reading = true, + highlight_groups = { + hovered_buffer_background = { bg = '#494d64' }, + }, } ) diff --git a/lua/yazi/buffer_highlighting/disposable_highlight.lua b/lua/yazi/buffer_highlighting/disposable_highlight.lua new file mode 100644 index 00000000..e756653f --- /dev/null +++ b/lua/yazi/buffer_highlighting/disposable_highlight.lua @@ -0,0 +1,46 @@ +local Log = require('yazi.log') + +---@class yazi.DisposableHighlight +---@field private old_winhighlight string +---@field private window_id integer +local DisposableHighlight = {} +DisposableHighlight.__index = DisposableHighlight + +---@param window_id integer +---@param highlight_config YaziConfigHighlightGroups +function DisposableHighlight.new(window_id, highlight_config) + local self = setmetatable({}, DisposableHighlight) + self.window_id = window_id + self.old_winhighlight = vim.wo.winhighlight + + vim.api.nvim_set_hl( + 0, + 'YaziBufferHoveredBackground', + highlight_config.hovered_buffer_background + ) + + vim.api.nvim_set_option_value( + 'winhighlight', + 'Normal:YaziBufferHoveredBackground', + { win = window_id } + ) + + return self +end + +function DisposableHighlight:dispose() + Log:debug( + string.format( + 'Disposing of the DisposableHighlight for window_id %s', + self.window_id + ) + ) + -- Revert winhighlight to its old value + if vim.api.nvim_win_is_valid(self.window_id) then + vim.api.nvim_set_option_value('winhighlight', self.old_winhighlight, { + win = self.window_id, + }) + end +end + +return DisposableHighlight diff --git a/lua/yazi/buffer_highlighting/highlight_hovered_buffer.lua b/lua/yazi/buffer_highlighting/highlight_hovered_buffer.lua new file mode 100644 index 00000000..2c0c392e --- /dev/null +++ b/lua/yazi/buffer_highlighting/highlight_hovered_buffer.lua @@ -0,0 +1,65 @@ +local Log = require('yazi.log') +local utils = require('yazi.utils') +local DisposableHighlight = + require('yazi.buffer_highlighting.disposable_highlight') + +local M = {} + +-- The currently highlighted windows. Global because there can only be one yazi +-- at a time. +---@type table +local window_highlights = {} + +function M.clear_highlights() + for _, hl in pairs(window_highlights) do + pcall(hl.dispose, hl) + end + + window_highlights = {} +end + +---@param url string +---@param highlight_config YaziConfigHighlightGroups +function M.highlight_hovered_buffer(url, highlight_config) + local visible_open_buffers = utils.get_visible_open_buffers() + for _, buffer in ipairs(visible_open_buffers) do + if buffer.renameable_buffer:matches_exactly(url) then + Log:debug( + 'highlighting buffer ' + .. buffer.renameable_buffer.bufnr + .. ' in window ' + .. buffer.window_id + ) + local hl = window_highlights[buffer.window_id] + if hl == nil then + hl = DisposableHighlight.new(buffer.window_id, highlight_config) + window_highlights[buffer.window_id] = hl + end + else + -- only one file can be hovered at a time, so let's clear + -- the highlight + local hl = window_highlights[buffer.window_id] + if hl ~= nil then + local success = pcall(hl.dispose, hl) + window_highlights[buffer.window_id] = nil + if success then + Log:debug( + 'disposed of highlight for buffer ' + .. buffer.renameable_buffer.bufnr + .. ' in window ' + .. buffer.renameable_buffer.bufnr + ) + else + Log:debug( + 'failed to dispose of highlight for buffer ' + .. buffer.renameable_buffer.bufnr + .. ' in window ' + .. buffer.window_id + ) + end + end + end + end +end + +return M diff --git a/lua/yazi/config.lua b/lua/yazi/config.lua index 98f46a67..3db6891c 100644 --- a/lua/yazi/config.lua +++ b/lua/yazi/config.lua @@ -21,6 +21,10 @@ function M.default() yazi_opened_multiple_files = openers.send_files_to_quickfix_list, }, + highlight_groups = { + hovered_buffer_background = nil, + }, + integrations = { grep_in_directory = function(directory) require('telescope.builtin').live_grep({ diff --git a/lua/yazi/process/ya_process.lua b/lua/yazi/process/ya_process.lua index 34023fd1..6c6eb797 100644 --- a/lua/yazi/process/ya_process.lua +++ b/lua/yazi/process/ya_process.lua @@ -1,10 +1,15 @@ ---@module "plenary.path" +---@alias WindowId integer + local Log = require('yazi.log') local utils = require('yazi.utils') +local highlight_hovered_buffer = + require('yazi.buffer_highlighting.highlight_hovered_buffer') ---@class (exact) YaProcess ---@field public events YaziEvent[] "The events that have been received from yazi" +---@field public new fun(config: YaziConfig): YaProcess ---@field private config YaziConfig ---@field private ya_process vim.SystemObj ---@field private retries integer @@ -13,7 +18,8 @@ local YaProcess = {} YaProcess.__index = YaProcess ---@param config YaziConfig -function YaProcess:new(config) +function YaProcess.new(config) + local self = setmetatable({}, YaProcess) self.config = config self.events = {} self.retries = 0 @@ -33,6 +39,7 @@ end function YaProcess:kill() Log:debug('Killing ya process') pcall(self.ya_process.kill, self.ya_process, 'sigterm') + highlight_hovered_buffer.clear_highlights() end function YaProcess:wait(timeout) @@ -42,7 +49,7 @@ function YaProcess:wait(timeout) end function YaProcess:start() - local ya_command = { 'ya', 'sub', 'rename,delete,trash,move,cd' } + local ya_command = { 'ya', 'sub', 'rename,delete,trash,move,cd,hover' } Log:debug( string.format( 'Opening ya with the command: (%s)', @@ -85,21 +92,27 @@ function YaProcess:start() if err then Log:debug(string.format("ya stdout error: '%s'", data)) end + data = data or '' - if data == nil then - -- weird event, ignore - return - end + Log:debug(string.format("ya stdout: '%s'", data)) - -- remove the final newline character because it's annoying in the logs - if data:sub(-1) == '\n' then - data = data:sub(1, -2) - end + 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("ya stdout: '%s'", data)) - local parsed = utils.safe_parse_events({ data }) for _, event in ipairs(parsed) do - self.events[#self.events + 1] = event + if event.type == 'hover' then + vim.schedule(function() + ---@cast event YaziHoverEvent + highlight_hovered_buffer.highlight_hovered_buffer( + event.url, + self.config.highlight_groups + ) + end) + else + self.events[#self.events + 1] = event + end end end, diff --git a/lua/yazi/types.lua b/lua/yazi/types.lua index 8ad91067..f6302128 100644 --- a/lua/yazi/types.lua +++ b/lua/yazi/types.lua @@ -11,6 +11,7 @@ ---@field public open_file_function? fun(chosen_file: string, config: YaziConfig, state: YaziClosedState): nil "a function that will be called when a file is chosen in yazi" ---@field public set_keymappings_function? fun(buffer: integer, config: YaziConfig): nil "the function that will set the keymappings for the yazi floating window. It will be called after the floating window is created." ---@field public hooks? YaziConfigHooks +---@field public highlight_groups? YaziConfigHighlightGroups ---@field public integrations? YaziConfigIntegrations ---@field public floating_window_scaling_factor? number "the scaling factor for the floating window. 1 means 100%, 0.9 means 90%, etc." ---@field public yazi_floating_window_winblend? number "the transparency of the yazi floating window (0-100). See :h winblend" @@ -25,7 +26,10 @@ ---@class (exact) YaziConfigIntegrations # Defines settings for integrations with other plugins and tools ---@field public grep_in_directory? fun(directory: string): nil "a function that will be called when the user wants to grep in a directory" ----@alias YaziEvent YaziRenameEvent | YaziMoveEvent | YaziDeleteEvent | YaziTrashEvent | YaziChangeDirectoryEvent +---@class (exact) YaziConfigHighlightGroups # Defines the highlight groups that will be used in yazi +---@field public hovered_buffer_background? vim.api.keyset.highlight # the color of the background of buffer that is hovered over + +---@alias YaziEvent YaziRenameEvent | YaziMoveEvent | YaziDeleteEvent | YaziTrashEvent | YaziChangeDirectoryEvent | YaziHoverEvent ---@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 @@ -64,6 +68,10 @@ ---@field public id string ---@field public url string +---@class (exact) YaziHoverEvent "The event that is emitted when the user hovers over a file in yazi" +---@field public type "hover" +---@field public url string + ---@class (exact) yazi.AutoCmdEvent # the nvim_create_autocmd() event object copied from the nvim help docs ---@field public id number ---@field public event string diff --git a/lua/yazi/utils.lua b/lua/yazi/utils.lua index 114326a6..70bb4a8a 100644 --- a/lua/yazi/utils.lua +++ b/lua/yazi/utils.lua @@ -128,6 +128,27 @@ function M.parse_events(events_file_lines) url = vim.json.decode(data_string)['url'], } table.insert(events, event) + elseif type == 'hover' then + -- example of a hover event: + -- hover,0,1720375364822700,{"tab":0,"url":"/tmp/test-directory/test"} + local data_string = table.concat(parts, ',', 4, #parts) + local json = vim.json.decode(data_string, { + luanil = { + array = true, + object = true, + }, + }) + + -- sometimes ya sends a hover event without a url, not sure why + ---@type string | nil + local url = json['url'] + + ---@type YaziHoverEvent + local event = { + type = type, + url = url or '', + } + table.insert(events, event) end end @@ -163,7 +184,7 @@ function M.get_open_buffers() local path = vim.api.nvim_buf_get_name(bufnr) local type = vim.api.nvim_get_option_value('buftype', { buf = bufnr }) - local is_ordinary_file = path ~= '' and type == '' + local is_ordinary_file = path ~= vim.NIL and path ~= '' and type == '' if is_ordinary_file then local renameable_buffer = RenameableBuffer.new(bufnr, path) open_buffers[#open_buffers + 1] = renameable_buffer @@ -173,6 +194,30 @@ function M.get_open_buffers() return open_buffers end +---@alias YaziVisibleBuffer { renameable_buffer: RenameableBuffer, window_id: integer } + +function M.get_visible_open_buffers() + local open_buffers = M.get_open_buffers() + + ---@type YaziVisibleBuffer[] + local visible_open_buffers = {} + for _, buffer in ipairs(open_buffers) do + local windows = vim.api.nvim_tabpage_list_wins(0) + for _, window_id in ipairs(windows) do + if vim.api.nvim_win_get_buf(window_id) == buffer.bufnr then + ---@type YaziVisibleBuffer + local data = { + renameable_buffer = buffer, + window_id = window_id, + } + visible_open_buffers[#visible_open_buffers + 1] = data + end + end + end + + return visible_open_buffers +end + ---@param path string ---@return boolean function M.is_buffer_open(path) diff --git a/lua/yazi/yazi_process.lua b/lua/yazi/yazi_process.lua index 628b9cf6..631952f8 100644 --- a/lua/yazi/yazi_process.lua +++ b/lua/yazi/yazi_process.lua @@ -27,7 +27,7 @@ function YaziProcess:start(config, path, on_exit) ) ) self.event_reader = config.use_ya_for_events_reading == true - and YaProcess:new(config) + and YaProcess.new(config) or LegacyEventReadingFromEventFile:new(config) local yazi_cmd = self.event_reader:get_yazi_command(path) diff --git a/spec/yazi/yazi_visible_buffer_spec.lua b/spec/yazi/yazi_visible_buffer_spec.lua new file mode 100644 index 00000000..489ffe41 --- /dev/null +++ b/spec/yazi/yazi_visible_buffer_spec.lua @@ -0,0 +1,41 @@ +local utils = require('yazi.utils') +local assert = require('luassert') + +describe('#focus YaziVisibleBuffer', 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 + + -- close all windows + for _, win in ipairs(vim.api.nvim_list_wins()) do + pcall(vim.api.nvim_win_close, win, true) + end + end) + + it('is found for a visible buffer editing a file', function() + -- + vim.cmd('edit file1') + vim.fn.bufadd('/YaziVisibleBuffer/file1') + + local visible_open_buffers = utils.get_visible_open_buffers() + + assert.equals(1, #visible_open_buffers) + end) + + it("is not found for a buffer that's not visible", function() + vim.cmd('edit file1') + vim.fn.bufadd('/YaziVisibleBuffer/file2') + + local visible_open_buffers = utils.get_visible_open_buffers() + + assert.equals(1, #visible_open_buffers) + local visible_open_buffer = visible_open_buffers[1] + + assert.equals( + 'file1', + visible_open_buffer.renameable_buffer.path.filename:match('file1') + ) + end) +end)