incremental_sync_spec.lua (18644B)
1 -- Test suite for testing interactions with the incremental sync algorithms powering the LSP client 2 local t = require('test.testutil') 3 local n = require('test.functional.testnvim')() 4 5 local api = n.api 6 local clear = n.clear 7 local eq = t.eq 8 local exec_lua = n.exec_lua 9 local feed = n.feed 10 11 before_each(function() 12 clear() 13 exec_lua(function() 14 local sync = require('vim.lsp.sync') 15 local events = {} 16 17 -- local format_line_ending = { 18 -- ["unix"] = '\n', 19 -- ["dos"] = '\r\n', 20 -- ["mac"] = '\r', 21 -- } 22 23 -- local line_ending = format_line_ending[vim.api.nvim_get_option_value('fileformat', {})] 24 25 --- @diagnostic disable-next-line:duplicate-set-field 26 function _G.test_register(bufnr, id, position_encoding, line_ending) 27 local prev_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) 28 29 local function callback(_, bufnr0, _changedtick, firstline, lastline, new_lastline) 30 if _G.test_unreg == id then 31 return true 32 end 33 34 local curr_lines = vim.api.nvim_buf_get_lines(bufnr0, 0, -1, true) 35 local incremental_change = sync.compute_diff( 36 prev_lines, 37 curr_lines, 38 firstline, 39 lastline, 40 new_lastline, 41 position_encoding, 42 line_ending 43 ) 44 45 table.insert(events, incremental_change) 46 prev_lines = curr_lines 47 end 48 local opts = { on_lines = callback, on_detach = callback, on_reload = callback } 49 vim.api.nvim_buf_attach(bufnr, false, opts) 50 end 51 52 --- @diagnostic disable-next-line:duplicate-set-field 53 function _G.get_events() 54 local ret_events = events 55 events = {} 56 return ret_events 57 end 58 end) 59 end) 60 61 --- @param edit_operations string[] 62 local function test_edit( 63 prev_buffer, 64 edit_operations, 65 expected_text_changes, 66 position_encoding, 67 line_ending 68 ) 69 position_encoding = position_encoding or 'utf-16' 70 line_ending = line_ending or '\n' 71 72 api.nvim_buf_set_lines(0, 0, -1, true, prev_buffer) 73 exec_lua(function() 74 return _G.test_register(0, 'test1', position_encoding, line_ending) 75 end) 76 77 for _, edit in ipairs(edit_operations) do 78 feed(edit) 79 end 80 eq( 81 expected_text_changes, 82 exec_lua(function() 83 return _G.get_events() 84 end) 85 ) 86 exec_lua(function() 87 _G.test_unreg = 'test1' 88 end) 89 end 90 91 describe('incremental synchronization', function() 92 describe('single line edit', function() 93 it('inserting a character in an empty buffer', function() 94 local expected_text_changes = { 95 { 96 range = { 97 ['start'] = { 98 character = 0, 99 line = 0, 100 }, 101 ['end'] = { 102 character = 0, 103 line = 0, 104 }, 105 }, 106 rangeLength = 0, 107 text = 'a', 108 }, 109 } 110 test_edit({ '' }, { 'ia' }, expected_text_changes, 'utf-16', '\n') 111 end) 112 it('inserting a character in the middle of a the first line', function() 113 local expected_text_changes = { 114 { 115 range = { 116 ['start'] = { 117 character = 1, 118 line = 0, 119 }, 120 ['end'] = { 121 character = 1, 122 line = 0, 123 }, 124 }, 125 rangeLength = 0, 126 text = 'a', 127 }, 128 } 129 test_edit({ 'ab' }, { 'lia' }, expected_text_changes, 'utf-16', '\n') 130 end) 131 it('deleting the only character in a buffer', function() 132 local expected_text_changes = { 133 { 134 range = { 135 ['start'] = { 136 character = 0, 137 line = 0, 138 }, 139 ['end'] = { 140 character = 1, 141 line = 0, 142 }, 143 }, 144 rangeLength = 1, 145 text = '', 146 }, 147 } 148 test_edit({ 'a' }, { 'x' }, expected_text_changes, 'utf-16', '\n') 149 end) 150 it('deleting a character in the middle of the line', function() 151 local expected_text_changes = { 152 { 153 range = { 154 ['start'] = { 155 character = 1, 156 line = 0, 157 }, 158 ['end'] = { 159 character = 2, 160 line = 0, 161 }, 162 }, 163 rangeLength = 1, 164 text = '', 165 }, 166 } 167 test_edit({ 'abc' }, { 'lx' }, expected_text_changes, 'utf-16', '\n') 168 end) 169 it('replacing a character', function() 170 local expected_text_changes = { 171 { 172 range = { 173 ['start'] = { 174 character = 0, 175 line = 0, 176 }, 177 ['end'] = { 178 character = 1, 179 line = 0, 180 }, 181 }, 182 rangeLength = 1, 183 text = 'b', 184 }, 185 } 186 test_edit({ 'a' }, { 'rb' }, expected_text_changes, 'utf-16', '\n') 187 end) 188 it('deleting the first line', function() 189 local expected_text_changes = { 190 { 191 range = { 192 ['start'] = { 193 character = 0, 194 line = 0, 195 }, 196 ['end'] = { 197 character = 0, 198 line = 1, 199 }, 200 }, 201 rangeLength = 6, 202 text = '', 203 }, 204 } 205 test_edit({ 'hello', 'world' }, { 'ggdd' }, expected_text_changes, 'utf-16', '\n') 206 end) 207 it('deleting the last line', function() 208 local expected_text_changes = { 209 { 210 range = { 211 ['start'] = { 212 character = 0, 213 line = 1, 214 }, 215 ['end'] = { 216 character = 0, 217 line = 2, 218 }, 219 }, 220 rangeLength = 6, 221 text = '', 222 }, 223 } 224 test_edit({ 'hello', 'world' }, { '2ggdd' }, expected_text_changes, 'utf-16', '\n') 225 end) 226 it('deleting all lines', function() 227 local expected_text_changes = { 228 { 229 range = { 230 ['start'] = { 231 character = 0, 232 line = 0, 233 }, 234 ['end'] = { 235 character = 5, 236 line = 1, 237 }, 238 }, 239 rangeLength = 11, 240 text = '', 241 }, 242 } 243 test_edit({ 'hello', 'world' }, { 'ggdG' }, expected_text_changes, 'utf-16', '\n') 244 end) 245 it('deleting an empty line', function() 246 local expected_text_changes = { 247 { 248 range = { 249 ['start'] = { 250 character = 0, 251 line = 1, 252 }, 253 ['end'] = { 254 character = 0, 255 line = 2, 256 }, 257 }, 258 rangeLength = 1, 259 text = '', 260 }, 261 } 262 test_edit({ 'hello world', '' }, { 'jdd' }, expected_text_changes, 'utf-16', '\n') 263 end) 264 it('adding a line', function() 265 local expected_text_changes = { 266 { 267 range = { 268 ['start'] = { 269 character = 11, 270 line = 0, 271 }, 272 ['end'] = { 273 character = 0, 274 line = 1, 275 }, 276 }, 277 rangeLength = 1, 278 text = '\nhello world\n', 279 }, 280 } 281 test_edit({ 'hello world' }, { 'yyp' }, expected_text_changes, 'utf-16', '\n') 282 end) 283 it('adding an empty line', function() 284 local expected_text_changes = { 285 { 286 range = { 287 ['start'] = { 288 character = 11, 289 line = 0, 290 }, 291 ['end'] = { 292 character = 0, 293 line = 1, 294 }, 295 }, 296 rangeLength = 1, 297 text = '\n\n', 298 }, 299 } 300 test_edit({ 'hello world' }, { 'o' }, expected_text_changes, 'utf-16', '\n') 301 end) 302 it('adding a line to an empty buffer', function() 303 local expected_text_changes = { 304 { 305 range = { 306 ['start'] = { 307 character = 0, 308 line = 0, 309 }, 310 ['end'] = { 311 character = 0, 312 line = 1, 313 }, 314 }, 315 rangeLength = 1, 316 text = '\n\n', 317 }, 318 } 319 test_edit({ '' }, { 'o' }, expected_text_changes, 'utf-16', '\n') 320 end) 321 it('insert a line above the current line', function() 322 local expected_text_changes = { 323 { 324 range = { 325 ['start'] = { 326 character = 0, 327 line = 0, 328 }, 329 ['end'] = { 330 character = 0, 331 line = 0, 332 }, 333 }, 334 rangeLength = 0, 335 text = '\n', 336 }, 337 } 338 test_edit({ '' }, { 'O' }, expected_text_changes, 'utf-16', '\n') 339 end) 340 end) 341 describe('multi line edit', function() 342 it('deletion and insertion', function() 343 local expected_text_changes = { 344 -- delete "_fsda" from end of line 1 345 { 346 range = { 347 ['start'] = { 348 character = 4, 349 line = 1, 350 }, 351 ['end'] = { 352 character = 9, 353 line = 1, 354 }, 355 }, 356 rangeLength = 5, 357 text = '', 358 }, 359 -- delete "hello world\n" from line 2 360 { 361 range = { 362 ['start'] = { 363 character = 0, 364 line = 2, 365 }, 366 ['end'] = { 367 character = 0, 368 line = 3, 369 }, 370 }, 371 rangeLength = 12, 372 text = '', 373 }, 374 -- delete "1234" from beginning of line 2 375 { 376 range = { 377 ['start'] = { 378 character = 0, 379 line = 2, 380 }, 381 ['end'] = { 382 character = 4, 383 line = 2, 384 }, 385 }, 386 rangeLength = 4, 387 text = '', 388 }, 389 -- add " asdf" to end of line 1 390 { 391 range = { 392 ['start'] = { 393 character = 4, 394 line = 1, 395 }, 396 ['end'] = { 397 character = 4, 398 line = 1, 399 }, 400 }, 401 rangeLength = 0, 402 text = ' asdf', 403 }, 404 -- delete " asdf\n" from line 2 405 { 406 range = { 407 ['start'] = { 408 character = 0, 409 line = 2, 410 }, 411 ['end'] = { 412 character = 0, 413 line = 3, 414 }, 415 }, 416 rangeLength = 6, 417 text = '', 418 }, 419 -- undo entire deletion 420 { 421 range = { 422 ['start'] = { 423 character = 4, 424 line = 1, 425 }, 426 ['end'] = { 427 character = 9, 428 line = 1, 429 }, 430 }, 431 rangeLength = 5, 432 text = '_fdsa\nhello world\n1234 asdf', 433 }, 434 -- redo entire deletion 435 { 436 range = { 437 ['start'] = { 438 character = 4, 439 line = 1, 440 }, 441 ['end'] = { 442 character = 9, 443 line = 3, 444 }, 445 }, 446 rangeLength = 27, 447 text = ' asdf', 448 }, 449 } 450 local original_lines = { 451 '\\begin{document}', 452 'test_fdsa', 453 'hello world', 454 '1234 asdf', 455 '\\end{document}', 456 } 457 test_edit(original_lines, { 'jf_vejjbhhdu<C-R>' }, expected_text_changes, 'utf-16', '\n') 458 end) 459 end) 460 461 describe('multi-operation edits', function() 462 it('mult-line substitution', function() 463 local expected_text_changes = { 464 { 465 range = { 466 ['end'] = { 467 character = 11, 468 line = 2, 469 }, 470 ['start'] = { 471 character = 10, 472 line = 2, 473 }, 474 }, 475 rangeLength = 1, 476 text = '', 477 }, 478 { 479 range = { 480 ['end'] = { 481 character = 10, 482 line = 2, 483 }, 484 start = { 485 character = 10, 486 line = 2, 487 }, 488 }, 489 rangeLength = 0, 490 text = '2', 491 }, 492 { 493 range = { 494 ['end'] = { 495 character = 11, 496 line = 3, 497 }, 498 ['start'] = { 499 character = 10, 500 line = 3, 501 }, 502 }, 503 rangeLength = 1, 504 text = '', 505 }, 506 { 507 range = { 508 ['end'] = { 509 character = 10, 510 line = 3, 511 }, 512 ['start'] = { 513 character = 10, 514 line = 3, 515 }, 516 }, 517 rangeLength = 0, 518 text = '3', 519 }, 520 { 521 range = { 522 ['end'] = { 523 character = 0, 524 line = 3, 525 }, 526 ['start'] = { 527 character = 12, 528 line = 2, 529 }, 530 }, 531 rangeLength = 1, 532 text = '\n', 533 }, 534 } 535 local original_lines = { 536 '\\begin{document}', 537 '\\section*{1}', 538 '\\section*{1}', 539 '\\section*{1}', 540 '\\end{document}', 541 } 542 test_edit(original_lines, { '3gg$h<C-V>jg<C-A>' }, expected_text_changes, 'utf-16', '\n') 543 end) 544 it('join and undo', function() 545 local expected_text_changes = { 546 { 547 range = { 548 ['start'] = { 549 character = 11, 550 line = 0, 551 }, 552 ['end'] = { 553 character = 11, 554 line = 0, 555 }, 556 }, 557 rangeLength = 0, 558 text = ' test3', 559 }, 560 { 561 range = { 562 ['start'] = { 563 character = 0, 564 line = 1, 565 }, 566 ['end'] = { 567 character = 0, 568 line = 2, 569 }, 570 }, 571 rangeLength = 6, 572 text = '', 573 }, 574 { 575 range = { 576 ['start'] = { 577 character = 11, 578 line = 0, 579 }, 580 ['end'] = { 581 character = 17, 582 line = 0, 583 }, 584 }, 585 rangeLength = 6, 586 text = '\ntest3', 587 }, 588 } 589 test_edit({ 'test1 test2', 'test3' }, { 'J', 'u' }, expected_text_changes, 'utf-16', '\n') 590 end) 591 end) 592 593 describe('multi-byte edits', function() 594 it('deleting a multibyte character', function() 595 local expected_text_changes = { 596 { 597 range = { 598 ['start'] = { 599 character = 0, 600 line = 0, 601 }, 602 ['end'] = { 603 character = 2, 604 line = 0, 605 }, 606 }, 607 rangeLength = 2, 608 text = '', 609 }, 610 } 611 test_edit({ '🔥' }, { 'x' }, expected_text_changes, 'utf-16', '\n') 612 end) 613 it('replacing a multibyte character with matching prefix', function() 614 local expected_text_changes = { 615 { 616 range = { 617 ['start'] = { 618 character = 0, 619 line = 1, 620 }, 621 ['end'] = { 622 character = 1, 623 line = 1, 624 }, 625 }, 626 rangeLength = 1, 627 text = '⟩', 628 }, 629 } 630 -- ⟨ is e29fa8, ⟩ is e29fa9 631 local original_lines = { 632 '\\begin{document}', 633 '⟨', 634 '\\end{document}', 635 } 636 test_edit(original_lines, { 'jr⟩' }, expected_text_changes, 'utf-16', '\n') 637 end) 638 it('replacing a multibyte character with matching suffix', function() 639 local expected_text_changes = { 640 { 641 range = { 642 ['start'] = { 643 character = 0, 644 line = 1, 645 }, 646 ['end'] = { 647 character = 1, 648 line = 1, 649 }, 650 }, 651 rangeLength = 1, 652 text = 'ḟ', 653 }, 654 } 655 -- ฟ is e0b89f, ḟ is e1b89f 656 local original_lines = { 657 '\\begin{document}', 658 'ฟ', 659 '\\end{document}', 660 } 661 test_edit(original_lines, { 'jrḟ' }, expected_text_changes, 'utf-16', '\n') 662 end) 663 it('inserting before a multibyte character', function() 664 local expected_text_changes = { 665 { 666 range = { 667 ['start'] = { 668 character = 0, 669 line = 1, 670 }, 671 ['end'] = { 672 character = 0, 673 line = 1, 674 }, 675 }, 676 rangeLength = 0, 677 text = ' ', 678 }, 679 } 680 local original_lines = { 681 '\\begin{document}', 682 '→', 683 '\\end{document}', 684 } 685 test_edit(original_lines, { 'ji ' }, expected_text_changes, 'utf-16', '\n') 686 end) 687 it('deleting a multibyte character from a long line', function() 688 local expected_text_changes = { 689 { 690 range = { 691 ['start'] = { 692 character = 85, 693 line = 1, 694 }, 695 ['end'] = { 696 character = 86, 697 line = 1, 698 }, 699 }, 700 rangeLength = 1, 701 text = '', 702 }, 703 } 704 local original_lines = { 705 '\\begin{document}', 706 '→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→', 707 '\\end{document}', 708 } 709 test_edit(original_lines, { 'jx' }, expected_text_changes, 'utf-16', '\n') 710 end) 711 it('deleting multiple lines containing multibyte characters', function() 712 local expected_text_changes = { 713 { 714 range = { 715 ['start'] = { 716 character = 0, 717 line = 1, 718 }, 719 ['end'] = { 720 character = 0, 721 line = 3, 722 }, 723 }, 724 --utf 16 len of 🔥 is 2 725 rangeLength = 8, 726 text = '', 727 }, 728 } 729 test_edit( 730 { 'a🔥', 'b🔥', 'c🔥', 'd🔥' }, 731 { 'j2dd' }, 732 expected_text_changes, 733 'utf-16', 734 '\n' 735 ) 736 end) 737 end) 738 end) 739 740 -- TODO(mjlbach): Add additional tests 741 -- deleting single lone line 742 -- 2 lines -> 2 line delete -> undo -> redo 743 -- describe('future tests', function() 744 -- -- This test is currently wrong, ask bjorn why dd on an empty line triggers on_lines 745 -- it('deleting an empty line', function() 746 -- local expected_text_changes = {{ }} 747 -- test_edit({""}, {"ggdd"}, expected_text_changes, 'utf-16', '\n') 748 -- end) 749 -- end)