// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// Implements extended marks for plugins. Marks sit in a MarkTree
// datastructure which provides both efficient mark insertations/lookups
// and adjustment to text changes. See marktree.c for more details.
//
// A map of pointers to the marks is used for fast lookup by mark id.
//
// Marks are moved by calls to extmark_splice. Some standard interfaces
// mark_adjust and inserted_bytes already adjust marks, check if these are
// being used before adding extmark_splice calls!
//
// Undo/Redo of marks is implemented by storing the call arguments to
// extmark_splice. The list of arguments is applied in extmark_apply_undo.
// We have to copy extmark positions when the extmarks are within a
// deleted/changed region.
//
// Marks live in namespaces that allow plugins/users to segregate marks
// from other users.
//
// Deleting marks only happens when explicitly calling extmark_del, deleting
// over a range of marks will only move the marks. Deleting on a mark will
// leave it in same position unless it is on the EOL of a line.
//
// Extmarks are used to implement buffer decoration. Decoration is mostly
// regarded as an application of extmarks, however for practical reasons code
// that deletes an extmark with decoration will call back into the decoration
// code for redrawing the line with the deleted decoration.

#include <assert.h>

#include "nvim/api/extmark.h"
#include "nvim/buffer.h"
#include "nvim/buffer_updates.h"
#include "nvim/charset.h"
#include "nvim/decoration.h"
#include "nvim/extmark.h"
#include "nvim/globals.h"
#include "nvim/lib/kbtree.h"
#include "nvim/map.h"
#include "nvim/memline.h"
#include "nvim/pos.h"
#include "nvim/undo.h"
#include "nvim/vim.h"

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "extmark.c.generated.h"
#endif

static ExtmarkNs *buf_ns_ref(buf_T *buf, uint64_t ns_id, bool put)
{
  return map_ref(uint64_t, ExtmarkNs)(buf->b_extmark_ns, ns_id, put);
}


/// Create or update an extmark
///
/// must not be used during iteration!
/// @returns the internal mark id
uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t *idp, int row, colnr_T col, int end_row,
                     colnr_T end_col, Decoration *decor, bool right_gravity, bool end_right_gravity,
                     ExtmarkOp op)
{
  ExtmarkNs *ns = buf_ns_ref(buf, ns_id, true);
  assert(ns != NULL);
  mtpos_t old_pos;
  uint64_t mark = 0;
  uint64_t id = idp ? *idp : 0;

  uint8_t decor_level = kDecorLevelNone;  // no decor
  if (decor) {
    decor_level = kDecorLevelVisible;  // decor affects redraw
    if (kv_size(decor->virt_lines)) {
      decor_level = kDecorLevelVirtLine;  // decor affects horizontal size
    }
  }

  if (id == 0) {
    id = ns->free_id++;
  } else {
    uint64_t old_mark = map_get(uint64_t, uint64_t)(ns->map, id);
    if (old_mark) {
      if (old_mark & MARKTREE_PAIRED_FLAG || end_row > -1) {
        extmark_del(buf, ns_id, id);
      } else {
        MarkTreeIter itr[1] = { 0 };
        old_pos = marktree_lookup(buf->b_marktree, old_mark, itr);
        assert(itr->node);
        if (old_pos.row == row && old_pos.col == col) {
          ExtmarkItem it = map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index,
                                                          old_mark);
          if (it.decor) {
            decor_remove(buf, row, row, it.decor);
          }
          mark = marktree_revise(buf->b_marktree, itr, decor_level);
          goto revised;
        }
        marktree_del_itr(buf->b_marktree, itr, false);
      }
    } else {
      ns->free_id = MAX(ns->free_id, id+1);
    }
  }

  if (end_row > -1) {
    mark = marktree_put_pair(buf->b_marktree,
                             row, col, right_gravity,
                             end_row, end_col, end_right_gravity, decor_level);
  } else {
    mark = marktree_put(buf->b_marktree, row, col, right_gravity, decor_level);
  }

