Skip to content
Open
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
5 changes: 5 additions & 0 deletions lua/vgit/features/screens/DiffScreen/Model.lua
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ function Model:unstage_hunk(filename, hunk)
return git_file:unstage_hunk(hunk)
end

function Model:reset_hunk(filename, hunk)
local git_file = GitFile(filename)
return git_file:reset_hunk(hunk)
end

function Model:stage_file(filename)
local reponame = git_repo.discover()
return git_stager.stage(reponame, filename)
Expand Down
68 changes: 68 additions & 0 deletions lua/vgit/features/screens/DiffScreen/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function DiffScreen:create_app_bar_view(scene, model)
return {
{ 'Stage', keymaps['buffer_stage'] },
{ 'Stage hunk', keymaps['buffer_hunk_stage'] },
{ 'Reset hunk', keymaps['buffer_hunk_reset'] },
{ 'Reset', keymaps['reset'] },
{ 'Switch to Unstage View', keymaps['toggle_view'] },
}
Expand Down Expand Up @@ -137,6 +138,10 @@ function DiffScreen:reset(buffer)
local filename = self.model:get_filename()
if not filename then return end

-- Performance: Suppress VGitSync during reset since we manually fetch below
local git_buffer_store = require('vgit.git.git_buffer_store')
git_buffer_store.suppress_sync_for(200)

loop.free_textlock()
self.model:reset_file(filename)

Expand Down Expand Up @@ -184,6 +189,11 @@ function DiffScreen:stage_hunk(buffer)
local hunk, index = self.diff_view:get_hunk_under_cursor()
if not hunk then return end

-- Performance: Suppress VGitSync during staging since we manually fetch below
-- This prevents refreshing all tracked buffers unnecessarily
local git_buffer_store = require('vgit.git.git_buffer_store')
git_buffer_store.suppress_sync_for(200)

self.model:stage_hunk(filename, hunk)

loop.free_textlock()
Expand Down Expand Up @@ -211,6 +221,10 @@ function DiffScreen:unstage_hunk(buffer)
local hunk, index = self.diff_view:get_hunk_under_cursor()
if not hunk then return end

-- Performance: Suppress VGitSync during unstaging since we manually fetch below
local git_buffer_store = require('vgit.git.git_buffer_store')
git_buffer_store.suppress_sync_for(200)

loop.free_textlock()
self.model:unstage_hunk(filename, hunk)

Expand All @@ -227,6 +241,45 @@ function DiffScreen:unstage_hunk(buffer)
self.diff_view:move_to_hunk(index, 'center')
end

function DiffScreen:reset_hunk(buffer)
if self.model:is_hunk() then return end
if self.model:is_staged() then return end

loop.free_textlock()
local filename = self.model:get_filename()
if not filename then return end

loop.free_textlock()
local hunk, index = self.diff_view:get_hunk_under_cursor()
if not hunk then return end

loop.free_textlock()
local decision = console.input(
'Are you sure you want to discard this hunk? (y/N) '
):lower()

if decision ~= 'yes' and decision ~= 'y' then return end

-- Performance: Suppress VGitSync during reset since we manually fetch below
local git_buffer_store = require('vgit.git.git_buffer_store')
git_buffer_store.suppress_sync_for(200)

loop.free_textlock()
self.model:reset_hunk(filename, hunk)

loop.free_textlock()
local _, refetch_err = self.model:fetch(buffer:get_name())
loop.free_textlock()

if refetch_err then
console.debug.error(refetch_err).error(refetch_err)
return
end

self.diff_view:render()
self.diff_view:move_to_hunk(index, 'center')
end

function DiffScreen:stage(buffer)
if self.model:is_hunk() then return end
if self.model:is_staged() then return end
Expand All @@ -235,6 +288,10 @@ function DiffScreen:stage(buffer)
local filename = self.model:get_filename()
if not filename then return end

-- Performance: Suppress VGitSync during staging since we manually fetch below
local git_buffer_store = require('vgit.git.git_buffer_store')
git_buffer_store.suppress_sync_for(200)

loop.free_textlock()
self.model:stage_file(filename)

Expand All @@ -256,6 +313,10 @@ function DiffScreen:unstage(buffer)
local filename = self.model:get_filename()
if not filename then return end

-- Performance: Suppress VGitSync during unstaging since we manually fetch below
local git_buffer_store = require('vgit.git.git_buffer_store')
git_buffer_store.suppress_sync_for(200)

loop.free_textlock()
self.model:unstage_file(filename)

