local utils = require('fff.utils') local file_picker = require('fff.file_picker') local image = require('fff.file_picker.image') local location_utils = require('fff.location_utils') local rust = require('fff.rust') local file_info_renderer = require('fff.file_picker.file_info') local M = {} -- Namespace dedicated to the file info panel highlights. M.file_info_ns = vim.api.nvim_create_namespace('fff_file_info') local function set_buffer_lines(bufnr, lines) if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) end local function append_buffer_lines(bufnr, lines) if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end if not lines or #lines == 0 then return end vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr }) local current_lines = vim.api.nvim_buf_line_count(bufnr) vim.api.nvim_buf_set_lines(bufnr, current_lines, current_lines, false, lines) vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) end local function show_preview_unavailable(bufnr) set_buffer_lines(bufnr, { 'No preview available' }) end local function set_preview_filetype(bufnr, filetype) -- FileType hooks can throw or mutate global v:errmsg during preview setup. -- pcall lets us restore it; callers treat failures as preview-local. local errmsg = vim.v.errmsg local ok, err = pcall(vim.api.nvim_set_option_value, 'filetype', filetype, { buf = bufnr }) vim.v.errmsg = errmsg return ok, err end local function find_existing_buffer(file_path) local abs_path = vim.fn.resolve(vim.fn.fnamemodify(file_path, ':p')) for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_is_loaded(bufnr) then local buf_name = vim.api.nvim_buf_get_name(bufnr) if buf_name ~= '' then local buf_path = vim.fn.resolve(vim.fn.fnamemodify(buf_name, ':p')) if buf_path == abs_path then return bufnr end end end end return nil end local function cleanup_file_operation() if M.state.file_operation then if M.state.file_operation.fd then pcall(vim.uv.fs_close, M.state.file_operation.fd) end M.state.file_operation = nil end end --- Process raw chunk data into complete lines, joining any leftover bytes --- from the previous chunk and storing any trailing partial line for the next. --- @param data string Raw chunk data --- @return string[] Complete lines (may be empty if the entire chunk is a partial line) local function split_chunk_with_remainder(data) if not data or data == '' then return {} end local fo = M.state.file_operation local prefix = fo and fo.remainder or '' local combined = prefix .. data local lines = vim.split(combined, '\n', { plain = true }) if combined:sub(-1) ~= '\n' then -- Data doesn't end on a line boundary: last element is a partial line local partial = table.remove(lines) or '' if fo then fo.remainder = partial end else -- Data ends on a line boundary: remove the trailing empty element if #lines > 0 and lines[#lines] == '' then table.remove(lines) end if fo then fo.remainder = '' end end return lines end local function init_dynamic_loading_async(file_path, callback) cleanup_file_operation() M.state.loaded_lines = 0 M.state.total_file_lines = nil M.state.has_more_content = true M.state.is_loading = false local generation = M.state.preview_generation vim.uv.fs_open(file_path, 'r', 438, function(err, fd) vim.schedule(function() -- Stale callback: preview moved on to a different file if M.state.preview_generation ~= generation then if fd then pcall(vim.uv.fs_close, fd) end return end if err or not fd then callback(false, 'Failed to open file: ' .. (err or 'unknown error')) return end M.state.file_operation = { fd = fd, file_path = file_path, position = 0, remainder = '', } callback(true) end) end) end local function load_forward_chunk_async(target_size, callback) if not M.state.file_operation or not M.state.file_operation.fd then callback('', 'No file handle available') return end M.state.is_loading = true local chunk_size = target_size or (M.config.chunk_size or 16384) local generation = M.state.preview_generation vim.uv.fs_read(M.state.file_operation.fd, chunk_size, M.state.file_operation.position, function(err, data) vim.schedule(function() -- Stale callback: a newer preview has started, discard this result if M.state.preview_generation ~= generation then return end M.state.is_loading = false if err then callback('', 'Read error: ' .. err) return end if not data or #data == 0 then M.state.has_more_content = false -- Flush any remaining partial line as the final piece of data local final_remainder = M.state.file_operation and M.state.file_operation.remainder or '' cleanup_file_operation() if final_remainder ~= '' then callback(final_remainder .. '\n', nil) else callback('', nil) end return end if M.state.file_operation then M.state.file_operation.position = M.state.file_operation.position + #data end callback(data, nil) end) end) end local function load_next_chunk_async(chunk_size, callback) if not M.state.file_operation or not M.state.has_more_content or M.state.is_loading then callback('', nil) return end load_forward_chunk_async(chunk_size, callback) end -- Forward declaration for ensure_content_loaded_async (used in read_file_streaming_async callback) local ensure_content_loaded_async local function read_file_streaming_async(file_path, callback) local generation = M.state.preview_generation init_dynamic_loading_async(file_path, function(success, error_msg) if M.state.preview_generation ~= generation then return end if not success then callback(nil, error_msg) return end -- Calculate initial chunk size based on location information local initial_chunk_size = M.config.chunk_size if M.state.location then local target_line = location_utils.get_target_line(M.state.location) if target_line then -- Estimate bytes needed: assume ~100 bytes per line average -- Add some buffer (50%) to account for variation in line lengths local estimated_bytes = target_line * 100 * 1.5 -- Cap at reasonable maximum to avoid memory issues local max_initial_chunk = M.config.max_size or (10 * 1024 * 1024) -- 10MB default initial_chunk_size = math.min(estimated_bytes, max_initial_chunk) -- Ensure we don't go below the standard chunk size initial_chunk_size = math.max(initial_chunk_size, M.config.chunk_size) end end load_next_chunk_async(initial_chunk_size, function(data, err) if M.state.preview_generation ~= generation then return end if data and data ~= '' then local lines = split_chunk_with_remainder(data) M.state.loaded_lines = #lines M.state.content_height = #lines -- If we have a location and didn't load enough lines, try to load more local loading_more = false if M.state.location then local target_line = location_utils.get_target_line(M.state.location) if target_line and #lines < target_line and M.state.has_more_content then loading_more = true vim.schedule(function() if M.state.preview_generation == generation then ensure_content_loaded_async(target_line) end end) end end callback(lines, err, loading_more) else callback(nil, err) end end) end) end ensure_content_loaded_async = function(target_line) if not M.state.bufnr or not vim.api.nvim_buf_is_valid(M.state.bufnr) then return end if not M.state.has_more_content or M.state.is_loading then return end -- Guard against missing file handle: without it load_next_chunk_async returns -- synchronously with empty data, which triggers apply_location_highlighting -- -> ensure_content_loaded_async again, causing infinite recursion (stack overflow). if not M.state.file_operation then M.state.has_more_content = false return end local current_buffer_lines = vim.api.nvim_buf_line_count(M.state.bufnr) local buffer_needed = target_line + 50 if current_buffer_lines >= buffer_needed then return end local generation = M.state.preview_generation -- Use a larger chunk to reach the target faster instead of many small 8KB reads local lines_needed = buffer_needed - current_buffer_lines local estimated_bytes = math.max(M.config.chunk_size, lines_needed * 120) load_next_chunk_async(estimated_bytes, function(data, err) -- Stale callback: preview moved on to a different file if M.state.preview_generation ~= generation then return end if not M.state.bufnr or not vim.api.nvim_buf_is_valid(M.state.bufnr) then return end if err then return end if data and data ~= '' then local chunk_lines = split_chunk_with_remainder(data) if #chunk_lines > 0 then append_buffer_lines(M.state.bufnr, chunk_lines) end M.state.content_height = vim.api.nvim_buf_line_count(M.state.bufnr) M.state.loaded_lines = M.state.content_height -- If we still haven't loaded enough, schedule another chunk if M.state.loaded_lines < buffer_needed and M.state.has_more_content then vim.schedule(function() if M.state.preview_generation == generation then ensure_content_loaded_async(target_line) end end) else -- Enough content loaded — re-apply location highlighting so the -- preview scrolls to the correct line now that it exists in the buffer M.apply_location_highlighting(M.state.bufnr) end else -- EOF with no additional data — mark loading as finished to prevent -- apply_location_highlighting -> ensure_content_loaded_async recursion, -- then apply highlighting with whatever content we have. M.state.has_more_content = false M.apply_location_highlighting(M.state.bufnr) end end) end local function link_buffer_content(source_bufnr, target_bufnr) local lines = vim.api.nvim_buf_get_lines(source_bufnr, 0, -1, false) set_buffer_lines(target_bufnr, lines) local source_ft = vim.api.nvim_get_option_value('filetype', { buf = source_bufnr }) if source_ft ~= '' then local ok = set_preview_filetype(target_bufnr, source_ft) if not ok then return false end end M.state.has_more_content = false M.state.total_file_lines = #lines M.state.loaded_lines = #lines M.state.content_height = #lines return true end M.config = nil M.state = { bufnr = nil, winid = nil, current_file = nil, scroll_offset = 0, content_height = 0, loaded_lines = 0, total_file_lines = nil, loading_chunk_size = 1000, is_loading = false, has_more_content = true, file_handle = nil, ---@type uv.uv_fs_t|nil file_operation = nil, -- Ongoing file operation: {fd?: any, file_path?: string, position?: number} location = nil, -- Current location data for highlighting location_namespace = nil, -- Namespace for location highlighting preview_generation = 0, -- Monotonically increasing token to detect stale async callbacks is_binary_preview = false, -- Whether the current preview is a hex dump hex_byte_offset = 0, -- Next byte offset for hex dump paging } --- Setup preview configuration --- @param config table Configuration options function M.setup(config) M.config = config or {} -- Create namespace for location highlighting if not M.state.location_namespace then M.state.location_namespace = vim.api.nvim_create_namespace('fff_preview_location') end end --- Check if file is too big for initial preview (inspired by snacks.nvim) --- @param file_path string Path to the file --- @return boolean True if file is too big for initial preview function M.is_big_file(file_path) -- Only check file size for early detection - no line limits with dynamic loading local stat = vim.uv.fs_stat(file_path) if stat and stat.size > M.config.max_size then return true end return false end --- Get file information --- @param file_path string Path to the file --- @return table | nil File information function M.get_file_info(file_path) local stat = vim.uv.fs_stat(file_path) if not stat then return nil end local info = { name = vim.fn.fnamemodify(file_path, ':t'), path = file_path, size = stat.size, modified = stat.mtime.sec, accessed = stat.atime.sec, type = stat.type, } info.extension = vim.fn.fnamemodify(file_path, ':e'):lower() info.filetype = utils.detect_filetype(file_path) or 'text' info.size_formatted = utils.format_file_size(info.size) info.modified_formatted = os.date('%Y-%m-%d %H:%M:%S', info.modified) info.accessed_formatted = os.date('%Y-%m-%d %H:%M:%S', info.accessed) return info end --- Preview a regular file --- @param file_path string Path to the file --- @param bufnr number Buffer number for preview --- @return boolean Success status function M.preview_file(file_path, bufnr) -- Early size detection to prevent memory issues if M.is_big_file(file_path) then local info = M.get_file_info(file_path) local lines = { 'File too large for preview', string.format( 'Size: %s (max: %s)', info and info.size_formatted or 'Unknown', string.format('%.1fMB', M.config.max_size / 1024 / 1024) ), '', 'Use a text editor to view this file.', } set_buffer_lines(bufnr, lines) return true end local info = M.get_file_info(file_path) if not info then return false end -- if the buffer is already opened for this file we reuse the buffer directly local existing_bufnr = find_existing_buffer(file_path) if existing_bufnr then local success = link_buffer_content(existing_bufnr, bufnr) if success then local file_config = M.get_file_config(file_path) vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) vim.api.nvim_set_option_value('readonly', true, { buf = bufnr }) vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr }) if M.state.winid and vim.api.nvim_win_is_valid(M.state.winid) then vim.api.nvim_set_option_value('wrap', file_config.wrap_lines or M.config.wrap_lines, { win = M.state.winid }) end M.state.scroll_offset = 0 -- Apply location highlighting if available (delayed to ensure buffer is ready) local gen = M.state.preview_generation vim.schedule(function() if M.state.preview_generation == gen then M.apply_location_highlighting(bufnr) end end) return true end end M.state.current_file = file_path M.state.bufnr = bufnr local generation = M.state.preview_generation read_file_streaming_async(file_path, function(content, err, loading_more) if M.state.preview_generation ~= generation then -- Preview moved on to a different file, discard cleanup_file_operation() return end if err or not content then if M.state.current_file == file_path then set_buffer_lines(bufnr, { 'Failed to load file: ' .. (err or 'unknown error') }) end return end if M.state.current_file == file_path then -- Guard against buffer being destroyed while async read was in-flight if not vim.api.nvim_buf_is_valid(bufnr) then return end M.clear_preview_visual_state(bufnr) set_buffer_lines(bufnr, content) local file_config = M.get_file_config(file_path) local ok = set_preview_filetype(bufnr, info.filetype) if not ok then show_preview_unavailable(bufnr) return end vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) vim.api.nvim_set_option_value('readonly', true, { buf = bufnr }) vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr }) if M.state.winid and vim.api.nvim_win_is_valid(M.state.winid) then vim.api.nvim_set_option_value('wrap', file_config.wrap_lines or M.config.wrap_lines, { win = M.state.winid }) end M.state.content_height = #content M.state.scroll_offset = 0 -- Apply location highlighting if available (delayed to ensure buffer is ready). -- Skip when more content is being loaded asynchronously to reach the target line — -- ensure_content_loaded_async will re-apply highlighting once the target is in the buffer. if not loading_more then vim.schedule(function() if M.state.preview_generation == generation then M.apply_location_highlighting(bufnr) end end) end end end) return true end -- Hex preview highlight support: dynamically create hl groups from "#rrggbb" local hex_ns = nil local hex_hl_cache = {} local function ensure_hex_ns() if not hex_ns then hex_ns = vim.api.nvim_create_namespace('fff_hex_preview') end return hex_ns end local function get_hex_hl_group(hex_color) local cached = hex_hl_cache[hex_color] if cached then return cached end local group = 'FffHex_' .. hex_color:sub(2) vim.api.nvim_set_hl(0, group, { fg = hex_color }) hex_hl_cache[hex_color] = group return group end --- Apply hex highlight spans to a buffer --- @param bufnr number Buffer number --- @param highlights table Array of {line_0idx, col_start, col_end, "#rrggbb"} --- @param line_offset number Lines to add to each highlight line index (for header) local function apply_hex_highlights(bufnr, highlights, line_offset) if not highlights or not vim.api.nvim_buf_is_valid(bufnr) then return end local ns = ensure_hex_ns() for _, hl in ipairs(highlights) do pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, hl[1] + line_offset, hl[2], { end_col = hl[3], hl_group = get_hex_hl_group(hl[4]), }) end end --- Load a page of hex dump content from the Rust backend --- @param file_path string Path to the binary file --- @param byte_offset number Byte offset to start reading from --- @return table|nil Result with lines, highlights, has_more, next_offset local function load_hex_page(file_path, byte_offset) local ok, result = pcall(rust.hex_dump, file_path, byte_offset, 4096) if ok and result then return result end return nil end --- Load more hex content when scrolling near the end of the buffer local function load_more_hex_content() if not M.state.bufnr or not vim.api.nvim_buf_is_valid(M.state.bufnr) then return end if not M.state.has_more_content or not M.state.current_file then return end local current_lines = vim.api.nvim_buf_line_count(M.state.bufnr) local result = load_hex_page(M.state.current_file, M.state.hex_byte_offset) if result and result.lines and #result.lines > 0 then append_buffer_lines(M.state.bufnr, result.lines) M.state.hex_byte_offset = result.next_offset M.state.has_more_content = result.has_more M.state.content_height = vim.api.nvim_buf_line_count(M.state.bufnr) M.state.loaded_lines = M.state.content_height apply_hex_highlights(M.state.bufnr, result.highlights, current_lines) else M.state.has_more_content = false end end --- Preview a binary file using hexyl-powered hex dump with paging --- @param file_path string Path to the file --- @param bufnr number Buffer number for preview --- @return boolean Success status function M.preview_binary_file(file_path, bufnr) local info = M.get_file_info(file_path) local lines = {} M.state.is_binary_preview = true M.state.hex_byte_offset = 0 -- Build header synchronously (file -b is fast, typically <10ms) if vim.fn.executable('file') == 1 then local output = vim.fn.system({ 'file', '-b', file_path }) if vim.v.shell_error == 0 and output then local file_type = output:gsub('\n', '') table.insert(lines, 'Binary file: ' .. file_type) if info and info.size_formatted then table.insert(lines, 'Size: ' .. info.size_formatted) end table.insert(lines, '') end end local hex_result = load_hex_page(file_path, 0) if hex_result and hex_result.lines then for _, hex_line in ipairs(hex_result.lines) do table.insert(lines, hex_line) end M.state.hex_byte_offset = hex_result.next_offset M.state.has_more_content = hex_result.has_more end set_buffer_lines(bufnr, lines) local ok = set_preview_filetype(bufnr, 'text') if not ok then show_preview_unavailable(bufnr) return false end vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) vim.api.nvim_set_option_value('readonly', true, { buf = bufnr }) M.state.content_height = #lines M.state.loaded_lines = #lines if hex_result and hex_result.highlights then local header_lines = #lines - (hex_result.lines and #hex_result.lines or 0) apply_hex_highlights(bufnr, hex_result.highlights, header_lines) end return true end --- Get file-specific configuration --- @param file_path string Path to the file --- @return table Configuration for the file function M.get_file_config(file_path) if not M.config or not M.config.filetypes then return {} end local filetype = utils.detect_filetype(file_path) or 'text' return M.config.filetypes[filetype] or {} end --- @param file_path string Path to the file or directory --- @param bufnr number Buffer number for preview --- @param location table|nil Optional location data for highlighting --- @param is_binary boolean|nil Whether the file is binary (from Rust indexer) --- @return boolean if the preview was successful function M.preview(file_path, bufnr, location, is_binary) if not file_path or file_path == '' then return false end -- Bump generation to invalidate any in-flight async callbacks from previous previews M.state.preview_generation = M.state.preview_generation + 1 if M.state.file_handle then ---@diagnostic disable-next-line: undefined-field M.state.file_handle:close() M.state.file_handle = nil end M.state.loaded_lines = 0 M.state.total_file_lines = nil M.state.has_more_content = true M.state.is_loading = false M.state.hex_byte_offset = 0 M.state.is_binary_preview = false M.state.current_file = file_path M.state.bufnr = bufnr M.state.location = location if image.is_image(file_path) then M.clear_buffer(bufnr) if not M.state.winid or not vim.api.nvim_win_is_valid(M.state.winid) then return false end return image.display_image(file_path, bufnr) elseif is_binary then return M.preview_binary_file(file_path, bufnr) else return M.preview_file(file_path, bufnr) end end function M.scroll(lines) if not M.state.bufnr or not vim.api.nvim_buf_is_valid(M.state.bufnr) then return end if not M.state.winid or not vim.api.nvim_win_is_valid(M.state.winid) then return end local win_height = vim.api.nvim_win_get_height(M.state.winid) local current_buffer_lines = vim.api.nvim_buf_line_count(M.state.bufnr) local current_offset = M.state.scroll_offset or 0 local new_offset = current_offset + lines -- If scrolling down and approaching end of loaded content, try to load more if lines > 0 and not M.state.is_loading then local target_line = new_offset + win_height local buffer_needed = target_line + 20 if current_buffer_lines < buffer_needed and M.state.has_more_content then if M.state.is_binary_preview then load_more_hex_content() -- Re-read line count after loading more current_buffer_lines = vim.api.nvim_buf_line_count(M.state.bufnr) else ensure_content_loaded_async(target_line) end end end local content_height = current_buffer_lines local half_screen = math.floor(win_height / 2) local max_scroll = math.max(0, content_height + half_screen - win_height) new_offset = math.max(0, math.min(max_scroll, new_offset)) if new_offset ~= current_offset then M.state.scroll_offset = new_offset M.state.content_height = content_height local target_line = math.min(content_height, math.max(1, new_offset + 1)) vim.api.nvim_win_call(M.state.winid, function() vim.api.nvim_win_set_cursor(M.state.winid, { target_line, 0 }) vim.cmd('normal! zt') end) end end --- Set preview window --- @param winid number Window ID for the preview function M.set_preview_window(winid) M.state.winid = winid end local function resolve_absolute_path(rel) if not rel or rel == '' then return nil end if rel:sub(1, 1) == '/' then return rel end local base = require('fff.conf').get().base_path if base and base ~= '' then return (base:sub(-1) == '/') and (base .. rel) or (base .. '/' .. rel) end return vim.fn.fnamemodify(rel, ':p') end local function build_file_info_data(file, file_metadata, file_index) local conf = require('fff.conf').get() local is_grep = file.line_number ~= nil local sections = (conf.debug and conf.debug.show_file_info) or { file_info = true, score_breakdown = true, timings = true, full_path = true } -- Grep matches don't have a per-file fuzzy score, so the breakdown would -- be all "N/A". Skip it but keep the rest of the panel identical. if is_grep then sections = vim.tbl_extend('force', sections, { score_breakdown = false }) end local abs_path = resolve_absolute_path(file.relative_path) local times_opened = 0 if abs_path then local ok, n = pcall(rust.get_file_access_count, abs_path) if ok and type(n) == 'number' then times_opened = n end end local file_view = { relative_path = file.relative_path, absolute_path = abs_path or file.absolute_path or file_metadata.path, size_formatted = file_metadata.size_formatted, filetype = file_metadata.filetype or 'text', git_status = file.git_status or 'clean', access_frecency_score = file.access_frecency_score or 0, modification_frecency_score = file.modification_frecency_score or 0, times_opened = times_opened, modified_formatted = file_metadata.modified_formatted, accessed_formatted = file_metadata.accessed_formatted, } local score = nil if not is_grep and file_index then score = file_picker.get_file_score(file_index) end return { file = file_view, score = score, sections = sections, hls = conf.hl, } end local function apply_file_info_extmarks(bufnr, extmarks) vim.api.nvim_buf_clear_namespace(bufnr, M.file_info_ns, 0, -1) for _, m in ipairs(extmarks) do local opts = {} if m.hl_group then opts.hl_group = m.hl_group if m.end_col then opts.end_col = m.end_col end end if m.virt_text then opts.virt_text = m.virt_text opts.virt_text_pos = m.virt_text_pos or 'overlay' end pcall(vim.api.nvim_buf_set_extmark, bufnr, M.file_info_ns, m.row, m.col or 0, opts) end end local function resize_file_info(file_info_win, target_h, preview_win) if not (file_info_win and vim.api.nvim_win_is_valid(file_info_win)) then return end local current_h = vim.api.nvim_win_get_height(file_info_win) local delta = target_h - current_h if delta == 0 then return end pcall(vim.api.nvim_win_set_config, file_info_win, { height = target_h }) if preview_win and vim.api.nvim_win_is_valid(preview_win) then local cfg = vim.api.nvim_win_get_config(preview_win) if cfg.row and cfg.height then local new_row = (type(cfg.row) == 'table' and cfg.row[false] or cfg.row) + delta pcall(vim.api.nvim_win_set_config, preview_win, { relative = cfg.relative or 'editor', row = new_row, col = cfg.col, width = cfg.width, height = math.max(3, cfg.height - delta), }) end end end --- Update file info buffer --- @param file table --- @param bufnr number --- @param file_index number|nil --- @param preview_win number|nil Preview window — shifted to keep flush with file_info --- @return boolean function M.update_file_info_buffer(file, bufnr, file_index, preview_win) if not file then set_buffer_lines(bufnr, { 'No file selected' }) return false end local info = M.get_file_info(file.relative_path) if not info then set_buffer_lines(bufnr, { 'File info unavailable' }) return false end local width = 80 local target_win local wins = vim.fn.win_findbuf(bufnr) for _, win in ipairs(wins) do if vim.api.nvim_win_is_valid(win) then width = vim.api.nvim_win_get_width(win) target_win = win break end end local data = build_file_info_data(file, info, file_index) data.width = width local result = file_info_renderer.build(data) set_buffer_lines(bufnr, result.lines) apply_file_info_extmarks(bufnr, result.extmarks) vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) vim.api.nvim_set_option_value('readonly', true, { buf = bufnr }) vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr }) for _, win in ipairs(wins) do if vim.api.nvim_win_is_valid(win) then vim.api.nvim_set_option_value('wrap', false, { win = win }) end end if result.height > 0 then resize_file_info(target_win, result.height, preview_win) end return true end function M.clear_preview_visual_state(bufnr) if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end -- Only clear visual state, don't affect buffer functionality -- Clear namespaces and extmarks for this buffer only vim.api.nvim_buf_clear_namespace(bufnr, -1, 0, -1) -- Clear location highlights if M.state.location_namespace then location_utils.clear_location_highlights(bufnr, M.state.location_namespace) end local wins = vim.fn.win_findbuf(bufnr) for _, win in ipairs(wins) do if vim.api.nvim_win_is_valid(win) then -- Reset folds pcall(vim.api.nvim_win_call, win, function() if vim.fn.has('folding') == 1 then vim.cmd('normal! zE') -- eliminate all folds vim.opt_local.foldenable = false -- disable folding end end) end end image.clear_buffer_images(bufnr) end function M.clear_buffer(bufnr) if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end cleanup_file_operation() M.clear_preview_visual_state(bufnr) pcall(vim.treesitter.stop, bufnr) vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr }) set_preview_filetype(bufnr, '') vim.api.nvim_set_option_value('syntax', '', { buf = bufnr }) vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr }) set_buffer_lines(bufnr, {}) end function M.clear() -- Bump generation to invalidate any in-flight async callbacks M.state.preview_generation = M.state.preview_generation + 1 cleanup_file_operation() M.state.loaded_lines = 0 M.state.total_file_lines = nil M.state.has_more_content = true M.state.is_loading = false if M.state.bufnr and vim.api.nvim_buf_is_valid(M.state.bufnr) then M.clear_buffer(M.state.bufnr) end M.state.current_file = nil M.state.scroll_offset = 0 M.state.content_height = 0 M.state.location = nil M.state.is_binary_preview = false M.state.hex_byte_offset = 0 end --- Apply location highlighting to the preview buffer --- @param bufnr number Buffer number function M.apply_location_highlighting(bufnr) -- Ensure namespace is created if not M.state.location_namespace then M.state.location_namespace = vim.api.nvim_create_namespace('fff_preview_location') end -- Always clear previous location highlights first if vim.api.nvim_buf_is_valid(bufnr) then location_utils.clear_location_highlights(bufnr, M.state.location_namespace) end if not M.state.location then return end location_utils.highlight_location(bufnr, M.state.location, M.state.location_namespace) if M.state.winid and vim.api.nvim_win_is_valid(M.state.winid) then local target_line = location_utils.get_target_line(M.state.location) if target_line then local buffer_lines = vim.api.nvim_buf_line_count(bufnr) if target_line > buffer_lines and M.state.has_more_content then -- Target line is beyond loaded content — load more first. -- ensure_content_loaded_async will re-apply highlighting when done. ensure_content_loaded_async(target_line) return end M.scroll_to_line(target_line) end end end --- Scroll preview to a specific line --- @param line number Target line number (1-indexed) function M.scroll_to_line(line) if not M.state.winid or not vim.api.nvim_win_is_valid(M.state.winid) then return end if not M.state.bufnr or not vim.api.nvim_buf_is_valid(M.state.bufnr) then return end local win_height = vim.api.nvim_win_get_height(M.state.winid) local buffer_lines = vim.api.nvim_buf_line_count(M.state.bufnr) local target_line = math.max(1, math.min(line, buffer_lines)) local half_screen = math.floor(win_height / 2) local new_offset = math.max(0, target_line - half_screen) M.state.scroll_offset = new_offset pcall(vim.api.nvim_win_call, M.state.winid, function() vim.api.nvim_win_set_cursor(M.state.winid, { target_line, 0 }) vim.cmd('normal! zt') end) end return M