rust-tools.nvim icon indicating copy to clipboard operation
rust-tools.nvim copied to clipboard

inlay hint with virtual lines

Open pca006132 opened this issue 2 years ago • 7 comments

For complicated pattern matching, the type inlay might get a bit too long and does not fit in the screen (especially when horizontal split is used). It would be nice if we can use virtual lines when the type inlay cannot be fully shown within the screen.

Before: image

After: (please ignore the type inlay on the right, I forgot to remove them) image

pca006132 avatar Mar 28 '22 14:03 pca006132

Prototype implementation:

diff --git a/lua/rust-tools/config.lua b/lua/rust-tools/config.lua
index 470c82d..ebd1b6b 100644
diff --git a/lua/rust-tools/inlay_hints.lua b/lua/rust-tools/inlay_hints.lua
index 8f73e74..8be2b58 100644
--- a/lua/rust-tools/inlay_hints.lua
+++ b/lua/rust-tools/inlay_hints.lua
@@ -117,6 +117,13 @@ local function get_max_len(bufnr, parsed_data)
   return max_len
 end
 
+function str_len_rtrim(s)
+  -- return string length excluding trailing whitespace
+  local n = #s
+  while n > 0 and s:find("^%s", n) do n = n - 1 end
+  return n
+end
+
 local function handler(err, result, ctx)
   if err then
     return
@@ -145,7 +152,29 @@ local function handler(err, result, ctx)
     )[1]
 
     if current_line then
+      local current_line_indent = string.match(current_line, "^%s+")
       local current_line_len = string.len(current_line)
+      local max_line_len = config.options.tools.inlay_hints.max_line_len
+
+      local virt_lines = {}
+      local function append(str)
+        if max_line_len == 0 then
+          virt_text = virt_text .. str
+        else
+          -- we don't really care about the length of trailing spaces
+          local s_len = str_len_rtrim(str)
+          if current_line_len + s_len > max_line_len then
+            table.insert(virt_lines, {{
+              virt_text, config.options.tools.inlay_hints.highlight
+            }})
+            virt_text = current_line_indent .. str
+            current_line_len = string.len(current_line_indent) + string.len(str)
+          else
+            virt_text = virt_text .. str
+            current_line_len = current_line_len + string.len(str)
+          end
+        end
+      end
 
       local param_hints = {}
       local other_hints = {}
@@ -163,19 +192,20 @@ local function handler(err, result, ctx)
 
       -- show parameter hints inside brackets with commas and a thin arrow
       if not vim.tbl_isempty(param_hints) and opts.show_parameter_hints then
-        virt_text = virt_text .. opts.parameter_hints_prefix .. "("
+        append(opts.parameter_hints_prefix)
+        append("(")
         for i, value_inner_inner in ipairs(param_hints) do
-          virt_text = virt_text .. value_inner_inner:sub(1, -2)
+          append(value_inner_inner:sub(1, -2))
           if i ~= #param_hints then
-            virt_text = virt_text .. ", "
+            append(", ")
           end
         end
-        virt_text = virt_text .. ") "
+        append(") ")
       end
 
       -- show other hints with commas and a thicc arrow
       if not vim.tbl_isempty(other_hints) then
-        virt_text = virt_text .. opts.other_hints_prefix
+        append(opts.other_hints_prefix)
         for i, value_inner_inner in ipairs(other_hints) do
           if value_inner_inner.kind == 2 and opts.show_variable_name then
             local char_start = value_inner_inner.range.start.character
@@ -185,52 +215,64 @@ local function handler(err, result, ctx)
               char_start + 1,
               char_end
             )
-            virt_text = virt_text
-              .. variable_name
-              .. ": "
-              .. value_inner_inner.label
+            append(variable_name .. ": " .. value_inner_inner.label)
           else
             if string.sub(value_inner_inner.label, 1, 2) == ": " then
-              virt_text = virt_text .. value_inner_inner.label:sub(3)
+              append(value_inner_inner.label:sub(3))
             else
-              virt_text = virt_text .. value_inner_inner.label
+              append(value_inner_inner.label)
             end
           end
           if i ~= #other_hints then
-            virt_text = virt_text .. ", "
+            append(", ")
           end
         end
       end
 
