diff --git a/README.md b/README.md index 84b27ca..e2e149d 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,13 @@ return { -- startVisible = true, -- showBlankVirtLine = true, -- hints = { - -- Caret = { text = "^", prio = 1 }, - -- Dollar = { text = "$", prio = 1 }, - -- w = { text = "w", prio = 10 }, - -- b = { text = "b", prio = 10 }, - -- e = { text = "e", prio = 10 }, + -- Caret = { text = "^", prio = 2 }, + -- Dollar = { text = "$", prio = 1 }, + -- MatchingPair = { text = "%", prio = 5 }, + -- Zero = { text = "0", prio = 1 }, + -- w = { text = "w", prio = 10 }, + -- b = { text = "b", prio = 9 }, + -- e = { text = "e", prio = 8 }, -- }, -- gutterHints = { -- --prio is not currentlt used for gutter hints @@ -40,8 +42,10 @@ return { ## ⚙️ Config -- Items can be hidden by settings their priority to 0 -- `lua showBlankVirtLine = false` +- 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 + below. +- `showBlankVirtLine = false` Setting this option will mean that if a Virtual Line would be blank it wont be rendered diff --git a/lua/precognition/horizontal_motions.lua b/lua/precognition/horizontal_motions.lua index d5e1b82..1052630 100644 --- a/lua/precognition/horizontal_motions.lua +++ b/lua/precognition/horizontal_motions.lua @@ -3,6 +3,12 @@ local cc = utils.char_classes local M = {} +local supportedBrackets = { + open = { "(", "[", "{" }, + middle = { nil, nil, nil }, + close = { ")", "]", "}" }, +} + ---@param str string ---@param _cursorcol integer ---@param _linelen integer @@ -138,4 +144,148 @@ function M.prev_word_boundary(str, cursorcol, _linelen) 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 - 2 + 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 616a8a4..a3eb6f7 100644 --- a/lua/precognition/init.lua +++ b/lua/precognition/init.lua @@ -14,6 +14,8 @@ local M = {} ---@field w Precognition.HintOpts ---@field e Precognition.HintOpts ---@field b Precognition.HintOpts +---@field Zero Precognition.HintOpts +---@field MatchingPair Precognition.HintOpts ---@field Caret Precognition.HintOpts ---@field Dollar Precognition.HintOpts @@ -39,8 +41,10 @@ local M = {} ---@field w Precognition.PlaceLoc ---@field e Precognition.PlaceLoc ---@field b Precognition.PlaceLoc +---@field Zero Precognition.PlaceLoc ---@field Caret Precognition.PlaceLoc ---@field Dollar Precognition.PlaceLoc +---@field MatchingPair Precognition.PlaceLoc ---@class (exact) Precognition.GutterHints ---@field G Precognition.PlaceLoc @@ -50,8 +54,10 @@ local M = {} ---@type Precognition.HintConfig local defaultHintConfig = { - Caret = { text = "^", prio = 1 }, + Caret = { text = "^", prio = 2 }, Dollar = { text = "$", prio = 1 }, + MatchingPair = { text = "%", prio = 5 }, + Zero = { text = "0", prio = 1 }, w = { text = "w", prio = 10 }, b = { text = "b", prio = 9 }, e = { text = "e", prio = 8 }, @@ -210,7 +216,9 @@ local function display_marks() w = hm.next_word_boundary(cur_line, cursorcol, line_len), e = hm.end_of_word(cur_line, cursorcol, line_len), b = hm.prev_word_boundary(cur_line, cursorcol, line_len), + 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 virt_line = build_virt_line(virtual_line_marks, line_len) diff --git a/tests/precognition/horizontal_motions_spec.lua b/tests/precognition/horizontal_motions_spec.lua index ef12043..719a69c 100644 --- a/tests/precognition/horizontal_motions_spec.lua +++ b/tests/precognition/horizontal_motions_spec.lua @@ -144,6 +144,93 @@ describe("boundaries", function() end) 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 a function that returns 0 for other characters", function() + local test_string = "abcdefghijklmnopqrstuvwxyz!@#$%^&*_+-=,.<>?|\\~`" + for i = 1, #test_string do + local func = hm.matching_pair(test_string, i, #test_string) + eq(0, func(test_string, i, #test_string)) + end + end) +end) + +describe("matching brackets", function() + it("if cursor is over a bracket it can find the pair", function() + eq(9, hm.matching_bracket("abc (efg)", 5, 9)) + eq(0, hm.matching_bracket("abc (efg)", 6, 9)) + eq(0, hm.matching_bracket("abc (efg)", 7, 9)) + eq(0, hm.matching_bracket("abc (efg)", 8, 9)) + eq(5, hm.matching_bracket("abc (efg)", 9, 9)) + end) + + it("if cursor is over a square bracket it can find the pair", function() + eq(9, hm.matching_bracket("abc [efg]", 5, 9)) + eq(0, hm.matching_bracket("abc [efg]", 6, 9)) + eq(0, hm.matching_bracket("abc [efg]", 7, 9)) + eq(0, hm.matching_bracket("abc [efg]", 8, 9)) + eq(5, hm.matching_bracket("abc [efg]", 9, 9)) + end) + + it("if cursor is over a curly bracket it can find the pair", function() + eq(9, hm.matching_bracket("abc {efg}", 5, 9)) + eq(0, hm.matching_bracket("abc {efg}", 6, 9)) + eq(0, hm.matching_bracket("abc {efg}", 7, 9)) + eq(0, hm.matching_bracket("abc {efg}", 8, 9)) + eq(5, hm.matching_bracket("abc {efg}", 9, 9)) + end) + + it("nested brackets find the correct pair", function() + eq(19, hm.matching_bracket("abc (efg [hij] klm)", 5, 19)) + eq(0, hm.matching_bracket("abc (efg [hij] klm)", 6, 19)) + eq(14, hm.matching_bracket("abc (efg [hij] klm)", 10, 19)) + eq(10, hm.matching_bracket("abc (efg [hij] klm)", 14, 19)) + eq(0, hm.matching_bracket("abc (efg [hij] klm)", 15, 19)) + eq(5, hm.matching_bracket("abc (efg [hij] klm)", 19, 19)) + end) + + it("nested brackets of the same type find the correct pair", function() + eq(19, hm.matching_bracket("abc (efg (hij) klm)", 5, 19)) + eq(0, hm.matching_bracket("abc (efg (hij) klm)", 6, 19)) + eq(14, hm.matching_bracket("abc (efg (hij) klm)", 10, 19)) + eq(10, hm.matching_bracket("abc (efg (hij) klm)", 14, 19)) + eq(0, hm.matching_bracket("abc (efg (hij) klm)", 15, 19)) + eq(5, hm.matching_bracket("abc (efg (hij) klm)", 19, 19)) + end) + + it("if cursor is over an unclosed bracket it returns 0", function() + eq(0, hm.matching_bracket("abc (efg", 5, 8)) + eq(0, hm.matching_bracket("abc [efg", 5, 8)) + eq(0, hm.matching_bracket("abc {efg", 5, 8)) + end) +end) + +describe("matching comments", function() + it("if cursor is over a comment it can find the pair", function() + eq(11, hm.matching_comment("abc /*efg*/", 5, 11)) + eq(11, hm.matching_comment("abc /*efg*/", 6, 11)) + eq(0, hm.matching_comment("abc /*efg*/", 7, 11)) + eq(5, hm.matching_comment("abc /*efg*/", 10, 11)) + eq(5, hm.matching_comment("abc /*efg*/", 11, 11)) + end) + + it("if cursor is over an unclosed comment it returns 0", function() + eq(0, hm.matching_comment("abc /*efg", 5, 9)) + eq(0, hm.matching_comment("abc /*efg", 6, 9)) + end) +end) + describe("edge case", function() it("can handle empty strings", function() eq(0, hm.next_word_boundary("", 1, 0))