diff --git a/.github/workflows/dts.yml b/.github/workflows/dts.yml new file mode 100644 index 0000000..63618fa --- /dev/null +++ b/.github/workflows/dts.yml @@ -0,0 +1,27 @@ +name: DTS + +on: + pull_request: + branches: + - "main" + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + nvim-versions: ['nightly'] + os: [ubuntu-latest] + fail-fast: false + name: DTS Tests + steps: + - name: checkout + uses: actions/checkout@v4 + + - uses: rhysd/action-setup-vim@v1 + with: + neovim: true + version: ${{ matrix.nvim-versions }} + + - name: run dts + run: make dts diff --git a/Makefile b/Makefile index bbf2ada..ca25237 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ TESTS_INIT=tests/minimal.lua TESTS_DIR=tests/ +DTS_SCRIPT=tests/precognition/dts.lua +SEED_START=0 +NUM_TESTS=500000 .PHONY: test @@ -9,3 +12,9 @@ test: --noplugin \ -u ${TESTS_INIT} \ -c "PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TESTS_INIT}' }" \ + +dts: + @nvim \ + --headless \ + -u ${TESTS_INIT} \ + -l ${DTS_SCRIPT} ${SEED_START} ${NUM_TESTS} \ diff --git a/lua/precognition/horizontal_motions.lua b/lua/precognition/horizontal_motions.lua deleted file mode 100644 index e4e858d..0000000 --- a/lua/precognition/horizontal_motions.lua +++ /dev/null @@ -1,303 +0,0 @@ -local M = {} - -local supportedBrackets = { - open = { "(", "[", "{" }, - middle = { nil, nil, nil }, - close = { ")", "]", "}" }, -} - ----@param str string ----@param _cursorcol integer ----@param _linelen integer ----@return Precognition.PlaceLoc -function M.line_start_non_whitespace(str, _cursorcol, _linelen) - return str:find("%S") or 0 -end - ----@param _str string ----@param _cursorcol integer ----@param linelen integer ----@return Precognition.PlaceLoc -function M.line_end(_str, _cursorcol, linelen) - return linelen or nil -end - ----@param str string ----@param cursorcol integer ----@param linelen integer ----@param big_word boolean ----@return Precognition.PlaceLoc -function M.next_word_boundary(str, cursorcol, linelen, big_word) - 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) - - if c_class ~= cc.whitespace then - 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 <= linelen do - offset = offset + 1 - char = vim.fn.strcharpart(str, offset - 1, 1) - end - if offset > linelen then - return 0 - end - - return offset -end - ----@param str string ----@param cursorcol integer ----@param linelen integer ----@param big_word boolean ----@return Precognition.PlaceLoc -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) - local next_char_class = utils.char_class(vim.fn.strcharpart(str, (offset - 1) + 1, 1), big_word) - local rev_offset - - if - (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) - c_class = utils.char_class(char, big_word) - next_char_class = utils.char_class(vim.fn.strcharpart(str, (offset - 1) + 1, 1), big_word) - end - - if c_class ~= cc.whitespace and next_char_class ~= cc.whitespace then - 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 - - 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) - 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) - end - end - end - - if rev_offset and rev_offset <= 0 then - return 0 - end - - if rev_offset ~= nil then - --e should never be behind the cursor - if rev_offset < cursorcol then - return 0 - end - return rev_offset - end - return offset - 1 -end - ----@param str string ----@param cursorcol integer ----@param linelen integer ----@param big_word boolean ----@return Precognition.PlaceLoc -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) - - if c_class == cc.whitespace then - while utils.char_class(char, big_word) == cc.whitespace and offset >= 0 do - offset = offset - 1 - char = vim.fn.strcharpart(str, offset - 1, 1) - end - c_class = utils.char_class(char, big_word) - end - - 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 0 - local remaining = string.sub(str, offset) - if remaining:match("^%s*$") and #remaining > 0 then - return 0 - end - end - - if offset == nil or offset > linelen or offset < 0 then - return 0 - end - return offset + 1 -end - ----@param str string ----@param cursorcol integer ----@param linelen integer ----@return Precognition.PlaceLoc -function M.matching_bracket(str, cursorcol, linelen) - local under_cursor = vim.fn.strcharpart(str, cursorcol - 1, 1) - local offset = cursorcol - - if - not vim.tbl_contains(supportedBrackets.open, under_cursor) - and not vim.tbl_contains(supportedBrackets.close, under_cursor) - then - -- walk until we find a bracket - return 0 - end - local idxFound = false - local bracketIdx - if not idxFound then - for i, bracket in ipairs(supportedBrackets.open) do - if bracket == under_cursor then - bracketIdx = i - idxFound = true - break - end - end - end - - if not idxFound then - for i, bracket in ipairs(supportedBrackets.close) do - if bracket == under_cursor then - bracketIdx = i - idxFound = true - break - end - end - end - - if not idxFound then - return 0 - end - - local openBracket = supportedBrackets.open[bracketIdx] or "" - local closeBracket = supportedBrackets.close[bracketIdx] or "" - local middleBracket = supportedBrackets.middle[bracketIdx] or "" - - if under_cursor == openBracket then - local depth = 1 - offset = offset + 1 - while offset <= linelen do - local char = vim.fn.strcharpart(str, offset - 1, 1) - if char == openBracket then - depth = depth + 1 - end - if char == closeBracket or char == middleBracket then - depth = depth - 1 - if depth == 0 then - break - end - end - offset = offset + 1 - end - end - - if under_cursor == closeBracket then - local depth = 1 - offset = offset - 1 - while offset >= 0 do - local char = vim.fn.strcharpart(str, offset - 1, 1) - if char == closeBracket then - depth = depth + 1 - end - if char == openBracket or char == middleBracket then - depth = depth - 1 - if depth == 0 then - break - end - end - offset = offset - 1 - end - end - - if offset < 0 or offset > linelen then - return 0 - end - return offset -end - ----@param str string ----@param cursorcol integer ----@param linelen integer ----@return Precognition.PlaceLoc -function M.matching_comment(str, cursorcol, linelen) - local offset = cursorcol - local char = vim.fn.strcharpart(str, offset - 1, 1) - local next_char = vim.fn.strcharpart(str, (offset - 1) + 1, 1) - local prev_char = vim.fn.strcharpart(str, (offset - 1) - 1, 1) - - if (char == "/" and next_char == "*") or (prev_char == "/" and char == "*") then - offset = offset + 1 - while offset <= linelen do - char = vim.fn.strcharpart(str, offset - 1, 1) - next_char = vim.fn.strcharpart(str, offset, 1) - if char == "*" and next_char == "/" then - -- return the slash of the closing comment - return offset + 1 - end - offset = offset + 1 - end - end - - if (char == "*" and next_char == "/") or (prev_char == "*" and char == "/") then - offset = offset - 1 - while offset >= 0 do - char = vim.fn.strcharpart(str, offset - 1, 1) - next_char = vim.fn.strcharpart(str, offset, 1) - if char == "/" and next_char == "*" then - return offset - end - offset = offset - 1 - end - end - - return 0 -end - ----@param str string ----@param cursorcol integer ----@param _linelen integer ----@return function -function M.matching_pair(str, cursorcol, _linelen) - local char = vim.fn.strcharpart(str, cursorcol - 1, 1) - if char == "/" or char == "*" then - return M.matching_comment - end - - if vim.tbl_contains(supportedBrackets.open, char) or vim.tbl_contains(supportedBrackets.close, char) then - return M.matching_bracket - end - - return function() - return 0 - end -end - -return M diff --git a/lua/precognition/init.lua b/lua/precognition/init.lua index 7b74bf7..560da09 100644 --- a/lua/precognition/init.lua +++ b/lua/precognition/init.lua @@ -243,29 +243,18 @@ local function display_marks() 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. - -- + ---@type Precognition.VirtLine - local virtual_line_marks = { - Caret = hm.line_start_non_whitespace(cur_line, cursorcol, line_len), - 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), - W = hm.next_word_boundary(cur_line, cursorcol, line_len, true), - E = hm.end_of_word(cur_line, cursorcol, line_len, true), - B = hm.prev_word_boundary(cur_line, cursorcol, line_len, true), - MatchingPair = hm.matching_pair(cur_line, cursorcol, line_len)(cur_line, cursorcol, line_len), - Dollar = hm.line_end(cur_line, cursorcol, line_len), - Zero = 1, - } + local virtual_line_marks = require("precognition.sim").check(cur_line, cursorcol) + ---return 0 for any hint that is not found in the simmed table + setmetatable(virtual_line_marks, { + __index = function() + return 0 + end, + }) if compat.inlay_hints_enabled({ bufnr = 0 }) then local inlays_hints = vim.lsp.inlay_hint.get({ @@ -490,6 +479,9 @@ local state = { ns = function() return ns end, + default_hint_config = function() + return defaultHintConfig + end, is_visible = function() return visible end, diff --git a/lua/precognition/sim.lua b/lua/precognition/sim.lua new file mode 100644 index 0000000..c797fb6 --- /dev/null +++ b/lua/precognition/sim.lua @@ -0,0 +1,71 @@ +local mt = require("mini.test") + +local M = {} + +local remote_instance = nil + +-- Metatable for remote instance +local remote_mt = { + __gc = function(self) + if self.instance then + self.instance.stop() + end + end, +} + +local function set_opts(cpooptions) + vim.o.cpoptions = cpooptions +end + +local function get_remote() + if not remote_instance then + local remote = mt.new_child_neovim() + remote.start() + remote.lua_func(set_opts, vim.o.cpoptions) + remote_instance = setmetatable({ instance = remote }, remote_mt) + end + return remote_instance.instance +end + +local function check_pos(string, col, default_config) + local result = {} + local locations = vim.tbl_keys(default_config) + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { " ", string, " " }) + vim.api.nvim_set_current_buf(buf) + for _, motion_name in ipairs(locations) do + local motion_key = default_config[motion_name].text + vim.fn.setcursorcharpos(2, col) + local start_col = vim.fn.getcursorcharpos(0)[3] + vim.api.nvim_feedkeys(motion_key, "x", true) + local cur_pos = vim.fn.getcursorcharpos(0) + if cur_pos[2] == 2 then + if motion_name == "MatchingPair" then + if cur_pos[3] ~= start_col then + result[motion_name] = cur_pos[3] + end + else + result[motion_name] = cur_pos[3] + end + end + end + + vim.api.nvim_buf_delete(buf, { force = true }) + + return result +end + +M.stop = function() + if remote_instance and remote_instance.instance then + remote_instance.instance.stop() + remote_instance.instance = nil + end +end + +M.check = function(line, col) + local remote = get_remote() + local result = remote.lua_func(check_pos, line, col, require("precognition").default_hint_config) + return result +end + +return M diff --git a/tests/minimal.lua b/tests/minimal.lua index 2023951..89c1194 100644 --- a/tests/minimal.lua +++ b/tests/minimal.lua @@ -7,6 +7,19 @@ local function tempdir(plugin) return vim.loop.os_tmpdir() .. "/" .. plugin end +local minitest_dir = os.getenv("MINI_TEST_DIR") or tempdir("mini.test") +if vim.fn.isdirectory(minitest_dir) == 0 then + vim.fn.system({ + "git", + "clone", + "https://github.com/echasnovski/mini.test", + minitest_dir, + }) +end +vim.opt.rtp:append(".") +vim.opt.rtp:append(minitest_dir) +require("mini.test").setup() + local plenary_dir = os.getenv("PLENARY_DIR") or tempdir("plenary.nvim") if vim.fn.isdirectory(plenary_dir) == 0 then vim.fn.system({ diff --git a/tests/precognition/dts.lua b/tests/precognition/dts.lua new file mode 100644 index 0000000..c11dec6 --- /dev/null +++ b/tests/precognition/dts.lua @@ -0,0 +1,87 @@ +local precognition = require("precognition") +local dts = require("tests.precognition.utils.dts") + +local USAGE = [[ +Runs dts testing for precognition marks + +USAGE: +nvim -u tests/minimal.lua -l tests/precognition/dts.lua SEED_START NUM_SIMS + +]] + +local M = {} + +function M.test(seed) + local data = dts.generate_random_line(seed) + + local cur_line = data.line + local cursorcol = data.cursor_col + + ---@type Precognition.VirtLine + local virtual_line_marks = require("precognition.sim").check(cur_line, cursorcol) + ---return 0 for any hint that is not found in the simmed table + setmetatable(virtual_line_marks, { + __index = function() + return 0 + end, + }) + + local temp_buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(temp_buf, 0, -1, false, { cur_line }) + + for loc, col in pairs(virtual_line_marks) do + local key = precognition.default_hint_config[loc].text + vim.api.nvim_set_current_buf(temp_buf) + vim.fn.setcursorcharpos(1, cursorcol) + local cur_before = vim.fn.getcursorcharpos(0) + vim.api.nvim_feedkeys(key, "x", true) + local cur_after = vim.fn.getcursorcharpos(0) + local actual_col = cur_after[3] + if col ~= 0 then + if col ~= actual_col then + vim.print(string.format("[SEED: %d]%s", seed, cur_line)) + vim.print( + string.format("with cursor at %s, motion %s, expected %s, got %s", cursorcol, key, col, actual_col) + ) + vim.print( + string.format( + "before: %s, input %s, after: %s", + vim.inspect(cur_before), + key, + vim.inspect(cur_after) + ) + ) + vim.print(vim.inspect(virtual_line_marks)) + require("precognition.sim").stop() + os.exit(1) + end + end + end + vim.api.nvim_buf_delete(temp_buf, { force = true }) +end + +local seed_start = tonumber(_G.arg[1]) +local num_sims = tonumber(_G.arg[2]) + +if (not num_sims or type(num_sims) ~= "number") or (not seed_start or type(seed_start) ~= "number") then + print(USAGE) +else + local seed = seed_start + local seed_end = seed_start + num_sims + local start_time = vim.uv.hrtime() + while seed <= seed_end do + M.test(seed) + if seed % 10000 == 0 then + vim.print(string.format("[SEED: %d]", seed)) + local cur_time = vim.uv.hrtime() + local elapsed_seconds = (cur_time - start_time) / 1e9 + local completed = seed - seed_start + local rate = completed / elapsed_seconds + local remaining = num_sims - completed + vim.print(string.format("%d sims remaing (est %d seconds)", remaining, remaining / rate)) + end + seed = seed + 1 + end +end + +return M diff --git a/tests/precognition/horizontal_motions_spec.lua b/tests/precognition/horizontal_motions_spec.lua index 7d0e0c9..a293376 100644 --- a/tests/precognition/horizontal_motions_spec.lua +++ b/tests/precognition/horizontal_motions_spec.lua @@ -1,7 +1,50 @@ -local hm = require("precognition.horizontal_motions") +local sim = require("precognition.sim") ---@diagnostic disable-next-line: undefined-field local eq = assert.are.same +local hm = {} + +hm.next_word_boundary = function(str, col, _, big) + local res = sim.check(str, col) + if big then + return res["W"] or 0 + else + return res["w"] or 0 + end +end + +hm.prev_word_boundary = function(str, col, _, big) + local res = sim.check(str, col) + if big then + return res["B"] or 0 + else + return res["b"] or 0 + end +end + +hm.end_of_word = function(str, col, _, big) + local res = sim.check(str, col) + if big then + return res["E"] or 0 + else + return res["e"] or 0 + end +end + +hm.matching_pair = function(_, _, _) + return hm.matching_bracket +end + +hm.matching_bracket = function(str, col, _) + local res = sim.check(str, col) + return res["MatchingPair"] or 0 +end + +hm.matching_comment = function(str, col, _) + local res = sim.check(str, col) + return res["MatchingPair"] or 0 +end + describe("boundaries", function() it("finds the next word boundary", function() local str = "abc efg" @@ -174,17 +217,17 @@ describe("boundaries", function() end) describe("matching_pair returns the correction function", function() - it("returns the correct function for the given character", function() - local test_string = "()[]{}/*" - eq(hm.matching_pair(test_string, 1, #test_string), hm.matching_bracket) - eq(hm.matching_pair(test_string, 2, #test_string), hm.matching_bracket) - eq(hm.matching_pair(test_string, 3, #test_string), hm.matching_bracket) - eq(hm.matching_pair(test_string, 4, #test_string), hm.matching_bracket) - eq(hm.matching_pair(test_string, 5, #test_string), hm.matching_bracket) - eq(hm.matching_pair(test_string, 6, #test_string), hm.matching_bracket) - eq(hm.matching_pair(test_string, 7, #test_string), hm.matching_comment) - eq(hm.matching_pair(test_string, 8, #test_string), hm.matching_comment) - end) + -- it("returns the correct function for the given character", function() + -- local test_string = "()[]{}/*" + -- eq(hm.matching_pair(test_string, 1, #test_string), hm.matching_bracket) + -- eq(hm.matching_pair(test_string, 2, #test_string), hm.matching_bracket) + -- eq(hm.matching_pair(test_string, 3, #test_string), hm.matching_bracket) + -- eq(hm.matching_pair(test_string, 4, #test_string), hm.matching_bracket) + -- eq(hm.matching_pair(test_string, 5, #test_string), hm.matching_bracket) + -- eq(hm.matching_pair(test_string, 6, #test_string), hm.matching_bracket) + -- eq(hm.matching_pair(test_string, 7, #test_string), hm.matching_comment) + -- eq(hm.matching_pair(test_string, 8, #test_string), hm.matching_comment) + -- end) it("returns a function that returns 0 for other characters", function() local test_string = "abcdefghijklmnopqrstuvwxyz!@#$%^&*_+-=,.<>?|\\~`" @@ -199,47 +242,47 @@ describe("matching brackets", function() it("if cursor is over a bracket it can find the pair", function() local str = "abc (efg)" eq(9, hm.matching_bracket(str, 5, #str)) - eq(0, hm.matching_bracket(str, 6, #str)) - eq(0, hm.matching_bracket(str, 7, #str)) - eq(0, hm.matching_bracket(str, 8, #str)) + eq(5, hm.matching_bracket(str, 6, #str)) + eq(5, hm.matching_bracket(str, 7, #str)) + eq(5, hm.matching_bracket(str, 8, #str)) eq(5, hm.matching_bracket(str, 9, #str)) end) it("if cursor is over a square bracket it can find the pair", function() local str = "abc [efg]" eq(9, hm.matching_bracket(str, 5, #str)) - eq(0, hm.matching_bracket(str, 6, #str)) - eq(0, hm.matching_bracket(str, 7, #str)) - eq(0, hm.matching_bracket(str, 8, #str)) + eq(5, hm.matching_bracket(str, 6, #str)) + eq(5, hm.matching_bracket(str, 7, #str)) + eq(5, hm.matching_bracket(str, 8, #str)) eq(5, hm.matching_bracket(str, 9, #str)) end) it("if cursor is over a curly bracket it can find the pair", function() local str = "abc {efg}" eq(9, hm.matching_bracket(str, 5, #str)) - eq(0, hm.matching_bracket(str, 6, #str)) - eq(0, hm.matching_bracket(str, 7, #str)) - eq(0, hm.matching_bracket(str, 8, #str)) + eq(5, hm.matching_bracket(str, 6, #str)) + eq(5, hm.matching_bracket(str, 7, #str)) + eq(5, hm.matching_bracket(str, 8, #str)) eq(5, hm.matching_bracket(str, 9, #str)) end) it("nested brackets find the correct pair", function() local str = "abc (efg [hij] klm)" eq(19, hm.matching_bracket(str, 5, #str)) - eq(0, hm.matching_bracket(str, 6, #str)) + eq(14, hm.matching_bracket(str, 6, #str)) eq(14, hm.matching_bracket(str, 10, #str)) eq(10, hm.matching_bracket(str, 14, #str)) - eq(0, hm.matching_bracket(str, 15, #str)) + eq(5, hm.matching_bracket(str, 15, #str)) eq(5, hm.matching_bracket(str, 19, #str)) end) it("nested brackets of the same type find the correct pair", function() local str = "abc (efg (hij) klm)" eq(19, hm.matching_bracket(str, 5, #str)) - eq(0, hm.matching_bracket(str, 6, #str)) + eq(14, hm.matching_bracket(str, 6, #str)) eq(14, hm.matching_bracket(str, 10, #str)) eq(10, hm.matching_bracket(str, 14, #str)) - eq(0, hm.matching_bracket(str, 15, #str)) + eq(5, hm.matching_bracket(str, 15, #str)) eq(5, hm.matching_bracket(str, 19, #str)) end) @@ -254,9 +297,9 @@ end) describe("matching comments", function() it("if cursor is over a comment it can find the pair", function() local str = "abc /*efg*/" - eq(11, hm.matching_comment(str, 5, #str)) - eq(11, hm.matching_comment(str, 6, #str)) - eq(0, hm.matching_comment(str, 7, #str)) + eq(10, hm.matching_comment(str, 5, #str)) + eq(10, hm.matching_comment(str, 6, #str)) + eq(5, hm.matching_comment(str, 7, #str)) eq(5, hm.matching_comment(str, 10, #str)) eq(5, hm.matching_comment(str, 11, #str)) end) diff --git a/tests/precognition/utils/dts.lua b/tests/precognition/utils/dts.lua new file mode 100644 index 0000000..35e99c4 --- /dev/null +++ b/tests/precognition/utils/dts.lua @@ -0,0 +1,51 @@ +local M = {} + +local ranges = { + { 32, 126 }, -- Basic Latin (ASCII) + { 160, 591 }, -- Latin-1 Supplement and Latin Extended-A + { 880, 2047 }, -- Greek, Cyrillic, Armenian, Hebrew + { 8192, 8303 }, -- General Punctuation + { 9728, 9983 }, -- Miscellaneous Symbols + { 12352, 12447 }, -- Hiragana + { 19904, 19967 }, -- Mahjong Tiles + { 0x1F300, 0x1F6FF }, -- Emoji +} + +---@class dts.Random +---@field cursor_col number +---@field line string + +---Generate a random line with Unicode characters. +---@param seed number +---@return dts.Random +function M.generate_random_line(seed) + math.randomseed(seed) -- Set the seed for reproducibility + + -- Randomize the line length (e.g., between 20 and 100 characters) + local line_length = math.random(20, 100) + + -- Function to generate a random printable Unicode character + local function random_unicode_char() + -- Randomly pick a range + local range = ranges[math.random(1, #ranges)] + -- Generate a random codepoint within the selected range + local codepoint = math.random(range[1], range[2]) + return vim.fn.nr2char(codepoint) -- Convert codepoint to UTF-8 character + end + + -- Generate the random line with Unicode characters + local line = "" + for _ = 1, line_length do + line = line .. random_unicode_char() + end + + -- Choose a random cursor position within the line + local cursor_col = math.random(1, vim.fn.strcharlen(line)) -- Ensure valid cursor position + ---@type dts.Random + return { + cursor_col = cursor_col, + line = line, + } +end + +return M diff --git a/tests/precognition/virtline_spec.lua b/tests/precognition/virtline_spec.lua index 26efc7d..75e5253 100644 --- a/tests/precognition/virtline_spec.lua +++ b/tests/precognition/virtline_spec.lua @@ -1,5 +1,4 @@ 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 @@ -79,12 +78,14 @@ describe("Build Virtual Line", function() local cur_line = line:gsub("\t", string.rep(" ", tab_width)) local line_len = vim.fn.strcharlen(cur_line) + local sim = require("precognition.sim").check(cur_line, cursorcol) + 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), + w = sim["w"], + e = sim["e"], + b = sim["b"], + Caret = sim["Caret"], + Dollar = sim["Dollar"], }, line_len, {}) eq("b e w $", virt_line[1][1]) @@ -98,12 +99,14 @@ describe("Build Virtual Line", function() local cur_line = line:gsub("\t", string.rep(" ", tab_width)) local line_len = vim.fn.strcharlen(cur_line) + local sim = require("precognition.sim").check(cur_line, cursorcol) + 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), + w = sim["w"], + e = sim["e"], + b = sim["b"], + Caret = sim["Caret"], + Dollar = sim["Dollar"], }, line_len, {}) eq(" ^ e w $", virt_line[1][1]) @@ -118,12 +121,14 @@ describe("Build Virtual Line", function() local line_len = vim.fn.strcharlen(cur_line) local extra_padding = { { start = 4, length = 4 } } + local sim = require("precognition.sim").check(cur_line, cursorcol) + 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), + w = sim["w"], + e = sim["e"], + b = sim["b"], + Caret = sim["Caret"], + Dollar = sim["Dollar"], }, line_len, extra_padding) local total_added = 0 @@ -143,12 +148,14 @@ describe("Build Virtual Line", function() local line_len = vim.fn.strcharlen(cur_line) local extra_padding = { { start = 4, length = 4 }, { start = 10, length = 5 } } + local sim = require("precognition.sim").check(cur_line, cursorcol) + 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), + w = sim["w"], + e = sim["e"], + b = sim["b"], + Caret = sim["Caret"], + Dollar = sim["Dollar"], }, line_len, extra_padding) local total_added = 0 @@ -170,12 +177,14 @@ describe("Build Virtual Line", function() utils.add_multibyte_padding(cur_line, extra_padding, line_len) + local sim = require("precognition.sim").check(cur_line, cursorcol) + 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), + w = sim["w"], + e = sim["e"], + b = sim["b"], + Caret = sim["Caret"], + Dollar = sim["Dollar"], }, line_len, extra_padding) eq("^ b e", virt_line[1][1])