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

fix(previewer): improve file_maker line splitting and timeouts #3261

Open
wants to merge 1 commit 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
18 changes: 1 addition & 17 deletions lua/telescope/previewers/buffer_previewer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions lua/telescope/previewers/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 3 additions & 11 deletions lua/telescope/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
49 changes: 49 additions & 0 deletions lua/tests/automated/previewer_spec.lua
Original file line number Diff line number Diff line change
@@ -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)