Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to sync locations after making changes in the results area #16

Merged
merged 4 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Grug find! Grug replace! Grug happy!
- Replace using almost the **full power** of `rg`. Some flags such as `--binary` and `--json`, etc. are [blacklisted][blacklistedReplaceFlags] in order to prevent unexpected output. The UI will warn you and prevent replace when using such flags.
- Open search results in quickfix list
- Goto file/line/column of match when pressing `<Enter>` in normal mode on lines in the results output (keybind configurable).
- Inline edit matched result lines and sync them their originating file locations using a configurable keybinding.

#### Searching:
<img width="100%" alt="image" src="https://github.com/MagicDuck/grug-far.nvim/assets/95201/b664f77c-6e12-4a4a-a179-ada2da204039">
Expand Down Expand Up @@ -73,9 +74,14 @@ Ultimately it leaves the power in your hands, and in any case recovery is just a

Search and replace to your heart's desire. You can create multiple such buffers with potentially
different searches, which will reflect in each buffer's title (configurable). The buffers should
be visible in the buffers list if you need to toggle to them. When you are done, it is recommended
to close the buffer with the configured keybinding (see Configuration section above) or just `:bd`
in order to save on resources as some search results can be quite beefy in size.
be visible in the buffers list if you need to toggle to them.

It is also possible to make edits to lines in the results section and have them synced to their
originating file lines. Simply make your changes on multiple lines and press `<C-i>` (by default).

When you are done, it is recommended to close the buffer with the configured keybinding
(see Configuration section above) or just `:bd` in order to save on resources as some search results
can be quite beefy in size.

Note that *grug-far.nvim* buffers will have `filetype=grug-far` if you need filter/exclude them in
any situations.
Expand Down
2 changes: 1 addition & 1 deletion lua/grug-far/actions/replace.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ local function getActionMessage(err, count, total, time)
return msg .. 'failed!'
end

if count == total and total ~= 0 then
if count == total and total ~= 0 and time then
return msg .. 'completed in ' .. time .. 'ms!'
end

Expand Down
192 changes: 192 additions & 0 deletions lua/grug-far/actions/syncLocations.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
local renderResultsHeader = require('grug-far/render/resultsHeader')
local resultsList = require('grug-far/render/resultsList')
local uv = vim.loop

local function writeChangedLine(params)
local changedLine = params.changedLine
local on_done = params.on_done
local file = changedLine.location.filename
local lnum = changedLine.location.lnum
local newLine = changedLine.newLine

local file_handle = io.open(file, 'r')
if not file_handle then
on_done('Could not open file: ' .. file)
return
end

local contents = file_handle:read("*a")
file_handle:close()
if not contents then
on_done('Cound not read file: ' .. file)
return
end

local lines = vim.split(contents, "\n")
if not lines[lnum] then
on_done('File does not have edited row anymore: ' .. file)
return
end

lines[lnum] = newLine

file_handle = io.open(file, 'w+')
if not file_handle then
on_done('Could not open file: ' .. file)
return
end

local h = file_handle:write(vim.fn.join(lines, "\n"))
if not h then
on_done('Cound not write to file: ' .. file)
return
end

file_handle:flush()
file_handle:close()

on_done(nil)
end

local function syncChangedLines(params)
local context = params.context
local changedLines = vim.deepcopy(params.changedLines)
local reportProgress = params.reportProgress
local on_finish = params.on_finish
local engagedWorkers = 0
local errorMessages = ''

