From 6059639b0e8e5406e0fdc7d7bd1ea18654307ea7 Mon Sep 17 00:00:00 2001 From: Mika Vilpas Date: Wed, 11 Dec 2024 21:22:39 +0200 Subject: [PATCH] feat: prevent conflicts with custom yazi config for `` (opt-in) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue ===== For custom ways to open files, such as "open in new vertical split", "open in new tab", etc., yazi.nvim uses the `` key to open the file. This is not very robust because the user might have set a custom keybinding for the `` key in their yazi config. In this case, the custom keybinding would be triggered instead of the file being opened. Solution ======== Instead of relying on the `` key, yazi.nvim can now use `ya emit open` to make yazi open the file(s) that are currently selected. This completely avoids the issue, but requires a recent version of yazi (0.4.0 or later). To opt into this behaviour, set the following in your config: ```lua { -- example for lazy.nvim "mikavilpas/yazi.nvim", -- ... (other settings here) ... ---@type YaziConfig opts = { future_features = { ya_emit_open = true, -- 👆🏻 this is the new setting }, }, } ``` This issue was found in https://github.com/mikavilpas/yazi.nvim/issues/611 where smart-enter.yazi was found to have this behaviour by default: https://github.com/yazi-rs/plugins/tree/main/smart-enter.yazi#advanced --- README.md | 10 +- integration-tests/MyTestDirectory.ts | 7 + .../opening-files-with-legacy-open.cy.ts | 244 ++++++++++++++++++ .../test-environment/.config/nvim/init.lua | 3 + ...fy_yazi_config_do_not_use_ya_emit_open.lua | 10 + lua/yazi/config.lua | 10 +- lua/yazi/health.lua | 8 + lua/yazi/keybinding_helpers.lua | 27 +- lua/yazi/process/yazi_process_api.lua | 7 + lua/yazi/types.lua | 3 +- spec/yazi/health_spec.lua | 33 +++ 11 files changed, 347 insertions(+), 15 deletions(-) create mode 100644 integration-tests/cypress/e2e/using-ya-to-read-events/opening-files-with-legacy-open.cy.ts create mode 100644 integration-tests/test-environment/config-modifications/modify_yazi_config_do_not_use_ya_emit_open.lua diff --git a/README.md b/README.md index ea07eb27..6ad3a70a 100644 --- a/README.md +++ b/README.md @@ -280,9 +280,13 @@ You can optionally configure yazi.nvim by setting any of the options below. future_features = { -- Whether to use `ya emit reveal` to reveal files in the file manager. - -- This requires yazi 0.4.0 but will likely be the default in the - -- future. - ya_emit_reveal = true, + -- Requires yazi 0.4.0 or later (from 2024-12-08). + ya_emit_reveal = false, + + -- Use `ya emit open` as a more robust implementation for opening files + -- in yazi. This can prevent conflicts with custom keymappings for the enter + -- key. Requires yazi 0.4.0 or later (from 2024-12-08). + ya_emit_open = false, }, }, } diff --git a/integration-tests/MyTestDirectory.ts b/integration-tests/MyTestDirectory.ts index 6f465035..a1805b92 100644 --- a/integration-tests/MyTestDirectory.ts +++ b/integration-tests/MyTestDirectory.ts @@ -93,6 +93,12 @@ export const MyTestDirectorySchema = z.object({ extension: z.literal("lua"), stem: z.literal("modify_yazi_config_and_set_help_key."), }), + "modify_yazi_config_do_not_use_ya_emit_open.lua": z.object({ + name: z.literal("modify_yazi_config_do_not_use_ya_emit_open.lua"), + type: z.literal("file"), + extension: z.literal("lua"), + stem: z.literal("modify_yazi_config_do_not_use_ya_emit_open."), + }), "modify_yazi_config_log_yazi_closed_successfully.lua": z.object({ name: z.literal( "modify_yazi_config_log_yazi_closed_successfully.lua", @@ -276,6 +282,7 @@ export const testDirectoryFiles = z.enum([ "config-modifications/modify_yazi_config_and_highlight_buffers_in_same_directory.lua", "config-modifications/modify_yazi_config_and_open_multiple_files.lua", "config-modifications/modify_yazi_config_and_set_help_key.lua", + "config-modifications/modify_yazi_config_do_not_use_ya_emit_open.lua", "config-modifications/modify_yazi_config_log_yazi_closed_successfully.lua", "config-modifications/modify_yazi_config_use_ya_emit_reveal.lua", "config-modifications/notify_custom_events.lua", diff --git a/integration-tests/cypress/e2e/using-ya-to-read-events/opening-files-with-legacy-open.cy.ts b/integration-tests/cypress/e2e/using-ya-to-read-events/opening-files-with-legacy-open.cy.ts new file mode 100644 index 00000000..65a83fc6 --- /dev/null +++ b/integration-tests/cypress/e2e/using-ya-to-read-events/opening-files-with-legacy-open.cy.ts @@ -0,0 +1,244 @@ +import type { MyTestDirectoryFile } from "MyTestDirectory" +import { + isFileNotSelectedInYazi, + isFileSelectedInYazi, +} from "./utils/yazi-utils" + +describe("opening files with yazi < 0.4.0", () => { + // versions before this do not support `ya emit open`. Instead, they send a + // fake keypress to yazi and react on the file that was opened by + // yazi. This approach is not very robust because the user might have set a + // custom enter key - in which case the whole thing does not work. + // + // This has been fixed for recent yazi versions by using `ya emit open` to + // send the file path to yazi. These tests are here until the legacy approach + // is removed. + beforeEach(() => { + cy.visit("/") + }) + + it("can open a file in a vertical split", () => { + cy.startNeovim({ + startupScriptModifications: [ + "modify_yazi_config_do_not_use_ya_emit_open.lua", + ], + }).then((dir) => { + cy.contains("If you see this text, Neovim is ready!") + cy.typeIntoTerminal("{upArrow}") + isFileNotSelectedInYazi("file2.txt" satisfies MyTestDirectoryFile) + cy.typeIntoTerminal( + `/${"file2.txt" satisfies MyTestDirectoryFile}{enter}`, + ) + cy.typeIntoTerminal("{esc}") // hide the search highlight + isFileSelectedInYazi("file2.txt" satisfies MyTestDirectoryFile) + cy.typeIntoTerminal("{control+v}") + + // yazi should now be closed + cy.contains("-- TERMINAL --").should("not.exist") + + // the file path must be visible at the bottom + cy.contains(dir.contents["file2.txt"].name) + cy.contains(dir.contents["initial-file.txt"].name) + }) + }) + + it("can open a file in a horizontal split", () => { + cy.startNeovim({ + startupScriptModifications: [ + "modify_yazi_config_do_not_use_ya_emit_open.lua", + ], + }).then((dir) => { + cy.contains("If you see this text, Neovim is ready!") + cy.typeIntoTerminal("{upArrow}") + cy.contains(dir.contents["file2.txt"].name) + cy.typeIntoTerminal(`/${dir.contents["file2.txt"].name}{enter}`) + cy.typeIntoTerminal("{esc}") // hide the search highlight + isFileSelectedInYazi(dir.contents["file2.txt"].name) + cy.typeIntoTerminal("{control+x}") + + // yazi should now be closed + cy.contains("-- TERMINAL --").should("not.exist") + + // the file path must be visible at the bottom + cy.contains(dir.contents["file2.txt"].name) + cy.contains(dir.contents["initial-file.txt"].name) + }) + }) + + it("can open a file in a new tab", () => { + cy.startNeovim({ + startupScriptModifications: [ + "modify_yazi_config_do_not_use_ya_emit_open.lua", + ], + }).then((dir) => { + cy.contains("If you see this text, Neovim is ready!") + cy.typeIntoTerminal("{upArrow}") + isFileNotSelectedInYazi(dir.contents["file2.txt"].name) + cy.contains(dir.contents["file2.txt"].name) + cy.typeIntoTerminal(`/${dir.contents["file2.txt"].name}{enter}`) + cy.typeIntoTerminal("{esc}") // hide the search highlight + isFileSelectedInYazi(dir.contents["file2.txt"].name) + cy.typeIntoTerminal("{control+t}") + + // yazi should now be closed + cy.contains("-- TERMINAL --").should("not.exist") + + cy.contains( + // match some text from inside the file + "Hello", + ) + cy.runExCommand({ command: "tabnext" }) + + cy.contains("If you see this text, Neovim is ready!") + + cy.contains(dir.contents["file2.txt"].name) + cy.contains(dir.contents["initial-file.txt"].name) + }) + }) + + it("can send file names to the quickfix list", () => { + cy.startNeovim({ + filename: "file2.txt", + startupScriptModifications: [ + "modify_yazi_config_do_not_use_ya_emit_open.lua", + ], + }).then((dir) => { + cy.contains("Hello") + cy.typeIntoTerminal("{upArrow}") + + // wait for yazi to open + cy.contains(dir.contents["file2.txt"].name) + + // file2.txt should be selected + isFileSelectedInYazi("file2.txt" satisfies MyTestDirectoryFile) + + // select file2, the cursor moves one line down to the next file + cy.typeIntoTerminal(" ") + isFileNotSelectedInYazi("file2.txt" satisfies MyTestDirectoryFile) + + // also select the next file because multiple files have to be selected + isFileSelectedInYazi("file3.txt" satisfies MyTestDirectoryFile) + cy.typeIntoTerminal(" ") + isFileNotSelectedInYazi("file3.txt" satisfies MyTestDirectoryFile) + cy.typeIntoTerminal("{control+q}") + + // yazi should now be closed + cy.contains("-- TERMINAL --").should("not.exist") + + // items in the quickfix list should now be visible + cy.contains(`${dir.contents["file2.txt"].name}||`) + cy.contains(`${dir.contents["file3.txt"].name}||`) + }) + }) + + it("can copy the relative path to the initial file", () => { + // the copied path should be relative to the file/directory yazi was + // started in (the initial file) + + cy.startNeovim({ + startupScriptModifications: [ + "modify_yazi_config_do_not_use_ya_emit_open.lua", + ], + }).then((dir) => { + cy.contains("If you see this text, Neovim is ready!") + + cy.typeIntoTerminal("{upArrow}") + isFileNotSelectedInYazi("file2.txt" satisfies MyTestDirectoryFile) + + // enter another directory and select a file + cy.typeIntoTerminal("/routes{enter}") + cy.contains("posts.$postId") + cy.typeIntoTerminal("{rightArrow}") + cy.contains( + dir.contents.routes.contents["posts.$postId"].contents["route.tsx"] + .name, + ) // file in the directory + cy.typeIntoTerminal("{rightArrow}") + cy.typeIntoTerminal( + `/${ + dir.contents.routes.contents["posts.$postId"].contents[ + "adjacent-file.txt" + ].name + }{enter}{esc}`, + // esc to hide the search highlight + ) + isFileSelectedInYazi( + dir.contents.routes.contents["posts.$postId"].contents[ + "adjacent-file.txt" + ].name, + ) + + // the file contents should now be visible + cy.contains("this file is adjacent-file.txt") + + cy.typeIntoTerminal("{control+y}") + + // yazi should now be closed + cy.contains( + dir.contents.routes.contents["posts.$postId"].contents["route.tsx"] + .name, + ).should("not.exist") + + // the relative path should now be in the clipboard. Let's paste it to + // the file to verify this. + // NOTE: the test-setup configures the `"` register to be the clipboard + cy.typeIntoTerminal("o{enter}{esc}") + cy.runLuaCode({ luaCode: `return vim.fn.getreg('"')` }).then((result) => { + expect(result.value).to.contain( + "routes/posts.$postId/adjacent-file.txt" satisfies MyTestDirectoryFile, + ) + }) + }) + }) + + it("can copy the relative paths of multiple selected files", () => { + // similarly, the copied path should be relative to the file/directory yazi + // was started in (the initial file) + + cy.startNeovim({ + startupScriptModifications: [ + "modify_yazi_config_do_not_use_ya_emit_open.lua", + ], + }).then((dir) => { + cy.contains("If you see this text, Neovim is ready!") + + cy.typeIntoTerminal("{upArrow}") + cy.contains(dir.contents["file2.txt"].name) + + // enter another directory and select a file + cy.typeIntoTerminal("/routes{enter}") + cy.contains("posts.$postId") + cy.typeIntoTerminal("{rightArrow}") + cy.contains( + dir.contents.routes.contents["posts.$postId"].contents["route.tsx"] + .name, + ) // file in the directory + cy.typeIntoTerminal("{rightArrow}") + cy.typeIntoTerminal("{control+a}") + + cy.typeIntoTerminal("{control+y}") + + // yazi should now be closed + cy.contains( + dir.contents.routes.contents["posts.$postId"].contents["route.tsx"] + .name, + ).should("not.exist") + + // the relative path should now be in the clipboard. Let's paste it to + // the file to verify this. + // NOTE: the test-setup configures the `"` register to be the clipboard + cy.typeIntoTerminal("o{enter}{esc}") + cy.runLuaCode({ luaCode: `return vim.fn.getreg('"')` }).then((result) => { + expect(result.value).to.eql( + ( + [ + "routes/posts.$postId/adjacent-file.txt", + "routes/posts.$postId/route.tsx", + "routes/posts.$postId/should-be-excluded-file.txt", + ] satisfies MyTestDirectoryFile[] + ).join("\n"), + ) + }) + }) + }) +}) diff --git a/integration-tests/test-environment/.config/nvim/init.lua b/integration-tests/test-environment/.config/nvim/init.lua index 30251c46..eb95c93e 100644 --- a/integration-tests/test-environment/.config/nvim/init.lua +++ b/integration-tests/test-environment/.config/nvim/init.lua @@ -60,6 +60,9 @@ local plugins = { clipboard_register = '"', -- allows logging debug data, which can be shown in CI when cypress tests fail log_level = vim.log.levels.DEBUG, + future_features = { + ya_emit_open = true, + }, integrations = { grep_in_directory = function(directory) require("telescope.builtin").live_grep({ diff --git a/integration-tests/test-environment/config-modifications/modify_yazi_config_do_not_use_ya_emit_open.lua b/integration-tests/test-environment/config-modifications/modify_yazi_config_do_not_use_ya_emit_open.lua new file mode 100644 index 00000000..4fd790cb --- /dev/null +++ b/integration-tests/test-environment/config-modifications/modify_yazi_config_do_not_use_ya_emit_open.lua @@ -0,0 +1,10 @@ +---@module "yazi" + +require("yazi").setup( + ---@type YaziConfig + { + future_features = { + ya_emit_open = false, + }, + } +) diff --git a/lua/yazi/config.lua b/lua/yazi/config.lua index eea7aeba..2e5f1bbb 100644 --- a/lua/yazi/config.lua +++ b/lua/yazi/config.lua @@ -114,7 +114,7 @@ function M.set_keymappings(yazi_buffer, config, context) { "t" }, config.keymaps.open_file_in_vertical_split, function() - keybinding_helpers.open_file_in_vertical_split(config) + keybinding_helpers.open_file_in_vertical_split(config, context.api) end, { buffer = yazi_buffer } ) @@ -125,7 +125,7 @@ function M.set_keymappings(yazi_buffer, config, context) { "t" }, config.keymaps.open_file_in_horizontal_split, function() - keybinding_helpers.open_file_in_horizontal_split(config) + keybinding_helpers.open_file_in_horizontal_split(config, context.api) end, { buffer = yazi_buffer } ) @@ -134,6 +134,7 @@ function M.set_keymappings(yazi_buffer, config, context) if config.keymaps.grep_in_directory ~= false then vim.keymap.set({ "t" }, config.keymaps.grep_in_directory, function() keybinding_helpers.select_current_file_and_close_yazi(config, { + api = context.api, on_file_opened = function(chosen_file, _, _) keybinding_helpers.grep_in_directory(config, chosen_file) end, @@ -146,7 +147,7 @@ function M.set_keymappings(yazi_buffer, config, context) if config.keymaps.open_file_in_tab ~= false then vim.keymap.set({ "t" }, config.keymaps.open_file_in_tab, function() - keybinding_helpers.open_file_in_tab(config) + keybinding_helpers.open_file_in_tab(config, context.api) end, { buffer = yazi_buffer }) end @@ -159,6 +160,7 @@ function M.set_keymappings(yazi_buffer, config, context) if config.keymaps.replace_in_directory ~= false then vim.keymap.set({ "t" }, config.keymaps.replace_in_directory, function() keybinding_helpers.select_current_file_and_close_yazi(config, { + api = context.api, on_file_opened = function(chosen_file) keybinding_helpers.replace_in_directory(config, chosen_file) end, @@ -173,6 +175,7 @@ function M.set_keymappings(yazi_buffer, config, context) vim.keymap.set({ "t" }, config.keymaps.send_to_quickfix_list, function() local openers = require("yazi.openers") keybinding_helpers.select_current_file_and_close_yazi(config, { + api = context.api, on_multiple_files_opened = openers.send_files_to_quickfix_list, on_file_opened = function(chosen_file) openers.send_files_to_quickfix_list({ chosen_file }) @@ -268,6 +271,7 @@ function M.set_keymappings(yazi_buffer, config, context) config.keymaps.copy_relative_path_to_selected_files, function() keybinding_helpers.select_current_file_and_close_yazi(config, { + api = context.api, on_file_opened = function(chosen_file) local relative_path = require("yazi.utils").relative_path( config, diff --git a/lua/yazi/health.lua b/lua/yazi/health.lua index 93cdf556..02f435b5 100644 --- a/lua/yazi/health.lua +++ b/lua/yazi/health.lua @@ -121,6 +121,14 @@ return { end end + if config.future_features and config.future_features.ya_emit_open then + if not checker.ge(yazi_semver, "0.4.0") then + vim.health.warn( + "You have enabled `future_features.ya_emit_open` in your config. This requires yazi.nvim version 0.4.0 or newer." + ) + end + end + vim.health.start("yazi.config") vim.health.info(table.concat({ "hint: execute the following command to see your configuration: >", diff --git a/lua/yazi/keybinding_helpers.lua b/lua/yazi/keybinding_helpers.lua index 9bff7d53..924a101e 100644 --- a/lua/yazi/keybinding_helpers.lua +++ b/lua/yazi/keybinding_helpers.lua @@ -9,22 +9,28 @@ local utils = require("yazi.utils") local YaziOpenerActions = {} ---@param config YaziConfig -function YaziOpenerActions.open_file_in_vertical_split(config) +---@param api YaziProcessApi +function YaziOpenerActions.open_file_in_vertical_split(config, api) YaziOpenerActions.select_current_file_and_close_yazi(config, { + api = api, on_file_opened = openers.open_file_in_vertical_split, }) end ---@param config YaziConfig -function YaziOpenerActions.open_file_in_horizontal_split(config) +---@param api YaziProcessApi +function YaziOpenerActions.open_file_in_horizontal_split(config, api) YaziOpenerActions.select_current_file_and_close_yazi(config, { + api = api, on_file_opened = openers.open_file_in_horizontal_split, }) end ---@param config YaziConfig -function YaziOpenerActions.open_file_in_tab(config) +---@param api YaziProcessApi +function YaziOpenerActions.open_file_in_tab(config, api) YaziOpenerActions.select_current_file_and_close_yazi(config, { + api = api, on_file_opened = openers.open_file_in_tab, }) end @@ -34,6 +40,7 @@ end -- -- ---@class (exact) YaziOpenerActionsCallbacks +---@field api YaziProcessApi ---@field on_file_opened fun(chosen_file: string, config: YaziConfig, state: YaziClosedState):nil ---@field on_multiple_files_opened? fun(chosen_files: string[], config: YaziConfig, state: YaziClosedState):nil @@ -54,11 +61,15 @@ function YaziOpenerActions.select_current_file_and_close_yazi(config, callbacks) config.hooks.yazi_opened_multiple_files = callbacks.on_multiple_files_opened - vim.api.nvim_feedkeys( - vim.api.nvim_replace_termcodes("", true, false, true), - "n", - true - ) + if config.future_features.ya_emit_open then + callbacks.api:open() + else + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes("", true, false, true), + "n", + true + ) + end end ---@param config YaziConfig diff --git a/lua/yazi/process/yazi_process_api.lua b/lua/yazi/process/yazi_process_api.lua index b360a4cc..cffe265e 100644 --- a/lua/yazi/process/yazi_process_api.lua +++ b/lua/yazi/process/yazi_process_api.lua @@ -23,6 +23,7 @@ function YaziProcessApi:cd(path) ) end +--- Tell yazi to focus (hover on) the given path. ---@see https://yazi-rs.github.io/docs/configuration/keymap#manager.reveal ---@param path string function YaziProcessApi:reveal(path) @@ -32,4 +33,10 @@ function YaziProcessApi:reveal(path) ) end +--- Tell yazi to open the currently selected file(s). +---@see https://yazi-rs.github.io/docs/configuration/keymap#manager.open +function YaziProcessApi:open() + vim.system({ "ya", "emit-to", self.yazi_id, "open" }, { timeout = 1000 }) +end + return YaziProcessApi diff --git a/lua/yazi/types.lua b/lua/yazi/types.lua index 87448528..b5ba047f 100644 --- a/lua/yazi/types.lua +++ b/lua/yazi/types.lua @@ -25,7 +25,8 @@ ---@field public future_features? yazi.OptInFeatures # Features that are not yet stable, but can be tested by the user. These features might change or be removed in the future. They may also become built-in features that are on by default, making it unnecessary to opt into using them. ---@class(exact) yazi.OptInFeatures ----@field ya_emit_reveal? boolean # Whether to use `ya emit reveal` to reveal files in the file manager. This requires https://github.com/sxyazi/yazi/pull/1979 (from 2024-12-01). +---@field ya_emit_reveal? boolean # Whether to use `ya emit reveal` to reveal files in the file manager. Requires yazi 0.4.0 or later (from 2024-12-08). +---@field ya_emit_open? boolean # Use `ya emit open` as a more robust implementation for opening files in yazi. This can prevent conflicts with custom keymappings for the enter key. Requires yazi 0.4.0 or later (from 2024-12-08). ---@alias YaziKeymap string | false # `string` is a keybinding such as "", false means the keybinding is disabled diff --git a/spec/yazi/health_spec.lua b/spec/yazi/health_spec.lua index 7acca89d..a13575dc 100644 --- a/spec/yazi/health_spec.lua +++ b/spec/yazi/health_spec.lua @@ -218,6 +218,39 @@ Options: assert_buffer_does_not_contain_text("future_features.ya_emit_reveal") end ) + + it( + "warns when yazi is < 0.4.0 and `config.future_features.ya_emit_open` is enabled", + function() + yazi.setup({ + future_features = { + ya_emit_open = true, + }, + }) + + vim.cmd("checkhealth yazi") + + assert_buffer_contains_text( + "You have enabled `future_features.ya_emit_open` in your config. This requires yazi.nvim version 0.4.0 or newer." + ) + end + ) + + it( + "does not warn when yazi is >= 0.4.0 and `config.future_features.ya_emit_open` is enabled", + function() + mock_app_versions["yazi"] = "yazi 0.4.0 (f5a7ace 2024-07-23)" + yazi.setup({ + future_features = { + ya_emit_open = true, + }, + }) + + vim.cmd("checkhealth yazi") + + assert_buffer_does_not_contain_text("future_features.ya_emit_open") + end + ) end) describe("the checks for resolve_relative_path_application", function()