// 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. Each mark exists in a btree of // lines containing btrees of columns. // // The btree provides efficient range lookups. // A map of pointers to the marks is used for fast lookup by mark id. // // Marks are moved by calls to: extmark_col_adjust, extmark_adjust, or // extmark_col_adjust_delete which are based on col_adjust and mark_adjust from // mark.c // // Undo/Redo of marks is implemented by storing the call arguments to // extmark_col_adjust or extmark_adjust. The list of arguments // is applied in extmark_apply_undo. The only case where we have to // copy extmarks is for the area being effected by a delete. // // Marks live in namespaces that allow plugins/users to segregate marks // from other users. // // For possible ideas for efficency improvements see: // http://blog.atom.io/2015/06/16/optimizing-an-important-atom-primitive.html // TODO(bfredl): These ideas could be used for an enhanced btree, which // wouldn't need separate line and column layers. // Other implementations exist in gtk and tk toolkits. // // Deleting marks only happens when explicitly calling extmark_del, deleteing // 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. #include #include "nvim/vim.h" #include "charset.h" #include "nvim/mark_extended.h" #include "nvim/memline.h" #include "nvim/pos.h" #include "nvim/globals.h" #include "nvim/map.h" #include "nvim/lib/kbtree.h" #include "nvim/undo.h" #include "nvim/buffer.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "mark_extended.c.generated.h" #endif /// Create or update an extmark /// /// must not be used during iteration! /// @returns whether a new mark was created int extmark_set(buf_T *buf, uint64_t ns, uint64_t id, linenr_T lnum, colnr_T col, ExtmarkOp op) { Extmark *extmark = extmark_from_id(buf, ns, id); if (!extmark) { extmark_create(buf, ns, id, lnum, col, op); return true; } else { ExtmarkLine *extmarkline = extmark->line; extmark_update(extmark, buf, ns, id, lnum, col, op, NULL); if (kb_size(&extmarkline->items) == 0) { kb_del(extmarklines, &buf->b_extlines, extmarkline); extmarkline_free(extmarkline); } return false; } } // Remove an extmark // Returns 0 on missing id int extmark_del(buf_T *buf, uint64_t ns, uint64_t id, ExtmarkOp op) { Extmark *extmark = extmark_from_id(buf, ns, id); if (!extmark) { return 0; } return extmark_delete(extmark, buf, ns, id, op); } // Free extmarks in a ns between lines // if ns = 0, it means clear all namespaces void extmark_clear(buf_T *buf, uint64_t ns, linenr_T l_lnum, linenr_T u_lnum, ExtmarkOp undo) { if (!buf->b_extmark_ns) { return; } bool marks_cleared = false; if (undo == kExtmarkUndo) { // Copy marks that would be effected by clear u_extmark_copy(buf, ns, l_lnum, 0, u_lnum, MAXCOL); } bool all_ns = ns == 0 ? true : false; ExtmarkNs *ns_obj; if (!all_ns) { ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns); if (!ns_obj) { // nothing to do return; } } FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, { FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, 0, MAXCOL, { if (extmark->ns_id == ns || all_ns) { marks_cleared = true; if (all_ns) { ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, extmark->ns_id); } else { ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns); } pmap_del(uint64_t)(ns_obj->map, extmark->mark_id); kb_del_itr(markitems, &extmarkline->items, &mitr); } }); if (kb_size(&extmarkline->items) == 0) { kb_del_itr(extmarklines, &buf->b_extlines, &itr); extmarkline_free(extmarkline); } }); // Record the undo for the actual move if (marks_cleared && undo == kExtmarkUndo) { u_extmark_clear(buf, ns, l_lnum, u_lnum); } } // 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 ExtmarkArray extmark_get(buf_T *buf, uint64_t ns, linenr_T l_lnum, colnr_T l_col, linenr_T u_lnum, colnr_T u_col, int64_t amount, bool reverse) { ExtmarkArray array = KV_INITIAL_VALUE; // Find all the marks if (!reverse) { FOR_ALL_EXTMARKS(buf, ns, l_lnum, l_col, u_lnum, u_col, { if (extmark->ns_id == ns) { kv_push(array, extmark); if (kv_size(array) == (size_t)amount) { return array; } } }) } else { FOR_ALL_EXTMARKS_PREV(buf, ns, l_lnum, l_col, u_lnum, u_col, { if (extmark->ns_id == ns) { kv_push(array, extmark); if (kv_size(array) == (size_t)amount) { return array; } } }) } return array; } static void extmark_create(buf_T *buf, uint64_t ns, uint64_t id, linenr_T lnum, colnr_T col, ExtmarkOp op) { if (!buf->b_extmark_ns) { buf->b_extmark_ns = pmap_new(uint64_t)(); } ExtmarkNs *ns_obj = NULL; ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns); // Initialize a new namespace for this buffer if (!ns_obj) { ns_obj = xmalloc(sizeof(ExtmarkNs)); ns_obj->map = pmap_new(uint64_t)(); pmap_put(uint64_t)(buf->b_extmark_ns, ns, ns_obj); } // Create or get a line ExtmarkLine *extmarkline = extmarkline_ref(buf, lnum, true); // Create and put mark on the line extmark_put(col, id, extmarkline, ns); // Marks do not have stable address so we have to look them up // by using the line instead of the mark pmap_put(uint64_t)(ns_obj->map, id, extmarkline); if (op != kExtmarkNoUndo) { u_extmark_set(buf, ns, id, lnum, col, kExtmarkSet); } // Set a free id so extmark_free_id_get works extmark_free_id_set(ns_obj, id); } // update the position of an extmark // to update while iterating pass the markitems itr static void extmark_update(Extmark *extmark, buf_T *buf, uint64_t ns, uint64_t id, linenr_T lnum, colnr_T col, ExtmarkOp op, kbitr_t(markitems) *mitr) { assert(op != kExtmarkNOOP); if (op != kExtmarkNoUndo) { u_extmark_update(buf, ns, id, extmark->line->lnum, extmark->col, lnum, col); } ExtmarkLine *old_line = extmark->line; // Move the mark to a new line and update column if (old_line->lnum != lnum) { ExtmarkLine *ref_line = extmarkline_ref(buf, lnum, true); extmark_put(col, id, ref_line, ns); // Update the hashmap ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns); pmap_put(uint64_t)(ns_obj->map, id, ref_line); // Delete old mark if (mitr != NULL) { kb_del_itr(markitems, &(old_line->items), mitr); } else { kb_del(markitems, &old_line->items, *extmark); } // Just update the column } else { if (mitr != NULL) { // The btree stays organized during iteration with kbitr_t extmark->col = col; } else { // Keep the btree in order kb_del(markitems, &old_line->items, *extmark); extmark_put(col, id, old_line, ns); } } } static int extmark_delete(Extmark *extmark, buf_T *buf, uint64_t ns, uint64_t id, ExtmarkOp op) { if (op != kExtmarkNoUndo) { u_extmark_set(buf, ns, id, extmark->line->lnum, extmark->col, kExtmarkDel); } // Remove our key from the namespace ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns); pmap_del(uint64_t)(ns_obj->map, id); // Remove the mark mark from the line ExtmarkLine *extmarkline = extmark->line; kb_del(markitems, &extmarkline->items, *extmark); // Remove the line if there are no more marks in the line if (kb_size(&extmarkline->items) == 0) { kb_del(extmarklines, &buf->b_extlines, extmarkline); extmarkline_free(extmarkline); } return true; } // Lookup an extmark by id Extmark *extmark_from_id(buf_T *buf, uint64_t ns, uint64_t id) { if (!buf->b_extmark_ns) { return NULL; } ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns); if (!ns_obj || !kh_size(ns_obj->map->table)) { return NULL; } ExtmarkLine *extmarkline = pmap_get(uint64_t)(ns_obj->map, id); if (!extmarkline) { return NULL; } FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, 0, MAXCOL, { if (extmark->ns_id == ns && extmark->mark_id == id) { return extmark; } }) return NULL; } // Lookup an extmark by position Extmark *extmark_from_pos(buf_T *buf, uint64_t ns, linenr_T lnum, colnr_T col) { if (!buf->b_extmark_ns) { return NULL; } FOR_ALL_EXTMARKS(buf, ns, lnum, col, lnum, col, { if (extmark->ns_id == ns) { if (extmark->col == col) { return extmark; } } }) return NULL; } // Returns an available id in a namespace uint64_t extmark_free_id_get(buf_T *buf, uint64_t ns) { if (!buf->b_extmark_ns) { return 1; } ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, ns); if (!ns_obj) { return 1; } return ns_obj->free_id; } // Set the next free id in a namesapce static void extmark_free_id_set(ExtmarkNs *ns_obj, uint64_t id) { // Simply Heurstic, the largest id + 1 ns_obj->free_id = id + 1; } // free extmarks from the buffer void extmark_free_all(buf_T *buf) { if (!buf->b_extmark_ns) { return; } uint64_t ns; ExtmarkNs *ns_obj; FOR_ALL_EXTMARKLINES(buf, 1, MAXLNUM, { kb_del_itr(extmarklines, &buf->b_extlines, &itr); extmarkline_free(extmarkline); }) map_foreach(buf->b_extmark_ns, ns, ns_obj, { (void)ns; pmap_free(uint64_t)(ns_obj->map); xfree(ns_obj); }); pmap_free(uint64_t)(buf->b_extmark_ns); buf->b_extmark_ns = NULL; // k?_init called to set pointers to NULL kb_destroy(extmarklines, (&buf->b_extlines)); kb_init(&buf->b_extlines); kv_destroy(buf->b_extmark_move_space); kv_init(buf->b_extmark_move_space); } // Save info for undo/redo of set marks static void u_extmark_set(buf_T *buf, uint64_t ns, uint64_t id, linenr_T lnum, colnr_T col, UndoObjectType undo_type) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } ExtmarkSet set; set.ns_id = ns; set.mark_id = id; set.lnum = lnum; set.col = col; ExtmarkUndoObject undo = { .type = undo_type, .data.set = set }; kv_push(uhp->uh_extmark, undo); } // Save info for undo/redo of deleted marks static void u_extmark_update(buf_T *buf, uint64_t ns, uint64_t id, linenr_T old_lnum, colnr_T old_col, linenr_T lnum, colnr_T col) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } ExtmarkUpdate update; update.ns_id = ns; update.mark_id = id; update.old_lnum = old_lnum; update.old_col = old_col; update.lnum = lnum; update.col = col; ExtmarkUndoObject undo = { .type = kExtmarkUpdate, .data.update = update }; kv_push(uhp->uh_extmark, undo); } // Hueristic works only for when the user is typing in insert mode // - Instead of 1 undo object for each char inserted, // we create 1 undo objet for all text inserted before the user hits esc // Return True if we compacted else False static bool u_compact_col_adjust(buf_T *buf, linenr_T lnum, colnr_T mincol, long lnum_amount, long col_amount) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return false; } if (kv_size(uhp->uh_extmark) < 1) { return false; } // Check the last action ExtmarkUndoObject object = kv_last(uhp->uh_extmark); if (object.type != kColAdjust) { return false; } ColAdjust undo = object.data.col_adjust; bool compactable = false; if (!undo.lnum_amount && !lnum_amount) { if (undo.lnum == lnum) { if ((undo.mincol + undo.col_amount) >= mincol) { compactable = true; } } } if (!compactable) { return false; } undo.col_amount = undo.col_amount + col_amount; ExtmarkUndoObject new_undo = { .type = kColAdjust, .data.col_adjust = undo }; kv_last(uhp->uh_extmark) = new_undo; return true; } // Save col_adjust info so we can undo/redo void u_extmark_col_adjust(buf_T *buf, linenr_T lnum, colnr_T mincol, long lnum_amount, long col_amount) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } if (!u_compact_col_adjust(buf, lnum, mincol, lnum_amount, col_amount)) { ColAdjust col_adjust; col_adjust.lnum = lnum; col_adjust.mincol = mincol; col_adjust.lnum_amount = lnum_amount; col_adjust.col_amount = col_amount; ExtmarkUndoObject undo = { .type = kColAdjust, .data.col_adjust = col_adjust }; kv_push(uhp->uh_extmark, undo); } } // Save col_adjust_delete info so we can undo/redo void u_extmark_col_adjust_delete(buf_T *buf, linenr_T lnum, colnr_T mincol, colnr_T endcol, int eol) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } ColAdjustDelete col_adjust_delete; col_adjust_delete.lnum = lnum; col_adjust_delete.mincol = mincol; col_adjust_delete.endcol = endcol; col_adjust_delete.eol = eol; ExtmarkUndoObject undo = { .type = kColAdjustDelete, .data.col_adjust_delete = col_adjust_delete }; kv_push(uhp->uh_extmark, undo); } // Save adjust info so we can undo/redo static void u_extmark_adjust(buf_T * buf, linenr_T line1, linenr_T line2, long amount, long amount_after) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } Adjust adjust; adjust.line1 = line1; adjust.line2 = line2; adjust.amount = amount; adjust.amount_after = amount_after; ExtmarkUndoObject undo = { .type = kLineAdjust, .data.adjust = adjust }; kv_push(uhp->uh_extmark, undo); } // save info to undo/redo a :move void u_extmark_move(buf_T *buf, linenr_T line1, linenr_T line2, linenr_T last_line, linenr_T dest, linenr_T num_lines, linenr_T extra) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } AdjustMove move; move.line1 = line1; move.line2 = line2; move.last_line = last_line; move.dest = dest; move.num_lines = num_lines; move.extra = extra; ExtmarkUndoObject undo = { .type = kAdjustMove, .data.move = move }; 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. // if ns = 0, it means copy all namespaces void u_extmark_copy(buf_T *buf, uint64_t ns, linenr_T l_lnum, colnr_T l_col, linenr_T u_lnum, colnr_T u_col) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } bool all_ns = ns == 0 ? true : false; ExtmarkCopy copy; ExtmarkUndoObject undo; FOR_ALL_EXTMARKS(buf, 1, l_lnum, l_col, u_lnum, u_col, { if (all_ns || extmark->ns_id == ns) { copy.ns_id = extmark->ns_id; copy.mark_id = extmark->mark_id; copy.lnum = extmark->line->lnum; copy.col = extmark->col; undo.data.copy = copy; undo.type = kExtmarkCopy; kv_push(uhp->uh_extmark, undo); } }); } void u_extmark_copy_place(buf_T *buf, linenr_T l_lnum, colnr_T l_col, linenr_T u_lnum, colnr_T u_col, linenr_T p_lnum, colnr_T p_col) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } ExtmarkCopyPlace copy_place; copy_place.l_lnum = l_lnum; copy_place.l_col = l_col; copy_place.u_lnum = u_lnum; copy_place.u_col = u_col; copy_place.p_lnum = p_lnum; copy_place.p_col = p_col; ExtmarkUndoObject undo = { .type = kExtmarkCopyPlace, .data.copy_place = copy_place }; kv_push(uhp->uh_extmark, undo); } // Save info for undo/redo of extmark_clear static void u_extmark_clear(buf_T *buf, uint64_t ns, linenr_T l_lnum, linenr_T u_lnum) { u_header_T *uhp = u_force_get_undo_header(buf); if (!uhp) { return; } ExtmarkClear clear; clear.ns_id = ns; clear.l_lnum = l_lnum; clear.u_lnum = u_lnum; ExtmarkUndoObject undo = { .type = kExtmarkClear, .data.clear = clear }; kv_push(uhp->uh_extmark, undo); } // undo or redo an extmark operation void extmark_apply_undo(ExtmarkUndoObject undo_info, bool undo) { linenr_T lnum; colnr_T mincol; long lnum_amount; long col_amount; linenr_T line1; linenr_T line2; long amount; long amount_after; // use extmark_col_adjust if (undo_info.type == kColAdjust) { // Undo if (undo) { lnum = (undo_info.data.col_adjust.lnum + undo_info.data.col_adjust.lnum_amount); lnum_amount = -undo_info.data.col_adjust.lnum_amount; col_amount = -undo_info.data.col_adjust.col_amount; mincol = (undo_info.data.col_adjust.mincol + (colnr_T)undo_info.data.col_adjust.col_amount); // Redo } else { lnum = undo_info.data.col_adjust.lnum; col_amount = undo_info.data.col_adjust.col_amount; lnum_amount = undo_info.data.col_adjust.lnum_amount; mincol = undo_info.data.col_adjust.mincol; } extmark_col_adjust(curbuf, lnum, mincol, lnum_amount, col_amount, kExtmarkNoUndo); // use extmark_col_adjust_delete } else if (undo_info.type == kColAdjustDelete) { if (undo) { mincol = undo_info.data.col_adjust_delete.mincol; col_amount = (undo_info.data.col_adjust_delete.endcol - undo_info.data.col_adjust_delete.mincol) + 1; extmark_col_adjust(curbuf, undo_info.data.col_adjust_delete.lnum, mincol, 0, col_amount, kExtmarkNoUndo); // Redo } else { extmark_col_adjust_delete(curbuf, undo_info.data.col_adjust_delete.lnum, undo_info.data.col_adjust_delete.mincol, undo_info.data.col_adjust_delete.endcol, kExtmarkNoUndo, undo_info.data.col_adjust_delete.eol); } // use extmark_adjust } else if (undo_info.type == kLineAdjust) { if (undo) { // Undo - call signature type one - insert now if (undo_info.data.adjust.amount == MAXLNUM) { line1 = undo_info.data.adjust.line1; line2 = MAXLNUM; amount = -undo_info.data.adjust.amount_after; amount_after = 0; // Undo - call singature type two - delete now } else if (undo_info.data.adjust.line2 == MAXLNUM) { line1 = undo_info.data.adjust.line1; line2 = undo_info.data.adjust.line2; amount = -undo_info.data.adjust.amount; amount_after = undo_info.data.adjust.amount_after; // Undo - call signature three - move lines } else { line1 = (undo_info.data.adjust.line1 + undo_info.data.adjust.amount); line2 = (undo_info.data.adjust.line2 + undo_info.data.adjust.amount); amount = -undo_info.data.adjust.amount; amount_after = -undo_info.data.adjust.amount_after; } // redo } else { line1 = undo_info.data.adjust.line1; line2 = undo_info.data.adjust.line2; amount = undo_info.data.adjust.amount; amount_after = undo_info.data.adjust.amount_after; } extmark_adjust(curbuf, line1, line2, amount, amount_after, kExtmarkNoUndo, false); // kExtmarkCopy } else if (undo_info.type == kExtmarkCopy) { // Redo should be handled by kColAdjustDelete or kExtmarkCopyPlace if (undo) { extmark_set(curbuf, undo_info.data.copy.ns_id, undo_info.data.copy.mark_id, undo_info.data.copy.lnum, undo_info.data.copy.col, kExtmarkNoUndo); } // uses extmark_copy_and_place } else if (undo_info.type == kExtmarkCopyPlace) { // Redo, undo is handle by kExtmarkCopy if (!undo) { extmark_copy_and_place(curbuf, undo_info.data.copy_place.l_lnum, undo_info.data.copy_place.l_col, undo_info.data.copy_place.u_lnum, undo_info.data.copy_place.u_col, undo_info.data.copy_place.p_lnum, undo_info.data.copy_place.p_col, kExtmarkNoUndo, true, NULL); } // kExtmarkClear } else if (undo_info.type == kExtmarkClear) { // Redo, undo is handle by kExtmarkCopy if (!undo) { extmark_clear(curbuf, undo_info.data.clear.ns_id, undo_info.data.clear.l_lnum, undo_info.data.clear.u_lnum, kExtmarkNoUndo); } // kAdjustMove } else if (undo_info.type == kAdjustMove) { apply_undo_move(undo_info, undo); // extmark_set } else if (undo_info.type == kExtmarkSet) { if (undo) { extmark_del(curbuf, undo_info.data.set.ns_id, undo_info.data.set.mark_id, kExtmarkNoUndo); // Redo } else { extmark_set(curbuf, undo_info.data.set.ns_id, undo_info.data.set.mark_id, undo_info.data.set.lnum, undo_info.data.set.col, kExtmarkNoUndo); } // extmark_set into update } else if (undo_info.type == kExtmarkUpdate) { if (undo) { extmark_set(curbuf, undo_info.data.update.ns_id, undo_info.data.update.mark_id, undo_info.data.update.old_lnum, undo_info.data.update.old_col, kExtmarkNoUndo); // Redo } else { extmark_set(curbuf, undo_info.data.update.ns_id, undo_info.data.update.mark_id, undo_info.data.update.lnum, undo_info.data.update.col, kExtmarkNoUndo); } // extmark_del } else if (undo_info.type == kExtmarkDel) { if (undo) { extmark_set(curbuf, undo_info.data.set.ns_id, undo_info.data.set.mark_id, undo_info.data.set.lnum, undo_info.data.set.col, kExtmarkNoUndo); // Redo } else { extmark_del(curbuf, undo_info.data.set.ns_id, undo_info.data.set.mark_id, kExtmarkNoUndo); } } } // undo/redo an kExtmarkMove operation static void apply_undo_move(ExtmarkUndoObject undo_info, bool undo) { // 3 calls are required , see comment in function do_move (ex_cmds.c) linenr_T line1 = undo_info.data.move.line1; linenr_T line2 = undo_info.data.move.line2; linenr_T last_line = undo_info.data.move.last_line; linenr_T dest = undo_info.data.move.dest; linenr_T num_lines = undo_info.data.move.num_lines; linenr_T extra = undo_info.data.move.extra; if (undo) { if (dest >= line2) { extmark_adjust(curbuf, dest - num_lines + 1, dest, last_line - dest + num_lines - 1, 0L, kExtmarkNoUndo, true); extmark_adjust(curbuf, dest - line2, dest - line1, dest - line2, 0L, kExtmarkNoUndo, false); } else { extmark_adjust(curbuf, line1-num_lines, line2-num_lines, last_line - (line1-num_lines), 0L, kExtmarkNoUndo, true); extmark_adjust(curbuf, (line1-num_lines) + 1, (line2-num_lines) + 1, -num_lines, 0L, kExtmarkNoUndo, false); } extmark_adjust(curbuf, last_line, last_line + num_lines - 1, line1 - last_line, 0L, kExtmarkNoUndo, true); // redo } else { extmark_adjust(curbuf, line1, line2, last_line - line2, 0L, kExtmarkNoUndo, true); if (dest >= line2) { extmark_adjust(curbuf, line2 + 1, dest, -num_lines, 0L, kExtmarkNoUndo, false); } else { extmark_adjust(curbuf, dest + 1, line1 - 1, num_lines, 0L, kExtmarkNoUndo, false); } extmark_adjust(curbuf, last_line - num_lines + 1, last_line, -(last_line - dest - extra), 0L, kExtmarkNoUndo, true); } } /// Get the column position for EOL on a line /// /// If the lnum doesn't exist, returns 0 colnr_T extmark_eol_col(buf_T *buf, linenr_T lnum) { if (lnum > buf->b_ml.ml_line_count) { return 0; } return (colnr_T)STRLEN(ml_get_buf(buf, lnum, false)) + 1; } // Adjust columns and rows for extmarks // based off mark_col_adjust in mark.c // returns true if something was moved otherwise false static bool extmark_col_adjust_impl(buf_T *buf, linenr_T lnum, colnr_T mincol, long lnum_amount, bool for_delete, long update_col) { bool marks_exist = false; ExtmarkLine *extmarkline = extmarkline_ref(buf, lnum, false); if (!extmarkline) { return false; } FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, mincol, MAXCOL, { marks_exist = true; // Calculate desired col amount where the adjustment should take place // (not taking) eol into account long col_amount; if (for_delete) { if (extmark->col < update_col) { // When mark inside range colnr_T start_effected_range = mincol - 1; col_amount = -(extmark->col - start_effected_range); } else { // Mark outside of range // -1 because a delete of width 0 should still move marks col_amount = -(update_col - mincol) - 1; } } else { // for anything other than deletes col_amount = update_col; } // No update required for this guy if (col_amount == 0 && lnum_amount == 0) { continue; } // Set mark to start of line if (col_amount < 0 && extmark->col <= (colnr_T)-col_amount) { extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id, extmarkline->lnum + lnum_amount, 1, kExtmarkNoUndo, &mitr); // Update the mark } else { // Note: The undo is handled by u_extmark_col_adjust, NoUndo here extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id, extmarkline->lnum + lnum_amount, extmark->col + (colnr_T)col_amount, kExtmarkNoUndo, &mitr); } }) if (kb_size(&extmarkline->items) == 0) { kb_del(extmarklines, &buf->b_extlines, extmarkline); extmarkline_free(extmarkline); } return marks_exist; } // Adjust columns and rows for extmarks // // based off mark_col_adjust in mark.c // use extmark_col_adjust_impl to move columns by inserting // Doesn't take the eol into consideration (possible to put marks in invalid // positions) void extmark_col_adjust(buf_T *buf, linenr_T lnum, colnr_T mincol, long lnum_amount, long col_amount, ExtmarkOp undo) { assert(col_amount > INT_MIN && col_amount <= INT_MAX); bool marks_moved = extmark_col_adjust_impl(buf, lnum, mincol, lnum_amount, false, col_amount); marks_moved |= bufhl_mark_col_adjust(buf, lnum, mincol, lnum_amount, col_amount); if (undo == kExtmarkUndo && marks_moved) { u_extmark_col_adjust(buf, lnum, mincol, lnum_amount, col_amount); } } // Adjust marks after a delete on a line // // Automatically readjusts to take the eol into account // TODO(timeyyy): change mincol to be for the mark to be copied, not moved // // @param mincol First column that needs to be moved (start of delete range) + 1 // @param endcol Last column which needs to be copied (end of delete range + 1) void extmark_col_adjust_delete(buf_T *buf, linenr_T lnum, colnr_T mincol, colnr_T endcol, ExtmarkOp undo, int _eol) { colnr_T start_effected_range = mincol; bool marks_moved; if (undo == kExtmarkUndo) { // Copy marks that would be effected by delete // -1 because we need to restore if a mark existed at the start pos u_extmark_copy(buf, 0, lnum, start_effected_range, lnum, endcol); } marks_moved = extmark_col_adjust_impl(buf, lnum, mincol, 0, true, (long)endcol); marks_moved |= bufhl_mark_col_adjust(buf, lnum, endcol, 0, mincol-(endcol+1)); // Deletes at the end of the line have different behaviour than the normal // case when deleted. // Cleanup any marks that are floating beyond the end of line. // we allow this to be passed in as well because the buffer may have already // been mutated. int eol = _eol; if (!eol) { eol = extmark_eol_col(buf, lnum); } FOR_ALL_EXTMARKS(buf, 1, lnum, eol, lnum, -1, { extmark_update(extmark, buf, extmark->ns_id, extmark->mark_id, extmarkline->lnum, (colnr_T)eol, kExtmarkNoUndo, &mitr); }) // Record the undo for the actual move if (marks_moved && undo == kExtmarkUndo) { u_extmark_col_adjust_delete(buf, lnum, mincol, endcol, eol); } } // 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, bool end_temp) { ExtmarkLine *_extline; // btree needs to be kept ordered to work, so far only :move requires this // 2nd call with end_temp = true unpack the lines from the temp position if (end_temp && amount < 0) { for (size_t i = 0; i < kv_size(buf->b_extmark_move_space); i++) { _extline = kv_A(buf->b_extmark_move_space, i); _extline->lnum += amount; kb_put(extmarklines, &buf->b_extlines, _extline); } kv_size(buf->b_extmark_move_space) = 0; return; } bool marks_exist = false; linenr_T *lp; linenr_T adj_start = line1; if (amount == MAXLNUM) { // Careful! marks from deleted region can end up on en extisting extmarkline // that is goinig to be adjusted to the target position. linenr_T join_num = line1 - amount_after; ExtmarkLine *joinline = (join_num > line2 ? extmarkline_ref(buf, join_num, false) : NULL); // extmark_adjust is already redoable, the copy should only be for undo marks_exist = extmark_copy_and_place(curbuf, line1, 1, line2, MAXCOL, line1, 1, kExtmarkUndoNoRedo, true, joinline); adj_start = line2+1; } FOR_ALL_EXTMARKLINES(buf, adj_start, MAXLNUM, { marks_exist = true; lp = &(extmarkline->lnum); if (*lp <= line2) { // 1st call with end_temp = true, store the lines in a temp position if (end_temp && amount > 0) { kb_del_itr_extmarklines(&buf->b_extlines, &itr); kv_push(buf->b_extmark_move_space, extmarkline); } *lp += amount; } else if (amount_after && *lp > line2) { *lp += amount_after; } }) if (undo == kExtmarkUndo && marks_exist) { u_extmark_adjust(buf, line1, line2, amount, amount_after); } } /// Range points to copy /// /// if part of a larger iteration we can't delete, then the caller /// must check for empty lines. bool extmark_copy_and_place(buf_T *buf, linenr_T l_lnum, colnr_T l_col, linenr_T u_lnum, colnr_T u_col, linenr_T p_lnum, colnr_T p_col, ExtmarkOp undo, bool delete, ExtmarkLine *destline) { bool marks_moved = false; if (undo == kExtmarkUndo || undo == kExtmarkUndoNoRedo) { // Copy marks that would be effected by delete u_extmark_copy(buf, 0, l_lnum, l_col, u_lnum, u_col); } // Move extmarks to their final position // Careful: if we move items within the same line, we might change order of // marks within the same extmarkline. Too keep it simple, first delete all // items from the extmarkline and put them back in the right order. FOR_ALL_EXTMARKLINES(buf, l_lnum, u_lnum, { kvec_t(Extmark) temp_space = KV_INITIAL_VALUE; bool same_line = extmarkline == destline; FOR_ALL_EXTMARKS_IN_LINE(extmarkline->items, (extmarkline->lnum > l_lnum) ? 0 : l_col, (extmarkline->lnum < u_lnum) ? MAXCOL : u_col, { if (!destline) { destline = extmarkline_ref(buf, p_lnum, true); same_line = extmarkline == destline; } marks_moved = true; if (!same_line) { extmark_put(p_col, extmark->mark_id, destline, extmark->ns_id); ExtmarkNs *ns_obj = pmap_get(uint64_t)(buf->b_extmark_ns, extmark->ns_id); pmap_put(uint64_t)(ns_obj->map, extmark->mark_id, destline); } else { kv_push(temp_space, *extmark); } // Delete old mark kb_del_itr(markitems, &extmarkline->items, &mitr); }) if (same_line) { for (size_t i = 0; i < kv_size(temp_space); i++) { Extmark mark = kv_A(temp_space, i); extmark_put(p_col, mark.mark_id, extmarkline, mark.ns_id); } kv_destroy(temp_space); } else if (delete && kb_size(&extmarkline->items) == 0) { kb_del_itr(extmarklines, &buf->b_extlines, &itr); extmarkline_free(extmarkline); } }) // Record the undo for the actual move if (marks_moved && undo == kExtmarkUndo) { u_extmark_copy_place(buf, l_lnum, l_col, u_lnum, u_col, p_lnum, p_col); } return marks_moved; } // Get reference to line in kbtree_t, allocating it if neccessary. ExtmarkLine *extmarkline_ref(buf_T *buf, linenr_T lnum, bool put) { kbtree_t(extmarklines) *b = &buf->b_extlines; ExtmarkLine t, **pp; t.lnum = lnum; pp = kb_get(extmarklines, b, &t); if (!pp) { if (!put) { return NULL; } ExtmarkLine *p = xcalloc(sizeof(ExtmarkLine), 1); p->lnum = lnum; // p->items zero initialized kb_put(extmarklines, b, p); return p; } // Return existing return *pp; } void extmarkline_free(ExtmarkLine *extmarkline) { kb_destroy(markitems, (&extmarkline->items)); xfree(extmarkline); } /// Put an extmark into a line, /// /// caller must ensure combination of id and ns_id isn't in use. void extmark_put(colnr_T col, uint64_t id, ExtmarkLine *extmarkline, uint64_t ns) { Extmark t; t.col = col; t.mark_id = id; t.line = extmarkline; t.ns_id = ns; kbtree_t(markitems) *b = &(extmarkline->items); // kb_put requries the key to not be there assert(!kb_getp(markitems, b, &t)); kb_put(markitems, b, t); }