Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(previewer): show unwritten buffer previewers for some pickers #2946

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lua/telescope/make_entry.lua
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,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,
Expand Down Expand Up @@ -957,6 +958,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)
Expand Down
175 changes: 128 additions & 47 deletions lua/telescope/previewers/buffer_previewer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -72,7 +76,9 @@ 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",
Expand Down Expand Up @@ -157,23 +163,44 @@ 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 output = capture(string.format([[file --mime-type -b "%s"]], filepath))
local mime_type = vim.split(output, "/")
if mime_type[1] ~= "text" and mime_type[1] ~= "inode" and mime_type[2] ~= "json" then
if type(opts.preview.mime_hook) == "function" then
opts.preview.mime_hook(filepath, bufnr, opts)
return
else
possible_binary = true
end
Expand All @@ -183,24 +210,17 @@ 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()
Path:new(filepath):_read_async(vim.schedule_wrap(function(data)
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)
Expand All @@ -209,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"
Expand All @@ -221,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
Expand All @@ -245,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 = vim.fn.expand(filepath)
end
Expand Down Expand Up @@ -563,35 +642,37 @@ 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)
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
vim.schedule(function()
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,
callback = function(bufnr)
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,
Expand Down
Loading