From 2768d5e9214dbf0531ee3fc20333d6d9b4a40ee5 Mon Sep 17 00:00:00 2001 From: James Trew Date: Sat, 24 Feb 2024 19:55:41 -0500 Subject: [PATCH] feat(previewer): show unwritten buffer previewers for some pickers Use actual buffer contents for buffer previewers for some pickers (buffers, treesitter, current_buffer_fuzzy_find, for starters). Not to be confused with reusing buffers for preview buffers. This lets users view previews of buffers with unwritten changes. For treesitter and current_buffer_fuzzy_find, not showing actual buffer content is arguably a bug considering the finder operates over the buffer (written or not). So there are instances where you can search over unwritten content in the buffer only to see an outdated, written only content in the preview. --- lua/telescope/make_entry.lua | 2 + lua/telescope/previewers/buffer_previewer.lua | 176 +++++++++++++----- 2 files changed, 131 insertions(+), 47 deletions(-) diff --git a/lua/telescope/make_entry.lua b/lua/telescope/make_entry.lua index a1806f3..62bbd86 100644 --- a/lua/telescope/make_entry.lua +++ b/lua/telescope/make_entry.lua @@ -708,6 +708,7 @@ function make_entry.gen_from_treesitter(opts) node_text = node_text, filename = get_filename(bufnr), + bufnr = opts.bufnr, -- need to add one since the previewer substacts one lnum = start_row + 1, col = start_col, @@ -971,6 +972,7 @@ function make_entry.gen_from_buffer_lines(opts) ordinal = entry.text, display = make_display, filename = entry.filename, + bufnr = opts.bufnr, lnum = entry.lnum, text = entry.text, }, opts) diff --git a/lua/telescope/previewers/buffer_previewer.lua b/lua/telescope/previewers/buffer_previewer.lua index 72854e4..0e08231 100644 --- a/lua/telescope/previewers/buffer_previewer.lua +++ b/lua/telescope/previewers/buffer_previewer.lua @@ -56,11 +56,15 @@ local function defaulter(f, default_opts) } end --- modified vim.split to incorporate a timer -local function split(s, sep, plain, opts) +--- modified vim.gsplit to incorporate a timer +---@param s string +---@param sep string +---@param opts table +---@return string[]? +local function split(s, sep, opts) opts = opts or {} local t = {} - for c in vim.gsplit(s, sep, plain) do + for c in vim.gsplit(s, sep) do local line = opts.file_encoding and vim.iconv(c, opts.file_encoding, "utf8") or c table.insert(t, line) if opts.preview.timeout then @@ -72,7 +76,10 @@ local function split(s, sep, plain, opts) end return t end -local bytes_to_megabytes = math.pow(1024, 2) + +local bytes_to_megabytes = function(bytes) + return bytes / math.pow(1024, 2) +end local color_hash = { ["p"] = "TelescopePreviewPipe", @@ -157,22 +164,43 @@ local handle_directory_preview = function(filepath, bufnr, opts) }) end +local function ft_known(ft) + return ft ~= nil and ft ~= "" +end + +---@param mb_filesize number +---@param filepath string +---@param bufnr number preview bufnr +---@param opts table +---@return boolean # whether to abort preview generation +local function filesize_ok(mb_filesize, filepath, bufnr, opts) + if opts.preview.filesize_limit then + if mb_filesize > opts.preview.filesize_limit then + if type(opts.preview.filesize_hook) == "function" then + opts.preview.filesize_hook(filepath, bufnr, opts) + else + putils.set_preview_message(bufnr, opts.winid, "File exceeds preview size limit", opts.preview.msg_bg_fillchar) + end + return false + end + end + return true +end + local handle_file_preview = function(filepath, bufnr, stat, opts) vim.schedule(function() opts.ft = opts.use_ft_detect and putils.filetype_detect(filepath) - local possible_binary = false - if type(opts.preview.filetype_hook) == "function" and opts.ft ~= nil and opts.ft ~= "" then - if not opts.preview.filetype_hook(filepath, bufnr, opts) then - return - end + if not opts.preview.filetype_hook(filepath, bufnr, opts) then + return end - if opts.preview.check_mime_type == true and has_file and (opts.ft == nil or opts.ft == "") then + + local possible_binary = false + if opts.preview.check_mime_type == true and has_file and not ft_known(opts.ft) then -- avoid SIGABRT in buffer previewer happening with utils.get_os_command_output local mime_type = capture(string.format([[file --mime-type -b "%s"]], filepath)) if putils.binary_mime_type(mime_type) then if type(opts.preview.mime_hook) == "function" then opts.preview.mime_hook(filepath, bufnr, opts) - return else possible_binary = true end @@ -182,16 +210,9 @@ local handle_file_preview = function(filepath, bufnr, stat, opts) end end - local mb_filesize = stat.size / bytes_to_megabytes - if opts.preview.filesize_limit then - if mb_filesize > opts.preview.filesize_limit then - if type(opts.preview.filesize_hook) == "function" then - opts.preview.filesize_hook(filepath, bufnr, opts) - else - putils.set_preview_message(bufnr, opts.winid, "File exceeds preview size limit", opts.preview.msg_bg_fillchar) - end - return - end + local mb_filesize = bytes_to_megabytes(stat.size) + if not filesize_ok(mb_filesize, filepath, bufnr, opts) then + return end opts.start_time = vim.loop.hrtime() @@ -199,7 +220,7 @@ local handle_file_preview = function(filepath, bufnr, stat, opts) if not vim.api.nvim_buf_is_valid(bufnr) then return end - local processed_data = split(data, "[\r]?\n", nil, opts) + local processed_data = split(data, "[\r]?\n", opts) if processed_data then local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, processed_data) @@ -208,7 +229,7 @@ local handle_file_preview = function(filepath, bufnr, stat, opts) end -- last resort, if ft is still empty at this point in time, -- we need to determine the filetype using the buffer contents - if opts.ft == nil or opts.ft == "" then + if not ft_known(opts.ft) then opts.ft = vim.filetype.match { filename = filepath, buf = bufnr } end -- we need to attempt to call filetype hook at this point "again" @@ -220,7 +241,7 @@ local handle_file_preview = function(filepath, bufnr, stat, opts) end end -- if we still dont have a ft we need to display the binary message - if (opts.ft == nil or opts.ft == "") and possible_binary then + if not ft_known(opts.ft) and possible_binary then putils.set_preview_message(bufnr, opts.winid, "Binary cannot be previewed", opts.preview.msg_bg_fillchar) return end @@ -244,22 +265,81 @@ local handle_file_preview = function(filepath, bufnr, stat, opts) end) end -local PREVIEW_TIMEOUT_MS = 250 -local PREVIEW_FILESIZE_MB = 25 -local PREVIEW_HIGHLIGHT_MB = 1 +--- create preview from buffer contents +--- this allows previewing unwritten changes to buffers +---@param filepath string +---@param bufnr integer preview bufnr +---@param opts table +local handle_buffer_preview = function(filepath, bufnr, opts) + vim.schedule(function() + opts.ft = vim.bo[opts.entry_bufnr].ft + if type(opts.preview.filetype_hook) == "function" then + if not opts.preview.filetype_hook(filepath, bufnr, opts) then + return + end + end + + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end + + local start_time = vim.loop.hrtime() + + local lines = vim.api.nvim_buf_get_lines(opts.entry_bufnr, 0, -1, false) + local contents = table.concat(lines, "\n") + local mb_bufsize = bytes_to_megabytes(#contents) + if not filesize_ok(mb_bufsize, filepath, bufnr, opts) then + return + end + + local end_time = vim.loop.hrtime() + if (end_time - start_time) / 1e6 > opts.preview.timeout then + if type(opts.preview.timeout_hook) == "function" then + opts.preview.timeout_hook(filepath, bufnr, opts) + else + putils.set_preview_message(bufnr, opts.winid, "Previewer timed out", opts.preview.msg_bg_fillchar) + end + return + end + + local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines) + if not ok then + return + end + + if opts.callback then + opts.callback(bufnr) + end + + if not (opts.preview.highlight_limit and mb_bufsize > opts.preview.highlight_limit) then + putils.highlighter(bufnr, opts.ft, opts) + end + end) +end previewers.file_maker = function(filepath, bufnr, opts) opts = vim.F.if_nil(opts, {}) opts.preview = vim.F.if_nil(opts.preview, {}) - opts.preview.timeout = vim.F.if_nil(opts.preview.timeout, PREVIEW_TIMEOUT_MS) - opts.preview.filesize_limit = vim.F.if_nil(opts.preview.filesize_limit, PREVIEW_FILESIZE_MB) - opts.preview.highlight_limit = vim.F.if_nil(opts.preview.highlight_limit, PREVIEW_HIGHLIGHT_MB) - opts.preview.msg_bg_fillchar = vim.F.if_nil(opts.preview.msg_bg_fillchar, "╱") - opts.preview.treesitter = vim.F.if_nil(opts.preview.treesitter, true) - if opts.use_ft_detect == nil then - opts.use_ft_detect = true - end - if opts.bufname ~= filepath then + opts.preview.timeout = vim.F.if_nil(opts.preview.timeout, conf.preview.timeout) + opts.preview.filesize_limit = vim.F.if_nil(opts.preview.filesize_limit, conf.preview.filesize_limit) + opts.preview.highlight_limit = vim.F.if_nil(opts.preview.highlight_limit, conf.preview.highlight_limit) + opts.preview.msg_bg_fillchar = vim.F.if_nil(opts.preview.msg_bg_fillchar, conf.preview.msg_bg_fillchar) + opts.preview.treesitter = vim.F.if_nil(opts.preview.treesitter, conf.preview.treesitter) + opts.use_ft_detect = vim.F.if_nil(opts.use_ft_detect, true) + + vim.validate { + filetype_hook = { opts.preview.filetype_hook, "f", true }, + mime_hook = { opts.preview.mime_hook, "f", true }, + filesize_hook = { opts.preview.filesize_hook, "f", true }, + timeout_hook = { opts.preview.timeout_hook, "f", true }, + } + opts.preview.filetype_hook = vim.F.if_nil(opts.preview.filetype_hook, function() + return true + end) + + if opts.entry_bufnr and ft_known(vim.bo[opts.entry_bufnr]) then + handle_buffer_preview(vim.api.nvim_buf_get_name(opts.entry_bufnr), bufnr, opts) + elseif opts.bufname ~= filepath then if not vim.in_fast_event() then filepath = utils.path_expand(filepath) end @@ -563,20 +643,21 @@ previewers.vimgrep = defaulter(function(opts) define_preview = function(self, entry) -- builtin.buffers: bypass path validation for terminal buffers that don't have appropriate path - local has_buftype = entry.bufnr - and vim.api.nvim_buf_is_valid(entry.bufnr) - and vim.api.nvim_buf_get_option(entry.bufnr, "buftype") ~= "" - or false - local p - if not has_buftype then - p = from_entry.path(entry, true, false) - if p == nil or p == "" then + local valid_buf = entry.bufnr and vim.api.nvim_buf_is_valid(entry.bufnr) + local has_buftype = valid_buf and vim.api.nvim_buf_get_option(entry.bufnr, "buftype") ~= "" + + local path + if valid_buf then + path = vim.api.nvim_buf_get_name(entry.bufnr) + else + path = from_entry.path(entry, true) + if path == nil or path == "" then return end end - -- Workaround for unnamed buffer when using builtin.buffer - if entry.bufnr and (p == "[No Name]" or has_buftype) then + -- Workaround for unnamed or terminal buffer when using builtin.buffer + if valid_buf and (path == "[No Name]" or has_buftype) then local lines = vim.api.nvim_buf_get_lines(entry.bufnr, 0, -1, false) vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, lines) -- schedule so that the lines are actually there and can be jumped onto when we call jump_to_line @@ -584,7 +665,7 @@ previewers.vimgrep = defaulter(function(opts) jump_to_line(self, self.state.bufnr, entry) end) else - conf.buffer_previewer_maker(p, self.state.bufnr, { + conf.buffer_previewer_maker(path, self.state.bufnr, { bufname = self.state.bufname, winid = self.state.winid, preview = opts.preview, @@ -592,6 +673,7 @@ previewers.vimgrep = defaulter(function(opts) jump_to_line(self, bufnr, entry) end, file_encoding = opts.file_encoding, + entry_bufnr = (valid_buf and not has_buftype) and entry.bufnr or nil, }) end end,