local MODES = { window = true, tab = true, current = true, } local state = { mode = nil, window = nil, tabpage = nil, origin = nil, } local mode = function() return state.mode end local set = function( value ) if value == nil or value == 'off' then state.mode = nil return nil end if not MODES[value] then return state.mode end state.mode = value return state.mode end local toggle = function( value ) if state.mode then return set( nil ) end return set( value or 'window' ) end local remember_origin = function() local win = vim.api.nvim_get_current_win() state.origin = { window = win, buffer = vim.api.nvim_win_get_buf( win ), cursor = vim.api.nvim_win_get_cursor( win ), } end --- @return integer? window local ensure_window = function() if state.window and vim.api.nvim_win_is_valid( state.window ) then return state.window end local current = vim.api.nvim_get_current_win() if not pcall( vim.cmd.vsplit ) then return current end state.window = vim.api.nvim_get_current_win() vim.api.nvim_set_current_win( current ) return state.window end --- @return integer? window local ensure_tab = function() if state.tabpage and vim.api.nvim_tabpage_is_valid( state.tabpage ) then local wins = vim.api.nvim_tabpage_list_wins( state.tabpage ) if #wins > 0 then return wins[1] end end local current = vim.api.nvim_get_current_win() if not pcall( vim.cmd.tabnew ) then return current end state.tabpage = vim.api.nvim_get_current_tabpage() local win = vim.api.nvim_get_current_win() vim.api.nvim_set_current_win( current ) return win end local resolve_buffer = function( location ) if location.buffer and vim.api.nvim_buf_is_valid( location.buffer ) then return location.buffer end if not location.path or location.path == '' then return nil end return vim.fn.bufadd( location.path ) end --- Follows a located agent event. --- Following is passive: it never steals the user's working window and --- never pollutes the jumplist. local to = function( location ) if not state.mode then return nil end if type( location ) ~= 'table' then return nil end local bufnr = resolve_buffer( location ) if not bufnr then return nil end vim.fn.bufload( bufnr ) if not state.origin then remember_origin() end local target if state.mode == 'current' then target = vim.api.nvim_get_current_win() elseif state.mode == 'tab' then target = ensure_tab() else target = ensure_window() end if target == nil or not vim.api.nvim_win_is_valid( target ) then return nil end local previous = vim.api.nvim_get_current_win() vim.api.nvim_win_set_buf( target, bufnr ) local count = vim.api.nvim_buf_line_count( bufnr ) local line = math.min( math.max( 1, location.line or 1 ), count ) pcall( vim.api.nvim_win_set_cursor, target, { line, location.column or 0, } ) if state.mode ~= 'current' and vim.api.nvim_win_is_valid( previous ) then vim.api.nvim_set_current_win( previous ) end return target end --- Navigates to a location because the user asked, not because follow mode is on. --- The same surfaces are reused, so there is only ever one window policy. local visit = function( location ) local chosen = state.mode if not chosen then state.mode = 'window' end local target = to( location ) state.mode = chosen return target end --- Returns to the location held before following began. local back = function() local origin = state.origin state.origin = nil if not origin then return false end if not vim.api.nvim_win_is_valid( origin.window ) then return false end vim.api.nvim_set_current_win( origin.window ) if vim.api.nvim_buf_is_valid( origin.buffer ) then vim.api.nvim_win_set_buf( origin.window, origin.buffer ) pcall( vim.api.nvim_win_set_cursor, origin.window, origin.cursor ) end return true end local reset = function() if state.window and vim.api.nvim_win_is_valid( state.window ) then pcall( vim.api.nvim_win_close, state.window, true ) end if state.tabpage and vim.api.nvim_tabpage_is_valid( state.tabpage ) then for _, win in ipairs( vim.api.nvim_tabpage_list_wins( state.tabpage ) ) do pcall( vim.api.nvim_win_close, win, true ) end end state.mode = nil state.window = nil state.tabpage = nil state.origin = nil end local window = function() return state.window end return { modes = MODES, mode = mode, set = set, toggle = toggle, to = to, visit = visit, back = back, reset = reset, window = window, }