revised:
  map_put(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark,
                                 (ExtmarkItem){ ns_id, id, decor });
  map_put(uint64_t, uint64_t)(ns->map, id, mark);

  if (op != kExtmarkNoUndo) {
    // TODO(bfredl): this doesn't cover all the cases and probably shouldn't
    // be done "prematurely". Any movement in undo history might necessitate
    // adding new marks to old undo headers.
    u_extmark_set(buf, mark, row, col);
  }

  if (decor) {
    if (kv_size(decor->virt_lines)) {
      buf->b_virt_line_blocks++;
    }
    decor_redraw(buf, row, end_row > -1 ? end_row : row, decor);
  }

  if (idp) {
    *idp = id;
  }
  return mark;
}

static bool extmark_setraw(buf_T *buf, uint64_t mark, int row, colnr_T col)
{
  MarkTreeIter itr[1] = { 0 };
  mtpos_t pos = marktree_lookup(buf->b_marktree, mark, itr);
  if (pos.row == -1) {
    return false;
  }

  if (pos.row == row && pos.col == col) {
    return true;
  }

  marktree_move(buf->b_marktree, itr, row, col);
  return true;
}

// Remove an extmark
// Returns 0 on missing id
bool extmark_del(buf_T *buf, uint64_t ns_id, uint64_t id)
{
  ExtmarkNs *ns = buf_ns_ref(buf, ns_id, false);
  if (!ns) {
    return false;
  }

  uint64_t mark = map_get(uint64_t, uint64_t)(ns->map, id);
  if (!mark) {
    return false;
  }

  MarkTreeIter itr[1] = { 0 };
  mtpos_t pos = marktree_lookup(buf->b_marktree, mark, itr);
  assert(pos.row >= 0);
  marktree_del_itr(buf->b_marktree, itr, false);
  ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark);
  mtpos_t pos2 = pos;

  if (mark & MARKTREE_PAIRED_FLAG) {
    pos2 = marktree_lookup(buf->b_marktree, mark|MARKTREE_END_FLAG, itr);
    assert(pos2.row >= 0);
    marktree_del_itr(buf->b_marktree, itr, false);
  }

  if (item.decor) {
    decor_remove(buf, pos.row, pos2.row, item.decor);
  }

  map_del(uint64_t, uint64_t)(ns->map, id);
  map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark);

  // TODO(bfredl): delete it from current undo header, opportunistically?
  return true;
}

// Free extmarks in a ns between lines
// if ns = 0, it means clear all namespaces
bool extmark_clear(buf_T *buf, uint64_t ns_id, int l_row, colnr_T l_col, int u_row, colnr_T u_col)
{
  if (!map_size(buf->b_extmark_ns)) {
    return false;
  }

  bool marks_cleared = false;

  bool all_ns = (ns_id == 0);
  ExtmarkNs *ns = NULL;
  if (!all_ns) {
    ns = buf_ns_ref(buf, ns_id, false);
    if (!ns) {
      // nothing to do
      return false;
    }

    // TODO(bfredl): if map_size(ns->map) << buf->b_marktree.n_nodes
    // it could be faster to iterate over the map instead
  }

  // the value is either zero or the lnum (row+1) if highlight was present.
  static Map(uint64_t, ssize_t) delete_set = MAP_INIT;
  typedef struct { Decoration *decor; int row1; } DecorItem;
  static kvec_t(DecorItem) decors;

  MarkTreeIter itr[1] = { 0 };
  marktree_itr_get(buf->b_marktree, l_row, l_col, itr);
  while (true) {
    mtmark_t mark = marktree_itr_current(itr);
    if (mark.row < 0
        || mark.row > u_row
        || (mark.row == u_row && mark.col > u_col)) {
      break;
    }
    ssize_t *del_status = map_ref(uint64_t, ssize_t)(&delete_set, mark.id,
                                                     false);
    if (del_status) {
      marktree_del_itr(buf->b_marktree, itr, false);
      if (*del_status >= 0) {  // we had a decor_id
        DecorItem it = kv_A(decors, *del_status);
        decor_remove(buf, it.row1, mark.row, it.decor);
      }
      map_del(uint64_t, ssize_t)(&delete_set, mark.id);
      continue;
    }

    uint64_t start_id = mark.id & ~MARKTREE_END_FLAG;
    ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index,
                                                      start_id);

    assert(item.ns_id > 0 && item.mark_id > 0);
    if (item.mark_id > 0 && (item.ns_id == ns_id || all_ns)) {
      marks_cleared = true;
      if (mark.id & MARKTREE_PAIRED_FLAG) {
        uint64_t other = mark.id ^ MARKTREE_END_FLAG;
        ssize_t decor_id = -1;
        if (item.decor) {
          // Save the decoration and the first pos. Clear the decoration
          // later when we know the full range.
          decor_id = (ssize_t)kv_size(decors);
          kv_push(decors,
                  ((DecorItem) { .decor = item.decor, .row1 = mark.row }));
        }
        map_put(uint64_t, ssize_t)(&delete_set, other, decor_id);
      } else if (item.decor) {
        decor_remove(buf, mark.row, mark.row, item.decor);
      }
      ExtmarkNs *my_ns = all_ns ? buf_ns_ref(buf, item.ns_id, false) : ns;
      map_del(uint64_t, uint64_t)(my_ns->map, item.mark_id);
      map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, start_id);
      marktree_del_itr(buf->b_marktree, itr, false);
    } else {
      marktree_itr_next(buf->b_marktree, itr);
    }
  }
  uint64_t id;
  ssize_t decor_id;
  map_foreach(&delete_set, id, decor_id, {
    mtpos_t pos = marktree_lookup(buf->b_marktree, id, itr);
    assert(itr->node);
    marktree_del_itr(buf->b_marktree, itr, false);
    if (decor_id >= 0) {
      DecorItem it = kv_A(decors, decor_id);
      decor_remove(buf, it.row1, pos.row, it.decor);
    }
  });
  map_clear(uint64_t, ssize_t)(&delete_set);
  kv_size(decors) = 0;
  return marks_cleared;
}

