feat: skip/timeout preview if file cannot be easily previewed (#1231)

* For full configuration, see `:h telescope.defaults.preview`
* Unblocks previewer on binaries, too large files, and files that take too long to read
* Allows toggling treesitter highlighting for buffer_previewer
* Allows to globally opt out of previewer
This commit is contained in:
fdschmidt93
2021-09-16 23:01:40 +02:00
committed by GitHub
parent ac03f495c6
commit 7c5b846f6f
4 changed files with 253 additions and 11 deletions

View File

@@ -279,6 +279,48 @@ telescope.setup({opts}) *telescope.setup()*
Default: 1000 Default: 1000
*telescope.defaults.preview*
preview: ~
This field handles the global configuration for previewers.
By default it is a table, with default values (more below).
To disable previewing, set it to false. If you have disabled previewers
globally, but want to opt in to previewing for single pickers, you will have to
pass `preview = true` or `preview = {...}` (your config) to the `opts` of
your picker.
Fields:
- check_mime_type: Use `file` if available to try to infer whether the
file to preview is a binary if plenary's
filetype detection fails.
Windows users get `file` from:
https://github.com/julian-r/file-windows
Set to false to attempt to preview any mime type.
Default: true
- filesize_limit: The maximum file size in MB attempted to be previewed.
Set to false to attempt to preview any file size.
Default: 25
- timeout: Timeout the previewer if the preview did not
complete within `timeout` milliseconds.
Set to false to not timeout preview.
Default: 250
- hook(s): Function(s) that takes `(filepath, bufnr, opts)`
to be run if the buffer previewer was not shown due to
the respective test.
Available hooks are: {mime, filesize, timeout}_hook, e.g.
preview = {
mime_hook = function(filepath, bufnr, opts) ... end
}
See `telescope/previewers/*.lua` for relevant examples.
Default: nil
- treesitter: Determines whether the previewer performs treesitter
highlighting, which falls back to regex-based highlighting.
`true`: treesitter highlighting for all available filetypes
`false`: regex-based highlighting for all filetypes
`table`: table of filetypes for which to attach treesitter
highlighting
Default: true
*telescope.defaults.vimgrep_arguments* *telescope.defaults.vimgrep_arguments*
vimgrep_arguments: ~ vimgrep_arguments: ~
Defines the command that will be used for `live_grep` and `grep_string` Defines the command that will be used for `live_grep` and `grep_string`

View File

@@ -382,6 +382,56 @@ append(
]] ]]
) )
append(
"preview",
{
check_mime_type = true,
filesize_limit = 25,
timeout = 250,
treesitter = true,
},
[[
This field handles the global configuration for previewers.
By default it is a table, with default values (more below).
To disable previewing, set it to false. If you have disabled previewers
globally, but want to opt in to previewing for single pickers, you will have to
pass `preview = true` or `preview = {...}` (your config) to the `opts` of
your picker.
Fields:
- check_mime_type: Use `file` if available to try to infer whether the
file to preview is a binary if plenary's
filetype detection fails.
Windows users get `file` from:
https://github.com/julian-r/file-windows
Set to false to attempt to preview any mime type.
Default: true
- filesize_limit: The maximum file size in MB attempted to be previewed.
Set to false to attempt to preview any file size.
Default: 25
- timeout: Timeout the previewer if the preview did not
complete within `timeout` milliseconds.
Set to false to not timeout preview.
Default: 250
- hook(s): Function(s) that takes `(filepath, bufnr, opts)`
to be run if the buffer previewer was not shown due to
the respective test.
Available hooks are: {mime, filesize, timeout}_hook, e.g.
preview = {
mime_hook = function(filepath, bufnr, opts) ... end
}
See `telescope/previewers/*.lua` for relevant examples.
Default: nil
- treesitter: Determines whether the previewer performs treesitter
highlighting, which falls back to regex-based highlighting.
`true`: treesitter highlighting for all available filetypes
`false`: regex-based highlighting for all filetypes
`table`: table of filetypes for which to attach treesitter
highlighting
Default: true
]]
)
append( append(
"vimgrep_arguments", "vimgrep_arguments",
{ "rg", "--color=never", "--no-heading", "--with-filename", "--line-number", "--column", "--smart-case" }, { "rg", "--color=never", "--no-heading", "--with-filename", "--line-number", "--column", "--smart-case" },

View File

@@ -1,6 +1,7 @@
local from_entry = require "telescope.from_entry" local from_entry = require "telescope.from_entry"
local Path = require "plenary.path" local Path = require "plenary.path"
local utils = require "telescope.utils" local utils = require "telescope.utils"
local strings = require "plenary.strings"
local putils = require "telescope.previewers.utils" local putils = require "telescope.previewers.utils"
local Previewer = require "telescope.previewers.previewer" local Previewer = require "telescope.previewers.previewer"
local conf = require("telescope.config").values local conf = require("telescope.config").values
@@ -9,12 +10,99 @@ local pfiletype = require "plenary.filetype"
local pscan = require "plenary.scandir" local pscan = require "plenary.scandir"
local buf_delete = utils.buf_delete local buf_delete = utils.buf_delete
local defaulter = utils.make_default_callable
local previewers = {} local previewers = {}
local ns_previewer = vim.api.nvim_create_namespace "telescope.previewers" local ns_previewer = vim.api.nvim_create_namespace "telescope.previewers"
local has_file = 1 == vim.fn.executable "file"
-- TODO(fdschmidt93) switch to Job once file_maker callbacks get cleaned up with plenary async
-- avoids SIGABRT from utils.get_os_command_output due to vim.time in fs_stat cb
local function capture(cmd, raw)
local f = assert(io.popen(cmd, "r"))
local s = assert(f:read "*a")
f:close()
if raw then
return s
end
s = string.gsub(s, "^%s+", "")
s = string.gsub(s, "%s+$", "")
s = string.gsub(s, "[\n\r]+", " ")
return s
end
local function defaulter(f, default_opts)
default_opts = default_opts or {}
return {
new = function(opts)
if conf.preview == false and not opts.preview then
return false
end
opts.preview = type(opts.preview) ~= "table" and {} or opts.preview
if type(conf.preview) == "table" then
for k, v in pairs(conf.preview) do
opts.preview[k] = vim.F.if_nil(opts.preview[k], v)
end
end
return f(opts)
end,
__call = function()
local ok, err = pcall(f(default_opts))
if not ok then
error(debug.traceback(err))
end
end,
}
end
local function set_timeout_message(bufnr, winid, message)
local height = vim.api.nvim_win_get_height(winid)
local width = vim.api.nvim_win_get_width(winid)
vim.api.nvim_buf_set_lines(
bufnr,
0,
-1,
false,
utils.repeated_table(height, table.concat(utils.repeated_table(width, ""), ""))
)
local anon_ns = vim.api.nvim_create_namespace ""
local padding = table.concat(utils.repeated_table(#message + 4, " "), "")
local lines = {
padding,
" " .. message .. " ",
padding,
}
local col = math.floor((width - strings.strdisplaywidth(lines[2])) / 2)
for i, line in ipairs(lines) do
vim.api.nvim_buf_set_extmark(
bufnr,
anon_ns,
math.floor(height / 2) - 1 + i,
0,
{ virt_text = { { line, "Normal" } }, virt_text_pos = "overlay", virt_text_win_col = col }
)
end
end
-- modified vim.split to incorporate a timer
local function split(s, sep, plain, opts)
opts = opts or {}
local t = {}
for c in vim.gsplit(s, sep, plain) do
table.insert(t, c)
if opts.preview.timeout then
local diff_time = (vim.loop.hrtime() - opts.start_time) / 1e6
if diff_time > opts.preview.timeout then
return
end
end
end
return t
end
local bytes_to_megabytes = math.pow(1024, 2)
local color_hash = { local color_hash = {
["p"] = "TelescopePreviewPipe", ["p"] = "TelescopePreviewPipe",
["c"] = "TelescopePreviewCharDev", ["c"] = "TelescopePreviewCharDev",
@@ -96,11 +184,13 @@ end
previewers.file_maker = function(filepath, bufnr, opts) previewers.file_maker = function(filepath, bufnr, opts)
opts = opts or {} opts = opts or {}
opts.preview = opts.preview or {}
opts.preview.timeout = vim.F.if_nil(opts.preview.timeout, 250) -- in ms
opts.preview.filesize_limit = vim.F.if_nil(opts.preview.filesize_limit, 25) -- in mb
if opts.use_ft_detect == nil then if opts.use_ft_detect == nil then
opts.use_ft_detect = true opts.use_ft_detect = true
end end
local ft = opts.use_ft_detect and pfiletype.detect(filepath) local ft = opts.use_ft_detect and pfiletype.detect(filepath)
if opts.bufname ~= filepath then if opts.bufname ~= filepath then
if not vim.in_fast_event() then if not vim.in_fast_event() then
filepath = vim.fn.expand(filepath) filepath = vim.fn.expand(filepath)
@@ -122,11 +212,45 @@ previewers.file_maker = function(filepath, bufnr, opts)
end), end),
}) })
else else
if opts.preview.check_mime_type == true and has_file and 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, "/")[1]
if mime_type ~= "text" and mime_type ~= "inode" then
if type(opts.preview.mime_hook) == "function" then
opts.preview.mime_hook(filepath, bufnr, opts)
else
vim.schedule(function()
set_timeout_message(bufnr, opts.winid, "Binary cannot be previewed")
end)
end
return
end
end
if opts.preview.filesize_limit then
local mb_filesize = math.floor(stat.size / bytes_to_megabytes)
if mb_filesize > opts.preview.filesize_limit then
if type(opts.preview.filesize_hook) == "function" then
opts.preview.filesize_hook(filepath, bufnr, opts)
else
vim.schedule(function()
set_timeout_message(bufnr, opts.winid, "File exceeds preview size limit")
end)
return
end
end
end
opts.start_time = vim.loop.hrtime()
Path:new(filepath):_read_async(vim.schedule_wrap(function(data) Path:new(filepath):_read_async(vim.schedule_wrap(function(data)
if not vim.api.nvim_buf_is_valid(bufnr) then if not vim.api.nvim_buf_is_valid(bufnr) then
return return
end end
local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, vim.split(data, "[\r]?\n")) 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)
if not ok then if not ok then
return return
end end
@@ -134,7 +258,15 @@ previewers.file_maker = function(filepath, bufnr, opts)
if opts.callback then if opts.callback then
opts.callback(bufnr) opts.callback(bufnr)
end end
putils.highlighter(bufnr, ft) putils.highlighter(bufnr, ft, opts)
else
if type(opts.preview.timeout_hook) == "function" then
opts.preview.timeout_hook(filepath, bufnr, opts)
else
set_timeout_message(bufnr, opts.winid, "Previewer timed out")
return
end
end
end)) end))
end end
end) end)
@@ -323,6 +455,8 @@ previewers.cat = defaulter(function(opts)
end end
conf.buffer_previewer_maker(p, self.state.bufnr, { conf.buffer_previewer_maker(p, self.state.bufnr, {
bufname = self.state.bufname, bufname = self.state.bufname,
winid = self.state.winid,
preview = opts.preview,
}) })
end, end,
} }
@@ -376,6 +510,8 @@ previewers.vimgrep = defaulter(function(opts)
conf.buffer_previewer_maker(p, self.state.bufnr, { conf.buffer_previewer_maker(p, self.state.bufnr, {
bufname = self.state.bufname, bufname = self.state.bufname,
winid = self.state.winid,
preview = opts.preview,
callback = function(bufnr) callback = function(bufnr)
jump_to_line(self, bufnr, entry.lnum) jump_to_line(self, bufnr, entry.lnum)
end, end,
@@ -432,6 +568,7 @@ previewers.ctags = defaulter(function(_)
define_preview = function(self, entry, status) define_preview = function(self, entry, status)
conf.buffer_previewer_maker(entry.filename, self.state.bufnr, { conf.buffer_previewer_maker(entry.filename, self.state.bufnr, {
bufname = self.state.bufname, bufname = self.state.bufname,
winid = self.state.winid,
callback = function(bufnr) callback = function(bufnr)
vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_buf_call(bufnr, function()
determine_jump(entry)(self, bufnr) determine_jump(entry)(self, bufnr)
@@ -462,6 +599,7 @@ previewers.builtin = defaulter(function(_)
conf.buffer_previewer_maker(entry.filename, self.state.bufnr, { conf.buffer_previewer_maker(entry.filename, self.state.bufnr, {
bufname = self.state.bufname, bufname = self.state.bufname,
winid = self.state.winid,
callback = function(bufnr) callback = function(bufnr)
search_cb_jump(self, bufnr, text) search_cb_jump(self, bufnr, text)
end, end,
@@ -486,6 +624,7 @@ previewers.help = defaulter(function(_)
conf.buffer_previewer_maker(entry.filename, self.state.bufnr, { conf.buffer_previewer_maker(entry.filename, self.state.bufnr, {
bufname = self.state.bufname, bufname = self.state.bufname,
winid = self.state.winid,
callback = function(bufnr) callback = function(bufnr)
putils.regex_highlighter(bufnr, "help") putils.regex_highlighter(bufnr, "help")
search_cb_jump(self, bufnr, query) search_cb_jump(self, bufnr, query)
@@ -729,6 +868,7 @@ previewers.git_file_diff = defaulter(function(opts)
end end
conf.buffer_previewer_maker(p, self.state.bufnr, { conf.buffer_previewer_maker(p, self.state.bufnr, {
bufname = self.state.bufname, bufname = self.state.bufname,
winid = self.state.winid,
}) })
else else
putils.job_maker({ "git", "--no-pager", "diff", entry.value }, self.state.bufnr, { putils.job_maker({ "git", "--no-pager", "diff", entry.value }, self.state.bufnr, {

View File

@@ -63,8 +63,18 @@ local function has_filetype(ft)
end end
--- Attach default highlighter which will choose between regex and ts --- Attach default highlighter which will choose between regex and ts
utils.highlighter = function(bufnr, ft) utils.highlighter = function(bufnr, ft, opts)
if not (utils.ts_highlighter(bufnr, ft)) then opts = opts or {}
opts.preview = opts.preview or {}
opts.preview.treesitter = vim.F.if_nil(opts.preview.treesitter, true)
local ts_highlighting = opts.preview.treesitter == true
or type(opts.preview.treesitter) == "table" and vim.tbl_contains(opts.preview.treesitter, ft)
local ts_success
if ts_highlighting then
ts_success = utils.ts_highlighter(bufnr, ft)
end
if not (ts_highlighting or ts_success) then
utils.regex_highlighter(bufnr, ft) utils.regex_highlighter(bufnr, ft)
end end
end end