diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt index c2d220041c..28e4a70393 100644 --- a/runtime/doc/channel.txt +++ b/runtime/doc/channel.txt @@ -183,22 +183,23 @@ If you want to type input for the job in a Vim window you have a few options: - Use a terminal window. This works well if what you type goes directly to the job and the job output is directly displayed in the window. See |terminal|. -- Use a window with a prompt buffer. This works well when entering a line for +- Use a window with a prompt buffer. This works well when entering lines for the job in Vim while displaying (possibly filtered) output from the job. A prompt buffer is created by setting 'buftype' to "prompt". You would normally only do that in a newly created buffer. -The user can edit and enter one line of text at the very last line of the -buffer. When pressing Enter in the prompt line the callback set with -|prompt_setcallback()| is invoked. It would normally send the line to a job. -Another callback would receive the output from the job and display it in the -buffer, below the prompt (and above the next prompt). +The user can edit and enter text at the very last line of the buffer. When +pressing Enter in the prompt line the callback set with |prompt_setcallback()| +is invoked. To enter multiple lines user can use Shift+Enter that'd add a new +line. The final Enter submits the lines to |prompt_setcallback()|. It would +normally send the line to a job. Another callback would receive the output +from the job and display it in the buffer, below the prompt (and above the +next prompt). -Only the text in the last line, after the prompt, is editable. The rest of the -buffer is not modifiable with Normal mode commands. It can be modified by -calling functions, such as |append()|. Using other commands may mess up the -buffer. +Only the text after the last prompt, is editable. The rest of the buffer is +not modifiable with Normal mode commands. It can be modified by calling +functions, such as |append()|. Using other commands may mess up the buffer. After setting 'buftype' to "prompt" Vim does not automatically start Insert mode, use `:startinsert` if you want to enter Insert mode, so that the user diff --git a/runtime/doc/motion.txt b/runtime/doc/motion.txt index 6e408d60ad..b0d8569121 100644 --- a/runtime/doc/motion.txt +++ b/runtime/doc/motion.txt @@ -795,6 +795,14 @@ m< or m> Set the |'<| or |'>| mark. Useful to change what the Note that the Visual mode cannot be set, only the start and end position. + *m:* +m: Special mark for prompt buffers. It always shows the + line where current prompt starts. Text from this line + and below will be submitted when user submits. + Note: This mark is readonly. You can not modify it's + location. Also this mark is unique to prompt buffers as + a result not available in regular buffers. + *:ma* *:mark* *E191* :[range]ma[rk] {a-zA-Z'} Set mark {a-zA-Z'} at last line number in [range], diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 727a9d82d2..e60e0a7901 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -472,6 +472,12 @@ Variables: instead of always being strings. |v:option_old| is now the old global value for all global-local options, instead of just string global-local options. +Prompt-Buffer: +- supports multiline inputs. +- supports multiline paste. +- supports undo/redo on current prompt. +- supports normal o/O operations. + Vimscript: - |:redir| nested in |execute()| works. diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 6c3204fde1..0cd1ae29b3 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -878,6 +878,7 @@ static void free_buffer(buf_T *buf) clear_fmark(&buf->b_last_cursor, 0); clear_fmark(&buf->b_last_insert, 0); clear_fmark(&buf->b_last_change, 0); + clear_fmark(&buf->b_prompt_start, 0); for (size_t i = 0; i < NMARKS; i++) { free_fmark(buf->b_namedm[i]); } @@ -2024,6 +2025,7 @@ buf_T *buflist_new(char *ffname_arg, char *sfname_arg, linenr_T lnum, int flags) buf->b_prompt_callback.type = kCallbackNone; buf->b_prompt_interrupt.type = kCallbackNone; buf->b_prompt_text = NULL; + clear_fmark(&buf->b_prompt_start, 0); return buf; } diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 96d811642d..8f19c62655 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -699,6 +699,7 @@ struct file_buffer { Callback b_prompt_interrupt; // set by prompt_setinterrupt() int b_prompt_insert; // value for restart_edit when entering // a prompt buffer window. + fmark_T b_prompt_start; // Start of the editable area of a prompt buffer. synblock_T b_s; // Info related to syntax highlighting. w_s // normally points to this, but some windows diff --git a/src/nvim/change.c b/src/nvim/change.c index 0dec1178cc..d70e0d4663 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -1746,7 +1746,27 @@ bool open_line(int dir, int flags, int second_line_indent, bool *did_do_comment) curbuf_splice_pending++; old_cursor = curwin->w_cursor; + int old_cmod_flags = cmdmod.cmod_flags; + char *prompt_moved = NULL; if (dir == BACKWARD) { + // In case of prompt buffer, when we are applying 'normal O' operation on line of prompt, + // we can't add a new line before the prompt. In this case, we move the prompt text one + // line below and create a new prompt line as current line. + if (bt_prompt(curbuf) && curwin->w_cursor.lnum == curbuf->b_prompt_start.mark.lnum) { + char *prompt_line = ml_get(curwin->w_cursor.lnum); + char *prompt = prompt_text(); + size_t prompt_len = strlen(prompt); + + if (strncmp(prompt_line, prompt, prompt_len) == 0) { + STRMOVE(prompt_line, prompt_line + prompt_len); + // We are moving the lines but the b_prompt_start mark needs to stay in + // place so freezing marks before making the move. + cmdmod.cmod_flags = cmdmod.cmod_flags | CMOD_LOCKMARKS; + ml_replace(curwin->w_cursor.lnum, prompt_line, true); + prompt_moved = concat_str(prompt, p_extra); + p_extra = prompt_moved; + } + } curwin->w_cursor.lnum--; } if ((State & VREPLACE_FLAG) == 0 || old_cursor.lnum >= orig_line_count) { @@ -1936,6 +1956,8 @@ theend: xfree(saved_line); xfree(next_line); xfree(allocated); + xfree(prompt_moved); + cmdmod.cmod_flags = old_cmod_flags; return retval; } diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 5c77c81507..7f3f81c94e 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -1077,7 +1077,7 @@ check_pum: cmdwin_result = CAR; return 0; } - if (bt_prompt(curbuf)) { + if ((mod_mask & MOD_MASK_SHIFT) == 0 && bt_prompt(curbuf)) { invoke_prompt_callback(); if (!bt_prompt(curbuf)) { // buffer changed to a non-prompt buffer, get out of @@ -1532,9 +1532,14 @@ static void init_prompt(int cmdchar_todo) { char *prompt = prompt_text(); - curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; + if (curwin->w_cursor.lnum < curbuf->b_prompt_start.mark.lnum) { + curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; + coladvance(curwin, MAXCOL); + } char *text = get_cursor_line_ptr(); - if (strncmp(text, prompt, strlen(prompt)) != 0) { + if ((curbuf->b_prompt_start.mark.lnum == curwin->w_cursor.lnum + && strncmp(text, prompt, strlen(prompt)) != 0) + || curbuf->b_prompt_start.mark.lnum > curwin->w_cursor.lnum) { // prompt is missing, insert it or append a line with it if (*text == NUL) { ml_replace(curbuf->b_ml.ml_line_count, prompt, true); @@ -1547,8 +1552,9 @@ static void init_prompt(int cmdchar_todo) } // Insert always starts after the prompt, allow editing text after it. - if (Insstart_orig.lnum != curwin->w_cursor.lnum || Insstart_orig.col != (colnr_T)strlen(prompt)) { - Insstart.lnum = curwin->w_cursor.lnum; + if (Insstart_orig.lnum != curbuf->b_prompt_start.mark.lnum + || Insstart_orig.col != (colnr_T)strlen(prompt)) { + Insstart.lnum = curbuf->b_prompt_start.mark.lnum; Insstart.col = (colnr_T)strlen(prompt); Insstart_orig = Insstart; Insstart_textlen = Insstart.col; @@ -1559,7 +1565,9 @@ static void init_prompt(int cmdchar_todo) if (cmdchar_todo == 'A') { coladvance(curwin, MAXCOL); } - curwin->w_cursor.col = MAX(curwin->w_cursor.col, (colnr_T)strlen(prompt)); + if (curbuf->b_prompt_start.mark.lnum == curwin->w_cursor.lnum) { + curwin->w_cursor.col = MAX(curwin->w_cursor.col, (colnr_T)strlen(prompt)); + } // Make sure the cursor is in a valid position. check_cursor(curwin); } @@ -1568,8 +1576,9 @@ static void init_prompt(int cmdchar_todo) bool prompt_curpos_editable(void) FUNC_ATTR_PURE { - return curwin->w_cursor.lnum == curbuf->b_ml.ml_line_count - && curwin->w_cursor.col >= (int)strlen(prompt_text()); + return curwin->w_cursor.lnum > curbuf->b_prompt_start.mark.lnum + || (curwin->w_cursor.lnum == curbuf->b_prompt_start.mark.lnum + && curwin->w_cursor.col >= (int)strlen(prompt_text())); } // Undo the previous edit_putchar(). diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 63f73e222e..a401f764b9 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -78,6 +78,7 @@ #include "nvim/strings.h" #include "nvim/tag.h" #include "nvim/types_defs.h" +#include "nvim/undo.h" #include "nvim/version.h" #include "nvim/vim_defs.h" #include "nvim/window.h" @@ -8663,30 +8664,46 @@ void invoke_prompt_callback(void) { typval_T rettv; typval_T argv[2]; - linenr_T lnum = curbuf->b_ml.ml_line_count; + linenr_T lnum_start = curbuf->b_prompt_start.mark.lnum; + linenr_T lnum_last = curbuf->b_ml.ml_line_count; // Add a new line for the prompt before invoking the callback, so that // text can always be inserted above the last line. - ml_append(lnum, "", 0, false); - appended_lines_mark(lnum, 1); - curwin->w_cursor.lnum = lnum + 1; + ml_append(lnum_last, "", 0, false); + appended_lines_mark(lnum_last, 1); + curwin->w_cursor.lnum = lnum_last + 1; curwin->w_cursor.col = 0; if (curbuf->b_prompt_callback.type == kCallbackNone) { - return; + goto theend; } - char *text = ml_get(lnum); + char *text = ml_get(lnum_start); char *prompt = prompt_text(); if (strlen(text) >= strlen(prompt)) { text += strlen(prompt); } + + char *full_text = xstrdup(text); + for (linenr_T i = lnum_start + 1; i <= lnum_last; i++) { + char *half_text = concat_str(full_text, "\n"); + xfree(full_text); + full_text = concat_str(half_text, ml_get(i)); + xfree(half_text); + } argv[0].v_type = VAR_STRING; - argv[0].vval.v_string = xstrdup(text); + argv[0].vval.v_string = full_text; argv[1].v_type = VAR_UNKNOWN; callback_call(&curbuf->b_prompt_callback, 1, argv, &rettv); tv_clear(&argv[0]); tv_clear(&rettv); + +theend: + // clear undo history on submit + u_clearallandblockfree(curbuf); + + pos_T next_prompt = { .lnum = curbuf->b_ml.ml_line_count, .col = 1, .coladd = 0 }; + RESET_FMARK(&curbuf->b_prompt_start, next_prompt, 0, ((fmarkv_T)INIT_FMARKV)); } /// @return true when the interrupt callback was invoked. diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 09babd0b4f..485d077f5c 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -469,6 +469,9 @@ fmark_T *mark_get_local(buf_T *buf, win_T *win, int name) // to where last change was made } else if (name == '.') { mark = &buf->b_last_change; + // prompt start location + } else if (name == ':' && bt_prompt(buf)) { + mark = &(buf->b_prompt_start); // Mark that are actually not marks but motions, e.g {, }, (, ), ... } else { mark = mark_get_motion(buf, win, name); @@ -908,6 +911,9 @@ void ex_marks(exarg_T *eap) show_one_mark(']', arg, &curbuf->b_op_end, NULL, true); show_one_mark('^', arg, &curbuf->b_last_insert.mark, NULL, true); show_one_mark('.', arg, &curbuf->b_last_change.mark, NULL, true); + if (bt_prompt(curbuf)) { + show_one_mark(':', arg, &curbuf->b_prompt_start.mark, NULL, true); + } // Show the marks as where they will jump to. pos_T *startp = &curbuf->b_visual.vi_start; @@ -1030,6 +1036,9 @@ void ex_delmarks(exarg_T *eap) case '^': clear_fmark(&curbuf->b_last_insert, timestamp); break; + case ':': + // Readonly mark. No deletion allowed. + break; case '.': clear_fmark(&curbuf->b_last_change, timestamp); break; @@ -1223,6 +1232,11 @@ void mark_adjust_buf(buf_T *buf, linenr_T line1, linenr_T line2, linenr_T amount ONE_ADJUST(&(buf->b_last_cursor.mark.lnum)); } + // on prompt buffer adjust the last prompt start location mark + if (bt_prompt(curbuf)) { + ONE_ADJUST_NODEL(&(buf->b_prompt_start.mark.lnum)); + } + // list of change positions for (int i = 0; i < buf->b_changelistlen; i++) { ONE_ADJUST_NODEL(&(buf->b_changelist[i].mark.lnum)); @@ -1712,6 +1726,9 @@ bool mark_set_local(const char name, buf_T *const buf, const fmark_T fm, const b fm_tgt = &(buf->b_last_cursor); } else if (name == '^') { fm_tgt = &(buf->b_last_insert); + } else if (name == ':') { + // Readonly mark for prompt buffer. Can't be edited on user side. + return false; } else if (name == '.') { fm_tgt = &(buf->b_last_change); } else { diff --git a/src/nvim/normal.c b/src/nvim/normal.c index ba30b5ddff..7141410f65 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -4458,10 +4458,6 @@ static void nv_kundo(cmdarg_T *cap) return; } - if (bt_prompt(curbuf)) { - clearopbeep(cap->oap); - return; - } u_undo(cap->count1); curwin->w_set_curswant = true; } @@ -6481,8 +6477,15 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent) } if (bt_prompt(curbuf) && !prompt_curpos_editable()) { - clearopbeep(cap->oap); - return; + if (curwin->w_cursor.lnum == curbuf->b_prompt_start.mark.lnum) { + curwin->w_cursor.col = (int)strlen(prompt_text()); + // Since we've shifted the cursor to the first editable char. We want to + // paste before that. + cap->cmdchar = 'P'; + } else { + clearopbeep(cap->oap); + return; + } } if (fix_indent) { @@ -6613,7 +6616,7 @@ static void nv_open(cmdarg_T *cap) } else if (VIsual_active) { // switch start and end of visual/ v_swap_corners(cap->cmdchar); - } else if (bt_prompt(curbuf)) { + } else if (bt_prompt(curbuf) && curwin->w_cursor.lnum < curbuf->b_prompt_start.mark.lnum) { clearopbeep(cap->oap); } else { n_opencmd(cap); diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index 6a8928451d..d25f60da0e 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -28,6 +28,7 @@ #include "nvim/indent_c.h" #include "nvim/insexpand.h" #include "nvim/macros_defs.h" +#include "nvim/mark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" @@ -695,6 +696,11 @@ const char *did_set_buftype(optset_T *args) || opt_strings_flags(buf->b_p_bt, opt_bt_values, NULL, false) != OK) { return e_invarg; } + // buftype=prompt: set the prompt start position to lastline. + if (buf->b_p_bt[0] == 'p') { + pos_T next_prompt = { .lnum = buf->b_ml.ml_line_count, .col = 1, .coladd = 0 }; + RESET_FMARK(&buf->b_prompt_start, next_prompt, 0, ((fmarkv_T)INIT_FMARKV)); + } if (win->w_status_height || global_stl_height()) { win->w_redr_status = true; redraw_later(win, UPD_VALID); diff --git a/test/functional/legacy/prompt_buffer_spec.lua b/test/functional/legacy/prompt_buffer_spec.lua index 6a4d5fe7f7..5a1c9d7c78 100644 --- a/test/functional/legacy/prompt_buffer_spec.lua +++ b/test/functional/legacy/prompt_buffer_spec.lua @@ -3,6 +3,7 @@ local n = require('test.functional.testnvim')() local Screen = require('test.functional.ui.screen') local feed = n.feed +local fn = n.call local source = n.source local clear = n.clear local command = n.command @@ -31,7 +32,7 @@ describe('prompt buffer', function() close else " Add the output above the current prompt. - call append(line("$") - 1, 'Command: "' . a:text . '"') + call append(line("$") - 1, split('Command: "' . a:text . '"', '\n')) " Reset &modified to allow the buffer to be closed. set nomodified call timer_start(20, {id -> TimerFunc(a:text)}) @@ -40,7 +41,7 @@ describe('prompt buffer', function() func TimerFunc(text) " Add the output above the current prompt. - call append(line("$") - 1, 'Result: "' . a:text .'"') + call append(line("$") - 1, split('Result: "' . a:text .'"', '\n')) " Reset &modified to allow the buffer to be closed. set nomodified endfunc @@ -245,4 +246,280 @@ describe('prompt buffer', function() Leave Close]]) end) + + it('can insert mutli line text', function() + source_script() + feed('line 1line 2line 3') + screen:expect([[ + cmd: line 1 | + line 2 | + line 3^ | + {1:~ }| + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('') + -- submiting multiline text works + screen:expect([[ + Result: "line 1 | + line 2 | + line 3" | + cmd: ^ | + {3:[Prompt] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + end) + + it('can paste multiline text', function() + source_script() + fn('setreg', 'a', 'line 1\nline 2\nline 3') + feed('"ap') + screen:expect([[ + cmd: ^line 1 | + line 2 | + line 3 | + {1:~ }| + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + | + ]]) + feed('i') + screen:expect([[ + Result: "line 1 | + line 2 | + line 3" | + cmd: ^ | + {3:[Prompt] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + end) + + it('undo works for current prompt', function() + source_script() + -- text editiing alowed in current prompt + feed('tests-initial') + feed('bimiddle-') + screen:expect([[ + cmd: tests-middle^-initial| + {1:~ }|*3 + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + | + ]]) + + feed('Fdx') + screen:expect([[ + cmd: tests-mid^le-initial | + {1:~ }|*3 + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + | + ]]) + + -- can undo edits until prompt has been submitted + feed('u') + screen:expect([[ + cmd: tests-mid^dle-initial| + {1:~ }|*3 + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + 1 change; {MATCH:.*} | + ]]) + + feed('u') + screen:expect([[ + cmd: tests-^initial | + {1:~ }|*3 + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + 1 change; {MATCH:.*} | + ]]) + + feed('i') + screen:expect([[ + cmd: tests-initial | + Command: "tests-initial" | + Result: "tests-initial" | + cmd:^ | + {3:[Prompt] }| + other buffer | + {1:~ }|*3 + | + ]]) + + -- after submit undo does nothing + feed('u') + screen:expect([[ + cmd: tests-initial | + Command: "tests-initial" | + cmd:^ | + {1:~ }| + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + 1 line {MATCH:.*} | + ]]) + end) + + it('o/O can create new lines', function() + source_script() + feed('line 1line 2line 3') + screen:expect([[ + cmd: line 1 | + line 2 | + line 3^ | + {1:~ }| + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('koafter') + + screen:expect([[ + cmd: line 1 | + line 2 | + after^ | + line 3 | + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('kObefore') + + screen:expect([[ + cmd: line 1 | + before^ | + line 2 | + after | + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('') + screen:expect([[ + line 2 | + after | + line 3" | + cmd: ^ | + {3:[Prompt] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('line 4line 5') + + feed('k0oafter prompt') + screen:expect([[ + after | + line 3" | + cmd: line 4 | + after prompt^ | + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('k0Oat prompt') + screen:expect([[ + after | + line 3" | + cmd: at prompt^ | + line 4 | + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('') + screen:expect([[ + line 4 | + after prompt | + line 5" | + cmd: ^ | + {3:[Prompt] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + end) + + it('deleting prompt adds it back on insert', function() + source_script() + feed('asdf') + screen:expect([[ + cmd: asdf^ | + {1:~ }|*3 + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('ddi') + screen:expect([[ + cmd: ^ | + {1:~ }|*3 + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('asdf') + screen:expect([[ + cmd: asdf^ | + {1:~ }|*3 + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + + feed('cc') + screen:expect([[ + cmd: ^ | + {1:~ }|*3 + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + end) + + it(': mark follows current prompt', function() + source_script() + feed('asdf') + eq({ 1, 1 }, api.nvim_buf_get_mark(0, ':')) + feed('') + eq({ 3, 1 }, api.nvim_buf_get_mark(0, ':')) + end) + + it(': mark only available in prompt buffer', function() + source_script() + feed('asdf') + eq({ 1, 1 }, api.nvim_buf_get_mark(0, ':')) + source('set buftype=') + eq(false, pcall(api.nvim_buf_get_mark, 0, ':')) + end) end) diff --git a/test/old/testdir/test_prompt_buffer.vim b/test/old/testdir/test_prompt_buffer.vim index 41e14ae427..9aa3b87e34 100644 --- a/test/old/testdir/test_prompt_buffer.vim +++ b/test/old/testdir/test_prompt_buffer.vim @@ -166,11 +166,12 @@ func Test_prompt_buffer_edit() normal! i call assert_beeps('normal! dd') call assert_beeps('normal! ~') - call assert_beeps('normal! o') - call assert_beeps('normal! O') - call assert_beeps('normal! p') - call assert_beeps('normal! P') - call assert_beeps('normal! u') + " Nvim: these operations are supported + " call assert_beeps('normal! o') + " call assert_beeps('normal! O') + " call assert_beeps('normal! p') + " call assert_beeps('normal! P') + " call assert_beeps('normal! u') call assert_beeps('normal! ra') call assert_beeps('normal! s') call assert_beeps('normal! S')