patch 9.1.1408: not easily possible to complete from register content

Problem:  not easily possible to complete from register content
Solution: add register-completion submode using i_CTRL-X_CTRL-R
          (glepnir)

closes: #17354

Signed-off-by: glepnir <glephunter@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt
index 00a09ae..d03d81e 100644
--- a/runtime/doc/index.txt
+++ b/runtime/doc/index.txt
@@ -1,4 +1,4 @@
-*index.txt*     For Vim version 9.1.  Last change: 2025 May 14
+*index.txt*     For Vim version 9.1.  Last change: 2025 May 26
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -163,6 +163,7 @@
 |i_CTRL-X_CTRL-N|	CTRL-X CTRL-N	next completion
 |i_CTRL-X_CTRL-O|	CTRL-X CTRL-O	omni completion
 |i_CTRL-X_CTRL-P|	CTRL-X CTRL-P	previous completion
+|i_CTRL-X_CTRL-R|	CTRL-X CTRL-R	complete words from registers
 |i_CTRL-X_CTRL-S|	CTRL-X CTRL-S	spelling suggestions
 |i_CTRL-X_CTRL-T|	CTRL-X CTRL-T	complete identifiers from thesaurus
 |i_CTRL-X_CTRL-Y|	CTRL-X CTRL-Y	scroll down
diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt
index f3d92b2..553183d 100644
--- a/runtime/doc/insert.txt
+++ b/runtime/doc/insert.txt
@@ -1,4 +1,4 @@
-*insert.txt*    For Vim version 9.1.  Last change: 2025 May 08
+*insert.txt*    For Vim version 9.1.  Last change: 2025 May 26
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -649,6 +649,7 @@
 11. omni completion					|i_CTRL-X_CTRL-O|
 12. Spelling suggestions				|i_CTRL-X_s|
 13. keywords in 'complete'				|i_CTRL-N| |i_CTRL-P|
+14. words from registers				|i_CTRL-X_CTRL-R|
 
 Additionally, |i_CTRL-X_CTRL-Z| stops completion without changing the text.
 
@@ -1019,6 +1020,21 @@
 			completion, for example: >
 				:imap <Tab> <C-X><C-V>
 
+
+Completing words from registers				*compl-register-words*
+							*i_CTRL-X_CTRL-R*
+CTRL-X CTRL-R		Guess what kind of item is in front of the cursor from
+			all registers and find the first match for it.
+			Further use of CTRL-R (without CTRL-X) will insert the
+			register content, see |i_CTRL-R|.
+			'ignorecase' applies to the matching.
+
+	CTRL-N		Search forwards for next match.  This match replaces
+			the previous one.
+
+	CTRL-P		Search backwards for previous match.  This match
+			replaces the previous one.
+
 User defined completion					*compl-function*
 
 Completion is done by a function that can be defined by the user with the
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index e74c5e8..f0a7e9e 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1,4 +1,4 @@
-*options.txt*	For Vim version 9.1.  Last change: 2025 May 16
+*options.txt*	For Vim version 9.1.  Last change: 2025 May 26
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -4658,7 +4658,8 @@
 'ignorecase' 'ic'	boolean	(default off)
 			global
 	Ignore case in search patterns, |cmdline-completion|, when
-	searching in the tags file, and non-|Vim9| |expr-==|.
+	searching in the tags file, non-|Vim9| |expr-==| and for Insert-mode
+	completion |ins-completion|.
 	Also see 'smartcase' and 'tagcase'.
 	Can be overruled by using "\c" or "\C" in the pattern, see
 	|/ignorecase|.
