plugin.lua

Branches > main > 48e5e95 > 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})

-- TODO I don't really get the point of this
local function normalize_bool(val)
  if type(val) == "boolean" then
    return val
  end

  if val == nil then
    return nil
  end

  return val ~= 0
end

local function get_option(opt)
  local b = vim.b[opt] or vim.g[opt]
  if b == nil then
    return vim.g[opt]
  else
    return b
  end
end

local function invoke_parinfer(bufnr, text, lnum, col)
  local _let_32_ = (state[bufnr]["prev-cursor"] or {})
  local prev_lnum = _let_32_[1]
  local prev_col = _let_32_[2]
  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

-- Does kidna flicker
-- Also bad still
local function update_buffer(bufnr, old_contents, new_contents)
  api.nvim_command("silent! undojoin")
  state[bufnr]["locked"] = true
  do
    local new_lines = vim.split(new_contents, "\n")
    local hunks = diff(old_contents, new_contents, {result_type = "indices"})
    for _, hunk in ipairs(hunks) do
      local start_a = hunk[1]
      local count_a = hunk[2]
      local start_b = hunk[3]
      local count_b = hunk[4]
      local lines
      do
        local t = {}
        local i = 0
        for num = start_b, start_b + count_b do
          local new_line = new_lines[num]
          if new_line ~= nil then
            i = i + 1
            t[i] = new_line
          else
          end
        end
        lines = t
      end

      api.nvim_buf_set_lines(bufnr, start_a - 1, start_a + count_a, false, lines)
    end
  end
  local function _35_()
    state[bufnr]["locked"] = false
    return nil
  end
  return vim.schedule(_35_)
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")
  else
    return nil
  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()
  local bufnr = vim.api.nvim_get_current_buf()
  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 should_enable()
  return not vim.wo.previewwindow and vim.bo.buftype ~= "prompt"
end

--- Sets up parinfer in the current buffer
local function setup()
  if should_enable() then
    local bufnr = vim.api.nvim_get_current_buf()
    enter_buffer()

    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
end

return {setup = setup}