local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" local previewer_base = require "fzf-lua.previewer" local raw_action = require("fzf.actions").raw_action local api = vim.api local fn = vim.fn local Previewer = {} Previewer.base = {} Previewer.buffer_or_file = {} Previewer.help_tags = {} Previewer.man_pages = {} Previewer.marks = {} -- Constructors call on Previewer.base.() for c, _ in pairs(Previewer) do setmetatable(Previewer[c], { __call = function (cls, ...) return cls:new(...) end, }) end function Previewer.base: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.title = o.title self.scrollbar = o.scrollbar if o.scrollchar and type(o.scrollchar) == 'string' then self.win.winopts.scrollchar = o.scrollchar end 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.backups = {} return self end function Previewer.base:close() self:restore_winopts(self.win.preview_winid) self:clear_preview_buf() self.backups = {} end function Previewer.base:gen_winopts() return { wrap = self.win.preview_wrap, number = true, relativenumber = false, cursorline = true, cursorlineopt = 'both', cursorcolumn = false, signcolumn = 'no', list = false, foldenable = false, foldmethod = 'manual', } end function Previewer.base:backup_winopts(win) if not win or not api.nvim_win_is_valid(win) then return end for opt, _ in pairs(self:gen_winopts()) do if utils.nvim_has_option(opt) then self.backups[opt] = api.nvim_win_get_option(win, opt) end end end function Previewer.base:restore_winopts(win) if not win or not api.nvim_win_is_valid(win) then return end for opt, value in pairs(self.backups) do vim.api.nvim_win_set_option(win, opt, value) end end function Previewer.base: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 end end function Previewer.base: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.base: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.base:display_last_entry() self:display_entry(self.last_entry) end function Previewer.base:display_entry(entry_str) if not entry_str then return else -- save last entry even if we don't display self.last_entry = entry_str end if not self.win or not self.win:validate_preview() then return end if rawequal(next(self.backups), nil) then self:backup_winopts(self.win.src_winid) end local previous_bufnr = api.nvim_win_get_buf(self.win.preview_winid) assert(not self.preview_bufnr or previous_bufnr == self.preview_bufnr) -- clear the current preview buffer -- store the new preview buffer self.preview_bufnr = self:clear_preview_buf() -- specialized previewer populate function self:populate_preview_buf(entry_str) -- is the preview a terminal buffer (alternative way) --[[ local is_terminal = vim.fn.getwininfo(self.win.preview_winid)[1].terminal == 1 ]] -- set preview window options if not utils.is_term_buffer(self.preview_bufnr) then self:set_winopts(self.win.preview_winid) end -- reset the preview window highlights self.win:reset_win_highlights(self.win.preview_winid) end function Previewer.base:action(_) local act = raw_action(function (items, _, _) self:display_entry(items[1]) return "" end, "{}") return act end function Previewer.base:cmdline(_) return vim.fn.shellescape(self:action()) -- return 'true' end function Previewer.base:preview_window(_) if self.win and not self.win.winopts.split then return 'nohidden:right:0' else return nil end end function Previewer.base:scroll(direction) local preview_winid = self.win.preview_winid if preview_winid < 0 or not direction then return end if direction == 0 then vim.api.nvim_win_call(preview_winid, function() -- for some reason 'nvim_win_set_cursor' -- only moves forward, so set to (1,0) first api.nvim_win_set_cursor(0, {1, 0}) api.nvim_win_set_cursor(0, self.orig_pos) utils.zz() end) else if utils.is_term_buffer(self.preview_bufnr) then -- can't use ":norm!" with terminal buffers due to: -- 'Vim(normal):Can't re-enter normal mode from terminal mode' -- https://github.com/neovim/neovim/issues/4895#issuecomment-303073838 -- according to the above comment feedkeys is the correct workaround -- TODO: hide the typed command from the user (possible?) local input = direction > 0 and "" or "" vim.cmd("stopinsert") utils.feed_keys_termcodes((':noa lua vim.api.nvim_win_call(' .. '%d, function() vim.cmd("norm! %s") vim.cmd("startinsert") end)'): format(tonumber(preview_winid), input)) else -- local input = direction > 0 and [[]] or [[]] -- local input = direction > 0 and [[]] or [[]] -- ^D = 0x04, ^U = 0x15 ('g8' on char to display) local input = ('%c'):format(utils._if(direction>0, 0x04, 0x15)) vim.api.nvim_win_call(preview_winid, function() vim.cmd([[norm! ]] .. input) utils.zz() end) end end if self.scrollbar then self.win:update_scrollbar() end end function Previewer.buffer_or_file:new(o, opts, fzf_win) self = setmetatable(Previewer.base(o, opts, fzf_win), { __index = vim.tbl_deep_extend("keep", self, Previewer.base )}) return self end function Previewer.buffer_or_file:parse_entry(entry_str) local entry = path.entry_to_file(entry_str, self.opts.cwd) if entry.bufnr and utils.is_term_buffer(entry.bufnr) then entry.line = tonumber(entry_str:match(":(%d+)$")) end return entry end function Previewer.buffer_or_file:populate_preview_buf(entry_str) local entry = self:parse_entry(entry_str) if entry.bufnr and api.nvim_buf_is_loaded(entry.bufnr) then -- must convert to number or our backup will have conflicting keys local bufnr = tonumber(entry.bufnr) -- display the buffer in the preview api.nvim_win_set_buf(self.win.preview_winid, bufnr) -- store current preview buffer self.preview_bufnr = bufnr self:preview_buf_post(entry) else if entry.bufnr then -- buffer was unloaded, can happen when calling `lines` -- with `set nohidden`, fix entry.path since it contains -- filename only entry.path = path.relative(vim.api.nvim_buf_get_name(entry.bufnr), vim.loop.cwd()) end -- mark the buffer for unloading the next call self.preview_bufloaded = true -- make sure the file is readable (or bad entry.path) if not vim.loop.fs_stat(entry.path) then return end -- 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(self.preview_bufnr) then return end local lines = vim.split(data, "[\r]?\n") -- if file ends in new line, don't write an empty string as the last -- line. if data:sub(#data, #data) == "\n" or data:sub(#data-1,#data) == "\r\n" then table.remove(lines) end local ok = pcall(vim.api.nvim_buf_set_lines, self.preview_bufnr, 0, -1, false, lines) if not ok then return end self:preview_buf_post(entry) end)) end end function Previewer.buffer_or_file:do_syntax(entry) if not entry then return end 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 -- prepend the buffer number to the path and -- set as buffer name, this makes sure 'filetype detect' -- gets the right filetype which enables the syntax local tempname = path.join({tostring(bufnr), entry.path}) pcall(api.nvim_buf_set_name, bufnr, tempname) -- nvim_buf_call has less side-effects than window switch api.nvim_buf_call(bufnr, function() vim.cmd('filetype detect') end) end end end end local function set_cursor_hl(self, entry) local lnum, col = tonumber(entry.line), tonumber(entry.col) or 1 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 end function Previewer.buffer_or_file:update_border(entry) if self.title then if self.opts.cwd then entry.path = path.relative(entry.path, self.opts.cwd) end 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.buffer_or_file:preview_buf_post(entry) -- set preview win options or load the file -- if not already loaded from buffer vim.api.nvim_win_call(self.win.preview_winid, function() set_cursor_hl(self, entry) end) -- 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.help_tags:new(o, opts, fzf_win) self = setmetatable(Previewer.base(o, opts, fzf_win), { __index = vim.tbl_deep_extend("keep", self, Previewer.base )}) self.split = o.split self.help_cmd = o.help_cmd or "help" self:init_help_win() return self end function Previewer.help_tags:gen_winopts() return { wrap = self.wrap, number = false, relativenumber = false, cursorline = true, cursorlineopt = 'both', cursorcolumn = false, signcolumn = 'no', list = false, foldenable = false, foldmethod = 'manual', } end function Previewer.help_tags:exec_cmd(str) str = str or '' vim.cmd(("%s %s %s"):format(self.split, self.help_cmd, str)) end function Previewer.help_tags:parse_entry(entry_str) return entry_str end function Previewer.help_tags:init_help_win(str) if not self.split or (self.split ~= "topleft" and self.split ~= "botright") then self.split = "botright" end local orig_winid = api.nvim_get_current_win() self:exec_cmd(str) self.help_bufnr = api.nvim_get_current_buf() self.help_winid = api.nvim_get_current_win() pcall(vim.api.nvim_win_set_height, 0, 0) pcall(vim.api.nvim_win_set_width, 0, 0) api.nvim_set_current_win(orig_winid) end function Previewer.help_tags:populate_preview_buf(entry_str) local entry = self:parse_entry(entry_str) vim.api.nvim_win_call(self.help_winid, function() self.prev_help_bufnr = api.nvim_get_current_buf() self:exec_cmd(entry) self.preview_bufnr = api.nvim_get_current_buf() self.orig_pos = api.nvim_win_get_cursor(0) end) api.nvim_win_set_buf(self.win.preview_winid, self.preview_bufnr) api.nvim_win_set_cursor(self.win.preview_winid, self.orig_pos) if self.scrollbar then self.win:update_scrollbar() end if self.prev_help_bufnr ~= self.preview_bufnr and -- only delete the help buffer when the help -- tag triggers opening a different help file api.nvim_buf_is_valid(self.prev_help_bufnr) then api.nvim_buf_delete(self.prev_help_bufnr, {force=true}) -- save the last buffer so we can close it -- at the win_leave event self.prev_help_bufnr = self.preview_bufnr end end function Previewer.help_tags:win_leave() if vim.api.nvim_win_is_valid(self.help_winid) then api.nvim_win_close(self.help_winid, true) end if vim.api.nvim_buf_is_valid(self.help_bufnr) then vim.api.nvim_buf_delete(self.help_bufnr, {force=true}) end if vim.api.nvim_buf_is_valid(self.prev_help_bufnr) then vim.api.nvim_buf_delete(self.prev_help_bufnr, {force=true}) end self.help_winid = nil self.help_bufnr = nil self.prev_help_bufnr = nil end -- inherit from help_tags function Previewer.man_pages:new(o, opts, fzf_win) self = setmetatable(Previewer.base(o, opts, fzf_win), { __index = vim.tbl_deep_extend("keep", self, Previewer.help_tags, Previewer.base )}) self.split = o.split self.help_cmd = o.help_cmd or "Man" self:init_help_win("echo") return self end function Previewer.man_pages:parse_entry(entry_str) return entry_str:match("[^[,( ]+") -- return require'fzf-lua.providers.manpages'.getmanpage(entry_str) end function Previewer.marks:new(o, opts, fzf_win) self = setmetatable(Previewer.buffer_or_file(o, opts, fzf_win), { __index = vim.tbl_deep_extend("keep", self, Previewer.buffer_or_file, Previewer.base )}) return self end function Previewer.marks:parse_entry(entry_str) local bufnr = nil local mark, lnum, col, filepath = entry_str:match("(.)%s+(%d+)%s+(%d+)%s+(.*)") -- try to acquire position from sending buffer -- if this succeeds (line>0) the mark is inside local pos = vim.api.nvim_buf_get_mark(self.win.src_bufnr, mark) if pos and pos[1] > 0 and pos[1] == tonumber(lnum) then bufnr = self.win.src_bufnr filepath = api.nvim_buf_get_name(bufnr) end if filepath and #filepath>0 then local ok, res = pcall(vim.fn.expand, filepath) if not ok then filepath = '' else filepath = res end filepath = path.relative(filepath, vim.loop.cwd()) end return { bufnr = bufnr, path = filepath, line = tonumber(lnum) or 1, col = tonumber(col) or 1, } end return Previewer