// Returns the position of marks between a range,
// marks found at the start or end index will be included,
// if upper_lnum or upper_col are negative the buffer
// will be searched to the start, or end
// dir can be set to control the order of the array
// amount = amount of marks to find or -1 for all
ExtmarkInfoArray extmark_get(buf_T *buf, uint64_t ns_id, int l_row, colnr_T l_col, int u_row,
                             colnr_T u_col, int64_t amount, bool reverse)
{
  ExtmarkInfoArray array = KV_INITIAL_VALUE;
  MarkTreeIter itr[1];
  // Find all the marks
  marktree_itr_get_ext(buf->b_marktree, (mtpos_t){ l_row, l_col },
                       itr, reverse, false, NULL);
  int order = reverse ? -1 : 1;
  while ((int64_t)kv_size(array) < amount) {
    mtmark_t mark = marktree_itr_current(itr);
    mtpos_t endpos = { -1, -1 };
    if (mark.row < 0
        || (mark.row - u_row) * order > 0
        || (mark.row == u_row && (mark.col - u_col) * order > 0)) {
      break;
    }
    if (mark.id & MARKTREE_END_FLAG) {
      goto next_mark;
    } else if (mark.id & MARKTREE_PAIRED_FLAG) {
      endpos = marktree_lookup(buf->b_marktree, mark.id | MARKTREE_END_FLAG,
                               NULL);
    }


    ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index,
                                                      mark.id);
    if (item.ns_id == ns_id) {
      kv_push(array, ((ExtmarkInfo) { .ns_id = item.ns_id,
                                      .mark_id = item.mark_id,
                                      .row = mark.row, .col = mark.col,
                                      .end_row = endpos.row,
                                      .end_col = endpos.col,
                                      .decor = item.decor }));
    }
next_mark:
    if (reverse) {
      marktree_itr_prev(buf->b_marktree, itr);
    } else {
      marktree_itr_next(buf->b_marktree, itr);
    }
  }
  return array;
}

// Lookup an extmark by id
ExtmarkInfo extmark_from_id(buf_T *buf, uint64_t ns_id, uint64_t id)
{
  ExtmarkNs *ns = buf_ns_ref(buf, ns_id, false);
  ExtmarkInfo ret = { 0, 0, -1, -1, -1, -1, NULL };
  if (!ns) {
    return ret;
  }

  uint64_t mark = map_get(uint64_t, uint64_t)(ns->map, id);
  if (!mark) {
    return ret;
  }

  mtpos_t pos = marktree_lookup(buf->b_marktree, mark, NULL);
  mtpos_t endpos = { -1, -1 };
  if (mark & MARKTREE_PAIRED_FLAG) {
    endpos = marktree_lookup(buf->b_marktree, mark | MARKTREE_END_FLAG, NULL);
  }
  assert(pos.row >= 0);

  ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index,
                                                    mark);

  ret.ns_id = ns_id;
  ret.mark_id = id;
  ret.row = pos.row;
  ret.col = pos.col;
  ret.end_row = endpos.row;
  ret.end_col = endpos.col;
  ret.decor = item.decor;

  return ret;
}


