Search based navigation combined with quick jump features.
Demo
Screenshots
Only Neovim 0.9+ is required, nothing more.
The main goal of this plugin is to quickly jump to any characters using a search pattern.
By default, the search is made forward and only in visible lines of the current buffer.
To start using SJ, you can add the lines below in your configuration for Neovim.
local sj = require("sj")
sj.setup()
vim.keymap.set("n", "s", sj.run)
vim.keymap.set("n", "<A-,>", sj.prev_match)
vim.keymap.set("n", "<A-;>", sj.next_match)
vim.keymap.set("n", "<localleader>s", sj.redo)
As soon as you use the keymap assigned to sj.run()
and start typing the pattern :
While searching, you can use the keymaps below :
Keymap | Description |
---|---|
<Escape> |
cancel the search |
<Enter> |
jump to the focused match |
:a , :b , :c |
jump to the the match with the label a , b or c |
<A-,> , <A-;> |
focus the previous or next match |
<C-p> , <C-n> |
select the previous or next pattern |
<BS> |
delete the previous character |
<C-w> |
delete the previous word |
<C-u> |
delete the whole pattern |
<A-BS> |
restore the pattern to the last version having matches |
<A-q> |
send the search results to the quickfix list |
After the search, you can call sj.prev_match()
and sj.next_match()
to jump on the
previous/next match or sj.redo()
to redo a search using the last pattern.
Notes :
max_pattern_length
and you reach that length limit, the labels color will
change to indicate that the next key should be for a label and not for the pattern ;
(When reaching this limit, no need to type :
before the label)separator = ""
which will avoid the need to type an
extra character but will reduce the number of available labels.Here is the default configuration :
local config = {
auto_jump = false, -- if true, automatically jump on the sole match
forward_search = true, -- if true, the search will be done from top to bottom
highlights_timeout = 0, -- if > 0, wait for 'updatetime' + N ms to clear hightlights (sj.prev_match/sj.next_match)
inclusive = false, -- if true, the jump target will be included with 'operator-pending' keymaps
max_pattern_length = 0, -- if > 0, wait for a label after N characters
pattern = "", -- predefined pattern to use at the start of a search
pattern_type = "vim", -- how to interpret the pattern (lua_plain, lua, vim, vim_very_magic)
preserve_highlights = true, -- if true, create an autocmd to preserve highlights when switching colorscheme
prompt_prefix = "", -- if set, the string will be used as a prefix in the command line
relative_labels = false, -- if true, labels are ordered from the cursor position, not from the top of the buffer
search_scope = "visible_lines", -- (current_line, visible_lines_above, visible_lines_below, visible_lines, buffer)
select_window = false, -- if true, ask for a window to jump to before starting the search
separator = ":", -- character used to split the user input in <pattern> and <label> (can be empty)
stop_on_fail = true, -- if true, the search will stop when a search fails (no matches)
update_search_register = false, -- if true, update the search register with the last used pattern
use_last_pattern = false, -- if true, reuse the last pattern for next calls
use_overlay = true, -- if true, apply an overlay to better identify labels and matches
wrap_jumps = vim.o.wrapscan, -- if true, wrap the jumps when focusing previous or next label
--- keymaps used during the search
keymaps = {
cancel = "<Esc>", -- cancel the search
validate = "<CR>", -- jump to the focused match
prev_match = "<A-,>", -- focus the previous match
next_match = "<A-;>", -- focus the next match
prev_pattern = "<C-p>", -- select the previous pattern while searching
next_pattern = "<C-n>", -- select the next pattern while searching
---
delete_prev_char = "<BS>", -- delete the previous character
delete_prev_word = "<C-w>", -- delete the previous word
delete_pattern = "<C-u>", -- delete the whole pattern
restore_pattern = "<A-BS>", -- restore the pattern to the last version having matches
---
send_to_qflist = "<A-q>", --- send the search results to the quickfix list
},
--- labels used for each matches. (one-character strings only)
-- stylua: ignore
labels = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ",", ";", "!",
},
}
and here is a configuration sample :
DISCLAIMER : This plugin is not intended to replace the native functions of Neovim.
I do not recommend adding keymaps that replaces /, ?, f/F, t/T...
.
local sj = require("sj")
local sj_cache = require("sj.cache")
--- Configuration ------------------------------------------------------------------------
sj.setup({
prompt_prefix = "/",
-- stylua: ignore
highlights = {
SjFocusedLabel = { bold = false, italic = false, fg = "#FFFFFF", bg = "#C000C0", },
SjLabel = { bold = true , italic = false, fg = "#000000", bg = "#5AA5DE", },
SjLimitReached = { bold = true , italic = false, fg = "#000000", bg = "#DE945A", },
SjMatches = { bold = false, italic = false, fg = "#DDDDDD", bg = "#005080", },
SjNoMatches = { bold = false, italic = false, fg = "#DE945A", },
SjOverlay = { bold = false, italic = false, fg = "#345576", },
},
keymaps = {
send_to_qflist = "<C-q>", --- send search result to the quickfix list
},
})
--- Keymaps ------------------------------------------------------------------------------
vim.keymap.set("n", "!", function()
sj.run({ select_window = true })
end)
vim.keymap.set("n", "<A-!>", function()
sj.select_window()
end)
--- visible lines -------------------------------------
vim.keymap.set({ "n", "o", "x" }, "S", function()
vim.fn.setpos("''", vim.fn.getpos("."))
sj.run({
forward_search = false,
})
end)
vim.keymap.set({ "n", "o", "x" }, "s", function()
vim.fn.setpos("''", vim.fn.getpos("."))
sj.run()
end)
vim.keymap.set("n", "<localleader>c", function()
sj.run({
max_pattern_length = 1,
pattern_type = "lua_plain",
})
end)
--- buffer --------------------------------------------
vim.keymap.set("n", "gS", function()
vim.fn.setpos("''", vim.fn.getpos("."))
sj.run({
forward_search = false,
search_scope = "buffer",
update_search_register = true,
})
end)
vim.keymap.set("n", "gs", function()
vim.fn.setpos("''", vim.fn.getpos("."))
sj.run({
search_scope = "buffer",
update_search_register = true,
})
end)
--- current line --------------------------------------
vim.keymap.set({ "n", "o", "x" }, "<localleader>l", function()
sj.run({
auto_jump = true,
max_pattern_length = 1,
pattern_type = "lua_plain",
search_scope = "current_line",
use_overlay = false,
})
end)
vim.keymap.set("o", "f", function()
sj.run({
auto_jump = true,
forward_search = true,
inclusive = true,
max_pattern_length = 1,
pattern_type = "lua_plain",
search_scope = "current_line", -- works with other scopes
use_overlay = false,
})
end)
vim.keymap.set("o", "F", function()
sj.run({
auto_jump = true,
forward_search = false,
inclusive = true,
max_pattern_length = 1,
pattern_type = "lua_plain",
search_scope = "current_line", -- works with other scopes
use_overlay = false,
})
end)
vim.keymap.set("o", "t", function()
sj.run({
auto_jump = true,
forward_search = true,
inclusive = false,
max_pattern_length = 1,
pattern_type = "lua_plain",
search_scope = "current_line", -- works with other scopes
use_overlay = false,
})
end)
vim.keymap.set("o", "T", function()
sj.run({
auto_jump = true,
forward_search = false,
inclusive = false,
max_pattern_length = 1,
pattern_type = "lua_plain",
search_scope = "current_line", -- works with other scopes
use_overlay = false,
})
end)
--- prev/next match -----------------------------------
vim.keymap.set("n", "<A-,>", function()
sj.prev_match()
if sj_cache.options.search_scope:match("^buffer") then
vim.cmd("normal! zzzv")
end
end)
vim.keymap.set("n", "<A-;>", function()
sj.next_match()
if sj_cache.options.search_scope:match("^buffer") then
vim.cmd("normal! zzzv")
end
end)
--- redo ----------------------------------------------
vim.keymap.set("n", "<localleader>a", function()
local relative_labels = sj_cache.options.relative_labels
sj.redo({
relative_labels = false,
max_pattern_length = 1,
})
sj_cache.options.relative_labels = relative_labels
end)
vim.keymap.set("n", "<localleader>s", function()
sj.redo({
relative_labels = true,
max_pattern_length = 1,
})
end)
DISCLAIMER : This plugin is not intended to replace the native functions of Neovim.
I do not recommend adding keymaps that replaces /, ?, f/F, t/T...
.
Why this plugin ?! Well, let me explain ! :smiley:
Using vertical/horizontal navigation with <count>k/j
, :<count><CR>
,
H/M/L/f/F/t/T/,/;b/e/w^/$
, is a very good way to navigate. But with the keyboards I use,
I have to press the <Shift>
key to type numbers and some of them are a bit to far for my
fingers. Once on the good line, I have to repeat pressing some horizontal movement keys
too much.
When navigating in a buffer, I often find the search based navigation to be easier, faster and more precise. But if there are too many matches, I have to repeat pressing a key to cycle between the matches. By adding jump features with labels, I can quickly jump to the match I want.
For me, one small caveat of the 'jump plugins', is that they generate the labels or 'hint keys' based on the cursor position. That is understandable and efficient but within the same buffer area, it means that you can have different labels for the same pattern or position which make the keys sequence for a jump less predictables. Also, in some contexts, you don't know if you'll have to use a 1, 2 or 3 characters for the label.
By using a search pattern with a 1-character label, you already know all the keys except one character for the label.