ada9461
Branches > main > ada9461 > files / nvim / lua
Nvim - Re-enable parinfer, fix multi-line edits
Commit: ada9461afdb15b1d69c9f3456e4d6262cdf84521
Parent: f198332
2 changed files:
nvim/ftplugin/clojure.lua view file
@@ -1,2 +1,2 @@ require("plugins.conjure")--- require("parinfer.plugin").setup()+require("parinfer.plugin").setup()nvim/lua/parinfer/plugin.lua 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