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 exec = helpers.exec local meths = helpers.meths local assert_alive = helpers.assert_alive local function expect(contents) return eq(contents, helpers.curbuf_contents()) end local function set_extmark(ns_id, id, line, col, opts) if opts == nil then opts = {} end if id ~= nil and id ~= 0 then opts.id = id end return curbufmeths.set_extmark(ns_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 get_extmark_by_id(ns_id, id, opts) if opts == nil then opts = {} end return curbufmeths.get_extmark_by_id(ns_id, id, opts) end local function check_undo_redo(ns, mark, sr, sc, er, ec) --s = start, e = end local rv = get_extmark_by_id(ns, mark) eq({er, ec}, rv) feed("u") rv = get_extmark_by_id(ns, mark) eq({sr, sc}, rv) feed("") rv = get_extmark_by_id(ns, mark) eq({er, ec}, rv) 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('validation', function() eq("Invalid 'end_col': expected Integer, got Array", pcall_err(set_extmark, ns, marks[2], 0, 0, { end_col = {}, end_row = 1 })) eq("Invalid 'end_row': expected Integer, got Array", pcall_err(set_extmark, ns, marks[2], 0, 0, { end_col = 1, end_row = {} })) eq("Invalid 'virt_text_pos': expected String, got Integer", pcall_err(set_extmark, ns, marks[2], 0, 0, { virt_text_pos = 0 })) eq("Invalid 'virt_text_pos': 'foo'", pcall_err(set_extmark, ns, marks[2], 0, 0, { virt_text_pos = 'foo' })) eq("Invalid 'hl_mode': expected String, got Integer", pcall_err(set_extmark, ns, marks[2], 0, 0, { hl_mode = 0 })) eq("Invalid 'hl_mode': 'foo'", pcall_err(set_extmark, ns, marks[2], 0, 0, { hl_mode = 'foo' })) eq("Invalid 'id': expected Integer, got Array", pcall_err(set_extmark, ns, {}, 0, 0, { end_col = 1, end_row = 1 })) eq("Invalid mark position: expected 2 Integer items", pcall_err(get_extmarks, ns, {}, {-1, -1})) eq("Invalid mark position: expected mark id Integer or 2-item Array", pcall_err(get_extmarks, ns, true, {-1, -1})) -- No memory leak with virt_text, virt_lines, sign_text eq("right_gravity is not a boolean", pcall_err(set_extmark, ns, marks[2], 0, 0, { virt_text = {{'foo', 'Normal'}}, virt_lines = {{{'bar', 'Normal'}}}, sign_text = 'a', right_gravity = 'baz', })) end) it("can end extranges past final newline using end_col = 0", function() set_extmark(ns, marks[1], 0, 0, { end_col = 0, end_row = 1 }) eq("Invalid 'end_col': out of range", pcall_err(set_extmark, ns, marks[2], 0, 0, { end_col = 1, end_row = 1 })) end) it("can end extranges past final newline when strict mode is false", function() set_extmark(ns, marks[1], 0, 0, { end_col = 1, end_row = 1, strict = false, }) end) it("can end extranges past final column when strict mode is false", function() set_extmark(ns, marks[1], 0, 0, { end_col = 6, end_row = 0, strict = false, }) 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 = 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 = 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 = 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('can undo with extmarks (#25147)', function() feed('itest') set_extmark(ns, 1, 0, 0) set_extmark(ns, 2, 1, 0) eq({ { 1, 0, 0 }, { 2, 1, 0 } }, get_extmarks(ns, {0, 0}, {-1, -1})) feed('dd') eq({ { 1, 1, 0 }, { 2, 1, 0 } }, get_extmarks(ns, {0, 0}, {-1, -1})) curbufmeths.clear_namespace(ns, 0, -1) eq({}, get_extmarks(ns, {0, 0}, {-1, -1})) set_extmark(ns, 1, 0, 0, { right_gravity = false }) set_extmark(ns, 2, 1, 0, { right_gravity = false }) eq({ { 1, 0, 0 }, { 2, 1, 0 } }, get_extmarks(ns, {0, 0}, {-1, -1})) feed('u') eq({ { 1, 0, 0 }, { 2, 1, 0 } }, get_extmarks(ns, {0, 0}, {-1, -1})) curbufmeths.clear_namespace(ns, 0, -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, #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, #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, #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, #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, #rv) rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=2}) eq(2, #rv) rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=3}) eq(3, #rv) -- now in reverse rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=1}) eq(1, #rv) rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=2}) eq(2, #rv) rv = get_extmarks(ns, {0, 0}, {-1, -1}, {limit=3}) eq(3, #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 change.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 = 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 change.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 change.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) it('can get overlapping extmarks', function() set_extmark(ns, 1, 0, 0, {end_row = 5, end_col=0}) set_extmark(ns, 2, 2, 5, {end_row = 2, end_col=30}) set_extmark(ns, 3, 0, 5, {end_row = 2, end_col=10}) set_extmark(ns, 4, 0, 0, {end_row = 1, end_col=0}) eq({{ 2, 2, 5 }}, get_extmarks(ns, {2, 0}, {2, -1}, { overlap=false })) eq({{ 1, 0, 0 }, { 3, 0, 5}, {2, 2, 5}}, get_extmarks(ns, {2, 0}, {2, -1}, { overlap=true })) end) 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, #rv) feed("") rv = get_extmarks(ns, {0, 0}, {-1, -1}) eq(3, #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, #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, #rv) feed("") rv = get_extmarks(ns, {0, 0}, {-1, -1}) eq(2, #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, #rv) rv = get_extmarks(ns2, {0, 0}, {-1, -1}) eq(1, #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, #rv) rv = get_extmarks(ns2, {0, 0}, positions[2], {limit=1}) eq(1, #rv) -- get_prev (limit set) rv = get_extmarks(ns, positions[1], {0, 0}, {limit=1}) eq(1, #rv) rv = get_extmarks(ns2, positions[1], {0, 0}, {limit=1}) eq(1, #rv) -- get_next (no limit) rv = get_extmarks(ns, positions[1], positions[2]) eq(2, #rv) rv = get_extmarks(ns2, positions[1], positions[2]) eq(2, #rv) -- get_prev (no limit) rv = get_extmarks(ns, positions[2], positions[1]) eq(2, #rv) rv = get_extmarks(ns2, positions[2], positions[1]) eq(2, #rv) curbufmeths.del_extmark(ns, marks[1]) rv = get_extmarks(ns, {0, 0}, {-1, -1}) eq(2, #rv) curbufmeths.del_extmark(ns2, marks[1]) rv = get_extmarks(ns2, {0, 0}, {-1, -1}) eq(2, #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 recommended, 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 = get_extmark_by_id(ns, marks[1]) eq({1, 3}, rv) rv = 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 = 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 = 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 = 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 = 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('substitutes 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('substitutes 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('substitutes over multiple lines with replace in substitution', 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}, get_extmark_by_id(ns, marks[3])) end) it('substitutes over multiple lines with replace in substitution', 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('substitutes over multiple lines with replace in substitution', 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('substitutes 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('substitutes 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('substitutes 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('substitutes 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('substitutes 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': 3", pcall_err(set_extmark, ns_invalid, marks[1], positions[1][1], positions[1][2])) eq("Invalid 'ns_id': 3", pcall_err(curbufmeths.del_extmark, ns_invalid, marks[1])) eq("Invalid 'ns_id': 3", pcall_err(get_extmarks, ns_invalid, positions[1], positions[2])) eq("Invalid 'ns_id': 3", pcall_err(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 = get_extmark_by_id(ns, marks[1]) eq({0, init_text:len()}, rv) -- Test another set_extmark(ns, marks[1], 0, -1) rv = 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("Invalid 'col': out of 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("Invalid 'line': out of range", pcall_err(set_extmark, ns, marks[1], invalid_lnum, invalid_col)) eq({}, 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, meths.get_option_value('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, 1, 0, {}) eq({{id, 1, 0}}, bufmeths.get_extmarks(buf, ns, 0, -1, {})) end) it('does not crash with append/delete/undo sequence', function() exec([[ let ns = nvim_create_namespace('myplugin') call nvim_buf_set_extmark(0, ns, 0, 0, {}) call append(0, '') %delete undo]]) assert_alive() end) it('works with left and right gravity', function() -- right gravity should move with inserted text, while -- left gravity should stay in place. curbufmeths.set_extmark(ns, 0, 5, {right_gravity = false}) curbufmeths.set_extmark(ns, 0, 5, {right_gravity = true}) feed([[Aasdfasdf]]) eq({ {1, 0, 5}, {2, 0, 13} }, curbufmeths.get_extmarks(ns, 0, -1, {})) -- but both move when text is inserted before feed([[Iasdf]]) -- eq({}, curbufmeths.get_lines(0, -1, true)) eq({ {1, 0, 9}, {2, 0, 17} }, curbufmeths.get_extmarks(ns, 0, -1, {})) -- clear text curbufmeths.set_text(0, 0, 0, 17, {}) -- handles set_text correctly as well eq({ {1, 0, 0}, {2, 0, 0} }, meths.buf_get_extmarks(0, ns, 0, -1, {})) curbufmeths.set_text(0, 0, 0, 0, {'asdfasdf'}) eq({ {1, 0, 0}, {2, 0, 8} }, curbufmeths.get_extmarks(ns, 0, -1, {})) feed('u') -- handles pasting exec([[let @a='asdfasdf']]) feed([["ap]]) eq({ {1, 0, 0}, {2, 0, 8} }, meths.buf_get_extmarks(0, ns, 0, -1, {})) end) it('can accept "end_row" or "end_line" #16548', function() set_extmark(ns, marks[1], 0, 0, { end_col = 0, end_line = 1 }) eq({ {1, 0, 0, { ns_id = 1, end_col = 0, end_row = 1, right_gravity = true, end_right_gravity = false, }} }, get_extmarks(ns, 0, -1, {details=true})) end) it('in prompt buffer', function() feed('dd') local id = set_extmark(ns, marks[1], 0, 0, {}) meths.set_option_value('buftype', 'prompt', {}) feed('i') eq({{id, 0, 2}}, get_extmarks(ns, 0, -1)) end) it('can get details', function() set_extmark(ns, marks[1], 0, 0, { conceal = "c", cursorline_hl_group = "Statement", end_col = 0, end_right_gravity = true, end_row = 1, hl_eol = true, hl_group = "String", hl_mode = "blend", line_hl_group = "Statement", number_hl_group = "Statement", priority = 0, right_gravity = false, sign_hl_group = "Statement", sign_text = ">>", spell = true, virt_lines = { { { "lines", "Macro" }, { "???" } }, { { "stack", { "Type", "Search" } }, { "!!!" } }, }, virt_lines_above = true, virt_lines_leftcol = true, virt_text = { { "text", "Macro" }, { "???" }, { "stack", { "Type", "Search" } } }, virt_text_hide = true, virt_text_pos = "right_align", }) set_extmark(ns, marks[2], 0, 0, { priority = 0, virt_text = { { "", "Macro" }, { "", { "Type", "Search" } }, { "" } }, virt_text_win_col = 1, }) eq({0, 0, { conceal = "c", cursorline_hl_group = "Statement", end_col = 0, end_right_gravity = true, end_row = 1, hl_eol = true, hl_group = "String", hl_mode = "blend", line_hl_group = "Statement", ns_id = 1, number_hl_group = "Statement", priority = 0, right_gravity = false, sign_hl_group = "Statement", sign_text = ">>", spell = true, virt_lines = { { { "lines", "Macro" }, { "???" } }, { { "stack", { "Type", "Search" } }, { "!!!" } }, }, virt_lines_above = true, virt_lines_leftcol = true, virt_text = { { "text", "Macro" }, { "???" }, { "stack", { "Type", "Search" } } }, virt_text_hide = true, virt_text_pos = "right_align", } }, get_extmark_by_id(ns, marks[1], { details = true })) eq({0, 0, { ns_id = 1, right_gravity = true, priority = 0, virt_text = { { "", "Macro" }, { "", { "Type", "Search" } }, { "" } }, virt_text_hide = false, virt_text_pos = "win_col", virt_text_win_col = 1, } }, get_extmark_by_id(ns, marks[2], { details = true })) set_extmark(ns, marks[3], 0, 0, { cursorline_hl_group = "Statement" }) eq({0, 0, { ns_id = 1, cursorline_hl_group = "Statement", right_gravity = true, } }, get_extmark_by_id(ns, marks[3], { details = true })) end) it('can get marks from anonymous namespaces', function() ns = request('nvim_create_namespace', "") ns2 = request('nvim_create_namespace', "") set_extmark(ns, 1, 0, 0, {}) set_extmark(ns2, 2, 1, 0, {}) eq({{ 1, 0, 0, { ns_id = ns, right_gravity = true }}, { 2, 1, 0, { ns_id = ns2, right_gravity = true }}}, get_extmarks(-1, 0, -1, { details = true })) end) it('can filter by extmark properties', function() set_extmark(ns, 1, 0, 0, {}) set_extmark(ns, 2, 0, 0, { hl_group = 'Normal' }) set_extmark(ns, 3, 0, 0, { sign_text = '>>' }) set_extmark(ns, 4, 0, 0, { virt_text = {{'text', 'Normal'}}}) set_extmark(ns, 5, 0, 0, { virt_lines = {{{ 'line', 'Normal' }}}}) eq(5, #get_extmarks(-1, 0, -1, {})) eq({{ 2, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'highlight' })) eq({{ 3, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'sign' })) eq({{ 4, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_text' })) eq({{ 5, 0, 0 }}, get_extmarks(-1, 0, -1, { type = 'virt_lines' })) end) it("invalidated marks are deleted", function() screen = Screen.new(40, 6) screen:attach() feed('dd6iaaa bbb cccgg') set_extmark(ns, 1, 0, 0, { invalidate = true, sign_text = 'S1' }) set_extmark(ns, 2, 1, 0, { invalidate = true, sign_text = 'S2' }) -- mark with invalidate is removed command('d') screen:expect([[ S2^aaa bbb ccc | aaa bbb ccc | aaa bbb ccc | aaa bbb ccc | aaa bbb ccc | | ]]) -- mark is restored with undo_restore == true command('silent undo') screen:expect([[ S1^aaa bbb ccc | S2aaa bbb ccc | aaa bbb ccc | aaa bbb ccc | aaa bbb ccc | | ]]) -- mark is deleted with undo_restore == false set_extmark(ns, 1, 0, 0, { invalidate = true, undo_restore = false, sign_text = 'S1' }) set_extmark(ns, 2, 1, 0, { invalidate = true, undo_restore = false, sign_text = 'S2' }) command('1d 2') eq(0, #get_extmarks(-1, 0, -1, {})) -- mark is not removed when deleting bytes before the range set_extmark(ns, 3, 0, 4, { invalidate = true, undo_restore = false, hl_group = 'Error', end_col = 7 }) feed('dw') eq(3, get_extmark_by_id(ns, 3, { details = true })[3].end_col) -- mark is not removed when deleting bytes at the start of the range feed('x') eq(2, get_extmark_by_id(ns, 3, { details = true })[3].end_col) -- mark is not removed when deleting bytes from the end of the range feed('lx') eq(1, get_extmark_by_id(ns, 3, { details = true})[3].end_col) -- mark is not removed when deleting bytes beyond end of the range feed('x') eq(1, get_extmark_by_id(ns, 3, { details = true})[3].end_col) -- mark is removed when all bytes in the range are deleted feed('hx') eq({}, get_extmark_by_id(ns, 3, {})) -- multiline mark is not removed when start of its range is deleted set_extmark(ns, 4, 1, 4, { undo_restore = false, invalidate = true, hl_group = 'Error', end_col = 7, end_row = 3 }) feed('ddDdd') eq({0, 0}, get_extmark_by_id(ns, 4, {})) -- multiline mark is removed when entirety of its range is deleted feed('vj2ed') eq({}, get_extmark_by_id(ns, 4, {})) 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) describe('API/win_extmark', function() local screen local marks, line1, line2 local ns before_each(function() -- Initialize some namespaces and insert text into a buffer marks = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} line1 = "non ui-watched line" line2 = "ui-watched line" clear() insert(line1) feed("o") insert(line2) ns = request('nvim_create_namespace', "extmark-ui") end) it('sends and only sends ui-watched marks to ui', function() screen = Screen.new(20, 4) screen:attach() -- should send this set_extmark(ns, marks[1], 1, 0, { ui_watched = true }) -- should not send this set_extmark(ns, marks[2], 0, 0, { ui_watched = false }) screen:expect({ grid = [[ non ui-watched line | ui-watched lin^e | ~ | | ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { {id = 1000}, 1, 1, 1, 16 }, } }, }) end) it('sends multiple ui-watched marks to ui', function() screen = Screen.new(20, 4) screen:attach() -- should send all of these set_extmark(ns, marks[1], 1, 0, { ui_watched = true, virt_text_pos = "overlay" }) set_extmark(ns, marks[2], 1, 2, { ui_watched = true, virt_text_pos = "overlay" }) set_extmark(ns, marks[3], 1, 4, { ui_watched = true, virt_text_pos = "overlay" }) set_extmark(ns, marks[4], 1, 6, { ui_watched = true, virt_text_pos = "overlay" }) set_extmark(ns, marks[5], 1, 8, { ui_watched = true }) screen:expect({ grid = [[ non ui-watched line | ui-watched lin^e | ~ | | ]], extmarks = { [2] = { -- earlier notifications { {id = 1000}, 1, 1, 1, 0 }, { {id = 1000}, 1, 1, 1, 0 }, { {id = 1000}, 1, 2, 1, 2 }, { {id = 1000}, 1, 1, 1, 0 }, { {id = 1000}, 1, 2, 1, 2 }, { {id = 1000}, 1, 3, 1, 4 }, { {id = 1000}, 1, 1, 1, 0 }, { {id = 1000}, 1, 2, 1, 2 }, { {id = 1000}, 1, 3, 1, 4 }, { {id = 1000}, 1, 4, 1, 6 }, -- final -- overlay { {id = 1000}, 1, 1, 1, 0 }, { {id = 1000}, 1, 2, 1, 2 }, { {id = 1000}, 1, 3, 1, 4 }, { {id = 1000}, 1, 4, 1, 6 }, -- eol { {id = 1000}, 1, 5, 1, 16 }, } }, }) end) it('updates ui-watched marks', function() screen = Screen.new(20, 4) screen:attach() -- should send this set_extmark(ns, marks[1], 1, 0, { ui_watched = true }) -- should not send this set_extmark(ns, marks[2], 0, 0, { ui_watched = false }) -- make some changes insert(" update") screen:expect({ grid = [[ non ui-watched line | ui-watched linupdat^e| e | | ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { {id = 1000}, 1, 1, 1, 16 }, -- updated and wrapped to 3rd line { {id = 1000}, 1, 1, 2, 2 }, } } }) feed("") screen:expect({ grid = [[ ui-watched linupdat^e| e | ~ | | ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { {id = 1000}, 1, 1, 1, 16 }, -- updated and wrapped to 3rd line { {id = 1000}, 1, 1, 2, 2 }, -- scrolled up one line, should be handled by grid scroll } } }) end) it('sends ui-watched to splits', function() screen = Screen.new(20, 8) screen:attach({ext_multigrid=true}) -- should send this set_extmark(ns, marks[1], 1, 0, { ui_watched = true }) -- should not send this set_extmark(ns, marks[2], 0, 0, { ui_watched = false }) command('split') screen:expect({ grid = [[ ## grid 1 [4:--------------------]| [4:--------------------]| [4:--------------------]| [No Name] [+] | [2:--------------------]| [2:--------------------]| [No Name] [+] | [3:--------------------]| ## grid 2 non ui-watched line | ui-watched line | ## grid 3 | ## grid 4 non ui-watched line | ui-watched lin^e | ~ | ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { {id = 1000}, 1, 1, 1, 16 }, -- updated after split { {id = 1000}, 1, 1, 1, 16 }, }, [4] = { -- only after split { {id = 1001}, 1, 1, 1, 16 }, } } }) -- make some changes insert(" update") screen:expect({ grid = [[ ## grid 1 [4:--------------------]| [4:--------------------]| [4:--------------------]| [No Name] [+] | [2:--------------------]| [2:--------------------]| [No Name] [+] | [3:--------------------]| ## grid 2 non ui-watched line | ui-watched linupd@@@| ## grid 3 | ## grid 4 non ui-watched line | ui-watched linupdat^e| e | ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { {id = 1000}, 1, 1, 1, 16 }, -- updated after split { {id = 1000}, 1, 1, 1, 16 }, }, [4] = { { {id = 1001}, 1, 1, 1, 16 }, -- updated { {id = 1001}, 1, 1, 2, 2 }, } } }) end) end)