[Concept,17/19] cli: Make Ctrl+K clear to end of line in multiline mode

Message ID 20260130035849.3580212-18-simon.glass@canonical.com
State New
Headers
Series Enhanced command-line editing with undo/redo support |

Commit Message

Simon Glass Jan. 30, 2026, 3:58 a.m. UTC
  In multiline mode, Ctrl+K (kill to end of line) currently erases all
text from the cursor to the end of the buffer. This is not the expected
behaviour for multiline editing.

Update cread_erase_to_eol() to only erase to the next newline character
in multiline mode, preserving text on subsequent lines.

Use a parameterized ERASE_TO() macro to share the erase logic between
the multiline-aware function (when CMDLINE_EDITOR is enabled) and a simpler
version (when disabled), avoiding code growth on boards without
CMDLINE_EDITOR

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 common/cli_readline.c | 114 +++++++++++++++++++++++++++++++++++++++---
 test/boot/editenv.c   |  17 +++++--
 test/boot/expo.c      |  33 ++++++++----
 3 files changed, 144 insertions(+), 20 deletions(-)
  

Patch

diff --git a/common/cli_readline.c b/common/cli_readline.c
index 4c25e9a04ba..847b49450b5 100644
--- a/common/cli_readline.c
+++ b/common/cli_readline.c
@@ -231,13 +231,86 @@  void cread_print_hist_list(void)
 	}
 }
 
-#define BEGINNING_OF_LINE() {			\
-	while (cls->num) {			\
+#define GOTO_LINE_START(target) {		\
+	while (cls->num > (target)) {		\
 		cls_putch(cls, CTL_BACKSPACE);	\
 		cls->num--;			\
 	}					\
 }
 
+#define ERASE_TO(erase_to) {					\
+	if (cls->num < (erase_to)) {				\
+		uint wlen = (erase_to) - cls->num;		\
+								\
+		/* erase characters on screen */		\
+		printf("%*s", wlen, "");			\
+		while (wlen--)					\
+			cls_putch(cls, CTL_BACKSPACE);		\
+								\
+		/* remove characters from buffer */		\
+		memmove(&buf[cls->num], &buf[erase_to],		\
+			cls->eol_num - (erase_to) + 1);		\
+		cls->eol_num -= (erase_to) - cls->num;		\
+	}							\
+}
+
+#if CONFIG_IS_ENABLED(CMDLINE_EDITOR)
+/**
+ * cread_start_of_line() - Move cursor to start of line
+ *
+ * In multiline mode, moves to the character after the previous newline.
+ * Otherwise moves to position 0.
+ *
+ * @cls: CLI line state
+ */
+static void cread_start_of_line(struct cli_line_state *cls)
+{
+	struct cli_editor_state *ed = cli_editor(cls);
+	uint target = 0;
+
+	if (ed && ed->multiline) {
+		char *buf = cls->buf;
+		uint i;
+
+		/* find previous newline */
+		for (i = cls->num; i > 0; i--) {
+			if (buf[i - 1] == '\n') {
+				target = i;
+				break;
+			}
+		}
+	}
+	GOTO_LINE_START(target);
+}
+#define BEGINNING_OF_LINE() cread_start_of_line(cls)
+#else
+#define BEGINNING_OF_LINE() GOTO_LINE_START(0)
+#endif
+
+#if CONFIG_IS_ENABLED(CMDLINE_EDITOR)
+static void cread_erase_to_eol(struct cli_line_state *cls)
+{
+	struct cli_editor_state *ed = cli_editor(cls);
+	char *buf = cls->buf;
+	uint erase_to;
+
+	if (cls->num >= cls->eol_num)
+		return;
+
+	/*
+	 * In multiline mode, only erase to end of current line (next newline
+	 * or end of buffer)
+	 */
+	erase_to = cls->eol_num;
+	if (ed && ed->multiline) {
+		char *nl = strchr(&buf[cls->num], '\n');
+
+		if (nl)
+			erase_to = nl - buf;
+	}
+	ERASE_TO(erase_to);
+}
+#else
 static void cread_erase_to_eol(struct cli_line_state *cls)
 {
 	if (cls->num < cls->eol_num) {
@@ -247,15 +320,44 @@  static void cread_erase_to_eol(struct cli_line_state *cls)
 		} while (--cls->eol_num > cls->num);
 	}
 }
