From 18a8b8de250e144a1cbdf0e4e7366a9d986b9cc4 Mon Sep 17 00:00:00 2001 From: James Trew Date: Sat, 17 Aug 2024 23:49:59 -0400 Subject: [PATCH] fix(previewer): improve `file_maker` line splitting and timeouts --- lua/telescope/previewers/buffer_previewer.lua | 18 +------ lua/telescope/previewers/utils.lua | 42 ++++++++++++++++ lua/telescope/utils.lua | 14 ++---- lua/tests/automated/previewer_spec.lua | 49 +++++++++++++++++++ 4 files changed, 95 insertions(+), 28 deletions(-) create mode 100644 lua/tests/automated/previewer_spec.lua diff --git a/lua/telescope/previewers/buffer_previewer.lua b/lua/telescope/previewers/buffer_previewer.lua index 18f483df18..80db6f8146 100644 --- a/lua/telescope/previewers/buffer_previewer.lua +++ b/lua/telescope/previewers/buffer_previewer.lua @@ -56,22 +56,6 @@ local function defaulter(f, default_opts) } 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 - local line = opts.file_encoding and vim.iconv(c, opts.file_encoding, "utf8") or c - table.insert(t, line) - 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 = { @@ -199,7 +183,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 = putils.timed_split_lines(data, opts) if processed_data then local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, processed_data) diff --git a/lua/telescope/previewers/utils.lua b/lua/telescope/previewers/utils.lua index b430e5a509..9ca478ac40 100644 --- a/lua/telescope/previewers/utils.lua +++ b/lua/telescope/previewers/utils.lua @@ -241,4 +241,46 @@ utils.binary_mime_type = function(mime_type) return true end +local CHECK_TIME_INTERVAL = 200 + +--- Split a string into lines, checking every `CHECK_TIME_INTERVAL` characters +--- whether to timeout. +--- +--- Roughly 4-5x faster than using `vim.gsplit` and checking timeout between each line. +--- The latter approach is also more prone to exceeding timeout if a file has huge lines. +---@param s string file content to split into lines +---@param opts {start_time: number, preview: { timeout: number }, file_encoding: string?} +function utils.timed_split_lines(s, opts) + local lines = {} + local line_start = 1 + + for i = 1, #s do + local ch = s:byte(i) + if ch == 10 then + local line + if s:byte(i - 1) ~= 13 then + line = s:sub(line_start, i - 1) + else + line = s:sub(line_start, i - 2) + end + line_start = i + 1 + table.insert(lines, opts.file_encoding and vim.iconv(line, opts.file_encoding, "utf8") or line) + end + + if i % CHECK_TIME_INTERVAL == 0 then + local diff_time = (vim.loop.hrtime() - opts.start_time) / 1e6 + if diff_time > opts.preview.timeout then + return + end + end + end + + table.insert( + lines, + opts.file_encoding and vim.iconv(s:sub(line_start), opts.file_encoding, "utf8") or s:sub(line_start) + ) + + return lines +end + return utils diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index 44ab5d9b08..7e6c066367 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -776,16 +776,8 @@ utils.reverse_table = function(input_table) return temp_table end -utils.split_lines = (function() - if utils.iswin then - return function(s, opts) - return vim.split(s, "\r?\n", opts) - end - else - return function(s, opts) - return vim.split(s, "\n", opts) - end - end -end)() +utils.split_lines = function(s, opts) + return vim.split(s, "\r?\n", opts) +end return utils diff --git a/lua/tests/automated/previewer_spec.lua b/lua/tests/automated/previewer_spec.lua new file mode 100644 index 0000000000..15dc295c0b --- /dev/null +++ b/lua/tests/automated/previewer_spec.lua @@ -0,0 +1,49 @@ +local putils = require "telescope.previewers.utils" +local utils = require "telescope.utils" + +describe("timed_split_lines", function() + local expect = { + "", + "", + "line3 of the file", + "", + "line5 of the file", + "", + "", + "line8 of the file, last line of file", + "", + } + + local function get_fake_file(line_ending) + return table.concat(expect, line_ending) + end + + local newline_file = get_fake_file "\n" + local carriage_newline_file = get_fake_file "\r\n" + + local split_lines = function(s) + return putils.timed_split_lines(s, { + start_time = vim.loop.hrtime(), + preview = { + timeout = 250, -- should be more than enough time + }, + }) + end + + if utils.iswin then + describe("handles files on Windows", function() + it("reads file with newline only", function() + assert.are.same(expect, split_lines(newline_file)) + end) + it("reads file with carriage return and newline", function() + assert.are.same(expect, split_lines(carriage_newline_file)) + end) + end) + else + describe("handles files on non Windows environment", function() + it("reads file with newline only", function() + assert.are.same(expect, split_lines(newline_file)) + end) + end) + end +end)