Skip to content

kaiuri/nvim-naughty

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Introduction

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 😀.

Multiline character search

Context πŸ§‘β€πŸ«

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.

Multiline ; and ,

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>")

Multiline f/F/t/T

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))

Multidirectional jumping with hints 😭

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
end

Toggling Comments

Sounds 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? 😭

The Dumb Way

  • 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)

The Crazy Way

  • Doesn't normalize comments.
DisclaimerThis 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)

Scrollbar πŸ“œ

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,
})

About

A collection of dirty lua snippets that grok you that feature you wanted, or needed, without using a plugin for it.

Topics

Resources

License

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages