Repository: nvim-lua/plenary.nvim
Pull request #276 (2021-12-01 23:12:39)

feat: truncate strings from the middle
Thought it might be useful to truncate strings from the middle.

Currently a bit of a rushed draft, and just guessed what the new tests should be (haven't actually run them yet 😅).
I will have another look over it in the next few days.

l-kershaw: All seems to work now 🙂

Patch:
diff --git a/lua/plenary/filetype.lua b/lua/plenary/filetype.lua
index f634b66..ef45102 100644
--- a/lua/plenary/filetype.lua
+++ b/lua/plenary/filetype.lua
@@ -62,6 +62,7 @@ filetype._get_extension_parts = function(filename)
       return possibilities
     end
   end
+  return possibilities
 end
 
 filetype._parse_modeline = function(tail)
diff --git a/lua/plenary/popup/init.lua b/lua/plenary/popup/init.lua
index 49ef913..92dec7f 100644
--- a/lua/plenary/popup/init.lua
+++ b/lua/plenary/popup/init.lua
@@ -386,7 +386,11 @@ function popup.create(what, vim_options)
   end
 
   if vim_options.highlight then
-    vim.api.nvim_win_set_option(win_id, "winhl", string.format("Normal:%s", vim_options.highlight))
+    vim.api.nvim_win_set_option(
+      win_id,
+      "winhl",
+      string.format("Normal:%s,EndOfBuffer:%s", vim_options.highlight, vim_options.highlight)
+    )
   end
 
   -- enter
diff --git a/lua/plenary/strings.lua b/lua/plenary/strings.lua
index 1a4df46..bc37473 100644
--- a/lua/plenary/strings.lua
+++ b/lua/plenary/strings.lua
@@ -92,10 +92,7 @@ M.strcharpart = (function()
   end
 end)()
 
-M.truncate = function(str, len, dots, direction)
-  str = tostring(str) -- We need to make sure its an actually a string and not a number
-  dots = dots or "…"
-  direction = direction or 1
+local truncate = function(str, len, dots, direction)
   if M.strdisplaywidth(str) <= len then
     return str
   end
@@ -123,6 +120,24 @@ M.truncate = function(str, len, dots, direction)
   return result
 end
 
+M.truncate = function(str, len, dots, direction)
+  str = tostring(str) -- We need to make sure its an actually a string and not a number
+  dots = dots or "…"
+  direction = direction or 1
+  if direction ~= 0 then
+    return truncate(str, len, dots, direction)
+  else
+    if M.strdisplaywidth(str) <= len then
+      return str
+    end
+    local len1 = math.floor((len + M.strdisplaywidth(dots)) / 2)
+    local s1 = truncate(str, len1, dots, 1)
+    local len2 = len - M.strdisplaywidth(s1) + M.strdisplaywidth(dots)
+    local s2 = truncate(str, len2, dots, -1)
+    return s1 .. s2:sub(dots:len() + 1)
+  end
+end
+
 M.align_str = function(string, width, right_justify)
   local str_len = M.strdisplaywidth(string)
   return right_justify and string.rep(" ", width - str_len) .. string or string .. string.rep(" ", width - str_len)
diff --git a/lua/plenary/window/border.lua b/lua/plenary/window/border.lua
index 7053dad..35448d0 100644
--- a/lua/plenary/window/border.lua
+++ b/lua/plenary/window/border.lua
@@ -55,23 +55,25 @@ local create_horizontal_line = function(title, pos, width, left_char, mid_char,
   return horizontal_line, ranges
 end
 
-function Border._create_lines(content_win_options, border_win_options)
+function Border._create_lines(content_win_id, content_win_options, border_win_options)
+  local content_pos = vim.api.nvim_win_get_position(content_win_id)
+  local content_height = vim.api.nvim_win_get_height(content_win_id)
+  local content_width = vim.api.nvim_win_get_width(content_win_id)
+
   -- TODO: Handle border width, which I haven't right here.
   local thickness = border_win_options.border_thickness
 
   local top_enabled = thickness.top == 1
-  local right_enabled = thickness.right == 1
+  local right_enabled = thickness.right == 1 and content_pos[2] + content_width < vim.o.columns
   local bot_enabled = thickness.bot == 1
-  local left_enabled = thickness.left == 1
+  local left_enabled = thickness.left == 1 and content_pos[2] > 0
+
+  border_win_options.border_thickness.left = left_enabled and 1 or 0
+  border_win_options.border_thickness.right = right_enabled and 1 or 0
 
   local border_lines = {}
   local ranges = {}
 
-  local topline = nil
-
-  local topleft = (left_enabled and border_win_options.topleft) or ""
-  local topright = (right_enabled and border_win_options.topright) or ""
-
   -- border_win_options.title should have be a list with entries of the
   -- form: { pos = foo, text = bar }.
   -- pos can take values in { "NW", "N", "NE", "SW", "S", "SE" }
@@ -79,17 +81,11 @@ function Border._create_lines(content_win_options, border_win_options)
     or border_win_options.title
     or {}
 
-  --[[
-  --  Ensure that the topline is drawn only if the row is positive (for an absolute position) or if when added to the current
-  --  cursor line (for a cursor relative position) it is also a positive value.
-  --]]
-  if
-    content_win_options.row > 0
-    or (
-      content_win_options.relative == "cursor"
-      and content_win_options.row + vim.api.nvim_win_get_cursor(0)[1] + vim.api.nvim_win_get_position(0)[1] > 1
-    )
-  then
+  local topline = nil
+  local topleft = (left_enabled and border_win_options.topleft) or ""
+  local topright = (right_enabled and border_win_options.topright) or ""
+  -- Only calculate the topline if there is space above the first content row (relative to the editor)
+  if content_pos[1] > 0 then
     for _, title in ipairs(titles) do
       if string.find(title.pos, "N") then
         local top_ranges
@@ -131,10 +127,10 @@ function Border._create_lines(content_win_options, border_win_options)
     table.insert(border_lines, middle_line)
   end
 
-  if bot_enabled then
-    local botline = nil
-    local botleft = (left_enabled and border_win_options.botleft) or ""
-    local botright = (right_enabled and border_win_options.botright) or ""
+  local botline = nil
+  local botleft = (left_enabled and border_win_options.botleft) or ""
+  local botright = (right_enabled and border_win_options.botright) or ""
+  if content_pos[1] + content_height < vim.o.lines then
     for _, title in ipairs(titles) do
       if string.find(title.pos, "S") then
         local bot_ranges
@@ -147,16 +143,21 @@ function Border._create_lines(content_win_options, border_win_options)
           botright
         )
         for _, r in pairs(bot_ranges) do
-          table.insert(ranges, { content_win_options.height + 1, r[1], r[2] })
+          table.insert(ranges, { content_win_options.height + thickness.top, r[1], r[2] })
         end
         break
       end
     end
     if botline == nil then
-      if top_enabled then
+      if bot_enabled then
         botline = botleft .. string.rep(border_win_options.bot, content_win_options.width) .. botright
       end
     end
+  else
+    border_win_options.border_thickness.bot = 0
+  end
+
+  if botline then
     table.insert(border_lines, botline)
   end
 
@@ -178,7 +179,11 @@ function Border:change_title(new_title)
   end
 
   self._border_win_options.title = new_title
-  self.contents, self.title_ranges = Border._create_lines(self.content_win_options, self._border_win_options)
+  self.contents, self.title_ranges = Border._create_lines(
+    self.content_win_id,
+    self.content_win_options,
+    self._border_win_options
+  )
   vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
 
   set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight)
