local t = require('test.testutil') local n = require('test.functional.testnvim')() local Screen = require('test.functional.ui.screen') local request = n.request local eq = t.eq local ok = t.ok local pcall_err = t.pcall_err local insert = n.insert local feed = n.feed local clear = n.clear local command = n.command local exec = n.exec local api = n.api local assert_alive = n.assert_alive local function expect(contents) return eq(contents, n.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 api.nvim_buf_set_extmark(0, ns_id, line, col, opts) end local function get_extmarks(ns_id, start, end_, opts) if opts == nil then opts = {} end return api.nvim_buf_get_extmarks(0, ns_id, start, end_, opts) end local function get_extmark_by_id(ns_id, id, opts) if opts == nil then opts = {} end return api.nvim_buf_get_extmark_by_id(0, 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, api.nvim_buf_del_extmark(0, ns, marks[1])) eq(false, api.nvim_buf_del_extmark(0, ns, marks[1])) eq(true, api.nvim_buf_del_extmark(0, ns, marks[2])) eq(false, api.nvim_buf_del_extmark(0, ns, marks[3])) eq(false, api.nvim_buf_del_extmark(0, 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') api.nvim_buf_clear_namespace(0, 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') api.nvim_buf_clear_namespace(0, -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 })) api.nvim_buf_clear_namespace(0, 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, 0, 0 } }, get_extmarks(ns, { 0, 0 }, { -1, -1 })) api.nvim_buf_clear_namespace(0, 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) feed('a1') feed('kJ') -- This shouldn't seg fault screen:expect([[ 12345^ 1 | {1:~ }|*8 | ]]) 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) set_extmark(ns, marks[1], 0, 3) feed('0') insert('abc') screen:expect([[ ab^c12345 | {1:~ }|*8 | ]]) 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_fast_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') api.nvim_buf_del_extmark(0, 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) api.nvim_buf_del_extmark(0, ns, marks[1]) rv = get_extmarks(ns, { 0, 0 }, { -1, -1 }) eq(2, #rv) api.nvim_buf_del_extmark(0, 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(api.nvim_buf_del_extmark, 0, 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, api.nvim_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 = api.nvim_buf_set_extmark(buf, ns, 1, 0, {}) eq({ { id, 1, 0 } }, api.nvim_buf_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. api.nvim_buf_set_extmark(0, ns, 0, 5, { right_gravity = false }) api.nvim_buf_set_extmark(0, ns, 0, 5, { right_gravity = true }) feed([[Aasdfasdf]]) eq({ { 1, 0, 5 }, { 2, 0, 13 } }, api.nvim_buf_get_extmarks(0, ns, 0, -1, {})) -- but both move when text is inserted before feed([[Iasdf]]) -- eq({}, api.nvim_buf_get_lines(0, 0, -1, true)) eq({ { 1, 0, 9 }, { 2, 0, 17 } }, api.nvim_buf_get_extmarks(0, ns, 0, -1, {})) -- clear text api.nvim_buf_set_text(0, 0, 0, 0, 17, {}) -- handles set_text correctly as well eq({ { 1, 0, 0 }, { 2, 0, 0 } }, api.nvim_buf_get_extmarks(0, ns, 0, -1, {})) api.nvim_buf_set_text(0, 0, 0, 0, 0, { 'asdfasdf' }) eq({ { 1, 0, 0 }, { 2, 0, 8 } }, api.nvim_buf_get_extmarks(0, ns, 0, -1, {})) feed('u') -- handles pasting exec([[let @a='asdfasdf']]) feed([["ap]]) eq({ { 1, 0, 0 }, { 2, 0, 8 } }, api.nvim_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, {}) api.nvim_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_repeat_linebreak = false, 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_repeat_linebreak = false, 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', priority = 4096, right_gravity = true, }, }, get_extmark_by_id(ns, marks[3], { details = true })) set_extmark(ns, marks[4], 0, 0, { end_col = 1, conceal = 'a', spell = true, }) eq({ 0, 0, { conceal = 'a', end_col = 1, end_right_gravity = false, end_row = 0, ns_id = 1, right_gravity = true, spell = true, }, }, get_extmark_by_id(ns, marks[4], { details = true })) set_extmark(ns, marks[5], 0, 0, { end_col = 1, spell = false, }) eq({ 0, 0, { end_col = 1, end_right_gravity = false, end_row = 0, ns_id = 1, right_gravity = true, spell = false, }, }, get_extmark_by_id(ns, marks[5], { details = true })) api.nvim_buf_clear_namespace(0, ns, 0, -1) -- legacy sign mark includes sign name command('sign define sign1 text=s1 texthl=Title linehl=LineNR numhl=Normal culhl=CursorLine') command('sign place 1 name=sign1 line=1') eq({ { 1, 0, 0, { cursorline_hl_group = 'CursorLine', invalidate = true, line_hl_group = 'LineNr', ns_id = 0, number_hl_group = 'Normal', priority = 10, right_gravity = true, sign_hl_group = 'Title', sign_name = 'sign1', sign_text = 's1', undo_restore = false, }, }, }, get_extmarks(-1, 0, -1, { 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) feed('dd6iaaa bbb cccgg') api.nvim_set_option_value('signcolumn', 'auto:2', {}) set_extmark(ns, 1, 0, 0, { invalidate = true, sign_text = 'S1', end_row = 1 }) set_extmark(ns, 2, 1, 0, { invalidate = true, sign_text = 'S2', end_row = 2 }) -- mark with invalidate is removed command('d2') screen:expect([[ {7:S2}^aaa bbb ccc | {7: }aaa bbb ccc |*3 {7: } | | ]]) -- mark is restored with undo_restore == true command('silent undo') screen:expect([[ {7:S1 }^aaa bbb ccc | {7:S2S1}aaa bbb ccc | {7:S2 }aaa bbb ccc | {7: }aaa bbb ccc |*2 | ]]) -- decor is not removed twice command('d3') api.nvim_buf_del_extmark(0, ns, 1) command('silent undo') -- 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 = true, hl_group = 'Error', end_col = 7, right_gravity = false, }) 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(true, get_extmark_by_id(ns, 3, { details = true })[3].invalid) -- mark is restored with undo_restore == true if pos did not change command('undo') eq(nil, get_extmark_by_id(ns, 3, { details = true })[3].invalid) -- 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) it('can set a URL', function() local url1 = 'https://example.com' local url2 = 'http://127.0.0.1' set_extmark(ns, 1, 0, 0, { url = url1, end_col = 3 }) set_extmark(ns, 2, 0, 3, { url = url2, hl_group = 'Search', end_col = 5 }) local extmarks = get_extmarks(ns, 0, -1, { details = true }) eq(2, #extmarks) eq(url1, extmarks[1][4].url) eq(url2, extmarks[2][4].url) eq('Search', extmarks[2][4].hl_group) end) it('respects priority', function() screen = Screen.new(15, 10) set_extmark(ns, marks[1], 0, 0, { hl_group = 'Comment', end_col = 2, priority = 20, }) -- Extmark defined after first extmark but has lower priority, first extmark "wins" set_extmark(ns, marks[2], 0, 0, { hl_group = 'String', end_col = 2, priority = 10, }) screen:expect { grid = [[ {1:12}34^5 | {2:~ }|*8 | ]], attr_ids = { [1] = { foreground = Screen.colors.Blue1 }, [2] = { foreground = Screen.colors.Blue1, bold = true }, }, } 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 api.nvim_buf_set_lines(0, 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() api.nvim_buf_clear_namespace(0, ns1, 0, -1) eq({}, get_marks(ns1)) eq(ns_marks[ns2], get_marks(ns2)) api.nvim_buf_clear_namespace(0, ns2, 0, -1) eq({}, get_marks(ns1)) eq({}, get_marks(ns2)) end) it('can clear line range', function() api.nvim_buf_clear_namespace(0, 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)) api.nvim_buf_clear_namespace(0, ns1, 0, 10) for id, mark in pairs(ns_marks[ns1]) do if mark[1] < 10 then ns_marks[ns1][id] = nil end end eq(ns_marks[ns1], get_marks(ns1)) eq(ns_marks[ns2], get_marks(ns2)) api.nvim_buf_clear_namespace(0, ns1, 20, -1) for id, mark in pairs(ns_marks[ns1]) do if 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) -- 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 | {1:~ }| | ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { 1000, ns, marks[1], 1, 16 }, }, }, }) end) it('sends multiple ui-watched marks to ui', function() screen = Screen.new(20, 4) feed('15A!') -- 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 line!!!!!| !!!!!!!!!^! | | ]], extmarks = { [2] = { -- notification from 1st call { 1000, ns, marks[1], 1, 0 }, -- notifications from 2nd call { 1000, ns, marks[1], 1, 0 }, { 1000, ns, marks[2], 1, 2 }, -- notifications from 3rd call { 1000, ns, marks[1], 1, 0 }, { 1000, ns, marks[2], 1, 2 }, { 1000, ns, marks[3], 1, 4 }, -- notifications from 4th call { 1000, ns, marks[1], 1, 0 }, { 1000, ns, marks[2], 1, 2 }, { 1000, ns, marks[3], 1, 4 }, { 1000, ns, marks[4], 1, 6 }, -- final -- overlay { 1000, ns, marks[1], 1, 0 }, { 1000, ns, marks[2], 1, 2 }, { 1000, ns, marks[3], 1, 4 }, { 1000, ns, marks[4], 1, 6 }, -- eol { 1000, ns, marks[5], 2, 11 }, }, }, }) end) it('updates ui-watched marks', function() screen = Screen.new(20, 4) -- 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 { 1000, ns, marks[1], 1, 16 }, -- updated and wrapped to 3rd line { 1000, ns, marks[1], 2, 2 }, }, }, }) feed('') screen:expect({ grid = [[ ui-watched linupdat^e| e | {1:~ }| | ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { 1000, ns, marks[1], 1, 16 }, -- updated and wrapped to 3rd line { 1000, ns, marks[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, { 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:--------------------]|*3 {3:[No Name] [+] }| [2:--------------------]|*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 | {1:~ }| ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { 1000, ns, marks[1], 1, 16 }, -- updated after split { 1000, ns, marks[1], 1, 16 }, }, [4] = { -- only after split { 1001, ns, marks[1], 1, 16 }, }, }, }) -- make some changes insert(' update') screen:expect({ grid = [[ ## grid 1 [4:--------------------]|*3 {3:[No Name] [+] }| [2:--------------------]|*2 {2:[No Name] [+] }| [3:--------------------]| ## grid 2 non ui-watched line | ui-watched linupd{1:@@@}| ## grid 3 | ## grid 4 non ui-watched line | ui-watched linupdat^e| e | ]], extmarks = { [2] = { -- positioned at the end of the 2nd line { 1000, ns, marks[1], 1, 16 }, -- updated after split { 1000, ns, marks[1], 1, 16 }, }, [4] = { { 1001, ns, marks[1], 1, 16 }, -- updated { 1001, ns, marks[1], 2, 2 }, }, }, }) end) end)