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

feat: inlay hints #38

Merged
merged 19 commits into from
Jul 31, 2024
Merged
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
1 change: 1 addition & 0 deletions .luarc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"describe",
"it",
"before_each",
"after_each",
]
}
10 changes: 10 additions & 0 deletions lua/precognition/compat.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
local M = {}

function M.inlay_hints_enabled(t)
if vim.lsp and vim.lsp.inlay_hint and vim.lsp.inlay_hint.is_enabled then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't you use has("nvim-0.10") here?

Copy link
Collaborator

@willothy willothy May 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this isn't a big deal then this PR is good to merge once conflicts are handled imo :)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to rebase and will check the field name. I know the lsp API changed a few times in 0.10. I can't remember which released

I will check docs tomorrow

return vim.lsp.inlay_hint.is_enabled(t)
end
return false
end

return M
26 changes: 22 additions & 4 deletions lua/precognition/init.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
local compat = require("precognition.compat")

local M = {}

---@class Precognition.HintOpts
Expand Down Expand Up @@ -109,17 +111,18 @@ local gutter_group = "precognition_gutter"

---@param marks Precognition.VirtLine
---@param line_len integer
---@param extra_padding Precognition.ExtraPadding
---@param extra_padding Precognition.ExtraPadding[]
---@return table
local function build_virt_line(marks, line_len, extra_padding)
local utils = require("precognition.utils")
if not marks then
return {}
end
if line_len == 0 then
return {}
end
local virt_line = {}
local line_table = require("precognition.utils").create_pad_array(line_len, " ")
local line_table = utils.create_pad_array(line_len, " ")

for mark, loc in pairs(marks) do
local hint = config.hints[mark].text or mark
Expand Down Expand Up @@ -224,8 +227,9 @@ local function apply_gutter_hints(gutter_hints, bufnr)
end

local function display_marks()
local utils = require("precognition.utils")
local bufnr = vim.api.nvim_get_current_buf()
if require("precognition.utils").is_blacklisted_buffer(bufnr) then
if utils.is_blacklisted_buffer(bufnr) then
return
end
local cursorline = vim.fn.line(".")
Expand Down Expand Up @@ -263,9 +267,23 @@ local function display_marks()
Zero = 1,
}

if compat.inlay_hints_enabled({ bufnr = 0 }) then
local inlays_hints = vim.lsp.inlay_hint.get({
bufnr = 0,
range = {
start = { line = cursorline - 1, character = 0 },
["end"] = { line = cursorline - 1, character = line_len - 1 },
},
})

for _, hint in ipairs(inlays_hints) do
local length, ws_offset = utils.calc_ws_offset(hint, tab_width, vim.api.nvim_get_current_line())
table.insert(extra_padding, { start = ws_offset, length = length })
end
end
--multicharacter padding

require("precognition.utils").add_multibyte_padding(cur_line, extra_padding, line_len)
utils.add_multibyte_padding(cur_line, extra_padding, line_len)

local virt_line = build_virt_line(virtual_line_marks, line_len, extra_padding)

Expand Down
16 changes: 16 additions & 0 deletions lua/precognition/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ function M.create_pad_array(len, str)
return pad_array
end

---calculates the white space offset of a partial string
---@param hint vim.lsp.inlay_hint.get.ret
---@param tab_width integer
---@param current_line string
---@return integer
---@return integer
function M.calc_ws_offset(hint, tab_width, current_line)
-- + 1 here because of trailing padding
local length = #hint.inlay_hint.label[1].value + 1
local start = hint.inlay_hint.position.character
local prefix = vim.fn.strcharpart(current_line, 0, start)
local expanded = string.gsub(prefix, "\t", string.rep(" ", tab_width))
local ws_offset = vim.fn.strcharlen(expanded)
return length, ws_offset
end

---Add extra padding for multi byte character characters
---@param cur_line string
---@param extra_padding Precognition.ExtraPadding[]
Expand Down
42 changes: 9 additions & 33 deletions tests/precognition/e2e_spec.lua
Original file line number Diff line number Diff line change
@@ -1,32 +1,8 @@
local precognition = require("precognition")
local tu = require("tests.precognition.utils.utils")
---@diagnostic disable-next-line: undefined-field
local eq = assert.are.same

local function get_gutter_extmarks(buffer)
local gutter_extmarks = {}
for _, extmark in
pairs(vim.api.nvim_buf_get_extmarks(buffer, -1, 0, -1, {
details = true,
}))
do
if extmark[4] and extmark[4].sign_name and extmark[4].sign_name:match(precognition.gutter_group) then
table.insert(gutter_extmarks, extmark)
end
end
return gutter_extmarks
end