// free extmarks from the buffer
void extmark_free_all(buf_T *buf)
{
  if (!map_size(buf->b_extmark_ns)) {
    return;
  }

  uint64_t id;
  ExtmarkNs ns;
  ExtmarkItem item;

  marktree_clear(buf->b_marktree);

  map_foreach(buf->b_extmark_ns, id, ns, {
    (void)id;
    map_destroy(uint64_t, uint64_t)(ns.map);
  });
  map_destroy(uint64_t, ExtmarkNs)(buf->b_extmark_ns);
  map_init(uint64_t, ExtmarkNs, buf->b_extmark_ns);

  map_foreach(buf->b_extmark_index, id, item, {
    (void)id;
    decor_free(item.decor);
  });
  map_destroy(uint64_t, ExtmarkItem)(buf->b_extmark_index);
  map_init(uint64_t, ExtmarkItem, buf->b_extmark_index);
}


// Save info for undo/redo of set marks
static void u_extmark_set(buf_T *buf, uint64_t mark, int row, colnr_T col)
{
  u_header_T *uhp = u_force_get_undo_header(buf);
  if (!uhp) {
    return;
  }

  ExtmarkSavePos pos;
  pos.mark = mark;
  pos.old_row = -1;
  pos.old_col = -1;
  pos.row = row;
  pos.col = col;

  ExtmarkUndoObject undo = { .type = kExtmarkSavePos,
                             .data.savepos = pos };

  kv_push(uhp->uh_extmark, undo);
}

/// copy extmarks data between range
///
/// useful when we cannot simply reverse the operation. This will do nothing on
/// redo, enforces correct position when undo.
void u_extmark_copy(buf_T *buf, int l_row, colnr_T l_col, int u_row, colnr_T u_col)
{
  u_header_T *uhp = u_force_get_undo_header(buf);
  if (!uhp) {
    return;
  }

  ExtmarkUndoObject undo;

  MarkTreeIter itr[1] = { 0 };
  marktree_itr_get(buf->b_marktree, l_row, l_col, itr);
  while (true) {
    mtmark_t mark = marktree_itr_current(itr);
    if (mark.row < 0
        || mark.row > u_row
        || (mark.row == u_row && mark.col > u_col)) {
      break;
    }
    ExtmarkSavePos pos;
    pos.mark = mark.id;
    pos.old_row = mark.row;
    pos.old_col = mark.col;
    pos.row = -1;
    pos.col = -1;

    undo.data.savepos = pos;
    undo.type = kExtmarkSavePos;
    kv_push(uhp->uh_extmark, undo);

    marktree_itr_next(buf->b_marktree, itr);
  }
}

/// undo or redo an extmark operation
void extmark_apply_undo(ExtmarkUndoObject undo_info, bool undo)
{
  // splice: any text operation changing position (except :move)
  if (undo_info.type == kExtmarkSplice) {
    // Undo
    ExtmarkSplice splice = undo_info.data.splice;
    if (undo) {
      extmark_splice_impl(curbuf,
                          splice.start_row, splice.start_col, splice.start_byte,
                          splice.new_row, splice.new_col, splice.new_byte,
                          splice.old_row, splice.old_col, splice.old_byte,
                          kExtmarkNoUndo);
    } else {
      extmark_splice_impl(curbuf,
                          splice.start_row, splice.start_col, splice.start_byte,
                          splice.old_row, splice.old_col, splice.old_byte,
                          splice.new_row, splice.new_col, splice.new_byte,
                          kExtmarkNoUndo);
    }
    // kExtmarkSavePos
  } else if (undo_info.type == kExtmarkSavePos) {
    ExtmarkSavePos pos = undo_info.data.savepos;
    if (undo) {
      if (pos.old_row >= 0) {
        extmark_setraw(curbuf, pos.mark, pos.old_row, pos.old_col);
      }
      // Redo
    } else {
      if (pos.row >= 0) {
        extmark_setraw(curbuf, pos.mark, pos.row, pos.col);
      }
    }
  } else if (undo_info.type == kExtmarkMove) {
    ExtmarkMove move = undo_info.data.move;
    if (undo) {
      extmark_move_region(curbuf,
                          move.new_row, move.new_col, move.new_byte,
                          move.extent_row, move.extent_col, move.extent_byte,
                          move.start_row, move.start_col, move.start_byte,
                          kExtmarkNoUndo);
    } else {
      extmark_move_region(curbuf,
                          move.start_row, move.start_col, move.start_byte,
                          move.extent_row, move.extent_col, move.extent_byte,
                          move.new_row, move.new_col, move.new_byte,
                          kExtmarkNoUndo);
    }
  }
}


