diff --git a/.luarc.json b/.luarc.json index 3c99bde..f0141ca 100644 --- a/.luarc.json +++ b/.luarc.json @@ -3,5 +3,6 @@ "describe", "it", "before_each", + "after_each", ] } diff --git a/lua/precognition/compat.lua b/lua/precognition/compat.lua new file mode 100644 index 0000000..04424d1 --- /dev/null +++ b/lua/precognition/compat.lua @@ -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 + return vim.lsp.inlay_hint.is_enabled(t) + end + return false +end + +return M diff --git a/lua/precognition/init.lua b/lua/precognition/init.lua index ff9c5c9..79e31fd 100644 --- a/lua/precognition/init.lua +++ b/lua/precognition/init.lua @@ -1,3 +1,5 @@ +local compat = require("precognition.compat") + local M = {} ---@class Precognition.HintOpts @@ -109,9 +111,10 @@ 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 @@ -119,7 +122,7 @@ local function build_virt_line(marks, line_len, extra_padding) 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 @@ -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(".") @@ -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) diff --git a/lua/precognition/utils.lua b/lua/precognition/utils.lua index 509198a..26b6505 100644 --- a/lua/precognition/utils.lua +++ b/lua/precognition/utils.lua @@ -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[] diff --git a/tests/precognition/e2e_spec.lua b/tests/precognition/e2e_spec.lua index 242839e..214d468 100644 --- a/tests/precognition/e2e_spec.lua +++ b/tests/precognition/e2e_spec.lua @@ -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({}) @@ -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 @@ -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, @@ -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 @@ -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 @@ -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) @@ -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 @@ -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 ") @@ -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) diff --git a/tests/precognition/inlay_hints_spec.lua b/tests/precognition/inlay_hints_spec.lua new file mode 100644 index 0000000..eaea20a --- /dev/null +++ b/tests/precognition/inlay_hints_spec.lua @@ -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) diff --git a/tests/precognition/utils/compat.lua b/tests/precognition/utils/compat.lua new file mode 100644 index 0000000..b36c14c --- /dev/null +++ b/tests/precognition/utils/compat.lua @@ -0,0 +1,5 @@ +local M = {} + +M.get_active_lsp_clients = vim.lsp.get_clients() or vim.lsp.get_active_clients() + +return M diff --git a/tests/precognition/utils/lsp.lua b/tests/precognition/utils/lsp.lua new file mode 100644 index 0000000..7218300 --- /dev/null +++ b/tests/precognition/utils/lsp.lua @@ -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 diff --git a/tests/precognition/utils/utils.lua b/tests/precognition/utils/utils.lua new file mode 100644 index 0000000..8f0a8cf --- /dev/null +++ b/tests/precognition/utils/utils.lua @@ -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 diff --git a/tests/precognition/virtline_spec.lua b/tests/precognition/virtline_spec.lua index 1a2a952..26efc7d 100644 --- a/tests/precognition/virtline_spec.lua +++ b/tests/precognition/virtline_spec.lua @@ -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))