aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/qreg.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/qreg.c')
-rw-r--r--src/qreg.c1542
1 files changed, 1542 insertions, 0 deletions
diff --git a/src/qreg.c b/src/qreg.c
new file mode 100644
index 0000000..5c39409
--- /dev/null
+++ b/src/qreg.c
@@ -0,0 +1,1542 @@
+/*
+ * 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 <glib.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "file-utils.h"
+#include "interface.h"
+#include "cmdline.h"
+#include "view.h"
+#include "undo.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "expressions.h"
+#include "doc.h"
+#include "ring.h"
+#include "eol.h"
+#include "error.h"
+#include "qreg.h"
+
+/**
+ * View used for editing Q-Registers.
+ * Initialized in main.c after the interface.
+ */
+teco_view_t *teco_qreg_view = NULL;
+/** Currently edited Q-Register */
+teco_qreg_t *teco_qreg_current = NULL;
+
+/**
+ * Table for global Q-Registers.
+ * Initialized in main.c after the interface.
+ */
+teco_qreg_table_t teco_qreg_table_globals;
+
+/** @private @static @memberof teco_qreg_t */
+static teco_qreg_t *
+teco_qreg_new(teco_qreg_vtable_t *vtable, const gchar *name, gsize len)
+{
+ /*
+ * FIXME: Test with g_slice_new()...
+ * It could however cause problems upon command-line termination
+ * and may not be measurably faster.
+ */
+ teco_qreg_t *qreg = g_new0(teco_qreg_t, 1);
+ qreg->vtable = vtable;
+ /*
+ * NOTE: This does not use GStringChunk/teco_string_init_chunk()
+ * since we want to implement Q-Register removing soon.
+ * Even without that, individual Q-Regs can be removed on rubout.
+ */
+ teco_string_init(&qreg->head.name, name, len);
+ teco_doc_init(&qreg->string);
+ return qreg;
+}
+
+/** @memberof teco_qreg_t */
+gboolean
+teco_qreg_execute(teco_qreg_t *qreg, teco_qreg_table_t *qreg_table_locals, GError **error)
+{
+ g_auto(teco_string_t) macro = {NULL, 0};
+
+ if (!qreg->vtable->get_string(qreg, &macro.data, &macro.len, error) ||
+ !teco_execute_macro(macro.data, macro.len, qreg_table_locals, error)) {
+ teco_error_add_frame_qreg(qreg->head.name.data, qreg->head.name.len);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/** @memberof teco_qreg_t */
+void
+teco_qreg_undo_set_eol_mode(teco_qreg_t *qreg)
+{
+ if (!qreg->must_undo)
+ return;
+
+ /*
+ * Necessary, so that upon rubout the
+ * string's parameters are restored.
+ */
+ teco_doc_update(&qreg->string, teco_qreg_view);
+
+ if (teco_qreg_current && teco_qreg_current->must_undo) // FIXME
+ teco_doc_undo_edit(&teco_qreg_current->string);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_SETEOLMODE,
+ teco_view_ssm(teco_qreg_view, SCI_GETEOLMODE, 0, 0), 0);
+
+ teco_doc_undo_edit(&qreg->string);
+}
+
+/** @memberof teco_qreg_t */
+void
+teco_qreg_set_eol_mode(teco_qreg_t *qreg, gint mode)
+{
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+ teco_view_ssm(teco_qreg_view, SCI_SETEOLMODE, mode, 0);
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+}
+
+/** @memberof teco_qreg_t */
+gboolean
+teco_qreg_load(teco_qreg_t *qreg, const gchar *filename, GError **error)
+{
+ if (!qreg->vtable->undo_set_string(qreg, error))
+ return FALSE;
+
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+ teco_doc_reset(&qreg->string);
+
+ /*
+ * teco_view_load() might change the EOL style.
+ */
+ teco_qreg_undo_set_eol_mode(qreg);
+
+ /*
+ * undo_set_string() pushes undo tokens that restore
+ * the previous document in the view.
+ * So if loading fails, teco_qreg_current will be
+ * made the current document again.
+ */
+ if (!teco_view_load(teco_qreg_view, filename, error))
+ return FALSE;
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+
+ return TRUE;
+}
+
+/** @memberof teco_qreg_t */
+gboolean
+teco_qreg_save(teco_qreg_t *qreg, const gchar *filename, GError **error)
+{
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+
+ if (!teco_view_save(teco_qreg_view, filename, error)) {
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+ return FALSE;
+ }
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_set_integer(teco_qreg_t *qreg, teco_int_t value, GError **error)
+{
+ qreg->integer = value;
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_undo_set_integer(teco_qreg_t *qreg, GError **error)
+{
+ if (qreg->must_undo) // FIXME
+ teco_undo_int(qreg->integer);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_get_integer(teco_qreg_t *qreg, teco_int_t *ret, GError **error)
+{
+ *ret = qreg->integer;
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ teco_doc_set_string(&qreg->string, str, len);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_undo_set_string(teco_qreg_t *qreg, GError **error)
+{
+ if (qreg->must_undo) // FIXME
+ teco_doc_undo_set_string(&qreg->string);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ /*
+ * NOTE: Will not create undo action if string is empty.
+ * Also, appending preserves the string's parameters.
+ */
+ if (!len)
+ return TRUE;
+
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_APPENDTEXT, len, (sptr_t)str);
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error)
+{
+ teco_doc_get_string(&qreg->string, str, len);
+ return TRUE;
+}
+
+static gint
+teco_qreg_plain_get_character(teco_qreg_t *qreg, guint position, GError **error)
+{
+ gint ret = -1;
+
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+
+ if (position < teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0))
+ ret = teco_view_ssm(teco_qreg_view, SCI_GETCHARAT, position, 0);
+ else
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Position %u out of range", position);
+ /* make sure we still restore the current Q-Register */
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+
+ return ret;
+}
+
+static gboolean
+teco_qreg_plain_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ teco_doc_exchange(&qreg->string, src);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ if (qreg->must_undo) // FIXME
+ teco_doc_undo_exchange(&qreg->string);
+ teco_doc_undo_exchange(src);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_edit(teco_qreg_t *qreg, GError **error)
+{
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+ teco_interface_show_view(teco_qreg_view);
+ teco_interface_info_update(qreg);
+
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_undo_edit(teco_qreg_t *qreg, GError **error)
+{
+ /*
+ * We might be switching the current document
+ * to a buffer.
+ */
+ teco_doc_update(&qreg->string, teco_qreg_view);
+
+ if (!qreg->must_undo) // FIXME
+ return TRUE;
+
+ undo__teco_interface_info_update_qreg(qreg);
+ teco_doc_undo_edit(&qreg->string);
+ undo__teco_interface_show_view(teco_qreg_view);
+ return TRUE;
+}
+
+#define TECO_INIT_QREG(...) { \
+ .set_integer = teco_qreg_plain_set_integer, \
+ .undo_set_integer = teco_qreg_plain_undo_set_integer, \
+ .get_integer = teco_qreg_plain_get_integer, \
+ .set_string = teco_qreg_plain_set_string, \
+ .undo_set_string = teco_qreg_plain_undo_set_string, \
+ .append_string = teco_qreg_plain_append_string, \
+ .undo_append_string = teco_qreg_plain_undo_set_string, \
+ .get_string = teco_qreg_plain_get_string, \
+ .get_character = teco_qreg_plain_get_character, \
+ .exchange_string = teco_qreg_plain_exchange_string, \
+ .undo_exchange_string = teco_qreg_plain_undo_exchange_string, \
+ .edit = teco_qreg_plain_edit, \
+ .undo_edit = teco_qreg_plain_undo_edit, \
+ ##__VA_ARGS__ \
+}
+
+/** @static @memberof teco_qreg_t */
+teco_qreg_t *
+teco_qreg_plain_new(const gchar *name, gsize len)
+{
+ static teco_qreg_vtable_t vtable = TECO_INIT_QREG();
+
+ return teco_qreg_new(&vtable, name, len);
+}
+
+/*
+ * NOTE: The integer-component is currently unused on the "*" special register.
+ */
+static gboolean
+teco_qreg_bufferinfo_set_integer(teco_qreg_t *qreg, teco_int_t value, GError **error)
+{
+ return teco_ring_edit(value, error);
+}
+
+static gboolean
+teco_qreg_bufferinfo_undo_set_integer(teco_qreg_t *qreg, GError **error)
+{
+ return teco_current_doc_undo_edit(error);
+}
+
+static gboolean
+teco_qreg_bufferinfo_get_integer(teco_qreg_t *qreg, teco_int_t *ret, GError **error)
+{
+ *ret = teco_ring_get_id(teco_ring_current);
+ return TRUE;
+}
+
+/*
+ * FIXME: These operations can and should be implemented.
+ * Setting the "*" register could for instance rename the file.
+ */
+static gboolean
+teco_qreg_bufferinfo_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE);
+ return FALSE;
+}
+
+static gboolean
+teco_qreg_bufferinfo_undo_set_string(teco_qreg_t *qreg, GError **error)
+{
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_bufferinfo_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE);
+ return FALSE;
+}
+
+static gboolean
+teco_qreg_bufferinfo_undo_append_string(teco_qreg_t *qreg, GError **error)
+{
+ return TRUE;
+}
+
+/*
+ * NOTE: The `string` component is currently unused on the "*" register.
+ */
+static gboolean
+teco_qreg_bufferinfo_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error)
+{
+ /*
+ * On platforms with a default non-forward-slash directory
+ * separator (i.e. Windows), Buffer::filename will have
+ * the wrong separator.
+ * To make the life of macros that evaluate "*" easier,
+ * the directory separators are normalized to "/" here.
+ */
+ if (str)
+ *str = teco_file_normalize_path(g_strdup(teco_ring_current->filename ? : ""));
+ /*
+ * NOTE: teco_file_normalize_path() does not change the size of the string.
+ */
+ *len = teco_ring_current->filename ? strlen(teco_ring_current->filename) : 0;
+ return TRUE;
+}
+
+static gint
+teco_qreg_bufferinfo_get_character(teco_qreg_t *qreg, guint position, GError **error)
+{
+ gsize max_len;
+
+ if (!teco_qreg_bufferinfo_get_string(qreg, NULL, &max_len, error))
+ return -1;
+
+ if (position >= max_len) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Position %u out of range", position);
+ return -1;
+ }
+
+ return teco_ring_current->filename[position];
+}
+
+static gboolean
+teco_qreg_bufferinfo_edit(teco_qreg_t *qreg, GError **error)
+{
+ if (!teco_qreg_plain_edit(qreg, error))
+ return FALSE;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_bufferinfo_get_string(qreg, &str.data, &str.len, error))
+ return FALSE;
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_ADDTEXT, str.len, (sptr_t)str.data);
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0);
+ return TRUE;
+}
+
+/** @static @memberof teco_qreg_t */
+teco_qreg_t *
+teco_qreg_bufferinfo_new(void)
+{
+ static teco_qreg_vtable_t vtable = TECO_INIT_QREG(
+ .set_integer = teco_qreg_bufferinfo_set_integer,
+ .undo_set_integer = teco_qreg_bufferinfo_undo_set_integer,
+ .get_integer = teco_qreg_bufferinfo_get_integer,
+ .set_string = teco_qreg_bufferinfo_set_string,
+ .undo_set_string = teco_qreg_bufferinfo_undo_set_string,
+ .append_string = teco_qreg_bufferinfo_append_string,
+ .undo_append_string = teco_qreg_bufferinfo_undo_append_string,
+ .get_string = teco_qreg_bufferinfo_get_string,
+ .get_character = teco_qreg_bufferinfo_get_character,
+ .edit = teco_qreg_bufferinfo_edit
+ );
+
+ return teco_qreg_new(&vtable, "*", 1);
+}
+
+static gboolean
+teco_qreg_workingdir_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ /*
+ * NOTE: Makes sure that `dir` will be null-terminated as str[len] may not be '\0'.
+ */
+ g_auto(teco_string_t) dir;
+ teco_string_init(&dir, str, len);
+
+ if (teco_string_contains(&dir, '\0')) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Directory contains null-character");
+ return FALSE;
+ }
+
+ int ret = g_chdir(dir.data);
+ if (ret) {
+ /* FIXME: Is errno usable on Windows here? */
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Cannot change working directory to \"%s\"", dir.data);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_workingdir_undo_set_string(teco_qreg_t *qreg, GError **error)
+{
+ teco_undo_change_dir_to_current();
+ return TRUE;
+}
+
+/*
+ * FIXME: Redundant with teco_qreg_bufferinfo_append_string()...
+ * Best solution would be to simply implement them.
+ */
+static gboolean
+teco_qreg_workingdir_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE);
+ return FALSE;
+}
+
+static gboolean
+teco_qreg_workingdir_undo_append_string(teco_qreg_t *qreg, GError **error)
+{
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_workingdir_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error)
+{
+ /*
+ * On platforms with a default non-forward-slash directory
+ * separator (i.e. Windows), teco_buffer_t::filename will have
+ * the wrong separator.
+ * To make the life of macros that evaluate "$" easier,
+ * the directory separators are normalized to "/" here.
+ * This does not change the size of the string, so
+ * the return value for str == NULL is still correct.
+ */
+ gchar *dir = g_get_current_dir();
+ *len = strlen(dir);
+ if (str)
+ *str = teco_file_normalize_path(dir);
+ else
+ g_free(dir);
+
+ return TRUE;
+}
+
+static gint
+teco_qreg_workingdir_get_character(teco_qreg_t *qreg, guint position, GError **error)
+{
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_workingdir_get_string(qreg, &str.data, &str.len, error))
+ return -1;
+
+ if (position >= str.len) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Position %u out of range", position);
+ return -1;
+ }
+
+ return str.data[position];
+}
+
+static gboolean
+teco_qreg_workingdir_edit(teco_qreg_t *qreg, GError **error)
+{
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_plain_edit(qreg, error) ||
+ !teco_qreg_workingdir_get_string(qreg, &str.data, &str.len, error))
+ return FALSE;
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_ADDTEXT, str.len, (sptr_t)str.data);
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_workingdir_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ g_auto(teco_string_t) other_str, own_str = {NULL, 0};
+
+ teco_doc_get_string(src, &other_str.data, &other_str.len);
+
+ if (!teco_qreg_workingdir_get_string(qreg, &own_str.data, &own_str.len, error) ||
+ /* FIXME: Why is teco_qreg_plain_set_string() sufficient? */
+ !teco_qreg_plain_set_string(qreg, other_str.data, other_str.len, error))
+ return FALSE;
+
+ teco_doc_set_string(src, own_str.data, own_str.len);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_workingdir_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ teco_undo_change_dir_to_current();
+ if (qreg->must_undo) // FIXME
+ teco_doc_undo_set_string(src);
+ return TRUE;
+}
+
+/** @static @memberof teco_qreg_t */
+teco_qreg_t *
+teco_qreg_workingdir_new(void)
+{
+ static teco_qreg_vtable_t vtable = TECO_INIT_QREG(
+ .set_string = teco_qreg_workingdir_set_string,
+ .undo_set_string = teco_qreg_workingdir_undo_set_string,
+ .append_string = teco_qreg_workingdir_append_string,
+ .undo_append_string = teco_qreg_workingdir_undo_append_string,
+ .get_string = teco_qreg_workingdir_get_string,
+ .get_character = teco_qreg_workingdir_get_character,
+ .edit = teco_qreg_workingdir_edit,
+ .exchange_string = teco_qreg_workingdir_exchange_string,
+ .undo_exchange_string = teco_qreg_workingdir_undo_exchange_string
+ );
+
+ /*
+ * FIXME: Dollar is not the best name for it since it is already
+ * heavily overloaded in the language and easily confused with Escape and
+ * the "\e" register also exists.
+ * Not to mention that environment variable regs also start with dollar.
+ * Perhaps "~" would be a better choice, although it is also already used?
+ * Most logical would be ".", but this should probably map to to Dot and
+ * is also ugly to write in practice.
+ * Perhaps "@"...
+ */
+ return teco_qreg_new(&vtable, "$", 1);
+}
+
+static gboolean
+teco_qreg_clipboard_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ g_assert(!teco_string_contains(&qreg->head.name, '\0'));
+ const gchar *clipboard_name = qreg->head.name.data + 1;
+
+ if (teco_ed & TECO_ED_AUTOEOL) {
+ /*
+ * NOTE: Currently uses GString instead of teco_string_t to make use of
+ * preallocation.
+ * On the other hand GString has a higher overhead.
+ */
+ g_autoptr(GString) str_converted = g_string_sized_new(len);
+
+ /*
+ * This will convert to the Q-Register view's EOL mode.
+ */
+ g_auto(teco_eol_writer_t) writer;
+ teco_eol_writer_init_mem(&writer, teco_view_ssm(teco_qreg_view, SCI_GETEOLMODE, 0, 0),
+ str_converted);
+
+ gssize bytes_written = teco_eol_writer_convert(&writer, str, len, error);
+ if (bytes_written < 0)
+ return FALSE;
+ g_assert(bytes_written == len);
+
+ return teco_interface_set_clipboard(clipboard_name, str_converted->str,
+ str_converted->len, error);
+ } else {
+ /*
+ * No EOL conversion necessary. The teco_eol_writer_t can handle
+ * this as well, but will result in unnecessary allocations.
+ */
+ return teco_interface_set_clipboard(clipboard_name, str, len, error);
+ }
+
+ /* should not be reached */
+ return TRUE;
+}
+
+/*
+ * FIXME: Redundant with teco_qreg_bufferinfo_append_string()...
+ * Best solution would be to simply implement them.
+ */
+static gboolean
+teco_qreg_clipboard_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE);
+ return FALSE;
+}
+
+static gboolean
+teco_qreg_clipboard_undo_append_string(teco_qreg_t *qreg, GError **error)
+{
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_clipboard_undo_set_string(teco_qreg_t *qreg, GError **error)
+{
+ /*
+ * Upon rubout, the current contents of the clipboard are
+ * restored.
+ * We are checking for teco_undo_enabled instead of relying on
+ * teco_undo_push(), since getting the clipboard
+ * is an expensive operation that we want to avoid.
+ */
+ if (!teco_undo_enabled)
+ return TRUE;
+
+ g_assert(!teco_string_contains(&qreg->head.name, '\0'));
+ const gchar *clipboard_name = qreg->head.name.data + 1;
+
+ /*
+ * Ownership of str is passed to the undo token.
+ * This avoids any EOL translation as that would be cumbersome
+ * and could also modify the clipboard in unexpected ways.
+ */
+ teco_string_t str;
+ if (!teco_interface_get_clipboard(clipboard_name, &str.data, &str.len, error))
+ return FALSE;
+ teco_interface_undo_set_clipboard(clipboard_name, str.data, str.len);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_clipboard_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error)
+{
+ g_assert(!teco_string_contains(&qreg->head.name, '\0'));
+ const gchar *clipboard_name = qreg->head.name.data + 1;
+
+ if (!(teco_ed & TECO_ED_AUTOEOL))
+ /*
+ * No auto-eol conversion - avoid unnecessary copying and allocations.
+ */
+ return teco_interface_get_clipboard(clipboard_name, str, len, error);
+
+ g_auto(teco_string_t) temp = {NULL, 0};
+ if (!teco_interface_get_clipboard(clipboard_name, &temp.data, &temp.len, error))
+ return FALSE;
+
+ g_auto(teco_eol_reader_t) reader;
+ teco_eol_reader_init_mem(&reader, temp.data, temp.len);
+
+ /*
+ * FIXME: Could be simplified if teco_eol_reader_convert_all() had the
+ * same conventions for passing NULL pointers.
+ */
+ teco_string_t str_converted;
+ if (teco_eol_reader_convert_all(&reader, &str_converted.data,
+ &str_converted.len, error) == G_IO_STATUS_ERROR)
+ return FALSE;
+
+ if (str)
+ *str = str_converted.data;
+ else
+ teco_string_clear(&str_converted);
+ *len = str_converted.len;
+
+ return TRUE;
+}
+
+static gint
+teco_qreg_clipboard_get_character(teco_qreg_t *qreg, guint position, GError **error)
+{
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_clipboard_get_string(qreg, &str.data, &str.len, error))
+ return -1;
+
+ if (position >= str.len) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Position %u out of range", position);
+ return -1;
+ }
+
+ return str.data[position];
+}
+
+static gboolean
+teco_qreg_clipboard_edit(teco_qreg_t *qreg, GError **error)
+{
+ if (!teco_qreg_plain_edit(qreg, error))
+ return FALSE;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_clipboard_get_string(qreg, &str.data, &str.len, error))
+ return FALSE;
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_APPENDTEXT, str.len, (sptr_t)str.data);
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0);
+ return TRUE;
+}
+
+/*
+ * FIXME: Very similar to teco_qreg_workingdir_exchange_string().
+ */
+static gboolean
+teco_qreg_clipboard_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ g_auto(teco_string_t) other_str, own_str = {NULL, 0};
+
+ teco_doc_get_string(src, &other_str.data, &other_str.len);
+
+ if (!teco_qreg_clipboard_get_string(qreg, &own_str.data, &own_str.len, error) ||
+ /* FIXME: Why is teco_qreg_plain_set_string() sufficient? */
+ !teco_qreg_plain_set_string(qreg, other_str.data, other_str.len, error))
+ return FALSE;
+
+ teco_doc_set_string(src, own_str.data, own_str.len);
+ return TRUE;
+}
+
+/*
+ * FIXME: Very similar to teco_qreg_workingdir_undo_exchange_string().
+ */
+static gboolean
+teco_qreg_clipboard_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ if (!teco_qreg_clipboard_undo_set_string(qreg, error))
+ return FALSE;
+ if (qreg->must_undo) // FIXME
+ teco_doc_undo_set_string(src);
+ return TRUE;
+}
+
+/** @static @memberof teco_qreg_t */
+teco_qreg_t *
+teco_qreg_clipboard_new(const gchar *name)
+{
+ static teco_qreg_vtable_t vtable = TECO_INIT_QREG(
+ .set_string = teco_qreg_clipboard_set_string,
+ .undo_set_string = teco_qreg_clipboard_undo_set_string,
+ .append_string = teco_qreg_clipboard_append_string,
+ .undo_append_string = teco_qreg_clipboard_undo_append_string,
+ .get_string = teco_qreg_clipboard_get_string,
+ .get_character = teco_qreg_clipboard_get_character,
+ .edit = teco_qreg_clipboard_edit,
+ .exchange_string = teco_qreg_clipboard_exchange_string,
+ .undo_exchange_string = teco_qreg_clipboard_undo_exchange_string
+ );
+
+ teco_qreg_t *qreg = teco_qreg_new(&vtable, "~", 1);
+ teco_string_append(&qreg->head.name, name, strlen(name));
+ return qreg;
+}
+
+/** @memberof teco_qreg_table_t */
+void
+teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo)
+{
+ rb3_reset_tree(&table->tree);
+ table->must_undo = must_undo;
+
+ /* general purpose registers */
+ for (gchar q = 'A'; q <= 'Z'; q++)
+ teco_qreg_table_insert(table, teco_qreg_plain_new(&q, sizeof(q)));
+ for (gchar q = '0'; q <= '9'; q++)
+ teco_qreg_table_insert(table, teco_qreg_plain_new(&q, sizeof(q)));
+}
+
+static inline void
+teco_qreg_table_remove(teco_qreg_t *reg)
+{
+ rb3_unlink_and_rebalance(&reg->head.head);
+ teco_qreg_free(reg);
+}
+TECO_DEFINE_UNDO_CALL(teco_qreg_table_remove, teco_qreg_t *);
+
+static inline void
+teco_qreg_table_undo_remove(teco_qreg_t *qreg)
+{
+ if (qreg->must_undo)
+ undo__teco_qreg_table_remove(qreg);
+}
+
+/** @memberof teco_qreg_table_t */
+teco_qreg_t *
+teco_qreg_table_edit_name(teco_qreg_table_t *table, const gchar *name, gsize len, GError **error)
+{
+ teco_qreg_t *qreg = teco_qreg_table_find(table, name, len);
+ if (!qreg) {
+ g_autofree gchar *name_printable = teco_string_echo(name, len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Q-Register \"%s\" not found", name_printable);
+ return NULL;
+ }
+
+ return teco_qreg_table_edit(table, qreg, error) ? qreg : NULL;
+}
+
+/**
+ * Import process environment into table
+ * by setting environment registers for every
+ * environment variable.
+ * It is assumed that the table does not yet
+ * contain any environment register.
+ *
+ * In general this method is only safe to call
+ * at startup.
+ *
+ * @memberof teco_qreg_table_t
+ */
+gboolean
+teco_qreg_table_set_environ(teco_qreg_table_t *table, GError **error)
+{
+ /*
+ * NOTE: Using g_get_environ() would be more efficient,
+ * but it appears to be broken, at least on Windows 2000.
+ */
+ g_auto(GStrv) env = g_listenv();
+
+ for (gchar **key = env; *key; key++) {
+ g_autofree gchar *name = g_strconcat("$", *key, NULL);
+
+ /*
+ * FIXME: It might be a good idea to wrap this into
+ * a convenience function.
+ */
+ teco_qreg_t *qreg = teco_qreg_plain_new(name, strlen(name));
+ teco_qreg_t *found = teco_qreg_table_insert(table, qreg);
+ if (found) {
+ teco_qreg_free(qreg);
+ qreg = found;
+ }
+
+ const gchar *value = g_getenv(*key);
+ g_assert(value != NULL);
+ if (!qreg->vtable->set_string(qreg, value, strlen(value), error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Export environment registers as a list of environment
+ * variables compatible with `g_get_environ()`.
+ *
+ * @return Zero-terminated list of strings in the form
+ * `NAME=VALUE`. Should be freed with `g_strfreev()`.
+ * NULL in case of errors.
+ *
+ * @memberof teco_qreg_table_t
+ */
+gchar **
+teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error)
+{
+ teco_qreg_t *first = (teco_qreg_t *)teco_rb3str_nfind(&table->tree, TRUE, "$", 1);
+
+ gint envp_len = 1;
+
+ /*
+ * Iterate over all registers beginning with "$" to
+ * guess the size required for the environment array.
+ * This may waste a few bytes because not __every__
+ * register beginning with "$" is an environment
+ * register.
+ */
+ for (teco_qreg_t *cur = first;
+ cur && cur->head.name.data[0] == '$';
+ cur = (teco_qreg_t *)teco_rb3str_get_next(&cur->head))
+ envp_len++;
+
+ gchar **envp, **p;
+ p = envp = g_new(gchar *, envp_len);
+
+ for (teco_qreg_t *cur = first;
+ cur && cur->head.name.data[0] == '$';
+ cur = (teco_qreg_t *)teco_rb3str_get_next(&cur->head)) {
+ const teco_string_t *name = &cur->head.name;
+
+ /*
+ * Ignore the "$" register (not an environment
+ * variable register) and registers whose
+ * name contains "=" or null (not allowed in environment
+ * variable names).
+ */
+ if (name->len == 1 ||
+ teco_string_contains(name, '=') || teco_string_contains(name, '\0'))
+ continue;
+
+ g_auto(teco_string_t) value = {NULL, 0};
+ if (!cur->vtable->get_string(cur, &value.data, &value.len, error)) {
+ g_strfreev(envp);
+ return NULL;
+ }
+ if (teco_string_contains(&value, '\0')) {
+ g_strfreev(envp);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Environment register \"%s\" must not contain null characters",
+ name->data);
+ return NULL;
+ }
+
+ /* more efficient than g_environ_setenv() */
+ *p++ = g_strconcat(name->data+1, "=", value.data, NULL);
+ }
+
+ *p = NULL;
+
+ return envp;
+}
+
+/**
+ * Empty Q-Register table except the currently edited register.
+ * If the table contains the currently edited register, it will
+ * throw an error and the table might be left half-emptied.
+ *
+ * @memberof teco_qreg_table_t
+ */
+gboolean
+teco_qreg_table_empty(teco_qreg_table_t *table, GError **error)
+{
+ struct rb3_head *cur;
+
+ while ((cur = rb3_get_root(&table->tree))) {
+ if ((teco_qreg_t *)cur == teco_qreg_current) {
+ const teco_string_t *name = &teco_qreg_current->head.name;
+ g_autofree gchar *name_printable = teco_string_echo(name->data, name->len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Currently edited Q-Register \"%s\" cannot be discarded", name_printable);
+ return FALSE;
+ }
+
+ rb3_unlink_and_rebalance(cur);
+ teco_qreg_free((teco_qreg_t *)cur);
+ }
+
+ return TRUE;
+}
+
+/** @memberof teco_qreg_table_t */
+void
+teco_qreg_table_clear(teco_qreg_table_t *table)
+{
+ struct rb3_head *cur;
+
+ while ((cur = rb3_get_root(&table->tree))) {
+ rb3_unlink_and_rebalance(cur);
+ teco_qreg_free((teco_qreg_t *)cur);
+ }
+}
+
+typedef struct {
+ teco_int_t integer;
+ teco_doc_t string;
+} teco_qreg_stack_entry_t;
+
+static inline void
+teco_qreg_stack_entry_clear(teco_qreg_stack_entry_t *entry)
+{
+ teco_doc_clear(&entry->string);
+}
+
+static GArray *teco_qreg_stack;
+
+static void __attribute__((constructor))
+teco_qreg_stack_init(void)
+{
+ teco_qreg_stack = g_array_sized_new(FALSE, FALSE, sizeof(teco_qreg_stack_entry_t), 1024);
+}
+
+static inline void
+teco_qreg_stack_remove_last(void)
+{
+ teco_qreg_stack_entry_clear(&g_array_index(teco_qreg_stack, teco_qreg_stack_entry_t,
+ teco_qreg_stack->len-1));
+ g_array_remove_index(teco_qreg_stack, teco_qreg_stack->len-1);
+}
+TECO_DEFINE_UNDO_CALL(teco_qreg_stack_remove_last);
+
+gboolean
+teco_qreg_stack_push(teco_qreg_t *qreg, GError **error)
+{
+ teco_qreg_stack_entry_t entry;
+ g_auto(teco_string_t) string = {NULL, 0};
+
+ if (!qreg->vtable->get_integer(qreg, &entry.integer, error) ||
+ !qreg->vtable->get_string(qreg, &string.data, &string.len, error))
+ return FALSE;
+ teco_doc_init(&entry.string);
+ teco_doc_set_string(&entry.string, string.data, string.len);
+ teco_doc_update(&entry.string, &qreg->string);
+
+ /* pass ownership of entry to teco_qreg_stack */
+ g_array_append_val(teco_qreg_stack, entry);
+ undo__teco_qreg_stack_remove_last();
+ return TRUE;
+}
+
+static void
+teco_qreg_stack_entry_action(teco_qreg_stack_entry_t *entry, gboolean run)
+{
+ if (run)
+ g_array_append_val(teco_qreg_stack, *entry);
+ else
+ teco_qreg_stack_entry_clear(entry);
+}
+
+static void
+teco_undo_qreg_stack_push_own(teco_qreg_stack_entry_t *entry)
+{
+ teco_qreg_stack_entry_t *ctx = teco_undo_push(teco_qreg_stack_entry);
+ if (ctx)
+ memcpy(ctx, entry, sizeof(*ctx));
+ else
+ teco_qreg_stack_entry_clear(entry);
+}
+
+gboolean
+teco_qreg_stack_pop(teco_qreg_t *qreg, GError **error)
+{
+ if (!teco_qreg_stack->len) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Q-Register stack empty");
+ return FALSE;
+ }
+
+ teco_qreg_stack_entry_t *entry;
+ entry = &g_array_index(teco_qreg_stack, teco_qreg_stack_entry_t, teco_qreg_stack->len-1);
+
+ if (!qreg->vtable->undo_set_integer(qreg, error) ||
+ !qreg->vtable->set_integer(qreg, entry->integer, error))
+ return FALSE;
+
+ /* exchange document ownership between stack entry and Q-Register */
+ if (!qreg->vtable->undo_exchange_string(qreg, &entry->string, error) ||
+ !qreg->vtable->exchange_string(qreg, &entry->string, error))
+ return FALSE;
+
+ /* pass entry ownership to undo stack. */
+ teco_undo_qreg_stack_push_own(entry);
+
+ g_array_remove_index(teco_qreg_stack, teco_qreg_stack->len-1);
+ return TRUE;
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_qreg_stack_clear(void)
+{
+ g_array_set_clear_func(teco_qreg_stack, (GDestroyNotify)teco_qreg_stack_entry_clear);
+ g_array_free(teco_qreg_stack, TRUE);
+}
+#endif
+
+gboolean
+teco_ed_hook(teco_ed_hook_t type, GError **error)
+{
+ if (!(teco_ed & TECO_ED_HOOKS))
+ return TRUE;
+
+ /*
+ * NOTE: It is crucial to declare this before the first goto,
+ * since it runs all destructors.
+ */
+ g_auto(teco_qreg_table_t) locals;
+ teco_qreg_table_init(&locals, FALSE);
+
+ teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "ED", 2);
+ if (!qreg) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Undefined ED-hook register (\"ED\")");
+ goto error_add_frame;
+ }
+
+ /*
+ * ED-hook execution should not see any
+ * integer parameters but the hook type.
+ * Such parameters could confuse the ED macro
+ * and macro authors do not expect side effects
+ * of ED macros on the expression stack.
+ * Also make sure it does not leave behind
+ * additional arguments on the stack.
+ *
+ * So this effectively executes:
+ * (typeM[ED]^[)
+ *
+ * FIXME: Temporarily stashing away the expression
+ * stack may be a more elegant solution.
+ */
+ teco_expressions_brace_open();
+ teco_expressions_push_int(type);
+
+ if (!teco_qreg_execute(qreg, &locals, error))
+ goto error_add_frame;
+
+ return teco_expressions_discard_args(error) &&
+ teco_expressions_brace_close(error);
+
+ static const gchar *type2name[] = {
+ [TECO_ED_HOOK_ADD-1] = "ADD",
+ [TECO_ED_HOOK_EDIT-1] = "EDIT",
+ [TECO_ED_HOOK_CLOSE-1] = "CLOSE",
+ [TECO_ED_HOOK_QUIT-1] = "QUIT"
+ };
+
+error_add_frame:
+ g_assert(0 <= type-1 && type-1 < G_N_ELEMENTS(type2name));
+ teco_error_add_frame_edhook(type2name[type-1]);
+ return FALSE;
+}
+
+/** @extends teco_machine_t */
+struct teco_machine_qregspec_t {
+ teco_machine_t parent;
+
+ /**
+ * Aliases bitfield with an integer.
+ * This allows teco_undo_guint(__flags),
+ * while still supporting easy-to-access flags.
+ */
+ union {
+ struct {
+ teco_qreg_type_t type : 8;
+ gboolean parse_only : 1;
+ };
+ guint __flags;
+ };
+
+ /** Local Q-Register table of the macro invocation frame. */
+ teco_qreg_table_t *qreg_table_locals;
+
+ teco_machine_stringbuilding_t machine_stringbuilding;
+ /*
+ * FIXME: Does it make sense to allow nested braces?
+ * Perhaps it's sufficient to support ^Q].
+ */
+ gint nesting;
+ teco_string_t name;
+
+ teco_qreg_t *result;
+ teco_qreg_table_t *result_table;
+};
+
+/*
+ * FIXME: All teco_state_qregspec_* states could be static?
+ */
+TECO_DECLARE_STATE(teco_state_qregspec_start);
+TECO_DECLARE_STATE(teco_state_qregspec_start_global);
+TECO_DECLARE_STATE(teco_state_qregspec_firstchar);
+TECO_DECLARE_STATE(teco_state_qregspec_secondchar);
+TECO_DECLARE_STATE(teco_state_qregspec_string);
+
+static teco_state_t *teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx,
+ gchar chr, GError **error);
+
+static teco_state_t *
+teco_state_qregspec_done(teco_machine_qregspec_t *ctx, GError **error)
+{
+ if (ctx->parse_only)
+ return &teco_state_qregspec_start;
+
+ ctx->result = teco_qreg_table_find(ctx->result_table, ctx->name.data, ctx->name.len);
+
+ switch (ctx->type) {
+ case TECO_QREG_REQUIRED:
+ if (!ctx->result) {
+ teco_error_invalidqreg_set(error, ctx->name.data, ctx->name.len,
+ ctx->result_table != &teco_qreg_table_globals);
+ return NULL;
+ }
+ break;
+
+ case TECO_QREG_OPTIONAL:
+ break;
+
+ case TECO_QREG_OPTIONAL_INIT:
+ if (!ctx->result) {
+ ctx->result = teco_qreg_plain_new(ctx->name.data, ctx->name.len);
+ teco_qreg_table_insert(ctx->result_table, ctx->result);
+ teco_qreg_table_undo_remove(ctx->result);
+ }
+ break;
+ }
+
+ return &teco_state_qregspec_start;
+}
+
+static teco_state_t *
+teco_state_qregspec_start_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * FIXME: We're using teco_state_qregspec_start as a success condition,
+ * so either '.' goes into its own state or we re-introduce a state attribute.
+ */
+ if (chr == '.') {
+ if (ctx->parent.must_undo)
+ teco_undo_ptr(ctx->result_table);
+ ctx->result_table = ctx->qreg_table_locals;
+ return &teco_state_qregspec_start_global;
+ }
+
+ return teco_state_qregspec_start_global_input(ctx, chr, error);
+}
+
+/* in cmdline.c */
+gboolean teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error);
+
+TECO_DEFINE_STATE(teco_state_qregspec_start,
+ .is_start = TRUE,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * FIXME: Disallow space characters?
+ */
+ switch (chr) {
+ case '#':
+ return &teco_state_qregspec_firstchar;
+
+ case '[':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nesting);
+ ctx->nesting++;
+ return &teco_state_qregspec_string;
+ }
+
+ if (!ctx->parse_only) {
+ if (ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->name, ctx->name.len);
+ teco_string_append_c(&ctx->name, g_ascii_toupper(chr));
+ }
+ return teco_state_qregspec_done(ctx, error);
+}
+
+/*
+ * NOTE: This state mainly exists so that we don't have to go back to teco_state_qregspec_start after
+ * an initial `.` -- this is currently used in teco_machine_qregspec_input() to check for completeness.
+ * Alternatively, we'd have to introduce a teco_machine_qregspec_t::status attribute.
+ * Or even better, why not use special pointers like ((teco_state_t *)"teco_state_qregspec_done")?
+ */
+TECO_DEFINE_STATE(teco_state_qregspec_start_global,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_qregspec_firstchar_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * FIXME: Disallow space characters?
+ */
+ if (!ctx->parse_only) {
+ if (ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->name, ctx->name.len);
+ teco_string_append_c(&ctx->name, g_ascii_toupper(chr));
+ }
+ return &teco_state_qregspec_secondchar;
+}
+
+TECO_DEFINE_STATE(teco_state_qregspec_firstchar,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_qregspec_secondchar_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * FIXME: Disallow space characters?
+ */
+ if (!ctx->parse_only) {
+ if (ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->name, ctx->name.len);
+ teco_string_append_c(&ctx->name, g_ascii_toupper(chr));
+ }
+ return teco_state_qregspec_done(ctx, error);
+}
+
+TECO_DEFINE_STATE(teco_state_qregspec_secondchar,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_qregspec_string_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * Makes sure that braces within string building constructs do not have to be
+ * escaped and that ^Q/^R can be used to escape braces.
+ *
+ * FIXME: Perhaps that's sufficient and we don't have to keep track of nesting?
+ */
+ if (ctx->machine_stringbuilding.parent.current->is_start) {
+ switch (chr) {
+ case '[':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nesting);
+ ctx->nesting++;
+ break;
+ case ']':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nesting);
+ ctx->nesting--;
+ if (!ctx->nesting)
+ return teco_state_qregspec_done(ctx, error);
+ break;
+ }
+ }
+
+ if (!ctx->parse_only && ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->name, ctx->name.len);
+
+ /*
+ * NOTE: machine_stringbuilding gets notified about parse-only mode by passing NULL
+ * as the target string.
+ */
+ if (!teco_machine_stringbuilding_input(&ctx->machine_stringbuilding, chr,
+ ctx->parse_only ? NULL : &ctx->name, error))
+ return NULL;
+
+ return &teco_state_qregspec_string;
+}
+
+/* in cmdline.c */
+gboolean teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx,
+ gchar key, GError **error);
+
+TECO_DEFINE_STATE(teco_state_qregspec_string,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_string_process_edit_cmd
+);
+
+/** @static @memberof teco_machine_qregspec_t */
+teco_machine_qregspec_t *
+teco_machine_qregspec_new(teco_qreg_type_t type, teco_qreg_table_t *locals, gboolean must_undo)
+{
+ /*
+ * FIXME: Allocate via g_slice?
+ */
+ teco_machine_qregspec_t *ctx = g_new0(teco_machine_qregspec_t, 1);
+ teco_machine_init(&ctx->parent, &teco_state_qregspec_start, must_undo);
+ ctx->type = type;
+ ctx->qreg_table_locals = locals;
+ teco_machine_stringbuilding_init(&ctx->machine_stringbuilding, '[', locals, must_undo);
+ ctx->result_table = &teco_qreg_table_globals;
+ return ctx;
+}
+
+/** @memberof teco_machine_qregspec_t */
+void
+teco_machine_qregspec_reset(teco_machine_qregspec_t *ctx)
+{
+ teco_machine_reset(&ctx->parent, &teco_state_qregspec_start);
+ teco_machine_stringbuilding_reset(&ctx->machine_stringbuilding);
+ if (ctx->parent.must_undo) {
+ teco_undo_string_own(ctx->name);
+ teco_undo_gint(ctx->nesting);
+ teco_undo_guint(ctx->__flags);
+ } else {
+ teco_string_clear(&ctx->name);
+ }
+ memset(&ctx->name, 0, sizeof(ctx->name));
+ ctx->nesting = 0;
+ ctx->result_table = &teco_qreg_table_globals;
+}
+
+/** @memberof teco_machine_qregspec_t */
+teco_machine_stringbuilding_t *
+teco_machine_qregspec_get_stringbuilding(teco_machine_qregspec_t *ctx)
+{
+ return &ctx->machine_stringbuilding;
+}
+
+/**
+ * Pass a character to the QRegister specification machine.
+ *
+ * @param ctx QRegister specification machine.
+ * @param chr Character to parse.
+ * @param result Pointer to QRegister or NULL in parse-only mode.
+ * If non-NULL it will be set once a specification is successfully parsed.
+ * @param result_table Pointer to QRegister table. May be NULL in parse-only mode.
+ * @param error GError or NULL.
+ * @return Returns TECO_MACHINE_QREGSPEC_DONE in case of complete specs.
+ *
+ * @memberof teco_machine_qregspec_t
+ */
+teco_machine_qregspec_status_t
+teco_machine_qregspec_input(teco_machine_qregspec_t *ctx, gchar chr,
+ teco_qreg_t **result, teco_qreg_table_t **result_table, GError **error)
+{
+ ctx->parse_only = result == NULL;
+
+ if (!teco_machine_input(&ctx->parent, chr, error))
+ return TECO_MACHINE_QREGSPEC_ERROR;
+
+ teco_machine_qregspec_get_results(ctx, result, result_table);
+ return ctx->parent.current == &teco_state_qregspec_start
+ ? TECO_MACHINE_QREGSPEC_DONE : TECO_MACHINE_QREGSPEC_MORE;
+}
+
+/** @memberof teco_machine_qregspec_t */
+void
+teco_machine_qregspec_get_results(teco_machine_qregspec_t *ctx,
+ teco_qreg_t **result, teco_qreg_table_t **result_table)
+{
+ if (result)
+ *result = ctx->result;
+ if (result_table)
+ *result_table = ctx->result_table;
+}
+
+/** @memberof teco_machine_qregspec_t */
+gboolean
+teco_machine_qregspec_auto_complete(teco_machine_qregspec_t *ctx, teco_string_t *insert)
+{
+ gsize restrict_len = 0;
+
+ /*
+ * NOTE: We could have separate process_edit_cmd_cb() for
+ * teco_state_qregspec_firstchar/teco_state_qregspec_secondchar
+ * and pass down restrict_len instead.
+ */
+ if (ctx->parent.current == &teco_state_qregspec_start ||
+ ctx->parent.current == &teco_state_qregspec_start_global)
+ /* single-letter Q-Reg */
+ restrict_len = 1;
+ else if (ctx->parent.current != &teco_state_qregspec_string)
+ /* two-letter Q-Reg */
+ restrict_len = 2;
+
+ return teco_rb3str_auto_complete(&ctx->result_table->tree, !restrict_len,
+ ctx->name.data, ctx->name.len, restrict_len, insert) &&
+ ctx->nesting == 1;
+}
+
+/** @memberof teco_machine_qregspec_t */
+void
+teco_machine_qregspec_free(teco_machine_qregspec_t *ctx)
+{
+ teco_machine_stringbuilding_clear(&ctx->machine_stringbuilding);
+ teco_string_clear(&ctx->name);
+ g_free(ctx);
+}
+
+TECO_DEFINE_UNDO_CALL(teco_machine_qregspec_free, teco_machine_qregspec_t *);
+TECO_DEFINE_UNDO_OBJECT_OWN(qregspec, teco_machine_qregspec_t *, teco_machine_qregspec_free);