diff --git a/runtime/doc/tags b/runtime/doc/tags
index e586212..d3c77d1 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -6654,6 +6654,7 @@
 compl-keyword	insert.txt	/*compl-keyword*
 compl-omni	insert.txt	/*compl-omni*
 compl-omni-filetypes	insert.txt	/*compl-omni-filetypes*
+compl-register-words	insert.txt	/*compl-register-words*
 compl-spelling	insert.txt	/*compl-spelling*
 compl-states	insert.txt	/*compl-states*
 compl-stop	insert.txt	/*compl-stop*
@@ -8425,6 +8426,7 @@
 i_CTRL-X_CTRL-N	insert.txt	/*i_CTRL-X_CTRL-N*
 i_CTRL-X_CTRL-O	insert.txt	/*i_CTRL-X_CTRL-O*
 i_CTRL-X_CTRL-P	insert.txt	/*i_CTRL-X_CTRL-P*
+i_CTRL-X_CTRL-R	insert.txt	/*i_CTRL-X_CTRL-R*
 i_CTRL-X_CTRL-S	insert.txt	/*i_CTRL-X_CTRL-S*
 i_CTRL-X_CTRL-T	insert.txt	/*i_CTRL-X_CTRL-T*
 i_CTRL-X_CTRL-U	insert.txt	/*i_CTRL-X_CTRL-U*
diff --git a/runtime/doc/todo.txt b/runtime/doc/todo.txt
index 5223687..578f5e8 100644
--- a/runtime/doc/todo.txt
+++ b/runtime/doc/todo.txt
@@ -1,4 +1,4 @@
-*todo.txt*      For Vim version 9.1.  Last change: 2025 Apr 24
+*todo.txt*      For Vim version 9.1.  Last change: 2025 May 26
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -4764,7 +4764,6 @@
 7   When expanding $HOME/dir with ^X^F keep the $HOME (with an option?).
 7   Add CTRL-X command in Insert mode like CTRL-X CTRL-N, that completes WORDS
     instead of words.
-8   Add CTRL-X CTRL-R: complete words from register contents.
 8   Add completion of previously inserted texts (like what CTRL-A does).
     Requires remembering a number of insertions.
 8   Add 'f' flag to 'complete': Expand file names.
diff --git a/runtime/doc/usr_24.txt b/runtime/doc/usr_24.txt
index 72f43f0..250bd17 100644
--- a/runtime/doc/usr_24.txt
+++ b/runtime/doc/usr_24.txt
@@ -187,6 +187,7 @@
 	CTRL-X CTRL-D		macro definitions (also in included files)
 	CTRL-X CTRL-I		current and included files
 	CTRL-X CTRL-K		words from a dictionary
+	CTRL-X CTRL-R		words from registers
 	CTRL-X CTRL-T		words from a thesaurus
 	CTRL-X CTRL-]		tags
 	CTRL-X CTRL-V		Vim command line
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index e03deed..662cb17 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt*  For Vim version 9.1.  Last change: 2025 May 16
+*version9.txt*  For Vim version 9.1.  Last change: 2025 May 26
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -41586,6 +41586,9 @@
 
 Support for a vertical |tabpanel| window similar to the 'tabline'.
 
+New Insert-mode completion: |i_CTRL-X_CTRL-R| to complete words from
+registers.
+
 							*changed-9.2*
 Changed~
 -------
diff --git a/runtime/doc/vi_diff.txt b/runtime/doc/vi_diff.txt
index 46db57a..ae14968 100644
--- a/runtime/doc/vi_diff.txt
+++ b/runtime/doc/vi_diff.txt
@@ -338,6 +338,7 @@
 	|i_CTRL-X_CTRL-D|	definitions or macros
 	|i_CTRL-X_CTRL-O|	Omni completion: clever completion
 				specifically for a file type
+	|i_CTRL-X_CTRL-R|	words from registers
 	etc.
 
 Long line support.					|'wrap'| |'linebreak'|
diff --git a/src/edit.c b/src/edit.c
index 4d45e8e..3661088 100644
--- a/src/edit.c
+++ b/src/edit.c
@@ -929,6 +929,8 @@
 	    break;
 
 	case Ctrl_R:	// insert the contents of a register
+	    if (ctrl_x_mode_register() && !ins_compl_active())
+		goto docomplete;
 	    ins_reg();
 	    auto_format(FALSE, TRUE);
 	    inserted_space = FALSE;
diff --git a/src/insexpand.c b/src/insexpand.c
index 76fb8f3..c7d6fd4 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -38,6 +38,7 @@
 # define CTRL_X_LOCAL_MSG	15	// only used in "ctrl_x_msgs"
 # define CTRL_X_EVAL		16	// for builtin function complete()
 # define CTRL_X_CMDLINE_CTRL_X	17	// CTRL-X typed in CTRL_X_CMDLINE
+# define CTRL_X_REGISTER	18	// complete words from registers
 
 # define CTRL_X_MSG(i) ctrl_x_msgs[(i) & ~CTRL_X_WANT_IDENT]
 
@@ -45,7 +46,7 @@
 static char *ctrl_x_msgs[] =
 {
     N_(" Keyword completion (^N^P)"), // CTRL_X_NORMAL, ^P/^N compl.
-    N_(" ^X mode (^]^D^E^F^I^K^L^N^O^Ps^U^V^Y)"),
+    N_(" ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)"),
     NULL, // CTRL_X_SCROLL: depends on state
     N_(" Whole line completion (^L^N^P)"),
     N_(" File name completion (^F^N^P)"),
@@ -62,6 +63,7 @@
     N_(" Keyword Local completion (^N^P)"),
     NULL,   // CTRL_X_EVAL doesn't use msg.
     N_(" Command-line completion (^V^N^P)"),
+    N_(" Register completion (^N^P)"),
 };
 
 #if defined(FEAT_COMPL_FUNC) || defined(FEAT_EVAL)
@@ -84,6 +86,7 @@
     NULL,		    // CTRL_X_LOCAL_MSG only used in "ctrl_x_msgs"
     "eval",
     "cmdline",
+    "register",
 };
 #endif
 
@@ -330,6 +333,8 @@
     { return ctrl_x_mode == CTRL_X_EVAL; }
 int ctrl_x_mode_line_or_eval(void)
     { return ctrl_x_mode == CTRL_X_WHOLE_LINE || ctrl_x_mode == CTRL_X_EVAL; }
+int ctrl_x_mode_register(void)
+    { return ctrl_x_mode == CTRL_X_REGISTER; }
 
 /*
  * Whether other than default completion has been selected.
@@ -460,7 +465,7 @@
 vim_is_ctrl_x_key(int c)
 {
     // Always allow ^R - let its results then be checked
-    if (c == Ctrl_R)
+    if (c == Ctrl_R && ctrl_x_mode != CTRL_X_REGISTER)
 	return TRUE;
 
     // Accept <PageUp> and <PageDown> if the popup menu is visible.
@@ -479,7 +484,7 @@
 		    || c == Ctrl_N || c == Ctrl_T || c == Ctrl_V
 		    || c == Ctrl_Q || c == Ctrl_U || c == Ctrl_O
 		    || c == Ctrl_S || c == Ctrl_K || c == 's'
-		    || c == Ctrl_Z);
+		    || c == Ctrl_Z || c == Ctrl_R);
 	case CTRL_X_SCROLL:
 	    return (c == Ctrl_Y || c == Ctrl_E);
 	case CTRL_X_WHOLE_LINE:
@@ -511,6 +516,8 @@
 	    return (c == Ctrl_S || c == Ctrl_P || c == Ctrl_N);
 	case CTRL_X_EVAL:
 	    return (c == Ctrl_P || c == Ctrl_N);
+	case CTRL_X_REGISTER:
+	    return (c == Ctrl_R || c == Ctrl_P || c == Ctrl_N);
     }
     internal_error("vim_is_ctrl_x_key()");
     return FALSE;
@@ -2535,7 +2542,7 @@
     static int
 set_ctrl_x_mode(int c)
 {
-    int retval = FALSE;
+    int	    retval = FALSE;
 
     switch (c)
     {
@@ -2563,8 +2570,11 @@
 	    ctrl_x_mode = CTRL_X_DICTIONARY;
 	    break;
 	case Ctrl_R:
-	    // Register insertion without exiting CTRL-X mode
-	    // Simply allow ^R to happen without affecting ^X mode
+	    // When CTRL-R is followed by '=', don't trigger register completion
+	    // This allows expressions like <C-R>=func()<CR> to work normally
+	    if (vpeekc() == '=')
+		break;
+	    ctrl_x_mode = CTRL_X_REGISTER;
 	    break;
 	case Ctrl_T:
 	    // complete words from a thesaurus
@@ -4784,6 +4794,83 @@
 #endif
 
 /*
+ * Get completion matches from register contents.
+ * Extracts words from all available registers and adds them to the completion list.
+ */
+    static void
+get_register_completion(void)
+{
+    int		dir = compl_direction;
+    yankreg_T	*reg = NULL;
+    void	*reg_ptr = NULL;
+
+    for (int i = 0; i < NUM_REGISTERS; i++)
+    {
+	int regname = 0;
+
+	if (i == 0)
+	    regname = '"';    // unnamed register
+	else if (i < 10)
+	    regname = '0' + i;
+	else if (i == DELETION_REGISTER)
+	    regname = '-';
+#ifdef FEAT_CLIPBOARD
+	else if (i == STAR_REGISTER)
+	    regname = '*';
+	else if (i == PLUS_REGISTER)
+	    regname = '+';
+#endif
+	else
+	    regname = 'a' + i - 10;
+
+	// Skip invalid or black hole register
+	if (!valid_yank_reg(regname, FALSE) || regname == '_')
+	    continue;
+
+	reg_ptr = get_register(regname, FALSE);
+	if (reg_ptr == NULL)
+	    continue;
+
+	reg = (yankreg_T *)reg_ptr;
+
+	for (int j = 0; j < reg->y_size; j++)
+	{
+	    char_u *str = reg->y_array[j].string;
+	    if (str == NULL)
+		continue;
+
+	    char_u *p = str;
+	    while (*p != NUL)
+	    {
+		p = find_word_start(p);
+		if (*p == NUL)
+		    break;
+
+		char_u *word_end = find_word_end(p);
+
+		// Add the word to the completion list
+		int len = (int)(word_end - p);
+		if (len > 0 && (!compl_orig_text.string
+			    || (p_ic ? STRNICMP(p, compl_orig_text.string,
+						compl_orig_text.length) == 0
+				: STRNCMP(p, compl_orig_text.string,
+					    compl_orig_text.length) == 0)))
+		{
+		    if (ins_compl_add_infercase(p, len, p_ic, NULL,
+							dir, FALSE, 0) == OK)
+			dir = FORWARD;
+		}
+
+		p = word_end;
+	    }
+	}
+
+	// Free the register copy
+	put_register(regname, reg_ptr);
+    }
+}
+
+/*
  * get the next set of completion matches for "type".
  * Returns TRUE if a new match is found. Otherwise returns FALSE.
  */
