diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 0000000..c8aa34d --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,21 @@ +--- +name: lua_ls-typecheck + +on: + push: + branches: + - "main" + pull_request: + branches: + - "main" + +jobs: + build: + name: Type Check Code Base + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: stevearc/nvim-typecheck-action@v2 + with: + level: Warning diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b03cb66 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contributing + +Contributions are welcome. What is expected from contributors is outlined below. If at any stage you require help, please just ask! + +## Issues first + +If there is something specific you want to work on, then please open an issue/discussion first to avoid duplication of efforts. Then: + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Make your changes +4. Review the steps below before committing your changes +5. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +6. Push to the Branch (`git push origin feature/AmazingFeature`) +7. Open a Pull Request + +## Before committing changes + +### Update documentation + +Ensure that the `README.md` is updated where changed due to the new feature. + +### Lua annotations + +Any new or changed functions and module level locals should be annotated with Lua specs. These not only provide documentation but also assist the Language Server with completion and signature information. You can learn more about Lua annotations [here](https://luals.github.io/wiki/annotations/). + +### Add tests + +Tests should be added to cover any changes or new features. These can be found in the `tests` folder. To run the tests, [Make](https://www.gnu.org/software/make/) is required. Run `make test` from the repository root. + +### Format code + +This project uses [StyLua](https://github.com/JohnnyMorganz/StyLua) to ensure consistent code formatting. + +The StyLua documentation details a number of ways this tool can be installed, including an executable you can just download. Then from the root of this repository run `stylua -g **/*.lua` (or `stylua -g **\*.lua` if on Windows). + +Please run StyLua before committing your code. Do not commit the StyLua executable to this repository. + +### Lint code + +This project uses [Luacheck](https://github.com/mpeterv/luacheck) for static analysis and linting of the code. + +### Continuous integration +The CI system used by this repository will run the tests, check the code formatting with Stylua, and lint the code with Luacheck. These checks must pass before a pull request can be merged, so performing these tasks locally first before committing will avoid having to push fix up commits. + +### Conventional commits for Commits and PR + +Please use the [Conventional Commits Specification](https://www.conventionalcommits.org/en/v1.0.0/) when writing your commit messages. diff --git a/README.md b/README.md index c645efd..310b168 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,10 @@ return { -- E = { text = "E", prio = 5 }, -- }, -- gutterHints = { - -- -- prio is not currently used for gutter hints - -- G = { text = "G", prio = 1 }, - -- gg = { text = "gg", prio = 1 }, - -- PrevParagraph = { text = "{", prio = 1 }, - -- NextParagraph = { text = "}", prio = 1 }, + -- G = { text = "G", prio = 10 }, + -- gg = { text = "gg", prio = 9 }, + -- PrevParagraph = { text = "{", prio = 8 }, + -- NextParagraph = { text = "}", prio = 8 }, -- }, }, } @@ -47,16 +46,21 @@ return { ## ⚙️ Config -- Items can be hidden by settings their priority to 0, if you want to hide the - entire virtual line. Set all elements to `prio = 0` in combination with the +- `hints` can be hidden by setting their priority to 0. If you want to hide the + entire virtual line, set all elements to `prio = 0` in combination with the below. - `showBlankVirtLine = false` - Setting this option will mean that if a Virtual Line would be blank it wont be + Setting this option will mean that if a Virtual Line would be blank it won't be rendered -- highlightColor can be set in two ways: +- `gutterHints` can be hidden by setting their priority to 0. +- `highlightColor` can be set in two ways: -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]()) + 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]()) + +### Hint priorities + +Any hints that could appear in the same place as others should have unique priorities to avoid conflicts. ## ❔Usage @@ -85,14 +89,10 @@ This plugin supports stable and nightly. >0.9 at the time of writing. Contributions are what makes the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. -If you have a suggestion to improve the plugin, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". -Don't forget to give the project a star! Thanks again! +If you have a suggestion to improve the plugin, please open an issue first, fork the repo, and create a pull request. -If there is something specific you want to work on then, please open an issue/discussion first to avoid duplication of efforts -If you have found a bug please open an issue, or submit a PR with a failing test. +If you have found a bug please open an issue, and submit a pull request with a failing test if possible. -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request +More details on how to contribute can be found in CONTRIBUTING.md. Please read this prior to creating a pull request. + +Don't forget to give the project a star! Thanks again! diff --git a/lua/precognition/horizontal_motions.lua b/lua/precognition/horizontal_motions.lua index 42b23d0..8cb1133 100644 --- a/lua/precognition/horizontal_motions.lua +++ b/lua/precognition/horizontal_motions.lua @@ -1,6 +1,3 @@ -local utils = require("precognition.utils") -local cc = utils.char_classes - local M = {} local supportedBrackets = { @@ -27,27 +24,29 @@ end ---@param str string ---@param cursorcol integer ----@param _linelen integer +---@param linelen integer ---@param big_word boolean ---@return Precognition.PlaceLoc -function M.next_word_boundary(str, cursorcol, _linelen, big_word) +function M.next_word_boundary(str, cursorcol, linelen, big_word) + local utils = require("precognition.utils") + local cc = utils.char_classes + local offset = cursorcol - local len = vim.fn.strcharlen(str) local char = vim.fn.strcharpart(str, offset - 1, 1) local c_class = utils.char_class(char, big_word) if c_class ~= cc.whitespace then - while utils.char_class(char, big_word) == c_class and offset <= len do + while utils.char_class(char, big_word) == c_class and offset <= linelen do offset = offset + 1 char = vim.fn.strcharpart(str, offset - 1, 1) end end - while utils.char_class(char, big_word) == cc.whitespace and offset <= len do + while utils.char_class(char, big_word) == cc.whitespace and offset <= linelen do offset = offset + 1 char = vim.fn.strcharpart(str, offset - 1, 1) end - if offset > len then + if offset > linelen then return 0 end @@ -63,6 +62,9 @@ function M.end_of_word(str, cursorcol, linelen, big_word) if cursorcol >= linelen then return 0 end + local utils = require("precognition.utils") + local cc = utils.char_classes + local offset = cursorcol local char = vim.fn.strcharpart(str, offset - 1, 1) local c_class = utils.char_class(char, big_word) @@ -70,7 +72,8 @@ function M.end_of_word(str, cursorcol, linelen, big_word) local rev_offset if - (c_class == cc.other and next_char_class ~= cc.other) or (next_char_class == cc.other and c_class ~= cc.other) + (c_class == cc.punctuation and next_char_class ~= cc.punctuation) + or (next_char_class == cc.punctuation and c_class ~= cc.punctuation) then offset = offset + 1 char = vim.fn.strcharpart(str, offset - 1, 1) @@ -114,11 +117,13 @@ end ---@param str string ---@param cursorcol integer ----@param _linelen integer +---@param linelen integer ---@param big_word boolean ---@return Precognition.PlaceLoc -function M.prev_word_boundary(str, cursorcol, _linelen, big_word) - local len = vim.fn.strcharlen(str) +function M.prev_word_boundary(str, cursorcol, linelen, big_word) + local utils = require("precognition.utils") + local cc = utils.char_classes + local offset = cursorcol - 1 local char = vim.fn.strcharpart(str, offset - 1, 1) local c_class = utils.char_class(char, big_word) @@ -134,14 +139,14 @@ function M.prev_word_boundary(str, cursorcol, _linelen, big_word) while utils.char_class(char, big_word) == c_class and offset >= 0 do offset = offset - 1 char = vim.fn.strcharpart(str, offset - 1, 1) - --if remaining string is whitespace, return nil_wrap + --if remaining string is whitespace, return 0 local remaining = string.sub(str, offset) if remaining:match("^%s*$") and #remaining > 0 then return 0 end end - if offset == nil or offset > len or offset < 0 then + if offset == nil or offset > linelen or offset < 0 then return 0 end return offset + 1 @@ -212,7 +217,7 @@ function M.matching_bracket(str, cursorcol, linelen) if under_cursor == closeBracket then local depth = 1 - offset = offset - 2 + offset = offset - 1 while offset >= 0 do local char = vim.fn.strcharpart(str, offset - 1, 1) if char == closeBracket then diff --git a/lua/precognition/init.lua b/lua/precognition/init.lua index 00f1fbc..18ed943 100644 --- a/lua/precognition/init.lua +++ b/lua/precognition/init.lua @@ -1,7 +1,3 @@ -local hm = require("precognition.horizontal_motions") -local vm = require("precognition.vertical_motions") -local utils = require("precognition.utils") - local M = {} ---@class Precognition.HintOpts @@ -54,6 +50,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 }, @@ -75,11 +75,10 @@ local default = { highlightColor = { link = "Comment" }, hints = defaultHintConfig, gutterHints = { - --prio is not currentlt used for gutter hints - G = { text = "G", prio = 1 }, - gg = { text = "gg", prio = 1 }, - PrevParagraph = { text = "{", prio = 1 }, - NextParagraph = { text = "}", prio = 1 }, + G = { text = "G", prio = 10 }, + gg = { text = "gg", prio = 9 }, + PrevParagraph = { text = "{", prio = 8 }, + NextParagraph = { text = "}", prio = 8 }, }, } @@ -108,8 +107,9 @@ local showcmd ---@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 @@ -117,7 +117,7 @@ local function build_virt_line(marks, line_len) return {} end local virt_line = {} - local line_table = utils.create_pad_array(line_len, " ") + local line_table = require("precognition.utils").create_pad_array(line_len, " ") for mark, loc in pairs(marks) do local hint = config.hints[mark].text or mark @@ -129,20 +129,26 @@ local function build_virt_line(marks, line_len) if existing == " " and existing ~= hint then line_table[col] = hint else -- if the character is not a space, then we need to check the prio - local existingKey + local existing_key for key, value in pairs(config.hints) do if value.text == existing then - existingKey = key + existing_key = key break end end - if existing ~= " " and config.hints[mark].prio > config.hints[existingKey].prio then + if existing ~= " " and config.hints[mark].prio > config.hints[existing_key].prio then line_table[col] = hint end end 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 {} @@ -153,6 +159,7 @@ end ---@return Precognition.GutterHints local function build_gutter_hints() + local vm = require("precognition.vertical_motions") ---@type Precognition.GutterHints local gutter_hints = { G = vm.file_end(), @@ -168,34 +175,50 @@ end ---@return nil local function apply_gutter_hints(gutter_hints, bufnr) bufnr = bufnr or vim.api.nvim_get_current_buf() - if utils.is_blacklisted_buffer(bufnr) then + if require("precognition.utils").is_blacklisted_buffer(bufnr) then return end + + local gutter_table = {} for hint, loc in pairs(gutter_hints) do - if config.gutterHints[hint] and loc ~= 0 and loc ~= nil then - if gutter_signs_cache[hint] then - vim.fn.sign_unplace(gutter_group, { id = gutter_signs_cache[hint].id }) - gutter_signs_cache[hint] = nil - end - vim.fn.sign_define(gutter_name_prefix .. hint, { - text = config.gutterHints[hint].text, - texthl = "PrecognitionHighlight", - }) - local ok, res = pcall(vim.fn.sign_place, 0, gutter_group, gutter_name_prefix .. hint, bufnr, { - lnum = loc, - priority = 100, - }) - if ok then - gutter_signs_cache[hint] = { line = loc, id = res } - end - if not ok and loc ~= 0 then - vim.notify_once( - "Failed to place sign: " .. hint .. " at line " .. loc .. vim.inspect(res), - vim.log.levels.WARN - ) + if gutter_signs_cache[hint] then + vim.fn.sign_unplace(gutter_group, { id = gutter_signs_cache[hint].id }) + gutter_signs_cache[hint] = nil + end + + local prio = config.gutterHints[hint].prio + + -- Build table of valid and priorised gutter hints. + if loc ~= 0 and loc ~= nil and prio > 0 then + local existing = gutter_table[loc] + if not existing or existing.prio < prio then + gutter_table[loc] = { hint = hint, prio = prio } end end end + + -- Only render valid and prioritised gutter hints. + for loc, data in pairs(gutter_table) do + local hint = data.hint + local sign_name = gutter_name_prefix .. hint + vim.fn.sign_define(sign_name, { + text = config.gutterHints[hint].text, + texthl = "PrecognitionHighlight", + }) + local ok, res = pcall(vim.fn.sign_place, 0, gutter_group, sign_name, bufnr, { + lnum = loc, + priority = 100, + }) + if ok then + gutter_signs_cache[hint] = { line = loc, id = res } + end + if not ok and loc ~= 0 then + vim.notify_once( + "Failed to place sign: " .. hint .. " at line " .. loc .. vim.inspect(res), + vim.log.levels.WARN + ) + end + end end local function display_marks() @@ -205,11 +228,11 @@ local function display_marks() return end local bufnr = vim.api.nvim_get_current_buf() - if utils.is_blacklisted_buffer(bufnr) then + if require("precognition.utils").is_blacklisted_buffer(bufnr) then return end - local cursorline, cursorcol = unpack(vim.api.nvim_win_get_cursor(0)) - cursorcol = cursorcol + 1 + local cursorline = vim.fn.line(".") + local cursorcol = vim.fn.charcol(".") if extmark and not dirty then return end @@ -217,11 +240,15 @@ 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) -- local under_cursor = vim.fn.strcharpart(cur_line, cursorcol - 1, 1) + local hm = require("precognition.horizontal_motions") + -- FIXME: Lua patterns don't play nice with utf-8, we need a better way to -- get char offsets for more complex motions. -- @@ -239,7 +266,11 @@ local function display_marks() Zero = 1, } - local virt_line = build_virt_line(virtual_line_marks, line_len) + --multicharacter padding + + require("precognition.utils").add_multibyte_padding(cur_line, extra_padding, line_len) + + 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)? diff --git a/lua/precognition/utils.lua b/lua/precognition/utils.lua index 7a303ce..b1ca53b 100644 --- a/lua/precognition/utils.lua +++ b/lua/precognition/utils.lua @@ -3,29 +3,35 @@ local M = {} ---@enum cc M.char_classes = { whitespace = 0, - other = 1, + punctuation = 1, word = 2, + emoji = 3, + other = "other", + UNKNOWN = -1, } ---@param char string ---@param big_word boolean ----@return integer +---@return cc function M.char_class(char, big_word) assert(type(big_word) == "boolean", "big_word must be a boolean") local cc = M.char_classes - local byte = string.byte(char) - if byte and byte < 0x100 then - if char == " " or char == "\t" or char == "\0" then - return cc.whitespace - end - if char == "_" or char:match("%w") then - return big_word and cc.other or cc.word - end - return cc.other + if char == "" then + return cc.UNKNOWN + end + + if char == "\0" then + return cc.whitespace + end + + local c_class = vim.fn.charclass(char) + + if big_word and c_class ~= 0 then + return cc.punctuation end - return cc.other -- scary unicode edge cases go here + return c_class end ---@param bufnr? integer @@ -104,4 +110,18 @@ function M.create_pad_array(len, str) return pad_array end +---Add extra padding for multi byte character characters +---@param cur_line string +---@param extra_padding Precognition.ExtraPadding[] +---@param line_len integer +function M.add_multibyte_padding(cur_line, extra_padding, line_len) + for i = 1, line_len do + local char = vim.fn.strcharpart(cur_line, i - 1, 1) + local width = vim.fn.strdisplaywidth(char) + if width > 1 then + table.insert(extra_padding, { start = i, length = width - 1 }) + end + end +end + return M diff --git a/tests/precognition/char_spec.lua b/tests/precognition/char_spec.lua index e9041e7..2826fe9 100644 --- a/tests/precognition/char_spec.lua +++ b/tests/precognition/char_spec.lua @@ -6,8 +6,11 @@ local eq = assert.are.same describe("static classes", function() it("are set correctly", function() eq(cc.whitespace, 0) - eq(cc.other, 1) + eq(cc.punctuation, 1) eq(cc.word, 2) + eq(cc.emoji, 3) + eq(cc.other, "other") + eq(cc.UNKNOWN, -1) end) end) @@ -30,6 +33,17 @@ describe("char classing", function() eq(utils.char_class("@", false), 1) eq(utils.char_class(".", false), 1) end) + + it("can class emoji characters", function() + eq(utils.char_class("🐱", false), 3) + eq(utils.char_class("😸", false), 3) + eq(utils.char_class("💩", false), 3) + end) + + it("can class nerdfont characters", function() + eq(utils.char_class("", false), 2) + eq(utils.char_class("", false), 2) + end) end) describe("big_word classing", function() diff --git a/tests/precognition/e2e_spec.lua b/tests/precognition/e2e_spec.lua index 7493c09..24fb435 100644 --- a/tests/precognition/e2e_spec.lua +++ b/tests/precognition/e2e_spec.lua @@ -270,3 +270,65 @@ describe("e2e tests", function() eq(customMark, vim.api.nvim_get_hl(0, { name = extmarks[3].virt_lines[1][1][2] })) end) end) + +describe("Gutter Priority", function() + it("0 priority item is not added", function() + precognition.setup({ + ---@diagnostic disable-next-line: missing-fields + gutterHints = { + G = { text = "G", prio = 0 }, + }, + }) + + local testBuf = vim.api.nvim_create_buf(true, false) + + vim.api.nvim_buf_set_lines(testBuf, 0, -1, false, { + "ABC", + "DEF", + "", + "GHI", + "", + "JKL", + "", + "MNO", + }) + vim.api.nvim_set_current_buf(testBuf) + vim.api.nvim_win_set_cursor(0, { 4, 0 }) + + precognition.on_cursor_moved() + + local gutter_extmarks = get_gutter_extmarks(testBuf) + + for _, extmark in pairs(gutter_extmarks) do + eq(true, extmark[4].sign_text ~= "G ") + eq(true, extmark[4].sign_name ~= "precognition_gutter_G") + end + end) + + it("higher priority item replaces", function() + precognition.setup({ + ---@diagnostic disable-next-line: missing-fields + gutterHints = { + G = { text = "G", prio = 3 }, + gg = { text = "gg", prio = 100 }, + NextParagraph = { text = "}", prio = 2 }, + PrevParagraph = { text = "{", prio = 1 }, + }, + }) + + local testBuf = vim.api.nvim_create_buf(true, false) + + vim.api.nvim_buf_set_lines(testBuf, 0, -1, false, { + "ABC", + }) + vim.api.nvim_set_current_buf(testBuf) + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + + precognition.on_cursor_moved() + + local gutter_extmarks = get_gutter_extmarks(testBuf) + + eq(1, vim.tbl_count(gutter_extmarks)) + eq("gg", gutter_extmarks[1][4].sign_text) + end) +end) diff --git a/tests/precognition/horizontal_motions_spec.lua b/tests/precognition/horizontal_motions_spec.lua index 5500d5d..42877ff 100644 --- a/tests/precognition/horizontal_motions_spec.lua +++ b/tests/precognition/horizontal_motions_spec.lua @@ -109,6 +109,9 @@ describe("boundaries", function() local str = "a big.word string" eq(3, hm.prev_word_boundary(str, 10, #str, true)) eq(3, hm.prev_word_boundary(str, 10, #str, true)) + + str = "big.word" + eq(1, hm.prev_word_boundary(str, 5, #str, true)) end) it("can walk string with b", function() @@ -304,4 +307,13 @@ describe("edge case", function() eq(16, hm.end_of_word(str, 15, #str, false)) eq(22, hm.end_of_word(str, 21, #str, false)) end) + + it("multibyte characters", function() + local str = "# 💭👀precognition.nvim" + local len = vim.fn.strcharlen(str) + eq(3, hm.next_word_boundary(str, 1, len, false)) + eq(17, hm.prev_word_boundary(str, 18, len, false)) + eq(3, hm.prev_word_boundary(str, 18, len, true)) + eq(3, hm.prev_word_boundary(str, 5, len, false)) + end) end) diff --git a/tests/precognition/virtline_spec.lua b/tests/precognition/virtline_spec.lua index 0bbf0cd..1a2a952 100644 --- a/tests/precognition/virtline_spec.lua +++ b/tests/precognition/virtline_spec.lua @@ -1,5 +1,6 @@ local precognition = require("precognition") local hm = require("precognition.horizontal_motions") +local utils = require("precognition.utils") ---@diagnostic disable-next-line: undefined-field local eq = assert.are.same describe("Build Virtual Line", function() @@ -8,7 +9,7 @@ describe("Build Virtual Line", function() Caret = 4, Dollar = 10, } - local virtual_line = precognition.build_virt_line(marks, 10) + local virtual_line = precognition.build_virt_line(marks, 10, {}) eq(" ^ $", virtual_line[1][1]) eq(10, #virtual_line[1][1]) end) @@ -17,7 +18,7 @@ describe("Build Virtual Line", function() local marks = { Caret = 4, } - local virtual_line = precognition.build_virt_line(marks, 10) + local virtual_line = precognition.build_virt_line(marks, 10, {}) eq(" ^ ", virtual_line[1][1]) eq(10, #virtual_line[1][1]) end) @@ -26,7 +27,7 @@ describe("Build Virtual Line", function() local marks = { Dollar = 10, } - local virtual_line = precognition.build_virt_line(marks, 10) + local virtual_line = precognition.build_virt_line(marks, 10, {}) eq(" $", virtual_line[1][1]) eq(10, #virtual_line[1][1]) end) @@ -35,7 +36,7 @@ describe("Build Virtual Line", function() local marks = { Caret = 1, } - local virtual_line = precognition.build_virt_line(marks, 10) + local virtual_line = precognition.build_virt_line(marks, 10, {}) eq("^ ", virtual_line[1][1]) eq(10, #virtual_line[1][1]) end) @@ -50,7 +51,7 @@ describe("Build Virtual Line", function() w = 10, Dollar = 50, } - local virtual_line = precognition.build_virt_line(marks, 50) + local virtual_line = precognition.build_virt_line(marks, 50, {}) local line_num = 1 for char in virtual_line[1][1]:gmatch(".") do if line_num == 1 then @@ -84,7 +85,7 @@ describe("Build Virtual Line", function() b = hm.prev_word_boundary(cur_line, cursorcol, line_len, false), Caret = hm.line_start_non_whitespace(cur_line, cursorcol, line_len), Dollar = hm.line_end(cur_line, cursorcol, line_len), - }, line_len) + }, line_len, {}) eq("b e w $", virt_line[1][1]) eq(#line, #virt_line[1][1]) @@ -104,11 +105,83 @@ describe("Build Virtual Line", function() b = hm.prev_word_boundary(cur_line, cursorcol, line_len, false), Caret = hm.line_start_non_whitespace(cur_line, cursorcol, line_len), Dollar = hm.line_end(cur_line, cursorcol, line_len), - }, line_len) + }, line_len, {}) eq(" ^ e w $", virt_line[1][1]) eq(#line, #virt_line[1][1]) end) + + it("can build a line with extra padding", function() + local line = " 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)) + local line_len = vim.fn.strcharlen(cur_line) + local extra_padding = { { start = 4, length = 4 } } + + local virt_line = precognition.build_virt_line({ + w = hm.next_word_boundary(cur_line, cursorcol, line_len, false), + e = hm.end_of_word(cur_line, cursorcol, line_len, false), + b = hm.prev_word_boundary(cur_line, cursorcol, line_len, false), + Caret = hm.line_start_non_whitespace(cur_line, cursorcol, line_len), + Dollar = hm.line_end(cur_line, cursorcol, line_len), + }, line_len, extra_padding) + + local total_added = 0 + for _, pad in ipairs(extra_padding) do + total_added = total_added + pad.length + end + + eq(" ^ e w $", virt_line[1][1]) + eq(#line + total_added, #virt_line[1][1]) + end) + + it("can build a line with multiple extra padddings", function() + local line = " 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)) + local line_len = vim.fn.strcharlen(cur_line) + local extra_padding = { { start = 4, length = 4 }, { start = 10, length = 5 } } + + local virt_line = precognition.build_virt_line({ + w = hm.next_word_boundary(cur_line, cursorcol, line_len, false), + e = hm.end_of_word(cur_line, cursorcol, line_len, false), + b = hm.prev_word_boundary(cur_line, cursorcol, line_len, false), + Caret = hm.line_start_non_whitespace(cur_line, cursorcol, line_len), + Dollar = hm.line_end(cur_line, cursorcol, line_len), + }, line_len, extra_padding) + + local total_added = 0 + for _, pad in ipairs(extra_padding) do + total_added = total_added + pad.length + end + + eq(" ^ e w $", virt_line[1][1]) + eq(#line + total_added, #virt_line[1][1]) + end) + + it("example virtual line with emoji", function() + local line = "# 💭👀precognition.nvim" + local cursorcol = 20 + local tab_width = vim.bo.expandtab and vim.bo.shiftwidth or vim.bo.tabstop + local cur_line = line:gsub("\t", string.rep(" ", tab_width)) + local line_len = vim.fn.strcharlen(cur_line) + local extra_padding = {} + + utils.add_multibyte_padding(cur_line, extra_padding, line_len) + + local virt_line = precognition.build_virt_line({ + w = hm.next_word_boundary(cur_line, cursorcol, line_len, false), + e = hm.end_of_word(cur_line, cursorcol, line_len, false), + b = hm.prev_word_boundary(cur_line, cursorcol, line_len, false), + Caret = hm.line_start_non_whitespace(cur_line, cursorcol, line_len), + Dollar = hm.line_end(cur_line, cursorcol, line_len), + }, line_len, extra_padding) + + eq("^ b e", virt_line[1][1]) + eq(vim.fn.strdisplaywidth(line), #virt_line[1][1]) + end) end) describe("Priority", function() @@ -133,7 +206,7 @@ describe("Priority", function() Dollar = 10, } - local virtual_line = precognition.build_virt_line(marks, 10) + local virtual_line = precognition.build_virt_line(marks, 10, {}) eq(" w ", virtual_line[1][1]) eq(10, #virtual_line[1][1]) end) @@ -159,7 +232,7 @@ describe("Priority", function() Dollar = 10, } - local virtual_line = precognition.build_virt_line(marks, 10) + local virtual_line = precognition.build_virt_line(marks, 10, {}) eq(" w $", virtual_line[1][1]) eq(10, #virtual_line[1][1]) end) @@ -184,7 +257,7 @@ describe("Priority", function() Dollar = 1, } - local virtual_line = precognition.build_virt_line(marks, 1) + local virtual_line = precognition.build_virt_line(marks, 1, {}) eq("$", virtual_line[1][1]) eq(1, #virtual_line[1][1]) @@ -202,7 +275,7 @@ describe("Priority", function() }, }) - virtual_line = precognition.build_virt_line(marks, 1) + virtual_line = precognition.build_virt_line(marks, 1, {}) eq("^", virtual_line[1][1]) eq(1, #virtual_line[1][1]) end) @@ -224,7 +297,7 @@ describe("replacment charcters", function() Caret = 1, } - local virtual_line = precognition.build_virt_line(marks, 1) + local virtual_line = precognition.build_virt_line(marks, 1, {}) eq("x", virtual_line[1][1]) eq(1, #virtual_line[1][1]) end) @@ -244,7 +317,7 @@ describe("replacment charcters", function() Caret = 1, } - local virtual_line = precognition.build_virt_line(marks, 1) + local virtual_line = precognition.build_virt_line(marks, 1, {}) eq("â", virtual_line[1][1]) eq(2, #virtual_line[1][1]) end) @@ -261,7 +334,7 @@ describe("replacment charcters", function() Dollar = 8, } - local virtual_line = precognition.build_virt_line(marks, 8) + local virtual_line = precognition.build_virt_line(marks, 8, {}) eq("0^e w $", virtual_line[1][1]) end) @@ -286,7 +359,7 @@ describe("replacment charcters", function() Dollar = 8, } - local virtual_line = precognition.build_virt_line(marks, 8) + local virtual_line = precognition.build_virt_line(marks, 8, {}) eq("0âe w $", virtual_line[1][1]) end) end)