local function syncNextChangedLine()
local changedLine = table.remove(changedLines)
if changedLine == nil then
if engagedWorkers == 0 then
on_finish(#errorMessages > 0 and 'error' or 'success', errorMessages)
end
return
end

engagedWorkers = engagedWorkers + 1
writeChangedLine({
changedLine = changedLine,
on_done = vim.schedule_wrap(function(err)
if err then
-- optimistically try to continue
errorMessages = errorMessages .. '\n' .. err
end

if reportProgress then
reportProgress()
end
engagedWorkers = engagedWorkers - 1
syncNextChangedLine()
end)
})
end

for _ = 1, context.options.maxWorkers do
syncNextChangedLine()
end
end

local function getActionMessage(err, count, total, time)
local msg = 'sync '
if err then
return msg .. 'failed!'
end

if count == total and total ~= 0 and time then
return msg .. 'completed in ' .. time .. 'ms!'
end

return msg .. count .. ' / ' .. total .. ' (buffer temporarily not modifiable)'
end

local function syncLocations(params)
local buf = params.buf
local context = params.context
local state = context.state

local extmarks = vim.api.nvim_buf_get_extmarks(0, context.locationsNamespace, 0, -1, {})
local changedLines = {}
for i = 1, #extmarks do
local markId, row = unpack(extmarks[i])
local location = context.state.resultLocationByExtmarkId[markId]

if location and location.rgResultLine then
local bufline = unpack(vim.api.nvim_buf_get_lines(buf, row, row + 1, true))
if bufline ~= location.rgResultLine then
local numColPrefix = string.sub(location.rgResultLine, 1, location.rgColEndIndex + 1)
if vim.startswith(bufline, numColPrefix) then
table.insert(changedLines, {
location = location,
-- note, skips (:)
newLine = string.sub(bufline, location.rgColEndIndex + 2, -1)
})
end
end
end
end

if #changedLines == 0 then
return
end

local changesCount = 0
local changesTotal = #changedLines
local startTime = uv.now()

-- initiate sync in UI
vim.schedule(function()
vim.api.nvim_buf_set_option(buf, 'modifiable', false)
state.status = 'progress'
state.progressCount = 0
state.actionMessage = getActionMessage(nil, changesCount, changesTotal)
renderResultsHeader(buf, context)
end)

local reportSyncedFilesUpdate = vim.schedule_wrap(function()
state.status = 'progress'
state.progressCount = state.progressCount + 1
changesCount = changesCount + 1
state.actionMessage = getActionMessage(nil, changesCount, changesTotal)
renderResultsHeader(buf, context)
end)

local reportError = function(errorMessage)
vim.api.nvim_buf_set_option(buf, 'modifiable', true)

state.status = 'error'
state.actionMessage = getActionMessage(errorMessage)
resultsList.setError(buf, context, errorMessage)
renderResultsHeader(buf, context)

vim.notify('grug-far: ' .. state.actionMessage, vim.log.levels.ERROR)
end

local on_finish_all = vim.schedule_wrap(function(status, errorMessage, customActionMessage)
vim.api.nvim_buf_set_option(buf, 'modifiable', true)

if status == 'error' then
reportError(errorMessage)
return
end

state.status = status
local time = uv.now() - startTime
-- not passing in total as 3rd arg cause of paranoia if counts don't end up matching
state.actionMessage = status == nil and customActionMessage or
getActionMessage(nil, changesCount, changesCount, time)
renderResultsHeader(buf, context)

vim.notify('grug-far: synced changes!', vim.log.levels.INFO)
end)

syncChangedLines({
context = context,
changedLines = changedLines,
reportProgress = reportSyncedFilesUpdate,
on_finish = on_finish_all
})
end

return syncLocations
7 changes: 7 additions & 0 deletions lua/grug-far/farBuffer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local search = require('grug-far/actions/search')
local replace = require("grug-far/actions/replace")
local qflist = require("grug-far/actions/qflist")
local gotoLocation = require("grug-far/actions/gotoLocation")
local syncLocations = require("grug-far/actions/syncLocations")
local close = require("grug-far/actions/close")
local utils = require('grug-far/utils')

Expand All @@ -19,6 +20,7 @@ end
local function setupKeymap(buf, context)
local keymaps = context.options.keymaps
if #keymaps.replace > 0 then
-- TODO (sbadragan): should modes be configurable?
setBufKeymap(buf, 'ni', 'Grug Far: apply replacements', keymaps.replace, function()
replace({ buf = buf, context = context })
end)
Expand All @@ -33,6 +35,11 @@ local function setupKeymap(buf, context)
gotoLocation({ buf = buf, context = context })
end)
end
if #keymaps.syncLocations > 0 then
setBufKeymap(buf, 'ni', 'Grug Far: sync edited results text to locations', keymaps.syncLocations, function()
syncLocations({ buf = buf, context = context })
end)
end
if #keymaps.close > 0 then
setBufKeymap(buf, 'niv', 'Grug Far: close', keymaps.close, function()
close()
Expand Down
1 change: 1 addition & 0 deletions lua/grug-far/opts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ M.defaultOptions = {
replace = '<C-enter>',
qflist = '<C-q>',
gotoLocation = '<enter>',
syncLocations = '<C-i>',
close = '<C-x>'
},

Expand Down
1 change: 1 addition & 0 deletions lua/grug-far/render/help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ local function renderHelp(params, context)
printMapping('Replace: ', keymaps.replace),
printMapping('Quickfix List: ', keymaps.qflist),
printMapping('Goto Location: ', keymaps.gotoLocation),
printMapping('Sync Edited Locations: ', keymaps.syncLocations),
printMapping('Close: ', keymaps.close),
}), ' | '),

Expand Down
2 changes: 2 additions & 0 deletions lua/grug-far/render/resultsList.lua
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ function M.appendResultsChunk(buf, context, data)
elseif hl == 'GrugFarResultsLineColumn' and lastLocation and not lastLocation.col then
-- omit ending ':', use first match on that line
lastLocation.col = tonumber(string.sub(line, highlight.start_col + 1, highlight.end_col))
lastLocation.rgResultLine = line
lastLocation.rgColEndIndex = highlight.end_col
end
end
end
Expand Down
Loading