From 48a869dc6d29514e943070da9f22f702f5179826 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Mon, 20 Jan 2020 19:29:12 +0100 Subject: shed biking: it's always extmarks, never marks extended --- src/nvim/api/buffer.c | 2 +- src/nvim/api/private/helpers.c | 2 +- src/nvim/api/vim.c | 2 +- src/nvim/buffer.c | 2 +- src/nvim/change.c | 2 +- src/nvim/edit.c | 2 +- src/nvim/ex_cmds.c | 2 +- src/nvim/extmark.c | 910 +++++++++++++++++ src/nvim/extmark.h | 93 ++ src/nvim/extmark_defs.h | 37 + src/nvim/fold.c | 2 +- src/nvim/indent.c | 2 +- src/nvim/map.h | 2 +- src/nvim/mark.c | 2 +- src/nvim/mark.h | 2 +- src/nvim/mark_extended.c | 910 ----------------- src/nvim/mark_extended.h | 93 -- src/nvim/mark_extended_defs.h | 37 - src/nvim/ops.c | 2 +- src/nvim/screen.c | 2 +- src/nvim/undo.c | 2 +- src/nvim/undo_defs.h | 2 +- test/functional/api/extmark_spec.lua | 1477 +++++++++++++++++++++++++++ test/functional/api/mark_extended_spec.lua | 1478 ---------------------------- 24 files changed, 2533 insertions(+), 2534 deletions(-) create mode 100644 src/nvim/extmark.c create mode 100644 src/nvim/extmark.h create mode 100644 src/nvim/extmark_defs.h delete mode 100644 src/nvim/mark_extended.c delete mode 100644 src/nvim/mark_extended.h delete mode 100644 src/nvim/mark_extended_defs.h create mode 100644 test/functional/api/extmark_spec.lua delete mode 100644 test/functional/api/mark_extended_spec.lua diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 3106011fe2..a666ed92da 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -26,7 +26,7 @@ #include "nvim/map_defs.h" #include "nvim/map.h" #include "nvim/mark.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/fileio.h" #include "nvim/move.h" #include "nvim/syntax.h" diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 37e31e0807..a1745ef777 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -24,7 +24,7 @@ #include "nvim/eval/typval.h" #include "nvim/map_defs.h" #include "nvim/map.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/option.h" #include "nvim/option_defs.h" #include "nvim/version.h" diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index baa0387fd8..9c58ce853b 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -40,7 +40,7 @@ #include "nvim/ops.h" #include "nvim/option.h" #include "nvim/state.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/syntax.h" #include "nvim/getchar.h" #include "nvim/os/input.h" diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 837fcb5cc1..eb71a8fc5e 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -53,7 +53,7 @@ #include "nvim/indent_c.h" #include "nvim/main.h" #include "nvim/mark.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" diff --git a/src/nvim/change.c b/src/nvim/change.c index 7eb6ea7328..a341b8fce1 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -17,7 +17,7 @@ #include "nvim/indent.h" #include "nvim/indent_c.h" #include "nvim/mark.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/memline.h" #include "nvim/misc1.h" #include "nvim/move.h" diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 0c183add16..68fa99484c 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -28,7 +28,7 @@ #include "nvim/indent.h" #include "nvim/indent_c.h" #include "nvim/main.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 53caaa6a67..bc6821f60f 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -40,7 +40,7 @@ #include "nvim/buffer_updates.h" #include "nvim/main.h" #include "nvim/mark.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/message.h" diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c new file mode 100644 index 0000000000..d60723c755 --- /dev/null +++ b/src/nvim/extmark.c @@ -0,0 +1,910 @@ +// 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_splice. Additionally mark_adjust +// might adjust extmarks to line inserts/deletes. +// +// Undo/Redo of marks is implemented by storing the call arguments to +// extmark_splice. 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/api/vim.h" +#include "nvim/vim.h" +#include "nvim/charset.h" +#include "nvim/extmark.h" +#include "nvim/buffer_updates.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" +#include "nvim/syntax.h" +#include "nvim/highlight.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) { + if (!buf->b_extmark_ns) { + if (!put) { + return NULL; + } + buf->b_extmark_ns = map_new(uint64_t, ExtmarkNs)(); + buf->b_extmark_index = map_new(uint64_t, ExtmarkItem)(); + } + + ExtmarkNs *ns = map_ref(uint64_t, ExtmarkNs)(buf->b_extmark_ns, ns_id, put); + if (put && ns->map == NULL) { + ns->map = map_new(uint64_t, uint64_t)(); + ns->free_id = 1; + } + return ns; +} + + +/// Create or update an extmark +/// +/// must not be used during iteration! +/// @returns the mark id +uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t id, + int row, colnr_T col, ExtmarkOp op) +{ + ExtmarkNs *ns = buf_ns_ref(buf, ns_id, true); + mtpos_t old_pos; + uint64_t mark = 0; + + 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) { + extmark_del(buf, ns_id, id); + } else { + // TODO(bfredl): we need to do more if "revising" a decoration mark. + MarkTreeIter itr[1]; + old_pos = marktree_lookup(buf->b_marktree, old_mark, itr); + assert(itr->node); + if (old_pos.row == row && old_pos.col == col) { + map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, old_mark); + mark = marktree_revise(buf->b_marktree, itr); + goto revised; + } + marktree_del_itr(buf->b_marktree, itr, false); + } + } else { + ns->free_id = MAX(ns->free_id, id+1); + } + } + + mark = marktree_put(buf->b_marktree, row, col, true); +revised: + map_put(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark, + (ExtmarkItem){ ns_id, id, 0, + KV_INITIAL_VALUE }); + 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); + } + return id; +} + +static bool extmark_setraw(buf_T *buf, uint64_t mark, int row, colnr_T col) +{ + MarkTreeIter itr[1]; + 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]; + 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); + + if (mark & MARKTREE_PAIRED_FLAG) { + mtpos_t 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.hl_id && pos2.row >= pos.row) { + redraw_buf_range_later(buf, pos.row+1, pos2.row+1); + } + } + + if (kv_size(item.virt_text)) { + redraw_buf_line_later(buf, pos.row+1); + } + clear_virttext(&item.virt_text); + + 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 (!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, uint64_t) *delete_set = NULL; + if (delete_set == NULL) { + delete_set = map_new(uint64_t, uint64_t)(); + } + + MarkTreeIter itr[1]; + 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; + } + uint64_t *del_status = map_ref(uint64_t, uint64_t)(delete_set, mark.id, + false); + if (del_status) { + marktree_del_itr(buf->b_marktree, itr, false); + map_del(uint64_t, uint64_t)(delete_set, mark.id); + if (*del_status > 0) { + redraw_buf_range_later(buf, (linenr_T)(*del_status), mark.row+1); + } + 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)) { + if (kv_size(item.virt_text)) { + redraw_buf_line_later(buf, mark.row+1); + } + clear_virttext(&item.virt_text); + marks_cleared = true; + if (mark.id & MARKTREE_PAIRED_FLAG) { + uint64_t other = mark.id ^ MARKTREE_END_FLAG; + uint64_t status = item.hl_id ? ((uint64_t)mark.row+1) : 0; + map_put(uint64_t, uint64_t)(delete_set, other, status); + } + 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, mark.id); + marktree_del_itr(buf->b_marktree, itr, false); + } else { + marktree_itr_next(buf->b_marktree, itr); + } + } + uint64_t id, status; + map_foreach(delete_set, id, status, { + mtpos_t pos = marktree_lookup(buf->b_marktree, id, itr); + assert(itr->node); + marktree_del_itr(buf->b_marktree, itr, false); + if (status > 0) { + redraw_buf_range_later(buf, (linenr_T)status, pos.row+1); + } + }); + map_clear(uint64_t, uint64_t)(delete_set); + 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 +ExtmarkArray 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) +{ + ExtmarkArray 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); + if (mark.row < 0 + || (mark.row - u_row) * order > 0 + || (mark.row == u_row && (mark.col - u_col) * order > 0)) { + break; + } + 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 })); + } + 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 }; + 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); + assert(pos.row >= 0); + + ret.ns_id = ns_id; + ret.mark_id = id; + ret.row = pos.row; + ret.col = pos.col; + + return ret; +} + + +// free extmarks from the buffer +void extmark_free_all(buf_T *buf) +{ + if (!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_free(uint64_t, uint64_t)(ns.map); + }); + map_free(uint64_t, ExtmarkNs)(buf->b_extmark_ns); + buf->b_extmark_ns = NULL; + + map_foreach(buf->b_extmark_index, id, item, { + (void)id; + clear_virttext(&item.virt_text); + }); + map_free(uint64_t, ExtmarkItem)(buf->b_extmark_index); + buf->b_extmark_index = NULL; +} + + +// 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]; + 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(curbuf, + splice.start_row, splice.start_col, + splice.newextent_row, splice.newextent_col, + splice.oldextent_row, splice.oldextent_col, + kExtmarkNoUndo); + + } else { + extmark_splice(curbuf, + splice.start_row, splice.start_col, + splice.oldextent_row, splice.oldextent_col, + splice.newextent_row, splice.newextent_col, + 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.extent_row, move.extent_col, + move.start_row, move.start_col, + kExtmarkNoUndo); + } else { + extmark_move_region(curbuf, + move.start_row, move.start_col, + move.extent_row, move.extent_col, + move.new_row, move.new_col, + 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) { + int old_extent, new_extent; + if (amount == MAXLNUM) { + old_extent = (int)(line2 - line1+1); + new_extent = (int)(amount_after + old_extent); + } 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_extent = 0; + new_extent = (int)amount; + } + extmark_splice(buf, + (int)line1-1, 0, + old_extent, 0, + new_extent, 0, undo); + } +} + +void extmark_splice(buf_T *buf, + int start_row, colnr_T start_col, + int oldextent_row, colnr_T oldextent_col, + int newextent_row, colnr_T newextent_col, + ExtmarkOp undo) +{ + buf_updates_send_splice(buf, start_row, start_col, + oldextent_row, oldextent_col, + newextent_row, newextent_col); + + if (undo == kExtmarkUndo && (oldextent_row > 0 || oldextent_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 + oldextent_row; + int end_col = (oldextent_row ? 0 : start_col) + oldextent_col; + u_extmark_copy(buf, start_row, start_col, end_row, end_col); + } + + + marktree_splice(buf->b_marktree, start_row, start_col, + oldextent_row, oldextent_col, + newextent_row, newextent_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 (oldextent_row == 0 && newextent_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->oldextent_row == 0 + && splice->newextent_row == 0) { + if (oldextent_col == 0 && start_col >= splice->start_col + && start_col <= splice->start_col+splice->newextent_col) { + splice->newextent_col += newextent_col; + merged = true; + } else if (newextent_col == 0 + && start_col == splice->start_col+splice->newextent_col) { + splice->oldextent_col += oldextent_col; + merged = true; + } else if (newextent_col == 0 + && start_col + oldextent_col == splice->start_col) { + splice->start_col = start_col; + splice->oldextent_col += oldextent_col; + merged = true; + } + } + } + } + + if (!merged) { + ExtmarkSplice splice; + splice.start_row = start_row; + splice.start_col = start_col; + splice.oldextent_row = oldextent_row; + splice.oldextent_col = oldextent_col; + splice.newextent_row = newextent_row; + splice.newextent_col = newextent_col; + + kv_push(uhp->uh_extmark, + ((ExtmarkUndoObject){ .type = kExtmarkSplice, + .data.splice = splice })); + } + } +} + + +void extmark_move_region(buf_T *buf, + int start_row, colnr_T start_col, + int extent_row, colnr_T extent_col, + int new_row, colnr_T new_col, + ExtmarkOp undo) +{ + // 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, + extent_row, extent_col, + 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, + 0, 0, + extent_row, extent_col); + + + 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.extent_row = extent_row; + move.extent_col = extent_col; + move.new_row = new_row; + move.new_col = new_col; + + kv_push(uhp->uh_extmark, + ((ExtmarkUndoObject){ .type = kExtmarkMove, + .data.move = move })); + } +} + +uint64_t src2ns(Integer *src_id) +{ + if (*src_id == 0) { + *src_id = (Integer)nvim_create_namespace((String)STRING_INIT); + } + if (*src_id < 0) { + return UINT64_MAX; + } else { + return (uint64_t)(*src_id); + } +} + +/// Adds a decoration to a buffer. +/// +/// Unlike matchaddpos() highlights, these follow changes to the the buffer +/// texts. Decorations are represented internally and in the API as extmarks. +/// +/// @param buf The buffer to add decorations to +/// @param ns_id A valid namespace id. +/// @param hl_id Id of the highlight group to use (or zero) +/// @param start_row The line to highlight +/// @param start_col First column to highlight +/// @param end_row The line to highlight +/// @param end_col The last column to highlight +/// @param virt_text Virtual text (currently placed at the EOL of start_row) +/// @return The extmark id inside the namespace +uint64_t extmark_add_decoration(buf_T *buf, uint64_t ns_id, int hl_id, + int start_row, colnr_T start_col, + int end_row, colnr_T end_col, + VirtText virt_text) +{ + ExtmarkNs *ns = buf_ns_ref(buf, ns_id, true); + ExtmarkItem item; + item.ns_id = ns_id; + item.mark_id = ns->free_id++; + item.hl_id = hl_id; + item.virt_text = virt_text; + + uint64_t mark; + + if (end_row > -1) { + mark = marktree_put_pair(buf->b_marktree, + start_row, start_col, true, + end_row, end_col, false); + } else { + mark = marktree_put(buf->b_marktree, start_row, start_col, true); + } + + map_put(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark, item); + map_put(uint64_t, uint64_t)(ns->map, item.mark_id, mark); + + redraw_buf_range_later(buf, start_row+1, + (end_row >= 0 ? end_row : start_row) + 1); + return item.mark_id; +} + +/// Add highlighting to a buffer, bounded by two cursor positions, +/// with an offset. +/// +/// @param buf Buffer to add highlights to +/// @param src_id src_id to use or 0 to use a new src_id group, +/// or -1 for ungrouped highlight. +/// @param hl_id Highlight group id +/// @param pos_start Cursor position to start the hightlighting at +/// @param pos_end Cursor position to end the highlighting at +/// @param offset Move the whole highlighting this many columns to the right +void bufhl_add_hl_pos_offset(buf_T *buf, + int src_id, + int hl_id, + lpos_T pos_start, + lpos_T pos_end, + colnr_T offset) +{ + colnr_T hl_start = 0; + colnr_T hl_end = 0; + + // TODO(bfredl): if decoration had blocky mode, we could avoid this loop + for (linenr_T lnum = pos_start.lnum; lnum <= pos_end.lnum; lnum ++) { + if (pos_start.lnum < lnum && lnum < pos_end.lnum) { + hl_start = offset-1; + hl_end = MAXCOL; + } else if (lnum == pos_start.lnum && lnum < pos_end.lnum) { + hl_start = pos_start.col + offset; + hl_end = MAXCOL; + } else if (pos_start.lnum < lnum && lnum == pos_end.lnum) { + hl_start = offset-1; + hl_end = pos_end.col + offset; + } else if (pos_start.lnum == lnum && pos_end.lnum == lnum) { + hl_start = pos_start.col + offset; + hl_end = pos_end.col + offset; + } + (void)extmark_add_decoration(buf, (uint64_t)src_id, hl_id, + (int)lnum-1, hl_start, (int)lnum-1, hl_end, + VIRTTEXT_EMPTY); + } +} + +void clear_virttext(VirtText *text) +{ + for (size_t i = 0; i < kv_size(*text); i++) { + xfree(kv_A(*text, i).text); + } + kv_destroy(*text); + *text = (VirtText)KV_INITIAL_VALUE; +} + +VirtText *extmark_find_virttext(buf_T *buf, int row, uint64_t ns_id) +{ + MarkTreeIter itr[1]; + marktree_itr_get(buf->b_marktree, row, 0, itr); + while (true) { + mtmark_t mark = marktree_itr_current(itr); + if (mark.row < 0 || mark.row > row) { + break; + } + ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, + mark.id, false); + if (item && (ns_id == 0 || ns_id == item->ns_id) + && kv_size(item->virt_text)) { + return &item->virt_text; + } + marktree_itr_next(buf->b_marktree, itr); + } + return NULL; +} + + +bool extmark_decorations_reset(buf_T *buf, DecorationState *state) +{ + state->row = -1; + return buf->b_extmark_index; +} + + +bool extmark_decorations_start(buf_T *buf, int top_row, DecorationState *state) +{ + kv_size(state->active) = 0; + state->top_row = top_row; + marktree_itr_get(buf->b_marktree, top_row, 0, state->itr); + if (!state->itr->node) { + return false; + } + marktree_itr_rewind(buf->b_marktree, state->itr); + while (true) { + mtmark_t mark = marktree_itr_current(state->itr); + if (mark.row < 0) { // || mark.row > end_row + break; + } + // TODO(bfredl): dedicated flag for being a decoration? + if ((mark.row < top_row && mark.id&MARKTREE_END_FLAG)) { + goto next_mark; + } + mtpos_t altpos = marktree_lookup(buf->b_marktree, + mark.id^MARKTREE_END_FLAG, NULL); + + uint64_t start_id = mark.id & ~MARKTREE_END_FLAG; + ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, + start_id, false); + if ((!(mark.id&MARKTREE_END_FLAG) && altpos.row < top_row + && !kv_size(item->virt_text)) + || ((mark.id&MARKTREE_END_FLAG) && altpos.row >= top_row)) { + goto next_mark; + } + + if (item && (item->hl_id > 0 || kv_size(item->virt_text))) { + int attr_id = item->hl_id > 0 ? syn_id2attr(item->hl_id) : 0; + VirtText *vt = kv_size(item->virt_text) ? &item->virt_text : NULL; + HlRange range; + if (mark.id&MARKTREE_END_FLAG) { + range = (HlRange){ altpos.row, altpos.col, mark.row, mark.col, + attr_id, vt }; + } else { + range = (HlRange){ mark.row, mark.col, altpos.row, + altpos.col, attr_id, vt }; + } + kv_push(state->active, range); + } +next_mark: + if (marktree_itr_node_done(state->itr)) { + break; + } + marktree_itr_next(buf->b_marktree, state->itr); + } + + return true; // TODO(bfredl): check if available in the region +} + +bool extmark_decorations_line(buf_T *buf, int row, DecorationState *state) +{ + if (state->row == -1) { + extmark_decorations_start(buf, row, state); + } + state->row = row; + state->col_until = -1; + return true; // TODO(bfredl): be more precise +} + +int extmark_decorations_col(buf_T *buf, int col, DecorationState *state) +{ + if (col <= state->col_until) { + return state->current; + } + state->col_until = MAXCOL; + while (true) { + mtmark_t mark = marktree_itr_current(state->itr); + if (mark.row < 0 || mark.row > state->row) { + break; + } else if (mark.row == state->row && mark.col > col) { + state->col_until = mark.col-1; + break; + } + + if ((mark.id&MARKTREE_END_FLAG)) { + // TODO(bfredl): check decorations flag + goto next_mark; + } + mtpos_t endpos = marktree_lookup(buf->b_marktree, + mark.id|MARKTREE_END_FLAG, NULL); + + ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, + mark.id, false); + + if (endpos.row < mark.row + || (endpos.row == mark.row && endpos.col <= mark.col)) { + if (!kv_size(item->virt_text)) { + goto next_mark; + } + } + + if (item && (item->hl_id > 0 || kv_size(item->virt_text))) { + int attr_id = item->hl_id > 0 ? syn_id2attr(item->hl_id) : 0; + VirtText *vt = kv_size(item->virt_text) ? &item->virt_text : NULL; + kv_push(state->active, ((HlRange){ mark.row, mark.col, + endpos.row, endpos.col, + attr_id, vt })); + } + +next_mark: + marktree_itr_next(buf->b_marktree, state->itr); + } + + int attr = 0; + size_t j = 0; + for (size_t i = 0; i < kv_size(state->active); i++) { + HlRange item = kv_A(state->active, i); + bool active = false, keep = true; + if (item.end_row < state->row + || (item.end_row == state->row && item.end_col <= col)) { + if (!(item.start_row >= state->row && item.virt_text)) { + keep = false; + } + } else { + if (item.start_row < state->row + || (item.start_row == state->row && item.start_col <= col)) { + active = true; + if (item.end_row == state->row) { + state->col_until = MIN(state->col_until, item.end_col-1); + } + } else { + if (item.start_row == state->row) { + state->col_until = MIN(state->col_until, item.start_col-1); + } + } + } + if (active && item.attr_id > 0) { + attr = hl_combine_attr(attr, item.attr_id); + } + if (keep) { + kv_A(state->active, j++) = kv_A(state->active, i); + } + } + kv_size(state->active) = j; + state->current = attr; + return attr; +} + +VirtText *extmark_decorations_virt_text(buf_T *buf, DecorationState *state) +{ + extmark_decorations_col(buf, MAXCOL, state); + for (size_t i = 0; i < kv_size(state->active); i++) { + HlRange item = kv_A(state->active, i); + if (item.start_row == state->row && item.virt_text) { + return item.virt_text; + } + } + return NULL; +} diff --git a/src/nvim/extmark.h b/src/nvim/extmark.h new file mode 100644 index 0000000000..829cbe0236 --- /dev/null +++ b/src/nvim/extmark.h @@ -0,0 +1,93 @@ +#ifndef NVIM_EXTMARK_H +#define NVIM_EXTMARK_H + +#include "nvim/buffer_defs.h" +#include "nvim/extmark_defs.h" +#include "nvim/marktree.h" + +EXTERN int extmark_splice_pending INIT(= 0); + +typedef struct +{ + uint64_t ns_id; + uint64_t mark_id; + int row; + colnr_T col; +} ExtmarkInfo; + +typedef kvec_t(ExtmarkInfo) ExtmarkArray; + + +// delete the columns between mincol and endcol +typedef struct { + int start_row; + colnr_T start_col; + int oldextent_row; + colnr_T oldextent_col; + int newextent_row; + colnr_T newextent_col; +} ExtmarkSplice; + +// adjust marks after :move operation +typedef struct { + int start_row; + int start_col; + int extent_row; + int extent_col; + int new_row; + int new_col; +} ExtmarkMove; + +// extmark was updated +typedef struct { + uint64_t mark; // raw mark id of the marktree + int old_row; + colnr_T old_col; + int row; + colnr_T col; +} ExtmarkSavePos; + +typedef enum { + kExtmarkSplice, + kExtmarkMove, + kExtmarkUpdate, + kExtmarkSavePos, + kExtmarkClear, +} UndoObjectType; + +// TODO(bfredl): reduce the number of undo action types +struct undo_object { + UndoObjectType type; + union { + ExtmarkSplice splice; + ExtmarkMove move; + ExtmarkSavePos savepos; + } data; +}; + + +typedef struct { + int start_row; + int start_col; + int end_row; + int end_col; + int attr_id; + VirtText *virt_text; +} HlRange; + +typedef struct { + MarkTreeIter itr[1]; + kvec_t(HlRange) active; + int top_row; + int row; + int col_until; + int current; + VirtText *virt_text; +} DecorationState; + + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "extmark.h.generated.h" +#endif + +#endif // NVIM_EXTMARK_H diff --git a/src/nvim/extmark_defs.h b/src/nvim/extmark_defs.h new file mode 100644 index 0000000000..c927048981 --- /dev/null +++ b/src/nvim/extmark_defs.h @@ -0,0 +1,37 @@ +#ifndef NVIM_EXTMARK_DEFS_H +#define NVIM_EXTMARK_DEFS_H + +#include "nvim/pos.h" // for colnr_T +#include "nvim/lib/kvec.h" + +typedef struct { + char *text; + int hl_id; +} VirtTextChunk; + +typedef kvec_t(VirtTextChunk) VirtText; +#define VIRTTEXT_EMPTY ((VirtText)KV_INITIAL_VALUE) + +typedef struct +{ + uint64_t ns_id; + uint64_t mark_id; + int hl_id; // highlight group + // TODO(bfredl): virt_text is pretty larger than the rest, + // pointer indirection? + VirtText virt_text; +} ExtmarkItem; + +typedef struct undo_object ExtmarkUndoObject; +typedef kvec_t(ExtmarkUndoObject) extmark_undo_vec_t; + +// Undo/redo extmarks + +typedef enum { + kExtmarkNOOP, // Extmarks shouldn't be moved + kExtmarkUndo, // Operation should be reversable/undoable + kExtmarkNoUndo, // Operation should not be reversable + kExtmarkUndoNoRedo, // Operation should be undoable, but not redoable +} ExtmarkOp; + +#endif // NVIM_EXTMARK_DEFS_H diff --git a/src/nvim/fold.c b/src/nvim/fold.c index addfab8f08..0b14a6affb 100644 --- a/src/nvim/fold.c +++ b/src/nvim/fold.c @@ -22,7 +22,7 @@ #include "nvim/func_attr.h" #include "nvim/indent.h" #include "nvim/buffer_updates.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/mark.h" #include "nvim/memline.h" #include "nvim/memory.h" diff --git a/src/nvim/indent.c b/src/nvim/indent.c index 2c5fdd8ea6..f8018c039d 100644 --- a/src/nvim/indent.c +++ b/src/nvim/indent.c @@ -13,7 +13,7 @@ #include "nvim/charset.h" #include "nvim/cursor.h" #include "nvim/mark.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/memline.h" #include "nvim/memory.h" #include "nvim/misc1.h" diff --git a/src/nvim/map.h b/src/nvim/map.h index 761938776d..0ad7865bf0 100644 --- a/src/nvim/map.h +++ b/src/nvim/map.h @@ -4,7 +4,7 @@ #include #include "nvim/map_defs.h" -#include "nvim/mark_extended_defs.h" +#include "nvim/extmark_defs.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/dispatch.h" #include "nvim/highlight_defs.h" diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 4a7452493a..fa7c7d61c9 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -20,7 +20,7 @@ #include "nvim/ex_cmds.h" #include "nvim/fileio.h" #include "nvim/fold.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" diff --git a/src/nvim/mark.h b/src/nvim/mark.h index d8370c367a..b3d9b5d95a 100644 --- a/src/nvim/mark.h +++ b/src/nvim/mark.h @@ -6,7 +6,7 @@ #include "nvim/buffer_defs.h" #include "nvim/func_attr.h" #include "nvim/mark_defs.h" -#include "nvim/mark_extended_defs.h" +#include "nvim/extmark_defs.h" #include "nvim/memory.h" #include "nvim/pos.h" #include "nvim/os/time.h" diff --git a/src/nvim/mark_extended.c b/src/nvim/mark_extended.c deleted file mode 100644 index b60d847676..0000000000 --- a/src/nvim/mark_extended.c +++ /dev/null @@ -1,910 +0,0 @@ -// 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_splice. Additionally mark_adjust -// might adjust extmarks to line inserts/deletes. -// -// Undo/Redo of marks is implemented by storing the call arguments to -// extmark_splice. 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/api/vim.h" -#include "nvim/vim.h" -#include "nvim/charset.h" -#include "nvim/mark_extended.h" -#include "nvim/buffer_updates.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" -#include "nvim/syntax.h" -#include "nvim/highlight.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "mark_extended.c.generated.h" -#endif - -static ExtmarkNs *buf_ns_ref(buf_T *buf, uint64_t ns_id, bool put) { - if (!buf->b_extmark_ns) { - if (!put) { - return NULL; - } - buf->b_extmark_ns = map_new(uint64_t, ExtmarkNs)(); - buf->b_extmark_index = map_new(uint64_t, ExtmarkItem)(); - } - - ExtmarkNs *ns = map_ref(uint64_t, ExtmarkNs)(buf->b_extmark_ns, ns_id, put); - if (put && ns->map == NULL) { - ns->map = map_new(uint64_t, uint64_t)(); - ns->free_id = 1; - } - return ns; -} - - -/// Create or update an extmark -/// -/// must not be used during iteration! -/// @returns the mark id -uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t id, - int row, colnr_T col, ExtmarkOp op) -{ - ExtmarkNs *ns = buf_ns_ref(buf, ns_id, true); - mtpos_t old_pos; - uint64_t mark = 0; - - 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) { - extmark_del(buf, ns_id, id); - } else { - // TODO(bfredl): we need to do more if "revising" a decoration mark. - MarkTreeIter itr[1]; - old_pos = marktree_lookup(buf->b_marktree, old_mark, itr); - assert(itr->node); - if (old_pos.row == row && old_pos.col == col) { - map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, old_mark); - mark = marktree_revise(buf->b_marktree, itr); - goto revised; - } - marktree_del_itr(buf->b_marktree, itr, false); - } - } else { - ns->free_id = MAX(ns->free_id, id+1); - } - } - - mark = marktree_put(buf->b_marktree, row, col, true); -revised: - map_put(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark, - (ExtmarkItem){ ns_id, id, 0, - KV_INITIAL_VALUE }); - 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); - } - return id; -} - -static bool extmark_setraw(buf_T *buf, uint64_t mark, int row, colnr_T col) -{ - MarkTreeIter itr[1]; - 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]; - 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); - - if (mark & MARKTREE_PAIRED_FLAG) { - mtpos_t 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.hl_id && pos2.row >= pos.row) { - redraw_buf_range_later(buf, pos.row+1, pos2.row+1); - } - } - - if (kv_size(item.virt_text)) { - redraw_buf_line_later(buf, pos.row+1); - } - clear_virttext(&item.virt_text); - - 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 (!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, uint64_t) *delete_set = NULL; - if (delete_set == NULL) { - delete_set = map_new(uint64_t, uint64_t)(); - } - - MarkTreeIter itr[1]; - 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; - } - uint64_t *del_status = map_ref(uint64_t, uint64_t)(delete_set, mark.id, - false); - if (del_status) { - marktree_del_itr(buf->b_marktree, itr, false); - map_del(uint64_t, uint64_t)(delete_set, mark.id); - if (*del_status > 0) { - redraw_buf_range_later(buf, (linenr_T)(*del_status), mark.row+1); - } - 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)) { - if (kv_size(item.virt_text)) { - redraw_buf_line_later(buf, mark.row+1); - } - clear_virttext(&item.virt_text); - marks_cleared = true; - if (mark.id & MARKTREE_PAIRED_FLAG) { - uint64_t other = mark.id ^ MARKTREE_END_FLAG; - uint64_t status = item.hl_id ? ((uint64_t)mark.row+1) : 0; - map_put(uint64_t, uint64_t)(delete_set, other, status); - } - 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, mark.id); - marktree_del_itr(buf->b_marktree, itr, false); - } else { - marktree_itr_next(buf->b_marktree, itr); - } - } - uint64_t id, status; - map_foreach(delete_set, id, status, { - mtpos_t pos = marktree_lookup(buf->b_marktree, id, itr); - assert(itr->node); - marktree_del_itr(buf->b_marktree, itr, false); - if (status > 0) { - redraw_buf_range_later(buf, (linenr_T)status, pos.row+1); - } - }); - map_clear(uint64_t, uint64_t)(delete_set); - 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 -ExtmarkArray 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) -{ - ExtmarkArray 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); - if (mark.row < 0 - || (mark.row - u_row) * order > 0 - || (mark.row == u_row && (mark.col - u_col) * order > 0)) { - break; - } - 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 })); - } - 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 }; - 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); - assert(pos.row >= 0); - - ret.ns_id = ns_id; - ret.mark_id = id; - ret.row = pos.row; - ret.col = pos.col; - - return ret; -} - - -// free extmarks from the buffer -void extmark_free_all(buf_T *buf) -{ - if (!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_free(uint64_t, uint64_t)(ns.map); - }); - map_free(uint64_t, ExtmarkNs)(buf->b_extmark_ns); - buf->b_extmark_ns = NULL; - - map_foreach(buf->b_extmark_index, id, item, { - (void)id; - clear_virttext(&item.virt_text); - }); - map_free(uint64_t, ExtmarkItem)(buf->b_extmark_index); - buf->b_extmark_index = NULL; -} - - -// 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]; - 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(curbuf, - splice.start_row, splice.start_col, - splice.newextent_row, splice.newextent_col, - splice.oldextent_row, splice.oldextent_col, - kExtmarkNoUndo); - - } else { - extmark_splice(curbuf, - splice.start_row, splice.start_col, - splice.oldextent_row, splice.oldextent_col, - splice.newextent_row, splice.newextent_col, - 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.extent_row, move.extent_col, - move.start_row, move.start_col, - kExtmarkNoUndo); - } else { - extmark_move_region(curbuf, - move.start_row, move.start_col, - move.extent_row, move.extent_col, - move.new_row, move.new_col, - 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) { - int old_extent, new_extent; - if (amount == MAXLNUM) { - old_extent = (int)(line2 - line1+1); - new_extent = (int)(amount_after + old_extent); - } 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_extent = 0; - new_extent = (int)amount; - } - extmark_splice(buf, - (int)line1-1, 0, - old_extent, 0, - new_extent, 0, undo); - } -} - -void extmark_splice(buf_T *buf, - int start_row, colnr_T start_col, - int oldextent_row, colnr_T oldextent_col, - int newextent_row, colnr_T newextent_col, - ExtmarkOp undo) -{ - buf_updates_send_splice(buf, start_row, start_col, - oldextent_row, oldextent_col, - newextent_row, newextent_col); - - if (undo == kExtmarkUndo && (oldextent_row > 0 || oldextent_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 + oldextent_row; - int end_col = (oldextent_row ? 0 : start_col) + oldextent_col; - u_extmark_copy(buf, start_row, start_col, end_row, end_col); - } - - - marktree_splice(buf->b_marktree, start_row, start_col, - oldextent_row, oldextent_col, - newextent_row, newextent_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 (oldextent_row == 0 && newextent_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->oldextent_row == 0 - && splice->newextent_row == 0) { - if (oldextent_col == 0 && start_col >= splice->start_col - && start_col <= splice->start_col+splice->newextent_col) { - splice->newextent_col += newextent_col; - merged = true; - } else if (newextent_col == 0 - && start_col == splice->start_col+splice->newextent_col) { - splice->oldextent_col += oldextent_col; - merged = true; - } else if (newextent_col == 0 - && start_col + oldextent_col == splice->start_col) { - splice->start_col = start_col; - splice->oldextent_col += oldextent_col; - merged = true; - } - } - } - } - - if (!merged) { - ExtmarkSplice splice; - splice.start_row = start_row; - splice.start_col = start_col; - splice.oldextent_row = oldextent_row; - splice.oldextent_col = oldextent_col; - splice.newextent_row = newextent_row; - splice.newextent_col = newextent_col; - - kv_push(uhp->uh_extmark, - ((ExtmarkUndoObject){ .type = kExtmarkSplice, - .data.splice = splice })); - } - } -} - - -void extmark_move_region(buf_T *buf, - int start_row, colnr_T start_col, - int extent_row, colnr_T extent_col, - int new_row, colnr_T new_col, - ExtmarkOp undo) -{ - // 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, - extent_row, extent_col, - 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, - 0, 0, - extent_row, extent_col); - - - 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.extent_row = extent_row; - move.extent_col = extent_col; - move.new_row = new_row; - move.new_col = new_col; - - kv_push(uhp->uh_extmark, - ((ExtmarkUndoObject){ .type = kExtmarkMove, - .data.move = move })); - } -} - -uint64_t src2ns(Integer *src_id) -{ - if (*src_id == 0) { - *src_id = (Integer)nvim_create_namespace((String)STRING_INIT); - } - if (*src_id < 0) { - return UINT64_MAX; - } else { - return (uint64_t)(*src_id); - } -} - -/// Adds a decoration to a buffer. -/// -/// Unlike matchaddpos() highlights, these follow changes to the the buffer -/// texts. Decorations are represented internally and in the API as extmarks. -/// -/// @param buf The buffer to add decorations to -/// @param ns_id A valid namespace id. -/// @param hl_id Id of the highlight group to use (or zero) -/// @param start_row The line to highlight -/// @param start_col First column to highlight -/// @param end_row The line to highlight -/// @param end_col The last column to highlight -/// @param virt_text Virtual text (currently placed at the EOL of start_row) -/// @return The extmark id inside the namespace -uint64_t extmark_add_decoration(buf_T *buf, uint64_t ns_id, int hl_id, - int start_row, colnr_T start_col, - int end_row, colnr_T end_col, - VirtText virt_text) -{ - ExtmarkNs *ns = buf_ns_ref(buf, ns_id, true); - ExtmarkItem item; - item.ns_id = ns_id; - item.mark_id = ns->free_id++; - item.hl_id = hl_id; - item.virt_text = virt_text; - - uint64_t mark; - - if (end_row > -1) { - mark = marktree_put_pair(buf->b_marktree, - start_row, start_col, true, - end_row, end_col, false); - } else { - mark = marktree_put(buf->b_marktree, start_row, start_col, true); - } - - map_put(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark, item); - map_put(uint64_t, uint64_t)(ns->map, item.mark_id, mark); - - redraw_buf_range_later(buf, start_row+1, - (end_row >= 0 ? end_row : start_row) + 1); - return item.mark_id; -} - -/// Add highlighting to a buffer, bounded by two cursor positions, -/// with an offset. -/// -/// @param buf Buffer to add highlights to -/// @param src_id src_id to use or 0 to use a new src_id group, -/// or -1 for ungrouped highlight. -/// @param hl_id Highlight group id -/// @param pos_start Cursor position to start the hightlighting at -/// @param pos_end Cursor position to end the highlighting at -/// @param offset Move the whole highlighting this many columns to the right -void bufhl_add_hl_pos_offset(buf_T *buf, - int src_id, - int hl_id, - lpos_T pos_start, - lpos_T pos_end, - colnr_T offset) -{ - colnr_T hl_start = 0; - colnr_T hl_end = 0; - - // TODO(bfredl): if decoration had blocky mode, we could avoid this loop - for (linenr_T lnum = pos_start.lnum; lnum <= pos_end.lnum; lnum ++) { - if (pos_start.lnum < lnum && lnum < pos_end.lnum) { - hl_start = offset-1; - hl_end = MAXCOL; - } else if (lnum == pos_start.lnum && lnum < pos_end.lnum) { - hl_start = pos_start.col + offset; - hl_end = MAXCOL; - } else if (pos_start.lnum < lnum && lnum == pos_end.lnum) { - hl_start = offset-1; - hl_end = pos_end.col + offset; - } else if (pos_start.lnum == lnum && pos_end.lnum == lnum) { - hl_start = pos_start.col + offset; - hl_end = pos_end.col + offset; - } - (void)extmark_add_decoration(buf, (uint64_t)src_id, hl_id, - (int)lnum-1, hl_start, (int)lnum-1, hl_end, - VIRTTEXT_EMPTY); - } -} - -void clear_virttext(VirtText *text) -{ - for (size_t i = 0; i < kv_size(*text); i++) { - xfree(kv_A(*text, i).text); - } - kv_destroy(*text); - *text = (VirtText)KV_INITIAL_VALUE; -} - -VirtText *extmark_find_virttext(buf_T *buf, int row, uint64_t ns_id) -{ - MarkTreeIter itr[1]; - marktree_itr_get(buf->b_marktree, row, 0, itr); - while (true) { - mtmark_t mark = marktree_itr_current(itr); - if (mark.row < 0 || mark.row > row) { - break; - } - ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, - mark.id, false); - if (item && (ns_id == 0 || ns_id == item->ns_id) - && kv_size(item->virt_text)) { - return &item->virt_text; - } - marktree_itr_next(buf->b_marktree, itr); - } - return NULL; -} - - -bool extmark_decorations_reset(buf_T *buf, DecorationState *state) -{ - state->row = -1; - return buf->b_extmark_index; -} - - -bool extmark_decorations_start(buf_T *buf, int top_row, DecorationState *state) -{ - kv_size(state->active) = 0; - state->top_row = top_row; - marktree_itr_get(buf->b_marktree, top_row, 0, state->itr); - if (!state->itr->node) { - return false; - } - marktree_itr_rewind(buf->b_marktree, state->itr); - while (true) { - mtmark_t mark = marktree_itr_current(state->itr); - if (mark.row < 0) { // || mark.row > end_row - break; - } - // TODO(bfredl): dedicated flag for being a decoration? - if ((mark.row < top_row && mark.id&MARKTREE_END_FLAG)) { - goto next_mark; - } - mtpos_t altpos = marktree_lookup(buf->b_marktree, - mark.id^MARKTREE_END_FLAG, NULL); - - uint64_t start_id = mark.id & ~MARKTREE_END_FLAG; - ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, - start_id, false); - if ((!(mark.id&MARKTREE_END_FLAG) && altpos.row < top_row - && !kv_size(item->virt_text)) - || ((mark.id&MARKTREE_END_FLAG) && altpos.row >= top_row)) { - goto next_mark; - } - - if (item && (item->hl_id > 0 || kv_size(item->virt_text))) { - int attr_id = item->hl_id > 0 ? syn_id2attr(item->hl_id) : 0; - VirtText *vt = kv_size(item->virt_text) ? &item->virt_text : NULL; - HlRange range; - if (mark.id&MARKTREE_END_FLAG) { - range = (HlRange){ altpos.row, altpos.col, mark.row, mark.col, - attr_id, vt }; - } else { - range = (HlRange){ mark.row, mark.col, altpos.row, - altpos.col, attr_id, vt }; - } - kv_push(state->active, range); - } -next_mark: - if (marktree_itr_node_done(state->itr)) { - break; - } - marktree_itr_next(buf->b_marktree, state->itr); - } - - return true; // TODO(bfredl): check if available in the region -} - -bool extmark_decorations_line(buf_T *buf, int row, DecorationState *state) -{ - if (state->row == -1) { - extmark_decorations_start(buf, row, state); - } - state->row = row; - state->col_until = -1; - return true; // TODO(bfredl): be more precise -} - -int extmark_decorations_col(buf_T *buf, int col, DecorationState *state) -{ - if (col <= state->col_until) { - return state->current; - } - state->col_until = MAXCOL; - while (true) { - mtmark_t mark = marktree_itr_current(state->itr); - if (mark.row < 0 || mark.row > state->row) { - break; - } else if (mark.row == state->row && mark.col > col) { - state->col_until = mark.col-1; - break; - } - - if ((mark.id&MARKTREE_END_FLAG)) { - // TODO(bfredl): check decorations flag - goto next_mark; - } - mtpos_t endpos = marktree_lookup(buf->b_marktree, - mark.id|MARKTREE_END_FLAG, NULL); - - ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, - mark.id, false); - - if (endpos.row < mark.row - || (endpos.row == mark.row && endpos.col <= mark.col)) { - if (!kv_size(item->virt_text)) { - goto next_mark; - } - } - - if (item && (item->hl_id > 0 || kv_size(item->virt_text))) { - int attr_id = item->hl_id > 0 ? syn_id2attr(item->hl_id) : 0; - VirtText *vt = kv_size(item->virt_text) ? &item->virt_text : NULL; - kv_push(state->active, ((HlRange){ mark.row, mark.col, - endpos.row, endpos.col, - attr_id, vt })); - } - -next_mark: - marktree_itr_next(buf->b_marktree, state->itr); - } - - int attr = 0; - size_t j = 0; - for (size_t i = 0; i < kv_size(state->active); i++) { - HlRange item = kv_A(state->active, i); - bool active = false, keep = true; - if (item.end_row < state->row - || (item.end_row == state->row && item.end_col <= col)) { - if (!(item.start_row >= state->row && item.virt_text)) { - keep = false; - } - } else { - if (item.start_row < state->row - || (item.start_row == state->row && item.start_col <= col)) { - active = true; - if (item.end_row == state->row) { - state->col_until = MIN(state->col_until, item.end_col-1); - } - } else { - if (item.start_row == state->row) { - state->col_until = MIN(state->col_until, item.start_col-1); - } - } - } - if (active && item.attr_id > 0) { - attr = hl_combine_attr(attr, item.attr_id); - } - if (keep) { - kv_A(state->active, j++) = kv_A(state->active, i); - } - } - kv_size(state->active) = j; - state->current = attr; - return attr; -} - -VirtText *extmark_decorations_virt_text(buf_T *buf, DecorationState *state) -{ - extmark_decorations_col(buf, MAXCOL, state); - for (size_t i = 0; i < kv_size(state->active); i++) { - HlRange item = kv_A(state->active, i); - if (item.start_row == state->row && item.virt_text) { - return item.virt_text; - } - } - return NULL; -} diff --git a/src/nvim/mark_extended.h b/src/nvim/mark_extended.h deleted file mode 100644 index f809148d9b..0000000000 --- a/src/nvim/mark_extended.h +++ /dev/null @@ -1,93 +0,0 @@ -#ifndef NVIM_MARK_EXTENDED_H -#define NVIM_MARK_EXTENDED_H - -#include "nvim/buffer_defs.h" -#include "nvim/mark_extended_defs.h" -#include "nvim/marktree.h" - -EXTERN int extmark_splice_pending INIT(= 0); - -typedef struct -{ - uint64_t ns_id; - uint64_t mark_id; - int row; - colnr_T col; -} ExtmarkInfo; - -typedef kvec_t(ExtmarkInfo) ExtmarkArray; - - -// delete the columns between mincol and endcol -typedef struct { - int start_row; - colnr_T start_col; - int oldextent_row; - colnr_T oldextent_col; - int newextent_row; - colnr_T newextent_col; -} ExtmarkSplice; - -// adjust marks after :move operation -typedef struct { - int start_row; - int start_col; - int extent_row; - int extent_col; - int new_row; - int new_col; -} ExtmarkMove; - -// extmark was updated -typedef struct { - uint64_t mark; // raw mark id of the marktree - int old_row; - colnr_T old_col; - int row; - colnr_T col; -} ExtmarkSavePos; - -typedef enum { - kExtmarkSplice, - kExtmarkMove, - kExtmarkUpdate, - kExtmarkSavePos, - kExtmarkClear, -} UndoObjectType; - -// TODO(bfredl): reduce the number of undo action types -struct undo_object { - UndoObjectType type; - union { - ExtmarkSplice splice; - ExtmarkMove move; - ExtmarkSavePos savepos; - } data; -}; - - -typedef struct { - int start_row; - int start_col; - int end_row; - int end_col; - int attr_id; - VirtText *virt_text; -} HlRange; - -typedef struct { - MarkTreeIter itr[1]; - kvec_t(HlRange) active; - int top_row; - int row; - int col_until; - int current; - VirtText *virt_text; -} DecorationState; - - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "mark_extended.h.generated.h" -#endif - -#endif // NVIM_MARK_EXTENDED_H diff --git a/src/nvim/mark_extended_defs.h b/src/nvim/mark_extended_defs.h deleted file mode 100644 index 439f7f0b36..0000000000 --- a/src/nvim/mark_extended_defs.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef NVIM_MARK_EXTENDED_DEFS_H -#define NVIM_MARK_EXTENDED_DEFS_H - -#include "nvim/pos.h" // for colnr_T -#include "nvim/lib/kvec.h" - -typedef struct { - char *text; - int hl_id; -} VirtTextChunk; - -typedef kvec_t(VirtTextChunk) VirtText; -#define VIRTTEXT_EMPTY ((VirtText)KV_INITIAL_VALUE) - -typedef struct -{ - uint64_t ns_id; - uint64_t mark_id; - int hl_id; // highlight group - // TODO(bfredl): virt_text is pretty larger than the rest, - // pointer indirection? - VirtText virt_text; -} ExtmarkItem; - -typedef struct undo_object ExtmarkUndoObject; -typedef kvec_t(ExtmarkUndoObject) extmark_undo_vec_t; - -// Undo/redo extmarks - -typedef enum { - kExtmarkNOOP, // Extmarks shouldn't be moved - kExtmarkUndo, // Operation should be reversable/undoable - kExtmarkNoUndo, // Operation should not be reversable - kExtmarkUndoNoRedo, // Operation should be undoable, but not redoable -} ExtmarkOp; - -#endif // NVIM_MARK_EXTENDED_DEFS_H diff --git a/src/nvim/ops.c b/src/nvim/ops.c index da2b81fd0a..641323ae5e 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -31,7 +31,7 @@ #include "nvim/indent.h" #include "nvim/log.h" #include "nvim/mark.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" diff --git a/src/nvim/screen.c b/src/nvim/screen.c index cb155cfc65..a451451726 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -87,7 +87,7 @@ #include "nvim/highlight.h" #include "nvim/main.h" #include "nvim/mark.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" diff --git a/src/nvim/undo.c b/src/nvim/undo.c index fda647106d..1f74bada41 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -93,7 +93,7 @@ #include "nvim/buffer_updates.h" #include "nvim/pos.h" // MAXLNUM #include "nvim/mark.h" -#include "nvim/mark_extended.h" +#include "nvim/extmark.h" #include "nvim/memline.h" #include "nvim/message.h" #include "nvim/misc1.h" diff --git a/src/nvim/undo_defs.h b/src/nvim/undo_defs.h index 0fa3b415ec..cc2c39a711 100644 --- a/src/nvim/undo_defs.h +++ b/src/nvim/undo_defs.h @@ -4,7 +4,7 @@ #include // for time_t #include "nvim/pos.h" -#include "nvim/mark_extended_defs.h" +#include "nvim/extmark_defs.h" #include "nvim/mark_defs.h" typedef struct u_header u_header_T; diff --git a/test/functional/api/extmark_spec.lua b/test/functional/api/extmark_spec.lua new file mode 100644 index 0000000000..5bf3fa554f --- /dev/null +++ b/test/functional/api/extmark_spec.lua @@ -0,0 +1,1477 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') + +local request = helpers.request +local eq = helpers.eq +local ok = helpers.ok +local curbufmeths = helpers.curbufmeths +local bufmeths = helpers.bufmeths +local pcall_err = helpers.pcall_err +local insert = helpers.insert +local feed = helpers.feed +local clear = helpers.clear +local command = helpers.command +local meths = helpers.meths + +local function expect(contents) + return eq(contents, helpers.curbuf_contents()) +end + +local function check_undo_redo(ns, mark, sr, sc, er, ec) --s = start, e = end + local rv = curbufmeths.get_extmark_by_id(ns, mark) + eq({er, ec}, rv) + feed("u") + rv = curbufmeths.get_extmark_by_id(ns, mark) + eq({sr, sc}, rv) + feed("") + rv = curbufmeths.get_extmark_by_id(ns, mark) + eq({er, ec}, rv) +end + +local function set_extmark(ns_id, id, line, col, opts) + if opts == nil then + opts = {} + end + return curbufmeths.set_extmark(ns_id, id, line, col, opts) +end + +local function get_extmarks(ns_id, start, end_, opts) + if opts == nil then + opts = {} + end + return curbufmeths.get_extmarks(ns_id, start, end_, opts) +end + +local function batch_set(ns_id, positions) + local ids = {} + for _, pos in ipairs(positions) do + table.insert(ids, set_extmark(ns_id, 0, pos[1], pos[2])) + end + return ids +end + +local function batch_check(ns_id, ids, positions) + local actual, expected = {}, {} + for i,id in ipairs(ids) do + expected[id] = positions[i] + end + for _, mark in pairs(get_extmarks(ns_id, 0, -1, {})) do + actual[mark[1]] = {mark[2], mark[3]} + end + eq(expected, actual) +end + +local function batch_check_undo_redo(ns_id, ids, before, after) + batch_check(ns_id, ids, after) + feed("u") + batch_check(ns_id, ids, before) + feed("") + batch_check(ns_id, ids, after) +end + +describe('API/extmarks', function() + local screen + local marks, positions, init_text, row, col + local ns, ns2 + + before_each(function() + -- Initialize some namespaces and insert 12345 into a buffer + marks = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + positions = {{0, 0,}, {0, 2}, {0, 3}} + + init_text = "12345" + row = 0 + col = 2 + + clear() + + insert(init_text) + ns = request('nvim_create_namespace', "my-fancy-plugin") + ns2 = request('nvim_create_namespace', "my-fancy-plugin2") + end) + + it('adds, updates and deletes marks', function() + local rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2]) + eq(marks[1], rv) + rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({positions[1][1], positions[1][2]}, rv) + -- Test adding a second mark on same row works + rv = set_extmark(ns, marks[2], positions[2][1], positions[2][2]) + eq(marks[2], rv) + + -- Test an update, (same pos) + rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2]) + eq(marks[1], rv) + rv = curbufmeths.get_extmark_by_id(ns, marks[2]) + eq({positions[2][1], positions[2][2]}, rv) + -- Test an update, (new pos) + row = positions[1][1] + col = positions[1][2] + 1 + rv = set_extmark(ns, marks[1], row, col) + eq(marks[1], rv) + rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({row, col}, rv) + + -- remove the test marks + eq(true, curbufmeths.del_extmark(ns, marks[1])) + eq(false, curbufmeths.del_extmark(ns, marks[1])) + eq(true, curbufmeths.del_extmark(ns, marks[2])) + eq(false, curbufmeths.del_extmark(ns, marks[3])) + eq(false, curbufmeths.del_extmark(ns, 1000)) + end) + + it('can clear a specific namespace range', function() + set_extmark(ns, 1, 0, 1) + set_extmark(ns2, 1, 0, 1) + -- force a new undo buffer + feed('o') + curbufmeths.clear_namespace(ns2, 0, -1) + eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1})) + eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) + feed('u') + eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1})) + eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) + feed('') + eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1})) + eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) + end) + + it('can clear a namespace range using 0,-1', function() + set_extmark(ns, 1, 0, 1) + set_extmark(ns2, 1, 0, 1) + -- force a new undo buffer + feed('o') + curbufmeths.clear_namespace(-1, 0, -1) + eq({}, get_extmarks(ns, {0, 0}, {-1, -1})) + eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) + feed('u') + eq({}, get_extmarks(ns, {0, 0}, {-1, -1})) + eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) + feed('') + eq({}, get_extmarks(ns, {0, 0}, {-1, -1})) + eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) + end) + + it('querying for information and ranges', function() + --marks = {1, 2, 3} + --positions = {{0, 0,}, {0, 2}, {0, 3}} + -- add some more marks + for i, m in ipairs(marks) do + if positions[i] ~= nil then + local rv = set_extmark(ns, m, positions[i][1], positions[i][2]) + eq(m, rv) + end + end + + -- {0, 0} and {-1, -1} work as extreme values + eq({{1, 0, 0}}, get_extmarks(ns, {0, 0}, {0, 0})) + eq({}, get_extmarks(ns, {-1, -1}, {-1, -1})) + local rv = get_extmarks(ns, {0, 0}, {-1, -1}) + for i, m in ipairs(marks) do + if positions[i] ~= nil then + eq({m, positions[i][1], positions[i][2]}, rv[i]) + end + end + + -- 0 and -1 works as short hand extreme values + eq({{1, 0, 0}}, get_extmarks(ns, 0, 0)) + eq({}, get_extmarks(ns, -1, -1)) + rv = get_extmarks(ns, 0, -1) + for i, m in ipairs(marks) do + if positions[i] ~= nil then + eq({m, positions[i][1], positions[i][2]}, rv[i]) + end + end + + -- next with mark id + rv = get_extmarks(ns, marks[1], {-1, -1}, {limit=1}) + eq({{marks[1], positions[1][1], positions[1][2]}}, rv) + rv = get_extmarks(ns, marks[2], {-1, -1}, {limit=1}) + eq({{marks[2], positions[2][1], positions[2][2]}}, rv) + -- next with positional when mark exists at position + rv = get_extmarks(ns, positions[1], {-1, -1}, {limit=1}) + eq({{marks[1], positions[1][1], positions[1][2]}}, rv) + -- next with positional index (no mark at position) + rv = get_extmarks(ns, {positions[1][1], positions[1][2] +1}, {-1, -1}, {limit=1}) + eq({{marks[2], positions[2][1], positions[2][2]}}, rv) + -- next with Extremity index + rv = get_extmarks(ns, {0,0}, {-1, -1}, {limit=1}) + eq({{marks[1], positions[1][1], positions[1][2]}}, rv) + + -- nextrange with mark id + rv = get_extmarks(ns, marks[1], marks[3]) + eq({marks[1], positions[1][1], positions[1][2]}, rv[1]) + eq({marks[2], positions[2][1], positions[2][2]}, rv[2]) + -- nextrange with `limit` + rv = get_extmarks(ns, marks[1], marks[3], {limit=2}) + eq(2, table.getn(rv)) + -- nextrange with positional when mark exists at position + rv = get_extmarks(ns, positions[1], positions[3]) + eq({marks[1], positions[1][1], positions[1][2]}, rv[1]) + eq({marks[2], positions[2][1], positions[2][2]}, rv[2]) + rv = get_extmarks(ns, positions[2], positions[3]) + eq(2, table.getn(rv)) + -- nextrange with positional index (no mark at position) + local lower = {positions[1][1], positions[2][2] -1} + local upper = {positions[2][1], positions[3][2] - 1} + rv = get_extmarks(ns, lower, upper) + eq({{marks[2], positions[2][1], positions[2][2]}}, rv) + lower = {positions[3][1], positions[3][2] + 1} + upper = {positions[3][1], positions[3][2] + 2} + rv = get_extmarks(ns, lower, upper) + eq({}, rv) + -- nextrange with extremity index + lower = {positions[2][1], positions[2][2]+1} + upper = {-1, -1} + rv = get_extmarks(ns, lower, upper) + eq({{marks[3], positions[3][1], positions[3][2]}}, rv) + + -- prev with mark id + rv = get_extmarks(ns, marks[3], {0, 0}, {limit=1}) + eq({{marks[3], positions[3][1], positions[3][2]}}, rv) + rv = get_extmarks(ns, marks[2], {0, 0}, {limit=1}) + eq({{marks[2], positions[2][1], positions[2][2]}}, rv) + -- prev with positional when mark exists at position + rv = get_extmarks(ns, positions[3], {0, 0}, {limit=1}) + eq({{marks[3], positions[3][1], positions[3][2]}}, rv) + -- prev with positional index (no mark at position) + rv = get_extmarks(ns, {positions[1][1], positions[1][2] +1}, {0, 0}, {limit=1}) + eq({{marks[1], positions[1][1], positions[1][2]}}, rv) + -- prev with Extremity index + rv = get_extmarks(ns, {-1,-1}, {0,0}, {limit=1}) + eq({{marks[3], positions[3][1], positions[3][2]}}, rv) + + -- prevrange with mark id + rv = get_extmarks(ns, marks[3], marks[1]) + eq({marks[3], positions[3][1], positions[3][2]}, rv[1]) + eq({marks[2], positions[2][1], positions[2][2]}, rv[2]) + eq({marks[1], positions[1][1], positions[1][2]}, rv[3]) + -- prevrange with limit + rv = get_extmarks(ns, marks[3], marks[1], {limit=2}) + eq(2, table.getn(rv)) + -- prevrange with positional when mark exists at position + rv = get_extmarks(ns, positions[3], positions[1]) + eq({{marks[3], positions[3][1], positions[3][2]}, + {marks[2], positions[2][1], positions[2][2]}, + {marks[1], positions[1][1], positions[1][2]}}, rv) + rv = get_extmarks(ns, positions[2], positions[1]) + eq(2, table.getn(rv)) + -- prevrange with positional index (no mark at position) + lower = {positions[2][1], positions[2][2] + 1} + upper = {positions[3][1], positions[3][2] + 1} + rv = get_extmarks(ns, upper, lower) + eq({{marks[3], positions[3][1], positions[3][2]}}, rv) + lower = {positions[3][1], positions[3][2] + 1} + upper = {positions[3][1], positions[3][2] + 2} + rv = get_extmarks(ns, upper, lower) + eq({}, rv) + -- prevrange with extremity index + lower = {0,0} + upper = {positions[2][1], positions[2][2] - 1} + rv = get_extmarks(ns, upper, lower) + eq({{marks[1], positions[1][1], positions[1][2]}}, rv) + end) + + it('querying for information with limit', function() + -- add some more marks + for i, m in ipairs(marks) do + if positions[i] ~= nil then + local rv = set_extmark(ns, m, positions[i][1], positions[i][2]) + eq(m, rv) + end + end + + local rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=1}) + eq(1, table.getn(rv)) + rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=2}) + eq(2, table.getn(rv)) + rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=3}) + eq(3, table.getn(rv)) + + -- now in reverse + rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=1}) + eq(1, table.getn(rv)) + rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=2}) + eq(2, table.getn(rv)) + rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=3}) + eq(3, table.getn(rv)) + end) + + it('get_marks works when mark col > upper col', function() + feed('A12345') + feed('A12345') + set_extmark(ns, 10, 0, 2) -- this shouldn't be found + set_extmark(ns, 11, 2, 1) -- this shouldn't be found + set_extmark(ns, marks[1], 0, 4) -- check col > our upper bound + set_extmark(ns, marks[2], 1, 1) -- check col < lower bound + set_extmark(ns, marks[3], 2, 0) -- check is inclusive + eq({{marks[1], 0, 4}, + {marks[2], 1, 1}, + {marks[3], 2, 0}}, + get_extmarks(ns, {0, 3}, {2, 0})) + end) + + it('get_marks works in reverse when mark col < lower col', function() + feed('A12345') + feed('A12345') + set_extmark(ns, 10, 0, 1) -- this shouldn't be found + set_extmark(ns, 11, 2, 4) -- this shouldn't be found + set_extmark(ns, marks[1], 2, 1) -- check col < our lower bound + set_extmark(ns, marks[2], 1, 4) -- check col > upper bound + set_extmark(ns, marks[3], 0, 2) -- check is inclusive + local rv = get_extmarks(ns, {2, 3}, {0, 2}) + eq({{marks[1], 2, 1}, + {marks[2], 1, 4}, + {marks[3], 0, 2}}, + rv) + end) + + it('get_marks limit=0 returns nothing', function() + set_extmark(ns, marks[1], positions[1][1], positions[1][2]) + local rv = get_extmarks(ns, {-1, -1}, {-1, -1}, {limit=0}) + eq({}, rv) + end) + + + it('marks move with line insertations', function() + set_extmark(ns, marks[1], 0, 0) + feed("yyP") + check_undo_redo(ns, marks[1], 0, 0, 1, 0) + end) + + it('marks move with multiline insertations', function() + feed("a2233") + set_extmark(ns, marks[1], 1, 1) + feed('ggVGyP') + check_undo_redo(ns, marks[1], 1, 1, 4, 1) + end) + + it('marks move with line join', function() + -- do_join in ops.c + feed("a222") + set_extmark(ns, marks[1], 1, 0) + feed('ggJ') + check_undo_redo(ns, marks[1], 1, 0, 0, 6) + end) + + it('join works when no marks are present', function() + screen = Screen.new(15, 10) + screen:attach() + feed("a1") + feed('kJ') + -- This shouldn't seg fault + screen:expect([[ + 12345^ 1 | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + | + ]]) + end) + + it('marks move with multiline join', function() + -- do_join in ops.c + feed("a222333444") + set_extmark(ns, marks[1], 3, 0) + feed('2GVGJ') + check_undo_redo(ns, marks[1], 3, 0, 1, 8) + end) + + it('marks move with line deletes', function() + feed("a222333444") + set_extmark(ns, marks[1], 2, 1) + feed('ggjdd') + check_undo_redo(ns, marks[1], 2, 1, 1, 1) + end) + + it('marks move with multiline deletes', function() + feed("a222333444") + set_extmark(ns, marks[1], 3, 0) + feed('gg2dd') + check_undo_redo(ns, marks[1], 3, 0, 1, 0) + -- regression test, undoing multiline delete when mark is on row 1 + feed('ugg3dd') + check_undo_redo(ns, marks[1], 3, 0, 0, 0) + end) + + it('marks move with open line', function() + -- open_line in misc1.c + -- testing marks below are also moved + feed("yyP") + set_extmark(ns, marks[1], 0, 4) + set_extmark(ns, marks[2], 1, 4) + feed('1G') + check_undo_redo(ns, marks[1], 0, 4, 1, 4) + check_undo_redo(ns, marks[2], 1, 4, 2, 4) + feed('2Go') + check_undo_redo(ns, marks[1], 1, 4, 1, 4) + check_undo_redo(ns, marks[2], 2, 4, 3, 4) + end) + + it('marks move with char inserts', function() + -- insertchar in edit.c (the ins_str branch) + screen = Screen.new(15, 10) + screen:attach() + set_extmark(ns, marks[1], 0, 3) + feed('0') + insert('abc') + screen:expect([[ + ab^c12345 | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + | + ]]) + local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({0, 6}, rv) + check_undo_redo(ns, marks[1], 0, 3, 0, 6) + end) + + -- gravity right as definted in tk library + it('marks have gravity right', function() + -- insertchar in edit.c (the ins_str branch) + set_extmark(ns, marks[1], 0, 2) + feed('03l') + insert("X") + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + + -- check multibyte chars + feed('03l') + insert("~~") + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + end) + + it('we can insert multibyte chars', function() + -- insertchar in edit.c + feed('a12345') + set_extmark(ns, marks[1], 1, 2) + -- Insert a fullwidth (two col) tilde, NICE + feed('0i~') + check_undo_redo(ns, marks[1], 1, 2, 1, 5) + end) + + it('marks move with blockwise inserts', function() + -- op_insert in ops.c + feed('a12345') + set_extmark(ns, marks[1], 1, 2) + feed('0lkI9') + check_undo_redo(ns, marks[1], 1, 2, 1, 3) + end) + + it('marks move with line splits (using enter)', function() + -- open_line in misc1.c + -- testing marks below are also moved + feed("yyP") + set_extmark(ns, marks[1], 0, 4) + set_extmark(ns, marks[2], 1, 4) + feed('1Gla') + check_undo_redo(ns, marks[1], 0, 4, 1, 2) + check_undo_redo(ns, marks[2], 1, 4, 2, 4) + end) + + it('marks at last line move on insert new line', function() + -- open_line in misc1.c + set_extmark(ns, marks[1], 0, 4) + feed('0i') + check_undo_redo(ns, marks[1], 0, 4, 1, 4) + end) + + it('yet again marks move with line splits', function() + -- the first test above wasn't catching all errors.. + feed("A67890") + set_extmark(ns, marks[1], 0, 4) + feed("04li") + check_undo_redo(ns, marks[1], 0, 4, 1, 0) + end) + + it('and one last time line splits...', function() + set_extmark(ns, marks[1], 0, 1) + set_extmark(ns, marks[2], 0, 2) + feed("02li") + check_undo_redo(ns, marks[1], 0, 1, 0, 1) + check_undo_redo(ns, marks[2], 0, 2, 1, 0) + end) + + it('multiple marks move with mark splits', function() + set_extmark(ns, marks[1], 0, 1) + set_extmark(ns, marks[2], 0, 3) + feed("0li") + check_undo_redo(ns, marks[1], 0, 1, 1, 0) + check_undo_redo(ns, marks[2], 0, 3, 1, 2) + end) + + it('deleting right before a mark works', function() + -- op_delete in ops.c + set_extmark(ns, marks[1], 0, 2) + feed('0lx') + check_undo_redo(ns, marks[1], 0, 2, 0, 1) + end) + + it('deleting right after a mark works', function() + -- op_delete in ops.c + set_extmark(ns, marks[1], 0, 2) + feed('02lx') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + end) + + it('marks move with char deletes', function() + -- op_delete in ops.c + set_extmark(ns, marks[1], 0, 2) + feed('02dl') + check_undo_redo(ns, marks[1], 0, 2, 0, 0) + -- from the other side (nothing should happen) + feed('$x') + check_undo_redo(ns, marks[1], 0, 0, 0, 0) + end) + + it('marks move with char deletes over a range', function() + -- op_delete in ops.c + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + feed('0l3dl') + check_undo_redo(ns, marks[1], 0, 2, 0, 1) + check_undo_redo(ns, marks[2], 0, 3, 0, 1) + -- delete 1, nothing should happen to our marks + feed('u') + feed('$x') + check_undo_redo(ns, marks[2], 0, 3, 0, 3) + end) + + it('deleting marks at end of line works', function() + set_extmark(ns, marks[1], 0, 4) + feed('$x') + check_undo_redo(ns, marks[1], 0, 4, 0, 4) + -- check the copy happened correctly on delete at eol + feed('$x') + check_undo_redo(ns, marks[1], 0, 4, 0, 3) + feed('u') + check_undo_redo(ns, marks[1], 0, 4, 0, 4) + end) + + it('marks move with blockwise deletes', function() + -- op_delete in ops.c + feed('a12345') + set_extmark(ns, marks[1], 1, 4) + feed('hhhkd') + check_undo_redo(ns, marks[1], 1, 4, 1, 1) + end) + + it('marks move with blockwise deletes over a range', function() + -- op_delete in ops.c + feed('a12345') + set_extmark(ns, marks[1], 0, 1) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 1, 2) + feed('0k3lx') + check_undo_redo(ns, marks[1], 0, 1, 0, 0) + check_undo_redo(ns, marks[2], 0, 3, 0, 0) + check_undo_redo(ns, marks[3], 1, 2, 1, 0) + -- delete 1, nothing should happen to our marks + feed('u') + feed('$jx') + check_undo_redo(ns, marks[2], 0, 3, 0, 3) + check_undo_redo(ns, marks[3], 1, 2, 1, 2) + end) + + it('works with char deletes over multilines', function() + feed('a12345test-me') + set_extmark(ns, marks[1], 2, 5) + feed('gg') + feed('dv?-m?') + check_undo_redo(ns, marks[1], 2, 5, 0, 0) + end) + + it('marks outside of deleted range move with visual char deletes', function() + -- op_delete in ops.c + set_extmark(ns, marks[1], 0, 3) + feed('0vx') + check_undo_redo(ns, marks[1], 0, 3, 0, 2) + + feed("u") + feed('0vlx') + check_undo_redo(ns, marks[1], 0, 3, 0, 1) + + feed("u") + feed('0v2lx') + check_undo_redo(ns, marks[1], 0, 3, 0, 0) + + -- from the other side (nothing should happen) + feed('$vx') + check_undo_redo(ns, marks[1], 0, 0, 0, 0) + end) + + it('marks outside of deleted range move with char deletes', function() + -- op_delete in ops.c + set_extmark(ns, marks[1], 0, 3) + feed('0x') + check_undo_redo(ns, marks[1], 0, 3, 0, 2) + + feed("u") + feed('02x') + check_undo_redo(ns, marks[1], 0, 3, 0, 1) + + feed("u") + feed('0v3lx') + check_undo_redo(ns, marks[1], 0, 3, 0, 0) + + -- from the other side (nothing should happen) + feed("u") + feed('$vx') + check_undo_redo(ns, marks[1], 0, 3, 0, 3) + end) + + it('marks move with P(backward) paste', function() + -- do_put in ops.c + feed('0iabc') + set_extmark(ns, marks[1], 0, 7) + feed('0veyP') + check_undo_redo(ns, marks[1], 0, 7, 0, 15) + end) + + it('marks move with p(forward) paste', function() + -- do_put in ops.c + feed('0iabc') + set_extmark(ns, marks[1], 0, 7) + feed('0veyp') + check_undo_redo(ns, marks[1], 0, 7, 0, 15) + end) + + it('marks move with blockwise P(backward) paste', function() + -- do_put in ops.c + feed('a12345') + set_extmark(ns, marks[1], 1, 4) + feed('hhkyP') + check_undo_redo(ns, marks[1], 1, 4, 1, 7) + end) + + it('marks move with blockwise p(forward) paste', function() + -- do_put in ops.c + feed('a12345') + set_extmark(ns, marks[1], 1, 4) + feed('hhkyp') + check_undo_redo(ns, marks[1], 1, 4, 1, 7) + end) + + describe('multiline regions', function() + before_each(function() + feed('dd') + -- Achtung: code has been spiced with some unicode, + -- to make life more interesting. + -- luacheck whines about TABs inside strings for whatever reason. + -- luacheck: push ignore 621 + insert([[ + static int nlua_rpcrequest(lua_State *lstate) + { + Ïf (!nlua_is_deferred_safe(lstate)) { + // strictly not allowed + Яetörn luaL_error(lstate, e_luv_api_disabled, "rpcrequest"); + } + return nlua_rpc(lstate, true); + }]]) + -- luacheck: pop + end) + + it('delete', function() + local pos1 = { + {2, 4}, {2, 12}, {2, 13}, {2, 14}, {2, 25}, + {4, 8}, {4, 10}, {4, 20}, + {5, 3}, {6, 10} + } + local ids = batch_set(ns, pos1) + batch_check(ns, ids, pos1) + feed('3Gfiv2+ftd') + batch_check_undo_redo(ns, ids, pos1, { + {2, 4}, {2, 12}, {2, 13}, {2, 13}, {2, 13}, + {2, 13}, {2, 15}, {2, 25}, + {3, 3}, {4, 10} + }) + end) + + -- TODO(bfredl): add more tests! + end) + + it('replace works', function() + set_extmark(ns, marks[1], 0, 2) + feed('0r2') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + end) + + it('blockwise replace works', function() + feed('a12345') + set_extmark(ns, marks[1], 0, 2) + feed('0llkr1') + check_undo_redo(ns, marks[1], 0, 2, 0, 3) + end) + + it('shift line', function() + -- shift_line in ops.c + feed(':set shiftwidth=4') + set_extmark(ns, marks[1], 0, 2) + feed('0>>') + check_undo_redo(ns, marks[1], 0, 2, 0, 6) + expect(' 12345') + + feed('>>') + -- this is counter-intuitive. But what happens + -- is that 4 spaces gets extended to one tab (== 8 spaces) + check_undo_redo(ns, marks[1], 0, 6, 0, 3) + expect('\t12345') + + feed('') -- have to escape, same as << + check_undo_redo(ns, marks[1], 0, 3, 0, 6) + end) + + it('blockwise shift', function() + -- shift_block in ops.c + feed(':set shiftwidth=4') + feed('a12345') + set_extmark(ns, marks[1], 1, 2) + feed('0k>') + check_undo_redo(ns, marks[1], 1, 2, 1, 6) + feed('j>') + expect('\t12345\n\t12345') + check_undo_redo(ns, marks[1], 1, 6, 1, 3) + + feed('j') + check_undo_redo(ns, marks[1], 1, 3, 1, 6) + end) + + it('tab works with expandtab', function() + -- ins_tab in edit.c + feed(':set expandtab') + feed(':set shiftwidth=2') + set_extmark(ns, marks[1], 0, 2) + feed('0i') + check_undo_redo(ns, marks[1], 0, 2, 0, 6) + end) + + it('tabs work', function() + -- ins_tab in edit.c + feed(':set noexpandtab') + feed(':set shiftwidth=2') + feed(':set softtabstop=2') + feed(':set tabstop=8') + set_extmark(ns, marks[1], 0, 2) + feed('0i') + check_undo_redo(ns, marks[1], 0, 2, 0, 4) + feed('0iX') + check_undo_redo(ns, marks[1], 0, 4, 0, 6) + end) + + it('marks move when using :move', function() + set_extmark(ns, marks[1], 0, 0) + feed('A2:1move 2') + check_undo_redo(ns, marks[1], 0, 0, 1, 0) + -- test codepath when moving lines up + feed(':2move 0') + check_undo_redo(ns, marks[1], 1, 0, 0, 0) + end) + + it('marks move when using :move part 2', function() + -- make sure we didn't get lucky with the math... + feed('A23456') + set_extmark(ns, marks[1], 1, 0) + feed(':2,3move 5') + check_undo_redo(ns, marks[1], 1, 0, 3, 0) + -- test codepath when moving lines up + feed(':4,5move 1') + check_undo_redo(ns, marks[1], 3, 0, 1, 0) + end) + + it('undo and redo of set and unset marks', function() + -- Force a new undo head + feed('o') + set_extmark(ns, marks[1], 0, 1) + feed('o') + set_extmark(ns, marks[2], 0, -1) + set_extmark(ns, marks[3], 0, -1) + + feed("u") + local rv = get_extmarks(ns, {0, 0}, {-1, -1}) + eq(3, table.getn(rv)) + + feed("") + rv = get_extmarks(ns, {0, 0}, {-1, -1}) + eq(3, table.getn(rv)) + + -- Test updates + feed('o') + set_extmark(ns, marks[1], positions[1][1], positions[1][2]) + rv = get_extmarks(ns, marks[1], marks[1], {limit=1}) + eq(1, table.getn(rv)) + feed("u") + feed("") + -- old value is NOT kept in history + check_undo_redo(ns, marks[1], positions[1][1], positions[1][2], positions[1][1], positions[1][2]) + + -- Test unset + feed('o') + curbufmeths.del_extmark(ns, marks[3]) + feed("u") + rv = get_extmarks(ns, {0, 0}, {-1, -1}) + -- undo does NOT restore deleted marks + eq(2, table.getn(rv)) + feed("") + rv = get_extmarks(ns, {0, 0}, {-1, -1}) + eq(2, table.getn(rv)) + end) + + it('undo and redo of marks deleted during edits', function() + -- test extmark_adjust + feed('A12345') + set_extmark(ns, marks[1], 1, 2) + feed('dd') + check_undo_redo(ns, marks[1], 1, 2, 1, 0) + end) + + it('namespaces work properly', function() + local rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2]) + eq(1, rv) + rv = set_extmark(ns2, marks[1], positions[1][1], positions[1][2]) + eq(1, rv) + rv = get_extmarks(ns, {0, 0}, {-1, -1}) + eq(1, table.getn(rv)) + rv = get_extmarks(ns2, {0, 0}, {-1, -1}) + eq(1, table.getn(rv)) + + -- Set more marks for testing the ranges + set_extmark(ns, marks[2], positions[2][1], positions[2][2]) + set_extmark(ns, marks[3], positions[3][1], positions[3][2]) + set_extmark(ns2, marks[2], positions[2][1], positions[2][2]) + set_extmark(ns2, marks[3], positions[3][1], positions[3][2]) + + -- get_next (limit set) + rv = get_extmarks(ns, {0, 0}, positions[2], {limit=1}) + eq(1, table.getn(rv)) + rv = get_extmarks(ns2, {0, 0}, positions[2], {limit=1}) + eq(1, table.getn(rv)) + -- get_prev (limit set) + rv = get_extmarks(ns, positions[1], {0, 0}, {limit=1}) + eq(1, table.getn(rv)) + rv = get_extmarks(ns2, positions[1], {0, 0}, {limit=1}) + eq(1, table.getn(rv)) + + -- get_next (no limit) + rv = get_extmarks(ns, positions[1], positions[2]) + eq(2, table.getn(rv)) + rv = get_extmarks(ns2, positions[1], positions[2]) + eq(2, table.getn(rv)) + -- get_prev (no limit) + rv = get_extmarks(ns, positions[2], positions[1]) + eq(2, table.getn(rv)) + rv = get_extmarks(ns2, positions[2], positions[1]) + eq(2, table.getn(rv)) + + curbufmeths.del_extmark(ns, marks[1]) + rv = get_extmarks(ns, {0, 0}, {-1, -1}) + eq(2, table.getn(rv)) + curbufmeths.del_extmark(ns2, marks[1]) + rv = get_extmarks(ns2, {0, 0}, {-1, -1}) + eq(2, table.getn(rv)) + end) + + it('mark set can create unique identifiers', function() + -- create mark with id 1 + eq(1, set_extmark(ns, 1, positions[1][1], positions[1][2])) + -- ask for unique id, it should be the next one, i e 2 + eq(2, set_extmark(ns, 0, positions[1][1], positions[1][2])) + eq(3, set_extmark(ns, 3, positions[2][1], positions[2][2])) + eq(4, set_extmark(ns, 0, positions[1][1], positions[1][2])) + + -- mixing manual and allocated id:s are not recommened, but it should + -- do something reasonable + eq(6, set_extmark(ns, 6, positions[2][1], positions[2][2])) + eq(7, set_extmark(ns, 0, positions[1][1], positions[1][2])) + eq(8, set_extmark(ns, 0, positions[1][1], positions[1][2])) + end) + + it('auto indenting with enter works', function() + -- op_reindent in ops.c + feed(':set cindent') + feed(':set autoindent') + feed(':set shiftwidth=2') + feed("0iint A {1M1b") + -- Set the mark on the M, should move.. + set_extmark(ns, marks[1], 0, 12) + -- Set the mark before the cursor, should stay there + set_extmark(ns, marks[2], 0, 10) + feed("i") + local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({1, 3}, rv) + rv = curbufmeths.get_extmark_by_id(ns, marks[2]) + eq({0, 10}, rv) + check_undo_redo(ns, marks[1], 0, 12, 1, 3) + end) + + it('auto indenting entire line works', function() + feed(':set cindent') + feed(':set autoindent') + feed(':set shiftwidth=2') + -- will force an indent of 2 + feed("0iint A {0i1M1") + set_extmark(ns, marks[1], 1, 1) + feed("0i") + local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({1, 3}, rv) + check_undo_redo(ns, marks[1], 1, 1, 1, 3) + -- now check when cursor at eol + feed("uA") + rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({1, 3}, rv) + end) + + it('removing auto indenting with works', function() + feed(':set cindent') + feed(':set autoindent') + feed(':set shiftwidth=2') + feed("0i") + set_extmark(ns, marks[1], 0, 3) + feed("bi") + local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({0, 1}, rv) + check_undo_redo(ns, marks[1], 0, 3, 0, 1) + -- check when cursor at eol + feed("uA") + rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({0, 1}, rv) + end) + + it('indenting multiple lines with = works', function() + feed(':set cindent') + feed(':set autoindent') + feed(':set shiftwidth=2') + feed("0iint A {1M12M2") + set_extmark(ns, marks[1], 1, 1) + set_extmark(ns, marks[2], 2, 1) + feed('=gg') + check_undo_redo(ns, marks[1], 1, 1, 1, 3) + check_undo_redo(ns, marks[2], 2, 1, 2, 5) + end) + + it('substitutes by deleting inside the replace matches', function() + -- do_sub in ex_cmds.c + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + feed(':s/34/xx') + check_undo_redo(ns, marks[1], 0, 2, 0, 4) + check_undo_redo(ns, marks[2], 0, 3, 0, 4) + end) + + it('substitutes when insert text > deleted', function() + -- do_sub in ex_cmds.c + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + feed(':s/34/xxx') + check_undo_redo(ns, marks[1], 0, 2, 0, 5) + check_undo_redo(ns, marks[2], 0, 3, 0, 5) + end) + + it('substitutes when marks around eol', function() + -- do_sub in ex_cmds.c + set_extmark(ns, marks[1], 0, 4) + set_extmark(ns, marks[2], 0, 5) + feed(':s/5/xxx') + check_undo_redo(ns, marks[1], 0, 4, 0, 7) + check_undo_redo(ns, marks[2], 0, 5, 0, 7) + end) + + it('substitutes over range insert text > deleted', function() + -- do_sub in ex_cmds.c + feed('Ax34xx') + feed('Axxx34') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 1, 1) + set_extmark(ns, marks[3], 2, 4) + feed(':1,3s/34/xxx') + check_undo_redo(ns, marks[1], 0, 2, 0, 5) + check_undo_redo(ns, marks[2], 1, 1, 1, 4) + check_undo_redo(ns, marks[3], 2, 4, 2, 6) + end) + + it('substitutes multiple matches in a line', function() + -- do_sub in ex_cmds.c + feed('ddi3x3x3') + set_extmark(ns, marks[1], 0, 0) + set_extmark(ns, marks[2], 0, 2) + set_extmark(ns, marks[3], 0, 4) + feed(':s/3/yy/g') + check_undo_redo(ns, marks[1], 0, 0, 0, 2) + check_undo_redo(ns, marks[2], 0, 2, 0, 5) + check_undo_redo(ns, marks[3], 0, 4, 0, 8) + end) + + it('substitions over multiple lines with newline in pattern', function() + feed('A67890xx') + set_extmark(ns, marks[1], 0, 3) + set_extmark(ns, marks[2], 0, 4) + set_extmark(ns, marks[3], 1, 0) + set_extmark(ns, marks[4], 1, 5) + set_extmark(ns, marks[5], 2, 0) + feed([[:1,2s:5\n:5 ]]) + check_undo_redo(ns, marks[1], 0, 3, 0, 3) + check_undo_redo(ns, marks[2], 0, 4, 0, 6) + check_undo_redo(ns, marks[3], 1, 0, 0, 6) + check_undo_redo(ns, marks[4], 1, 5, 0, 11) + check_undo_redo(ns, marks[5], 2, 0, 1, 0) + end) + + it('inserting', function() + feed('A67890xx') + set_extmark(ns, marks[1], 0, 3) + set_extmark(ns, marks[2], 0, 4) + set_extmark(ns, marks[3], 1, 0) + set_extmark(ns, marks[4], 1, 5) + set_extmark(ns, marks[5], 2, 0) + set_extmark(ns, marks[6], 1, 2) + feed([[:1,2s:5\n67:X]]) + check_undo_redo(ns, marks[1], 0, 3, 0, 3) + check_undo_redo(ns, marks[2], 0, 4, 0, 5) + check_undo_redo(ns, marks[3], 1, 0, 0, 5) + check_undo_redo(ns, marks[4], 1, 5, 0, 8) + check_undo_redo(ns, marks[5], 2, 0, 1, 0) + check_undo_redo(ns, marks[6], 1, 2, 0, 5) + end) + + it('substitions with multiple newlines in pattern', function() + feed('A67890xx') + set_extmark(ns, marks[1], 0, 4) + set_extmark(ns, marks[2], 0, 5) + set_extmark(ns, marks[3], 1, 0) + set_extmark(ns, marks[4], 1, 5) + set_extmark(ns, marks[5], 2, 0) + feed([[:1,2s:\n.*\n:X]]) + check_undo_redo(ns, marks[1], 0, 4, 0, 4) + check_undo_redo(ns, marks[2], 0, 5, 0, 6) + check_undo_redo(ns, marks[3], 1, 0, 0, 6) + check_undo_redo(ns, marks[4], 1, 5, 0, 6) + check_undo_redo(ns, marks[5], 2, 0, 0, 6) + end) + + it('substitions over multiple lines with replace in substition', function() + feed('A67890xx') + set_extmark(ns, marks[1], 0, 1) + set_extmark(ns, marks[2], 0, 2) + set_extmark(ns, marks[3], 0, 4) + set_extmark(ns, marks[4], 1, 0) + set_extmark(ns, marks[5], 2, 0) + feed([[:1,2s:3:\r]]) + check_undo_redo(ns, marks[1], 0, 1, 0, 1) + check_undo_redo(ns, marks[2], 0, 2, 1, 0) + check_undo_redo(ns, marks[3], 0, 4, 1, 1) + check_undo_redo(ns, marks[4], 1, 0, 2, 0) + check_undo_redo(ns, marks[5], 2, 0, 3, 0) + feed('u') + feed([[:1,2s:3:\rxx]]) + eq({1, 3}, curbufmeths.get_extmark_by_id(ns, marks[3])) + end) + + it('substitions over multiple lines with replace in substition', function() + feed('Ax3xx') + set_extmark(ns, marks[1], 1, 0) + set_extmark(ns, marks[2], 1, 1) + set_extmark(ns, marks[3], 1, 2) + feed([[:2,2s:3:\r]]) + check_undo_redo(ns, marks[1], 1, 0, 1, 0) + check_undo_redo(ns, marks[2], 1, 1, 2, 0) + check_undo_redo(ns, marks[3], 1, 2, 2, 0) + end) + + it('substitions over multiple lines with replace in substition', function() + feed('Ax3xx') + set_extmark(ns, marks[1], 0, 1) + set_extmark(ns, marks[2], 0, 2) + set_extmark(ns, marks[3], 0, 4) + set_extmark(ns, marks[4], 1, 1) + set_extmark(ns, marks[5], 2, 0) + feed([[:1,2s:3:\r]]) + check_undo_redo(ns, marks[1], 0, 1, 0, 1) + check_undo_redo(ns, marks[2], 0, 2, 1, 0) + check_undo_redo(ns, marks[3], 0, 4, 1, 1) + check_undo_redo(ns, marks[4], 1, 1, 3, 0) + check_undo_redo(ns, marks[5], 2, 0, 4, 0) + feed('u') + feed([[:1,2s:3:\rxx]]) + check_undo_redo(ns, marks[3], 0, 4, 1, 3) + end) + + it('substitions with newline in match and sub, delta is 0', function() + feed('A67890xx') + set_extmark(ns, marks[1], 0, 3) + set_extmark(ns, marks[2], 0, 4) + set_extmark(ns, marks[3], 0, 5) + set_extmark(ns, marks[4], 1, 0) + set_extmark(ns, marks[5], 1, 5) + set_extmark(ns, marks[6], 2, 0) + feed([[:1,1s:5\n:\r]]) + check_undo_redo(ns, marks[1], 0, 3, 0, 3) + check_undo_redo(ns, marks[2], 0, 4, 1, 0) + check_undo_redo(ns, marks[3], 0, 5, 1, 0) + check_undo_redo(ns, marks[4], 1, 0, 1, 0) + check_undo_redo(ns, marks[5], 1, 5, 1, 5) + check_undo_redo(ns, marks[6], 2, 0, 2, 0) + end) + + it('substitions with newline in match and sub, delta > 0', function() + feed('A67890xx') + set_extmark(ns, marks[1], 0, 3) + set_extmark(ns, marks[2], 0, 4) + set_extmark(ns, marks[3], 0, 5) + set_extmark(ns, marks[4], 1, 0) + set_extmark(ns, marks[5], 1, 5) + set_extmark(ns, marks[6], 2, 0) + feed([[:1,1s:5\n:\r\r]]) + check_undo_redo(ns, marks[1], 0, 3, 0, 3) + check_undo_redo(ns, marks[2], 0, 4, 2, 0) + check_undo_redo(ns, marks[3], 0, 5, 2, 0) + check_undo_redo(ns, marks[4], 1, 0, 2, 0) + check_undo_redo(ns, marks[5], 1, 5, 2, 5) + check_undo_redo(ns, marks[6], 2, 0, 3, 0) + end) + + it('substitions with newline in match and sub, delta < 0', function() + feed('A67890xxxx') + set_extmark(ns, marks[1], 0, 3) + set_extmark(ns, marks[2], 0, 4) + set_extmark(ns, marks[3], 0, 5) + set_extmark(ns, marks[4], 1, 0) + set_extmark(ns, marks[5], 1, 5) + set_extmark(ns, marks[6], 2, 1) + set_extmark(ns, marks[7], 3, 0) + feed([[:1,2s:5\n.*\n:\r]]) + check_undo_redo(ns, marks[1], 0, 3, 0, 3) + check_undo_redo(ns, marks[2], 0, 4, 1, 0) + check_undo_redo(ns, marks[3], 0, 5, 1, 0) + check_undo_redo(ns, marks[4], 1, 0, 1, 0) + check_undo_redo(ns, marks[5], 1, 5, 1, 0) + check_undo_redo(ns, marks[6], 2, 1, 1, 1) + check_undo_redo(ns, marks[7], 3, 0, 2, 0) + end) + + it('substitions with backrefs, newline inserted into sub', function() + feed('A67890xxxx') + set_extmark(ns, marks[1], 0, 3) + set_extmark(ns, marks[2], 0, 4) + set_extmark(ns, marks[3], 0, 5) + set_extmark(ns, marks[4], 1, 0) + set_extmark(ns, marks[5], 1, 5) + set_extmark(ns, marks[6], 2, 0) + feed([[:1,1s:5\(\n\):\0\1]]) + check_undo_redo(ns, marks[1], 0, 3, 0, 3) + check_undo_redo(ns, marks[2], 0, 4, 2, 0) + check_undo_redo(ns, marks[3], 0, 5, 2, 0) + check_undo_redo(ns, marks[4], 1, 0, 2, 0) + check_undo_redo(ns, marks[5], 1, 5, 2, 5) + check_undo_redo(ns, marks[6], 2, 0, 3, 0) + end) + + it('substitions a ^', function() + set_extmark(ns, marks[1], 0, 0) + set_extmark(ns, marks[2], 0, 1) + feed([[:s:^:x]]) + check_undo_redo(ns, marks[1], 0, 0, 0, 1) + check_undo_redo(ns, marks[2], 0, 1, 0, 2) + end) + + it('using without increase in order of magnitude', function() + -- do_addsub in ops.c + feed('ddiabc998xxxTc') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 0, 5) + set_extmark(ns, marks[4], 0, 6) + set_extmark(ns, marks[5], 0, 7) + feed('') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + check_undo_redo(ns, marks[2], 0, 3, 0, 6) + check_undo_redo(ns, marks[3], 0, 5, 0, 6) + check_undo_redo(ns, marks[4], 0, 6, 0, 6) + check_undo_redo(ns, marks[5], 0, 7, 0, 7) + end) + + it('using when increase in order of magnitude', function() + -- do_addsub in ops.c + feed('ddiabc999xxxTc') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 0, 5) + set_extmark(ns, marks[4], 0, 6) + set_extmark(ns, marks[5], 0, 7) + feed('') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + check_undo_redo(ns, marks[2], 0, 3, 0, 7) + check_undo_redo(ns, marks[3], 0, 5, 0, 7) + check_undo_redo(ns, marks[4], 0, 6, 0, 7) + check_undo_redo(ns, marks[5], 0, 7, 0, 8) + end) + + it('using when negative and without decrease in order of magnitude', function() + feed('ddiabc-999xxxT-') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 0, 6) + set_extmark(ns, marks[4], 0, 7) + set_extmark(ns, marks[5], 0, 8) + feed('') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + check_undo_redo(ns, marks[2], 0, 3, 0, 7) + check_undo_redo(ns, marks[3], 0, 6, 0, 7) + check_undo_redo(ns, marks[4], 0, 7, 0, 7) + check_undo_redo(ns, marks[5], 0, 8, 0, 8) + end) + + it('using when negative and decrease in order of magnitude', function() + feed('ddiabc-1000xxxT-') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 0, 7) + set_extmark(ns, marks[4], 0, 8) + set_extmark(ns, marks[5], 0, 9) + feed('') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + check_undo_redo(ns, marks[2], 0, 3, 0, 7) + check_undo_redo(ns, marks[3], 0, 7, 0, 7) + check_undo_redo(ns, marks[4], 0, 8, 0, 7) + check_undo_redo(ns, marks[5], 0, 9, 0, 8) + end) + + it('using without decrease in order of magnitude', function() + -- do_addsub in ops.c + feed('ddiabc999xxxTc') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 0, 5) + set_extmark(ns, marks[4], 0, 6) + set_extmark(ns, marks[5], 0, 7) + feed('') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + check_undo_redo(ns, marks[2], 0, 3, 0, 6) + check_undo_redo(ns, marks[3], 0, 5, 0, 6) + check_undo_redo(ns, marks[4], 0, 6, 0, 6) + check_undo_redo(ns, marks[5], 0, 7, 0, 7) + end) + + it('using when decrease in order of magnitude', function() + -- do_addsub in ops.c + feed('ddiabc1000xxxTc') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 0, 6) + set_extmark(ns, marks[4], 0, 7) + set_extmark(ns, marks[5], 0, 8) + feed('') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + check_undo_redo(ns, marks[2], 0, 3, 0, 6) + check_undo_redo(ns, marks[3], 0, 6, 0, 6) + check_undo_redo(ns, marks[4], 0, 7, 0, 6) + check_undo_redo(ns, marks[5], 0, 8, 0, 7) + end) + + it('using when negative and without increase in order of magnitude', function() + feed('ddiabc-998xxxT-') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 0, 6) + set_extmark(ns, marks[4], 0, 7) + set_extmark(ns, marks[5], 0, 8) + feed('') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + check_undo_redo(ns, marks[2], 0, 3, 0, 7) + check_undo_redo(ns, marks[3], 0, 6, 0, 7) + check_undo_redo(ns, marks[4], 0, 7, 0, 7) + check_undo_redo(ns, marks[5], 0, 8, 0, 8) + end) + + it('using when negative and increase in order of magnitude', function() + feed('ddiabc-999xxxT-') + set_extmark(ns, marks[1], 0, 2) + set_extmark(ns, marks[2], 0, 3) + set_extmark(ns, marks[3], 0, 6) + set_extmark(ns, marks[4], 0, 7) + set_extmark(ns, marks[5], 0, 8) + feed('') + check_undo_redo(ns, marks[1], 0, 2, 0, 2) + check_undo_redo(ns, marks[2], 0, 3, 0, 8) + check_undo_redo(ns, marks[3], 0, 6, 0, 8) + check_undo_redo(ns, marks[4], 0, 7, 0, 8) + check_undo_redo(ns, marks[5], 0, 8, 0, 9) + end) + + it('throws consistent error codes', function() + local ns_invalid = ns2 + 1 + eq("Invalid ns_id", pcall_err(set_extmark, ns_invalid, marks[1], positions[1][1], positions[1][2])) + eq("Invalid ns_id", pcall_err(curbufmeths.del_extmark, ns_invalid, marks[1])) + eq("Invalid ns_id", pcall_err(get_extmarks, ns_invalid, positions[1], positions[2])) + eq("Invalid ns_id", pcall_err(curbufmeths.get_extmark_by_id, ns_invalid, marks[1])) + end) + + it('when col = line-length, set the mark on eol', function() + set_extmark(ns, marks[1], 0, -1) + local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({0, init_text:len()}, rv) + -- Test another + set_extmark(ns, marks[1], 0, -1) + rv = curbufmeths.get_extmark_by_id(ns, marks[1]) + eq({0, init_text:len()}, rv) + end) + + it('when col = line-length, set the mark on eol', function() + local invalid_col = init_text:len() + 1 + eq("col value outside range", pcall_err(set_extmark, ns, marks[1], 0, invalid_col)) + end) + + it('fails when line > line_count', function() + local invalid_col = init_text:len() + 1 + local invalid_lnum = 3 + eq('line value outside range', pcall_err(set_extmark, ns, marks[1], invalid_lnum, invalid_col)) + eq({}, curbufmeths.get_extmark_by_id(ns, marks[1])) + end) + + it('bug from check_col in extmark_set', function() + -- This bug was caused by extmark_set always using check_col. check_col + -- always uses the current buffer. This wasn't working during undo so we + -- now use check_col and check_lnum only when they are required. + feed('A67890xx') + feed('A1234567890xx') + set_extmark(ns, marks[1], 3, 4) + feed([[:1,5s:5\n:5 ]]) + check_undo_redo(ns, marks[1], 3, 4, 2, 6) + end) + + it('in read-only buffer', function() + command("view! runtime/doc/help.txt") + eq(true, curbufmeths.get_option('ro')) + local id = set_extmark(ns, 0, 0, 2) + eq({{id, 0, 2}}, get_extmarks(ns,0, -1)) + end) + + it('can set a mark to other buffer', function() + local buf = request('nvim_create_buf', 0, 1) + request('nvim_buf_set_lines', buf, 0, -1, 1, {"", ""}) + local id = bufmeths.set_extmark(buf, ns, 0, 1, 0, {}) + eq({{id, 1, 0}}, bufmeths.get_extmarks(buf, ns, 0, -1, {})) + end) + + it('does not crash with append/delete/undo seqence', function() + meths.exec([[ + let ns = nvim_create_namespace('myplugin') + call nvim_buf_set_extmark(0, ns, 0, 0, 0, {}) + call append(0, '') + %delete + undo]],false) + eq(2, meths.eval('1+1')) -- did not crash + end) +end) + +describe('Extmarks buffer api with many marks', function() + local ns1 + local ns2 + local ns_marks = {} + before_each(function() + clear() + ns1 = request('nvim_create_namespace', "ns1") + ns2 = request('nvim_create_namespace', "ns2") + ns_marks = {[ns1]={}, [ns2]={}} + local lines = {} + for i = 1,30 do + lines[#lines+1] = string.rep("x ",i) + end + curbufmeths.set_lines(0, -1, true, lines) + local ns = ns1 + local q = 0 + for i = 0,29 do + for j = 0,i do + local id = set_extmark(ns,0, i,j) + eq(nil, ns_marks[ns][id]) + ok(id > 0) + ns_marks[ns][id] = {i,j} + ns = ns1+ns2-ns + q = q + 1 + end + end + eq(233, #ns_marks[ns1]) + eq(232, #ns_marks[ns2]) + + end) + + local function get_marks(ns) + local mark_list = get_extmarks(ns, 0, -1) + local marks = {} + for _, mark in ipairs(mark_list) do + local id, row, col = unpack(mark) + eq(nil, marks[id], "duplicate mark") + marks[id] = {row,col} + end + return marks + end + + it("can get marks", function() + eq(ns_marks[ns1], get_marks(ns1)) + eq(ns_marks[ns2], get_marks(ns2)) + end) + + it("can clear all marks in ns", function() + curbufmeths.clear_namespace(ns1, 0, -1) + eq({}, get_marks(ns1)) + eq(ns_marks[ns2], get_marks(ns2)) + curbufmeths.clear_namespace(ns2, 0, -1) + eq({}, get_marks(ns1)) + eq({}, get_marks(ns2)) + end) + + it("can clear line range", function() + curbufmeths.clear_namespace(ns1, 10, 20) + for id, mark in pairs(ns_marks[ns1]) do + if 10 <= mark[1] and mark[1] < 20 then + ns_marks[ns1][id] = nil + end + end + eq(ns_marks[ns1], get_marks(ns1)) + eq(ns_marks[ns2], get_marks(ns2)) + end) + + it("can delete line", function() + feed('10Gdd') + for _, marks in pairs(ns_marks) do + for id, mark in pairs(marks) do + if mark[1] == 9 then + marks[id] = {9,0} + elseif mark[1] >= 10 then + mark[1] = mark[1] - 1 + end + end + end + eq(ns_marks[ns1], get_marks(ns1)) + eq(ns_marks[ns2], get_marks(ns2)) + end) + + it("can delete lines", function() + feed('10G10dd') + for _, marks in pairs(ns_marks) do + for id, mark in pairs(marks) do + if 9 <= mark[1] and mark[1] < 19 then + marks[id] = {9,0} + elseif mark[1] >= 19 then + mark[1] = mark[1] - 10 + end + end + end + eq(ns_marks[ns1], get_marks(ns1)) + eq(ns_marks[ns2], get_marks(ns2)) + end) + + it("can wipe buffer", function() + command('bwipe!') + eq({}, get_marks(ns1)) + eq({}, get_marks(ns2)) + end) +end) diff --git a/test/functional/api/mark_extended_spec.lua b/test/functional/api/mark_extended_spec.lua deleted file mode 100644 index 8aa8ed07c5..0000000000 --- a/test/functional/api/mark_extended_spec.lua +++ /dev/null @@ -1,1478 +0,0 @@ -local helpers = require('test.functional.helpers')(after_each) -local Screen = require('test.functional.ui.screen') - -local request = helpers.request -local eq = helpers.eq -local ok = helpers.ok -local curbufmeths = helpers.curbufmeths -local bufmeths = helpers.bufmeths -local pcall_err = helpers.pcall_err -local insert = helpers.insert -local feed = helpers.feed -local clear = helpers.clear -local command = helpers.command -local meths = helpers.meths - -local function expect(contents) - return eq(contents, helpers.curbuf_contents()) -end - -local function check_undo_redo(ns, mark, sr, sc, er, ec) --s = start, e = end - local rv = curbufmeths.get_extmark_by_id(ns, mark) - eq({er, ec}, rv) - feed("u") - rv = curbufmeths.get_extmark_by_id(ns, mark) - eq({sr, sc}, rv) - feed("") - rv = curbufmeths.get_extmark_by_id(ns, mark) - eq({er, ec}, rv) -end - -local function set_extmark(ns_id, id, line, col, opts) - if opts == nil then - opts = {} - end - return curbufmeths.set_extmark(ns_id, id, line, col, opts) -end - -local function get_extmarks(ns_id, start, end_, opts) - if opts == nil then - opts = {} - end - return curbufmeths.get_extmarks(ns_id, start, end_, opts) -end - -local function batch_set(ns_id, positions) - local ids = {} - for _, pos in ipairs(positions) do - table.insert(ids, set_extmark(ns_id, 0, pos[1], pos[2])) - end - return ids -end - -local function batch_check(ns_id, ids, positions) - local actual, expected = {}, {} - for i,id in ipairs(ids) do - expected[id] = positions[i] - end - for _, mark in pairs(get_extmarks(ns_id, 0, -1, {})) do - actual[mark[1]] = {mark[2], mark[3]} - end - eq(expected, actual) -end - -local function batch_check_undo_redo(ns_id, ids, before, after) - batch_check(ns_id, ids, after) - feed("u") - batch_check(ns_id, ids, before) - feed("") - batch_check(ns_id, ids, after) -end - -describe('API/extmarks', function() - local screen - local marks, positions, init_text, row, col - local ns, ns2 - - before_each(function() - -- Initialize some namespaces and insert 12345 into a buffer - marks = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} - positions = {{0, 0,}, {0, 2}, {0, 3}} - - init_text = "12345" - row = 0 - col = 2 - - clear() - - insert(init_text) - ns = request('nvim_create_namespace', "my-fancy-plugin") - ns2 = request('nvim_create_namespace', "my-fancy-plugin2") - end) - - it('adds, updates and deletes marks', function() - local rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2]) - eq(marks[1], rv) - rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({positions[1][1], positions[1][2]}, rv) - -- Test adding a second mark on same row works - rv = set_extmark(ns, marks[2], positions[2][1], positions[2][2]) - eq(marks[2], rv) - - -- Test an update, (same pos) - rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2]) - eq(marks[1], rv) - rv = curbufmeths.get_extmark_by_id(ns, marks[2]) - eq({positions[2][1], positions[2][2]}, rv) - -- Test an update, (new pos) - row = positions[1][1] - col = positions[1][2] + 1 - rv = set_extmark(ns, marks[1], row, col) - eq(marks[1], rv) - rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({row, col}, rv) - - -- remove the test marks - eq(true, curbufmeths.del_extmark(ns, marks[1])) - eq(false, curbufmeths.del_extmark(ns, marks[1])) - eq(true, curbufmeths.del_extmark(ns, marks[2])) - eq(false, curbufmeths.del_extmark(ns, marks[3])) - eq(false, curbufmeths.del_extmark(ns, 1000)) - end) - - it('can clear a specific namespace range', function() - set_extmark(ns, 1, 0, 1) - set_extmark(ns2, 1, 0, 1) - -- force a new undo buffer - feed('o') - curbufmeths.clear_namespace(ns2, 0, -1) - eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1})) - eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) - feed('u') - eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1})) - eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) - feed('') - eq({{1, 0, 1}}, get_extmarks(ns, {0, 0}, {-1, -1})) - eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) - end) - - it('can clear a namespace range using 0,-1', function() - set_extmark(ns, 1, 0, 1) - set_extmark(ns2, 1, 0, 1) - -- force a new undo buffer - feed('o') - curbufmeths.clear_namespace(-1, 0, -1) - eq({}, get_extmarks(ns, {0, 0}, {-1, -1})) - eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) - feed('u') - eq({}, get_extmarks(ns, {0, 0}, {-1, -1})) - eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) - feed('') - eq({}, get_extmarks(ns, {0, 0}, {-1, -1})) - eq({}, get_extmarks(ns2, {0, 0}, {-1, -1})) - end) - - it('querying for information and ranges', function() - --marks = {1, 2, 3} - --positions = {{0, 0,}, {0, 2}, {0, 3}} - -- add some more marks - for i, m in ipairs(marks) do - if positions[i] ~= nil then - local rv = set_extmark(ns, m, positions[i][1], positions[i][2]) - eq(m, rv) - end - end - - -- {0, 0} and {-1, -1} work as extreme values - eq({{1, 0, 0}}, get_extmarks(ns, {0, 0}, {0, 0})) - eq({}, get_extmarks(ns, {-1, -1}, {-1, -1})) - local rv = get_extmarks(ns, {0, 0}, {-1, -1}) - for i, m in ipairs(marks) do - if positions[i] ~= nil then - eq({m, positions[i][1], positions[i][2]}, rv[i]) - end - end - - -- 0 and -1 works as short hand extreme values - eq({{1, 0, 0}}, get_extmarks(ns, 0, 0)) - eq({}, get_extmarks(ns, -1, -1)) - rv = get_extmarks(ns, 0, -1) - for i, m in ipairs(marks) do - if positions[i] ~= nil then - eq({m, positions[i][1], positions[i][2]}, rv[i]) - end - end - - -- next with mark id - rv = get_extmarks(ns, marks[1], {-1, -1}, {limit=1}) - eq({{marks[1], positions[1][1], positions[1][2]}}, rv) - rv = get_extmarks(ns, marks[2], {-1, -1}, {limit=1}) - eq({{marks[2], positions[2][1], positions[2][2]}}, rv) - -- next with positional when mark exists at position - rv = get_extmarks(ns, positions[1], {-1, -1}, {limit=1}) - eq({{marks[1], positions[1][1], positions[1][2]}}, rv) - -- next with positional index (no mark at position) - rv = get_extmarks(ns, {positions[1][1], positions[1][2] +1}, {-1, -1}, {limit=1}) - eq({{marks[2], positions[2][1], positions[2][2]}}, rv) - -- next with Extremity index - rv = get_extmarks(ns, {0,0}, {-1, -1}, {limit=1}) - eq({{marks[1], positions[1][1], positions[1][2]}}, rv) - - -- nextrange with mark id - rv = get_extmarks(ns, marks[1], marks[3]) - eq({marks[1], positions[1][1], positions[1][2]}, rv[1]) - eq({marks[2], positions[2][1], positions[2][2]}, rv[2]) - -- nextrange with `limit` - rv = get_extmarks(ns, marks[1], marks[3], {limit=2}) - eq(2, table.getn(rv)) - -- nextrange with positional when mark exists at position - rv = get_extmarks(ns, positions[1], positions[3]) - eq({marks[1], positions[1][1], positions[1][2]}, rv[1]) - eq({marks[2], positions[2][1], positions[2][2]}, rv[2]) - rv = get_extmarks(ns, positions[2], positions[3]) - eq(2, table.getn(rv)) - -- nextrange with positional index (no mark at position) - local lower = {positions[1][1], positions[2][2] -1} - local upper = {positions[2][1], positions[3][2] - 1} - rv = get_extmarks(ns, lower, upper) - eq({{marks[2], positions[2][1], positions[2][2]}}, rv) - lower = {positions[3][1], positions[3][2] + 1} - upper = {positions[3][1], positions[3][2] + 2} - rv = get_extmarks(ns, lower, upper) - eq({}, rv) - -- nextrange with extremity index - lower = {positions[2][1], positions[2][2]+1} - upper = {-1, -1} - rv = get_extmarks(ns, lower, upper) - eq({{marks[3], positions[3][1], positions[3][2]}}, rv) - - -- prev with mark id - rv = get_extmarks(ns, marks[3], {0, 0}, {limit=1}) - eq({{marks[3], positions[3][1], positions[3][2]}}, rv) - rv = get_extmarks(ns, marks[2], {0, 0}, {limit=1}) - eq({{marks[2], positions[2][1], positions[2][2]}}, rv) - -- prev with positional when mark exists at position - rv = get_extmarks(ns, positions[3], {0, 0}, {limit=1}) - eq({{marks[3], positions[3][1], positions[3][2]}}, rv) - -- prev with positional index (no mark at position) - rv = get_extmarks(ns, {positions[1][1], positions[1][2] +1}, {0, 0}, {limit=1}) - eq({{marks[1], positions[1][1], positions[1][2]}}, rv) - -- prev with Extremity index - rv = get_extmarks(ns, {-1,-1}, {0,0}, {limit=1}) - eq({{marks[3], positions[3][1], positions[3][2]}}, rv) - - -- prevrange with mark id - rv = get_extmarks(ns, marks[3], marks[1]) - eq({marks[3], positions[3][1], positions[3][2]}, rv[1]) - eq({marks[2], positions[2][1], positions[2][2]}, rv[2]) - eq({marks[1], positions[1][1], positions[1][2]}, rv[3]) - -- prevrange with limit - rv = get_extmarks(ns, marks[3], marks[1], {limit=2}) - eq(2, table.getn(rv)) - -- prevrange with positional when mark exists at position - rv = get_extmarks(ns, positions[3], positions[1]) - eq({{marks[3], positions[3][1], positions[3][2]}, - {marks[2], positions[2][1], positions[2][2]}, - {marks[1], positions[1][1], positions[1][2]}}, rv) - rv = get_extmarks(ns, positions[2], positions[1]) - eq(2, table.getn(rv)) - -- prevrange with positional index (no mark at position) - lower = {positions[2][1], positions[2][2] + 1} - upper = {positions[3][1], positions[3][2] + 1} - rv = get_extmarks(ns, upper, lower) - eq({{marks[3], positions[3][1], positions[3][2]}}, rv) - lower = {positions[3][1], positions[3][2] + 1} - upper = {positions[3][1], positions[3][2] + 2} - rv = get_extmarks(ns, upper, lower) - eq({}, rv) - -- prevrange with extremity index - lower = {0,0} - upper = {positions[2][1], positions[2][2] - 1} - rv = get_extmarks(ns, upper, lower) - eq({{marks[1], positions[1][1], positions[1][2]}}, rv) - end) - - it('querying for information with limit', function() - -- add some more marks - for i, m in ipairs(marks) do - if positions[i] ~= nil then - local rv = set_extmark(ns, m, positions[i][1], positions[i][2]) - eq(m, rv) - end - end - - local rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=1}) - eq(1, table.getn(rv)) - rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=2}) - eq(2, table.getn(rv)) - rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=3}) - eq(3, table.getn(rv)) - - -- now in reverse - rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=1}) - eq(1, table.getn(rv)) - rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=2}) - eq(2, table.getn(rv)) - rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=3}) - eq(3, table.getn(rv)) - end) - - it('get_marks works when mark col > upper col', function() - feed('A12345') - feed('A12345') - set_extmark(ns, 10, 0, 2) -- this shouldn't be found - set_extmark(ns, 11, 2, 1) -- this shouldn't be found - set_extmark(ns, marks[1], 0, 4) -- check col > our upper bound - set_extmark(ns, marks[2], 1, 1) -- check col < lower bound - set_extmark(ns, marks[3], 2, 0) -- check is inclusive - eq({{marks[1], 0, 4}, - {marks[2], 1, 1}, - {marks[3], 2, 0}}, - get_extmarks(ns, {0, 3}, {2, 0})) - end) - - it('get_marks works in reverse when mark col < lower col', function() - feed('A12345') - feed('A12345') - set_extmark(ns, 10, 0, 1) -- this shouldn't be found - set_extmark(ns, 11, 2, 4) -- this shouldn't be found - set_extmark(ns, marks[1], 2, 1) -- check col < our lower bound - set_extmark(ns, marks[2], 1, 4) -- check col > upper bound - set_extmark(ns, marks[3], 0, 2) -- check is inclusive - local rv = get_extmarks(ns, {2, 3}, {0, 2}) - eq({{marks[1], 2, 1}, - {marks[2], 1, 4}, - {marks[3], 0, 2}}, - rv) - end) - - it('get_marks limit=0 returns nothing', function() - set_extmark(ns, marks[1], positions[1][1], positions[1][2]) - local rv = get_extmarks(ns, {-1, -1}, {-1, -1}, {limit=0}) - eq({}, rv) - end) - - - it('marks move with line insertations', function() - set_extmark(ns, marks[1], 0, 0) - feed("yyP") - check_undo_redo(ns, marks[1], 0, 0, 1, 0) - end) - - it('marks move with multiline insertations', function() - feed("a2233") - set_extmark(ns, marks[1], 1, 1) - feed('ggVGyP') - check_undo_redo(ns, marks[1], 1, 1, 4, 1) - end) - - it('marks move with line join', function() - -- do_join in ops.c - feed("a222") - set_extmark(ns, marks[1], 1, 0) - feed('ggJ') - check_undo_redo(ns, marks[1], 1, 0, 0, 6) - end) - - it('join works when no marks are present', function() - screen = Screen.new(15, 10) - screen:attach() - feed("a1") - feed('kJ') - -- This shouldn't seg fault - screen:expect([[ - 12345^ 1 | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - | - ]]) - end) - - it('marks move with multiline join', function() - -- do_join in ops.c - feed("a222333444") - set_extmark(ns, marks[1], 3, 0) - feed('2GVGJ') - check_undo_redo(ns, marks[1], 3, 0, 1, 8) - end) - - it('marks move with line deletes', function() - feed("a222333444") - set_extmark(ns, marks[1], 2, 1) - feed('ggjdd') - check_undo_redo(ns, marks[1], 2, 1, 1, 1) - end) - - it('marks move with multiline deletes', function() - feed("a222333444") - set_extmark(ns, marks[1], 3, 0) - feed('gg2dd') - check_undo_redo(ns, marks[1], 3, 0, 1, 0) - -- regression test, undoing multiline delete when mark is on row 1 - feed('ugg3dd') - check_undo_redo(ns, marks[1], 3, 0, 0, 0) - end) - - it('marks move with open line', function() - -- open_line in misc1.c - -- testing marks below are also moved - feed("yyP") - set_extmark(ns, marks[1], 0, 4) - set_extmark(ns, marks[2], 1, 4) - feed('1G') - check_undo_redo(ns, marks[1], 0, 4, 1, 4) - check_undo_redo(ns, marks[2], 1, 4, 2, 4) - feed('2Go') - check_undo_redo(ns, marks[1], 1, 4, 1, 4) - check_undo_redo(ns, marks[2], 2, 4, 3, 4) - end) - - it('marks move with char inserts', function() - -- insertchar in edit.c (the ins_str branch) - screen = Screen.new(15, 10) - screen:attach() - set_extmark(ns, marks[1], 0, 3) - feed('0') - insert('abc') - screen:expect([[ - ab^c12345 | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - ~ | - | - ]]) - local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({0, 6}, rv) - check_undo_redo(ns, marks[1], 0, 3, 0, 6) - end) - - -- gravity right as definted in tk library - it('marks have gravity right', function() - -- insertchar in edit.c (the ins_str branch) - set_extmark(ns, marks[1], 0, 2) - feed('03l') - insert("X") - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - - -- check multibyte chars - feed('03l') - insert("~~") - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - end) - - it('we can insert multibyte chars', function() - -- insertchar in edit.c - feed('a12345') - set_extmark(ns, marks[1], 1, 2) - -- Insert a fullwidth (two col) tilde, NICE - feed('0i~') - check_undo_redo(ns, marks[1], 1, 2, 1, 5) - end) - - it('marks move with blockwise inserts', function() - -- op_insert in ops.c - feed('a12345') - set_extmark(ns, marks[1], 1, 2) - feed('0lkI9') - check_undo_redo(ns, marks[1], 1, 2, 1, 3) - end) - - it('marks move with line splits (using enter)', function() - -- open_line in misc1.c - -- testing marks below are also moved - feed("yyP") - set_extmark(ns, marks[1], 0, 4) - set_extmark(ns, marks[2], 1, 4) - feed('1Gla') - check_undo_redo(ns, marks[1], 0, 4, 1, 2) - check_undo_redo(ns, marks[2], 1, 4, 2, 4) - end) - - it('marks at last line move on insert new line', function() - -- open_line in misc1.c - set_extmark(ns, marks[1], 0, 4) - feed('0i') - check_undo_redo(ns, marks[1], 0, 4, 1, 4) - end) - - it('yet again marks move with line splits', function() - -- the first test above wasn't catching all errors.. - feed("A67890") - set_extmark(ns, marks[1], 0, 4) - feed("04li") - check_undo_redo(ns, marks[1], 0, 4, 1, 0) - end) - - it('and one last time line splits...', function() - set_extmark(ns, marks[1], 0, 1) - set_extmark(ns, marks[2], 0, 2) - feed("02li") - check_undo_redo(ns, marks[1], 0, 1, 0, 1) - check_undo_redo(ns, marks[2], 0, 2, 1, 0) - end) - - it('multiple marks move with mark splits', function() - set_extmark(ns, marks[1], 0, 1) - set_extmark(ns, marks[2], 0, 3) - feed("0li") - check_undo_redo(ns, marks[1], 0, 1, 1, 0) - check_undo_redo(ns, marks[2], 0, 3, 1, 2) - end) - - it('deleting right before a mark works', function() - -- op_delete in ops.c - set_extmark(ns, marks[1], 0, 2) - feed('0lx') - check_undo_redo(ns, marks[1], 0, 2, 0, 1) - end) - - it('deleting right after a mark works', function() - -- op_delete in ops.c - set_extmark(ns, marks[1], 0, 2) - feed('02lx') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - end) - - it('marks move with char deletes', function() - -- op_delete in ops.c - set_extmark(ns, marks[1], 0, 2) - feed('02dl') - check_undo_redo(ns, marks[1], 0, 2, 0, 0) - -- from the other side (nothing should happen) - feed('$x') - check_undo_redo(ns, marks[1], 0, 0, 0, 0) - end) - - it('marks move with char deletes over a range', function() - -- op_delete in ops.c - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - feed('0l3dl') - check_undo_redo(ns, marks[1], 0, 2, 0, 1) - check_undo_redo(ns, marks[2], 0, 3, 0, 1) - -- delete 1, nothing should happen to our marks - feed('u') - feed('$x') - check_undo_redo(ns, marks[2], 0, 3, 0, 3) - end) - - it('deleting marks at end of line works', function() - -- mark_extended.c/extmark_col_adjust_delete - set_extmark(ns, marks[1], 0, 4) - feed('$x') - check_undo_redo(ns, marks[1], 0, 4, 0, 4) - -- check the copy happened correctly on delete at eol - feed('$x') - check_undo_redo(ns, marks[1], 0, 4, 0, 3) - feed('u') - check_undo_redo(ns, marks[1], 0, 4, 0, 4) - end) - - it('marks move with blockwise deletes', function() - -- op_delete in ops.c - feed('a12345') - set_extmark(ns, marks[1], 1, 4) - feed('hhhkd') - check_undo_redo(ns, marks[1], 1, 4, 1, 1) - end) - - it('marks move with blockwise deletes over a range', function() - -- op_delete in ops.c - feed('a12345') - set_extmark(ns, marks[1], 0, 1) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 1, 2) - feed('0k3lx') - check_undo_redo(ns, marks[1], 0, 1, 0, 0) - check_undo_redo(ns, marks[2], 0, 3, 0, 0) - check_undo_redo(ns, marks[3], 1, 2, 1, 0) - -- delete 1, nothing should happen to our marks - feed('u') - feed('$jx') - check_undo_redo(ns, marks[2], 0, 3, 0, 3) - check_undo_redo(ns, marks[3], 1, 2, 1, 2) - end) - - it('works with char deletes over multilines', function() - feed('a12345test-me') - set_extmark(ns, marks[1], 2, 5) - feed('gg') - feed('dv?-m?') - check_undo_redo(ns, marks[1], 2, 5, 0, 0) - end) - - it('marks outside of deleted range move with visual char deletes', function() - -- op_delete in ops.c - set_extmark(ns, marks[1], 0, 3) - feed('0vx') - check_undo_redo(ns, marks[1], 0, 3, 0, 2) - - feed("u") - feed('0vlx') - check_undo_redo(ns, marks[1], 0, 3, 0, 1) - - feed("u") - feed('0v2lx') - check_undo_redo(ns, marks[1], 0, 3, 0, 0) - - -- from the other side (nothing should happen) - feed('$vx') - check_undo_redo(ns, marks[1], 0, 0, 0, 0) - end) - - it('marks outside of deleted range move with char deletes', function() - -- op_delete in ops.c - set_extmark(ns, marks[1], 0, 3) - feed('0x') - check_undo_redo(ns, marks[1], 0, 3, 0, 2) - - feed("u") - feed('02x') - check_undo_redo(ns, marks[1], 0, 3, 0, 1) - - feed("u") - feed('0v3lx') - check_undo_redo(ns, marks[1], 0, 3, 0, 0) - - -- from the other side (nothing should happen) - feed("u") - feed('$vx') - check_undo_redo(ns, marks[1], 0, 3, 0, 3) - end) - - it('marks move with P(backward) paste', function() - -- do_put in ops.c - feed('0iabc') - set_extmark(ns, marks[1], 0, 7) - feed('0veyP') - check_undo_redo(ns, marks[1], 0, 7, 0, 15) - end) - - it('marks move with p(forward) paste', function() - -- do_put in ops.c - feed('0iabc') - set_extmark(ns, marks[1], 0, 7) - feed('0veyp') - check_undo_redo(ns, marks[1], 0, 7, 0, 15) - end) - - it('marks move with blockwise P(backward) paste', function() - -- do_put in ops.c - feed('a12345') - set_extmark(ns, marks[1], 1, 4) - feed('hhkyP') - check_undo_redo(ns, marks[1], 1, 4, 1, 7) - end) - - it('marks move with blockwise p(forward) paste', function() - -- do_put in ops.c - feed('a12345') - set_extmark(ns, marks[1], 1, 4) - feed('hhkyp') - check_undo_redo(ns, marks[1], 1, 4, 1, 7) - end) - - describe('multiline regions', function() - before_each(function() - feed('dd') - -- Achtung: code has been spiced with some unicode, - -- to make life more interesting. - -- luacheck whines about TABs inside strings for whatever reason. - -- luacheck: push ignore 621 - insert([[ - static int nlua_rpcrequest(lua_State *lstate) - { - Ïf (!nlua_is_deferred_safe(lstate)) { - // strictly not allowed - Яetörn luaL_error(lstate, e_luv_api_disabled, "rpcrequest"); - } - return nlua_rpc(lstate, true); - }]]) - -- luacheck: pop - end) - - it('delete', function() - local pos1 = { - {2, 4}, {2, 12}, {2, 13}, {2, 14}, {2, 25}, - {4, 8}, {4, 10}, {4, 20}, - {5, 3}, {6, 10} - } - local ids = batch_set(ns, pos1) - batch_check(ns, ids, pos1) - feed('3Gfiv2+ftd') - batch_check_undo_redo(ns, ids, pos1, { - {2, 4}, {2, 12}, {2, 13}, {2, 13}, {2, 13}, - {2, 13}, {2, 15}, {2, 25}, - {3, 3}, {4, 10} - }) - end) - - -- TODO(bfredl): add more tests! - end) - - it('replace works', function() - set_extmark(ns, marks[1], 0, 2) - feed('0r2') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - end) - - it('blockwise replace works', function() - feed('a12345') - set_extmark(ns, marks[1], 0, 2) - feed('0llkr1') - check_undo_redo(ns, marks[1], 0, 2, 0, 3) - end) - - it('shift line', function() - -- shift_line in ops.c - feed(':set shiftwidth=4') - set_extmark(ns, marks[1], 0, 2) - feed('0>>') - check_undo_redo(ns, marks[1], 0, 2, 0, 6) - expect(' 12345') - - feed('>>') - -- this is counter-intuitive. But what happens - -- is that 4 spaces gets extended to one tab (== 8 spaces) - check_undo_redo(ns, marks[1], 0, 6, 0, 3) - expect('\t12345') - - feed('') -- have to escape, same as << - check_undo_redo(ns, marks[1], 0, 3, 0, 6) - end) - - it('blockwise shift', function() - -- shift_block in ops.c - feed(':set shiftwidth=4') - feed('a12345') - set_extmark(ns, marks[1], 1, 2) - feed('0k>') - check_undo_redo(ns, marks[1], 1, 2, 1, 6) - feed('j>') - expect('\t12345\n\t12345') - check_undo_redo(ns, marks[1], 1, 6, 1, 3) - - feed('j') - check_undo_redo(ns, marks[1], 1, 3, 1, 6) - end) - - it('tab works with expandtab', function() - -- ins_tab in edit.c - feed(':set expandtab') - feed(':set shiftwidth=2') - set_extmark(ns, marks[1], 0, 2) - feed('0i') - check_undo_redo(ns, marks[1], 0, 2, 0, 6) - end) - - it('tabs work', function() - -- ins_tab in edit.c - feed(':set noexpandtab') - feed(':set shiftwidth=2') - feed(':set softtabstop=2') - feed(':set tabstop=8') - set_extmark(ns, marks[1], 0, 2) - feed('0i') - check_undo_redo(ns, marks[1], 0, 2, 0, 4) - feed('0iX') - check_undo_redo(ns, marks[1], 0, 4, 0, 6) - end) - - it('marks move when using :move', function() - set_extmark(ns, marks[1], 0, 0) - feed('A2:1move 2') - check_undo_redo(ns, marks[1], 0, 0, 1, 0) - -- test codepath when moving lines up - feed(':2move 0') - check_undo_redo(ns, marks[1], 1, 0, 0, 0) - end) - - it('marks move when using :move part 2', function() - -- make sure we didn't get lucky with the math... - feed('A23456') - set_extmark(ns, marks[1], 1, 0) - feed(':2,3move 5') - check_undo_redo(ns, marks[1], 1, 0, 3, 0) - -- test codepath when moving lines up - feed(':4,5move 1') - check_undo_redo(ns, marks[1], 3, 0, 1, 0) - end) - - it('undo and redo of set and unset marks', function() - -- Force a new undo head - feed('o') - set_extmark(ns, marks[1], 0, 1) - feed('o') - set_extmark(ns, marks[2], 0, -1) - set_extmark(ns, marks[3], 0, -1) - - feed("u") - local rv = get_extmarks(ns, {0, 0}, {-1, -1}) - eq(3, table.getn(rv)) - - feed("") - rv = get_extmarks(ns, {0, 0}, {-1, -1}) - eq(3, table.getn(rv)) - - -- Test updates - feed('o') - set_extmark(ns, marks[1], positions[1][1], positions[1][2]) - rv = get_extmarks(ns, marks[1], marks[1], {limit=1}) - eq(1, table.getn(rv)) - feed("u") - feed("") - -- old value is NOT kept in history - check_undo_redo(ns, marks[1], positions[1][1], positions[1][2], positions[1][1], positions[1][2]) - - -- Test unset - feed('o') - curbufmeths.del_extmark(ns, marks[3]) - feed("u") - rv = get_extmarks(ns, {0, 0}, {-1, -1}) - -- undo does NOT restore deleted marks - eq(2, table.getn(rv)) - feed("") - rv = get_extmarks(ns, {0, 0}, {-1, -1}) - eq(2, table.getn(rv)) - end) - - it('undo and redo of marks deleted during edits', function() - -- test extmark_adjust - feed('A12345') - set_extmark(ns, marks[1], 1, 2) - feed('dd') - check_undo_redo(ns, marks[1], 1, 2, 1, 0) - end) - - it('namespaces work properly', function() - local rv = set_extmark(ns, marks[1], positions[1][1], positions[1][2]) - eq(1, rv) - rv = set_extmark(ns2, marks[1], positions[1][1], positions[1][2]) - eq(1, rv) - rv = get_extmarks(ns, {0, 0}, {-1, -1}) - eq(1, table.getn(rv)) - rv = get_extmarks(ns2, {0, 0}, {-1, -1}) - eq(1, table.getn(rv)) - - -- Set more marks for testing the ranges - set_extmark(ns, marks[2], positions[2][1], positions[2][2]) - set_extmark(ns, marks[3], positions[3][1], positions[3][2]) - set_extmark(ns2, marks[2], positions[2][1], positions[2][2]) - set_extmark(ns2, marks[3], positions[3][1], positions[3][2]) - - -- get_next (limit set) - rv = get_extmarks(ns, {0, 0}, positions[2], {limit=1}) - eq(1, table.getn(rv)) - rv = get_extmarks(ns2, {0, 0}, positions[2], {limit=1}) - eq(1, table.getn(rv)) - -- get_prev (limit set) - rv = get_extmarks(ns, positions[1], {0, 0}, {limit=1}) - eq(1, table.getn(rv)) - rv = get_extmarks(ns2, positions[1], {0, 0}, {limit=1}) - eq(1, table.getn(rv)) - - -- get_next (no limit) - rv = get_extmarks(ns, positions[1], positions[2]) - eq(2, table.getn(rv)) - rv = get_extmarks(ns2, positions[1], positions[2]) - eq(2, table.getn(rv)) - -- get_prev (no limit) - rv = get_extmarks(ns, positions[2], positions[1]) - eq(2, table.getn(rv)) - rv = get_extmarks(ns2, positions[2], positions[1]) - eq(2, table.getn(rv)) - - curbufmeths.del_extmark(ns, marks[1]) - rv = get_extmarks(ns, {0, 0}, {-1, -1}) - eq(2, table.getn(rv)) - curbufmeths.del_extmark(ns2, marks[1]) - rv = get_extmarks(ns2, {0, 0}, {-1, -1}) - eq(2, table.getn(rv)) - end) - - it('mark set can create unique identifiers', function() - -- create mark with id 1 - eq(1, set_extmark(ns, 1, positions[1][1], positions[1][2])) - -- ask for unique id, it should be the next one, i e 2 - eq(2, set_extmark(ns, 0, positions[1][1], positions[1][2])) - eq(3, set_extmark(ns, 3, positions[2][1], positions[2][2])) - eq(4, set_extmark(ns, 0, positions[1][1], positions[1][2])) - - -- mixing manual and allocated id:s are not recommened, but it should - -- do something reasonable - eq(6, set_extmark(ns, 6, positions[2][1], positions[2][2])) - eq(7, set_extmark(ns, 0, positions[1][1], positions[1][2])) - eq(8, set_extmark(ns, 0, positions[1][1], positions[1][2])) - end) - - it('auto indenting with enter works', function() - -- op_reindent in ops.c - feed(':set cindent') - feed(':set autoindent') - feed(':set shiftwidth=2') - feed("0iint A {1M1b") - -- Set the mark on the M, should move.. - set_extmark(ns, marks[1], 0, 12) - -- Set the mark before the cursor, should stay there - set_extmark(ns, marks[2], 0, 10) - feed("i") - local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({1, 3}, rv) - rv = curbufmeths.get_extmark_by_id(ns, marks[2]) - eq({0, 10}, rv) - check_undo_redo(ns, marks[1], 0, 12, 1, 3) - end) - - it('auto indenting entire line works', function() - feed(':set cindent') - feed(':set autoindent') - feed(':set shiftwidth=2') - -- will force an indent of 2 - feed("0iint A {0i1M1") - set_extmark(ns, marks[1], 1, 1) - feed("0i") - local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({1, 3}, rv) - check_undo_redo(ns, marks[1], 1, 1, 1, 3) - -- now check when cursor at eol - feed("uA") - rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({1, 3}, rv) - end) - - it('removing auto indenting with works', function() - feed(':set cindent') - feed(':set autoindent') - feed(':set shiftwidth=2') - feed("0i") - set_extmark(ns, marks[1], 0, 3) - feed("bi") - local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({0, 1}, rv) - check_undo_redo(ns, marks[1], 0, 3, 0, 1) - -- check when cursor at eol - feed("uA") - rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({0, 1}, rv) - end) - - it('indenting multiple lines with = works', function() - feed(':set cindent') - feed(':set autoindent') - feed(':set shiftwidth=2') - feed("0iint A {1M12M2") - set_extmark(ns, marks[1], 1, 1) - set_extmark(ns, marks[2], 2, 1) - feed('=gg') - check_undo_redo(ns, marks[1], 1, 1, 1, 3) - check_undo_redo(ns, marks[2], 2, 1, 2, 5) - end) - - it('substitutes by deleting inside the replace matches', function() - -- do_sub in ex_cmds.c - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - feed(':s/34/xx') - check_undo_redo(ns, marks[1], 0, 2, 0, 4) - check_undo_redo(ns, marks[2], 0, 3, 0, 4) - end) - - it('substitutes when insert text > deleted', function() - -- do_sub in ex_cmds.c - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - feed(':s/34/xxx') - check_undo_redo(ns, marks[1], 0, 2, 0, 5) - check_undo_redo(ns, marks[2], 0, 3, 0, 5) - end) - - it('substitutes when marks around eol', function() - -- do_sub in ex_cmds.c - set_extmark(ns, marks[1], 0, 4) - set_extmark(ns, marks[2], 0, 5) - feed(':s/5/xxx') - check_undo_redo(ns, marks[1], 0, 4, 0, 7) - check_undo_redo(ns, marks[2], 0, 5, 0, 7) - end) - - it('substitutes over range insert text > deleted', function() - -- do_sub in ex_cmds.c - feed('Ax34xx') - feed('Axxx34') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 1, 1) - set_extmark(ns, marks[3], 2, 4) - feed(':1,3s/34/xxx') - check_undo_redo(ns, marks[1], 0, 2, 0, 5) - check_undo_redo(ns, marks[2], 1, 1, 1, 4) - check_undo_redo(ns, marks[3], 2, 4, 2, 6) - end) - - it('substitutes multiple matches in a line', function() - -- do_sub in ex_cmds.c - feed('ddi3x3x3') - set_extmark(ns, marks[1], 0, 0) - set_extmark(ns, marks[2], 0, 2) - set_extmark(ns, marks[3], 0, 4) - feed(':s/3/yy/g') - check_undo_redo(ns, marks[1], 0, 0, 0, 2) - check_undo_redo(ns, marks[2], 0, 2, 0, 5) - check_undo_redo(ns, marks[3], 0, 4, 0, 8) - end) - - it('substitions over multiple lines with newline in pattern', function() - feed('A67890xx') - set_extmark(ns, marks[1], 0, 3) - set_extmark(ns, marks[2], 0, 4) - set_extmark(ns, marks[3], 1, 0) - set_extmark(ns, marks[4], 1, 5) - set_extmark(ns, marks[5], 2, 0) - feed([[:1,2s:5\n:5 ]]) - check_undo_redo(ns, marks[1], 0, 3, 0, 3) - check_undo_redo(ns, marks[2], 0, 4, 0, 6) - check_undo_redo(ns, marks[3], 1, 0, 0, 6) - check_undo_redo(ns, marks[4], 1, 5, 0, 11) - check_undo_redo(ns, marks[5], 2, 0, 1, 0) - end) - - it('inserting', function() - feed('A67890xx') - set_extmark(ns, marks[1], 0, 3) - set_extmark(ns, marks[2], 0, 4) - set_extmark(ns, marks[3], 1, 0) - set_extmark(ns, marks[4], 1, 5) - set_extmark(ns, marks[5], 2, 0) - set_extmark(ns, marks[6], 1, 2) - feed([[:1,2s:5\n67:X]]) - check_undo_redo(ns, marks[1], 0, 3, 0, 3) - check_undo_redo(ns, marks[2], 0, 4, 0, 5) - check_undo_redo(ns, marks[3], 1, 0, 0, 5) - check_undo_redo(ns, marks[4], 1, 5, 0, 8) - check_undo_redo(ns, marks[5], 2, 0, 1, 0) - check_undo_redo(ns, marks[6], 1, 2, 0, 5) - end) - - it('substitions with multiple newlines in pattern', function() - feed('A67890xx') - set_extmark(ns, marks[1], 0, 4) - set_extmark(ns, marks[2], 0, 5) - set_extmark(ns, marks[3], 1, 0) - set_extmark(ns, marks[4], 1, 5) - set_extmark(ns, marks[5], 2, 0) - feed([[:1,2s:\n.*\n:X]]) - check_undo_redo(ns, marks[1], 0, 4, 0, 4) - check_undo_redo(ns, marks[2], 0, 5, 0, 6) - check_undo_redo(ns, marks[3], 1, 0, 0, 6) - check_undo_redo(ns, marks[4], 1, 5, 0, 6) - check_undo_redo(ns, marks[5], 2, 0, 0, 6) - end) - - it('substitions over multiple lines with replace in substition', function() - feed('A67890xx') - set_extmark(ns, marks[1], 0, 1) - set_extmark(ns, marks[2], 0, 2) - set_extmark(ns, marks[3], 0, 4) - set_extmark(ns, marks[4], 1, 0) - set_extmark(ns, marks[5], 2, 0) - feed([[:1,2s:3:\r]]) - check_undo_redo(ns, marks[1], 0, 1, 0, 1) - check_undo_redo(ns, marks[2], 0, 2, 1, 0) - check_undo_redo(ns, marks[3], 0, 4, 1, 1) - check_undo_redo(ns, marks[4], 1, 0, 2, 0) - check_undo_redo(ns, marks[5], 2, 0, 3, 0) - feed('u') - feed([[:1,2s:3:\rxx]]) - eq({1, 3}, curbufmeths.get_extmark_by_id(ns, marks[3])) - end) - - it('substitions over multiple lines with replace in substition', function() - feed('Ax3xx') - set_extmark(ns, marks[1], 1, 0) - set_extmark(ns, marks[2], 1, 1) - set_extmark(ns, marks[3], 1, 2) - feed([[:2,2s:3:\r]]) - check_undo_redo(ns, marks[1], 1, 0, 1, 0) - check_undo_redo(ns, marks[2], 1, 1, 2, 0) - check_undo_redo(ns, marks[3], 1, 2, 2, 0) - end) - - it('substitions over multiple lines with replace in substition', function() - feed('Ax3xx') - set_extmark(ns, marks[1], 0, 1) - set_extmark(ns, marks[2], 0, 2) - set_extmark(ns, marks[3], 0, 4) - set_extmark(ns, marks[4], 1, 1) - set_extmark(ns, marks[5], 2, 0) - feed([[:1,2s:3:\r]]) - check_undo_redo(ns, marks[1], 0, 1, 0, 1) - check_undo_redo(ns, marks[2], 0, 2, 1, 0) - check_undo_redo(ns, marks[3], 0, 4, 1, 1) - check_undo_redo(ns, marks[4], 1, 1, 3, 0) - check_undo_redo(ns, marks[5], 2, 0, 4, 0) - feed('u') - feed([[:1,2s:3:\rxx]]) - check_undo_redo(ns, marks[3], 0, 4, 1, 3) - end) - - it('substitions with newline in match and sub, delta is 0', function() - feed('A67890xx') - set_extmark(ns, marks[1], 0, 3) - set_extmark(ns, marks[2], 0, 4) - set_extmark(ns, marks[3], 0, 5) - set_extmark(ns, marks[4], 1, 0) - set_extmark(ns, marks[5], 1, 5) - set_extmark(ns, marks[6], 2, 0) - feed([[:1,1s:5\n:\r]]) - check_undo_redo(ns, marks[1], 0, 3, 0, 3) - check_undo_redo(ns, marks[2], 0, 4, 1, 0) - check_undo_redo(ns, marks[3], 0, 5, 1, 0) - check_undo_redo(ns, marks[4], 1, 0, 1, 0) - check_undo_redo(ns, marks[5], 1, 5, 1, 5) - check_undo_redo(ns, marks[6], 2, 0, 2, 0) - end) - - it('substitions with newline in match and sub, delta > 0', function() - feed('A67890xx') - set_extmark(ns, marks[1], 0, 3) - set_extmark(ns, marks[2], 0, 4) - set_extmark(ns, marks[3], 0, 5) - set_extmark(ns, marks[4], 1, 0) - set_extmark(ns, marks[5], 1, 5) - set_extmark(ns, marks[6], 2, 0) - feed([[:1,1s:5\n:\r\r]]) - check_undo_redo(ns, marks[1], 0, 3, 0, 3) - check_undo_redo(ns, marks[2], 0, 4, 2, 0) - check_undo_redo(ns, marks[3], 0, 5, 2, 0) - check_undo_redo(ns, marks[4], 1, 0, 2, 0) - check_undo_redo(ns, marks[5], 1, 5, 2, 5) - check_undo_redo(ns, marks[6], 2, 0, 3, 0) - end) - - it('substitions with newline in match and sub, delta < 0', function() - feed('A67890xxxx') - set_extmark(ns, marks[1], 0, 3) - set_extmark(ns, marks[2], 0, 4) - set_extmark(ns, marks[3], 0, 5) - set_extmark(ns, marks[4], 1, 0) - set_extmark(ns, marks[5], 1, 5) - set_extmark(ns, marks[6], 2, 1) - set_extmark(ns, marks[7], 3, 0) - feed([[:1,2s:5\n.*\n:\r]]) - check_undo_redo(ns, marks[1], 0, 3, 0, 3) - check_undo_redo(ns, marks[2], 0, 4, 1, 0) - check_undo_redo(ns, marks[3], 0, 5, 1, 0) - check_undo_redo(ns, marks[4], 1, 0, 1, 0) - check_undo_redo(ns, marks[5], 1, 5, 1, 0) - check_undo_redo(ns, marks[6], 2, 1, 1, 1) - check_undo_redo(ns, marks[7], 3, 0, 2, 0) - end) - - it('substitions with backrefs, newline inserted into sub', function() - feed('A67890xxxx') - set_extmark(ns, marks[1], 0, 3) - set_extmark(ns, marks[2], 0, 4) - set_extmark(ns, marks[3], 0, 5) - set_extmark(ns, marks[4], 1, 0) - set_extmark(ns, marks[5], 1, 5) - set_extmark(ns, marks[6], 2, 0) - feed([[:1,1s:5\(\n\):\0\1]]) - check_undo_redo(ns, marks[1], 0, 3, 0, 3) - check_undo_redo(ns, marks[2], 0, 4, 2, 0) - check_undo_redo(ns, marks[3], 0, 5, 2, 0) - check_undo_redo(ns, marks[4], 1, 0, 2, 0) - check_undo_redo(ns, marks[5], 1, 5, 2, 5) - check_undo_redo(ns, marks[6], 2, 0, 3, 0) - end) - - it('substitions a ^', function() - set_extmark(ns, marks[1], 0, 0) - set_extmark(ns, marks[2], 0, 1) - feed([[:s:^:x]]) - check_undo_redo(ns, marks[1], 0, 0, 0, 1) - check_undo_redo(ns, marks[2], 0, 1, 0, 2) - end) - - it('using without increase in order of magnitude', function() - -- do_addsub in ops.c - feed('ddiabc998xxxTc') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 0, 5) - set_extmark(ns, marks[4], 0, 6) - set_extmark(ns, marks[5], 0, 7) - feed('') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - check_undo_redo(ns, marks[2], 0, 3, 0, 6) - check_undo_redo(ns, marks[3], 0, 5, 0, 6) - check_undo_redo(ns, marks[4], 0, 6, 0, 6) - check_undo_redo(ns, marks[5], 0, 7, 0, 7) - end) - - it('using when increase in order of magnitude', function() - -- do_addsub in ops.c - feed('ddiabc999xxxTc') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 0, 5) - set_extmark(ns, marks[4], 0, 6) - set_extmark(ns, marks[5], 0, 7) - feed('') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - check_undo_redo(ns, marks[2], 0, 3, 0, 7) - check_undo_redo(ns, marks[3], 0, 5, 0, 7) - check_undo_redo(ns, marks[4], 0, 6, 0, 7) - check_undo_redo(ns, marks[5], 0, 7, 0, 8) - end) - - it('using when negative and without decrease in order of magnitude', function() - feed('ddiabc-999xxxT-') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 0, 6) - set_extmark(ns, marks[4], 0, 7) - set_extmark(ns, marks[5], 0, 8) - feed('') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - check_undo_redo(ns, marks[2], 0, 3, 0, 7) - check_undo_redo(ns, marks[3], 0, 6, 0, 7) - check_undo_redo(ns, marks[4], 0, 7, 0, 7) - check_undo_redo(ns, marks[5], 0, 8, 0, 8) - end) - - it('using when negative and decrease in order of magnitude', function() - feed('ddiabc-1000xxxT-') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 0, 7) - set_extmark(ns, marks[4], 0, 8) - set_extmark(ns, marks[5], 0, 9) - feed('') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - check_undo_redo(ns, marks[2], 0, 3, 0, 7) - check_undo_redo(ns, marks[3], 0, 7, 0, 7) - check_undo_redo(ns, marks[4], 0, 8, 0, 7) - check_undo_redo(ns, marks[5], 0, 9, 0, 8) - end) - - it('using without decrease in order of magnitude', function() - -- do_addsub in ops.c - feed('ddiabc999xxxTc') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 0, 5) - set_extmark(ns, marks[4], 0, 6) - set_extmark(ns, marks[5], 0, 7) - feed('') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - check_undo_redo(ns, marks[2], 0, 3, 0, 6) - check_undo_redo(ns, marks[3], 0, 5, 0, 6) - check_undo_redo(ns, marks[4], 0, 6, 0, 6) - check_undo_redo(ns, marks[5], 0, 7, 0, 7) - end) - - it('using when decrease in order of magnitude', function() - -- do_addsub in ops.c - feed('ddiabc1000xxxTc') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 0, 6) - set_extmark(ns, marks[4], 0, 7) - set_extmark(ns, marks[5], 0, 8) - feed('') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - check_undo_redo(ns, marks[2], 0, 3, 0, 6) - check_undo_redo(ns, marks[3], 0, 6, 0, 6) - check_undo_redo(ns, marks[4], 0, 7, 0, 6) - check_undo_redo(ns, marks[5], 0, 8, 0, 7) - end) - - it('using when negative and without increase in order of magnitude', function() - feed('ddiabc-998xxxT-') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 0, 6) - set_extmark(ns, marks[4], 0, 7) - set_extmark(ns, marks[5], 0, 8) - feed('') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - check_undo_redo(ns, marks[2], 0, 3, 0, 7) - check_undo_redo(ns, marks[3], 0, 6, 0, 7) - check_undo_redo(ns, marks[4], 0, 7, 0, 7) - check_undo_redo(ns, marks[5], 0, 8, 0, 8) - end) - - it('using when negative and increase in order of magnitude', function() - feed('ddiabc-999xxxT-') - set_extmark(ns, marks[1], 0, 2) - set_extmark(ns, marks[2], 0, 3) - set_extmark(ns, marks[3], 0, 6) - set_extmark(ns, marks[4], 0, 7) - set_extmark(ns, marks[5], 0, 8) - feed('') - check_undo_redo(ns, marks[1], 0, 2, 0, 2) - check_undo_redo(ns, marks[2], 0, 3, 0, 8) - check_undo_redo(ns, marks[3], 0, 6, 0, 8) - check_undo_redo(ns, marks[4], 0, 7, 0, 8) - check_undo_redo(ns, marks[5], 0, 8, 0, 9) - end) - - it('throws consistent error codes', function() - local ns_invalid = ns2 + 1 - eq("Invalid ns_id", pcall_err(set_extmark, ns_invalid, marks[1], positions[1][1], positions[1][2])) - eq("Invalid ns_id", pcall_err(curbufmeths.del_extmark, ns_invalid, marks[1])) - eq("Invalid ns_id", pcall_err(get_extmarks, ns_invalid, positions[1], positions[2])) - eq("Invalid ns_id", pcall_err(curbufmeths.get_extmark_by_id, ns_invalid, marks[1])) - end) - - it('when col = line-length, set the mark on eol', function() - set_extmark(ns, marks[1], 0, -1) - local rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({0, init_text:len()}, rv) - -- Test another - set_extmark(ns, marks[1], 0, -1) - rv = curbufmeths.get_extmark_by_id(ns, marks[1]) - eq({0, init_text:len()}, rv) - end) - - it('when col = line-length, set the mark on eol', function() - local invalid_col = init_text:len() + 1 - eq("col value outside range", pcall_err(set_extmark, ns, marks[1], 0, invalid_col)) - end) - - it('fails when line > line_count', function() - local invalid_col = init_text:len() + 1 - local invalid_lnum = 3 - eq('line value outside range', pcall_err(set_extmark, ns, marks[1], invalid_lnum, invalid_col)) - eq({}, curbufmeths.get_extmark_by_id(ns, marks[1])) - end) - - it('bug from check_col in extmark_set', function() - -- This bug was caused by extmark_set always using check_col. check_col - -- always uses the current buffer. This wasn't working during undo so we - -- now use check_col and check_lnum only when they are required. - feed('A67890xx') - feed('A1234567890xx') - set_extmark(ns, marks[1], 3, 4) - feed([[:1,5s:5\n:5 ]]) - check_undo_redo(ns, marks[1], 3, 4, 2, 6) - end) - - it('in read-only buffer', function() - command("view! runtime/doc/help.txt") - eq(true, curbufmeths.get_option('ro')) - local id = set_extmark(ns, 0, 0, 2) - eq({{id, 0, 2}}, get_extmarks(ns,0, -1)) - end) - - it('can set a mark to other buffer', function() - local buf = request('nvim_create_buf', 0, 1) - request('nvim_buf_set_lines', buf, 0, -1, 1, {"", ""}) - local id = bufmeths.set_extmark(buf, ns, 0, 1, 0, {}) - eq({{id, 1, 0}}, bufmeths.get_extmarks(buf, ns, 0, -1, {})) - end) - - it('does not crash with append/delete/undo seqence', function() - meths.exec([[ - let ns = nvim_create_namespace('myplugin') - call nvim_buf_set_extmark(0, ns, 0, 0, 0, {}) - call append(0, '') - %delete - undo]],false) - eq(2, meths.eval('1+1')) -- did not crash - end) -end) - -describe('Extmarks buffer api with many marks', function() - local ns1 - local ns2 - local ns_marks = {} - before_each(function() - clear() - ns1 = request('nvim_create_namespace', "ns1") - ns2 = request('nvim_create_namespace', "ns2") - ns_marks = {[ns1]={}, [ns2]={}} - local lines = {} - for i = 1,30 do - lines[#lines+1] = string.rep("x ",i) - end - curbufmeths.set_lines(0, -1, true, lines) - local ns = ns1 - local q = 0 - for i = 0,29 do - for j = 0,i do - local id = set_extmark(ns,0, i,j) - eq(nil, ns_marks[ns][id]) - ok(id > 0) - ns_marks[ns][id] = {i,j} - ns = ns1+ns2-ns - q = q + 1 - end - end - eq(233, #ns_marks[ns1]) - eq(232, #ns_marks[ns2]) - - end) - - local function get_marks(ns) - local mark_list = get_extmarks(ns, 0, -1) - local marks = {} - for _, mark in ipairs(mark_list) do - local id, row, col = unpack(mark) - eq(nil, marks[id], "duplicate mark") - marks[id] = {row,col} - end - return marks - end - - it("can get marks", function() - eq(ns_marks[ns1], get_marks(ns1)) - eq(ns_marks[ns2], get_marks(ns2)) - end) - - it("can clear all marks in ns", function() - curbufmeths.clear_namespace(ns1, 0, -1) - eq({}, get_marks(ns1)) - eq(ns_marks[ns2], get_marks(ns2)) - curbufmeths.clear_namespace(ns2, 0, -1) - eq({}, get_marks(ns1)) - eq({}, get_marks(ns2)) - end) - - it("can clear line range", function() - curbufmeths.clear_namespace(ns1, 10, 20) - for id, mark in pairs(ns_marks[ns1]) do - if 10 <= mark[1] and mark[1] < 20 then - ns_marks[ns1][id] = nil - end - end - eq(ns_marks[ns1], get_marks(ns1)) - eq(ns_marks[ns2], get_marks(ns2)) - end) - - it("can delete line", function() - feed('10Gdd') - for _, marks in pairs(ns_marks) do - for id, mark in pairs(marks) do - if mark[1] == 9 then - marks[id] = {9,0} - elseif mark[1] >= 10 then - mark[1] = mark[1] - 1 - end - end - end - eq(ns_marks[ns1], get_marks(ns1)) - eq(ns_marks[ns2], get_marks(ns2)) - end) - - it("can delete lines", function() - feed('10G10dd') - for _, marks in pairs(ns_marks) do - for id, mark in pairs(marks) do - if 9 <= mark[1] and mark[1] < 19 then - marks[id] = {9,0} - elseif mark[1] >= 19 then - mark[1] = mark[1] - 10 - end - end - end - eq(ns_marks[ns1], get_marks(ns1)) - eq(ns_marks[ns2], get_marks(ns2)) - end) - - it("can wipe buffer", function() - command('bwipe!') - eq({}, get_marks(ns1)) - eq({}, get_marks(ns2)) - end) -end) -- cgit