diff --git a/lua/telescope/make_entry.lua b/lua/telescope/make_entry.lua index bd1bf73..d23dcb7 100644 --- a/lua/telescope/make_entry.lua +++ b/lua/telescope/make_entry.lua @@ -463,7 +463,8 @@ function make_entry.gen_from_buffer(opts) local icon_width = 0 if not disable_devicons then - icon_width = vim.fn.strdisplaywidth(get_devicons('fname', disable_devicons)) + local icon, _ = get_devicons('fname', disable_devicons) + icon_width = utils.strdisplaywidth(icon) end local displayer = entry_display.create { diff --git a/lua/telescope/pickers/entry_display.lua b/lua/telescope/pickers/entry_display.lua index c7a51ac..7e38ac8 100644 --- a/lua/telescope/pickers/entry_display.lua +++ b/lua/telescope/pickers/entry_display.lua @@ -1,13 +1,27 @@ +local utils = require('telescope.utils') + local entry_display = {} -local function truncate(str, len) +entry_display.truncate = function(str, len) str = tostring(str) -- We need to make sure its an actually a string and not a number - -- TODO: This doesn't handle multi byte chars... - if vim.fn.strdisplaywidth(str) > len then - str = str:sub(1, len - 1) - str = str .. "…" + if utils.strdisplaywidth(str) <= len then + return str end - return str + local charlen = 0 + local cur_len = 0 + local result = '' + local len_of_dots = utils.strdisplaywidth('…') + while true do + local part = utils.strcharpart(str, charlen, 1) + cur_len = cur_len + utils.strdisplaywidth(part) + if (cur_len + len_of_dots) > len then + result = result .. '…' + break + end + result = result .. part + charlen = charlen + 1 + end + return result end entry_display.create = function(configuration) @@ -18,9 +32,9 @@ entry_display.create = function(configuration) local format_str = "%" .. justify .. v.width .. "s" table.insert(generator, function(item) if type(item) == 'table' then - return string.format(format_str, truncate(item[1], v.width)), item[2] + return string.format(format_str, entry_display.truncate(item[1], v.width)), item[2] else - return string.format(format_str, truncate(item, v.width)) + return string.format(format_str, entry_display.truncate(item, v.width)) end end) else diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index 913c9a2..9cfc23c 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -202,4 +202,86 @@ function utils.get_os_command_output(cmd) return Job:new({ command = command, args = cmd }):sync() end +utils.strdisplaywidth = (function() + if jit then + local ffi = require('ffi') + ffi.cdef[[ + typedef unsigned char char_u; + int linetabsize_col(int startcol, char_u *s); + ]] + + return function(str, col) + local startcol = col or 0 + local s = ffi.new('char[?]', #str + 1) + ffi.copy(s, str) + return ffi.C.linetabsize_col(startcol, s) - startcol + end + else + return function(str, col) + return #str - (col or 0) + end + end +end)() + +utils.utf_ptr2len = (function() + if jit then + local ffi = require('ffi') + ffi.cdef[[ + typedef unsigned char char_u; + int utf_ptr2len(const char_u *const p); + ]] + + return function(str) + local c_str = ffi.new('char[?]', #str + 1) + ffi.copy(c_str, str) + return ffi.C.utf_ptr2len(c_str) + end + else + return function(str) + return str == '' and 0 or 1 + end + end +end)() + +function utils.strcharpart(str, nchar, charlen) + local nbyte = 0 + if nchar > 0 then + while nchar > 0 and nbyte < #str do + nbyte = nbyte + utils.utf_ptr2len(str:sub(nbyte + 1)) + nchar = nchar - 1 + end + else + nbyte = nchar + end + + local len = 0 + if charlen then + while charlen > 0 and nbyte + len < #str do + local off = nbyte + len + if off < 0 then + len = len + 1 + else + len = len + utils.utf_ptr2len(str:sub(off + 1)) + end + charlen = charlen - 1 + end + else + len = #str - nbyte + end + + if nbyte < 0 then + len = len + nbyte + nbyte = 0 + elseif nbyte > #str then + nbyte = #str + end + if len < 0 then + len = 0 + elseif nbyte + len > #str then + len = #str - nbyte + end + + return str:sub(nbyte + 1, nbyte + len) +end + return utils diff --git a/lua/tests/automated/entry_display_spec.lua b/lua/tests/automated/entry_display_spec.lua new file mode 100644 index 0000000..fff78cd --- /dev/null +++ b/lua/tests/automated/entry_display_spec.lua @@ -0,0 +1,32 @@ +local entry_display = require('telescope.pickers.entry_display') + +describe('truncate', function() + for _, ambiwidth in ipairs{'single', 'double'} do + for _, case in ipairs{ + {args = {'abcde', 6}, expected = {single = 'abcde', double = 'abcde'}}, + {args = {'abcde', 5}, expected = {single = 'abcde', double = 'abcde'}}, + {args = {'abcde', 4}, expected = {single = 'abc…', double = 'ab…'}}, + {args = {'アイウエオ', 11}, expected = {single = 'アイウエオ', double = 'アイウエオ'}}, + {args = {'アイウエオ', 10}, expected = {single = 'アイウエオ', double = 'アイウエオ'}}, + {args = {'アイウエオ', 9}, expected = {single = 'アイウエ…', double = 'アイウ…'}}, + {args = {'アイウエオ', 8}, expected = {single = 'アイウ…', double = 'アイウ…'}}, + {args = {'├─┤', 7}, expected = {single = '├─┤', double = '├─┤'}}, + {args = {'├─┤', 6}, expected = {single = '├─┤', double = '├─┤'}}, + {args = {'├─┤', 5}, expected = {single = '├─┤', double = '├…'}}, + {args = {'├─┤', 4}, expected = {single = '├─┤', double = '├…'}}, + {args = {'├─┤', 3}, expected = {single = '├─┤', double = '…'}}, + {args = {'├─┤', 2}, expected = {single = '├…', double = '…'}}, + } do + local msg = ('can truncate: ambiwidth = %s, [%s, %d] -> %s'):format(ambiwidth, case.args[1], case.args[2], case.expected[ambiwidth]) + it(msg, function() + local original = vim.o.ambiwidth + vim.o.ambiwidth = ambiwidth + assert.are.same( + case.expected[ambiwidth], + entry_display.truncate(case.args[1], case.args[2]) + ) + vim.o.ambiwidth = original + end) + end + end +end)