Expand Down Expand Up @@ -327,6 +388,13 @@ function DiffScreen:setup_keymaps(buffer)
mapping = keymaps.buffer_hunk_unstage,
handler = handlers.hunk_unstage,
},
{
mode = 'n',
mapping = keymaps.buffer_hunk_reset,
handler = loop.debounce_coroutine(function()
self:reset_hunk(buffer)
end, 100),
},
{
mode = 'n',
mapping = {
Expand Down
4 changes: 4 additions & 0 deletions lua/vgit/git/GitFile.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ function GitFile:unstage_hunk(hunk)
return git_stager.unstage_hunk(self.reponame, self.filename, hunk)
end

function GitFile:reset_hunk(hunk)
return git_stager.reset_hunk(self.reponame, self.filename, hunk)
end

function GitFile:stage()
return git_stager.stage(self.reponame, self.filename)
end
Expand Down
78 changes: 73 additions & 5 deletions lua/vgit/git/git_buffer_store.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ local events = {
}
local is_registered = false

-- Performance optimization: control VGitSync refresh behavior during staging
local staging_state = {
active = false, -- True when selective refresh mode is active
target_buffer = nil, -- The specific buffer to refresh (nil = all buffers)
suppress_sync = false, -- True to completely suppress VGitSync events
}

local git_buffer_store = {}

git_buffer_store.register_events = loop.coroutine(function()
Expand All @@ -35,6 +42,32 @@ git_buffer_store.register_events = loop.coroutine(function()

loop.free_textlock()
local git_dirname = git_repo.dirname()

-- Register VGitSync handler once, outside the filesystem callback
-- Use vim.schedule to avoid "must not be called in a lua loop callback" error
vim.schedule(function()
event.custom_on('VGitSync', function()
-- Skip sync entirely if suppressed (e.g., during batch staging operations)
if staging_state.suppress_sync then
return
end

-- Selective refresh: only refresh specific buffer if configured
if staging_state.active and staging_state.target_buffer then
local target_buf = staging_state.target_buffer
if target_buf:is_valid() then
git_buffer_store.dispatch(target_buf, 'sync')
end
return
end

-- Default behavior: refresh all tracked buffers
git_buffer_store.for_each(function(buffer)
git_buffer_store.dispatch(buffer, 'sync')
end)
end)
end)

local ok = handle:start(
git_dirname,
{},
Expand All @@ -44,16 +77,12 @@ git_buffer_store.register_events = loop.coroutine(function()
loop.free_textlock()
local git_dir = git_repo.dirname()
loop.free_textlock()
-- Emit event to trigger the handler registered above
event.emit('VGitSync', {
git_dir = git_dir,
filename = filename,
event_name = event_name,
})
event.custom_on('VGitSync', function()
git_buffer_store.for_each(function(buffer)
git_buffer_store.dispatch(buffer, 'sync')
end)
end)
end
end, 10)
)
Expand Down Expand Up @@ -174,4 +203,43 @@ git_buffer_store.collect = function()
git_buffer_store.dispatch(git_buffer, 'attach')
end

-- Performance: API for controlling VGitSync behavior during staging operations
-- Reduces buffer refresh cascade by limiting which buffers get refreshed

-- Suppress all VGitSync events for a specified duration
-- Useful for batch operations where multiple git index changes occur
-- Usage: git_buffer_store.suppress_sync_for(500)
git_buffer_store.suppress_sync_for = function(ms)
staging_state.suppress_sync = true
vim.defer_fn(function()
staging_state.suppress_sync = false
end, ms)
end

-- Enable selective refresh mode - only refresh the specified buffer on VGitSync
-- Usage: git_buffer_store.begin_selective_staging(buffer)
git_buffer_store.begin_selective_staging = function(buffer)
staging_state.active = true
staging_state.target_buffer = buffer
end

-- Disable selective refresh mode, returning to default behavior
git_buffer_store.end_selective_staging = function()
staging_state.active = false
staging_state.target_buffer = nil
end

-- Convenience wrapper: execute staging function with selective refresh
-- Automatically enables selective mode, runs function, then restores after delay
-- Usage: git_buffer_store.with_selective_staging(buffer, function() ... end)
git_buffer_store.with_selective_staging = function(buffer, stage_fn)
git_buffer_store.begin_selective_staging(buffer)
local ok, err = pcall(stage_fn)
-- Keep selective mode active briefly to catch the filesystem watcher event
vim.defer_fn(function()
git_buffer_store.end_selective_staging()
end, 150) -- Filesystem watcher has 10ms debounce, add margin
if not ok then error(err) end
end

return git_buffer_store
28 changes: 28 additions & 0 deletions lua/vgit/git/git_stager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,32 @@ function git_stager.unstage_hunk(reponame, filename, hunk)
return nil, err
end

-- Reset (discard) a hunk in the working directory
function git_stager.reset_hunk(reponame, filename, hunk)
if not reponame then return nil, { 'reponame is required' } end
if not filename then return nil, { 'filename is required' } end
if not hunk then return nil, { 'hunk is required' } end

local patch = GitPatch(filename, hunk)
local patch_filename = fs.tmpname()

fs.write_file(patch_filename, patch)

-- Apply the patch in reverse to the working directory (not staged)
local _, err = gitcli.run({
'-C',
reponame,
'--no-pager',
'apply',
'--reverse',
'--whitespace=nowarn',
'--unidiff-zero',
patch_filename,
})

fs.remove_file(patch_filename)

return nil, err
end

return git_stager
6 changes: 5 additions & 1 deletion lua/vgit/settings/diff_preview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ return Config({
desc = 'Unstage'
},
reset = {
key = 'r',
key = 'R',
desc = 'Reset'
},
buffer_hunk_stage = {
Expand All @@ -22,6 +22,10 @@ return Config({
key = 'u',
desc = 'Unstage hunk'
},
buffer_hunk_reset = {
key = 'r',
desc = 'Reset hunk'
},
toggle_view = 't',
},
})
Loading