Skip to content

Commit

Permalink
feat: can limit search and replace to selected files only (#277)
Browse files Browse the repository at this point in the history
* feat: can limit search and replace to selected files only

Previously the search and replace operation would be applied to all
files in the directory where the operation was started. This commit adds
a feature to limit the search and replace operation to only the files
that were visually selected (with `v` or `<space>` by default in yazi)
in the yazi view.

This allows fine tuning of the search and replace operation to only the
files that are relevant to the user.

* fixup! feat: can limit search and replace to selected files only
  • Loading branch information
mikavilpas authored Jul 27, 2024
1 parent 40d38ea commit 5a12444
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 14 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ integration-tests/pid.txt
integration-tests/server/build
*.pem
integration-tests/test-environment/testdirs
integration-tests/cypress/screenshots
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ These are the default keybindings that are available when yazi is open:
if available.
- `<c-g>`: 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

Expand Down
7 changes: 4 additions & 3 deletions integration-tests/client/testEnvironmentTypes.ts
Original file line number Diff line number Diff line change
@@ -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[]
}

Expand Down Expand Up @@ -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
}
}

Expand Down
5 changes: 5 additions & 0 deletions integration-tests/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
})
Expand All @@ -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
Expand All @@ -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")
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this file should be excluded
59 changes: 50 additions & 9 deletions lua/yazi/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
})
Expand All @@ -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',
'',
Expand Down
3 changes: 2 additions & 1 deletion lua/yazi/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 5a12444

Please sign in to comment.