local path = require "fzf-lua.path" local shell = require "fzf-lua.shell" local utils = require "fzf-lua.utils" local Object = require "fzf-lua.class" local api = vim.api local uv = vim.loop local fn = vim.fn local Previewer = {} Previewer.base = Object:extend() function Previewer.base:new(o, opts, fzf_win) local function default(var, def) if var ~= nil then return var else return def end end o = o or {} self.type = "builtin" self.opts = opts; self.win = fzf_win self.delay = self.win.winopts.preview.delay or 100 self.title = self.win.winopts.preview.title self.title_align = self.win.winopts.preview.title_align self.winopts = self.win.winopts.preview.winopts self.syntax = default(o.syntax, true) self.syntax_delay = default(o.syntax_delay, 0) self.syntax_limit_b = default(o.syntax_limit_b, 1024*1024) self.syntax_limit_l = default(o.syntax_limit_l, 0) self.limit_b = default(o.limit_b, 1024*1024*10) self.backups = {} -- convert extension map to lower case if o.extensions then self.extensions = {} for k, v in pairs(o.extensions) do self.extensions[k:lower()] = v end end -- validatee the ueberzug image scaler local uz_scalers = { ["crop"] = "crop", ["distort"] = "distort", ["contain"] = "contain", ["fit_contain"] = "fit_contain", ["cover"] = "cover", ["forced_cover"] = "forced_cover", } self.ueberzug_scaler = o.ueberzug_scaler and uz_scalers[o.ueberzug_scaler] if o.ueberzug_scaler and not self.ueberzug_scaler then utils.warn(("Invalid ueberzug image scaler '%s', option will be omitted.") :format(o.ueberzug_scaler)) end 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() local winopts = { wrap = self.win.preview_wrap } return vim.tbl_extend("keep", winopts, self.winopts) 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 self.do_not_set_winopts then return end 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:preview_is_terminal() if not self.win or not self.win:validate_preview() then return end return vim.fn.getwininfo(self.win.preview_winid)[1].terminal == 1 end function Previewer.base:get_tmp_buffer() local tmp_buf = api.nvim_create_buf(false, true) api.nvim_buf_set_option(tmp_buf, 'bufhidden', 'wipe') return tmp_buf end function Previewer.base:set_preview_buf(newbuf) if not self.win or not self.win:validate_preview() then return end api.nvim_win_set_buf(self.win.preview_winid, newbuf) self.preview_bufnr = newbuf -- set preview window options self:set_winopts(self.win.preview_winid) end function Previewer.base:clear_preview_buf() local retbuf = nil -- we don't use 'self.win:validate_preview()' because we want -- to detach the buffer even when 'self.win.closing = true' if self.win and self.win.preview_winid and tonumber(self.win.preview_winid)>0 and api.nvim_win_is_valid(self.win.preview_winid) then -- attach a temp buffer to the window -- so we can safely delete the buffer -- ('nvim_buf_delete' removes the attached win) retbuf = self:get_tmp_buffer() api.nvim_win_set_buf(self.win.preview_winid, retbuf) end -- since our temp buffers have 'bufhidden=wipe' the tmp -- buffer will be automatically wiped and 'nvim_buf_is_valid' -- will return false -- one case where the buffer may remain valid after detaching -- from the preview window is with URI type entries after calling -- 'vim.lsp.util.jump_to_location' which can reuse existing buffers -- so techinically this should never be executed unless we're the -- user wrote an fzf-lua extension and set the preview buffer to -- a random buffer without the 'bufhidden' property if not self.do_not_unload and self.preview_bufnr and vim.api.nvim_buf_is_valid(self.preview_bufnr) then api.nvim_buf_call(self.preview_bufnr, function() vim.cmd('delm \\"') end) vim.api.nvim_buf_delete(self.preview_bufnr, {force=true}) end self.preview_bufnr = nil self.loaded_entry = 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) -- clears the current preview buffer and set to a new temp buffer -- recommended to return false from 'should_clear_preview' and use -- 'self:set_preview_buf()' instead for flicker-free exeperience local should_clear = self.should_clear_preview and self:should_clear_preview(entry_str) if should_clear ~= false then self.preview_bufnr = self:clear_preview_buf() end local populate_preview_buf = function(entry_str_) if not self.win or not self.win:validate_preview() then return end -- specialized previewer populate function self:populate_preview_buf(entry_str_) -- set preview window options if not self.do_not_set_winopts then self:set_winopts(self.win.preview_winid) else -- removes 'number', 'signcolumn', 'cursorline', etc -- call after 'set_preview_buf' or 'cursorline' becomes true self.win:set_style_minimal(self.win.preview_winid) end -- reset the preview window highlights self.win:reset_win_highlights(self.win.preview_winid) end -- debounce preview entries if tonumber(self.delay)>0 then if not self._entry_count then self._entry_count=1 else self._entry_count = self._entry_count+1 end local entry_count = self._entry_count vim.defer_fn(function() -- only display if entry hasn't changed if self._entry_count == entry_count then populate_preview_buf(entry_str) end end, self.delay) else populate_preview_buf(entry_str) end end function Previewer.base:cmdline(_) local act = shell.raw_action(function (items, _, _) self:display_entry(items[1]) return "" end, "{}", self.opts.debug) return act 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 not api.nvim_win_is_valid(preview_winid) then return end if direction == 0 then pcall(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}) if self.orig_pos then api.nvim_win_set_cursor(0, self.orig_pos) end utils.zz() end) elseif not self:preview_is_terminal() then -- 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)) pcall(vim.api.nvim_win_call, preview_winid, function() vim.cmd([[norm! ]] .. input) utils.zz() end) else -- we get here when using custom term commands using -- the extensions map (i.e. view term images with 'vui') -- we 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)) end -- 'cursorline' is effectively our match highlight, once the -- user scrolls, the highlight is no longer relevenat (#462) -- conditionally toggle 'cursorline' based on cursor position if self.orig_pos and self.winopts.cursorline then local wininfo = vim.fn.getwininfo(preview_winid) if wininfo and wininfo[1] and self.orig_pos[1] >= wininfo[1].topline and self.orig_pos[1] <= wininfo[1].botline then -- reset cursor pos even when it's already there, no bigggie -- local curpos = vim.api.nvim_win_get_cursor(preview_winid) vim.api.nvim_win_set_cursor(preview_winid, self.orig_pos) vim.api.nvim_win_set_option(preview_winid, 'cursorline', true) else vim.api.nvim_win_set_option(preview_winid, 'cursorline', false) end end self.win:update_scrollbar() end Previewer.buffer_or_file = Previewer.base:extend() function Previewer.buffer_or_file:new(o, opts, fzf_win) Previewer.buffer_or_file.super.new(self, o, opts, fzf_win) return self end function Previewer.buffer_or_file:close() self:restore_winopts(self.win.preview_winid) self:clear_preview_buf() self:stop_ueberzug() self.backups = {} end function Previewer.buffer_or_file:parse_entry(entry_str) local entry = path.entry_to_file(entry_str, self.opts) return entry end function Previewer.buffer_or_file:should_clear_preview(_) return false end function Previewer.buffer_or_file:should_load_buffer(entry) -- we don't have a previous entry to compare to -- return 'true' so the buffer will be loaded in -- ::populate_preview_buf if not self.loaded_entry then return true end if type(entry) == 'string' then entry = self:parse_entry(entry) end if (entry.bufnr and entry.bufnr == self.loaded_entry.bufnr) or (not entry.bufnr and entry.path and entry.path == self.loaded_entry.path) then return false end return true end function Previewer.buffer_or_file:start_ueberzug() if self._ueberzug_fifo then return self._ueberzug_fifo end local fifo = ("fzf-lua-%d-ueberzug"):format(vim.fn.getpid()) self._ueberzug_fifo = vim.fn.systemlist({"mktemp", "--dry-run", "--suffix", fifo})[1] vim.fn.system({"mkfifo", self._ueberzug_fifo}) self._ueberzug_job = vim.fn.jobstart({ "sh", "-c", ("tail --follow %s | ueberzug layer --parser json") :format(vim.fn.shellescape(self._ueberzug_fifo)) }, { on_exit = function(_, rc, _) if rc ~= 0 and rc ~= 143 then utils.warn(("ueberzug exited with error %d"):format(rc) .. ", run ':messages' to see the detailed error.") end end, on_stderr = function(_, data, _) for _, l in ipairs(data or {}) do if #l>0 then utils.info(l) end end -- populate the preview buffer with the error message if self.preview_bufnr and self.preview_bufnr > 0 and vim.api.nvim_buf_is_valid(self.preview_bufnr) then local lines = vim.api.nvim_buf_get_lines(self.preview_bufnr, 0, -1, false) for _, l in ipairs(data or {}) do table.insert(lines, l) end vim.api.nvim_buf_set_lines(self.preview_bufnr, 0, -1, false, lines) end end } ) self._ueberzug_pid = vim.fn.jobpid(self._ueberzug_job) return self._ueberzug_fifo end function Previewer.buffer_or_file:stop_ueberzug() if self._ueberzug_job then vim.fn.jobstop(self._ueberzug_job) if type(uv.os_getpriority(self._ueberzug_pid)) == 'number' then uv.kill(self._ueberzug_pid, 9) end self._ueberzug_job = nil self._ueberzug_pid = nil end if self._ueberzug_fifo and uv.fs_stat(self._ueberzug_fifo) then vim.fn.delete(self._ueberzug_fifo) self._ueberzug_fifo = nil end end function Previewer.buffer_or_file:populate_terminal_cmd(tmpbuf, cmd, entry) if not cmd then return end cmd = type(cmd) == 'table' and utils.deepcopy(cmd) or { cmd } if not cmd[1] or vim.fn.executable(cmd[1]) ~= 1 then return false end -- set this to prevent calling 'set_winopts' -- preview buf must be attached beforehand -- for terminal image previews to have the -- correct size self.loaded_entry = nil self.do_not_set_winopts = true -- both ueberzug and terminal cmds need a clear -- on redraw to fit the new window dimentions self.clear_on_redraw = true self:set_preview_buf(tmpbuf) if cmd[1]:match("ueberzug") then local fifo = self:start_ueberzug() if not fifo then return end local wincfg = vim.api.nvim_win_get_config(self.win.preview_winid) local winpos = vim.api.nvim_win_get_position(self.win.preview_winid) local params = { action = "add", identifier = "preview", x = winpos[2], y = winpos[1], width = wincfg.width, height = wincfg.height, scaler = self.ueberzug_scaler, path = path.starts_with_separator(entry.path) and entry.path or path.join({self.opts.cwd or uv.cwd(), entry.path}), } local json = vim.json.encode(params) -- both 'fs_open|write|close' and 'vim.fn.system' work -- we prefer the libuv method as it doesn't rely on the shell -- cmd = { "sh", "-c", ("echo '%s' > %s"):format(json, self._ueberzug_fifo) } -- vim.fn.system(cmd) local fd = uv.fs_open(self._ueberzug_fifo, "a", -1) if fd then uv.fs_write(fd, json.."\n", nil, function(_) uv.fs_close(fd) end) end else -- add filename as last parameter table.insert(cmd, entry.path) -- must be modifiable or 'termopen' fails vim.bo[tmpbuf].modifiable = true vim.api.nvim_buf_call(tmpbuf, function() self._job_id = vim.fn.termopen(cmd, { cwd = self.opts.cwd, on_exit = function() -- run post only after terminal job finished if self._job_id then self:preview_buf_post(entry) self._job_id = nil end end }) end) end -- run here so title gets updated -- even if the image is still loading self:preview_buf_post(entry) return true end function Previewer.buffer_or_file:populate_preview_buf(entry_str) if not self.win or not self.win:validate_preview() then return end local entry = self:parse_entry(entry_str) if vim.tbl_isempty(entry) then return end if not self:should_load_buffer(entry) then -- same file/buffer as previous entry -- no need to reload content -- call post to set cusror location self:preview_buf_post(entry) return end -- stop ueberzug shell job self:stop_ueberzug() -- kill previously running terminal jobs -- when using external commands extension map if self._job_id and self._job_id > 0 then vim.fn.jobstop(self._job_id) self._job_id = nil end -- mark terminal buffers so we don't call 'set_winopts' -- mark uri entries so we do not delete the preview buffer self.clear_on_redraw = false self.do_not_unload = (entry.uri ~= nil) self.do_not_set_winopts = entry.terminal if entry.bufnr and api.nvim_buf_is_loaded(entry.bufnr) then -- WE NO LONGER REUSE THE CURRENT BUFFER -- this changes the buffer's 'getbufinfo[1].lastused' -- which messes up our `buffers()` sort entry.filetype = vim.api.nvim_buf_get_option(entry.bufnr, 'filetype') local lines = vim.api.nvim_buf_get_lines(entry.bufnr, 0, -1, false) local tmpbuf = self:get_tmp_buffer() vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, lines) self:set_preview_buf(tmpbuf) self:preview_buf_post(entry) elseif entry.uri then -- LSP 'jdt://' entries, see issue #195 -- https://github.com/ibhagwan/fzf-lua/issues/195 pcall(vim.api.nvim_win_call, self.win.preview_winid, function() vim.lsp.util.jump_to_location(entry, "utf-16") self.preview_bufnr = vim.api.nvim_get_current_buf() end) self:preview_buf_post(entry) else if entry.bufnr and vim.api.nvim_buf_is_valid(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 -- make sure the file is readable (or bad entry.path) local fs_stat = vim.loop.fs_stat(entry.path) if not entry.path or not fs_stat then return end local tmpbuf = self:get_tmp_buffer() if self.extensions and not vim.tbl_isempty(self.extensions) then local ext = path.extension(entry.path) local cmd = ext and self.extensions[ext:lower()] if cmd and self:populate_terminal_cmd(tmpbuf, cmd, entry) then -- will return 'false' when cmd isn't executable -- if we get here it means preview was successful -- it can still fail if using wrong command flags -- but the use will be able to see the error in -- the preview win return end end do local lines = nil if utils.perl_file_is_binary(entry.path) then lines = { "Preview is not supported for binary files." } elseif tonumber(self.limit_b)>0 and fs_stat.size>self.limit_b then lines = { ("Preview file size limit (>%dMB) reached, file size %dMB.") :format(self.limit_b/(1024*1024), fs_stat.size/(1024*1024)), -- "(configured via 'previewers.builtin.limit_b')" } end if lines then vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, lines) -- swap preview buffer with new one self:set_preview_buf(tmpbuf) self:preview_buf_post(entry) return end end -- read the file into the buffer utils.read_file_async(entry.path, vim.schedule_wrap(function(data) 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 vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, lines) -- swap preview buffer with new one self:set_preview_buf(tmpbuf) self:preview_buf_post(entry) end)) end end function Previewer.buffer_or_file:do_syntax(entry) if not self.preview_bufnr then return end if not entry or not entry.path then return end local bufnr = self.preview_bufnr local preview_winid = self.win.preview_winid if api.nvim_buf_is_valid(bufnr) 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 if entry.filetype == 'help' then -- if entry.filetype and #entry.filetype>0 then -- filetype was saved from a loaded buffer -- this helps avoid losing highlights for help buffers -- which are '.txt' files with 'ft=help' -- api.nvim_buf_set_option(bufnr, 'filetype', entry.filetype) pcall(api.nvim_buf_set_option, bufnr, 'filetype', entry.filetype) else -- 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) end -- nvim_buf_call has less side-effects than window switch local ok, _ = pcall(api.nvim_buf_call, bufnr, function() vim.cmd('filetype detect') end) if not ok then utils.warn(("syntax highlighting failed for filetype '%s', ") :format(entry.path and path.extension(entry.path) or "") .. "open the file and run ':filetype detect' for more info.") end end end end end function Previewer.buffer_or_file:set_cursor_hl(entry) pcall(vim.api.nvim_win_call, self.win.preview_winid, function() 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 self.win.winopts.__hl.cursor and not (lnum<=1 and col<=1) then fn.matchaddpos(self.win.winopts.__hl.cursor, {{lnum, math.max(1, col)}}, 11) end end) end function Previewer.buffer_or_file:update_border(entry) if self.title then if entry.path and self.opts.cwd then entry.path = path.relative(entry.path, self.opts.cwd) end entry.path = path.HOME_to_tilde(entry.path) local title = (' %s '):format(entry.path or entry.uri) 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 self.win:update_scrollbar() end function Previewer.buffer_or_file:preview_buf_post(entry) if not self.win or not self.win:validate_preview() then return end if not self:preview_is_terminal() then -- set cursor highlights for line|col or tag self:set_cursor_hl(entry) -- syntax highlighting if self.syntax then if self.syntax_delay > 0 then vim.defer_fn(function() self:do_syntax(entry) end, self.syntax_delay) else self:do_syntax(entry) end end end self:update_border(entry) -- save the loaded entry so we can compare -- bufnr|path with the next entry, if equal -- we can skip loading the buffer again self.loaded_entry = entry end Previewer.help_tags = Previewer.base:extend() function Previewer.help_tags:should_clear_preview(_) return false end function Previewer.help_tags:new(o, opts, fzf_win) Previewer.help_tags.super.new(self, o, opts, fzf_win) self.split = o.split self.help_cmd = o.help_cmd or "help" self.filetype = "help" -- do not unload preview buffer -- it's our dedicated ':help' buffer self.do_not_unload = true self:init_help_win() return self end function Previewer.help_tags:gen_winopts() local winopts = { wrap = self.win.preview_wrap, number = false } return vim.tbl_extend("keep", winopts, self.winopts) end function Previewer.help_tags:exec_cmd(str) str = str or '' vim.cmd(("noauto %s %s %s"):format(self.split, self.help_cmd, str)) end function Previewer.help_tags:parse_entry(entry_str) return entry_str:match("[^%s]+") end local function curtab_helpbuf() for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do local bufnr = vim.api.nvim_win_get_buf(w) local bufinfo = vim.fn.getbufinfo(bufnr)[1] if bufinfo.variables and bufinfo.variables.current_syntax == 'help' then return bufnr, w end end return nil, nil 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.help_bufnr, self.help_winid = curtab_helpbuf() -- do not open a new 'help' window -- if one already exists if not self.help_bufnr then 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 end function Previewer.help_tags:populate_preview_buf(entry_str) local entry = self:parse_entry(entry_str) pcall(vim.api.nvim_win_call, self.help_winid, function() self.prev_help_bufnr = api.nvim_get_current_buf() self:exec_cmd(entry) vim.api.nvim_buf_set_option(0, 'filetype', self.filetype) 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) self.win:update_scrollbar() 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 self.help_winid and vim.api.nvim_win_is_valid(self.help_winid) then api.nvim_win_close(self.help_winid, true) end if self.help_bufnr and vim.api.nvim_buf_is_valid(self.help_bufnr) then vim.api.nvim_buf_delete(self.help_bufnr, {force=true}) end if self.prev_help_bufnr and 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 Previewer.help_file = Previewer.buffer_or_file:extend() function Previewer.help_file:new(o, opts, fzf_win) Previewer.help_file.super.new(self, o, opts, fzf_win) return self end function Previewer.help_file:parse_entry(entry_str) local tag, filename = entry_str:match("(.*)%s+(.*)$") return { htag = tag, hregex = ([[\V*%s*]]):format(tag:gsub([[\]], [[\\]])), path = filename, filetype = 'help', } end function Previewer.help_file:gen_winopts() local winopts = { wrap = self.win.preview_wrap, number = false } return vim.tbl_extend("keep", winopts, self.winopts) end function Previewer.help_file:set_cursor_hl(entry) pcall(api.nvim_win_call, self.win.preview_winid, function() -- start searching at line 1 in case we -- didn't reload the buffer (same file) api.nvim_win_set_cursor(0, {1, 0}) fn.clearmatches() fn.search(entry.hregex, "W") if self.win.winopts.__hl.search then fn.matchadd(self.win.winopts.__hl.search, entry.hregex) end self.orig_pos = api.nvim_win_get_cursor(0) utils.zz() end) end Previewer.man_pages = Previewer.base:extend() function Previewer.man_pages:should_clear_preview(_) return false end function Previewer.man_pages:gen_winopts() local winopts = { wrap = self.win.preview_wrap, number = false } return vim.tbl_extend("keep", winopts, self.winopts) end function Previewer.man_pages:new(o, opts, fzf_win) Previewer.man_pages.super.new(self, o, opts, fzf_win) self.filetype = "man" self.cmd = o.cmd or "man -c %s | col -bx" 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.man_pages:populate_preview_buf(entry_str) local entry = self:parse_entry(entry_str) local cmd = self.cmd:format(entry) if type(cmd) == 'string' then cmd = {"sh", "-c", cmd} end local output, _ = utils.io_systemlist(cmd) local tmpbuf = self:get_tmp_buffer() -- vim.api.nvim_buf_set_option(tmpbuf, 'modifiable', true) vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, output) vim.api.nvim_buf_set_option(tmpbuf, 'filetype', self.filetype) self:set_preview_buf(tmpbuf) self.win:update_scrollbar() end Previewer.marks = Previewer.buffer_or_file:extend() function Previewer.marks:new(o, opts, fzf_win) Previewer.marks.super.new(self, o, opts, fzf_win) 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 Previewer.jumps = Previewer.buffer_or_file:extend() function Previewer.jumps:new(o, opts, fzf_win) Previewer.jumps.super.new(self, o, opts, fzf_win) return self end function Previewer.jumps:parse_entry(entry_str) local bufnr = nil local _, lnum, col, filepath = entry_str:match("(%d+)%s+(%d+)%s+(%d+)%s+(.*)") if filepath then local ok, res = pcall(vim.fn.expand, filepath) if ok then filepath = path.relative(res, vim.loop.cwd()) end if not vim.loop.fs_stat(filepath) then -- file is not accessible, -- text is a string from current buffer bufnr = self.win.src_bufnr filepath = vim.api.nvim_buf_get_name(self.win.src_bufnr) end end return { bufnr = bufnr, path = filepath, line = tonumber(lnum) or 1, col = tonumber(col)+1 or 1, } end Previewer.tags = Previewer.buffer_or_file:extend() function Previewer.tags:new(o, opts, fzf_win) Previewer.tags.super.new(self, o, opts, fzf_win) return self end function Previewer.tags:parse_entry(entry_str) -- first parse as normal entry -- must use 'super.' and send self as 1st arg -- or the ':' syntactic suger will send super's -- self which doesn't have self.opts local entry = self.super.parse_entry(self, entry_str) entry.ctag = path.entry_to_ctag(entry_str) return entry end function Previewer.tags:set_cursor_hl(entry) -- pcall(vim.fn.clearmatches, self.win.preview_winid) pcall(api.nvim_win_call, self.win.preview_winid, function() -- start searching at line 1 in case we -- didn't reload the buffer (same file) api.nvim_win_set_cursor(0, {1, 0}) fn.clearmatches() fn.search(entry.ctag, "W") if self.win.winopts.__hl.search then fn.matchadd(self.win.winopts.__hl.search, entry.ctag) end self.orig_pos = api.nvim_win_get_cursor(0) utils.zz() end) end Previewer.highlights = Previewer.base:extend() function Previewer.highlights:should_clear_preview(_) return false end function Previewer.highlights:gen_winopts() local winopts = { wrap = self.win.preview_wrap, number = false } return vim.tbl_extend("keep", winopts, self.winopts) end function Previewer.highlights:new(o, opts, fzf_win) Previewer.highlights.super.new(self, o, opts, fzf_win) self.ns_previewer = vim.api.nvim_create_namespace("fzf-lua.previewer.hl") return self end function Previewer.highlights:close() Previewer.highlights.super.close(self) self.tmpbuf = nil end function Previewer.highlights:populate_preview_buf(entry_str) if not self.tmpbuf then local output = vim.split(vim.fn.execute "highlight", "\n") local hl_groups = {} for _, v in ipairs(output) do if v ~= "" then if v:sub(1, 1) == " " then local part_of_old = v:match "%s+(.*)" hl_groups[#hl_groups] = hl_groups[#hl_groups] .. part_of_old else table.insert(hl_groups, v) end end end self.tmpbuf = self:get_tmp_buffer() vim.api.nvim_buf_set_lines(self.tmpbuf, 0, -1, false, hl_groups) for k, v in ipairs(hl_groups) do local startPos = string.find(v, "xxx", 1, true) - 1 local endPos = startPos + 3 local hlgroup = string.match(v, "([^ ]*)%s+.*") pcall(vim.api.nvim_buf_add_highlight, self.tmpbuf, 0, hlgroup, k - 1, startPos, endPos) end self:set_preview_buf(self.tmpbuf) end local selected_hl = "^" .. utils.strip_ansi_coloring(entry_str) .. "\\>" pcall(vim.api.nvim_buf_clear_namespace, self.tmpbuf, self.ns_previewer, 0, -1) pcall(api.nvim_win_call, self.win.preview_winid, function() -- start searching at line 1 in case we -- didn't reload the buffer (same file) api.nvim_win_set_cursor(0, {1, 0}) fn.clearmatches() fn.search(selected_hl, "W") if self.win.winopts.__hl.search then fn.matchadd(self.win.winopts.__hl.search, selected_hl) end self.orig_pos = api.nvim_win_get_cursor(0) utils.zz() end) self.win:update_scrollbar() end return Previewer