Skip to content

Commit

Permalink
feat: add optional search and replace integration (grug-far.nvim) (#250)
Browse files Browse the repository at this point in the history
* feat: add optional search and replace integration (grug-far.nvim)

This commit adds an integration with grug-far.nvim, a plugin that
provides a way to search and replace in the current working directory.
It was recently added in LazyVim as the default, and it's really easy to
use.

When you press the keybinding (default: `<c-g>`), yazi will close and
grug-far.nvim will open with the current directory prefilled in the
search field. This makes it easy to search and replace in that directory
only. Note that this only seems to work when the target directory is
under cwd, so it's not possible to search in a parent directory.

The integration is added as a keymap in the config. The keymap is set to
`<c-g>` by default, but it can be changed in the config.

To configure it, set one of these in your yazi.nvim config:

```lua
-- to disable using the keymap entirely
{
  keymaps = {
    replace_in_directory = false
  }
}

-- to change the keybinding to something else
{
  keymaps = {
    replace_in_directory = "<c-o>"
  }
}

-- advanced: to create a custom integration for yourself
{
  integrations = {
    replace_in_directory = function (directory)
      -- your custom implementation
    end
  },
}
```

<https://github.com/MagicDuck/grug-far.nvim>

* fixup! feat: add optional search and replace integration (grug-far.nvim)

* fixup! feat: add optional search and replace integration (grug-far.nvim)

* fixup! feat: add optional search and replace integration (grug-far.nvim)
  • Loading branch information
mikavilpas authored Jul 24, 2024
1 parent 4329a2c commit b512d38
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ You can optionally configure yazi.nvim by setting any of the options below.
open_file_in_horizontal_split = '<c-x>',
open_file_in_tab = '<c-t>',
grep_in_directory = '<c-s>',
replace_in_directory = '<c-g>',
cycle_open_buffers = '<tab>',
},