local function hex2dec(hex)
hex = hex:gsub("#", "")
local r = tonumber("0x" .. hex:sub(1, 2))
local g = tonumber("0x" .. hex:sub(3, 4))
local b = tonumber("0x" .. hex:sub(5, 6))

local dec = (r * 256 ^ 2) + (g * 256) + b

return dec
end

describe("e2e tests", function()
before_each(function()
precognition.setup({})
Expand Down Expand Up @@ -65,7 +41,7 @@ describe("e2e tests", function()
details = true,
})

local gutter_extmarks = get_gutter_extmarks(buffer)
local gutter_extmarks = tu.get_gutter_extmarks(buffer)

for _, extmark in pairs(gutter_extmarks) do
if extmark[4].sign_text == "G " then
Expand Down Expand Up @@ -104,7 +80,7 @@ describe("e2e tests", function()
vim.api.nvim_win_set_cursor(0, { 2, 1 })
precognition.on_cursor_moved()

gutter_extmarks = get_gutter_extmarks(buffer)
gutter_extmarks = tu.get_gutter_extmarks(buffer)

extmarks = vim.api.nvim_buf_get_extmark_by_id(buffer, precognition.ns, precognition.extmark, {
details = true,
Expand All @@ -129,7 +105,7 @@ describe("e2e tests", function()

vim.api.nvim_win_set_cursor(0, { 4, 1 })
precognition.on_cursor_moved()
gutter_extmarks = get_gutter_extmarks(buffer)
gutter_extmarks = tu.get_gutter_extmarks(buffer)

for _, extmark in pairs(gutter_extmarks) do
if extmark[4].sign_text == "G " then
Expand Down Expand Up @@ -165,7 +141,7 @@ describe("e2e tests", function()
details = true,
})

local gutter_extmarks = get_gutter_extmarks(buffer)
local gutter_extmarks = tu.get_gutter_extmarks(buffer)

for _, extmark in pairs(gutter_extmarks) do
if extmark[4].sign_text == "G " then
Expand Down Expand Up @@ -199,7 +175,7 @@ describe("e2e tests", function()
local background = "#00ff00"
local foreground = "#ff0000"
local customColor = { foreground = foreground, background = background }
local customMark = { fg = hex2dec(foreground), bg = hex2dec(background) }
local customMark = { fg = tu.hex2dec(foreground), bg = tu.hex2dec(background) }
precognition.setup({ highlightColor = customColor })
local buffer = vim.api.nvim_create_buf(true, false)
vim.api.nvim_set_current_buf(buffer)
Expand All @@ -218,7 +194,7 @@ describe("e2e tests", function()
details = true,
})

local gutter_extmarks = get_gutter_extmarks(buffer)
local gutter_extmarks = tu.get_gutter_extmarks(buffer)

for _, extmark in pairs(gutter_extmarks) do
if extmark[4].sign_text == "G " then
Expand Down Expand Up @@ -275,7 +251,7 @@ describe("Gutter Priority", function()

precognition.on_cursor_moved()

local gutter_extmarks = get_gutter_extmarks(testBuf)
local gutter_extmarks = tu.get_gutter_extmarks(testBuf)

for _, extmark in pairs(gutter_extmarks) do
eq(true, extmark[4].sign_text ~= "G ")
Expand Down Expand Up @@ -304,7 +280,7 @@ describe("Gutter Priority", function()

precognition.on_cursor_moved()

local gutter_extmarks = get_gutter_extmarks(testBuf)
local gutter_extmarks = tu.get_gutter_extmarks(testBuf)

eq(1, vim.tbl_count(gutter_extmarks))
eq("gg", gutter_extmarks[1][4].sign_text)
Expand Down
74 changes: 74 additions & 0 deletions tests/precognition/inlay_hints_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
local server = require("tests.precognition.utils.lsp").server
local compat = require("tests.precognition.utils.compat")
local precognition = require("precognition")
---@diagnostic disable-next-line: undefined-field
local eq = assert.are.same
---@diagnostic disable-next-line: undefined-field
local neq = assert.are_not.same
local buf

local function wait(condition, msg)
vim.wait(100, condition)
local result = condition()
neq(false, result, msg)
neq(nil, result, msg)
end

describe("lsp based tests", function()
before_each(function()
require("tests.precognition.utils.lsp").Reset()
buf = vim.api.nvim_create_buf(true, false)
local srv = vim.lsp.start_client({ cmd = server })
if srv then
vim.lsp.buf_attach_client(buf, srv)
end
end)

it("initialize lsp", function()
eq(2, #require("tests.precognition.utils.lsp").messages)
eq("initialize", require("tests.precognition.utils.lsp").messages[1].method)
eq("initialized", require("tests.precognition.utils.lsp").messages[2].method)
end)

it("can enable inlay hints", function()
vim.lsp.inlay_hint.enable(true, { bufnr = buf })

eq(3, #require("tests.precognition.utils.lsp").messages)
eq("textDocument/inlayHint", require("tests.precognition.utils.lsp").messages[3].method)
end)

it("inlay hint shifts the line", function()
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "here is a string" })
vim.api.nvim_set_current_buf(buf)
vim.api.nvim_win_set_cursor(0, { 1, 1 })

precognition.on_cursor_moved()

local extmarks = vim.api.nvim_buf_get_extmark_by_id(buf, precognition.ns, precognition.extmark, {
details = true,
})

eq("b e w $", extmarks[3].virt_lines[1][1][1])

vim.lsp.inlay_hint.enable(true, { bufnr = buf })
-- NOTE:The test LSP replies with an inlay hint, that suggest "foo" as line 1, position 4
-- This means that the inlay hint is shifted by 3 chars

precognition.on_cursor_moved()

extmarks = vim.api.nvim_buf_get_extmark_by_id(buf, precognition.ns, precognition.extmark, {
details = true,
})

eq("b e w $", extmarks[3].virt_lines[1][1][1])
end)

after_each(function()
vim.lsp.inlay_hint.enable(false, { bufnr = buf })
vim.api.nvim_buf_delete(buf, { force = true })
vim.lsp.stop_client(compat.get_active_lsp_clients())
wait(function()
return vim.tbl_count(compat.get_active_lsp_clients()) == 0
end, "clients must stop")
end)
end)
5 changes: 5 additions & 0 deletions tests/precognition/utils/compat.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
local M = {}

M.get_active_lsp_clients = vim.lsp.get_clients() or vim.lsp.get_active_clients()

return M
53 changes: 53 additions & 0 deletions tests/precognition/utils/lsp.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
local M = {}

M.messages = {}

function M.server()
local closing = false
local srv = {}

function srv.request(method, params, handler)
table.insert(M.messages, { method = method, params = params })
if method == "initialize" then
handler(nil, {
capabilities = {
inlayHintProvider = true,
},
})
elseif method == "shutdown" then
handler(nil, nil)
elseif method == "textDocument/inlayHint" then
handler(nil, {
{
position = { line = 0, character = 3 },
label = { { value = "foo" } },
},
})
else
assert(false, "Unhandled method: " .. method)
end
end

function srv.notify(method, params)
table.insert(M.messages, { method = method, params = params })
if method == "exit" then
closing = true
end
end

function srv.is_closing()
return closing
end

function srv.terminate()
closing = true
end

return srv
end

function M.Reset()
M.messages = {}
end

return M
30 changes: 30 additions & 0 deletions tests/precognition/utils/utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
local precognition = require("precognition")

local M = {}

function M.get_gutter_extmarks(buffer)
local gutter_extmarks = {}
for _, extmark in
pairs(vim.api.nvim_buf_get_extmarks(buffer, -1, 0, -1, {
details = true,
}))
do
if extmark[4] and extmark[4].sign_name and extmark[4].sign_name:match(precognition.gutter_group) then
table.insert(gutter_extmarks, extmark)
end
end
return gutter_extmarks
end

function M.hex2dec(hex)
hex = hex:gsub("#", "")
local r = tonumber("0x" .. hex:sub(1, 2))
local g = tonumber("0x" .. hex:sub(3, 4))
local b = tonumber("0x" .. hex:sub(5, 6))

local dec = (r * 256 ^ 2) + (g * 256) + b

return dec
end

return M
1 change: 0 additions & 1 deletion tests/precognition/virtline_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ describe("Build Virtual Line", function()

it("example virtual line with whitespace padding", function()
local line = " abc def"
-- abc def
local cursorcol = 5
local tab_width = vim.bo.expandtab and vim.bo.shiftwidth or vim.bo.tabstop
local cur_line = line:gsub("\t", string.rep(" ", tab_width))
Expand Down
Loading