From patchwork Fri Jan 30 03:58:42 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 1794 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org Authentication-Results: mail.u-boot.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=chromium.org header.i=@chromium.org header.a=rsa-sha256 header.s=google header.b=jhN1+qho; dkim-atps=neutral Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 0A7F8697DD for ; Thu, 29 Jan 2026 20:59:56 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id XZ1sHlI7bdsR for ; Thu, 29 Jan 2026 20:59:55 -0700 (MST) Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id ED89F697CE for ; Thu, 29 Jan 2026 20:59:55 -0700 (MST) Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 65C0D69738 for ; Thu, 29 Jan 2026 20:59:53 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id aKWPBTrRgtDo for ; Thu, 29 Jan 2026 20:59:53 -0700 (MST) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=209.85.167.178; helo=mail-oi1-f178.google.com; envelope-from=sjg@chromium.org; receiver=u-boot.org Received: from mail-oi1-f178.google.com (mail-oi1-f178.google.com [209.85.167.178]) by mail.u-boot.org (Postfix) with ESMTPS id 6E624697F4 for ; Thu, 29 Jan 2026 20:59:49 -0700 (MST) Received: by mail-oi1-f178.google.com with SMTP id 5614622812f47-45c889aba0dso1532770b6e.0 for ; Thu, 29 Jan 2026 19:59:49 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=chromium.org; s=google; t=1769745588; x=1770350388; darn=u-boot.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=MmzAgpy6PAj47SMP7j8yFG4cbMHiNJliKbjOLfGGjrQ=; b=jhN1+qhoEZN6PuzNBfGYAvgnyKzdWZ//d76XaCpWQAB1gH/RUoUPfwri6Ot2h+0VEG 3x9FwHVlLMd7KwPY2OMoe32EuT7S/EidtyUfSF+5gPce94BgVkPOJRLMYpwOSmH/dYg9 3AGIrJG4TzX8SVYvC/PN4UrP44b9hW2IIhcdw= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1769745588; x=1770350388; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=MmzAgpy6PAj47SMP7j8yFG4cbMHiNJliKbjOLfGGjrQ=; b=S6jwhpTeR9tPVMObncWmzYtd3GLNHELUvs18jmcnDIL7j9J3r98CsemzqA3zN/670k +Umqa7G6LjhGl2tmd8wxhLHx0txYU8+Thlj9+VyLQvODX4YVZCgAqf57/uFVL9v1kjCc MMh0mxTdyPDLfizdw6MIELPhpYRcK4rQTEia4ZkbVz0oFqvU9yGTVurYXlcWy9hTLmlT yif3bDhfKwMSE2hsQceKHjo2iuNJvCbDVUFAo+cnRbKY+dCTGaJVa4v1Wwh5aPVazhqd t5oU860H5PNQaiLbv7xoub3bOpvXewjoYbdkZem/OZ3JJKf6iq3YnxCamymGn8QlPSQU sKGw== X-Gm-Message-State: AOJu0YxMMS8KH+v+uFljvhu/lK9Rui59/6RpTWp9kK80848jrMCbLiZS FQwPSQ4VWE3rNa+zfZjgSTxcU8hdVL4M8Q/+6yZitpcNxE3nM9mjrNIxnkZP5CEdcxQ1MB2Opbn ZR2I8Hg== X-Gm-Gg: AZuq6aKcussx/elmHqYKFzgCmU/qkFFwCWPYkXhqPmS6On3KfAHlgmiow4QBA3mQetz t/cI5E5hbL7OUVxK/yWgYxCfwFc75Sc1BIvJqSwMmIwIwo7yP/5czRp/KS+Yc/vbTMfVA8VKUjY EwdjHQyYxPUH06jQSZDJAQyxtqxeFzvO7i9KueUXPv7EpiNCCekH8evLko8jWrsPkVgMWQQNYgM d61FmYj9CYx2h+RPEQNm+pmoiXkGzMFK8EgshbApyCWn7jgPPi+zfIcHKEf5GV+bN48wLBotySo SnlEq3jcZidsABqSUIxbvKt/68vENvbPHo+2BGAQf42XvA2Bqm1Cn8ArcHgbQByFwYA0rhP8D5M eDJKKlKaKD0FaD63dqFZILuW1PO6+IPyJylNuenP0CzRZjMZ5w3MgYfuWAJ1BUzyPjpObbSsWNF cgtMJt8N6lgOZxpDtx X-Received: by 2002:a05:6820:2109:b0:659:9a49:8ef4 with SMTP id 006d021491bc7-663103f83f8mr631437eaf.40.1769745587782; Thu, 29 Jan 2026 19:59:47 -0800 (PST) Received: from chromium.org ([73.34.74.121]) by smtp.gmail.com with ESMTPSA id 006d021491bc7-662f9a4e491sm4128687eaf.16.2026.01.29.19.59.44 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 29 Jan 2026 19:59:46 -0800 (PST) From: Simon Glass X-Google-Original-From: Simon Glass To: U-Boot Concept Date: Thu, 29 Jan 2026 20:58:42 -0700 Message-ID: <20260130035849.3580212-20-simon.glass@canonical.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260130035849.3580212-1-simon.glass@canonical.com> References: <20260130035849.3580212-1-simon.glass@canonical.com> MIME-Version: 1.0 Message-ID-Hash: PTBOPWDKNIR6TZVVEXAQSW26PD4UJRKS X-Message-ID-Hash: PTBOPWDKNIR6TZVVEXAQSW26PD4UJRKS X-MailFrom: sjg@chromium.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , "Claude Opus 4 . 5" X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 19/19] cli: Add multi-level undo/redo support List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Convert the single-level undo buffer to a ring buffer supporting multiple undo/redo levels. This allows users to undo multiple editing operations and redo them if needed. Key changes: - Replace single undo state with ring buffer (struct cli_undo_state) - Add redo ring buffer for undone states - Add Ctrl+Shift+Z (via Ctrl+G) for redo operation - Track undo/redo counts separately from buffer allocation - Clear redo history on new edits (standard editor behaviour) The number of undo levels is configurable via CONFIG_CMDLINE_UNDO_COUNT (default 64). Each level stores a complete copy of the edit buffer plus cursor position. Also fix scene_txtin_open() which was allocating the yank buffer twice - once in cli_cread_init_undo() and again directly. Remove the duplicate. Co-developed-by: Claude Opus 4.5 Signed-off-by: Simon Glass --- boot/scene_txtin.c | 36 +++++++++++- cmd/Kconfig | 15 +++++ common/cli_getch.c | 19 +++++- common/cli_readline.c | 40 ++++++++++--- common/cli_undo.c | 133 ++++++++++++++++++++++++++++++++++++++---- doc/develop/expo.rst | 14 +++++ doc/usage/cmdline.rst | 68 +++++++++++++++++++++ include/cli.h | 86 ++++++++++++++++++++++++--- test/boot/editenv.c | 62 ++++++++++++++++++-- test/boot/expo.c | 12 +++- 10 files changed, 446 insertions(+), 39 deletions(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index ab4fd5056a0..101616acc07 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,12 @@ #include #include "scene_internal.h" +#ifdef CONFIG_CMDLINE_UNDO_COUNT +#define UNDO_COUNT CONFIG_CMDLINE_UNDO_COUNT +#else +#define UNDO_COUNT 64 +#endif + int scene_txtin_init(struct scene_txtin *tin, uint size, uint line_chars) { char *buf; @@ -161,6 +168,10 @@ static void scene_txtin_putch(struct cli_line_state *cls, int ch) void scene_txtin_close(struct scene *scn, struct scene_txtin *tin) { + struct cli_line_state *cls = &tin->cls; + + cli_cread_uninit(cls); + /* cursor is not needed now */ vidconsole_readline_end(scn->expo->cons, tin->ctx); } @@ -265,7 +276,7 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, struct udevice *cons = scn->expo->cons; struct scene_obj_txt *txt; void *ctx; - int ret; + int ret, i; ctx = tin->ctx; if (!ctx) { @@ -286,10 +297,31 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, vidconsole_set_cursor_pos(cons, ctx, txt->obj.bbox.x0, txt->obj.bbox.y0); vidconsole_entry_start(cons, ctx); - cli_cread_init(cls, abuf_data(&tin->buf), abuf_size(&tin->buf)); + cli_cread_init_undo(cls, abuf_data(&tin->buf), abuf_size(&tin->buf)); cls->insert = true; ed->putch = scene_txtin_putch; cls->priv = scn; + + /* Initialise undo ring buffer */ + alist_init_struct(&ed->undo.pos, struct cli_undo_pos); + for (i = 0; i < UNDO_COUNT; i++) { + struct cli_undo_pos *pos; + + pos = alist_ensure(&ed->undo.pos, i, struct cli_undo_pos); + abuf_init_size(&pos->buf, abuf_size(&tin->buf)); + } + + /* Initialise redo ring buffer */ + alist_init_struct(&ed->redo.pos, struct cli_undo_pos); + for (i = 0; i < UNDO_COUNT; i++) { + struct cli_undo_pos *pos; + + pos = alist_ensure(&ed->redo.pos, i, struct cli_undo_pos); + abuf_init_size(&pos->buf, abuf_size(&tin->buf)); + } + + /* yank buffer is initialised by cli_cread_init_undo() above */ + if (obj->type == SCENEOBJT_TEXTEDIT) { ed->multiline = true; ed->line_nav = scene_txtin_line_nav; diff --git a/cmd/Kconfig b/cmd/Kconfig index 9d96c12fd86..03e8014786b 100644 --- a/cmd/Kconfig +++ b/cmd/Kconfig @@ -66,6 +66,8 @@ config CMDLINE_EDITOR - Undo/redo support (Ctrl+Z / Ctrl+Shift+Z) - Yank/paste of killed text (Ctrl+Y) + This uses additional memory to store the undo, redo, and yank buffers. + config CMDLINE_UNDO bool "Support undo in command-line editing" depends on CMDLINE_EDITOR @@ -75,6 +77,19 @@ config CMDLINE_UNDO pressing Ctrl+Z restores the previous state of the edit buffer. This uses additional memory to store the undo state. +config CMDLINE_UNDO_COUNT + int "Number of undo levels" + depends on CMDLINE_UNDO + default 64 + range 1 64 + help + Number of undo/redo levels to support. Each level requires memory + to store a copy of the edit buffer. With multiple levels, + pressing Ctrl+Z repeatedly undoes progressively older changes, and + Ctrl+Shift+Z redoes them. Set to 1 for single-level undo/redo, or + higher for multi-level. Note that any new edit clears the redo + history. + config CMDLINE_PS_SUPPORT bool "Enable support for changing the command prompt string at run-time" depends on HUSH_PARSER diff --git a/common/cli_getch.c b/common/cli_getch.c index 8df1997911a..810128a9fca 100644 --- a/common/cli_getch.c +++ b/common/cli_getch.c @@ -134,14 +134,20 @@ static int cli_ch_esc(struct cli_ch_state *cch, int ichar, cch->esc_save[3] == ';') act = ESC_SAVE; break; + case '6': + /* Ctrl+Shift+key: ESC [ 1 ; 6 */ + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && + cch->esc_save[3] == ';') + act = ESC_SAVE; + break; } break; case 5: if (ichar == '~') { /* bracketed paste */ ichar = 0; act = ESC_CONVERTED; - } else if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && - cch->esc_save[4] == '5') { + } + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && cch->esc_save[4] == '5') { /* Ctrl+arrow: ESC [ 1 ; 5 D/C */ switch (ichar) { case 'D': /* Ctrl+<- key */ @@ -154,6 +160,15 @@ static int cli_ch_esc(struct cli_ch_state *cch, int ichar, break; /* pass to forward-word handler */ } } + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && cch->esc_save[4] == '6') { + /* Ctrl+Shift+key: ESC [ 1 ; 6 x */ + switch (ichar) { + case 'z': /* Ctrl+Shift+Z: redo */ + ichar = CTL_CH('g'); + act = ESC_CONVERTED; + break; + } + } } *actp = act; diff --git a/common/cli_readline.c b/common/cli_readline.c index d554b7241c6..fac5080cc07 100644 --- a/common/cli_readline.c +++ b/common/cli_readline.c @@ -113,6 +113,14 @@ static char hist_data[HIST_MAX][HIST_SIZE + 1]; #endif static char *hist_list[HIST_MAX]; +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) +#ifdef CONFIG_CMDLINE_UNDO_COUNT +#define UNDO_COUNT CONFIG_CMDLINE_UNDO_COUNT +#else +#define UNDO_COUNT 64 +#endif +#endif + #define add_idx_minus_one() ((hist_add_idx == 0) ? hist_max : hist_add_idx-1) /** @@ -358,7 +366,7 @@ static void cread_end_of_line(struct cli_line_state *cls) #define REFRESH_TO_EOL() GOTO_LINE_END(cls->eol_num) #endif -/* undo/yank functions are in cli_undo.c when CMDLINE_UNDO is enabled */ +/* undo/redo/yank functions are in cli_undo.c when CMDLINE_EDITOR is enabled */ static void cread_add_char(struct cli_line_state *cls, char ichar, int insert, uint *num, uint *eol_num, char *buf, uint len) @@ -374,6 +382,9 @@ static void cread_add_char(struct cli_line_state *cls, char ichar, int insert, (*eol_num)++; } + /* new edit invalidates redo history */ + cread_clear_redo(cls); + if (insert) { wlen = *eol_num - *num; if (wlen > 1) @@ -472,6 +483,7 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) if (cls->num < cls->eol_num) { uint wlen; + cread_save_undo(cls); wlen = cls->eol_num - cls->num - 1; if (wlen) { memmove(&buf[cls->num], &buf[cls->num + 1], @@ -511,6 +523,7 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) if (cls->num) { uint base, wlen; + cread_save_undo(cls); for (base = cls->num - 1; base >= 0 && buf[base] == ' ';) base--; @@ -519,7 +532,6 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) /* now delete chars from base to cls->num */ wlen = cls->num - base; - cread_save_undo(cls); cread_save_yank(cls, &buf[base], wlen); cls->eol_num -= wlen; memmove(&buf[base], &buf[cls->num], @@ -541,13 +553,18 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) } break; case CTL_CH('y'): -#if CONFIG_IS_ENABLED(CMDLINE_UNDO) +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) cread_yank(cls); #endif break; case CTL_CH('z'): cread_restore_undo(cls); break; + case CTL_CH('g'): +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) + cread_redo(cls); +#endif + break; case CTL_CH('u'): cread_save_undo(cls); cread_save_yank(cls, buf, cls->eol_num); @@ -560,6 +577,7 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) if (cls->num) { uint wlen; + cread_save_undo(cls); wlen = cls->eol_num - cls->num; cls->num--; memmove(&buf[cls->num], &buf[cls->num + 1], wlen); @@ -605,6 +623,8 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) break; } + cread_save_undo(cls); + /* nuke the current line */ /* first, go home */ BEGINNING_OF_LINE(); @@ -632,6 +652,7 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) buf[cls->num] = '\0'; col = strlen(cls->prompt) + cls->eol_num; num2 = cls->num; + cread_save_undo(cls); if (cmd_auto_complete(cls->prompt, buf, &num2, &col)) { col = num2 - cls->num; cls->num += col; @@ -669,17 +690,22 @@ void cli_cread_init_undo(struct cli_line_state *cls, char *buf, uint buf_size) if (CONFIG_IS_ENABLED(CMDLINE_UNDO)) { struct cli_editor_state *ed = cli_editor(cls); - abuf_init_size(&ed->undo.buf, buf_size); abuf_init_size(&ed->yank, buf_size); } } void cli_cread_uninit(struct cli_line_state *cls) { - if (CONFIG_IS_ENABLED(CMDLINE_UNDO)) { + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR)) { struct cli_editor_state *ed = cli_editor(cls); - - abuf_uninit(&ed->undo.buf); + struct cli_undo_pos *pos; + + alist_for_each(pos, &ed->undo.pos) + abuf_uninit(&pos->buf); + alist_uninit(&ed->undo.pos); + alist_for_each(pos, &ed->redo.pos) + abuf_uninit(&pos->buf); + alist_uninit(&ed->redo.pos); abuf_uninit(&ed->yank); } } diff --git a/common/cli_undo.c b/common/cli_undo.c index 4aa9a719ebf..43858d4636d 100644 --- a/common/cli_undo.c +++ b/common/cli_undo.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0+ /* - * CLI undo/yank support + * CLI undo/redo/yank support * * Copyright 2025 Google LLC * Written by Simon Glass @@ -55,26 +55,134 @@ static void cls_putchars(struct cli_line_state *cls, int count, int ch) #define getcmd_cbeep(cls) cls_putch(cls, '\a') +void cread_save_redo(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + struct cli_undo_state *redo = &ed->redo; + struct cli_undo_pos *pos; + uint idx; + + if (!redo->pos.alloc) + return; + + /* save at current head position */ + idx = redo->head; + pos = alist_getw(&redo->pos, idx, struct cli_undo_pos); + memcpy(abuf_data(&pos->buf), cls->buf, cls->len); + pos->num = cls->num; + pos->eol_num = cls->eol_num; + + /* advance head (ring buffer) */ + redo->head = (redo->head + 1) % redo->pos.alloc; + + /* track how many redo levels are available */ + if (redo->count < redo->pos.alloc) + redo->count++; +} + +void cread_clear_redo(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + + ed->redo.count = 0; + ed->redo.head = 0; +} + void cread_save_undo(struct cli_line_state *cls) { struct cli_editor_state *ed = cli_editor(cls); struct cli_undo_state *undo = &ed->undo; + struct cli_undo_pos *pos; + uint idx; - if (abuf_size(&undo->buf)) { - memcpy(abuf_data(&undo->buf), cls->buf, cls->len); - undo->num = cls->num; - undo->eol_num = cls->eol_num; - } + if (!undo->pos.alloc) + return; + + /* save at current head position */ + idx = undo->head; + pos = alist_getw(&undo->pos, idx, struct cli_undo_pos); + memcpy(abuf_data(&pos->buf), cls->buf, cls->len); + pos->num = cls->num; + pos->eol_num = cls->eol_num; + + /* advance head (ring buffer) */ + undo->head = (undo->head + 1) % undo->pos.alloc; + + /* track how many undo levels are available */ + if (undo->count < undo->pos.alloc) + undo->count++; + + /* new edit invalidates redo history */ + cread_clear_redo(cls); } void cread_restore_undo(struct cli_line_state *cls) +{ + struct cli_undo_state *undo = &cli_editor(cls)->undo; + const struct cli_undo_pos *pos; + uint idx; + + if (!undo->pos.alloc || !undo->count) + return; + + /* save current state to redo buffer before restoring */ + cread_save_redo(cls); + + /* move back to previous undo state */ + undo->head = undo->head ? undo->head - 1 : undo->pos.alloc - 1; + undo->count--; + idx = undo->head; + + /* go to start of line */ + while (cls->num) { + cls_putch(cls, CTL_BACKSPACE); + cls->num--; + } + + /* erase current content on screen */ + cls_putchars(cls, cls->eol_num, ' '); + cls_putchars(cls, cls->eol_num, CTL_BACKSPACE); + + /* restore from undo buffer */ + pos = alist_get(&undo->pos, idx, struct cli_undo_pos); + memcpy(cls->buf, abuf_data(&pos->buf), cls->len); + cls->eol_num = pos->eol_num; + + /* display restored content */ + cls_putnstr(cls, cls->buf, cls->eol_num); + + /* position cursor */ + cls_putchars(cls, cls->eol_num - pos->num, CTL_BACKSPACE); + cls->num = pos->num; +} + +void cread_redo(struct cli_line_state *cls) { struct cli_editor_state *ed = cli_editor(cls); struct cli_undo_state *undo = &ed->undo; + struct cli_undo_state *redo = &ed->redo; + struct cli_undo_pos *pos; + const struct cli_undo_pos *rpos; + uint idx; - if (!abuf_size(&undo->buf)) + if (!redo->pos.alloc || !redo->count) return; + /* save current state to undo buffer */ + idx = undo->head; + pos = alist_getw(&undo->pos, idx, struct cli_undo_pos); + memcpy(abuf_data(&pos->buf), cls->buf, cls->len); + pos->num = cls->num; + pos->eol_num = cls->eol_num; + undo->head = (undo->head + 1) % undo->pos.alloc; + if (undo->count < undo->pos.alloc) + undo->count++; + + /* move back to previous redo state */ + redo->head = redo->head ? redo->head - 1 : redo->pos.alloc - 1; + redo->count--; + idx = redo->head; + /* go to start of line */ while (cls->num) { cls_putch(cls, CTL_BACKSPACE); @@ -85,16 +193,17 @@ void cread_restore_undo(struct cli_line_state *cls) cls_putchars(cls, cls->eol_num, ' '); cls_putchars(cls, cls->eol_num, CTL_BACKSPACE); - /* restore from undo buffer */ - memcpy(cls->buf, abuf_data(&undo->buf), cls->len); - cls->eol_num = undo->eol_num; + /* restore from redo buffer */ + rpos = alist_get(&redo->pos, idx, struct cli_undo_pos); + memcpy(cls->buf, abuf_data(&rpos->buf), cls->len); + cls->eol_num = rpos->eol_num; /* display restored content */ cls_putnstr(cls, cls->buf, cls->eol_num); /* position cursor */ - cls_putchars(cls, cls->eol_num - undo->num, CTL_BACKSPACE); - cls->num = undo->num; + cls_putchars(cls, cls->eol_num - rpos->num, CTL_BACKSPACE); + cls->num = rpos->num; } void cread_save_yank(struct cli_line_state *cls, const char *text, uint len) diff --git a/doc/develop/expo.rst b/doc/develop/expo.rst index 71e227c532d..046b464d9f2 100644 --- a/doc/develop/expo.rst +++ b/doc/develop/expo.rst @@ -366,6 +366,9 @@ type "textline" A line of text which can be edited + "textedit" + A multi-line text editor + "box" A rectangle with a given line width (not filled) @@ -446,6 +449,17 @@ max-chars: Specifies the maximum number of characters permitted to be in the textline. The user will be prevented from adding more. +Textedit nodes have the same properties as textline nodes, with the following +differences: + +- The editor supports multiple lines of text +- Pressing Enter inserts a newline instead of closing the editor +- Home/End move to start/end of the current line +- Ctrl+K kills to end of the current line (not entire buffer) +- Up/Down (Ctrl+P/N) navigate between lines + +See :doc:`../usage/cmdline` for a full list of editing keys. + Box nodes have the following additional properties: width diff --git a/doc/usage/cmdline.rst b/doc/usage/cmdline.rst index 58240c5279c..e1525b12884 100644 --- a/doc/usage/cmdline.rst +++ b/doc/usage/cmdline.rst @@ -91,3 +91,71 @@ convenient:: => i2c speed 0x30000 Setting bus speed to 196608 Hz + +Command-line editing +-------------------- + +U-Boot supports command-line editing when `CONFIG_CMDLINE_EDITING` is enabled. +This provides an Emacs-like interface for editing commands before they are +executed. The following key bindings are available: + +Cursor movement +~~~~~~~~~~~~~~~ + +- **Left arrow** or **Ctrl+B**: Move cursor left one character +- **Right arrow** or **Ctrl+F**: Move cursor right one character +- **Ctrl+Left** or **Alt+B**: Move cursor left one word +- **Ctrl+Right** or **Alt+F**: Move cursor right one word +- **Home** or **Ctrl+A**: Move to beginning of line +- **End** or **Ctrl+E**: Move to end of line + +Character deletion +~~~~~~~~~~~~~~~~~~ + +- **Backspace** or **Ctrl+H**: Delete character before cursor +- **Delete** or **Ctrl+D**: Delete character at cursor +- **Ctrl+K**: Kill (delete) from cursor to end of line +- **Ctrl+W**: Kill word before cursor +- **Ctrl+U**: Kill entire line +- **Ctrl+X**: Kill entire line (same as Ctrl+U) + +History +~~~~~~~ + +- **Up arrow** or **Ctrl+P**: Previous command in history +- **Down arrow** or **Ctrl+N**: Next command in history + +Undo, redo, and yank +~~~~~~~~~~~~~~~~~~~~ + +When `CONFIG_CMDLINE_UNDO` is enabled, the following features are available: + +- **Ctrl+Z**: Undo the last edit operation +- **Ctrl+Shift+Z**: Redo the last undone operation +- **Ctrl+Y**: Yank (paste) previously killed text + +Text killed by Ctrl+K, Ctrl+W, Ctrl+U, or Ctrl+X is saved to a yank buffer +and can be pasted with Ctrl+Y. + +The number of undo/redo levels can be configured with `CONFIG_CMDLINE_UNDO_COUNT` +(default 1, maximum 64). Each level saves the complete buffer state, +so higher values use more memory. Note that any new edit clears the redo +history. + +Other +~~~~~ + +- **Tab**: Command and argument completion (if `CONFIG_AUTO_COMPLETE` is enabled) +- **Ctrl+C**: Cancel current input +- **Enter**: Execute command + +Multiline editing +~~~~~~~~~~~~~~~~~ + +In multiline mode (used by expo text editors), some keys have modified +behaviour: + +- **Home/End**: Move to start/end of current line (not entire buffer) +- **Ctrl+K**: Kill to end of current line (not entire buffer) +- **Ctrl+P/N** or **Up/Down**: Navigate between lines +- **Enter**: Insert newline (instead of executing) diff --git a/include/cli.h b/include/cli.h index 3040342de8e..1b23caaa078 100644 --- a/include/cli.h +++ b/include/cli.h @@ -8,6 +8,7 @@ #define __CLI_H #include +#include #include #include @@ -29,18 +30,44 @@ struct cli_ch_state { struct cli_line_state; /** - * struct cli_undo_state - state for undo buffer + * struct cli_undo_pos - saved state for a single undo/redo level * - * @buf: Buffer for saved state - * @num: Saved cursor position - * @eol_num: Saved end-of-line position + * Before any editing operation (insert, delete, kill, etc.), the entire + * buffer state is saved so it can be restored on undo. The buffer contents, + * cursor position, and line length are captured together. + * + * @buf: Complete copy of the edit buffer at the time of save + * @num: Cursor position (offset from start of buffer) + * @eol_num: Number of characters in the buffer (end-of-line position) */ -struct cli_undo_state { +struct cli_undo_pos { struct abuf buf; uint num; uint eol_num; }; +/** + * struct cli_undo_state - state for undo/redo ring buffer + * + * This implements a ring buffer for storing undo or redo states. Each state + * consists of a complete copy of the edit buffer plus the cursor position. + * The ring buffer allows multiple levels of undo/redo up to alloc entries. + * + * When saving a new state, it is written at the @head index, then @head + * advances (wrapping at alloc). When restoring, @head moves back and the + * state at that index is restored. The @count tracks how many valid states + * are available for undo/redo. + * + * @pos: List of &struct cli_undo_pos entries + * @head: Index where the next state will be saved (0 to alloc-1) + * @count: Number of valid states available (0 to alloc) + */ +struct cli_undo_state { + struct alist pos; + uint head; + uint count; +}; + /** * struct cli_editor_state - state for enhanced editing features * @@ -51,6 +78,7 @@ struct cli_undo_state { * @multiline: true if input may contain multiple lines (enables * Ctrl-P/N for line navigation instead of history) * @undo: Undo ring buffer state + * @redo: Redo ring buffer state * @yank: Buffer for killed text (for Ctrl+Y yank) * @yank_len: Length of killed text in yank buffer */ @@ -81,6 +109,9 @@ struct cli_editor_state { /** @undo: Undo state (if CONFIG_CMDLINE_UNDO) */ struct cli_undo_state undo; + /** @redo: Redo state (if CONFIG_CMDLINE_UNDO) */ + struct cli_undo_state redo; + /** @yank: Buffer for killed text (for Ctrl+Y yank) */ struct abuf yank; @@ -393,13 +424,14 @@ void cli_cread_add_initial(struct cli_line_state *cls); /** cread_print_hist_list() - Print the command-line history list */ void cread_print_hist_list(void); -/* - * Undo/yank functions - implementations in cli_undo.c when CMDLINE_UNDO is enabled - */ -#if CONFIG_IS_ENABLED(CMDLINE_UNDO) +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) /** * cread_save_undo() - Save current state for undo * + * Saves the buffer contents and cursor position to the undo ring buffer. + * Each call pushes a new undo state that can be restored with Ctrl+Z. + * Also clears the redo buffer since a new edit invalidates redo history. + * * @cls: CLI line state */ void cread_save_undo(struct cli_line_state *cls); @@ -407,13 +439,29 @@ void cread_save_undo(struct cli_line_state *cls); /** * cread_restore_undo() - Restore previous state from undo buffer * + * Restores the buffer contents and cursor position from the most recent + * undo state. Multiple calls restore progressively older states. The + * current state is saved to the redo buffer before restoring. + * * @cls: CLI line state */ void cread_restore_undo(struct cli_line_state *cls); +/** + * cread_redo() - Redo previously undone change + * + * Restores the buffer contents and cursor position from the redo buffer. + * The current state is saved to the undo buffer before restoring. + * + * @cls: CLI line state + */ +void cread_redo(struct cli_line_state *cls); + /** * cread_save_yank() - Save killed text to yank buffer * + * Saves the specified text so it can be yanked (pasted) later with Ctrl+Y. + * * @cls: CLI line state * @text: Text to save * @len: Length of text @@ -423,9 +471,21 @@ void cread_save_yank(struct cli_line_state *cls, const char *text, uint len); /** * cread_yank() - Insert yanked text at cursor position * + * Inserts the previously killed text at the current cursor position. + * * @cls: CLI line state */ void cread_yank(struct cli_line_state *cls); + +/** + * cread_clear_redo() - Clear the redo buffer + * + * Called when a new edit is made to invalidate the redo history. This should + * be called for any edit operation that modifies the buffer. + * + * @cls: CLI line state + */ +void cread_clear_redo(struct cli_line_state *cls); #else static inline void cread_save_undo(struct cli_line_state *cls) { @@ -435,6 +495,10 @@ static inline void cread_restore_undo(struct cli_line_state *cls) { } +static inline void cread_redo(struct cli_line_state *cls) +{ +} + static inline void cread_save_yank(struct cli_line_state *cls, const char *text, uint len) { @@ -443,6 +507,10 @@ static inline void cread_save_yank(struct cli_line_state *cls, const char *text, static inline void cread_yank(struct cli_line_state *cls) { } + +static inline void cread_clear_redo(struct cli_line_state *cls) +{ +} #endif #endif diff --git a/test/boot/editenv.c b/test/boot/editenv.c index ab3c6648886..9a41d269d17 100644 --- a/test/boot/editenv.c +++ b/test/boot/editenv.c @@ -74,8 +74,8 @@ static int editenv_test_base(struct unit_test_state *uts) int ret; /* - * Type "test" then press Ctrl-S to save - * \x13 is Ctrl-S + * Type "test" then press Ctrl-S to accept (Enter inserts newline in + * multiline mode) */ console_in_puts("test\x13"); ret = expo_editenv("myvar", NULL, buf, sizeof(buf)); @@ -94,7 +94,7 @@ static int editenv_test_initial(struct unit_test_state *uts) /* * Start with "world", go to start with Ctrl-A, type "hello ", then - * press Ctrl-S to save + * press Ctrl-S to accept */ console_in_puts("\x01hello \x13"); ret = expo_editenv("myvar", "world", buf, sizeof(buf)); @@ -190,11 +190,61 @@ static int editenv_test_funcs(struct unit_test_state *uts) ut_assertok(editenv_send(&info, CTL_CH('k'))); ut_asserteq(16033, ut_check_video(uts, "kill")); + /* Test undo - should restore the killed text */ + ut_assertok(editenv_send(&info, CTL_CH('z'))); + ut_asserteq(16877, ut_check_video(uts, "undo")); + + /* Kill again and yank it back - text should be restored */ + ut_assertok(editenv_send(&info, CTL_CH('k'))); + ut_asserteq(16033, ut_check_video(uts, "kill2")); + + ut_assertok(editenv_send(&info, CTL_CH('y'))); + ut_asserteq(16808, ut_check_video(uts, "yank")); + + /* Test Home - should go to start of current line */ + ut_assertok(editenv_send(&info, CTL_CH('a'))); + ut_asserteq(0, info.ted->tin.cls.num); + ut_asserteq(16845, ut_check_video(uts, "home")); + + /* Test End - should go to end of current line */ + ut_assertok(editenv_send(&info, CTL_CH('e'))); + ut_asserteq(16808, ut_check_video(uts, "end")); + + /* Go left two words with Ctrl+R */ + ut_assertok(editenv_send(&info, CTL_CH('r'))); + ut_asserteq(16838, ut_check_video(uts, "left1")); + + ut_assertok(editenv_send(&info, CTL_CH('r'))); + ut_asserteq(16812, ut_check_video(uts, "left2")); + + /* Delete three words with Ctrl+W */ + ut_assertok(editenv_send(&info, CTL_CH('w'))); + ut_asserteq(16691, ut_check_video(uts, "delw1")); + + ut_assertok(editenv_send(&info, CTL_CH('w'))); + ut_asserteq(16445, ut_check_video(uts, "delw2")); + + ut_assertok(editenv_send(&info, CTL_CH('w'))); + ut_asserteq(16118, ut_check_video(uts, "delw3")); + + /* Undo to restore one deleted word */ + ut_assertok(editenv_send(&info, CTL_CH('z'))); + ut_asserteq(16445, ut_check_video(uts, "undo1")); + + /* Type a character - this clears the redo buffer */ + ut_assertok(editenv_send(&info, '!')); + ut_asserteq(16469, ut_check_video(uts, "type")); + + /* Redo (Ctrl+G) should do nothing since typing cleared the redo buffer */ + ut_assertok(editenv_send(&info, CTL_CH('g'))); + ut_asserteq(16469, ut_check_video(uts, "redo")); + + /* Press Ctrl-S to save */ ut_asserteq(1, editenv_send(&info, BKEY_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")); + /* The '*' and '!' are inserted; redo did nothing since it was cleared */ + ut_assert(strstr(expo_editenv_result(&info), "tes*ted")); + ut_asserteq(16469, ut_check_video(uts, "save")); expo_editenv_uninit(&info); diff --git a/test/boot/expo.c b/test/boot/expo.c index 5445fed19c1..7a3285535b5 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1735,6 +1735,16 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(90, ted->tin.cls.eol_num); ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[89]); + /* Ctrl+Z undoes the yank */ + ut_assertok(expo_send_key(exp, CTL_CH('z'))); + ut_asserteq(85, ted->tin.cls.num); + ut_asserteq(86, ted->tin.cls.eol_num); + + /* Ctrl+Shift+Z (internal code 'g') redoes the yank */ + ut_assertok(expo_send_key(exp, CTL_CH('g'))); + ut_asserteq(89, ted->tin.cls.num); + ut_asserteq(90, ted->tin.cls.eol_num); + /* clear multiline mode, close the textedit with Enter (BKEY_SELECT) */ ted->obj.flags &= ~SCENEOF_MULTILINE; ut_assertok(expo_send_key(exp, BKEY_SELECT)); @@ -1743,7 +1753,7 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(OBJ_TEXTED, act.select.id); ut_assertok(scene_set_open(scn, act.select.id, false)); - /* check the textedit is closed and text is changed */ + /* check the textedit is closed and text is changed (redo restored latr) */ ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN); ut_asserteq_str("This\ns the initial contents of the text " "editor but it is ely that more will be added latr\n",