+#endif
 
-#define REFRESH_TO_EOL() {				\
-	if (cls->num < cls->eol_num) {			\
-		uint wlen = cls->eol_num - cls->num;	\
+#define GOTO_LINE_END(target) {				\
+	if (cls->num < (target)) {			\
+		uint wlen = (target) - cls->num;	\
 		cls_putnstr(cls, buf + cls->num, wlen);	\
-		cls->num = cls->eol_num;		\
+		cls->num = (target);			\
 	}						\
 }
 
+#if CONFIG_IS_ENABLED(CMDLINE_EDITOR)
+/**
+ * cread_end_of_line() - Move cursor to end of line
+ *
+ * In multiline mode, moves to the next newline character.
+ * Otherwise moves to end of buffer.
+ *
+ * @cls: CLI line state
+ */
+static void cread_end_of_line(struct cli_line_state *cls)
+{
+	struct cli_editor_state *ed = cli_editor(cls);
+	char *buf = cls->buf;
+	uint target = cls->eol_num;
+
+	if (ed && ed->multiline) {
+		char *nl = strchr(&buf[cls->num], '\n');
+
+		if (nl)
+			target = nl - buf;
+	}
+	GOTO_LINE_END(target);
+}
+#define REFRESH_TO_EOL() cread_end_of_line(cls)
+#else
+#define REFRESH_TO_EOL() GOTO_LINE_END(cls->eol_num)
+#endif
+
 static void cread_add_char(struct cli_line_state *cls, char ichar, int insert,
 			   uint *num, uint *eol_num, char *buf, uint len)
 {
diff --git a/test/boot/editenv.c b/test/boot/editenv.c
index 69e543ea51f..ab3c6648886 100644
--- a/test/boot/editenv.c
+++ b/test/boot/editenv.c
@@ -179,15 +179,22 @@  static int editenv_test_funcs(struct unit_test_state *uts)
 	ut_assertok(editenv_send(&info, BKEY_DOWN));
 	ut_asserteq(16611, ut_check_video(uts, "down"));
 
-	/* Type a character and press Ctrl-S to save */
+	/* Navigate with up arrow and insert '*' */
+	ut_assertok(editenv_send(&info, BKEY_UP));
+	ut_asserteq(16684, ut_check_video(uts, "up2"));
+
 	ut_assertok(editenv_send(&info, '*'));
-	ut_asserteq(16689, ut_check_video(uts, "insert"));
+	ut_asserteq(16877, ut_check_video(uts, "insert"));
+
+	/* Use Ctrl-K to kill to end of line (stops at the existing newline) */
+	ut_assertok(editenv_send(&info, CTL_CH('k')));
+	ut_asserteq(16033, ut_check_video(uts, "kill"));
 
 	ut_asserteq(1, editenv_send(&info, BKEY_SAVE));
 
-	/* The '*' should be appended to the initial text */
-	ut_assert(strstr(expo_editenv_result(&info), "editor.*"));
-	ut_asserteq(16689, ut_check_video(uts, "save"));
+	/* The '*' is inserted after "tes", Ctrl-K killed "ted properly." */
+	ut_assert(strstr(expo_editenv_result(&info), "tes*\n"));
+	ut_asserteq(16033, ut_check_video(uts, "save"));
 
 	expo_editenv_uninit(&info);
 
diff --git a/test/boot/expo.c b/test/boot/expo.c
index f598b9cb86c..366183e4a79 100644
--- a/test/boot/expo.c
+++ b/test/boot/expo.c
@@ -1681,16 +1681,16 @@  static int expo_render_textedit(struct unit_test_state *uts)
 	ut_assertok(expo_render(exp));
 	ut_asserteq(21211, video_compress_fb(uts, dev, false));
 
-	/* go to start of buffer and delete a character */
+	/* go to start of line (multiline Home goes to start of current line) */
 	ut_assertok(expo_send_key(exp, CTL_CH('a')));
-	ut_asserteq(0, ted->tin.cls.num);
+	ut_asserteq(5, ted->tin.cls.num);
 	ut_asserteq(91, ted->tin.cls.eol_num);
 	ut_assertok(expo_send_key(exp, CTL_CH('d')));
-	ut_asserteq(0, ted->tin.cls.num);
+	ut_asserteq(5, ted->tin.cls.num);
 	ut_asserteq(90, ted->tin.cls.eol_num);
 	ut_assertok(scene_arrange(scn));
 	ut_assertok(expo_render(exp));
-	ut_asserteq(21147, video_compress_fb(uts, dev, false));
+	ut_asserteq(21174, video_compress_fb(uts, dev, false));
 
 	/* go to end of buffer and backspace */
 	ut_assertok(expo_send_key(exp, CTL_CH('e')));
@@ -1701,7 +1701,7 @@  static int expo_render_textedit(struct unit_test_state *uts)
 	ut_asserteq(89, ted->tin.cls.eol_num);
 	ut_assertok(scene_arrange(scn));
 	ut_assertok(expo_render(exp));
-	ut_asserteq(21083, video_compress_fb(uts, dev, false));
+	ut_asserteq(21079, video_compress_fb(uts, dev, false));
 
 	/* set multiline mode and check Enter inserts newline */
 	ted->obj.flags |= SCENEOF_MULTILINE;
@@ -1712,7 +1712,22 @@  static int expo_render_textedit(struct unit_test_state *uts)
 	ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[89]);
 	ut_assertok(scene_arrange(scn));
 	ut_assertok(expo_render(exp));
