plugin.lua

Branches > main > 449f22a > 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 diff = vim.text.diff or vim.diff
local parinfer = require("parinfer.parinfer")
local modes = {indent = parinfer.indentMode,
               paren = parinfer.parenMode,
               smart = parinfer.smartMode}

local ns = api.nvim_create_namespace("parinfer")

local state = setmetatable({}, {__index = function(t, k)
  t[k] = {}
  return t[k]
end})

-- 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(opt)
  local b = vim.b[opt]
  if b == nil then
    return vim.g[opt]
  else
    return b
  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

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
  end

  return t
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_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)
  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")
  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
end

local function should_run(bufnr)
  return normalize_bool(get_option("parinfer_enabled"))
    and not state[bufnr].locked
    and not vim.o.paste
    and not vim.bo.readonly
    and vim.bo.modifiable
    and vim.b[bufnr].changedtick ~= state[bufnr].changedtick
    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)
  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))
  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
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)
end

local function setup_buffer(bufnr)
  enter_buffer(bufnr)

  vim.api.nvim_exec_autocmds("User", {pattern = "Parinfer"})

  local ag = vim.api.nvim_create_augroup("parinfer", {clear = true})

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

local function should_enable()
  return not vim.wo.previewwindow and vim.bo.buftype ~= "prompt"
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)
  end
end

return {setup = setup}