|
| 1 | +local M = {} |
| 2 | + |
| 3 | +-- NOTE: line numbers are 1-indexed, column numbers are 0-indexed |
| 4 | + |
| 5 | +local utils = require("urlview.utils") |
| 6 | +local search_helpers = require("urlview.search.helpers") |
| 7 | + |
| 8 | +local END_COL = -1 |
| 9 | + |
| 10 | +--- Return the starting positions of `match` in `line` |
| 11 | +---@param line string |
| 12 | +---@param match string |
| 13 | +---@param offset number @added to each position |
| 14 | +---@return table (list) of offsetted starting indicies |
| 15 | +function M.line_match_positions(line, match, offset) |
| 16 | + local res = {} |
| 17 | + local init = 1 |
| 18 | + while init <= #line do |
| 19 | + local start, finish = line:find(match, init, true) |
| 20 | + if start == nil then |
| 21 | + return res |
| 22 | + end |
| 23 | + |
| 24 | + table.insert(res, start + offset - 1) |
| 25 | + init = finish |
| 26 | + end |
| 27 | + |
| 28 | + return res |
| 29 | +end |
| 30 | + |
| 31 | +--- Returns a starting column position not on a URL |
| 32 | +---@param line_start number @line number at cursor |
| 33 | +---@param col_start number @column number at cursor |
| 34 | +---@param reversed boolean @direction |
| 35 | +---@return number @corrected starting column |
| 36 | +function M.correct_start_col(line_start, col_start, reversed) |
| 37 | + local full_line = vim.fn.getline(line_start) |
| 38 | + local matches = search_helpers.content(full_line) |
| 39 | + for _, match in ipairs(matches) do |
| 40 | + local positions = M.line_match_positions(full_line, match, 0) |
| 41 | + for _, position in ipairs(positions) do |
| 42 | + local url_end = position + #match |
| 43 | + local on_url = col_start >= position and col_start < url_end |
| 44 | + -- edge case for going backwards with cursor at start of URL |
| 45 | + if on_url and reversed and position == col_start then |
| 46 | + return math.max(col_start - 1, 0) |
| 47 | + -- generally if on a URL, move column to be after the URL |
| 48 | + elseif on_url then |
| 49 | + return url_end |
| 50 | + end |
| 51 | + end |
| 52 | + end |
| 53 | + return col_start |
| 54 | +end |
| 55 | + |
| 56 | +local reversed_sort_function_lookup = { |
| 57 | + -- reversed == true: descending sort |
| 58 | + [true] = function(a, b) |
| 59 | + return a > b |
| 60 | + end, |
| 61 | + -- reversed == false: ascending sort |
| 62 | + [false] = function(a, b) |
| 63 | + return a < b |
| 64 | + end, |
| 65 | +} |
| 66 | + |
| 67 | +--- Finds the position of the previous / next URL |
| 68 | +---@param winnr number @id of current window |
| 69 | +---@param reversed boolean @direction false for forward, true for backwards |
| 70 | +---@return table|nil @position |
| 71 | +function M.find_url(winnr, reversed) |
| 72 | + local line_no, col_no = unpack(vim.api.nvim_win_get_cursor(winnr)) |
| 73 | + local total_lines = vim.api.nvim_buf_line_count(0) |
| 74 | + col_no = M.correct_start_col(line_no, col_no, reversed) |
| 75 | + |
| 76 | + local sort_function = reversed_sort_function_lookup[reversed] |
| 77 | + local line_last = utils.ternary(reversed, 0, total_lines + 1) |
| 78 | + while line_no ~= line_last do |
| 79 | + local full_line = vim.fn.getline(line_no) |
| 80 | + col_no = utils.ternary(col_no == END_COL, #full_line, col_no) |
| 81 | + local line = utils.ternary(reversed, full_line:sub(1, col_no), full_line:sub(col_no + 1)) |
| 82 | + local matches = search_helpers.content(line) |
| 83 | + |
| 84 | + if not vim.tbl_isempty(matches) then |
| 85 | + -- sorted table(list) of starting column numbers for URLs in line |
| 86 | + -- normal order: ascending, reversed order: descending |
| 87 | + local indices = {} |
| 88 | + for _, match in ipairs(matches) do |
| 89 | + local offset = utils.ternary(reversed, 0, col_no) |
| 90 | + vim.list_extend(indices, M.line_match_positions(line, match, offset)) |
| 91 | + end |
| 92 | + table.sort(indices, sort_function) |
| 93 | + -- find first valid (before or after current column) |
| 94 | + for _, index in ipairs(indices) do |
| 95 | + local valid = utils.ternary(reversed, index <= col_no, index >= col_no) |
| 96 | + if valid then |
| 97 | + return { line_no, index } |
| 98 | + end |
| 99 | + end |
| 100 | + end |
| 101 | + |
| 102 | + line_no = utils.ternary(reversed, line_no - 1, line_no + 1) |
| 103 | + col_no = utils.ternary(reversed, END_COL, 0) |
| 104 | + end |
| 105 | +end |
| 106 | + |
| 107 | +--- Forward / backward jump generator |
| 108 | +---@param reversed boolean @direction false for forward, true for backwards |
| 109 | +---@return function @when called, jumps to the URL in the given direction |
| 110 | +local function goto_url(reversed) |
| 111 | + return function() |
| 112 | + local direction = utils.ternary(reversed, "previous", "next") |
| 113 | + local winnr = vim.api.nvim_get_current_win() |
| 114 | + local pos = M.find_url(winnr, reversed) |
| 115 | + if not pos then |
| 116 | + utils.log(string.format("Cannot find any %s URLs in buffer", direction)) |
| 117 | + return |
| 118 | + end |
| 119 | + |
| 120 | + if vim.api.nvim_win_is_valid(winnr) then |
| 121 | + vim.cmd("normal! m'") -- add to jump list |
| 122 | + vim.api.nvim_win_set_cursor(winnr, pos) |
| 123 | + else |
| 124 | + utils.log(string.format("The %s URL was found in window number %s, which is no longer valid", direction, winnr)) |
| 125 | + end |
| 126 | + end |
| 127 | +end |
| 128 | + |
| 129 | +--- Jump to the next URL |
| 130 | +M.next_url = goto_url(false) |
| 131 | + |
| 132 | +--- Jump to the previous URL |
| 133 | +M.prev_url = goto_url(true) |
| 134 | + |
| 135 | +--- Register URL jump mappings |
| 136 | +---@param jump_opts table |
| 137 | +function M.register_mappings(jump_opts) |
| 138 | + if type(jump_opts) ~= "table" then |
| 139 | + utils.log("Invalid type for option `jump` (expected: table with prev_url and next_url keys)") |
| 140 | + else |
| 141 | + if jump_opts.prev ~= "" then |
| 142 | + utils.keymap( |
| 143 | + "n", |
| 144 | + jump_opts.prev, |
| 145 | + [[<Cmd>lua require("urlview.jump").prev_url()<CR>]], |
| 146 | + { desc = "Previous URL", noremap = true } |
| 147 | + ) |
| 148 | + end |
| 149 | + if jump_opts.next ~= "" then |
| 150 | + utils.keymap( |
| 151 | + "n", |
| 152 | + jump_opts.next, |
| 153 | + [[<Cmd>lua require("urlview.jump").next_url()<CR>]], |
| 154 | + { desc = "Next URL", noremap = true } |
| 155 | + ) |
| 156 | + end |
| 157 | + end |
| 158 | +end |
| 159 | + |
| 160 | +return M |
0 commit comments