local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" local previewer_base = require "fzf-lua.previewer".base local raw_action = require("fzf.actions").raw_action local api = vim.api local fn = vim.fn local cmd = vim.cmd local Previewer = {} -- signgleton instance used for our keymappings local _self = nil setmetatable(Previewer, { __call = function (cls, ...) return cls:new(...) end, }) function Previewer:setup_keybinds() if not self.win or not self.win.fzf_bufnr then return end local keymap_tbl = { toggle_full = { module = 'previewer.builtin', fnc = 'toggle_full()' }, toggle_wrap = { module = 'previewer.builtin', fnc = 'toggle_wrap()' }, toggle_hide = { module = 'previewer.builtin', fnc = 'toggle_hide()' }, page_up = { module = 'previewer.builtin', fnc = 'scroll(-1)' }, page_down = { module = 'previewer.builtin', fnc = 'scroll(1)' }, page_reset = { module = 'previewer.builtin', fnc = 'scroll(0)' }, } local function funcref_str(keymap) return ([[lua require('fzf-lua.%s').%s]]):format(keymap.module, keymap.fnc) end for action, key in pairs(self.keymap) do local keymap = keymap_tbl[action] if keymap and not vim.tbl_isempty(keymap) and key ~= false then api.nvim_buf_set_keymap(self.win.fzf_bufnr, 't', key, funcref_str(keymap), {nowait = true, noremap = true}) end end end function Previewer:new(o, opts, fzf_win) self = setmetatable(previewer_base(o, opts), { __index = vim.tbl_deep_extend("keep", self, previewer_base )}) self.type = "builtin" self.win = fzf_win self.wrap = o.wrap self.title = o.title self.scrollbar = o.scrollbar if o.scrollchar then self.win.winopts.scrollchar = o.scrollchar end self.expand = o.expand or o.fullscreen self.hidden = o.hidden self.syntax = o.syntax self.syntax_delay = o.syntax_delay self.syntax_limit_b = o.syntax_limit_b self.syntax_limit_l = o.syntax_limit_l self.hl_cursor = o.hl_cursor self.hl_cursorline = o.hl_cursorline self.hl_range = o.hl_range self.keymap = o.keymap self.backups = {} _self = self return self end function Previewer:close() -- restore winopts backup for those that weren't restored -- (usually the last previewed loaded buffer) for bufnr, _ in pairs(self.backups) do self:restore_winopts(bufnr, self.win.preview_winid) end self:clear_preview_buf() self.backups = {} _self = nil end function Previewer:update_border(entry) if self.title then local title = (' %s '):format(entry.path) if entry.bufnr then -- local border_width = api.nvim_win_get_width(self.win.preview_winid) local buf_str = ('buf %d:'):format(entry.bufnr) title = (' %s %s '):format(buf_str, entry.path) end self.win:update_title(title) end if self.scrollbar then self.win:update_scrollbar() end end function Previewer:gen_winopts() return { wrap = self.wrap, number = true, relativenumber = false, cursorline = true, cursorlineopt = 'both', cursorcolumn = false, signcolumn = 'no', foldenable = false, foldmethod = 'manual', } end function Previewer:backup_winopts(key, win) if not key then return end if not win or not api.nvim_win_is_valid(win) then return end self.backups[key] = {} for opt, _ in pairs(self:gen_winopts()) do if utils.nvim_has_option(opt) then self.backups[key][opt] = api.nvim_win_get_option(win, opt) end end end function Previewer:restore_winopts(key, win) if not self.backups[key] then return end if not win or not api.nvim_win_is_valid(win) then return end for opt, v in pairs(self.backups[key]) do if utils.nvim_has_option(opt) then api.nvim_win_set_option(win, opt, v) end end self.backups[key] = nil end function Previewer:set_winopts(win) if not win or not api.nvim_win_is_valid(win) then return end for opt, v in pairs(self:gen_winopts()) do if utils.nvim_has_option(opt) then api.nvim_win_set_option(win, opt, v) end --[[ api.nvim_win_call(win, function() api.nvim_win_set_option(0, opt, v) end) ]] end end local function set_cursor_hl(self, entry) local lnum, col = tonumber(entry.line), tonumber(entry.col) local pattern = entry.pattern or entry.text if not lnum or lnum < 1 then api.nvim_win_set_cursor(0, {1, 0}) if pattern ~= '' then fn.search(pattern, 'c') end else if not pcall(api.nvim_win_set_cursor, 0, {lnum, math.max(0, col - 1)}) then return end end utils.zz() self.orig_pos = api.nvim_win_get_cursor(0) fn.clearmatches() if lnum and lnum > 0 and col and col > 1 then fn.matchaddpos(self.hl_cursor, {{lnum, math.max(1, col)}}, 11) end cmd(('noa call nvim_set_current_win(%d)'):format(self.win.preview_winid)) end function Previewer:do_syntax(entry) if not self.preview_bufnr then return end local bufnr = self.preview_bufnr local preview_winid = self.win.preview_winid if self.preview_bufloaded and vim.bo[bufnr].filetype == '' then if fn.bufwinid(bufnr) == preview_winid then -- do not enable for large files, treesitter still has perf issues: -- https://github.com/nvim-treesitter/nvim-treesitter/issues/556 -- https://github.com/nvim-treesitter/nvim-treesitter/issues/898 local lcount = api.nvim_buf_line_count(bufnr) local bytes = api.nvim_buf_get_offset(bufnr, lcount) local syntax_limit_reached = 0 if self.syntax_limit_l > 0 and lcount > self.syntax_limit_l then syntax_limit_reached = 1 end if self.syntax_limit_b > 0 and bytes > self.syntax_limit_b then syntax_limit_reached = 2 end if syntax_limit_reached > 0 then utils.info(string.format( "syntax disabled for '%s' (%s), consider increasing '%s(%d)'", entry.path, utils._if(syntax_limit_reached==1, ("%d lines"):format(lcount), ("%db"):format(bytes)), utils._if(syntax_limit_reached==1, 'syntax_limit_l', 'syntax_limit_b'), utils._if(syntax_limit_reached==1, self.syntax_limit_l, self.syntax_limit_b) )) end if syntax_limit_reached == 0 then -- nvim_buf_call is less side-effects than changing window -- make sure that buffer in preview window must not in normal window -- greedy match anything after last dot local ext = entry.path:match("[^.]*$") if ext then pcall(api.nvim_buf_set_option, bufnr, 'filetype', ext) end api.nvim_buf_call(bufnr, function() vim.cmd('filetype detect') end) end end end end function Previewer:set_tmp_buffer() if not self.win or not self.win:validate_preview() then return end local tmp_buf = api.nvim_create_buf(false, true) api.nvim_buf_set_option(tmp_buf, 'bufhidden', 'wipe') api.nvim_win_set_buf(self.win.preview_winid, tmp_buf) return tmp_buf end function Previewer:clear_preview_buf() local retbuf = nil if self.win and self.win:validate_preview() then -- attach a temp buffer to the window -- so we can safely delete the buffer -- ('nvim_buf_delete' removes the attached win) retbuf = self:set_tmp_buffer() end if self.preview_bufloaded then local bufnr = self.preview_bufnr if vim.api.nvim_buf_is_valid(bufnr) then api.nvim_buf_call(bufnr, function() vim.cmd('delm \\"') end) vim.api.nvim_buf_delete(bufnr, {force=true}) end end self.preview_bufnr = nil self.preview_bufloaded = nil return retbuf end function Previewer:display_last_entry() self:display_entry(self.last_entry) end function Previewer:preview_buf_post(entry) -- backup window options local bufnr = self.preview_bufnr local preview_winid = self.win.preview_winid self:backup_winopts(bufnr, preview_winid) -- set preview win options or load the file -- if not already loaded from buffer utils.win_execute(preview_winid, function() set_cursor_hl(self, entry) end) -- set preview window options if not utils.is_term_buffer(bufnr) then self:set_winopts(preview_winid) end -- reset the preview window highlights self.win:reset_win_highlights(preview_winid) -- local ml = vim.bo[entry.bufnr].ml -- vim.bo[entry.bufnr].ml = false if self.syntax then vim.defer_fn(function() self:do_syntax(entry) -- vim.bo[entry.bufnr].ml = ml end, self.syntax_delay) end self:update_border(entry) end function Previewer:display_entry(entry) if not entry then return else -- save last entry even if we don't display self.last_entry = entry end if not self.win or not self.win:validate_preview() then return end local preview_winid = self.win.preview_winid local previous_bufnr = api.nvim_win_get_buf(preview_winid) assert(not self.preview_bufnr or previous_bufnr == self.preview_bufnr) -- restore settings for the buffer we were previously viewing self:restore_winopts(previous_bufnr, preview_winid) -- clear the current preview buffer local bufnr = self:clear_preview_buf() -- store the preview buffer self.preview_bufnr = bufnr if entry.bufnr and api.nvim_buf_is_loaded(entry.bufnr) then -- must convert to number or our backup will have conflicting keys bufnr = tonumber(entry.bufnr) -- display the buffer in the preview api.nvim_win_set_buf(preview_winid, bufnr) -- store current preview buffer self.preview_bufnr = bufnr self:preview_buf_post(entry) else -- mark the buffer for unloading the next call self.preview_bufloaded = true -- read the file into the buffer utils.read_file_async(entry.path, vim.schedule_wrap(function(data) if not vim.api.nvim_buf_is_valid(bufnr) then return end local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, vim.split(data, "[\r]?\n")) if not ok then return end self:preview_buf_post(entry) end)) end end function Previewer:action(_) local act = raw_action(function (items, _, _) local entry = path.entry_to_file(items[1], self.opts.cwd) self:display_entry(entry) return "" end) return act end function Previewer:cmdline(_) return vim.fn.shellescape(self:action()) -- return 'true' end function Previewer:preview_window(_) return 'nohidden:right:0' end function Previewer:override_fzf_preview_window() return self.win and not self.win.winopts.split end function Previewer.scroll(direction) if not _self then return end local self = _self local preview_winid = self.win.preview_winid if preview_winid < 0 or not direction then return end if utils.is_term_buffer(self.preview_bufnr) then local input = direction > 0 and [[]] or [[]] local count = math.abs(direction) vim.api.nvim_win_call(preview_winid, function() -- TODO: why does this cause an error? -- 'Vim(normal):Can't re-enter normal mode from terminal mode' -- vim.cmd([[normal! ]] .. count .. input) print([[normal! ]] .. count .. input) end) return end utils.win_execute(preview_winid, function() if direction == 0 then api.nvim_win_set_cursor(preview_winid, self.orig_pos) else -- ^D = 0x04, ^U = 0x15 fn.execute(('norm! %c'):format(direction > 0 and 0x04 or 0x15)) end utils.zz() cmd(('noa call nvim_set_current_win(%d)'):format(self.win.fzf_winid)) end) if self.scrollbar then self.win:update_scrollbar() end end function Previewer.toggle_wrap() if not _self then return end local self = _self self.wrap = not self.wrap if self.win and self.win:validate_preview() then api.nvim_win_set_option(self.win.preview_winid, 'wrap', self.wrap) end end function Previewer.toggle_full() if not _self then return end local self = _self self.expand = not self.expand if self.win and self.win:validate_preview() then self.win:redraw_preview() end end function Previewer.toggle_hide() if not _self then return end local self = _self if self.win then if self.win:validate_preview() then self.win:close_preview() else self.win:redraw_preview() self:display_last_entry() end end -- close_preview() calls Previewer:close() -- which will clear out our singleton so -- we must save it again to call redraw _self = self end return Previewer