-      if config.options.tools.inlay_hints.right_align then
-        virt_text = virt_text
-          .. string.rep(
-            " ",
-            config.options.tools.inlay_hints.right_align_padding
-          )
-      end
+      -- if config.options.tools.inlay_hints.right_align then
+      --   virt_text = virt_text
+      --     .. string.rep(
+      --       " ",
+      --       config.options.tools.inlay_hints.right_align_padding
+      --     )
+      -- end
+      --
+      -- if config.options.tools.inlay_hints.max_len_align then
+      --   local max_len = get_max_len(bufnr, parsed)
+      --   virt_text = string.rep(
+      --     " ",
+      --     max_len
+      --       - current_line_len
+      --       + config.options.tools.inlay_hints.max_len_align_padding
+      --   ) .. virt_text
+      -- end
 
-      if config.options.tools.inlay_hints.max_len_align then
-        local max_len = get_max_len(bufnr, parsed)
-        virt_text = string.rep(
-          " ",
-          max_len
-            - current_line_len
-            + config.options.tools.inlay_hints.max_len_align_padding
-        ) .. virt_text
+      -- put the virt text into the lines
+      if virt_text ~= "" then
+        table.insert(virt_lines, {{
+          virt_text, config.options.tools.inlay_hints.highlight
+        }})
       end
 
-      -- set the virtual text if it is not empty
-      if virt_text ~= "" then
-        vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
-          virt_text_pos = config.options.tools.inlay_hints.right_align
-              and "right_align"
-            or "eol",
-          virt_text = {
-            { virt_text, config.options.tools.inlay_hints.highlight },
-          },
-          hl_mode = "combine",
-        })
+      -- set the virtual text and virtual lines
+      if #virt_lines > 0 and virt_lines[1] ~= "" then
+        if virt_lines[1] ~= "" then
+          vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
+            virt_text_pos = config.options.tools.inlay_hints.right_align
+                and "right_align"
+              or "eol",
+            virt_text = {
+              virt_lines[1][1]
+            },
+            hl_mode = "combine",
+          })
+        end
+        table.remove(virt_lines, 1)
+        if #virt_lines > 0 then
+          vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
+            virt_lines = virt_lines,
+          })
+        end
       end

Demo: image

Problems:

  1. I am not sure how to take into account of tab width when calculating line length, considering some users will use tab instead of spaces as indentation.
  2. Some symbols are a bit weird when break into multiple lines.

pca006132 avatar Mar 29 '22 06:03 pca006132

Damn this looks really nice! I would definitely implement this in the rewrite (https://github.com/simrat39/rust-tools.nvim/tree/modularize_and_inlay_rewrite). Thanks for the code!

Edit: We could make it so that the hints that do not fit go entirely to the next line. Not too sure about that though

simrat39 avatar Mar 31 '22 05:03 simrat39

We could make it so that the hints that do not fit go entirely to the next line. Not too sure about that though

Yes this is an option, but in some cases the type hint might be too long to put in 1 line (complex pattern matching for example), then we will need to split the type hint. I wonder if we should duplicate the arrows for each line or somehow differentiate parameter hints with other hints in a way that remains obvious when split into multiple lines.

pca006132 avatar Mar 31 '22 07:03 pca006132

Combined with https://git.sr.ht/~whynothugo/lsp_lines.nvim, being able to have all the inlay hints take up their own virtual line instead of being at the end, would be a real winner.

I use a font size large enough that most virtual texts at ends of lines just vanish for me, so having them consistently either above or below the line they're illustrating (maybe above so it doesn't get in the way of lsp-lines), would make them a lot more useful.

Diablo-D3 avatar Aug 12 '22 23:08 Diablo-D3

@Diablo-D3 checkout https://github.com/simrat39/inlay-hints.nvim if you want something like that

simrat39 avatar Aug 12 '22 23:08 simrat39

I saw that by spying on your GH account. Is this a more flexible generic version of what has grown up in rust-tools?

Diablo-D3 avatar Aug 13 '22 02:08 Diablo-D3

I saw that by spying on your GH account. Is this a more flexible generic version of what has grown up in rust-tools?

Yes, this will pretty much be the next gen of the inlay hints inside of rust-tools. We already support a virtual line, end of line, and dynamic (virt line if more than 2 hints, else eol) renderers.

simrat39 avatar Aug 13 '22 03:08 simrat39