// Adjust extmark row for inserted/deleted rows (columns stay fixed).
void extmark_adjust(buf_T *buf, linenr_T line1, linenr_T line2, long amount, long amount_after,
                    ExtmarkOp undo)
{
  if (curbuf_splice_pending) {
    return;
  }
  bcount_t start_byte = ml_find_line_or_offset(buf, line1, NULL, true);
  bcount_t old_byte = 0, new_byte = 0;
  int old_row, new_row;
  if (amount == MAXLNUM) {
    old_row = (int)(line2 - line1+1);
    // TODO(bfredl): ej kasta?
    old_byte = (bcount_t)buf->deleted_bytes2;

    new_row = (int)(amount_after + old_row);
  } else {
    // A region is either deleted (amount == MAXLNUM) or
    // added (line2 == MAXLNUM). The only other case is :move
    // which is handled by a separate entry point extmark_move_region.
    assert(line2 == MAXLNUM);
    old_row = 0;
    new_row = (int)amount;
  }
  if (new_row > 0) {
    new_byte = ml_find_line_or_offset(buf, line1+new_row, NULL, true)
               - start_byte;
  }
  extmark_splice_impl(buf,
                      (int)line1-1, 0, start_byte,
                      old_row, 0, old_byte,
                      new_row, 0, new_byte, undo);
}

// Adjust extmarks following a text edit.
//
// @param buf
// @param start_row   Start row of the region to be changed
// @param start_col   Start col of the region to be changed
// @param old_row     End row of the region to be changed.
//                      Encoded as an offset to start_row.
// @param old_col     End col of the region to be changed. Encodes
//                      an offset from start_col if old_row = 0; otherwise,
//                      encodes the end column of the old region.
// @param old_byte    Byte extent of the region to be changed.
// @param new_row     Row offset of the new region.
// @param new_col     Col offset of the new region. Encodes an offset from
//                      start_col if new_row = 0; otherwise, encodes
//                      the end column of the new region.
// @param new_byte    Byte extent of the new region.
// @param undo
void extmark_splice(buf_T *buf, int start_row, colnr_T start_col, int old_row, colnr_T old_col,
                    bcount_t old_byte, int new_row, colnr_T new_col, bcount_t new_byte,
                    ExtmarkOp undo)
{
  long offset = ml_find_line_or_offset(buf, start_row + 1, NULL, true);

  // On empty buffers, when editing the first line, the line is buffered,
  // causing offset to be < 0. While the buffer is not actually empty, the
  // buffered line has not been flushed (and should not be) yet, so the call is
  // valid but an edge case.
  //
  // TODO(vigoux): maybe the is a better way of testing that ?
  if (offset < 0 && buf->b_ml.ml_chunksize == NULL) {
    offset = 0;
  }
  extmark_splice_impl(buf, start_row, start_col, offset + start_col,
                      old_row, old_col, old_byte, new_row, new_col, new_byte,
                      undo);
}

