From 44307bd7998e5f1fc81d63d74edaf4756ddf5a47 Mon Sep 17 00:00:00 2001 From: Robin Haberkorn Date: Tue, 8 Apr 2025 22:23:35 +0300 Subject: improved rubbing out commands with modifiers * This was actually broken if the command is preceded by `@` and `:` characters, which are __not__ modifiers. E.g. `Q:@I/foo^W` would have rubbed out the `:` register as well. * Also, since it was all done in teco_state_process_edit_cmd(), it would also rub out modifier characters from within string arguments, E.g. `@I/::^EQ^W` * Real commands now have their own ^W rubout implementation, while the generic fallback just rubs out until the start state is re-established. This fails to rub out modifiers as in `@I/^W`, though. * Real command characters now use the common TECO_DEFINE_STATE_COMMAND(). * Added test cases for CTRL+W rub out. A few control characters are now portably available to tests via environment variables `$ESCAPE`, `$RUBOUT` and `$RUBOUT_WORD`. --- doc/sciteco.7.template | 2 + src/cmdline.c | 119 ++++++++++++++++++++++++++++++++----------------- src/core-commands.c | 43 +++++++++++------- tests/atlocal.in | 9 ++++ tests/testsuite.at | 32 ++++++++++--- 5 files changed, 142 insertions(+), 63 deletions(-) diff --git a/doc/sciteco.7.template b/doc/sciteco.7.template index d5fc4af..1fe1dba 100644 --- a/doc/sciteco.7.template +++ b/doc/sciteco.7.template @@ -518,6 +518,8 @@ Miscelleaneous .br (modifier \fIdisabled\fP) T};T{ +\# Strictly speaking, it only does that from the start state. +\# At the beginning of strings or Q-reg specs, it only erases until the start state. Rub out until the beginning of the last command, which is not a no-op (whitespace). \(lq@\(rq and \(lq:\(rq modifiers are considered part of the command and also rubbed out. T} diff --git a/src/cmdline.c b/src/cmdline.c index 3fb7cb9..e367e9a 100644 --- a/src/cmdline.c +++ b/src/cmdline.c @@ -446,56 +446,22 @@ teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gun } return TRUE; - case TECO_CTL_KEY('W'): /* rubout/reinsert command */ + case TECO_CTL_KEY('W'): /* rubout/reinsert construct */ teco_interface_popup_clear(); - /* - * This mimics the behavior of the `Y` command, - * so it also rubs out no-op commands. - * See also teco_find_words(). - */ if (teco_cmdline.modifier_enabled) { - /* reinsert command */ - /* @ and : are not separate states, but practically belong to the command */ - while (ctx->current->is_start && - teco_cmdline.effective_len < teco_cmdline.str.len && - (teco_cmdline.str.data[teco_cmdline.effective_len] == ':' || - teco_cmdline.str.data[teco_cmdline.effective_len] == '@')) - if (!teco_cmdline_rubin(error)) - return FALSE; - + /* reinsert construct */ do { if (!teco_cmdline_rubin(error)) return FALSE; } while (!ctx->current->is_start && teco_cmdline.effective_len < teco_cmdline.str.len); - - while (ctx->current->is_start && - teco_cmdline.effective_len < teco_cmdline.str.len && - strchr(TECO_NOOPS, teco_cmdline.str.data[teco_cmdline.effective_len])) - if (!teco_cmdline_rubin(error)) - return FALSE; - - return TRUE; + } else { + /* rubout construct */ + do + teco_cmdline_rubout(); + while (!ctx->current->is_start); } - - /* rubout command */ - while (ctx->current->is_start && - teco_cmdline.effective_len > 0 && - strchr(TECO_NOOPS, teco_cmdline.str.data[teco_cmdline.effective_len-1])) - teco_cmdline_rubout(); - - do - teco_cmdline_rubout(); - while (!ctx->current->is_start); - - /* @ and : are not separate states, but practically belong to the command */ - while (ctx->current->is_start && - teco_cmdline.effective_len > 0 && - (teco_cmdline.str.data[teco_cmdline.effective_len-1] == ':' || - teco_cmdline.str.data[teco_cmdline.effective_len-1] == '@')) - teco_cmdline_rubout(); - return TRUE; #if !defined(INTERFACE_GTK) && defined(SIGTSTP) @@ -533,6 +499,69 @@ teco_state_caseinsensitive_process_edit_cmd(teco_machine_t *ctx, teco_machine_t return teco_state_process_edit_cmd(ctx, parent_ctx, key, error); } +gboolean +teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) +{ + switch (key) { + case TECO_CTL_KEY('W'): /* rubout/reinsert command */ + teco_interface_popup_clear(); + + /* + * This mimics the behavior of the `Y` command, + * so it also rubs out no-op commands. + * See also teco_find_words(). + */ + if (teco_cmdline.modifier_enabled) { + /* reinsert command */ + /* @ and : are not separate states, but practically belong to the command */ + while (ctx->parent.current->is_start && + teco_cmdline.effective_len < teco_cmdline.str.len && + (teco_cmdline.str.data[teco_cmdline.effective_len] == ':' || + teco_cmdline.str.data[teco_cmdline.effective_len] == '@')) + if (!teco_cmdline_rubin(error)) + return FALSE; + + do { + if (!teco_cmdline_rubin(error)) + return FALSE; + } while (!ctx->parent.current->is_start && + teco_cmdline.effective_len < teco_cmdline.str.len); + + while (ctx->parent.current->is_start && + teco_cmdline.effective_len < teco_cmdline.str.len && + strchr(TECO_NOOPS, teco_cmdline.str.data[teco_cmdline.effective_len])) + if (!teco_cmdline_rubin(error)) + return FALSE; + + return TRUE; + } + + /* rubout command */ + while (ctx->parent.current->is_start && + teco_cmdline.effective_len > 0 && + strchr(TECO_NOOPS, teco_cmdline.str.data[teco_cmdline.effective_len-1])) + teco_cmdline_rubout(); + + do + teco_cmdline_rubout(); + while (!ctx->parent.current->is_start); + + /* + * @ and : are not separate states, but practically belong to the command. + * We cannot rely on the last character though, since it might + * be part of another command. + */ + while (ctx->parent.current->is_start && + (ctx->modifier_at || ctx->modifier_colon) && + teco_cmdline.effective_len > 0) + teco_cmdline_rubout(); + + return TRUE; + } + + return teco_state_caseinsensitive_process_edit_cmd(&ctx->parent, parent_ctx, key, error); +} + gboolean teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) @@ -678,6 +707,10 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * * Chaining to the parent (embedding) state machine's handler * makes sure that ^W at the beginning of the string argument * rubs out the entire string command. + * + * FIXME: This does not rub out modifiers in front of + * string commands since this callback could be used in recursively + * embedded string building machines as well. */ return teco_state_process_edit_cmd(parent_ctx, NULL, key, error); } @@ -1050,6 +1083,10 @@ teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_ * the state machine. In particular ^W would crash. * This also makes sure that commands like are completely * rub out via ^W. + * + * FIXME: This does not rub out modifiers in front of + * Q-Reg commands since this callback could be used in recursively + * embedded Q-Reg specification machines as well. */ return teco_state_process_edit_cmd(parent_ctx, NULL, key, error); } diff --git a/src/core-commands.c b/src/core-commands.c index 5d6e3b9..7845dc2 100644 --- a/src/core-commands.c +++ b/src/core-commands.c @@ -47,6 +47,25 @@ #include "move-commands.h" #include "core-commands.h" +gboolean teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar key, GError **error); + +/** + * @class TECO_DEFINE_STATE_COMMAND + * @implements TECO_DEFINE_STATE_CASEINSENSITIVE + * @ingroup states + * + * Base state for everything that is the beginning of a one or two + * letter command. + */ +#define TECO_DEFINE_STATE_COMMAND(NAME, ...) \ + TECO_DEFINE_STATE_CASEINSENSITIVE(NAME, \ + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ + teco_state_command_process_edit_cmd, \ + .style = SCE_SCITECO_COMMAND, \ + ##__VA_ARGS__ \ + ) + static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error); /* @@ -822,11 +841,10 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) teco_ascii_toupper(chr), error); } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_start, +TECO_DEFINE_STATE_COMMAND(teco_state_start, .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */ .is_start = TRUE, - .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE, - .style = SCE_SCITECO_COMMAND + .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE ); /*$ F< @@ -983,9 +1001,7 @@ teco_state_fcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error teco_ascii_toupper(chr), error); } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_fcommand, - .style = SCE_SCITECO_COMMAND -); +TECO_DEFINE_STATE_COMMAND(teco_state_fcommand); static void teco_undo_change_dir_action(gchar **dir, gboolean run) @@ -1192,7 +1208,7 @@ teco_state_condcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **er return &teco_state_start; } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_condcommand, +TECO_DEFINE_STATE_COMMAND(teco_state_condcommand, .style = SCE_SCITECO_OPERATOR ); @@ -1533,9 +1549,7 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error) teco_ascii_toupper(chr), error); } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_control, - .style = SCE_SCITECO_COMMAND -); +TECO_DEFINE_STATE_COMMAND(teco_state_control); static teco_state_t * teco_state_ascii_input(teco_machine_main_t *ctx, gunichar chr, GError **error) @@ -1661,15 +1675,14 @@ teco_state_escape_end_of_macro(teco_machine_t *ctx, GError **error) return teco_expressions_discard_args(error); } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_escape, +TECO_DEFINE_STATE_COMMAND(teco_state_escape, .end_of_macro_cb = teco_state_escape_end_of_macro, /* * The state should behave like teco_state_start * when it comes to function key macro masking. */ .is_start = TRUE, - .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE, - .style = SCE_SCITECO_COMMAND + .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE ); /*$ ED flags @@ -2464,9 +2477,7 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error teco_ascii_toupper(chr), error); } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_ecommand, - .style = SCE_SCITECO_COMMAND -); +TECO_DEFINE_STATE_COMMAND(teco_state_ecommand); gboolean teco_state_insert_initial(teco_machine_main_t *ctx, GError **error) diff --git a/tests/atlocal.in b/tests/atlocal.in index adaf928..8465f96 100644 --- a/tests/atlocal.in +++ b/tests/atlocal.in @@ -13,6 +13,15 @@ fi # For testing command-line editing: SCITECO_CMDLINE="$SCITECO --no-profile --fake-cmdline" +# Control characters for testing immediate editing commands with $SCITECO_CMDLINE. +# Often, we can use {...} for testing rubout, but sometimes this is not enough. +# Directly embedding escapes into strings is not portable. +# Theoretically, we could directly embed control codes, but for the time being +# I am trying to keep non-TECO sources clean of non-printable characters. +RUBOUT=`printf '\8'` +RUBOUT_WORD=`printf '\27'` +ESCAPE=`printf '\33'` + # Make sure that the standard library from the source package # is used. SCITECOPATH="@abs_top_srcdir@/lib" diff --git a/tests/testsuite.at b/tests/testsuite.at index cb1fe61..20869f4 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -198,6 +198,23 @@ AT_CHECK([$SCITECO -e "@EC'dd if=/dev/zero bs=512 count=1' Z= Z-512\"N(0/0)'"], AT_CHECK([$SCITECO -e "0,128ED @EC'dd if=/dev/zero bs=512 count=1' Z= Z-512\"N(0/0)'"], 0, ignore, ignore) AT_CLEANUP +# +# Command-line editing. +# +# This either uses the portable $RUBOUT and $RUBOUT_WORD variables or +# we use the push/pop command-line commands { and } from start states. +# +# NOTE: Most errors are not reported in exit codes - you must check stderr. +# +AT_SETUP([Rub out with immediate editing commands]) +# Must rub out @, but not the colon from the Q-Reg specification. +AT_CHECK([$SCITECO_CMDLINE "Q:@I/XXX/ ${RUBOUT_WORD}{Z-2\"N(0/0)'}"], 0, ignore, stderr) +AT_FAIL_IF([$GREP "^Error:" stderr]) +# Should not rub out @ and : characters. +AT_CHECK([$SCITECO_CMDLINE "@I/ @:foo ${RUBOUT_WORD}/ Z-3\"N(0/0)'"], 0, ignore, stderr) +AT_FAIL_IF([$GREP "^Error:" stderr]) +AT_CLEANUP + AT_BANNER([Regression Tests]) AT_SETUP([Glob patterns with character classes]) @@ -293,12 +310,8 @@ AT_CHECK([$SCITECO_CMDLINE "!foo!{-5D}"], 0, ignore, stderr) AT_CLEANUP # -# Command-line editing bugs. -# -# NOTE: It would generally be possible to use control codes like ^H (8) -# and ^W (23) for rubout as well, but this is tricky to write in a portable manner. -# Therefore we usally use the push/pop command-line commands { and }. -# NOTE: Most errors are not reported in exit codes - you must check stderr. +# Command-line editing regressions: +# See above for rules. # AT_SETUP([Rub out string append]) AT_CHECK([$SCITECO_CMDLINE "@I/XXX/ H:Xa{-4D} :Qa-0\"N(0/0)'"], 0, ignore, stderr) @@ -364,3 +377,10 @@ AT_SETUP([Recursion overflow]) AT_CHECK([$SCITECO -e "@^Um{U.a Q.a-100000\"<%.aMm'} 0Mm"], 0, ignore, ignore) AT_XFAIL_IF(true) AT_CLEANUP + +AT_SETUP([Rub out from empty string argument]) +# Should rub out the modifiers as well. +AT_CHECK([$SCITECO_CMDLINE ":@^Ua/${RUBOUT_WORD}{Z\"N(0/0)'}"], 0, ignore, stderr) +AT_CHECK([$GREP "^Error:" stderr], 0, ignore ignore) +AT_XFAIL_IF(true) +AT_CLEANUP -- cgit v1.2.3