aboutsummaryrefslogtreecommitdiff
path: root/test/functional/editor/put_spec.lua
blob: 47068470bba5eedb320903d0a7e784cb065a7bc3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
local Screen = require('test.functional.ui.screen')
local helpers = require('test.functional.helpers')(after_each)

local clear = helpers.clear
local insert = helpers.insert
local feed = helpers.feed
local expect = helpers.expect
local eq = helpers.eq
local map = helpers.tbl_map
local filter = helpers.tbl_filter
local feed_command = helpers.feed_command
local command = helpers.command
local curbuf_contents = helpers.curbuf_contents
local funcs = helpers.funcs
local dedent = helpers.dedent

local function reset()
  command('bwipe! | new')
  insert([[
  Line of words 1
  Line of words 2]])
  command('goto 1')
  feed('itest_string.<esc>u')
  funcs.setreg('a', 'test_stringa', 'V')
  funcs.setreg('b', 'test_stringb\ntest_stringb\ntest_stringb', 'b')
  funcs.setreg('"', 'test_string"', 'v')
end

-- We check the last inserted register ". in each of these tests because it is
-- implemented completely differently in do_put().
-- It is implemented differently so that control characters and imap'ped
-- characters work in the same manner when pasted as when inserted.
describe('put command', function()
  clear()
  before_each(reset)

  local function visual_marks_zero()
    for _,v in pairs(funcs.getpos("'<")) do
      if v ~= 0 then
        return false
      end
    end
    for _,v in pairs(funcs.getpos("'>")) do
      if v ~= 0 then
        return false
      end
    end
    return true
  end

  -- {{{ Where test definitions are run
  local function run_test_variations(test_variations, extra_setup)
    reset()
    if extra_setup then extra_setup() end
    local init_contents = curbuf_contents()
    local init_cursorpos = funcs.getcurpos()
    local assert_no_change = function (exception_table, after_undo)
      expect(init_contents)
      -- When putting the ". register forwards, undo doesn't move
      -- the cursor back to where it was before.
      -- This is because it uses the command character 'a' to
      -- start the insert, and undo after that leaves the cursor
      -- one place to the right (unless we were at the end of the
      -- line when we pasted).
      if not (exception_table.undo_position and after_undo) then
        eq(init_cursorpos, funcs.getcurpos())
      end
    end

    for _, test in pairs(test_variations) do
      it(test.description, function()
        if extra_setup then extra_setup() end
        local orig_dotstr = funcs.getreg('.')
        helpers.ok(visual_marks_zero())
        -- Make sure every test starts from the same conditions
        assert_no_change(test.exception_table, false)
        local was_cli = test.test_action()
        test.test_assertions(test.exception_table, false)
        -- Check that undo twice puts us back to the original conditions
        -- (i.e. puts the cursor and text back to before)
        feed('u')
        assert_no_change(test.exception_table, true)

        -- Should not have changed the ". register
        -- If we paste the ". register with a count we can't avoid
        -- changing this register, hence avoid this check.
        if not test.exception_table.dot_reg_changed then
          eq(orig_dotstr, funcs.getreg('.'))
        end

        -- Doing something, undoing it, and then redoing it should
        -- leave us in the same state as just doing it once.
        -- For :ex actions we want '@:', for normal actions we want '.'

        -- The '.' redo doesn't work for visual put so just exit if
        -- it was tested.
        -- We check that visual put was used by checking if the '< and
        -- '> marks were changed.
        if not visual_marks_zero() then
          return
        end

        if test.exception_table.undo_position then
          funcs.setpos('.', init_cursorpos)
        end
        if was_cli then
          feed('@:')
        else
          feed('.')
        end

        test.test_assertions(test.exception_table, true)
      end)
    end
  end -- run_test_variations()
  -- }}}

  local function create_test_defs(test_defs, command_base, command_creator, -- {{{
                                  expect_base, expect_creator)
    local rettab = {}
    local exceptions
    for _, v in pairs(test_defs) do
      if v[4] then
        exceptions = v[4]
      else
        exceptions = {}
      end
      table.insert(rettab,
      {
        test_action = command_creator(command_base, v[1]),
        test_assertions = expect_creator(expect_base, v[2]),
        description = v[3],
        exception_table = exceptions,
      })
    end
    return rettab
  end -- create_test_defs() }}}

  local function find_cursor_position(expect_string) -- {{{
    -- There must only be one occurrence of the character 'x' in
    -- expect_string.
    -- This function removes that occurrence, and returns the position that
    -- it was in.
    -- This returns the cursor position that would leave the 'x' in that
    -- place if we feed 'ix<esc>' and the string existed before it.
    for linenum, line in pairs(funcs.split(expect_string, '\n', 1)) do
      local column = line:find('x')
      if column then
        return {linenum, column}, expect_string:gsub('x', '')
      end
    end
  end -- find_cursor_position() }}}

  -- Action function creators {{{
  local function create_p_action(test_map, substitution)
    local temp_val = test_map:gsub('p', substitution)
    return function()
      feed(temp_val)
      return false
    end
  end

  local function create_put_action(command_base, substitution)
    local temp_val = command_base:gsub('put', substitution)
    return function()
      feed_command(temp_val)
      return true
    end
  end
  -- }}}

  -- Expect function creator {{{
  local function expect_creator(conversion_function, expect_base, conversion_table)
    local temp_expect_string = conversion_function(expect_base, conversion_table)
    local cursor_position, expect_string = find_cursor_position(temp_expect_string)
    return function(exception_table, after_redo)
      expect(expect_string)

      -- Have to use getcurpos() instead of curwinmeths.get_cursor() in
      -- order to account for virtualedit.
      -- We always want the curswant element in getcurpos(), which is
      -- sometimes different to the column element in
      -- curwinmeths.get_cursor().
      -- NOTE: The ".gp command leaves the cursor after the pasted text
      -- when running, but does not when the command is redone with the
      -- '.' command.
      if not (exception_table.redo_position and after_redo) then
        local actual_position = funcs.getcurpos()
        eq(cursor_position, {actual_position[2], actual_position[5]})
      end
    end
  end -- expect_creator() }}}

  -- Test definitions {{{
  local function copy_def(def)
    local rettab = { '', {}, '', nil }
    rettab[1] = def[1]
    for k,v in pairs(def[2]) do
      rettab[2][k] = v
    end
    rettab[3] = def[3]
    if def[4] then
      rettab[4] = {}
      for k,v in pairs(def[4]) do
        rettab[4][k] = v
      end
    end
    return rettab
  end

  local normal_command_defs = {
    {
      'p',
      {cursor_after = false, put_backwards = false, dot_register = false},
      'pastes after cursor with p',
    },
    {
      'gp',
      {cursor_after = true, put_backwards = false, dot_register = false},
      'leaves cursor after text with gp',
    },
    {
      '".p',
      {cursor_after = false, put_backwards = false, dot_register = true},
      'works with the ". register',
    },
    {
      '".gp',
      {cursor_after = true, put_backwards = false, dot_register = true},
      'gp works with the ". register',
      {redo_position = true},
    },
    {
      'P',
      {cursor_after = false, put_backwards = true, dot_register = false},
      'pastes before cursor with P',
    },
    {
      'gP',
      {cursor_after = true, put_backwards = true, dot_register = false},
      'gP pastes before cursor and leaves cursor after text',
    },
    {
      '".P',
      {cursor_after = false, put_backwards = true, dot_register = true},
      'P works with ". register',
    },
    {
      '".gP',
      {cursor_after = true, put_backwards = true, dot_register = true},
      'gP works with ". register',
      {redo_position = true},
    },
  }

  -- Add a definition applying a count for each definition above.
  -- Could do this for each transformation (p -> P, p -> gp etc), but I think
  -- it's neater this way (balance between being explicit and too verbose).
  for i = 1,#normal_command_defs do
    local cur = normal_command_defs[i]

    -- Make modified copy of current definition that includes a count.
    local newdef = copy_def(cur)
    newdef[2].count = 2
    cur[2].count = 1
    newdef[1] = '2' .. newdef[1]
    newdef[3] = 'double ' .. newdef[3]

    if cur[2].dot_register then
      if not cur[4] then
        newdef[4] = {}
      end
      newdef[4].dot_reg_changed = true
    end

    normal_command_defs[#normal_command_defs + 1] = newdef
  end

  local ex_command_defs = {
    {
      'put',
      {put_backwards = false, dot_register = false},
      'pastes linewise forwards with :put',
    },
    {
      'put!',
      {put_backwards = true, dot_register = false},
      'pastes linewise backwards with :put!',
    },
    {
      'put .',
      {put_backwards = false, dot_register = true},
      'pastes linewise with the dot register',
    },
    {
      'put! .',
      {put_backwards = true, dot_register = true},
      'pastes linewise backwards with the dot register',
    },
  }

  local function non_dotdefs(def_table)
    return filter(function(d) return not d[2].dot_register end, def_table)
  end

  -- }}}

  -- Conversion functions {{{
  local function convert_charwise(expect_base, conversion_table,
                                       virtualedit_end, visual_put)
    expect_base = dedent(expect_base)
    -- There is no difference between 'P' and 'p' when VIsual_active
    if not visual_put then
      if conversion_table.put_backwards then
        -- Special case for virtualedit at the end of a line.
        local replace_string
        if not virtualedit_end then
          replace_string = 'test_stringx"%1'
        else
          replace_string = 'test_stringx"'
        end
        expect_base = expect_base:gsub('(.)test_stringx"', replace_string)
      end
    end
    if conversion_table.count > 1 then
      local rep_string = 'test_string"'
      local extra_puts =  rep_string:rep(conversion_table.count - 1)
      expect_base = expect_base:gsub('test_stringx"', extra_puts .. 'test_stringx"')
    end
    if conversion_table.cursor_after then
      expect_base = expect_base:gsub('test_stringx"', 'test_string"x')
    end
    if conversion_table.dot_register then
      expect_base = expect_base:gsub('(test_stringx?)"', '%1.')
    end
    return expect_base
  end -- convert_charwise()

  local function make_back(string)
    local prev_line
    local rettab = {}
    local string_found = false
    for _, line in pairs(funcs.split(string, '\n', 1)) do
      if line:find('test_string') then
        string_found = true
        table.insert(rettab, line)
      else
        if string_found then
          if prev_line then
            table.insert(rettab, prev_line)
            prev_line = nil
          end
          table.insert(rettab, line)
        else
          table.insert(rettab, prev_line)
          prev_line = line
        end
      end
    end
    -- In case there are no lines after the text that was put.
    if prev_line and string_found then
      table.insert(rettab, prev_line)
    end
    return table.concat(rettab, '\n')
  end -- make_back()

  local function convert_linewise(expect_base, conversion_table, _, use_a, indent)
    expect_base = dedent(expect_base)
    if conversion_table.put_backwards then
      expect_base = make_back(expect_base)
    end
    local p_str = 'test_string"'
    if use_a then
      p_str = 'test_stringa'
    end

    if conversion_table.dot_register then
      expect_base = expect_base:gsub('x' .. p_str, 'xtest_string.')
      p_str = 'test_string.'
    end

    if conversion_table.cursor_after then
      expect_base = expect_base:gsub('x' .. p_str .. '\n', p_str .. '\nx')
    end

    -- The 'indent' argument is only used here because a single put with an
    -- indent doesn't require special handling. It doesn't require special
    -- handling because the cursor is never put before the indent, hence
    -- the modification of 'test_stringx"' gives the same overall answer as
    -- modifying '    test_stringx"'.

    -- Only happens when using normal mode command actions.
    if conversion_table.count and conversion_table.count > 1 then
      if not indent then
        indent = ''
      end
      local rep_string = indent .. p_str .. '\n'
      local extra_puts =  rep_string:rep(conversion_table.count - 1)
      local orig_string, new_string
      if conversion_table.cursor_after then
        orig_string = indent .. p_str .. '\nx'
        new_string = extra_puts .. orig_string
      else
        orig_string = indent .. 'x' .. p_str .. '\n'
        new_string = orig_string .. extra_puts
      end
      expect_base = expect_base:gsub(orig_string, new_string)
    end
    return expect_base
  end

  local function put_x_last(orig_line, p_str)
    local prev_end, cur_end, cur_start = 0, 0, 0
    while cur_start do
      prev_end = cur_end
      cur_start, cur_end = orig_line:find(p_str, prev_end)
    end
    -- Assume (because that is the only way I call it) that p_str matches
    -- the pattern 'test_string.'
    return orig_line:sub(1, prev_end - 1) .. 'x' .. orig_line:sub(prev_end)
  end

  local function convert_blockwise(expect_base, conversion_table, visual,
                                   use_b, trailing_whitespace)
    expect_base = dedent(expect_base)
    local p_str = 'test_string"'
    if use_b then
      p_str = 'test_stringb'
    end

    if conversion_table.dot_register then
      expect_base = expect_base:gsub('(x?)' .. p_str, '%1test_string.')
      -- Looks strange, but the dot is a special character in the pattern
      -- and a literal character in the replacement.
      expect_base = expect_base:gsub('test_stringx.', 'test_stringx.')
      p_str = 'test_string.'
    end

    -- No difference between 'p' and 'P' in visual mode.
    if not visual then
      if conversion_table.put_backwards then
        -- One for the line where the cursor is left, one for all other
        -- lines.
        expect_base = expect_base:gsub('([^x])' .. p_str, p_str .. '%1')
        expect_base = expect_base:gsub('([^x])x' .. p_str, 'x' .. p_str .. '%1')
        if not trailing_whitespace then
          expect_base = expect_base:gsub(' \n', '\n')
          expect_base = expect_base:gsub(' $', '')
        end
      end
    end

    if conversion_table.count and conversion_table.count > 1 then
      local p_pattern = p_str:gsub('%.', '%%.')
      expect_base = expect_base:gsub(p_pattern,
                                     p_str:rep(conversion_table.count))
      expect_base = expect_base:gsub('test_stringx([b".])',
                                     p_str:rep(conversion_table.count - 1)
                                     .. '%0')
    end

    if conversion_table.cursor_after then
      if not visual then
        local prev_line
        local rettab = {}
        local prev_in_block = false
        for _, line in pairs(funcs.split(expect_base, '\n', 1)) do
          if line:find('test_string') then
            if prev_line then
              prev_line = prev_line:gsub('x', '')
              table.insert(rettab, prev_line)
            end
            prev_line = line
            prev_in_block = true
          else
            if prev_in_block then
              prev_line = put_x_last(prev_line, p_str)
              table.insert(rettab, prev_line)
              prev_in_block = false
            end
            table.insert(rettab, line)
          end
        end
        if prev_line and prev_in_block then
          table.insert(rettab, put_x_last(prev_line, p_str))
        end

        expect_base = table.concat(rettab, '\n')
      else
        expect_base = expect_base:gsub('x(.)', '%1x')
      end
    end

    return expect_base
  end
  -- }}}

  -- Convenience functions {{{
  local function run_normal_mode_tests(test_string, base_map, extra_setup,
                                       virtualedit_end, selection_string)
    local function convert_closure(e, c)
      return convert_charwise(e, c, virtualedit_end, selection_string)
    end
    local function expect_normal_creator(expect_base, conversion_table)
      local test_expect = expect_creator(convert_closure, expect_base, conversion_table)
      return function(exception_table, after_redo)
        test_expect(exception_table, after_redo)
        if selection_string then
          if not conversion_table.put_backwards then
            eq(selection_string, funcs.getreg('"'))
          end
        else
          eq('test_string"', funcs.getreg('"'))
        end
      end
    end
    run_test_variations(
      create_test_defs(
        normal_command_defs,
        base_map,
        create_p_action,
        test_string,
        expect_normal_creator
      ),
      extra_setup
    )
  end -- run_normal_mode_tests()

  local function convert_linewiseer(expect_base, conversion_table)
    return expect_creator(convert_linewise, expect_base, conversion_table)
  end

  local function run_linewise_tests(expect_base, base_command, extra_setup)
    local linewise_test_defs = create_test_defs(
        ex_command_defs, base_command,
        create_put_action, expect_base, convert_linewiseer)
    run_test_variations(linewise_test_defs, extra_setup)
  end -- run_linewise_tests()
  -- }}}

  -- Actual tests
  describe('default pasting', function()
    local expect_string = [[
    Ltest_stringx"ine of words 1
    Line of words 2]]
    run_normal_mode_tests(expect_string, 'p')

    run_linewise_tests([[
      Line of words 1
      xtest_string"
      Line of words 2]],
      'put'
    )
  end)

  describe('linewise register', function()
    -- put with 'p'
    local local_ex_command_defs = non_dotdefs(normal_command_defs)
    local base_expect_string = [[
    Line of words 1
    xtest_stringa
    Line of words 2]]
    local function local_convert_linewise(expect_base, conversion_table)
      return convert_linewise(expect_base, conversion_table, nil, true)
    end
    local function expect_lineput(expect_base, conversion_table)
      return expect_creator(local_convert_linewise, expect_base, conversion_table)
    end
    run_test_variations(
      create_test_defs(
        local_ex_command_defs,
        '"ap',
        create_p_action,
        base_expect_string,
        expect_lineput
      )
    )

    -- put with :put
    local linewise_put_defs = non_dotdefs(ex_command_defs)
    base_expect_string = [[
    Line of words 1
    xtest_stringa
    Line of words 2]]
    run_test_variations(
      create_test_defs(
        linewise_put_defs,
        'put a', create_put_action,
        base_expect_string, convert_linewiseer
      )
    )

  end)

  describe('blockwise register', function()
    local blockwise_put_defs = non_dotdefs(normal_command_defs)
    local test_base = [[
    Lxtest_stringbine of words 1
    Ltest_stringbine of words 2
     test_stringb]]

    local function expect_block_creator(expect_base, conversion_table)
      return expect_creator(function(e,c) return convert_blockwise(e,c,nil,true) end,
                    expect_base, conversion_table)
    end

    run_test_variations(
      create_test_defs(
        blockwise_put_defs,
        '"bp',
        create_p_action,
        test_base,
        expect_block_creator
      )
    )
  end)

  it('adds correct indentation when put with [p and ]p', function()
    feed('G>>"a]pix<esc>')
    -- luacheck: ignore
    expect([[
    Line of words 1
    	Line of words 2
    	xtest_stringa]])
    feed('uu"a[pix<esc>')
    -- luacheck: ignore
    expect([[
    Line of words 1
    	xtest_stringa
    	Line of words 2]])
  end)

  describe('linewise paste with autoindent', function()
    -- luacheck: ignore
    run_linewise_tests([[
        Line of words 1
        	Line of words 2
        xtest_string"]],
        'put'
      ,
      function()
        funcs.setline('$', '	Line of words 2')
        -- Set curswant to '8' to be at the end of the tab character
        -- This is where the cursor is put back after the 'u' command.
        funcs.setpos('.', {0, 2, 1, 0, 8})
        command('set autoindent')
      end
    )
  end)

  describe('put inside tabs with virtualedit', function()
    local test_string = [[
    Line of words 1
       test_stringx"     Line of words 2]]
    run_normal_mode_tests(test_string, 'p', function()
      funcs.setline('$', '	Line of words 2')
      command('setlocal virtualedit=all')
      funcs.setpos('.', {0, 2, 1, 2, 3})
    end)
  end)

  describe('put after the line with virtualedit', function()
    -- luacheck: ignore 621
    local test_string = [[
    Line of words 1  test_stringx"
    	Line of words 2]]
    run_normal_mode_tests(test_string, 'p', function()
      funcs.setline('$', '	Line of words 2')
      command('setlocal virtualedit=all')
      funcs.setpos('.', {0, 1, 16, 1, 17})
    end, true)
  end)

  describe('Visual put', function()
    describe('basic put', function()
      local test_string = [[
      test_stringx" words 1
      Line of words 2]]
      run_normal_mode_tests(test_string, 'v2ep', nil, nil, 'Line of')
    end)
    describe('over trailing newline', function()
      local test_string =  'Line of test_stringx"Line of words 2'
      run_normal_mode_tests(test_string, 'v$p', function()
        funcs.setpos('.', {0, 1, 9, 0, 9})
      end,
      nil,
      'words 1\n')
    end)
    describe('linewise mode', function()
      local test_string = [[
      xtest_string"
      Line of words 2]]
      local function expect_vis_linewise(expect_base, conversion_table)
        return expect_creator(function(e, c)
          return convert_linewise(e, c, nil, nil)
        end,
        expect_base, conversion_table)
      end
      run_test_variations(
        create_test_defs(
          normal_command_defs,
          'Vp',
          create_p_action,
          test_string,
          expect_vis_linewise
        ),
        function() funcs.setpos('.', {0, 1, 1, 0, 1}) end
      )

      describe('with whitespace at bol', function()
        local function expect_vis_lineindented(expect_base, conversion_table)
          local test_expect = expect_creator(function(e, c)
              return convert_linewise(e, c, nil, nil, '    ')
            end,
            expect_base, conversion_table)
          return function(exception_table, after_redo)
            test_expect(exception_table, after_redo)
            if not conversion_table.put_backwards then
              eq('Line of words 1\n', funcs.getreg('"'))
            end
          end
        end
        local base_expect_string = [[
            xtest_string"
        Line of words 2]]
        run_test_variations(
          create_test_defs(
            normal_command_defs,
            'Vp',
            create_p_action,
            base_expect_string,
            expect_vis_lineindented
          ),
          function()
            feed('i    test_string.<esc>u')
            funcs.setreg('"', '    test_string"', 'v')
          end
        )
      end)

    end)

    describe('blockwise visual mode', function()
      local test_base = [[
        test_stringx"e of words 1
        test_string"e of words 2]]

      local function expect_block_creator(expect_base, conversion_table)
        local test_expect = expect_creator(function(e, c)
            return convert_blockwise(e, c, true)
          end, expect_base, conversion_table)
        return function(e,c)
          test_expect(e,c)
          if not conversion_table.put_backwards then
            eq('Lin\nLin', funcs.getreg('"'))
          end
        end
      end

      local select_down_test_defs = create_test_defs(
          normal_command_defs,
          '<C-v>jllp',
          create_p_action,
          test_base,
          expect_block_creator
      )
      run_test_variations(select_down_test_defs)


      -- Undo and redo of a visual block put leave the cursor in the top
      -- left of the visual block area no matter where the cursor was
      -- when it started.
      local undo_redo_no = map(function(table)
          local rettab = copy_def(table)
          if not rettab[4] then
            rettab[4] = {}
          end
          rettab[4].undo_position = true
          rettab[4].redo_position = true
          return rettab
        end,
        normal_command_defs)

      -- Selection direction doesn't matter
      run_test_variations(
        create_test_defs(
          undo_redo_no,
          '<C-v>kllp',
          create_p_action,
          test_base,
          expect_block_creator
        ),
        function() funcs.setpos('.', {0, 2, 1, 0, 1}) end
      )

      describe('blockwise cursor after undo', function()
        -- A bit of a hack of the reset above.
        -- In the tests that selection direction doesn't matter, we
        -- don't check the undo/redo position because it doesn't fit
        -- the same pattern as everything else.
        -- Here we fix this by directly checking the undo/redo position
        -- in the test_assertions of our test definitions.
        local function assertion_creator(_,_)
          return function(_,_)
            feed('u')
            -- Have to use feed('u') here to set curswant, because
            -- ex_undo() doesn't do that.
            eq({0, 1, 1, 0, 1}, funcs.getcurpos())
            feed('<C-r>')
            eq({0, 1, 1, 0, 1}, funcs.getcurpos())
          end
        end

        run_test_variations(
          create_test_defs(
            undo_redo_no,
            '<C-v>kllp',
            create_p_action,
            test_base,
            assertion_creator
          ),
          function() funcs.setpos('.', {0, 2, 1, 0, 1}) end
        )
      end)
    end)


    describe("with 'virtualedit'", function()
      describe('splitting a tab character', function()
        local base_expect_string = [[
        Line of words 1
          test_stringx"     Line of words 2]]
        run_normal_mode_tests(
          base_expect_string,
          'vp',
          function()
            funcs.setline('$', '	Line of words 2')
            command('setlocal virtualedit=all')
            funcs.setpos('.', {0, 2, 1, 2, 3})
          end,
          nil,
          ' '
        )
      end)
      describe('after end of line', function()
        local base_expect_string = [[
        Line of words 1  test_stringx"
        Line of words 2]]
        run_normal_mode_tests(
          base_expect_string,
          'vp',
          function()
            command('setlocal virtualedit=all')
            funcs.setpos('.', {0, 1, 16, 2, 18})
          end,
          true,
          ' '
        )
      end)
    end)
  end)

  describe('. register special tests', function()
    -- luacheck: ignore 621
    before_each(reset)
    it('applies control character actions', function()
      feed('i<C-t><esc>u')
      expect([[
      Line of words 1
      Line of words 2]])
      feed('".p')
      expect([[
      	Line of words 1
      Line of words 2]])
      feed('u1go<C-v>j".p')
      eq([[
	ine of words 1
	ine of words 2]], curbuf_contents())
    end)

    local screen
    setup(function()
      screen = Screen.new()
      screen:attach()
    end)

    local function bell_test(actions, should_ring)
      if should_ring then
        -- check bell is not set by nvim before the action
        screen:sleep(50)
      end
      helpers.ok(not screen.bell and not screen.visualbell)
      actions()
      screen:expect{condition=function()
        if should_ring then
          if not screen.bell and not screen.visualbell then
            error('Bell was not rung after action')
          end
        else
          if screen.bell or screen.visualbell then
            error('Bell was rung after action')
          end
        end
      end, unchanged=(not should_ring)}
      screen.bell = false
      screen.visualbell = false
    end

    it('should not ring the bell with gp at end of line', function()
      bell_test(function() feed('$".gp') end)

      -- Even if the last character is a multibyte character.
      reset()
      funcs.setline(1, 'helloม')
      bell_test(function() feed('$".gp') end)
    end)

    it('should not ring the bell with gp and end of file', function()
      funcs.setpos('.', {0, 2, 1, 0})
      bell_test(function() feed('$vl".gp') end)
    end)

    it('should ring the bell when deleting if not appropriate', function()
      command('goto 2')
      feed('i<bs><esc>')
      expect([[
      ine of words 1
      Line of words 2]])
      bell_test(function() feed('".P') end, true)
    end)

    it('should restore cursor position after undo of ".p', function()
      local origpos = funcs.getcurpos()
      feed('".pu')
      eq(origpos, funcs.getcurpos())
    end)

    it("should be unaffected by 'autoindent' with V\".2p", function()
      command('set autoindent')
      feed('i test_string.<esc>u')
      feed('V".2p')
      expect([[
       test_string.
       test_string.
      Line of words 2]])
    end)
  end)
end)