summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfdschmidt93 <39233597+fdschmidt93@users.noreply.github.com>2021-09-16 23:01:40 +0200
committerGitHub <noreply@github.com>2021-09-16 23:01:40 +0200
commit7c5b846f6f8caa8acf4b63dc4c33a801e2ea78a0 (patch)
tree128ca47803814e4ba35c629ac0e194325dbfc830
parentac03f495c6ec1f832488556969f72677b69ef33d (diff)
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
-rw-r--r--doc/telescope.txt42
-rw-r--r--lua/telescope/config.lua50
-rw-r--r--lua/telescope/previewers/buffer_previewer.lua158
-rw-r--r--lua/telescope/previewers/utils.lua14
4 files changed, 253 insertions, 11 deletions
diff --git a/doc/telescope.txt b/doc/telescope.txt
index 36e454c..a795b65 100644
--- a/doc/telescope.txt
+++ b/doc/telescope.txt
@@ -279,6 +279,48 @@ telescope.setup({opts}) *telescope.setup()*
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*
vimgrep_arguments: ~
Defines the command that will be used for `live_grep` and `grep_string`
diff --git a/lua/telescope/config.lua b/lua/telescope/config.lua
index 406d3c6..5fa0cf1 100644
--- a/lua/telescope/config.lua
+++ b/lua/telescope/config.lua
@@ -383,6 +383,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(
"vimgrep_arguments",
{ "rg", "--color=never", "--no-heading", "--with-filename", "--line-number", "--column", "--smart-case" },
[[
diff --git a/lua/telescope/previewers/buffer_previewer.lua b/lua/telescope/previewers/buffer_previewer.lua
index a4da833..ef64377 100644
--- a/lua/telescope/previewers/buffer_previewer.lua
+++ b/lua/telescope/previewers/buffer_previewer.lua
@@ -1,6 +1,7 @@
local from_entry = require "telescope.from_entry"
local Path = require "plenary.path"
local utils = require "telescope.utils"
+local strings = require "plenary.strings"
local putils = require "telescope.previewers.utils"
local Previewer = require "telescope.previewers.previewer"
local conf = require("telescope.config").values
@@ -9,12 +10,99 @@ local pfiletype = require "plenary.filetype"
local pscan = require "plenary.scandir"
local buf_delete = utils.buf_delete
-local defaulter = utils.make_default_callable
local 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 = {
["p"] = "TelescopePreviewPipe",
["c"] = "TelescopePreviewCharDev",
@@ -96,11 +184,13 @@ end
previewers.file_maker = function(filepath, bufnr, opts)
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
opts.use_ft_detect = true
end
local ft = opts.use_ft_detect and pfiletype.detect(filepath)
-
if opts.bufname ~= filepath then
if not vim.in_fast_event() then
filepath = vim.fn.expand(filepath)
@@ -122,19 +212,61 @@ previewers.file_maker = function(filepath, bufnr, opts)
end),
})
else
- Path:new(filepath):_read_async(vim.schedule_wrap(function(data)
- if not vim.api.nvim_buf_is_valid(bufnr) then
+ 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
- local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, vim.split(data, "[\r]?\n"))
- if not ok then
+ 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)
+ if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
+ 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
+ return
+ end
- if opts.callback then
- opts.callback(bufnr)
+ if opts.callback then
+ opts.callback(bufnr)
+ end
+ 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
- putils.highlighter(bufnr, ft)
end))
end
end)
@@ -323,6 +455,8 @@ previewers.cat = defaulter(function(opts)
end
conf.buffer_previewer_maker(p, self.state.bufnr, {
bufname = self.state.bufname,
+ winid = self.state.winid,
+ preview = opts.preview,
})
end,
}
@@ -376,6 +510,8 @@ previewers.vimgrep = defaulter(function(opts)
conf.buffer_previewer_maker(p, self.state.bufnr, {
bufname = self.state.bufname,
+ winid = self.state.winid,
+ preview = opts.preview,
callback = function(bufnr)
jump_to_line(self, bufnr, entry.lnum)
end,
@@ -432,6 +568,7 @@ previewers.ctags = defaulter(function(_)
define_preview = function(self, entry, status)
conf.buffer_previewer_maker(entry.filename, self.state.bufnr, {
bufname = self.state.bufname,
+ winid = self.state.winid,
callback = function(bufnr)
vim.api.nvim_buf_call(bufnr, function()
determine_jump(entry)(self, bufnr)
@@ -462,6 +599,7 @@ previewers.builtin = defaulter(function(_)
conf.buffer_previewer_maker(entry.filename, self.state.bufnr, {
bufname = self.state.bufname,
+ winid = self.state.winid,
callback = function(bufnr)
search_cb_jump(self, bufnr, text)
end,
@@ -486,6 +624,7 @@ previewers.help = defaulter(function(_)
conf.buffer_previewer_maker(entry.filename, self.state.bufnr, {
bufname = self.state.bufname,
+ winid = self.state.winid,
callback = function(bufnr)
putils.regex_highlighter(bufnr, "help")
search_cb_jump(self, bufnr, query)
@@ -729,6 +868,7 @@ previewers.git_file_diff = defaulter(function(opts)
end
conf.buffer_previewer_maker(p, self.state.bufnr, {
bufname = self.state.bufname,
+ winid = self.state.winid,
})
else
putils.job_maker({ "git", "--no-pager", "diff", entry.value }, self.state.bufnr, {
diff --git a/lua/telescope/previewers/utils.lua b/lua/telescope/previewers/utils.lua
index 54435fd..abb44e3 100644
--- a/lua/telescope/previewers/utils.lua
+++ b/lua/telescope/previewers/utils.lua
@@ -63,8 +63,18 @@ local function has_filetype(ft)
end
--- Attach default highlighter which will choose between regex and ts
-utils.highlighter = function(bufnr, ft)
- if not (utils.ts_highlighter(bufnr, ft)) then
+utils.highlighter = function(bufnr, ft, opts)
+ 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)
end
end