Expand Down
8 changes: 6 additions & 2 deletions integration-tests/client/testEnvironmentTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ export type FileEntry = {
* actually found. Otherwise the tests are brittle and can break easily.
*/
export type TestDirectory = {
/** The path to the unique test directory itself (the root). */
rootPath: string
/** The path to the unique test directory (the root). */
rootPathAbsolute: string

/** The path to the unique test directory, relative to the root of the
* test-environment directory. */
rootPathRelativeToTestEnvironmentDir: string

contents: {
["initial-file.txt"]: FileEntry
Expand Down
12 changes: 9 additions & 3 deletions integration-tests/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { constants } from "fs"
import { access, mkdir, mkdtemp, readdir, readFile, rm } from "fs/promises"
import path from "path"
import { fileURLToPath } from "url"
import type { TestDirectory } from "./cypress/support/commands"
import type { TestDirectory } from "./client/testEnvironmentTypes"

const __dirname = fileURLToPath(new URL(".", import.meta.resolve(".")))

Expand All @@ -21,12 +21,14 @@ const yaziLogFile = path.join(

console.log(`yaziLogFile: ${yaziLogFile}`)

const testEnvironmentDir = path.join(__dirname, "test-environment")
const testdirs = path.join(testEnvironmentDir, "testdirs")

export default defineConfig({
e2e: {
setupNodeEvents(on, _config) {
on("after:browser:launch", async (): Promise<void> => {
// delete everything under the ./test-environment/testdirs/ directory
const testdirs = path.join(__dirname, "test-environment", "testdirs")
await mkdir(testdirs, { recursive: true })
const files = await readdir(testdirs)

Expand Down Expand Up @@ -65,7 +67,11 @@ export default defineConfig({
const dir = await createUniqueDirectory()

const directory: TestDirectory = {
rootPath: dir,
rootPathAbsolute: dir,
rootPathRelativeToTestEnvironmentDir: path.relative(
testEnvironmentDir,
dir,
),
contents: {
"initial-file.txt": {
name: "initial-file.txt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,12 @@ describe("highlighting the buffer with 'hover' events", () => {

const testFile = dir.contents["test.lua"].name
// open an adjacent file and wait for it to be displayed
cy.typeIntoTerminal(`:vsplit ${dir.rootPath}/${testFile}{enter}`, {
delay: 1,
})
cy.typeIntoTerminal(
`:vsplit ${dir.rootPathAbsolute}/${testFile}{enter}`,
{
delay: 1,
},
)
cy.contains("how to initialize the test environment")

// start yazi - the initial file should be highlighted
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import path = require("path")
import { startNeovimWithYa } from "./startNeovimWithYa"

describe("integrations to other tools", () => {
beforeEach(() => {
cy.visit("http://localhost:5173")
})

it("can use grug-far.nvim to search and replace in the cwd", () => {
startNeovimWithYa().then((dir) => {
// wait until text on the start screen is visible
cy.contains("If you see this text, Neovim is ready!")
cy.typeIntoTerminal("{upArrow}")
cy.typeIntoTerminal("/routes{enter}")
cy.typeIntoTerminal("{rightArrow}")

// contents in the directory should be visible in yazi
cy.contains(dir.contents["routes/posts.$postId/adjacent-file.txt"].name)

// close yazi and start grug-far.nvim
cy.typeIntoTerminal("{control+g}")
cy.contains("Grug FAR")

// the directory we were in should be prefilled in grug-far.nvim's view
cy.contains("testdirs")
const p = path.join(
dir.rootPathRelativeToTestEnvironmentDir,
"routes",
"**",
)
cy.contains(p)
})
})
})
2 changes: 1 addition & 1 deletion integration-tests/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Cypress.Commands.add("startNeovim", (startArguments?: StartNeovimArguments) => {
cy.window().then((win) => {
// eslint-disable-next-line @typescript-eslint/require-await
cy.task("createTempDir").then(async (dir) => {
void win.startNeovim(dir.rootPath, startArguments)
void win.startNeovim(dir.rootPathAbsolute, startArguments)
return dir
})
})
Expand Down
1 change: 1 addition & 0 deletions integration-tests/test-environment/test-setup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ local plugins = {
},
{ 'nvim-telescope/telescope.nvim', lazy = true },
{ 'catppuccin/nvim', name = 'catppuccin', priority = 1000 },
{ 'https://github.com/MagicDuck/grug-far.nvim', opts = {} },
}
require('lazy').setup({ spec = plugins })

Expand Down
35 changes: 35 additions & 0 deletions lua/yazi/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function M.default()
open_file_in_horizontal_split = '<c-x>',
open_file_in_tab = '<c-t>',
grep_in_directory = '<c-s>',
replace_in_directory = '<c-g>',
cycle_open_buffers = '<tab>',
},
set_keymappings_function = nil,
Expand All @@ -41,6 +42,15 @@ function M.default()
cwd = directory,
})
end,
replace_in_directory = function(directory)
-- limit the search to the given path, based on cwd
local filter = directory:joinpath('**'):make_relative(vim.uv.cwd())
require('grug-far').grug_far({
prefills = {
filesFilter = filter,
},
})
end,
},

floating_window_scaling_factor = 0.9,
Expand Down Expand Up @@ -115,6 +125,31 @@ function M.set_keymappings(yazi_buffer, config, context)
keybinding_helpers.cycle_open_buffers(context)
end, { buffer = yazi_buffer })
end

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, {
on_file_opened = function(_, _, state)
if config.integrations.replace_in_directory == nil then
return
end

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)
require('yazi.log'):debug(
vim.inspect({ message = message, error = result_or_error })
)
end
end,
})
end, { buffer = yazi_buffer })
end
end

---@param yazi_buffer integer
Expand Down
2 changes: 2 additions & 0 deletions lua/yazi/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
---@field open_file_in_horizontal_split? YaziKeymap # When a file is hovered, open it in a horizontal split
---@field open_file_in_tab? YaziKeymap # When a file is hovered, open it in a new tab
---@field grep_in_directory? YaziKeymap # Close yazi and open a grep (default: telescope) narrowed to the directory yazi is in
---@field replace_in_directory? YaziKeymap # Close yazi and open a replacer (default: grug-far.nvim) narrowed to the directory yazi is in
---@field cycle_open_buffers? YaziKeymap # When Neovim has multiple splits open and visible, make yazi jump to the directory of the next one

---@class (exact) YaziActiveContext # context state for a single yazi session
Expand All @@ -41,6 +42,7 @@

---@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"

---@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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit b512d38

Please sign in to comment.