diff --git a/.gitignore b/.gitignore index be71cf3..3701cde 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ integration-tests/pid.txt integration-tests/server/build *.pem integration-tests/test-environment/testdirs +integration-tests/cypress/screenshots diff --git a/README.md b/README.md index eabf465..27048b3 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,8 @@ These are the default keybindings that are available when yazi is open: if available. - ``: search and replace in the current yazi directory using [grug-far](https://github.com/MagicDuck/grug-far.nvim), if available + - if multiple files/directories are selected in yazi, the search and replace + will only be done in the selected files/directories ## 🪛 Customizing yazi diff --git a/integration-tests/client/testEnvironmentTypes.ts b/integration-tests/client/testEnvironmentTypes.ts index 565d3b8..e3d5cc8 100644 --- a/integration-tests/client/testEnvironmentTypes.ts +++ b/integration-tests/client/testEnvironmentTypes.ts @@ -1,12 +1,12 @@ export type MultipleFiles = { - openInVerticalSplits: File[] + openInVerticalSplits: IntegrationTestFile[] } -type File = TestDirectoryFile | "." +export type IntegrationTestFile = TestDirectoryFile | "." /** The arguments given from the tests to send to the server */ export type StartNeovimArguments = { - filename?: File | MultipleFiles + filename?: IntegrationTestFile | MultipleFiles startupScriptModifications?: StartupScriptModification[] } @@ -73,6 +73,7 @@ export type TestDirectory = { ["other-subdirectory/other-sub-file.txt"]: FileEntry ["routes/posts.$postId/route.tsx"]: FileEntry ["routes/posts.$postId/adjacent-file.txt"]: FileEntry + ["routes/posts.$postId/should-be-excluded-file.txt"]: FileEntry } } diff --git a/integration-tests/cypress.config.ts b/integration-tests/cypress.config.ts index 045c5d7..c252216 100644 --- a/integration-tests/cypress.config.ts +++ b/integration-tests/cypress.config.ts @@ -113,6 +113,11 @@ export default defineConfig({ stem: "route", extension: ".tsx", }, + "routes/posts.$postId/should-be-excluded-file.txt": { + name: "should-be-excluded-file.txt", + stem: "should-be-excluded-file", + extension: ".txt", + }, }, } directory satisfies Serializable // required by cypress diff --git a/integration-tests/cypress/e2e/using-ya-to-read-events/integrations.cy.ts b/integration-tests/cypress/e2e/using-ya-to-read-events/integrations.cy.ts index bf223ef..10de678 100644 --- a/integration-tests/cypress/e2e/using-ya-to-read-events/integrations.cy.ts +++ b/integration-tests/cypress/e2e/using-ya-to-read-events/integrations.cy.ts @@ -1,7 +1,8 @@ import path = require("path") +import type { IntegrationTestFile } from "../../../client/testEnvironmentTypes" import { startNeovimWithYa } from "./startNeovimWithYa" -describe("integrations to other tools", () => { +describe("grug-far integration (search and replace)", () => { beforeEach(() => { cy.visit("http://localhost:5173") }) @@ -12,6 +13,7 @@ describe("integrations to other tools", () => { cy.contains("If you see this text, Neovim is ready!") cy.typeIntoTerminal("{upArrow}") cy.typeIntoTerminal("/routes{enter}") + cy.contains("posts.$postId") // contents of the directory should be visible cy.typeIntoTerminal("{rightArrow}") // contents in the directory should be visible in yazi @@ -35,4 +37,37 @@ describe("integrations to other tools", () => { // works locally }) }) + + it("can search and replace, limited to selected files only", () => { + startNeovimWithYa({ + filename: "routes/posts.$postId/adjacent-file.txt", + }).then((dir) => { + cy.typeIntoTerminal("{upArrow}") + cy.contains(dir.contents["routes/posts.$postId/route.tsx"].name) + + // select the current file and the file below. There are three files in + // this directory so two will be selected and one will be left + // unselected + cy.typeIntoTerminal("vj") + cy.typeIntoTerminal("{control+g}") + + cy.typeIntoTerminal("ithis") + cy.typeIntoTerminal("{esc}") + + // close the split on the right so we can get some more space + cy.typeIntoTerminal(":only{enter}") + + // the selected files should be visible in the view, used as the files to + // whitelist into the search and replace operation + type File = IntegrationTestFile + cy.contains("routes/posts.$postId/adjacent-file.txt" satisfies File) + cy.contains("routes/posts.$postId/route.tsx" satisfies File) + + // the files in the same directory that were not selected should not be + // visible in the view + cy.contains( + "routes/posts.$postId/should-be-excluded-file.txt" satisfies File, + ).should("not.exist") + }) + }) }) diff --git a/integration-tests/test-environment/routes/posts.$postId/should-be-excluded-file.txt b/integration-tests/test-environment/routes/posts.$postId/should-be-excluded-file.txt new file mode 100644 index 0000000..fbe3972 --- /dev/null +++ b/integration-tests/test-environment/routes/posts.$postId/should-be-excluded-file.txt @@ -0,0 +1 @@ +this file should be excluded diff --git a/lua/yazi/config.lua b/lua/yazi/config.lua index 13fb6e7..d268525 100644 --- a/lua/yazi/config.lua +++ b/lua/yazi/config.lua @@ -53,6 +53,19 @@ function M.default() }, }) end, + replace_in_selected_files = function(selected_files) + ---@type string[] + local files = {} + for _, path in ipairs(selected_files) do + files[#files + 1] = path:make_relative(vim.uv.cwd()):gsub(' ', '\\ ') + end + + require('grug-far').grug_far({ + prefills = { + paths = table.concat(files, ' '), + }, + }) + end, }, floating_window_scaling_factor = 0.9, @@ -131,23 +144,49 @@ function M.set_keymappings(yazi_buffer, config, context) end if config.keymaps.replace_in_directory ~= false then + if config.integrations.replace_in_directory == nil then + return + end + vim.keymap.set({ 't' }, config.keymaps.replace_in_directory, function() keybinding_helpers.select_current_file_and_close_yazi(config, { on_file_opened = function(_, _, state) - if config.integrations.replace_in_directory == nil then - return - end - + -- search and replace in the directory local success, result_or_error = pcall( config.integrations.replace_in_directory, state.last_directory ) if not success then - local message = 'yazi.nvim: error replacing with grug-far.nvim.' - vim.notify(message, vim.log.levels.WARN) + local detail = vim.inspect({ + message = 'yazi.nvim: error replacing with grug-far.nvim.', + error = result_or_error, + }) + vim.notify(detail, vim.log.levels.WARN) require('yazi.log'):debug( - vim.inspect({ message = message, error = result_or_error }) + vim.inspect({ message = detail, error = result_or_error }) + ) + end + end, + on_multiple_files_opened = function(chosen_files) + -- limit the replace operation to the selected files only + local plenary_path = require('plenary.path') + local paths = {} + for _, path in ipairs(chosen_files) do + table.insert(paths, plenary_path:new(path)) + end + + local success, result_or_error = + pcall(config.integrations.replace_in_selected_files, paths) + + if not success then + local detail = vim.inspect({ + message = 'yazi.nvim: error replacing with grug-far.nvim.', + error = result_or_error, + }) + vim.notify(detail, vim.log.levels.WARN) + require('yazi.log'):debug( + vim.inspect({ message = detail, error = result_or_error }) ) end end, @@ -166,7 +205,7 @@ function M.set_keymappings(yazi_buffer, config, context) relative = 'win', bufpos = { 5, 30 }, noautocmd = true, - width = math.min(40, math.floor(w * 0.5)), + width = math.min(46, math.floor(w * 0.5)), height = math.min(11, math.floor(h * 0.5)), border = config.yazi_floating_window_border, }) @@ -184,7 +223,9 @@ function M.set_keymappings(yazi_buffer, config, context) .. config.keymaps.open_file_in_vertical_split .. ' - open file in vertical split', '' .. config.keymaps.grep_in_directory .. ' - search in directory', - '' .. config.keymaps.replace_in_directory .. ' - replace in directory', + '' + .. config.keymaps.replace_in_directory + .. ' - replace in directory / selected files', '' .. config.keymaps.cycle_open_buffers .. ' - cycle open buffers', '' .. config.keymaps.show_help .. ' - show this help', '', diff --git a/lua/yazi/types.lua b/lua/yazi/types.lua index 8420a69..ef51752 100644 --- a/lua/yazi/types.lua +++ b/lua/yazi/types.lua @@ -43,7 +43,8 @@ ---@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" ----@field public replace_in_directory? fun(directory: Path): nil "called to start a replacement operation on some directory; by default grug-far.nvim" +---@field public replace_in_directory? fun(directory: Path, selected_files?: Path[]): nil "called to start a replacement operation on some directory; by default uses grug-far.nvim" +---@field public replace_in_selected_files? fun(selected_files?: Path[]): nil "called to start a replacement operation on files that were selected in yazi; by default uses grug-far.nvim" ---@class (exact) YaziConfigHighlightGroups # Defines the highlight groups that will be used in yazi ---@field public hovered_buffer? vim.api.keyset.highlight # the color of a buffer that is hovered over in yazi