Skip to content

Commit cf4774d

Browse files
authored
Jump to previous/next URL in buffer (#33)
1 parent ac8dcca commit cf4774d

File tree

11 files changed

+649
-4
lines changed

11 files changed

+649
-4
lines changed

.github/workflows/default.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- uses: actions/checkout@v2
14-
- uses: JohnnyMorganz/stylua-action@1.0.0
14+
- uses: JohnnyMorganz/stylua-action@v1
1515
with:
1616
token: ${{ secrets.GITHUB_TOKEN }}
1717
args: --color always --check .

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,27 @@ This plugin requires **Neovim 0.6+**. If necessary, please check out **Alternati
3535

3636
## 🚀 Usage
3737

38+
### Searching contexts
39+
3840
1. Use the command `:UrlView` to see all the URLs in the current buffer.
3941

40-
- For your convenience, feel free to set a keybind for this using `vim.api.nvim_set_keymap`
42+
- For your convenience, feel free to setup a keybind for this using `vim.api.nvim_set_keymap` (v0.6+) or `vim.keymap.set` (v0.7+)
43+
44+
```lua
45+
vim.keymap.set("n", "\\u", "<Cmd>UrlView<CR>", { desc = "view buffer URLs" })
46+
vim.keymap.set("n", "\\U", "<Cmd>UrlView packer<CR>", { desc = "view plugin URLs" })
47+
```
48+
4149
- You can also hit `:UrlView <tab>` to see additional contexts that you can search from
4250
- e.g. `:UrlView packer` to view links for installed [packer.nvim](https://github.com/wbthomason/packer.nvim) plugins
4351

4452
2. You can optionally select a link to bring it up in your browser.
4553

54+
### Buffer URL navigation
55+
56+
1. You can use `[u` and `]u` (default bindings) to jump to the previous and next URL in the buffer respectively.
57+
2. This keymap can be altered under the `jump` config option.
58+
4659
## 📦 Installation
4760

4861
Free free to install this plugin manually or with your favourite plugin manager. As an example, using [packer.nvim](https://github.com/wbthomason/packer.nvim):
@@ -76,6 +89,11 @@ require("urlview").setup({
7689
sorted = true,
7790
-- Logs user warnings (recommended for error detection)
7891
debug = true,
92+
-- Keymaps for jumping to previous / next URL in buffer
93+
jump = {
94+
prev = "[u",
95+
next = "]u",
96+
},
7997
-- Custom search captures
8098
-- NOTE: captures follow Lua pattern matching (https://riptutorial.com/lua/example/20315/lua-pattern-matching)
8199
custom_searches = {

lua/urlview/config.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ local default_config = {
2727
sorted = true,
2828
-- Logs user warnings (recommended for error detection)
2929
debug = true,
30+
-- Keymaps for jumping to previous / next URL in buffer
31+
jump = {
32+
prev = "[u",
33+
next = "]u",
34+
},
3035
-- Custom search captures
3136
custom_searches = {},
3237
}

lua/urlview/init.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ local M = {}
22

33
local actions = require("urlview.actions")
44
local config = require("urlview.config")
5-
local pickers = require("urlview.pickers")
5+
local jump = require("urlview.jump")
66
local search = require("urlview.search")
77
local search_helpers = require("urlview.search.helpers")
88
local search_validation = require("urlview.search.validation")
9+
local pickers = require("urlview.pickers")
910
local utils = require("urlview.utils")
1011

1112
--- Searchs the provided context for links
@@ -81,6 +82,7 @@ function M.setup(user_config)
8182
check_breaking()
8283

8384
search_helpers.register_custom_searches(config.custom_searches)
85+
jump.register_mappings(config.jump)
8486
end
8587

8688
return M

lua/urlview/jump.lua

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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

lua/urlview/search/helpers.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ end
4040

4141
--- Generates a simple search function from a template table
4242
---@param patterns table (map) with `capture` and `format` keys
43-
---@return function
43+
---@return function|nil
4444
local function default_custom_generator(patterns)
4545
if not patterns.capture or not patterns.format then
4646
return nil

lua/urlview/utils.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,17 @@ function M.string_to_boolean(value)
113113
return bool_map[value]
114114
end
115115

116+
function M.keymap(mode, lhs, rhs, opts)
117+
if vim.keymap then
118+
if opts.noremap ~= nil then
119+
opts.remap = not opts.noremap
120+
opts.noremap = nil
121+
end
122+
vim.keymap.set(mode, lhs, rhs, opts)
123+
else
124+
opts.desc = nil
125+
vim.api.nvim_set_keymap(mode, lhs, rhs, opts)
126+
end
127+
end
128+
116129
return M

0 commit comments

Comments
 (0)