patch 9.1.1426: completion: register contents not completed

Problem:  CTRL-X CTRL-R only completes individual words from registers,
          making it difficult to insert complete register content.
Solution: Add consecutive CTRL-X CTRL-R support - first press completes
          words, second press completes full register lines, similar to
          CTRL-X CTRL-L and CTRL-X CTRL-P behavior (glepnir).

closes: #17395

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 d03d81e..ff27994 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 26
+*index.txt*     For Vim version 9.1.  Last change: 2025 Jun 02
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -163,7 +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-R|	CTRL-X CTRL-R	complete contents 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 553183d..9ec4682 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 26
+*insert.txt*    For Vim version 9.1.  Last change: 2025 Jun 02
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -649,7 +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|
+14. contents from registers				|i_CTRL-X_CTRL-R|
 
 Additionally, |i_CTRL-X_CTRL-Z| stops completion without changing the text.
 
@@ -1021,7 +1021,7 @@
 				:imap <Tab> <C-X><C-V>
 
 
-Completing words from registers				*compl-register-words*
+Completing contents 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.
@@ -1035,6 +1035,11 @@
 	CTRL-P		Search backwards for previous match.  This match
 			replaces the previous one.
 
+	CTRL-X CTRL-R	Further use of CTRL-X CTRL-R will copy the line
+			following the previous expansion in other contexts
+			unless a double CTRL-X is used (e.g. this switches
+			from completing register words to register contents).
+
 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/usr_24.txt b/runtime/doc/usr_24.txt
index 250bd17..0f72495 100644
--- a/runtime/doc/usr_24.txt
+++ b/runtime/doc/usr_24.txt
@@ -1,4 +1,4 @@
-*usr_24.txt*	For Vim version 9.1.  Last change: 2018 Mar 18
+*usr_24.txt*	For Vim version 9.1.  Last change: 2025 Jun 02
 
 		     VIM USER MANUAL - by Bram Moolenaar
 
@@ -187,7 +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-R		contents 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/vi_diff.txt b/runtime/doc/vi_diff.txt
index ae14968..b35fe77 100644
--- a/runtime/doc/vi_diff.txt
+++ b/runtime/doc/vi_diff.txt
@@ -1,4 +1,4 @@
-*vi_diff.txt*   For Vim version 9.1.  Last change: 2025 Mar 28
+*vi_diff.txt*   For Vim version 9.1.  Last change: 2025 Jun 02
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -338,7 +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
+	|i_CTRL-X_CTRL-R|	contents from registers
 	etc.
 
 Long line support.					|'wrap'| |'linebreak'|
diff --git a/src/insexpand.c b/src/insexpand.c
index 51dd675..9b5eec5 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -4935,35 +4935,83 @@
 
 	reg = (yankreg_T *)reg_ptr;
 
