diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 9290176974..f5367fb699 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -65,7 +65,7 @@ typedef struct { #define BF_NEW 0x10 // file didn't exist when editing started #define BF_NEW_W 0x20 // Warned for BF_NEW and file created #define BF_READERR 0x40 // got errors while reading the file -#define BF_DUMMY 0x80 // dummy buffer, only used internally +#define BF_DUMMY 0x80 // Internal-only dummy buffer. #define BF_SYN_SET 0x200 // 'syntax' option was set // Mask to check for flags that prevent normal writing diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index a77e2314d4..ae8cf74d3c 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -7142,6 +7142,8 @@ void restore_current_state(save_state_T *sst) ui_cursor_shape(); // may show different cursor shape } +/// True while evaluating an `` mapping. +/// Buffer changes are not allowed, except for internal-only dummy buffers. bool expr_map_locked(void) { return expr_map_lock > 0 && !(curbuf->b_flags & BF_DUMMY); diff --git a/src/nvim/insert.c b/src/nvim/insert.c index 4f9d75540a..edfadc2aa5 100644 --- a/src/nvim/insert.c +++ b/src/nvim/insert.c @@ -1342,23 +1342,21 @@ static void insert_handle_key_post(InsertState *s) /// edit(): Start inserting text. /// -/// "cmdchar" can be: -/// 'i' normal insert command -/// 'a' normal append command -/// 'R' replace command -/// 'r' "r" command: insert one . -/// Note: count can be > 1, for redo, but still only one is inserted. -/// is not used for redo. -/// 'g' "gI" command. -/// 'V' "gR" command for Virtual Replace mode. -/// 'v' "gr" command for single character Virtual Replace mode. +/// May nest: ":normal i…" and multicursor insert-cascade enters it while an outer edit() is +/// suspended. But i_CTRL-O does not nest: edit() returns, and the caller runs the Normal-mode +/// command. /// -/// This function is not called recursively. For CTRL-O commands, it returns -/// and lets the caller handle the Normal-mode command. -/// -/// @param cmdchar command that started the insert -/// @param startln if true, insert at start of line -/// @param count repeat count for the command +/// @param cmdchar Command that started the insert: +/// - 'i' +/// - 'a' +/// - 'R' +/// - 'r': "r" command: insert one . Note: count can be > 1, for redo, but still only one +/// is inserted. is not used for redo. +/// - 'g': "gI" command. +/// - 'V': "gR" command: Virtual Replace mode. +/// - 'v': "gr" command: single-character Virtual Replace mode. +/// @param startln If true, insert at start of line. +/// @param count Repeat count for the command. /// /// @return true if a CTRL-O command caused the return (insert mode pending). bool edit(int cmdchar, bool startln, int count) @@ -1381,10 +1379,10 @@ bool edit(int cmdchar, bool startln, int count) return false; } - // Don't allow changes in the buffer while editing the cmdline. The - // caller of getcmdline() may get confused. - // Don't allow recursive insert mode when busy with completion. - // Allow in dummy buffers since they are only used internally + // Disallow edit() (insert-mode) when: + // - ins-completion is active + // - textlock (the caller of getcmdline() may get confused) + // - mapping eval if (textlock != 0 || ins_compl_active() || compl_busy || pum_visible() || expr_map_locked()) { emsg(_(e_textlock)); diff --git a/src/nvim/mcursor.c b/src/nvim/mcursor.c index f288be7fd0..54774167b7 100644 --- a/src/nvim/mcursor.c +++ b/src/nvim/mcursor.c @@ -33,6 +33,7 @@ #include "nvim/input.h" #include "nvim/input_cmdatom.h" #include "nvim/insert.h" +#include "nvim/insexpand.h" #include "nvim/keycodes.h" #include "nvim/log.h" #include "nvim/lua/executor.h" @@ -51,6 +52,7 @@ #include "nvim/os/input.h" #include "nvim/os/time.h" #include "nvim/plines.h" +#include "nvim/popupmenu.h" #include "nvim/pos_defs.h" #include "nvim/register.h" #include "nvim/register_defs.h" @@ -525,7 +527,7 @@ void mc_clock_edge(bool map_edit) } } -/// Removes cursors that overlap another cursor, so an edit does not apply N times at one position. +/// Prunes cursors that overlap others, so an edit does not apply N times at one position. static void mc_dedupe(void) { const bool had_cursors = kv_size(mc_cursors) > 0; @@ -839,8 +841,11 @@ void mc_ins_cascade(void) } else if (ins.size > mc_ins_span.done_len && mc_ins_keys_nonliteral(ins.data + mc_ins_span.done_len, ins.size - mc_ins_span.done_len)) { - // Non-literal keys pending (BS, CTRL-U, ...): re-execute instead of previewing. - mc_ins_span_flush(&ins, false); + // Non-literal keys pending (BS, CTRL-U, …): re-execute instead of previewing. + // Not during completion: edit() would raise E565. #41602 + if (!ins_compl_active() && !pum_visible()) { + mc_ins_span_flush(&ins, false); + } } else { MTPair p = extmark_from_id(curbuf, mc_session_ns(), mc_ins_span.region); if (p.start.id != 0) { diff --git a/test/functional/editor/mcursor_spec.lua b/test/functional/editor/mcursor_spec.lua index dbaa66d55b..4780f68dc3 100644 --- a/test/functional/editor/mcursor_spec.lua +++ b/test/functional/editor/mcursor_spec.lua @@ -1401,6 +1401,17 @@ describe('multicursor', function() feed('cc') feed('f') -- cycle, then cancel back to the leader eq({ 'f', 'f', 'f' }, { get_lines()[4], get_lines()[5], get_lines()[6] }) + + -- No candidates, {Visual}Q cursors: mid-completion. #41602 + clear_cursors() + api.nvim_buf_set_lines(0, 0, -1, true, { '', '', '' }) + command('let v:errmsg = ""') + feed('VggQ') + feed('i a') + eq('', api.nvim_get_vvar('errmsg')) + feed('') + eq('', api.nvim_get_vvar('errmsg')) + eq({ ' ', ' ', ' ' }, get_lines()) end) it('InsertCharPre-driven complete() plugin (cmp-style)', function()