ada9461

Branches > main > ada9461 > files / nvim / lua

Nvim - Re-enable parinfer, fix multi-line edits

Grahp, 2026-10-25

Commit: ada9461afdb15b1d69c9f3456e4d6262cdf84521
Parent: f198332

2 changed files:

nvim/ftplugin/clojure.lua modified (view file)

@@ -1,2 +1,2 @@ require("plugins.conjure")--- require("parinfer.plugin").setup()+require("parinfer.plugin").setup()

nvim/lua/parinfer/plugin.lua modified (view file)

@@ -14,11 +14,10 @@                smart = parinfer.smartMode}  local ns = api.nvim_create_namespace("parinfer")+local group = api.nvim_create_augroup("parinfer", {clear = true}) -local state = setmetatable({}, {__index = function(t, k)-  t[k] = {}-  return t[k]-end})+--- 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@@ -31,8 +30,8 @@ end  -- Looks up and returns 'opt' in the buffer-local and global option tables vim.b and vim.g-local function get_option(opt)-  local b = vim.b[opt]+local function get_option(bufnr, opt)+  local b = vim.b[bufnr][opt]   if b == nil then     return vim.g[opt]   else@@ -40,212 +39,135 @@   end end -local function invoke_parinfer(bufnr, text, lnum, col)-  local prev_lnum, prev_col = unpack(state[bufnr]["prev-cursor"])-  local changes = state[bufnr].changes-  local request = {commentChars = get_option("parinfer_comment_chars"),-                   prevCursorLine = prev_lnum,-                   prevCursorX = prev_col,-                   cursorLine = lnum,-                   cursorX = col + 1,-                   changes = changes,-                   forceBalance = normalize_bool(get_option("parinfer_force_balance"))}-  local response = modes[get_option("parinfer_mode")](text, request)-  state[bufnr].changes = {}-  return response-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 function build_replacement_lines(new_lines, start_b, count_b)-  local t = {}-  local i = 0--  for num = start_b, start_b + count_b do-    local line = new_lines[num]-    if line ~= nil then-      i = i + 1-      t[i] = line-    end+  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 t+  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 -local function apply_hunk(bufnr, new_lines, hunk)-  local start_a, count_a, start_b, count_b = unpack(hunk)-  api.nvim_buf_set_lines(bufnr, start_a - 1, start_a + count_a, false, build_replacement_lines(new_lines, start_b, count_b))-end+local function update_buffer(bufnr, old_text, new_text)+  local new_lines = vim.split(new_text, "\n", {plain = true})+  local hunks = diff(old_text .. "\n", new_text .. "\n", {result_type = "indices"}) -local function update_buffer(bufnr, old_contents, new_contents)-  api.nvim_command("silent! undojoin")-  state[bufnr].locked = true--  local new_lines = vim.split(new_contents, "\n")-  local hunks = diff(old_contents, new_contents, {result_type = "indices"})-  for _, hunk in ipairs(hunks) do-    apply_hunk(bufnr, new_lines, hunk)+  vim.cmd("silent! undojoin")+  --- Bottom up, so applying a hunk doesn't move the lines of the ones after it+  for i = #hunks, 1, -1 do+    local start_a, count_a, start_b, count_b = unpack(hunks[i])+    --- A pure insertion goes after start_a+    if count_a == 0 then start_a = start_a + 1 end+    api.nvim_buf_set_lines(bufnr, start_a - 1, start_a - 1 + count_a, false,+                           {unpack(new_lines, start_b, start_b + count_b - 1)})   end--  return vim.schedule(function()-    state[bufnr].locked = false-  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-    return vim.highlight.range(bufnr, ns, "Error", {lnum, col}, {lnum, col + 1}, "c")+    vim.hl.range(bufnr, ns, "Error", {lnum, col}, {lnum, col + 1})   end end  local function is_undo_leaf()   local undotree = vim.fn.undotree()-  local seq_cur = undotree["seq_cur"]-  local seq_last = undotree["seq_last"]-  return seq_cur == seq_last+  return undotree.seq_cur == undotree.seq_last end  local function should_run(bufnr)-  return normalize_bool(get_option("parinfer_enabled"))-    and not state[bufnr].locked+  return normalize_bool(get_option(bufnr, "parinfer_enabled"))     and not vim.o.paste-    and not vim.bo.readonly-    and vim.bo.modifiable-    and vim.b[bufnr].changedtick ~= state[bufnr].changedtick+    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 elapsed_times = setmetatable({}, {__index = function(t, k)-  t[k] = {}-  return rawget(t, k)-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 function process_buffer(bufnr)-  if should_run(bufnr) then-    local start = vim.loop.hrtime()-    local winnr = api.nvim_get_current_win()-    local cursor = api.nvim_win_get_cursor(winnr)-    local lnum = cursor[1]-    local col = cursor[2]-    local contents = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)-    local text = table.concat(contents, "\n")-    local response = invoke_parinfer(bufnr, text, lnum, col)-    local new_lnum = response["cursorLine"]-    local new_col = response["cursorX"]-    state[bufnr]["changedtick"] = vim.b[bufnr].changedtick-    state[bufnr]["tabstops"] = response.tabStops-    state[bufnr]["prev-cursor"] = {new_lnum, new_col}-    if (response.text ~= text) then-      vim.schedule(function ()-        update_buffer(bufnr, text, response.text)-        return api.nvim_win_set_cursor(winnr, {new_lnum, (new_col - 1)})-      end-    )-    end-    highlight_error(bufnr, response.error)-    return table.insert(elapsed_times[bufnr], (vim.loop.hrtime() - start))+  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-end--local function slice(lines, start_row, start_col, row_offset, col_offset)-  local start_row0 = start_row + 1-  local start_col0 = start_col + 1--  local function get_offset_or_something()-    if row_offset == 0 then-      return start_col0 + col_offset - 1-    else-      return -1-    end-  end--  local first_line = string.sub(lines[start_row0], start_col0, get_offset_or_something())-  local out = {first_line}--  for i = (start_row0 + 1), ((start_row0 + row_offset) - 1) do-    local line = lines[i]-    table.insert(out, line)-  end--  if row_offset ~= 0 then-    local function still_dont_know()-      if ((0 < col_offset) and lines[(start_row0 + row_offset)]) then-        return string.sub(lines[(start_row0 + row_offset)], 1, col_offset)-      else-        return ""-      end-    end-    table.insert(out, still_dont_know())-  end-  return out-end---- TODO bad function-local function on_bytes(_, bufnr, _0, start_row, start_col, _1, old_end_row, old_end_col, _2, new_end_row, new_end_col)-  if (normalize_bool(get_option("parinfer_enabled")) and not state[bufnr].locked) then-    local prev_contents = state[bufnr]["prev-contents"]-    local contents = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)-    if (start_row < #prev_contents) then-      local old_text = slice(prev_contents, start_row, start_col, old_end_row, old_end_col)-      local new_text-      if (start_row < #contents) then-        new_text = slice(contents, start_row, start_col, new_end_row, new_end_col)-      else-        new_text = {""}-      end-      if not state[bufnr].changes then-        state[bufnr]["changes"] = {}-      else-      end-      table.insert(state[bufnr].changes, {oldText = table.concat(old_text, "\n"), newText = table.concat(new_text, "\n"), lineNo = (start_row + 1), x = (start_col + 1)})-    else-    end-    state[bufnr]["prev-contents"] = contents-  end-end--local function enter_buffer(bufnr)-  local contents = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)-  state[bufnr]["changedtick"] = -1-  state[bufnr]["prev-contents"] = contents-  vim.api.nvim_buf_attach(bufnr, false, {on_bytes = on_bytes})-  local mode = vim.g.parinfer_mode-  vim.g.parinfer_mode = "paren"-  process_buffer(bufnr)-  vim.g.parinfer_mode = mode+  s.cursor = {response.cursorLine, response.cursorX}+  highlight_error(bufnr, response.error) end  local function cursor_moved(bufnr)-  return vim.schedule(function()-    local cursor = vim.api.nvim_win_get_cursor(0)-    local lnum = cursor[1]-    local col = cursor[2]-    state[bufnr]["prev-cursor"] = {lnum, (col + 1)}-  end)+  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)-  enter_buffer(bufnr)+  local lnum, col = unpack(api.nvim_win_get_cursor(0))+  state[bufnr] = {cursor = {lnum, col + 1}} -  vim.api.nvim_exec_autocmds("User", {pattern = "Parinfer"})+  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}) -  local ag = vim.api.nvim_create_augroup("parinfer", {clear = true})+  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, {}) -  vim.api.nvim_create_autocmd({"TextChanged", "TextChangedI", "TextChangedP"}, {buffer = bufnr, group = ag, callback = function() process_buffer(bufnr) end})-  vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, {buffer = bufnr, group = ag, callback = function() cursor_moved(bufnr) 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()-  return not vim.wo.previewwindow and vim.bo.buftype ~= "prompt"+  --- 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()-  print("alright")   if should_enable() then-    print("okay")-    local bufnr = vim.api.nvim_get_current_buf()-    setup_buffer(bufnr)+    setup_buffer(api.nvim_get_current_buf())   end end