-	ut_asserteq(21091, video_compress_fb(uts, dev, false));
+	ut_asserteq(21109, video_compress_fb(uts, dev, false));
+
+	/* go back 5 characters (before the newline) and use Ctrl+K */
+	ut_assertok(expo_send_key(exp, CTL_CH('b')));
+	ut_assertok(expo_send_key(exp, CTL_CH('b')));
+	ut_assertok(expo_send_key(exp, CTL_CH('b')));
+	ut_assertok(expo_send_key(exp, CTL_CH('b')));
+	ut_assertok(expo_send_key(exp, CTL_CH('b')));
+	ut_asserteq(85, ted->tin.cls.num);
+	ut_asserteq(90, ted->tin.cls.eol_num);
+
+	/* Ctrl+K in multiline mode should only delete to the newline */
+	ut_assertok(expo_send_key(exp, CTL_CH('k')));
+	ut_asserteq(85, ted->tin.cls.num);
+	ut_asserteq(86, ted->tin.cls.eol_num);
+	ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[85]);
 
 	/* clear multiline mode, close the textedit with Enter (BKEY_SELECT) */
 	ted->obj.flags &= ~SCENEOF_MULTILINE;
@@ -1724,12 +1739,12 @@  static int expo_render_textedit(struct unit_test_state *uts)
 
 	/* check the textedit is closed and text is changed */
 	ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN);
-	ut_asserteq_str("his\nis the initial contents of the text "
-		"editor but it is ely that more will be added latr\n",
+	ut_asserteq_str("This\ns the initial contents of the text "
+		"editor but it is ely that more will be added \n",
 		abuf_data(&ted->tin.buf));
 	ut_assertok(scene_arrange(scn));
 	ut_assertok(expo_render(exp));
-	ut_asserteq(21230, video_compress_fb(uts, dev, false));
+	ut_asserteq(21099, video_compress_fb(uts, dev, false));
 
 	abuf_uninit(&buf);
 	abuf_uninit(&logo_copy);