aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/cmdline.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmdline.c')
-rw-r--r--src/cmdline.c1058
1 files changed, 1058 insertions, 0 deletions
diff --git a/src/cmdline.c b/src/cmdline.c
new file mode 100644
index 0000000..85cbbdd
--- /dev/null
+++ b/src/cmdline.c
@@ -0,0 +1,1058 @@
+/*
+ * Copyright (C) 2012-2021 Robin Haberkorn
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+#include <signal.h>
+
+#ifdef HAVE_MALLOC_H
+#include <malloc.h>
+#endif
+#ifdef HAVE_MALLOC_NP_H
+#include <malloc_np.h>
+#endif
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "file-utils.h"
+#include "interface.h"
+#include "view.h"
+#include "expressions.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "qreg-commands.h"
+#include "qreg.h"
+#include "ring.h"
+#include "goto.h"
+#include "help.h"
+#include "undo.h"
+#include "scintilla.h"
+#include "spawn.h"
+#include "eol.h"
+#include "error.h"
+#include "qreg.h"
+#include "cmdline.h"
+
+#if defined(HAVE_MALLOC_TRIM) && !defined(HAVE_DECL_MALLOC_TRIM)
+int malloc_trim(size_t pad);
+#endif
+
+#define TECO_DEFAULT_BREAK_CHARS " \t\v\r\n\f<>,;@"
+
+teco_cmdline_t teco_cmdline = {};
+
+/*
+ * FIXME: Should this be here?
+ * Should perhaps rather be in teco_machine_main_t or teco_cmdline_t.
+ */
+gboolean teco_quit_requested = FALSE;
+
+/** Last terminated command line */
+static teco_string_t teco_last_cmdline = {NULL, 0};
+
+/**
+ * Insert string into command line and execute
+ * it immediately.
+ * It already handles command line replacement (TECO_ERROR_CMDLINE).
+ *
+ * @param data String to insert.
+ * NULL inserts a character from the previously
+ * rubbed out command line (rubin).
+ * @param len Length of string to insert.
+ * @param error A GError.
+ * @return FALSE to throw a GError
+ */
+/*
+ * FIXME: Passing data == NULL to perform a rubin is inelegant.
+ * Better make teco_cmdline_rubin() a proper function.
+ * FIXME: The inner loop should be factored out.
+ */
+gboolean
+teco_cmdline_insert(const gchar *data, gsize len, GError **error)
+{
+ const teco_string_t src = {(gchar *)data, len};
+ teco_string_t old_cmdline = {NULL, 0};
+ guint repl_pc = 0;
+
+ teco_cmdline.machine.macro_pc = teco_cmdline.pc = teco_cmdline.effective_len;
+
+ if (!data) {
+ if (teco_cmdline.effective_len < teco_cmdline.str.len)
+ teco_cmdline.effective_len++;
+ } else {
+ if (!teco_string_cmp(&src, teco_cmdline.str.data + teco_cmdline.effective_len,
+ teco_cmdline.str.len - teco_cmdline.effective_len)) {
+ teco_cmdline.effective_len += len;
+ } else {
+ if (teco_cmdline.effective_len < teco_cmdline.str.len)
+ /* automatically disable immediate editing modifier */
+ teco_cmdline.modifier_enabled = FALSE;
+
+ teco_cmdline.str.len = teco_cmdline.effective_len;
+ teco_string_append(&teco_cmdline.str, data, len);
+ teco_cmdline.effective_len = teco_cmdline.str.len;
+ }
+ }
+
+ /*
+ * Parse/execute characters, one at a time so
+ * undo tokens get emitted for the corresponding characters.
+ */
+ while (teco_cmdline.pc < teco_cmdline.effective_len) {
+ g_autoptr(GError) tmp_error = NULL;
+
+ if (!teco_machine_main_step(&teco_cmdline.machine, teco_cmdline.str.data,
+ teco_cmdline.pc+1, &tmp_error)) {
+ if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_CMDLINE)) {
+ /*
+ * Result of command line replacement (}):
+ * Exchange command lines, avoiding deep copying
+ */
+ teco_qreg_t *cmdline_reg = teco_qreg_table_find(&teco_qreg_table_globals, "\e", 1);
+ teco_string_t new_cmdline;
+
+ if (!cmdline_reg->vtable->get_string(cmdline_reg, &new_cmdline.data, &new_cmdline.len, error))
+ return FALSE;
+
+ /*
+ * Search for first differing character in old and
+ * new command line. This avoids unnecessary rubouts
+ * and insertions when the command line is updated.
+ */
+ teco_cmdline.pc = teco_string_diff(&teco_cmdline.str, new_cmdline.data, new_cmdline.len);
+
+ teco_undo_pop(teco_cmdline.pc);
+
+ g_assert(old_cmdline.len == 0);
+ old_cmdline = teco_cmdline.str;
+ teco_cmdline.str = new_cmdline;
+ teco_cmdline.effective_len = new_cmdline.len;
+ teco_cmdline.machine.macro_pc = repl_pc = teco_cmdline.pc;
+
+ continue;
+ }
+
+ if (tmp_error->domain != TECO_ERROR || tmp_error->code < TECO_ERROR_CMDLINE) {
+ teco_error_add_frame_toplevel();
+ teco_error_display_short(tmp_error);
+
+ if (old_cmdline.len > 0) {
+ /*
+ * Error during command-line replacement.
+ * Replay previous command-line.
+ * This avoids deep copying.
+ */
+ teco_undo_pop(repl_pc);
+
+ teco_string_clear(&teco_cmdline.str);
+ teco_cmdline.str = old_cmdline;
+ teco_cmdline.machine.macro_pc = teco_cmdline.pc = repl_pc;
+
+ /* rubout cmdline replacement command */
+ teco_cmdline.effective_len--;
+ continue;
+ }
+ }
+
+ /* error is handled in teco_cmdline_keypress_c() */
+ g_propagate_error(error, g_steal_pointer(&tmp_error));
+ return FALSE;
+ }
+
+ teco_cmdline.pc++;
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_cmdline_keypress_c(gchar key, GError **error)
+{
+ teco_machine_t *machine = &teco_cmdline.machine.parent;
+ g_autoptr(GError) tmp_error = NULL;
+
+ /*
+ * Cleanup messages,etc...
+ */
+ teco_interface_msg_clear();
+
+ /*
+ * Process immediate editing commands, inserting
+ * characters as necessary into the command line.
+ */
+ if (!machine->current->process_edit_cmd_cb(machine, NULL, key, &tmp_error)) {
+ if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) {
+ /*
+ * Return from top-level macro, results
+ * in command line termination.
+ * The return "arguments" are currently
+ * ignored.
+ */
+ g_assert(machine->current == &teco_state_start);
+
+ teco_interface_popup_clear();
+
+ if (teco_quit_requested) {
+ /* cought by user interface */
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, "");
+ return FALSE;
+ }
+
+ teco_undo_clear();
+ /* also empties all Scintilla undo buffers */
+ teco_ring_set_scintilla_undo(TRUE);
+ teco_view_set_scintilla_undo(teco_qreg_view, TRUE);
+ /*
+ * FIXME: Reset main machine?
+ */
+ teco_goto_table_clear(&teco_cmdline.machine.goto_table);
+ teco_expressions_clear();
+ g_array_remove_range(teco_loop_stack, 0, teco_loop_stack->len);
+
+ teco_string_clear(&teco_last_cmdline);
+ teco_last_cmdline = teco_cmdline.str;
+ memset(&teco_cmdline.str, 0, sizeof(teco_cmdline.str));
+ teco_cmdline.effective_len = 0;
+ } else {
+ /*
+ * NOTE: Error message already displayed in
+ * teco_cmdline_insert().
+ *
+ * Undo tokens may have been emitted
+ * (or had to be) before the exception
+ * is thrown. They must be executed so
+ * as if the character had never been
+ * inserted.
+ */
+ teco_undo_pop(teco_cmdline.pc);
+ teco_cmdline.effective_len = teco_cmdline.pc;
+ /* program counter could be messed up */
+ teco_cmdline.machine.macro_pc = teco_cmdline.effective_len;
+ }
+
+#ifdef HAVE_MALLOC_TRIM
+ /*
+ * Undo stacks can grow very large - sometimes large enough to
+ * make the system swap and become unresponsive.
+ * This shrinks the program break after lots of memory has
+ * been freed, reducing the virtual memory size and aiding
+ * in recovering from swapping issues.
+ *
+ * This is particularily important with some memory limiting backends
+ * after hitting the memory limit* as otherwise the program's resident
+ * size won't shrink and it would be impossible to recover.
+ */
+ if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN) ||
+ g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_MEMLIMIT))
+ malloc_trim(0);
+#endif
+ }
+
+ /*
+ * Echo command line
+ */
+ teco_interface_cmdline_update(&teco_cmdline);
+ return TRUE;
+}
+
+gboolean
+teco_cmdline_fnmacro(const gchar *name, GError **error)
+{
+ /*
+ * NOTE: It should be safe to allocate on the stack since
+ * there are only a limited number of possible function key macros.
+ */
+ gchar macro_name[1 + strlen(name)];
+ macro_name[0] = TECO_CTL_KEY('F');
+ memcpy(macro_name+1, name, sizeof(macro_name)-1);
+
+ teco_qreg_t *macro_reg;
+
+ if (teco_ed & TECO_ED_FNKEYS &&
+ (macro_reg = teco_qreg_table_find(&teco_qreg_table_globals, macro_name, sizeof(macro_name)))) {
+ teco_int_t macro_mask;
+ if (!macro_reg->vtable->get_integer(macro_reg, &macro_mask, error))
+ return FALSE;
+
+ if (macro_mask & teco_cmdline.machine.parent.current->fnmacro_mask)
+ return TRUE;
+
+ g_auto(teco_string_t) macro_str = {NULL, 0};
+ return macro_reg->vtable->get_string(macro_reg, &macro_str.data, &macro_str.len, error) &&
+ teco_cmdline_keypress(macro_str.data, macro_str.len, error);
+ }
+
+ /*
+ * Most function key macros have no default action,
+ * except "CLOSE" which quits the application
+ * (this may loose unsaved data but is better than
+ * not doing anything if the user closes the window).
+ * NOTE: Doing the check here is less efficient than
+ * doing it in the UI implementations, but defines
+ * the default actions centrally.
+ * Also, fnmacros are only handled after key presses.
+ */
+ if (!strcmp(name, "CLOSE")) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, "");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_cmdline_cleanup(void)
+{
+ teco_machine_main_clear(&teco_cmdline.machine);
+ teco_string_clear(&teco_cmdline.str);
+ teco_string_clear(&teco_last_cmdline);
+}
+#endif
+
+/*
+ * Commandline key processing.
+ *
+ * These are all the implementations of teco_state_process_edit_cmd_cb_t.
+ * It makes sense to use state callbacks for key processing, as it is
+ * largely state-dependant; but it defines interactive-mode-only
+ * behaviour which can be kept isolated from the rest of the states'
+ * implementation.
+ */
+
+gboolean
+teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ switch (key) {
+ case '\n': /* insert EOL sequence */
+ teco_interface_popup_clear();
+
+ if (teco_ed & TECO_ED_AUTOEOL) {
+ if (!teco_cmdline_insert("\n", 1, error))
+ return FALSE;
+ } else {
+ const gchar *eol = teco_eol_get_seq(teco_interface_ssm(SCI_GETEOLMODE, 0, 0));
+ if (!teco_cmdline_insert(eol, strlen(eol), error))
+ return FALSE;
+ }
+ return TRUE;
+
+ case TECO_CTL_KEY('G'): /* toggle immediate editing modifier */
+ teco_interface_popup_clear();
+
+ teco_cmdline.modifier_enabled = !teco_cmdline.modifier_enabled;
+ teco_interface_msg(TECO_MSG_INFO,
+ "Immediate editing modifier is now %s.",
+ teco_cmdline.modifier_enabled ? "enabled" : "disabled");
+ return TRUE;
+
+ case TECO_CTL_KEY('H'): /* rubout/reinsert character */
+ teco_interface_popup_clear();
+
+ if (teco_cmdline.modifier_enabled) {
+ /* re-insert character */
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+ } else {
+ /* rubout character */
+ teco_cmdline_rubout();
+ }
+ return TRUE;
+
+ case TECO_CTL_KEY('W'): /* rubout/reinsert command */
+ teco_interface_popup_clear();
+
+ if (teco_cmdline.modifier_enabled) {
+ /* reinsert command */
+ do {
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+ } while (!ctx->current->is_start &&
+ teco_cmdline.effective_len < teco_cmdline.str.len);
+ } else {
+ /* rubout command */
+ do
+ teco_cmdline_rubout();
+ while (!ctx->current->is_start);
+ }
+ return TRUE;
+
+#ifdef SIGTSTP
+ case TECO_CTL_KEY('Z'):
+ /*
+ * <CTL/Z> does not raise signal if handling of
+ * special characters temporarily disabled in terminal
+ * (Curses), or command-line is detached from
+ * terminal (GTK+).
+ * This does NOT change the state of the popup window.
+ */
+ raise(SIGTSTP);
+ return TRUE;
+#endif
+ }
+
+ teco_interface_popup_clear();
+ return teco_cmdline_insert(&key, sizeof(key), error);
+}
+
+gboolean
+teco_state_caseinsensitive_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ if (teco_ed & TECO_ED_AUTOCASEFOLD)
+ /* will not modify non-letter keys */
+ key = g_ascii_islower(key) ? g_ascii_toupper(key)
+ : g_ascii_tolower(key);
+
+ return teco_state_process_edit_cmd(ctx, parent_ctx, key, error);
+}
+
+/*
+ * NOTE: The wordchars are null-terminated, so the null byte
+ * is always considered to be a non-wordchar.
+ */
+static inline gboolean
+teco_is_wordchar(const gchar *wordchars, gchar c)
+{
+ return c != '\0' && strchr(wordchars, c);
+}
+
+gboolean
+teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
+ gchar key, GError **error)
+{
+ teco_state_t *current = ctx->parent.current;
+
+ switch (key) {
+ case TECO_CTL_KEY('W'): { /* rubout/reinsert word */
+ teco_interface_popup_clear();
+
+ g_autofree gchar *wchars = g_malloc(teco_interface_ssm(SCI_GETWORDCHARS, 0, 0));
+ teco_interface_ssm(SCI_GETWORDCHARS, 0, (sptr_t)wchars);
+
+ if (teco_cmdline.modifier_enabled) {
+ /* reinsert word chars */
+ while (ctx->parent.current == current &&
+ teco_cmdline.effective_len < teco_cmdline.str.len &&
+ teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len]))
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+
+ /* reinsert non-word chars */
+ while (ctx->parent.current == current &&
+ teco_cmdline.effective_len < teco_cmdline.str.len &&
+ !teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len]))
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+
+ return TRUE;
+ }
+
+ /*
+ * FIXME: In parse-only mode (ctx->result == NULL), we only
+ * get the default behaviour of teco_state_process_edit_cmd().
+ * This may not be a real-life issue serious enough to maintain
+ * a result string even in parse-only mode.
+ *
+ * FIXME: Does not properly rubout string-building commands at the
+ * start of the string argument -- ctx->result->len is not
+ * a valid indicator of argument emptyness.
+ * Since it chains to teco_state_process_edit_cmd() we will instead
+ * rubout the entire command.
+ */
+ if (ctx->result && ctx->result->len > 0) {
+ gboolean is_wordchar = teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]);
+ teco_cmdline_rubout();
+ if (ctx->parent.current != current) {
+ /* rub out string building command */
+ while (ctx->result->len > 0 && ctx->parent.current != current)
+ teco_cmdline_rubout();
+ return TRUE;
+ }
+
+ /*
+ * rubout non-word chars
+ * FIXME: This might rub out part of string building commands, e.g. "EQ[A] ^W"
+ */
+ if (!is_wordchar) {
+ while (ctx->result->len > 0 &&
+ !teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ teco_cmdline_rubout();
+ }
+
+ /* rubout word chars */
+ while (ctx->result->len > 0 &&
+ teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ teco_cmdline_rubout();
+
+ return TRUE;
+ }
+
+ /*
+ * Otherwise, the entire string command will be rubbed out.
+ */
+ break;
+ }
+
+ case TECO_CTL_KEY('U'): /* rubout/reinsert entire string */
+ teco_interface_popup_clear();
+
+ if (teco_cmdline.modifier_enabled) {
+ /* reinsert string */
+ while (ctx->parent.current == current &&
+ teco_cmdline.effective_len < teco_cmdline.str.len)
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+
+ return TRUE;
+ }
+
+ /*
+ * FIXME: In parse only mode (ctx->result == NULL),
+ * this will chain to teco_state_process_edit_cmd() and rubout
+ * only a single character.
+ */
+ if (ctx->result) {
+ /* rubout string */
+ while (ctx->result->len > 0)
+ teco_cmdline_rubout();
+ return TRUE;
+ }
+
+ break;
+
+ case '\t': { /* autocomplete file name */
+ /*
+ * FIXME: Does not autocomplete in parse-only mode (ctx->result == NULL).
+ */
+ if (!teco_cmdline.modifier_enabled || !ctx->result)
+ break;
+
+ /*
+ * TODO: In insertion commands, we can autocomplete
+ * the string at the buffer cursor.
+ */
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ const gchar *filename = teco_string_last_occurrence(ctx->result,
+ TECO_DEFAULT_BREAK_CHARS);
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_file_auto_complete(filename, G_FILE_TEST_EXISTS, &new_chars);
+ teco_machine_stringbuilding_escape(ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ' ');
+
+ if (!teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error))
+ return FALSE;
+
+ /* may be reset if there was a rubbed out command line */
+ teco_cmdline.modifier_enabled = TRUE;
+
+ return TRUE;
+ }
+ }
+
+ /*
+ * 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.
+ */
+ return teco_state_process_edit_cmd(parent_ctx, NULL, key, error);
+}
+
+gboolean
+teco_state_stringbuilding_qreg_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
+ gchar chr, GError **error)
+{
+ g_assert(ctx->machine_qregspec != NULL);
+ /* We downcast since teco_machine_qregspec_t is private in qreg.c */
+ teco_machine_t *machine = (teco_machine_t *)ctx->machine_qregspec;
+ return machine->current->process_edit_cmd_cb(machine, &ctx->parent, chr, error);
+}
+
+gboolean
+teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* insert <TAB> indention */
+ if (teco_cmdline.modifier_enabled || teco_interface_ssm(SCI_GETUSETABS, 0, 0))
+ break;
+
+ teco_interface_popup_clear();
+
+ /* insert soft tabs */
+ gint spaces = teco_interface_ssm(SCI_GETTABWIDTH, 0, 0);
+ gint pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ spaces -= teco_interface_ssm(SCI_GETCOLUMN, pos, 0) % spaces;
+
+ while (spaces--)
+ if (!teco_cmdline_insert(" ", 1, error))
+ return FALSE;
+
+ return TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case TECO_CTL_KEY('W'): /* rubout/reinsert file names including directories */
+ teco_interface_popup_clear();
+
+ if (teco_cmdline.modifier_enabled) {
+ /* reinsert one level of file name */
+ while (stringbuilding_ctx->parent.current == stringbuilding_current &&
+ teco_cmdline.effective_len < teco_cmdline.str.len &&
+ !G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len]))
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+
+ /* reinsert final directory separator */
+ if (stringbuilding_ctx->parent.current == stringbuilding_current &&
+ teco_cmdline.effective_len < teco_cmdline.str.len &&
+ G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len]) &&
+ !teco_cmdline_rubin(error))
+ return FALSE;
+
+ return TRUE;
+ }
+
+ if (ctx->expectstring.string.len > 0) {
+ /* rubout directory separator */
+ if (G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ teco_cmdline_rubout();
+
+ /* rubout one level of file name */
+ while (ctx->expectstring.string.len > 0 &&
+ !G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ teco_cmdline_rubout();
+
+ return TRUE;
+ }
+
+ /*
+ * Rub out entire command instead of rubbing out nothing.
+ */
+ break;
+
+ case '\t': { /* autocomplete file name */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ /* null-byte not allowed in file names */
+ return TRUE;
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_file_auto_complete(ctx->expectstring.string.data, G_FILE_TEST_EXISTS, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous && ctx->expectstring.nesting == 1)
+ teco_string_append_c(&new_chars_escaped,
+ ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
+
+ return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error);
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete directory */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ /* null-byte not allowed in file names */
+ return TRUE;
+
+ /*
+ * FIXME: We might terminate the command in case of leaf directories.
+ */
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ teco_file_auto_complete(ctx->expectstring.string.data, G_FILE_TEST_IS_DIR, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+
+ return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error);
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ g_assert(ctx->expectqreg != NULL);
+ /*
+ * NOTE: teco_machine_qregspec_t is private, so we downcast to teco_machine_t.
+ * Otherwise, we'd have to move this callback into qreg.c.
+ */
+ teco_state_t *expectqreg_current = ((teco_machine_t *)ctx->expectqreg)->current;
+ return expectqreg_current->process_edit_cmd_cb((teco_machine_t *)ctx->expectqreg, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ switch (key) {
+ case '\t': { /* autocomplete Q-Register name */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ /*
+ * NOTE: This is only for short Q-Register specifications,
+ * so there is no escaping.
+ */
+ g_auto(teco_string_t) new_chars;
+ teco_machine_qregspec_auto_complete(ctx, &new_chars);
+
+ return new_chars.len ? teco_cmdline_insert(new_chars.data, new_chars.len, error) : TRUE;
+ }
+ }
+
+ /*
+ * We chain to the parent (embedding) state machine's handler
+ * since rubout could otherwise rubout the command, invalidating
+ * the state machine. In particular ^W would crash.
+ * This also makes sure that commands like <EQ> are completely
+ * rub out via ^W.
+ */
+ return teco_state_process_edit_cmd(parent_ctx, NULL, key, error);
+}
+
+gboolean
+teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = teco_machine_qregspec_get_stringbuilding(ctx);
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: teco_machine_qregspec_t is private, so we downcast to teco_machine_t.
+ * Otherwise, we'd have to move this callback into qreg.c.
+ *
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, (teco_machine_t *)ctx, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete Q-Register name */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_machine_qregspec_auto_complete(ctx, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ']');
+
+ return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, (teco_machine_t *)ctx, key, error);
+}
+
+gboolean
+teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete file name */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ /*
+ * In the EC command, <TAB> completes files just like ^T
+ *
+ * TODO: Implement shell-command completion by iterating
+ * executables in $PATH
+ */
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ const gchar *filename = teco_string_last_occurrence(&ctx->expectstring.string,
+ TECO_DEFAULT_BREAK_CHARS);
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_file_auto_complete(filename, G_FILE_TEST_EXISTS, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ' ');
+
+ return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error);
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete Scintilla symbol */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ const gchar *symbol = teco_string_last_occurrence(&ctx->expectstring.string, ",");
+ teco_symbol_list_t *list = symbol == ctx->expectstring.string.data
+ ? &teco_symbol_list_scintilla
+ : &teco_symbol_list_scilexer;
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_symbol_list_auto_complete(list, symbol, &new_chars);
+ /*
+ * FIXME: Does not escape `,`. Also, <^Q,> is not allowed currently?
+ */
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ',');
+
+ return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error);
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete goto label */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ teco_string_t label = ctx->expectstring.string;
+ gint i = teco_string_rindex(&label, ',');
+ if (i >= 0) {
+ label.data += i+1;
+ label.len -= i+1;
+ }
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_goto_table_auto_complete(&ctx->goto_table, label.data, label.len, &new_chars);
+ /*
+ * FIXME: This does not escape `,`. Cannot be escaped via ^Q currently?
+ */
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ',');
+
+ return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete help term */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ /* help term must not contain null-byte */
+ return TRUE;
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_help_auto_complete(ctx->expectstring.string.data, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous && ctx->expectstring.nesting == 1)
+ teco_string_append_c(&new_chars_escaped,
+ ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
+
+ return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+/*
+ * Command states
+ */
+
+static teco_state_t *
+teco_state_save_cmdline_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode != TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (!qreg->vtable->undo_set_string(qreg, error) ||
+ !qreg->vtable->set_string(qreg, teco_last_cmdline.data, teco_last_cmdline.len, error))
+ return NULL;
+
+ return &teco_state_start;
+}
+
+/*$ *q
+ * *q -- Save last command line
+ *
+ * Only at the very beginning of a command-line, this command
+ * may be used to save the last command line as a string in
+ * Q-Register <q>.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_save_cmdline,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);