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 13 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
33 changes: 31 additions & 2 deletions lua/precognition/init.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local hm = require("precognition.horizontal_motions")
local vm = require("precognition.vertical_motions")
local utils = require("precognition.utils")
local compat = require("precognition.compat")

local M = {}

Expand Down Expand Up @@ -54,6 +55,10 @@ local M = {}
---@field PrevParagraph Precognition.PlaceLoc
---@field NextParagraph Precognition.PlaceLoc

---@class Precognition.ExtraPadding
---@field start integer
---@field length integer

---@type Precognition.HintConfig
local defaultHintConfig = {
Caret = { text = "^", prio = 2 },
Expand Down Expand Up @@ -106,8 +111,9 @@ local gutter_group = "precognition_gutter"

---@param marks Precognition.VirtLine
---@param line_len integer
---@param extra_padding Precognition.ExtraPadding[]
---@return table
local function build_virt_line(marks, line_len)
local function build_virt_line(marks, line_len, extra_padding)
if not marks then
return {}
end
Expand Down Expand Up @@ -141,6 +147,12 @@ local function build_virt_line(marks, line_len)
end
end

if #extra_padding > 0 then
for _, padding in ipairs(extra_padding) do
line_table[padding.start] = line_table[padding.start] .. string.rep(" ", padding.length)
end
end

local line = table.concat(line_table)
if line:match("^%s+$") then
return {}
Expand Down Expand Up @@ -210,6 +222,8 @@ local function display_marks()
local tab_width = vim.bo.expandtab and vim.bo.shiftwidth or vim.bo.tabstop
local cur_line = vim.api.nvim_get_current_line():gsub("\t", string.rep(" ", tab_width))
local line_len = vim.fn.strcharlen(cur_line)
---@type Precognition.ExtraPadding[]
local extra_padding = {}
-- local after_cursor = vim.fn.strcharpart(cur_line, cursorcol + 1)
-- local before_cursor = vim.fn.strcharpart(cur_line, 0, cursorcol - 1)
-- local before_cursor_rev = string.reverse(before_cursor)
Expand All @@ -232,7 +246,22 @@ local function display_marks()
Zero = 1,
}

local virt_line = build_virt_line(virtual_line_marks, line_len)
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

local virt_line = build_virt_line(virtual_line_marks, line_len, extra_padding)

-- TODO: can we add indent lines to the virt line to match indent-blankline or similar (if installed)?

Expand Down
17 changes: 17 additions & 0 deletions lua/precognition/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,21 @@ 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))
vim.print(expanded)
willothy marked this conversation as resolved.
Show resolved Hide resolved
local ws_offset = vim.fn.strcharlen(expanded)
return length, ws_offset
end

return M
38 changes: 7 additions & 31 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
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
Loading
Loading