plugin.lua
Branches > main > b31b6de > files / nvim / lua / parinfer / plugin.lua
--- Modified version of https://github.com/gpanders/nvim-parinfer
--- See ./LICENSE
vim.g.parinfer_mode = "smart"
vim.g.parinfer_enabled = true
vim.g.parinfer_force_balance = false
vim.g.parinfer_comment_chars = {";"}
local api = vim.api
local parinfer = require("parinfer.parinfer")
local modes = {indent = parinfer.indentMode,
paren = parinfer.parenMode,
smart = parinfer.smartMode}
local ns = api.nvim_create_namespace("parinfer")
local group = api.nvim_create_augroup("parinfer", {clear = true})
--- Per buffer: the text and changedtick as of the last run, and the cursor before the current edit
local state = {}
-- Returns true if 'val' is truthy, and not 0, otherwise returns false
-- 0 and 1 are commonly used in vim functions for true/false, but 0 is a truthy value in Lua
local function normalize_bool(val)
if type(val) == "boolean" or val == nil then
return val
else
return val ~= 0
end
end
-- Looks up and returns 'opt' in the buffer-local and global option tables vim.b and vim.g
local function get_option(bufnr, opt)
local b = vim.b[bufnr][opt]
if b == nil then
return vim.g[opt]
else
return b
end
end
--- Returns how many bytes 'old' and 'new' have in common at the start, and then at the end
local function common_ends(old, new)
local max = math.min(#old, #new)
local prefix = 0
while prefix < max and old:byte(prefix + 1) == new:byte(prefix + 1) do
prefix = prefix + 1
end
local suffix = 0
while suffix < max - prefix and old:byte(#old - suffix) == new:byte(#new - suffix) do
suffix = suffix + 1
end
return prefix, suffix
end
--- Collapses everything that happened since the last run into one change.
--- Linewise edits (V>, Vd + p, :m, ...) used to be passed as several on_bytes changes, each in the
--- coordinates of a buffer that had been changed again since. Smart mode then shifted the
--- indentation of the wrong lines (or the same lines more than once) and mangled the forms.
local function get_change(old, new)
if old == nil or old == new then return nil end
local prefix, suffix = common_ends(old, new)
local before = old:sub(1, prefix)
local _, line_breaks = before:gsub("\n", "")
local line_start = before:find("[^\n]*$")
return {lineNo = line_breaks + 1,
x = prefix - line_start + 2,
oldText = old:sub(prefix + 1, #old - suffix),
newText = new:sub(prefix + 1, #new - suffix)}
end
--- Only replaces the bytes parinfer changed. Replacing whole lines deletes the extmarks on them, so
--- the LSP semantic token highlights of every fixed line blinked off until the server sent new ones.
local function update_buffer(bufnr, old_text, new_text)
local old_lines = vim.split(old_text, "\n", {plain = true})
local new_lines = vim.split(new_text, "\n", {plain = true})
vim.cmd("silent! undojoin")
--- Parinfer only moves parens and indentation, it shouldn't ever add or remove lines
if #old_lines ~= #new_lines then
api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines)
return
end
for i, old in ipairs(old_lines) do
local new = new_lines[i]
if old ~= new then
local prefix, suffix = common_ends(old, new)
api.nvim_buf_set_text(bufnr, i - 1, prefix, i - 1, #old - suffix,
{new:sub(prefix + 1, #new - suffix)})
end
end
end
local function highlight_error(bufnr, err)
api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
if err then
local lnum, col = err.lineNo - 1, err.x - 1
vim.hl.range(bufnr, ns, "Error", {lnum, col}, {lnum, col + 1})
end
end
local function is_undo_leaf()
local undotree = vim.fn.undotree()
return undotree.seq_cur == undotree.seq_last
end
local function should_run(bufnr)
return normalize_bool(get_option(bufnr, "parinfer_enabled"))
and not vim.o.paste
and not vim.bo[bufnr].readonly
and vim.bo[bufnr].modifiable
--- Don't fight undo/redo, rewriting an undone state would throw away the redo branch
and is_undo_leaf()
end
local function process_buffer(bufnr, mode)
local s = state[bufnr]
local tick = vim.b[bufnr].changedtick
if not s or s.tick == tick then return end
local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, true), "\n")
local change = get_change(s.text, text)
s.text, s.tick = text, tick
if not should_run(bufnr) then return end
local win = api.nvim_get_current_win()
local lnum, col = unpack(api.nvim_win_get_cursor(win))
local response = modes[mode or get_option(bufnr, "parinfer_mode")](text, {
commentChars = get_option(bufnr, "parinfer_comment_chars"),
prevCursorLine = s.cursor[1],
prevCursorX = s.cursor[2],
cursorLine = lnum,
cursorX = col + 1,
changes = change and {change},
forceBalance = normalize_bool(get_option(bufnr, "parinfer_force_balance"))})
if response.success and response.text ~= text then
update_buffer(bufnr, text, response.text)
api.nvim_win_set_cursor(win, {response.cursorLine, response.cursorX - 1})
s.text, s.tick = response.text, vim.b[bufnr].changedtick
end
s.cursor = {response.cursorLine, response.cursorX}
highlight_error(bufnr, response.error)
end
local function cursor_moved(bufnr)
local s = state[bufnr]
--- CursorMoved fires before TextChanged, keep the cursor from before the edit until it's processed
if s.tick ~= vim.b[bufnr].changedtick then return end
local lnum, col = unpack(api.nvim_win_get_cursor(0))
s.cursor = {lnum, col + 1}
end
local function setup_buffer(bufnr)
local lnum, col = unpack(api.nvim_win_get_cursor(0))
state[bufnr] = {cursor = {lnum, col + 1}}
api.nvim_clear_autocmds({group = group, buffer = bufnr})
api.nvim_create_autocmd({"TextChanged", "TextChangedI", "TextChangedP"}, {buffer = bufnr, group = group, callback = function() process_buffer(bufnr) end})
api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, {buffer = bufnr, group = group, callback = function() cursor_moved(bufnr) end})
api.nvim_create_autocmd("BufWipeout", {buffer = bufnr, group = group, callback = function() state[bufnr] = nil end})
api.nvim_buf_create_user_command(bufnr, "ParinferToggle", function()
vim.b[bufnr].parinfer_enabled = not normalize_bool(get_option(bufnr, "parinfer_enabled"))
if not vim.b[bufnr].parinfer_enabled then api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) end
vim.notify("Parinfer " .. (vim.b[bufnr].parinfer_enabled and "enabled" or "disabled"))
end, {})
--- Paren mode fixes up the indentation first, so smart mode doesn't restructure the file on the first edit
process_buffer(bufnr, "paren")
end
local function should_enable()
--- Skips prompts, scratch buffers, the conjure log, etc
return not vim.wo.previewwindow and vim.bo.buftype == ""
end
--- Sets up parinfer in the current buffer
local function setup()
if should_enable() then
setup_buffer(api.nvim_get_current_buf())
end
end
return {setup = setup}