@@ -4838,6 +4925,10 @@
 	    get_next_spell_completion(st->first_match_pos.lnum);
 	    break;
 
+	case CTRL_X_REGISTER:
+	    get_register_completion();
+	    break;
+
 	default:	// normal ^P/^N and ^X^L
 	    found_new_match = get_next_default_completion(st, ini);
 	    if (found_new_match == FAIL && st->ins_buf == curbuf)
@@ -6125,6 +6216,10 @@
 	    return FAIL;
 	*line_invalid = TRUE;	// "line" may have become invalid
     }
+    else if (ctrl_x_mode_register())
+    {
+	return get_normal_compl_info(line, startcol, curs_col);
+    }
     else
     {
 	internal_error("ins_complete()");
diff --git a/src/proto/insexpand.pro b/src/proto/insexpand.pro
index e9ff626..8965aca 100644
--- a/src/proto/insexpand.pro
+++ b/src/proto/insexpand.pro
@@ -17,6 +17,7 @@
 int ctrl_x_mode_line_or_eval(void);
 int ctrl_x_mode_not_default(void);
 int ctrl_x_mode_not_defined_yet(void);
+int ctrl_x_mode_register(void);
 int compl_status_adding(void);
 int compl_status_sol(void);
 int compl_status_local(void);
diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index 8bce0e4..2a2df51 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -4521,4 +4521,85 @@
   delfunc TestComplete
 endfunc
 
+func Test_register_completion()
+  let @a = "completion test apple application"
+  let @b = "banana behavior better best"
+  let @c = "complete completion compliment computer"
+  let g:save_reg = ''
+  func GetItems()
+    let g:result = complete_info(['pum_visible'])
+  endfunc
+
+  new
+  call setline(1, "comp")
+  call cursor(1, 4)
+  call feedkeys("a\<C-X>\<C-R>\<C-N>\<C-N>\<Esc>", 'tx')
+  call assert_equal("compliment", getline(1))
+
+  inoremap <buffer><F2> <C-R>=GetItems()<CR>
+  call feedkeys("S\<C-X>\<C-R>\<F2>\<ESC>", 'tx')
+  call assert_equal(1, g:result['pum_visible'])
+
+  call setline(1, "app")
+  call cursor(1, 3)
+  call feedkeys("a\<C-X>\<C-R>\<C-N>\<Esc>", 'tx')
+  call assert_equal("application", getline(1))
+
+  " Test completion with case differences
+  set ignorecase
+  let @e = "TestCase UPPERCASE lowercase"
+  call setline(1, "testc")
+  call cursor(1, 5)
+  call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx')
+  call assert_equal("TestCase", getline(1))
+
+  " Test clipboard registers if available
+  if has('clipboard_working')
+    let g:save_reg = getreg('*')
+    call setreg('*', "clipboard selection unique words")
+    call setline(1, "uni")
+    call cursor(1, 3)
+    call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx')
+    call assert_equal("unique", getline(1))
+    call setreg('*', g:save_reg)
+
+    let g:save_reg = getreg('+')
+    call setreg('+', "system clipboard special content")
+    call setline(1, "spe")
+    call cursor(1, 3)
+    call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx')
+    call assert_equal("special", getline(1))
+    call setreg('+', g:save_reg)
+
+    call setreg('*', g:save_reg)
+    call setreg('a', "normal register")
+    call setreg('*', "clipboard mixed content")
+    call setline(1, "mix")
+    call cursor(1, 3)
+    call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx')
+    call assert_equal("mixed", getline(1))
+    call setreg('*', g:save_reg)
+  endif
+
+  " Test black hole register should be skipped
+  let @_ = "blackhole content should not appear"
+  call setline(1, "black")
+  call cursor(1, 5)
+  call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx')
+  call assert_equal("black", getline(1))
+
+  let @1 = "recent yank zero"
+  call setline(1, "ze")
+  call cursor(1, 2)
+  call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx')
+  call assert_equal("zero", getline(1))
+
+  " Clean up
+  bwipe!
+  delfunc GetItems
+  unlet g:result
+  unlet g:save_reg
+  set ignorecase&
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab nofoldenable
diff --git a/src/version.c b/src/version.c
index 637f178..ceedc5f 100644
--- a/src/version.c
+++ b/src/version.c
@@ -710,6 +710,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1408,
+/**/
     1407,
 /**/
     1406,