A collection of dirty Lua snippets that grok you that feature you wanted, or needed, without using a plugin for it.
Because sometimes you just want to sneakβ’ your way somewhere while also getting that nice UI. Or maybe you want to fzf your way to a file without using a plugin for it. All examples here can be required from Lua lua/nvim-naughty if you're feeling lazy i.e. you are welcome to clone install this repository as a plugin and use it as such, it's your choice π€.
In Vim you can jump the cursor to an occurrence of a character on the same line. f jumps to the next character, F is f backward, t to the character before what f would jump to, and T is t backward. ; repeats the last character motion (e.g. f, F, t, and T). Why doesn't it work on many lines? You got me, I don't know, but we can fix that rather quickly.
First we need ; and , to work on multiple lines.
vim.g.charsearch = function(reverse)
---@type {char: string, forward: 0|1, until: 0|1}
local info = vim.fn.getcharsearch()
local pattern = string.format("%s[%s]", info["until"] == 1 and "." or "", info["char"]:gsub("[%^]", "\\%1"))
local flag = info["forward"] == reverse and "sb" or "s"
for _ = 1, vim.v.count1 do -- accept a count, but default to 1 if none given i.e. v:count1 is always 1
-- break if no more occurrences
if vim.fn.search(pattern, flag) == 0 then
break
end
end
end
vim.keymap.set("", ";", "<cmd>call g:charsearch(0)<cr>")
vim.keymap.set("", ",", "<cmd>call g:charsearch(1)<cr>")Once we have multi-line ; and , working, π we create a shim that properly sets the result of getcharsearch (so that it's repeatable) and does a char search.
local multiline_x = "<cmd>call setcharsearch(#{char:getcharstr(),until:%s,forward:%s})|call g:charsearch(0)<cr>"
vim.keymap.set("", "f", multiline_x:format(0, 1))
vim.keymap.set("", "F", multiline_x:format(0, 0))
vim.keymap.set("", "t", multiline_x:format(1, 1))
vim.keymap.set("", "T", multiline_x:format(0, 0))First we create a function (for reusability purposes π§βπ¬) g:jump that will do the work, then bind it to the key we want (e.g. <c-s>).
vim.keymap.set("", "<c-s>", "<cmd>call g:jump()<cr>")
function vim.g.jump()
-- ask for a charactger from the user
local char = vim.fn.getcharstr()
-- if character isn't printable, bail
if not char:find("[%w%p]") then return end
local window = vim.api.nvim_get_current_win()
local buffer = vim.api.nvim_win_get_buf(window)
-- find every ocurrence of the escaped character on the buffer lines visible in window ('w0' is the first visible line and 'w$' is the last)
---@type { lnum: integer, byteidx: integer, text:string }[]
local list = vim.fn.matchbufline(buffer, vim.fn.escape(char, "^$.*[\\^/~]"), vim.fn.line("w0", window), vim.fn.line("w$", window))
if #list == 0 then return end -- if no ocurrences, bail
local ns = vim.api.nvim_create_namespace("jump")
local charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
local charset_len = #charset
local opts = { virt_text = { { "", "Cursor" --[[highlight of our hints]] } }, virt_text_pos = "overlay", hl_mode = "combine" }
while #list > 1 do
local chunk_size = math.ceil(#list / charset_len)
-- use the same hint character for each chunk of matches
for i = 1, #list do
local match = list[i]
local chunk_idx = math.ceil(i / chunk_size)
opts.virt_text[1][1] = charset:sub(chunk_idx, chunk_idx)
vim.api.nvim_buf_set_extmark(0, ns, match.lnum - 1, match.byteidx, opts)
end
vim.api.nvim__redraw({ valid = true }) -- otherwise, extmarks might not be displayed
-- ask for a hint character
local key = vim.fn.getcharstr()
vim.api.nvim_buf_clear_namespace(0, ns, 0, -1)
-- get the group index of the hint character given by the user
local label_idx = charset:find(key, 1, true)
-- if there's no such group, bail
if not label_idx then return end
-- shrink the match list to the group of matches indexed by the hint character
list = table.move(list, (label_idx - 1) * chunk_size + 1, math.min(label_idx * chunk_size, #list), 1, {})
end
-- if list isn't empty, set the cursor to the first match
if list[1] then
vim.api.nvim_win_set_cursor(window, { list[1].lnum, list[1].byteidx })
end
endSounds weird, but Vim and Neovim haven't always had built-in comment toggling. Now they both do, though, so this is not as useful anymore. Even if there's built-in commenting, we keep it here for posterity and because it's arguably faster than the built-in commenting? π€ Or is it? π
- You get a comment normalization for free.
vim.keymap.set("", "<c-l>", function()
local start, stop = vim.fn.line ".", vim.fn.line "v"
if start > stop then start, stop = stop, start end
start = start - 1
local lines = vim.api.nvim_buf_get_lines(0, start, stop, false)
local commentstring = vim.o.commentstring -- of the form "<left>'%s'<right>"
local left, right = commentstring:match("^(.-)%%s(.-)$")
local uncomment = ("^(%%s*)(%s)(.*)(%s)$"):format(vim.pesc(left), vim.pesc(right))
local is_comment = true
local indent = math.huge
-- we loop through the lines to figure out if we'll be doing commenting, or uncommenting, by collecting uncommented lines, and the indentation those lines
for i = 1, #lines do
if lines[i]:len() > 0 then
local newline, n = lines[i]:gsub(uncomment, "%1%3")
lines[i] = newline:gsub(uncomment, "%1%3")
indent = math.min(indent, select(2, lines[i]:find("%s*")))
is_comment = is_comment and n > 0
end
end
if not is_comment then
-- if it's not a chunk of commented lines, we're obviously commenting
local template = ("%s%s"):format((" "):rep(indent), commentstring)
for i = 1, #lines do
if lines[i]:len() > 0 then
lines[i] = template:format(lines[i]:sub(indent + 1))
end
end
end
vim.api.nvim_buf_set_lines(0, start, stop, false, lines)
end)- Doesn't normalize comments.
Disclaimer
This will get messy.---@class Pattern<Ret>: vim.lpeg.Pattern
---@field match fun(self: Pattern<Ret>, str: string): Ret
---@alias ParsedLine {indent: string, text: string, left?: string, right?: string}
-- LPEG Grammar for parsing block comments
local BLOCK = [=[
{| {:indent: ' '* :} {:left: %left :} {:text: [^%right]+ :} {:right: %right :} |}
/ {| {:indent: ' '* :} {:text: .* :} |}
]=]
-- LPEG Grammar for parsing line comments
local INLINE = [=[
{| {:indent: ' '* :} {:left: %left :} {:text: .* :} |}
/ {| {:indent: ' '* :} {:text: .* :} |}
]=]
-- These grammars capture both indentation, left, right, and text of the line
local Commentary = function(start, stop)
if (stop - start) < 0 then
return
end -- invalid range
local commentstring = vim.bo.commentstring
local left, right = commentstring:match("^(.-)%%s(.-)$")
-- invalid commentstring, bail because it should at least be an empty string
if not left then return end
---@type Pattern<ParsedLine>
local parser = vim.re.compile(right:len() > 0 and BLOCK or INLINE, { left = left, right = right })
local is_comment = true
local indentation = math.huge
local lines = vim.api.nvim_buf_get_lines(0, start, stop, false)
local parsed_lines = {}
for i, line in ipairs(lines) do
local result = parser:match(line) --[=[@as ParsedLine]=]
is_comment = is_comment and result.left ~= nil
indentation = math.min(indentation, result.indent:len())
table.insert(parsed_lines, result)
end
if is_comment then
-- if it's a comment, we join each line's indentation and text into a single string
for i, parsed in ipairs(parsed_lines) do
lines[i] = parsed["indent"] .. parsed["text"]
end
else
-- but if it's not a comment, then we comment it all, preserving indentation
local fmt = (" "):rep(indentation) .. commentstring
for i, parsed in ipairs(parsed_lines) do
if parsed.indent:len() > indentation then
parsed.text = parsed.indent:sub(indentation + 1) .. parsed.text
end
if parsed.left then
parsed.text = parsed.left .. parsed.text
end
if parsed.right then
parsed.text = parsed.text .. parsed.right
end
lines[i] = fmt:format(parsed.text)
end
end
vim.api.nvim_buf_set_lines(0, start, stop, false, lines)
end
vim.keymap.set("n", "<C-l>", function()
local lnum = vim.fn.line(".")
Commentary(lnum - 1, lnum)
end)
vim.keymap.set("x", "<C-l>", function()
local region = vim.fn.getregionpos(vim.fn.getpos("."), vim.fn.getpos("v"), { mode = "v" })
local sel_start, sel_stop = region[1][1][2], region[#region][2][2]
if sel_start > sel_stop then
sel_start, sel_stop = sel_stop, sel_start
end
Commentary(sel_start - 1, sel_stop)
end)Dead simple scrollbars for Neovim.
local scrolly_ns = vim.api.nvim_create_namespace("scrollbar")
vim.api.nvim_set_decoration_provider(scrolly_ns, {
on_win = function(_, winid, bufnr, toprow, botrow)
if vim.api.nvim_win_get_config(winid).relative ~= "" then return false end
vim.api.nvim_buf_clear_namespace(bufnr, scrolly_ns, toprow, botrow)
local linecount = vim.api.nvim_buf_line_count(bufnr)
local winheight = vim.api.nvim_win_get_height(winid)
if linecount <= winheight then
return false
end
local percent = winheight / linecount
local start = math.floor(toprow + (toprow * percent))
local stop = math.floor(math.min(linecount, toprow + (botrow * percent)))
---@type vim.api.keyset.set_extmark
local opts = {
virt_text = { { "β", "NonText" } },
virt_text_pos = "right_align",
virt_text_repeat_linebreak = true,
ephemeral = true,
}
for row = start, stop do
vim.api.nvim_buf_set_extmark(bufnr, scrolly_ns, row, 0, opts)
end
vim.api.nvim__redraw({ win = winid, valid = false })
end,
})