@@ -201,6 +206,14 @@ function Border:__align_calc_config(content_win_options, border_win_options)
     bot = "═",
   })
 
+  -- Ensure the relevant contents and border win_options are set
+  self._border_win_options = border_win_options
+  self.content_win_options = content_win_options
+  -- Update border characters and title_ranges
+  self.contents, self.title_ranges = Border._create_lines(self.content_win_id, content_win_options, border_win_options)
+
+  vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
+
   local thickness = border_win_options.border_thickness
   local nvim_win_config = {
     anchor = content_win_options.anchor,
@@ -215,14 +228,6 @@ function Border:__align_calc_config(content_win_options, border_win_options)
     focusable = vim.F.if_nil(border_win_options.focusable, false),
   }
 
-  -- Ensure the relevant contests and border win_options are set
-  self._border_win_options = border_win_options
-  self.content_win_options = content_win_options
-  -- Update border characters and title_ranges
-  self.contents, self.title_ranges = Border._create_lines(content_win_options, border_win_options)
-
-  vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
-
   return nvim_win_config
 end
 


Test patch:
diff --git a/tests/plenary/popup_spec.lua b/tests/plenary/popup_spec.lua
index 81d7e8d..a02453d 100644
--- a/tests/plenary/popup_spec.lua
+++ b/tests/plenary/popup_spec.lua
@@ -27,7 +27,7 @@ describe("plenary.popup", function()
       highlight = "PopupColor1",
     })
 
