/*
* Copyright (C) 2012-2024 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 .
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include
#include
#include
#include
#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;
/** Table of currently edited Q-Register */
const teco_qreg_table_t *teco_qreg_table_current = 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};
/*
* SciTECO macros must be in UTF-8, but we don't check the encoding,
* so as not to complicate TECO_ED_DEFAULT_ANSI mode.
* The UTF-8 byte sequences are checked anyway.
*/
if (!qreg->vtable->get_string(qreg, ¯o.data, ¯o.len, NULL, 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_default_codepage());
teco_view_ssm(teco_qreg_view, SCI_SETEOLMODE, mode, 0);
if (teco_qreg_current)
teco_doc_edit(&teco_qreg_current->string, 0);
}
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,
guint codepage, GError **error)
{
teco_doc_set_string(&qreg->string, str, len, codepage);
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 (qreg->must_undo) { // FIXME
/*
* 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);
teco_doc_undo_reset(&qreg->string);
undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0);
}
if (teco_qreg_current)
teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
teco_doc_edit(&qreg->string, teco_default_codepage());
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, 0);
/*
* Make sure these undo tokens are only generated now,
* so that teco_doc_edit() always initializes the document and
* the default codepage.
*/
if (qreg->must_undo) // FIXME
teco_doc_undo_edit(&qreg->string);
return TRUE;
}
static gboolean
teco_qreg_plain_get_string(teco_qreg_t *qreg, gchar **str, gsize *len,
guint *codepage, GError **error)
{
teco_doc_get_string(&qreg->string, str, len, codepage);
return TRUE;
}
static gboolean
teco_qreg_plain_get_character(teco_qreg_t *qreg, teco_int_t position,
teco_int_t *chr, GError **error)
{
if (teco_qreg_current)
teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
teco_doc_edit(&qreg->string, teco_default_codepage());
sptr_t len = teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0);
gssize off = teco_view_glyphs2bytes(teco_qreg_view, position);
gboolean ret = off >= 0 && off != len;
if (!ret)
g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
"Position %" TECO_INT_FORMAT " out of range", position);
/* make sure we still restore the current Q-Register */
else
*chr = teco_view_get_character(teco_qreg_view, off, len);
if (teco_qreg_current)
teco_doc_edit(&teco_qreg_current->string, 0);
return ret;
}
static teco_int_t
teco_qreg_plain_get_length(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_default_codepage());
sptr_t len = teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0);
teco_int_t ret = teco_view_bytes2glyphs(teco_qreg_view, len);
if (teco_qreg_current)
teco_doc_edit(&teco_qreg_current->string, 0);
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_default_codepage());
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;
}
static gboolean
teco_qreg_plain_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_default_codepage());
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, 0);
return TRUE;
}
static gboolean
teco_qreg_plain_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, teco_default_codepage());
gboolean ret = teco_view_save(teco_qreg_view, filename, error);
if (teco_qreg_current)
teco_doc_edit(&teco_qreg_current->string, 0);
return ret;
}
/**
* Initializer for vtables of Q-Registers with "plain" storage of strings.
* These store their string part as teco_docs.
*/
#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, \
.get_string = teco_qreg_plain_get_string, \
.get_character = teco_qreg_plain_get_character, \
.get_length = teco_qreg_plain_get_length, \
.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, \
.load = teco_qreg_plain_load, \
.save = teco_qreg_plain_save, \
##__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);
}
static gboolean
teco_qreg_external_edit(teco_qreg_t *qreg, GError **error)
{
g_auto(teco_string_t) str = {NULL, 0};
if (!teco_qreg_plain_edit(qreg, error) ||
!qreg->vtable->get_string(qreg, &str.data, &str.len, NULL, 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_external_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
{
g_auto(teco_string_t) other_str, own_str = {NULL, 0};
guint other_cp, own_cp;
teco_doc_get_string(src, &other_str.data, &other_str.len, &other_cp);
if (!qreg->vtable->get_string(qreg, &own_str.data, &own_str.len, &own_cp, error) ||
!qreg->vtable->set_string(qreg, other_str.data, other_str.len, other_cp, error))
return FALSE;
teco_doc_set_string(src, own_str.data, own_str.len, own_cp);
return TRUE;
}
static gboolean
teco_qreg_external_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
{
if (!qreg->vtable->undo_set_string(qreg, error))
return FALSE;
if (qreg->must_undo) // FIXME
teco_doc_undo_set_string(src);
return TRUE;
}
static gboolean
teco_qreg_external_get_character(teco_qreg_t *qreg, teco_int_t position,
teco_int_t *chr, GError **error)
{
g_auto(teco_string_t) str = {NULL, 0};
if (!qreg->vtable->get_string(qreg, &str.data, &str.len, NULL, error))
return FALSE;
if (position < 0 || position >= g_utf8_strlen(str.data, str.len)) {
g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
"Position %" TECO_INT_FORMAT " out of range", position);
return FALSE;
}
const gchar *p = g_utf8_offset_to_pointer(str.data, position);
/*
* Make sure that the -1/-2 error values are preserved.
* The sign bit in UCS-4/UTF-32 is unused, so this will even
* suffice if TECO_INTEGER == 32.
*/
*chr = (gint32)g_utf8_get_char_validated(p, -1);
return TRUE;
}
static teco_int_t
teco_qreg_external_get_length(teco_qreg_t *qreg, GError **error)
{
g_auto(teco_string_t) str = {NULL, 0};
if (!qreg->vtable->get_string(qreg, &str.data, &str.len, NULL, error))
return -1;
return g_utf8_strlen(str.data, str.len);
}
/*
* NOTE: This does not perform EOL normalization unlike teco_view_load().
* It shouldn't be critical since "external" registers are mainly used for filenames.
* Otherwise we could of course load into the view() and call set_string() afterwards.
*/
static gboolean
teco_qreg_external_load(teco_qreg_t *qreg, const gchar *filename, GError **error)
{
g_auto(teco_string_t) str = {NULL, 0};
return g_file_get_contents(filename, &str.data, &str.len, error) &&
qreg->vtable->undo_set_string(qreg, error) &&
qreg->vtable->set_string(qreg, str.data, str.len, teco_default_codepage(), error);
}
/*
* NOTE: This does not simply use g_file_set_contents(), as we have to create
* save point files as well.
* FIXME: On the other hand, this does not set the correct EOL style on the document,
* so teco_view_save() will save only with the default EOL style.
* It might therefore still be a good idea to avoid any conversion.
*/
static gboolean
teco_qreg_external_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, teco_default_codepage());
g_auto(teco_string_t) str = {NULL, 0};
if (!qreg->vtable->get_string(qreg, &str.data, &str.len, NULL, 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);
gboolean ret = teco_view_save(teco_qreg_view, filename, error);
if (teco_qreg_current)
teco_doc_edit(&teco_qreg_current->string, 0);
return ret;
}
/**
* Initializer for vtables of Q-Registers with "external" storage of strings.
* These rely on custom implementations of get_string() and set_string().
*/
#define TECO_INIT_QREG_EXTERNAL(...) TECO_INIT_QREG( \
.exchange_string = teco_qreg_external_exchange_string, \
.undo_exchange_string = teco_qreg_external_undo_exchange_string, \
.edit = teco_qreg_external_edit, \
.get_character = teco_qreg_external_get_character, \
.get_length = teco_qreg_external_get_length, \
.load = teco_qreg_external_load, \
.save = teco_qreg_external_save, \
##__VA_ARGS__ \
)
/*
* 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: Something could be implemented here. There are 2 possibilities:
* Either it renames the current buffer, or opens a file (alternative to EB).
*/
static gboolean
teco_qreg_bufferinfo_set_string(teco_qreg_t *qreg, const gchar *str, gsize len,
guint codepage, 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;
}
/*
* 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,
guint *codepage, 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;
if (codepage)
*codepage = teco_default_codepage();
return TRUE;
}
/** @static @memberof teco_qreg_t */
teco_qreg_t *
teco_qreg_bufferinfo_new(void)
{
static teco_qreg_vtable_t vtable = TECO_INIT_QREG_EXTERNAL(
.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,
.get_string = teco_qreg_bufferinfo_get_string,
/*
* As teco_qreg_bufferinfo_set_string() is not implemented,
* it's important to not inherit teco_qreg_external_exchange_string().
* `[*` and `]*` will still work though.
* The inherited teco_qreg_external_load() will simply fail.
*/
.exchange_string = teco_qreg_plain_exchange_string,
.undo_exchange_string = teco_qreg_plain_undo_exchange_string
);
return teco_qreg_new(&vtable, "*", 1);
}
static gboolean
teco_qreg_workingdir_set_string(teco_qreg_t *qreg, const gchar *str, gsize len,
guint codepage, 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;
}
g_assert(dir.data != NULL);
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_get_string(teco_qreg_t *qreg, gchar **str, gsize *len,
guint *codepage, 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);
if (codepage)
*codepage = teco_default_codepage();
return TRUE;
}
/** @static @memberof teco_qreg_t */
teco_qreg_t *
teco_qreg_workingdir_new(void)
{
static teco_qreg_vtable_t vtable = TECO_INIT_QREG_EXTERNAL(
.set_string = teco_qreg_workingdir_set_string,
.undo_set_string = teco_qreg_workingdir_undo_set_string,
.append_string = teco_qreg_workingdir_append_string,
.get_string = teco_qreg_workingdir_get_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,
guint codepage, 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_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,
guint *codepage, 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;
if (codepage)
*codepage = teco_default_codepage();
return TRUE;
}
/*
* Regardless of whether EOL normalization is enabled,
* this will never perform it.
* Other than that, it's very similar to teco_qreg_external_load().
*/
static gboolean
teco_qreg_clipboard_load(teco_qreg_t *qreg, const gchar *filename, GError **error)
{
g_assert(!teco_string_contains(&qreg->head.name, '\0'));
const gchar *clipboard_name = qreg->head.name.data + 1;
g_auto(teco_string_t) str = {NULL, 0};
return g_file_get_contents(filename, &str.data, &str.len, error) &&
teco_qreg_clipboard_undo_set_string(qreg, error) &&
teco_interface_set_clipboard(clipboard_name, str.data, str.len, error);
}
/** @static @memberof teco_qreg_t */
teco_qreg_t *
teco_qreg_clipboard_new(const gchar *name)
{
static teco_qreg_vtable_t vtable = TECO_INIT_QREG_EXTERNAL(
.set_string = teco_qreg_clipboard_set_string,
.undo_set_string = teco_qreg_clipboard_undo_set_string,
.append_string = teco_qreg_clipboard_append_string,
.get_string = teco_qreg_clipboard_get_string,
.load = teco_qreg_clipboard_load
);
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(®->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.
*
* 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)
{
g_auto(GStrv) env = g_get_environ();
for (gchar **key = env; *key; key++) {
const gchar *p = *key;
/*
* FIXME: On Win32, the key sometimes starts with `=`
* which shouldn't be possible and in reality it is a `!`.
* For instance `=C:=C:\msys64`.
*/
#ifdef G_OS_WIN32
if (G_UNLIKELY(*p == '='))
p++;
#endif
gchar *value = strchr(p, '=');
assert(value != NULL);
*value++ = '\0';
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;
}
if (!qreg->vtable->set_string(qreg, value, strlen(value),
teco_default_codepage(), 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, NULL, 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};
guint codepage;
if (!qreg->vtable->get_integer(qreg, &entry.integer, error) ||
!qreg->vtable->get_string(qreg, &string.data, &string.len, &codepage, error))
return FALSE;
teco_doc_init(&entry.string);
teco_doc_set_string(&entry.string, string.data, string.len, codepage);
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;
}
void
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);
}
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;
if (teco_qreg_table_current == &locals) {
/* currently editing local Q-Register that's about to be freed */
teco_error_editinglocalqreg_set(error, teco_qreg_current->head.name.data,
teco_qreg_current->head.name.len);
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;
bool 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,
gunichar 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, gunichar 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, gunichar 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, gunichar 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_wc(&ctx->name, g_unichar_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, gunichar 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_wc(&ctx->name, g_unichar_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, gunichar 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_wc(&ctx->name, g_unichar_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, gunichar 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,
gunichar 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, gunichar 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)
{
guint 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;
/*
* FIXME: This is not quite right as it will propose even
* lower case single or two-letter Q-Register names.
*/
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);