fix(multicursor): 'autocomplete' during insert-cascade raises E565 #41625

Problem:
With mcursors and 'autocomplete', a non-literal key (`<BS>`) during an
active completion attempts to insert-cascade, which attempts a nested
`edit()`, which refuses with E565.

Solution:
Defer the flush while completion is active; the pending keys will be
handled later.
This commit is contained in:
Justin M. Keyes
2026-09-02 11:20:55 -04:00
committed by GitHub
parent 80e9708afa
commit c75aae2bfb
5 changed files with 40 additions and 24 deletions

View File

@@ -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

View File

@@ -7142,6 +7142,8 @@ void restore_current_state(save_state_T *sst)
ui_cursor_shape(); // may show different cursor shape
}
/// True while evaluating an `<expr>` 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);

View File

@@ -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<CR>" command: insert one <CR>.
/// Note: count can be > 1, for redo, but still only one <CR> is inserted.
/// <Esc> 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<CR>" command: insert one <CR>. Note: count can be > 1, for redo, but still only one
/// <CR> is inserted. <Esc> 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)
// - <expr> mapping eval
if (textlock != 0 || ins_compl_active() || compl_busy || pum_visible()
|| expr_map_locked()) {
emsg(_(e_textlock));

View File

@@ -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) {

View File

@@ -1401,6 +1401,17 @@ describe('multicursor', function()
feed('cc')
feed('f<C-n><C-n><C-e><Esc>') -- 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: <BS> mid-completion. #41602
clear_cursors()
api.nvim_buf_set_lines(0, 0, -1, true, { '', '', '' })
command('let v:errmsg = ""')
feed('VggQ')
feed('i a<BS>')
eq('', api.nvim_get_vvar('errmsg'))
feed('<Esc>')
eq('', api.nvim_get_vvar('errmsg'))
eq({ ' ', ' ', ' ' }, get_lines())
end)
it('InsertCharPre-driven complete() plugin (cmp-style)', function()