-    eq("Normal:PopupColor1", vim.api.nvim_win_get_option(win_id, "winhl"))
+    eq("Normal:PopupColor1,EndOfBuffer:PopupColor1", vim.api.nvim_win_get_option(win_id, "winhl"))
   end)
 
   it("can create a border", function()
diff --git a/tests/plenary/strings_spec.lua b/tests/plenary/strings_spec.lua
index 120cfda..4bf1039 100644
--- a/tests/plenary/strings_spec.lua
+++ b/tests/plenary/strings_spec.lua
@@ -71,11 +71,11 @@ describe("strings", function()
       { args = { "abcde", 5, nil, -1 }, expected = { single = "abcde", double = "abcde" } },
       { args = { "abcde", 4, nil, -1 }, expected = { single = "…cde", double = "…de" } },
       {
-        args = { "アイウエオ", 11, nil, 1 },
+        args = { "アイウエオ", 11, nil, -1 },
         expected = { single = "アイウエオ", double = "アイウエオ" },
       },
       {
-        args = { "アイウエオ", 10, nil, 1 },
+        args = { "アイウエオ", 10, nil, -1 },
         expected = { single = "アイウエオ", double = "アイウエオ" },
       },
       {
@@ -89,11 +89,34 @@ describe("strings", function()
       { args = { "├─┤", 4, nil, -1 }, expected = { single = "├─┤", double = "…┤" } },
       { args = { "├─┤", 3, nil, -1 }, expected = { single = "├─┤", double = "…" } },
       { args = { "├─┤", 2, nil, -1 }, expected = { single = "…┤", double = "…" } },
+      -- truncations from the middle
+      { args = { "abcde", 6, nil, 0 }, expected = { single = "abcde", double = "abcde" } },
+      { args = { "abcde", 5, nil, 0 }, expected = { single = "abcde", double = "abcde" } },
+      { args = { "abcde", 4, nil, 0 }, expected = { single = "a…de", double = "a…e" } },
+      {
+        args = { "アイウエオ", 11, nil, 0 },
+        expected = { single = "アイウエオ", double = "アイウエオ" },
+      },
+      {
+        args = { "アイウエオ", 10, nil, 0 },
+        expected = { single = "アイウエオ", double = "アイウエオ" },
+      },
+      {
+        args = { "アイウエオ", 9, nil, 0 },
+        expected = { single = "アイ…エオ", double = "ア…エオ" },
+      },
+      { args = { "アイウエオ", 8, nil, 0 }, expected = { single = "ア…エオ", double = "ア…エオ" } },
+      { args = { "├─┤", 7, nil, 0 }, expected = { single = "├─┤", double = "├─┤" } },
+      { args = { "├─┤", 6, nil, 0 }, expected = { single = "├─┤", double = "├─┤" } },
+      { args = { "├─┤", 5, nil, 0 }, expected = { single = "├─┤", double = "…┤" } },
+      { args = { "├─┤", 4, nil, 0 }, expected = { single = "├─┤", double = "…┤" } },
+      { args = { "├─┤", 3, nil, 0 }, expected = { single = "├─┤", double = "…" } },
+      { args = { "├─┤", 2, nil, 0 }, expected = { single = "…┤", double = "…" } },
     } do
       for _, ambiwidth in ipairs { "single", "double" } do
         local msg = ("ambiwidth = %s, direction = %s, [%s, %d] -> %s"):format(
           ambiwidth,
-          (case.args[4] > 0) and "right" or "left",
+          (case.args[4] > 0) and "right" or (case.args[4] < 0) and "left" or "middle",
           case.args[1],
           case.args[2],
           case.expected[ambiwidth]