void extmark_splice_impl(buf_T *buf, int start_row, colnr_T start_col, bcount_t start_byte,
                         int old_row, colnr_T old_col, bcount_t old_byte, int new_row,
                         colnr_T new_col, bcount_t new_byte, ExtmarkOp undo)
{
  buf->deleted_bytes2 = 0;
  buf_updates_send_splice(buf, start_row, start_col, start_byte,
                          old_row, old_col, old_byte,
                          new_row, new_col, new_byte);

  if (undo == kExtmarkUndo && (old_row > 0 || old_col > 0)) {
    // Copy marks that would be effected by delete
    // TODO(bfredl): Be "smart" about gravity here, left-gravity at the
    // beginning and right-gravity at the end need not be preserved.
    // Also be smart about marks that already have been saved (important for
    // merge!)
    int end_row = start_row + old_row;
    int end_col = (old_row ? 0 : start_col) + old_col;
    u_extmark_copy(buf, start_row, start_col, end_row, end_col);
  }


  marktree_splice(buf->b_marktree, start_row, start_col,
                  old_row, old_col,
                  new_row, new_col);

  if (undo == kExtmarkUndo) {
    u_header_T *uhp = u_force_get_undo_header(buf);
    if (!uhp) {
      return;
    }

    bool merged = false;
    // TODO(bfredl): this is quite rudimentary. We merge small (within line)
    // inserts with each other and small deletes with each other. Add full
    // merge algorithm later.
    if (old_row == 0 && new_row == 0 && kv_size(uhp->uh_extmark)) {
      ExtmarkUndoObject *item = &kv_A(uhp->uh_extmark,
                                      kv_size(uhp->uh_extmark)-1);
      if (item->type == kExtmarkSplice) {
        ExtmarkSplice *splice = &item->data.splice;
        if (splice->start_row == start_row && splice->old_row == 0
            && splice->new_row == 0) {
          if (old_col == 0 && start_col >= splice->start_col
              && start_col <= splice->start_col+splice->new_col) {
            splice->new_col += new_col;
            splice->new_byte += new_byte;
            merged = true;
          } else if (new_col == 0
                     && start_col == splice->start_col+splice->new_col) {
            splice->old_col += old_col;
            splice->old_byte += old_byte;
            merged = true;
          } else if (new_col == 0
                     && start_col + old_col == splice->start_col) {
            splice->start_col = start_col;
            splice->start_byte = start_byte;
            splice->old_col += old_col;
            splice->old_byte += old_byte;
            merged = true;
          }
        }
      }
    }

    if (!merged) {
      ExtmarkSplice splice;
      splice.start_row = start_row;
      splice.start_col = start_col;
      splice.start_byte = start_byte;
      splice.old_row = old_row;
      splice.old_col = old_col;
      splice.old_byte = old_byte;
      splice.new_row = new_row;
      splice.new_col = new_col;
      splice.new_byte = new_byte;

      kv_push(uhp->uh_extmark,
              ((ExtmarkUndoObject){ .type = kExtmarkSplice,
                                    .data.splice = splice }));
    }
  }
}

void extmark_splice_cols(buf_T *buf, int start_row, colnr_T start_col, colnr_T old_col,
                         colnr_T new_col, ExtmarkOp undo)
{
  extmark_splice(buf, start_row, start_col,
                 0, old_col, old_col,
                 0, new_col, new_col, undo);
}

void extmark_move_region(buf_T *buf, int start_row, colnr_T start_col, bcount_t start_byte,
                         int extent_row, colnr_T extent_col, bcount_t extent_byte, int new_row,
                         colnr_T new_col, bcount_t new_byte, ExtmarkOp undo)
{
  buf->deleted_bytes2 = 0;
  // TODO(bfredl): this is not synced to the buffer state inside the callback.
  // But unless we make the undo implementation smarter, this is not ensured
  // anyway.
  buf_updates_send_splice(buf, start_row, start_col, start_byte,
                          extent_row, extent_col, extent_byte,
                          0, 0, 0);

  marktree_move_region(buf->b_marktree, start_row, start_col,
                       extent_row, extent_col,
                       new_row, new_col);

  buf_updates_send_splice(buf, new_row, new_col, new_byte,
                          0, 0, 0,
                          extent_row, extent_col, extent_byte);


  if (undo == kExtmarkUndo) {
    u_header_T *uhp = u_force_get_undo_header(buf);
    if (!uhp) {
      return;
    }

    ExtmarkMove move;
    move.start_row = start_row;
    move.start_col = start_col;
    move.start_byte = start_byte;
    move.extent_row = extent_row;
    move.extent_col = extent_col;
    move.extent_byte = extent_byte;
    move.new_row = new_row;
    move.new_col = new_col;
    move.new_byte = new_byte;

    kv_push(uhp->uh_extmark,
            ((ExtmarkUndoObject){ .type = kExtmarkMove,
                                  .data.move = move }));
  }
}