-	for (int j = 0; j < reg->y_size; j++)
+	if (compl_status_adding())
 	{
-	    char_u *str = reg->y_array[j].string;
-	    if (str == NULL)
-		continue;
-
-	    char_u *p = str;
-	    while (*p != NUL)
+	    for (int j = 0; j < reg->y_size; j++)
 	    {
-		p = find_word_start(p);
-		if (*p == NUL)
-		    break;
+		char_u *str = reg->y_array[j].string;
+		if (str == NULL)
+		    continue;
 
-		char_u *word_end = find_word_end(p);
+		int str_len = (int)STRLEN(str);
+		if (str_len == 0)
+		    continue;
 
-		// 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 (!compl_orig_text.string
+			|| (p_ic ? STRNICMP(str, compl_orig_text.string,
+					    compl_orig_text.length) == 0
+				: STRNCMP(str, compl_orig_text.string,
+					    compl_orig_text.length) == 0))
 		{
-		    if (ins_compl_add_infercase(p, len, p_ic, NULL,
-							dir, FALSE, 0) == OK)
+		    if (ins_compl_add_infercase(str, str_len, p_ic, NULL, dir, FALSE, 0) == OK)
 			dir = FORWARD;
 		}
+	    }
+	}
+	else
+	{
+	    for (int j = 0; j < reg->y_size; j++)
+	    {
+		char_u *str = reg->y_array[j].string;
+		if (str == NULL)
+		    continue;
 
-		p = word_end;
+		// Calculate the safe end of string to avoid null byte issues
+		char_u *str_end = str + STRLEN(str);
+		char_u *p = str;
+
+		// Safely iterate through the string
+		while (p < str_end && *p != NUL)
+		{
+		    char_u *old_p = p;
+		    p = find_word_start(p);
+		    if (p >= str_end || *p == NUL)
+			break;
+
+		    char_u *word_end = find_word_end(p);
+
+		    if (word_end <= p)
+		    {
+			if (has_mbyte)
+			    word_end = p + (*mb_ptr2len)(p);
+			else
+			    word_end = p + 1;
+		    }
+
+		    if (word_end > str_end)
+			word_end = str_end;
+
+		    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;
+
+		    if (p <= old_p)
+		    {
+			p = old_p + 1;
+			if (has_mbyte && p < str_end)
+			    p = old_p + (*mb_ptr2len)(old_p);
+		    }
+		}
 	    }
 	}
 
@@ -6379,7 +6427,7 @@
     static int
 compl_get_info(char_u *line, int startcol, colnr_T curs_col, int *line_invalid)
 {
-    if (ctrl_x_mode_normal()
+    if (ctrl_x_mode_normal() || ctrl_x_mode_register()
 	    || (ctrl_x_mode & CTRL_X_WANT_IDENT
 		&& !thesaurus_func_complete(ctrl_x_mode)))
     {
@@ -6410,10 +6458,6 @@
 	    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()");
@@ -6480,7 +6524,7 @@
 	if (compl_length < 1)
 	    compl_cont_status &= CONT_LOCAL;
     }
-    else if (ctrl_x_mode_line_or_eval())
+    else if (ctrl_x_mode_line_or_eval() || ctrl_x_mode_register())
 	compl_cont_status = CONT_ADDING | CONT_N_ADDS;
     else
 	compl_cont_status = 0;
diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index f3c29d1..7d67e9f 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -4670,6 +4670,27 @@
   call feedkeys("Sze\<C-X>\<C-R>\<C-R>=string(complete_info(['mode']))\<CR>\<ESC>", "tx")
   call assert_equal("zero{'mode': 'register'}", getline(1))
 
+  " Test consecutive CTRL-X CTRL-R (adding mode)
+  " First CTRL-X CTRL-R should split into words, second should use full content
+  let @f = "hello world test complete"
+  call setline(1, "hel")
+  call cursor(1, 3)
+  call feedkeys("a\<C-X>\<C-R>\<C-N>\<Esc>", 'tx')
+  call assert_equal("hello", getline(1))
+
+  " Second consecutive CTRL-X CTRL-R should complete with full content
+  call setline(1, "hello")
+  call cursor(1, 5)
+  call feedkeys("a\<C-X>\<C-R>\<C-X>\<C-R>\<Esc>", 'tx')
+  call assert_equal("hello world test complete", getline(1))
+
+  " Test consecutive completion with multi-line register
+  let @g = "first line content\nsecond line here\nthird line data"
+  call setline(1, "first")
+  call cursor(1, 5)
+  call feedkeys("a\<C-X>\<C-R>\<C-X>\<C-R>\<Esc>", 'tx')
+  call assert_equal("first line content", getline(1))
+
   " Clean up
   bwipe!
   delfunc GetItems
diff --git a/src/version.c b/src/version.c
index df6eb58..3160bdf 100644
--- a/src/version.c
+++ b/src/version.c
@@ -710,6 +710,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1426,
+/**/
     1425,
 /**/
     1424,