From 7a76d432f9d30f7ae79968a464d2fced69149d39 Mon Sep 17 00:00:00 2001 From: Tristan Knight Date: Fri, 14 Jun 2024 12:42:16 +0100 Subject: [PATCH 1/5] fix(horizontal_motions): handle quoted strings in word boundaries (#68) * fix(horizontal_motions): handle quoted strings in word boundaries This commit improves the handling of quoted strings in word boundaries. The `end_of_word` function now correctly identifies the end of a word when the next word starts with punctuation or is a single character. A new test case has been added to verify this behavior. * fix(horizontal_motions): handle punctuation in end_of_word This commit modifies the `end_of_word` function in `horizontal_motions.lua` to handle cases where the next word starts with punctuation. This change ensures that the function correctly identifies the end of a word in such scenarios. Corresponding tests have been added in `horizontal_motions_spec.lua` to verify this behavior. --- lua/precognition/horizontal_motions.lua | 6 +++++- tests/precognition/horizontal_motions_spec.lua | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lua/precognition/horizontal_motions.lua b/lua/precognition/horizontal_motions.lua index 8cb1133..e4e858d 100644 --- a/lua/precognition/horizontal_motions.lua +++ b/lua/precognition/horizontal_motions.lua @@ -91,9 +91,13 @@ function M.end_of_word(str, cursorcol, linelen, big_word) if c_class == cc.whitespace or next_char_class == cc.whitespace then local next_word_start = M.next_word_boundary(str, cursorcol, linelen, big_word) if next_word_start then + c_class = utils.char_class(vim.fn.strcharpart(str, next_word_start - 1, 1), big_word) next_char_class = utils.char_class(vim.fn.strcharpart(str, (next_word_start - 1) + 1, 1), big_word) - --next word is single char if next_char_class == cc.whitespace then + --next word is single char + rev_offset = next_word_start + elseif c_class == cc.punctuation and next_char_class ~= cc.punctuation then + --next word starts with punctuation rev_offset = next_word_start else rev_offset = M.end_of_word(str, next_word_start, linelen, big_word) diff --git a/tests/precognition/horizontal_motions_spec.lua b/tests/precognition/horizontal_motions_spec.lua index 42877ff..7d0e0c9 100644 --- a/tests/precognition/horizontal_motions_spec.lua +++ b/tests/precognition/horizontal_motions_spec.lua @@ -316,4 +316,12 @@ describe("edge case", function() eq(3, hm.prev_word_boundary(str, 18, len, true)) eq(3, hm.prev_word_boundary(str, 5, len, false)) end) + + it("quoted strings", function() + local str = 'this = "that"' + eq(8, hm.end_of_word(str, 6, #str, false)) + + str = 'b = "^", c = 2 },' + eq(8, hm.end_of_word(str, 3, #str, false)) + end) end) From 6aab38a34847bc2881138541ed5a91ed7b058086 Mon Sep 17 00:00:00 2001 From: Osamu Aoki Date: Mon, 17 Jun 2024 06:44:00 +0900 Subject: [PATCH 2/5] feat: toggling returns visible value (#73) --- README.md | 10 ++++++++++ lua/precognition/init.lua | 2 ++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 8f4821f..b972a78 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,16 @@ or require("precognition").toggle() ``` +The return value indicating the visible state can be used to produce a notification. + +```lua +if require("precognition").toggle() then + vim.notify("precognition on") +else + vim.notify("precognition off") +end +``` + The subcommands and functions `show` and `hide` are also available. ### Peeking diff --git a/lua/precognition/init.lua b/lua/precognition/init.lua index b0b9d49..c4f5559 100644 --- a/lua/precognition/init.lua +++ b/lua/precognition/init.lua @@ -407,12 +407,14 @@ function M.hide() end --- Toggle automatic showing of hints +--- with return value indicating the visible state function M.toggle() if visible then M.hide() else M.show() end + return visible end ---@param opts Precognition.PartialConfig From 02dcc8f8db677fe02d2dd68da6155177283fe711 Mon Sep 17 00:00:00 2001 From: Travis Finkenauer Date: Sun, 16 Jun 2024 14:45:17 -0700 Subject: [PATCH 3/5] fix: support calling setup() w/o args (#71) Co-authored-by: Tristan Knight --- lua/precognition/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/precognition/init.lua b/lua/precognition/init.lua index c4f5559..1d00e27 100644 --- a/lua/precognition/init.lua +++ b/lua/precognition/init.lua @@ -419,7 +419,8 @@ end ---@param opts Precognition.PartialConfig function M.setup(opts) - config = vim.tbl_deep_extend("force", default, opts or {}) + opts = opts or {} + config = vim.tbl_deep_extend("force", default, opts) if opts.highlightColor then config.highlightColor = opts.highlightColor end From 2a566f03eb06859298eff837f3a6686dfa5304a5 Mon Sep 17 00:00:00 2001 From: Mauricio A Rovira Galvez <8482308+marovira@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:17:47 -0700 Subject: [PATCH 4/5] feat: disable by filetype (#78) --- README.md | 5 +++++ lua/precognition/init.lua | 4 ++++ lua/precognition/utils.lua | 13 ++++++++++++- tests/precognition/blacklist_spec.lua | 7 +++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b972a78..906b919 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ return { -- PrevParagraph = { text = "{", prio = 8 }, -- NextParagraph = { text = "}", prio = 8 }, -- }, + -- disabled_fts = { + -- "startify", + -- }, }, } ``` @@ -58,6 +61,8 @@ return { 1. As a table containing a link property pointing to an existing highlight group (see `:highlight` for valid options). 2. As a table specifying custom highlight values, such as foreground and background colors. ([more info]()) +- `disabled_fts` can be used to disable `precognition` on specific filetypes. + ### Hint priorities Any hints that could appear in the same place as others should have unique priorities to avoid conflicts. diff --git a/lua/precognition/init.lua b/lua/precognition/init.lua index 1d00e27..ff9c5c9 100644 --- a/lua/precognition/init.lua +++ b/lua/precognition/init.lua @@ -27,6 +27,7 @@ local M = {} ---@field highlightColor vim.api.keyset.highlight ---@field hints Precognition.HintConfig ---@field gutterHints Precognition.GutterHintConfig +---@field disabled_fts string[] ---@class Precognition.PartialConfig ---@field startVisible? boolean @@ -80,6 +81,9 @@ local default = { PrevParagraph = { text = "{", prio = 8 }, NextParagraph = { text = "}", prio = 8 }, }, + disabled_fts = { + "startify", + }, } ---@type Precognition.Config diff --git a/lua/precognition/utils.lua b/lua/precognition/utils.lua index 256ee9a..509198a 100644 --- a/lua/precognition/utils.lua +++ b/lua/precognition/utils.lua @@ -35,12 +35,23 @@ function M.char_class(char, big_word) end ---@param bufnr? integer +---@param disabled_fts? string[] ---@return boolean -function M.is_blacklisted_buffer(bufnr) +function M.is_blacklisted_buffer(bufnr, disabled_fts) bufnr = bufnr or vim.api.nvim_get_current_buf() if vim.api.nvim_get_option_value("buftype", { buf = bufnr }) ~= "" then return true end + + if disabled_fts == nil then + return false + end + + for _, ft in ipairs(disabled_fts) do + if vim.api.nvim_get_option_value("filetype", { buf = bufnr }) == ft then + return true + end + end return false end diff --git a/tests/precognition/blacklist_spec.lua b/tests/precognition/blacklist_spec.lua index 94d4810..7853bf0 100644 --- a/tests/precognition/blacklist_spec.lua +++ b/tests/precognition/blacklist_spec.lua @@ -48,4 +48,11 @@ describe("blacklist buffers", function() vim.api.nvim_open_term(test_buffer, {}) eq(utils.is_blacklisted_buffer(test_buffer), true) end) + + it("blacklisted buffer by filetype", function() + local test_buffer = vim.api.nvim_create_buf(true, false) + local test_fts = { "startify" } + vim.api.nvim_set_option_value("filetype", "startify", { buf = test_buffer }) + eq(utils.is_blacklisted_buffer(test_buffer, test_fts), true) + end) end) From 6772d3a6aa98f6e68b53fd7d4dfd7762bf3ae3bf Mon Sep 17 00:00:00 2001 From: Tristan Knight Date: Wed, 31 Jul 2024 22:24:09 +0100 Subject: [PATCH 5/5] feat: inlay hints (#38) Co-authored-by: Will Hopkins --- .luarc.json | 1 + lua/precognition/compat.lua | 10 ++++ lua/precognition/init.lua | 26 +++++++-- lua/precognition/utils.lua | 16 ++++++ tests/precognition/e2e_spec.lua | 42 +++----------- tests/precognition/inlay_hints_spec.lua | 74 +++++++++++++++++++++++++ tests/precognition/utils/compat.lua | 5 ++ tests/precognition/utils/lsp.lua | 53 ++++++++++++++++++ tests/precognition/utils/utils.lua | 30 ++++++++++ tests/precognition/virtline_spec.lua | 1 - 10 files changed, 220 insertions(+), 38 deletions(-) create mode 100644 lua/precognition/compat.lua create mode 100644 tests/precognition/inlay_hints_spec.lua create mode 100644 tests/precognition/utils/compat.lua create mode 100644 tests/precognition/utils/lsp.lua create mode 100644 tests/precognition/utils/utils.lua 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))