From 7c5b846f6f8caa8acf4b63dc4c33a801e2ea78a0 Mon Sep 17 00:00:00 2001 From: fdschmidt93 <39233597+fdschmidt93@users.noreply.github.com> Date: Thu, 16 Sep 2021 23:01:40 +0200 Subject: [PATCH] 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 --- doc/telescope.txt | 42 +++++ lua/telescope/config.lua | 50 ++++++ lua/telescope/previewers/buffer_previewer.lua | 158 +++++++++++++++++- lua/telescope/previewers/utils.lua | 14 +- 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 @@ -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( "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 + 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) if not vim.api.nvim_buf_is_valid(bufnr) then return end - local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, vim.split(data, "[\r]?\n")) - if not ok then - return - end + local processed_data = split(data, "[\r]?\n", _, opts) - if opts.callback then - opts.callback(bufnr) + 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) + 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