aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am1
-rw-r--r--src/cmdline.c344
-rw-r--r--src/cmdline.h56
-rw-r--r--src/core-commands.c834
-rw-r--r--src/core-commands.h93
-rw-r--r--src/doc.c5
-rw-r--r--src/doc.h6
-rw-r--r--src/eol.c2
-rw-r--r--src/eol.h2
-rw-r--r--src/error.c4
-rw-r--r--src/error.h21
-rw-r--r--src/expressions.c40
-rw-r--r--src/expressions.h2
-rw-r--r--src/file-utils.c21
-rw-r--r--src/file-utils.h6
-rw-r--r--src/glob.c56
-rw-r--r--src/glob.h9
-rw-r--r--src/goto-commands.c120
-rw-r--r--src/goto-commands.h9
-rw-r--r--src/goto.c2
-rw-r--r--src/goto.h11
-rw-r--r--src/help.c11
-rw-r--r--src/help.h4
-rw-r--r--src/interface-curses/curses-icons.c8
-rw-r--r--src/interface-curses/curses-icons.h2
-rw-r--r--src/interface-curses/curses-info-popup.c54
-rw-r--r--src/interface-curses/curses-info-popup.h2
-rw-r--r--src/interface-curses/curses-utils.c2
-rw-r--r--src/interface-curses/curses-utils.h5
-rw-r--r--src/interface-curses/interface.c824
-rw-r--r--src/interface-gtk/gtk-info-popup.c21
-rw-r--r--src/interface-gtk/gtk-info-popup.h2
-rw-r--r--src/interface-gtk/gtk-label.c60
-rw-r--r--src/interface-gtk/gtk-label.h6
-rw-r--r--src/interface-gtk/interface.c461
-rw-r--r--src/interface-gtk/view.c6
-rw-r--r--src/interface.c81
-rw-r--r--src/interface.h57
-rw-r--r--src/lexer.c71
-rw-r--r--src/lexer.h16
-rw-r--r--src/list.h2
-rw-r--r--src/main.c119
-rw-r--r--src/memory.c20
-rw-r--r--src/memory.h2
-rw-r--r--src/move-commands.c43
-rw-r--r--src/move-commands.h2
-rw-r--r--src/parser.c411
-rw-r--r--src/parser.h86
-rw-r--r--src/qreg-commands.c235
-rw-r--r--src/qreg-commands.h48
-rw-r--r--src/qreg.c165
-rw-r--r--src/qreg.h45
-rw-r--r--src/rb3str.c12
-rw-r--r--src/rb3str.h2
-rw-r--r--src/ring.c431
-rw-r--r--src/ring.h33
-rw-r--r--src/sciteco.h23
-rw-r--r--src/search.c228
-rw-r--r--src/search.h15
-rw-r--r--src/spawn.c40
-rw-r--r--src/spawn.h6
-rw-r--r--src/stdio-commands.c405
-rw-r--r--src/stdio-commands.h30
-rw-r--r--src/string-utils.c44
-rw-r--r--src/string-utils.h37
-rwxr-xr-xsrc/symbols-extract.tes16
-rw-r--r--src/symbols.c81
-rw-r--r--src/symbols.h4
-rw-r--r--src/undo.c3
-rw-r--r--src/undo.h5
-rw-r--r--src/view.c180
-rw-r--r--src/view.h17
72 files changed, 4074 insertions, 2053 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index e7a8545..ff2e86b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -45,6 +45,7 @@ libsciteco_base_la_SOURCES = main.c sciteco.h list.h \
parser.c parser.h \
core-commands.c core-commands.h \
move-commands.c move-commands.h \
+ stdio-commands.c stdio-commands.h \
search.c search.h \
spawn.c spawn.h \
glob.c glob.h \
diff --git a/src/cmdline.c b/src/cmdline.c
index b03f72a..fa69d91 100644
--- a/src/cmdline.c
+++ b/src/cmdline.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -61,17 +61,49 @@ int malloc_trim(size_t pad);
#define TECO_DEFAULT_BREAK_CHARS " \t\v\r\n\f<>,;@"
-teco_cmdline_t teco_cmdline = {};
+/** Style used for the asterisk at the beginning of the command line */
+#define STYLE_ASTERISK 64
-/*
- * FIXME: Should this be here?
- * Should perhaps rather be in teco_machine_main_t or teco_cmdline_t.
- */
-gboolean teco_quit_requested = FALSE;
+teco_cmdline_t teco_cmdline = {
+ .height = 1
+};
-/** Last terminated command line */
+/**
+ * Last terminated command line.
+ * This is not a teco_doc_scintilla_t since we have to return it as a string
+ * at the end of the day.
+ */
static teco_string_t teco_last_cmdline = {NULL, 0};
+void
+teco_cmdline_init(void)
+{
+ teco_cmdline.view = teco_view_new();
+ teco_view_setup(teco_cmdline.view);
+
+ teco_cmdline_ssm(SCI_SETUNDOCOLLECTION, FALSE, 0);
+ teco_cmdline_ssm(SCI_SETVSCROLLBAR, FALSE, 0);
+ teco_cmdline_ssm(SCI_STYLESETBOLD, STYLE_ASTERISK, TRUE);
+ teco_cmdline_ssm(SCI_SETMARGINTYPEN, 1, SC_MARGIN_TEXT);
+ teco_cmdline_ssm(SCI_MARGINSETSTYLE, 0, STYLE_ASTERISK);
+ teco_cmdline_ssm(SCI_SETMARGINWIDTHN, 1,
+ teco_cmdline_ssm(SCI_TEXTWIDTH, STYLE_ASTERISK, (sptr_t)"*"));
+ /* NOTE: might not work on all UIs */
+ teco_cmdline_ssm(SCI_INDICSETSTYLE, INDICATOR_RUBBEDOUT, INDIC_STRIKE);
+ teco_cmdline_ssm(SCI_INDICSETFORE, INDICATOR_RUBBEDOUT,
+ teco_cmdline_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+
+ /* single line mode - EOL characters won't break the line */
+ teco_cmdline_ssm(SCI_SETLINEENDTYPESALLOWED, SC_LINE_END_TYPE_NONE, 0);
+ /* render tabs as "TAB" without indentation */
+ teco_cmdline_ssm(SCI_SETTABDRAWMODE, SCTD_CONTROLCHAR, 0);
+
+ /*
+ * FIXME: Something resets the margin text, so we have to set it last.
+ */
+ teco_cmdline_ssm(SCI_MARGINSETTEXT, 0, (sptr_t)"*");
+}
+
/**
* Insert string into command line and execute
* it immediately.
@@ -89,40 +121,49 @@ teco_cmdline_insert(const gchar *data, gsize len, GError **error)
g_auto(teco_string_t) old_cmdline = {NULL, 0};
gsize repl_pc = 0;
- teco_cmdline.machine.macro_pc = teco_cmdline.pc = teco_cmdline.effective_len;
+ gsize effective_len = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_cmdline.machine.macro_pc = teco_cmdline.pc = effective_len;
+
+ const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
- if (len <= teco_cmdline.str.len - teco_cmdline.effective_len &&
- !teco_string_cmp(&src, teco_cmdline.str.data + teco_cmdline.effective_len, len)) {
- teco_cmdline.effective_len += len;
+ if (len <= macro_len - effective_len &&
+ !teco_string_cmp(src, macro + effective_len, len)) {
+ /* extend effective command line from rubbed out part */
+ teco_cmdline_ssm(SCI_GOTOPOS, effective_len+len, 0);
} else {
- if (teco_cmdline.effective_len < teco_cmdline.str.len)
+ /* discard rubbed out part of the command line */
+ if (effective_len < macro_len)
/*
* Automatically disable immediate editing modifier.
* FIXME: Should we show a message as when pressing ^G?
*/
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;
+ teco_cmdline_ssm(SCI_DELETERANGE, effective_len, macro_len - effective_len);
+ teco_cmdline_ssm(SCI_ADDTEXT, len, (sptr_t)data);
+
+ /* the pointer shouldn't have changed... */
+ macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
}
+ effective_len += 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) {
+ while (teco_cmdline.pc < 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 (!teco_machine_main_step(&teco_cmdline.machine, macro, 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
+ * Exchange command lines
*/
teco_qreg_t *cmdline_reg = teco_qreg_table_find(&teco_qreg_table_globals, "\e", 1);
- teco_string_t new_cmdline;
+ g_auto(teco_string_t) new_cmdline = {NULL, 0};
if (!cmdline_reg->vtable->get_string(cmdline_reg, &new_cmdline.data, &new_cmdline.len,
NULL, error))
@@ -133,16 +174,26 @@ teco_cmdline_insert(const gchar *data, gsize len, GError **error)
* 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_cmdline.pc = teco_string_diff(new_cmdline, macro, effective_len);
teco_undo_pop(teco_cmdline.pc);
+ /*
+ * We don't replace the command line's document, since that would
+ * reset the line end type and other configurable settings.
+ * Also, we don't clear the document to avoid unnecessary restylings
+ * if syntax highlighting is enabled on the command line.
+ */
g_assert(old_cmdline.len == 0);
- old_cmdline = teco_cmdline.str;
- teco_cmdline.str = new_cmdline;
- teco_cmdline.effective_len = new_cmdline.len;
+ teco_string_init(&old_cmdline, macro, effective_len);
+ teco_cmdline_ssm(SCI_DELETERANGE, teco_cmdline.pc,
+ old_cmdline.len-teco_cmdline.pc);
+ teco_cmdline_ssm(SCI_ADDTEXT, new_cmdline.len-teco_cmdline.pc,
+ (sptr_t)new_cmdline.data+teco_cmdline.pc);
+
+ macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ macro_len = effective_len = new_cmdline.len;
teco_cmdline.machine.macro_pc = repl_pc = teco_cmdline.pc;
-
continue;
}
@@ -154,17 +205,26 @@ teco_cmdline_insert(const gchar *data, gsize len, GError **error)
/*
* Error during command-line replacement.
* Replay previous command-line.
- * This avoids deep copying.
+ * The commands leading up to the failed replacement
+ * will be left rubbed out.
*/
teco_undo_pop(repl_pc);
- teco_string_clear(&teco_cmdline.str);
- teco_cmdline.str = old_cmdline;
+ /*
+ * May cause restyling of the command lines,
+ * but that's probably okay - it's just a fallback.
+ */
+ teco_cmdline_ssm(SCI_CLEARALL, 0, 0);
+ teco_cmdline_ssm(SCI_ADDTEXT, old_cmdline.len, (sptr_t)old_cmdline.data);
+ teco_string_clear(&old_cmdline);
memset(&old_cmdline, 0, sizeof(old_cmdline));
teco_cmdline.machine.macro_pc = teco_cmdline.pc = repl_pc;
- /* rubout cmdline replacement command */
- teco_cmdline.effective_len--;
+ /* rub out cmdline replacement command */
+ teco_cmdline_ssm(SCI_GOTOPOS, --effective_len, 0);
+
+ macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
continue;
}
}
@@ -183,14 +243,18 @@ teco_cmdline_insert(const gchar *data, gsize len, GError **error)
static gboolean
teco_cmdline_rubin(GError **error)
{
- if (!teco_cmdline.str.len)
- return TRUE;
-
- const gchar *start, *end, *next;
- start = teco_cmdline.str.data+teco_cmdline.effective_len;
- end = teco_cmdline.str.data+teco_cmdline.str.len;
- next = g_utf8_find_next_char(start, end) ? : end;
- return teco_cmdline_insert(start, next-start, error);
+ gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
+ gsize pos = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0);
+ gchar buf[4+1];
+ struct Sci_TextRangeFull range = {
+ .chrg = {pos, MIN(macro_len, pos+sizeof(buf)-1)},
+ .lpstrText = buf
+ };
+ gsize len = teco_cmdline_ssm(SCI_GETTEXTRANGEFULL, 0, (sptr_t)&range);
+
+ const gchar *end = buf+len;
+ const gchar *next = g_utf8_find_next_char(buf, end) ? : end;
+ return teco_cmdline_insert(buf, next-buf, error);
}
/**
@@ -211,10 +275,9 @@ teco_cmdline_rubin(GError **error)
gboolean
teco_cmdline_keypress(const gchar *data, gsize len, GError **error)
{
- const teco_string_t str = {(gchar *)data, len};
teco_machine_t *machine = &teco_cmdline.machine.parent;
- if (!teco_string_validate_utf8(&str)) {
+ if (!teco_string_validate_utf8((teco_string_t){(gchar *)data, len})) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
"Invalid UTF-8 sequence");
return FALSE;
@@ -225,7 +288,7 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error)
*/
teco_interface_msg_clear();
- gsize start_pc = teco_cmdline.effective_len;
+ gsize start_pc = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0);
for (guint i = 0; i < len; i = g_utf8_next_char(data+i) - data) {
gunichar chr = g_utf8_get_char(data+i);
@@ -252,9 +315,9 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error)
* up until the insertion point.
*/
teco_undo_pop(start_pc);
- teco_cmdline.effective_len = start_pc;
+ teco_cmdline_ssm(SCI_GOTOPOS, start_pc, 0);
/* program counter could be messed up */
- teco_cmdline.machine.macro_pc = teco_cmdline.effective_len;
+ teco_cmdline.machine.macro_pc = start_pc;
#ifdef HAVE_MALLOC_TRIM
/*
@@ -285,7 +348,7 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error)
teco_interface_popup_clear();
- if (teco_quit_requested) {
+ if (teco_ed & TECO_ED_EXIT) {
/* caught by user interface */
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, "");
return FALSE;
@@ -303,9 +366,16 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error)
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;
+ teco_last_cmdline.len = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_last_cmdline.data = g_malloc(teco_last_cmdline.len + 1);
+ teco_cmdline_ssm(SCI_GETTEXT, teco_last_cmdline.len,
+ (sptr_t)teco_last_cmdline.data);
+ /*
+ * FIXME: Preserve the command line after the $$.
+ * This would be useful for command line editing macros.
+ * Perhaps just call teco_cmdline_insert().
+ */
+ teco_cmdline_ssm(SCI_CLEARALL, 0, 0);
#ifdef HAVE_MALLOC_TRIM
/* see above */
@@ -320,10 +390,8 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error)
start_pc = 0;
}
- /*
- * Echo command line
- */
- teco_interface_cmdline_update(&teco_cmdline);
+ teco_cmdline_update();
+
return TRUE;
}
@@ -380,20 +448,48 @@ teco_cmdline_keymacro(const gchar *name, gssize name_len, GError **error)
static void
teco_cmdline_rubout(void)
{
- const gchar *p;
- p = g_utf8_find_prev_char(teco_cmdline.str.data,
- teco_cmdline.str.data+teco_cmdline.effective_len);
- if (p) {
- teco_cmdline.effective_len = p - teco_cmdline.str.data;
- teco_undo_pop(teco_cmdline.effective_len);
+ gsize effective_len = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0);
+ gssize p = teco_view_glyphs2bytes_relative(teco_cmdline.view, effective_len, -1);
+ if (p >= 0) {
+ teco_cmdline_ssm(SCI_GOTOPOS, p, 0);
+ teco_undo_pop(p);
}
}
-static void TECO_DEBUG_CLEANUP
+/**
+ * Update the command line, i.e. prepare it for displaying.
+ *
+ * This updates the indicators and scrolls the caret, which isn't done every time
+ * we touch the command line itself.
+ */
+void
+teco_cmdline_update(void)
+{
+ /*
+ * FIXME: Perhaps this can be avoided completely by updating the
+ * indicators in teco_cmdline_insert().
+ */
+ gsize effective_len = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0);
+ gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
+ teco_cmdline_ssm(SCI_SETINDICATORCURRENT, INDICATOR_RUBBEDOUT, 0);
+ teco_cmdline_ssm(SCI_INDICATORCLEARRANGE, 0, macro_len);
+ teco_cmdline_ssm(SCI_INDICATORFILLRANGE, effective_len, macro_len - effective_len);
+
+ teco_cmdline_ssm(SCI_SCROLLCARET, 0, 0);
+
+ /*
+ * FIXME: This gets reset repeatedly.
+ * Setting it once per keypress however means you can no longer customize
+ * the margin text.
+ */
+ teco_cmdline_ssm(SCI_MARGINSETTEXT, 0, (sptr_t)"*");
+}
+
+void
teco_cmdline_cleanup(void)
{
teco_machine_main_clear(&teco_cmdline.machine);
- teco_string_clear(&teco_cmdline.str);
+ teco_view_free(teco_cmdline.view);
teco_string_clear(&teco_last_cmdline);
}
@@ -451,11 +547,12 @@ teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gun
if (teco_cmdline.modifier_enabled) {
/* reinsert construct */
+ gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
do {
if (!teco_cmdline_rubin(error))
return FALSE;
} while (!ctx->current->is_start &&
- teco_cmdline.effective_len < teco_cmdline.str.len);
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len);
} else {
/* rubout construct */
do
@@ -475,6 +572,11 @@ teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gun
raise(SIGTSTP);
return TRUE;
#endif
+
+ case TECO_CTL_KEY('L'):
+ /* causes a complete screen redraw */
+ teco_interface_refresh(TRUE);
+ return TRUE;
}
teco_interface_popup_clear();
@@ -506,6 +608,9 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa
case TECO_CTL_KEY('W'): /* rubout/reinsert command */
teco_interface_popup_clear();
+ const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
+
/*
* This mimics the behavior of the `Y` command,
* so it also rubs out no-op commands.
@@ -515,9 +620,8 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa
/* reinsert command */
/* @ and : are not separate states, but practically belong to the command */
while (ctx->parent.current->is_start &&
- teco_cmdline.effective_len < teco_cmdline.str.len &&
- (teco_cmdline.str.data[teco_cmdline.effective_len] == ':' ||
- teco_cmdline.str.data[teco_cmdline.effective_len] == '@'))
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len &&
+ strchr(":@", macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)]) != NULL)
if (!teco_cmdline_rubin(error))
return FALSE;
@@ -525,11 +629,11 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa
if (!teco_cmdline_rubin(error))
return FALSE;
} while (!ctx->parent.current->is_start &&
- teco_cmdline.effective_len < teco_cmdline.str.len);
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len);
while (ctx->parent.current->is_start &&
- teco_cmdline.effective_len < teco_cmdline.str.len &&
- strchr(TECO_NOOPS, teco_cmdline.str.data[teco_cmdline.effective_len]))
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len &&
+ teco_is_noop(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)]))
if (!teco_cmdline_rubin(error))
return FALSE;
@@ -538,8 +642,8 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa
/* rubout command */
while (ctx->parent.current->is_start &&
- teco_cmdline.effective_len > 0 &&
- strchr(TECO_NOOPS, teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) > 0 &&
+ teco_is_noop(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1]))
teco_cmdline_rubout();
do
@@ -553,7 +657,7 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa
*/
while (ctx->parent.current->is_start &&
(ctx->flags.modifier_at || ctx->flags.modifier_colon) &&
- teco_cmdline.effective_len > 0)
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) > 0)
teco_cmdline_rubout();
return TRUE;
@@ -568,6 +672,9 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *
{
teco_state_t *current = ctx->parent.current;
+ const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
+
switch (key) {
case TECO_CTL_KEY('W'): { /* rubout/reinsert word */
teco_interface_popup_clear();
@@ -585,15 +692,15 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *
if (teco_cmdline.modifier_enabled) {
/* reinsert word chars */
while (ctx->parent.current == current &&
- teco_cmdline.effective_len < teco_cmdline.str.len &&
- teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len]))
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len &&
+ teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)]))
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_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len]))
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len &&
+ !teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)]))
if (!teco_cmdline_rubin(error))
return FALSE;
@@ -607,7 +714,7 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *
* a result string even in parse-only mode.
*/
if (ctx->result && ctx->result->len > 0) {
- gboolean is_wordchar = teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]);
+ gboolean is_wordchar = teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1]);
teco_cmdline_rubout();
if (ctx->parent.current != current) {
/* rub out string building command */
@@ -623,13 +730,13 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *
*/
if (!is_wordchar) {
while (ctx->result->len > 0 &&
- !teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ !teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1]))
teco_cmdline_rubout();
}
/* rubout word chars */
while (ctx->result->len > 0 &&
- teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1]))
teco_cmdline_rubout();
return TRUE;
@@ -646,8 +753,7 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *
if (teco_cmdline.modifier_enabled) {
/* reinsert string */
- while (ctx->parent.current == current &&
- teco_cmdline.effective_len < teco_cmdline.str.len)
+ while (ctx->parent.current == current && teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len)
if (!teco_cmdline_rubin(error))
return FALSE;
@@ -685,7 +791,7 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *
return TRUE;
}
- const gchar *filename = teco_string_last_occurrence(ctx->result,
+ 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);
@@ -716,11 +822,11 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *
}
gboolean
-teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, const teco_string_t *str, GError **error)
+teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, teco_string_t str, GError **error)
{
g_auto(teco_string_t) str_escaped;
- teco_machine_stringbuilding_escape(ctx, str->data, str->len, &str_escaped);
- if (!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1]))
+ teco_machine_stringbuilding_escape(ctx, str.data, str.len, &str_escaped);
+ if (!str.len || !G_IS_DIR_SEPARATOR(str.data[str.len-1]))
teco_string_append_c(&str_escaped, ' ');
return teco_cmdline_insert(str_escaped.data, str_escaped.len, error);
}
@@ -765,7 +871,7 @@ teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_
}
gboolean
-teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
@@ -825,18 +931,21 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t
case TECO_CTL_KEY('W'): /* rubout/reinsert file names including directories */
teco_interface_popup_clear();
+ const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0);
+
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]))
+ teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len &&
+ !G_IS_DIR_SEPARATOR(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)]))
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_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len &&
+ G_IS_DIR_SEPARATOR(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)]) &&
!teco_cmdline_rubin(error))
return FALSE;
@@ -845,12 +954,12 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t
if (ctx->expectstring.string.len > 0) {
/* rubout directory separator */
- if (G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ if (G_IS_DIR_SEPARATOR(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-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]))
+ !G_IS_DIR_SEPARATOR(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1]))
teco_cmdline_rubout();
return TRUE;
@@ -871,7 +980,7 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t
return TRUE;
}
- if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ if (teco_string_contains(ctx->expectstring.string, '\0'))
/* null-byte not allowed in file names */
return TRUE;
@@ -890,13 +999,13 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t
}
gboolean
-teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
g_auto(teco_string_t) str_escaped;
- teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped);
- if ((!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1])) &&
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped);
+ if ((!str.len || !G_IS_DIR_SEPARATOR(str.data[str.len-1])) &&
ctx->expectstring.nesting == 1)
teco_string_append_wc(&str_escaped,
ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
@@ -927,7 +1036,7 @@ teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t
return TRUE;
}
- if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ if (teco_string_contains(ctx->expectstring.string, '\0'))
/* null-byte not allowed in file names */
return TRUE;
@@ -958,14 +1067,14 @@ teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t
}
gboolean
-teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
- g_autofree gchar *pattern_escaped = teco_globber_escape_pattern(str->data);
+ g_autofree gchar *pattern_escaped = teco_globber_escape_pattern(str.data);
g_auto(teco_string_t) str_escaped;
teco_machine_stringbuilding_escape(stringbuilding_ctx, pattern_escaped, strlen(pattern_escaped), &str_escaped);
- if ((!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1])) &&
+ if ((!str.len || !G_IS_DIR_SEPARATOR(str.data[str.len-1])) &&
ctx->expectstring.nesting == 1)
teco_string_append_wc(&str_escaped,
ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
@@ -996,7 +1105,7 @@ teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *
return TRUE;
}
- if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ if (teco_string_contains(ctx->expectstring.string, '\0'))
/* null-byte not allowed in file names */
return TRUE;
@@ -1016,7 +1125,7 @@ teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *
}
gboolean
-teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
@@ -1024,7 +1133,7 @@ teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, const teco_stri
* FIXME: We might terminate the command in case of leaf directories.
*/
g_auto(teco_string_t) str_escaped;
- teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped);
return teco_cmdline_insert(str_escaped.data, str_escaped.len, error);
}
@@ -1041,7 +1150,7 @@ teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t
}
gboolean
-teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
g_assert(ctx->expectqreg != NULL);
/*
@@ -1092,9 +1201,9 @@ teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_
}
gboolean
-teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, GError **error)
+teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, teco_string_t str, GError **error)
{
- return teco_cmdline_insert(str->data, str->len, error);
+ return teco_cmdline_insert(str.data, str.len, error);
}
gboolean
@@ -1138,12 +1247,12 @@ teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_m
}
gboolean
-teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, GError **error)
+teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, teco_string_t str, GError **error)
{
teco_machine_stringbuilding_t *stringbuilding_ctx = teco_machine_qregspec_get_stringbuilding(ctx);
g_auto(teco_string_t) str_escaped;
- teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped);
teco_string_append_c(&str_escaped, ']');
return teco_cmdline_insert(str_escaped.data, str_escaped.len, error);
}
@@ -1178,7 +1287,7 @@ teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa
return TRUE;
}
- const gchar *filename = teco_string_last_occurrence(&ctx->expectstring.string,
+ 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);
@@ -1217,7 +1326,7 @@ teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_mac
return TRUE;
}
- const gchar *symbol = teco_string_last_occurrence(&ctx->expectstring.string, ",");
+ 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;
@@ -1239,12 +1348,12 @@ teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_mac
}
gboolean
-teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
g_auto(teco_string_t) str_escaped;
- teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped);
teco_string_append_c(&str_escaped, ',');
return teco_cmdline_insert(str_escaped.data, str_escaped.len, error);
}
@@ -1274,7 +1383,7 @@ teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren
}
teco_string_t label = ctx->expectstring.string;
- gint i = teco_string_rindex(&label, ',');
+ gint i = teco_string_rindex(label, ',');
if (i >= 0) {
label.data += i+1;
label.len -= i+1;
@@ -1297,12 +1406,12 @@ teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren
}
gboolean
-teco_state_goto_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_goto_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
g_auto(teco_string_t) str_escaped;
- teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped);
/*
* FIXME: This does not escape `,`. Cannot be escaped via ^Q currently?
*/
@@ -1334,7 +1443,7 @@ teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren
return TRUE;
}
- if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ if (teco_string_contains(ctx->expectstring.string, '\0'))
/* help term must not contain null-byte */
return TRUE;
@@ -1353,12 +1462,12 @@ teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren
}
gboolean
-teco_state_help_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_help_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
g_auto(teco_string_t) str_escaped;
- teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped);
if (ctx->expectstring.nesting == 1)
teco_string_append_wc(&str_escaped,
ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
@@ -1394,5 +1503,6 @@ teco_state_save_cmdline_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg
* Q-Register <q>.
*/
TECO_DEFINE_STATE_EXPECTQREG(teco_state_save_cmdline,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_save_cmdline_got_register
);
diff --git a/src/cmdline.h b/src/cmdline.h
index ebdf1e1..f6d0345 100644
--- a/src/cmdline.h
+++ b/src/cmdline.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -19,10 +19,13 @@
#include <glib.h>
#include "sciteco.h"
-#include "string-utils.h"
#include "parser.h"
+#include "view.h"
#include "undo.h"
+/** Indicator number used for the rubbed out part of the command line */
+#define INDICATOR_RUBBEDOUT (INDICATOR_CONTAINER+0)
+
typedef struct {
/**
* State machine used for interactive mode (commandline macro).
@@ -34,16 +37,17 @@ typedef struct {
teco_machine_main_t machine;
/**
- * String containing the current command line
- * (both effective and rubbed out).
- */
- teco_string_t str;
- /**
- * Effective command line length.
- * The length of the rubbed out part of the command line
- * is (teco_cmdline.str.len - teco_cmdline.effective_len).
+ * Command-line Scintilla view.
+ * It's document contains the current command line macro.
+ * The current position (cursor) marks the end of the
+ * "effective" command line, while everything afterwards
+ * is the rubbed out part of the command line.
+ * The rubbed out part should be highlighted with an indicator.
*/
- gsize effective_len;
+ teco_view_t *view;
+
+ /** Height of the command line view in lines */
+ guint height;
/** Program counter within the command-line macro */
gsize pc;
@@ -60,6 +64,30 @@ typedef struct {
extern teco_cmdline_t teco_cmdline;
+void teco_cmdline_init(void);
+
+static inline sptr_t
+teco_cmdline_ssm(unsigned int iMessage, uptr_t wParam, sptr_t lParam)
+{
+ return teco_view_ssm(teco_cmdline.view, iMessage, wParam, lParam);
+}
+
+/**
+ * Update scroll beavior on command line after window resizes.
+ *
+ * This should ensure that the caret jumps to the middle of the command line.
+ *
+ * @param width Window (command line view) width in pixels or columns.
+ *
+ * @fixme
+ * On the other hand this limits how you can customize the scroll behavior.
+ */
+static inline void
+teco_cmdline_resized(guint width)
+{
+ teco_cmdline_ssm(SCI_SETXCARETPOLICY, CARET_SLOP | CARET_EVEN, width/2);
+}
+
gboolean teco_cmdline_keypress(const gchar *data, gsize len, GError **error);
typedef enum {
@@ -84,10 +112,12 @@ teco_cmdline_keymacro_c(gchar key, GError **error)
return TRUE;
}
-extern gboolean teco_quit_requested;
+void teco_cmdline_update(void);
+
+void teco_cmdline_cleanup(void);
/*
* Command states
*/
-TECO_DECLARE_STATE(teco_state_save_cmdline);
+extern teco_state_t teco_state_save_cmdline;
diff --git a/src/core-commands.c b/src/core-commands.c
index dbf86bd..f81bdf3 100644
--- a/src/core-commands.c
+++ b/src/core-commands.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -18,7 +18,9 @@
#include "config.h"
#endif
+#include <time.h>
#include <string.h>
+#include <stdio.h>
#include <glib.h>
#include <glib/gstdio.h>
@@ -42,31 +44,76 @@
#include "memory.h"
#include "eol.h"
#include "qreg.h"
+#include "stdio-commands.h"
#include "qreg-commands.h"
#include "goto-commands.h"
#include "move-commands.h"
#include "core-commands.h"
-gboolean teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
- gunichar key, GError **error);
+static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error);
+static teco_state_t *teco_state_ctlc_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error);
/**
- * @class TECO_DEFINE_STATE_COMMAND
- * @implements TECO_DEFINE_STATE_CASEINSENSITIVE
- * @ingroup states
- *
- * Base state for everything that is the beginning of a one or two
- * letter command.
+ * Translate buffer range arguments from the expression stack to
+ * a from-position and length in bytes.
+ *
+ * If only one argument is given, it is interpreted as a number of lines
+ * beginning with dot.
+ * If two arguments are given, it is interpreted as two buffer positions
+ * in glyphs.
+ *
+ * @param cmd Name of the command
+ * @param from_ret Where to store the from-position in bytes
+ * @param len_ret Where to store the length of the range in bytes
+ * @param error A GError
+ * @return FALSE if an error occurred
+ *
+ * @fixme There are still redundancies with teco_state_start_kill().
+ * But it needs to discern between invalid ranges and other errors.
*/
-#define TECO_DEFINE_STATE_COMMAND(NAME, ...) \
- TECO_DEFINE_STATE_CASEINSENSITIVE(NAME, \
- .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
- teco_state_command_process_edit_cmd, \
- .style = SCE_SCITECO_COMMAND, \
- ##__VA_ARGS__ \
- )
+gboolean
+teco_get_range_args(const gchar *cmd, gsize *from_ret, gsize *len_ret, GError **error)
+{
+ gssize from, len; /* in bytes */
-static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error);
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+
+ if (teco_expressions_args() <= 1) {
+ teco_int_t line;
+
+ if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error))
+ return FALSE;
+
+ from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0);
+
+ if (!teco_validate_line(line)) {
+ teco_error_range_set(error, cmd);
+ return FALSE;
+ }
+
+ len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from;
+
+ if (len < 0) {
+ from += len;
+ len *= -1;
+ }
+ } else {
+ gssize to = teco_interface_glyphs2bytes(teco_expressions_pop_num(0));
+ from = teco_interface_glyphs2bytes(teco_expressions_pop_num(0));
+ len = to - from;
+
+ if (len < 0 || from < 0 || to < 0) {
+ teco_error_range_set(error, cmd);
+ return FALSE;
+ }
+ }
+
+ *from_ret = from;
+ *len_ret = len;
+ return TRUE;
+}
/*
* NOTE: This needs some extra code in teco_state_start_input().
@@ -213,19 +260,20 @@ teco_state_start_backslash(teco_machine_main_t *ctx, GError **error)
return;
if (teco_expressions_args()) {
- teco_int_t value;
-
- if (!teco_expressions_pop_num_calc(&value, 0, error))
- return;
-
gchar buffer[TECO_EXPRESSIONS_FORMAT_LEN];
- gchar *str = teco_expressions_format(buffer, value,
+ gchar *str = teco_expressions_format(buffer,
+ teco_expressions_pop_num(0),
ctx->qreg_table_locals->radix);
g_assert(*str != '\0');
gsize len = strlen(str);
- teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
- teco_undo_gsize(teco_ranges[0].to) = teco_ranges[0].from + len;
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos);
+ /*
+ * We can assume that `len` is already in glyphs,
+ * i.e. formatted numbers will never use multi-byte/Unicode characters.
+ */
+ teco_undo_int(teco_ranges[0].to) = teco_ranges[0].from + len;
teco_undo_guint(teco_ranges_count) = 1;
teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
@@ -276,8 +324,7 @@ static void
teco_state_start_loop_open(teco_machine_main_t *ctx, GError **error)
{
teco_loop_context_t lctx;
- if (!teco_expressions_eval(FALSE, error) ||
- !teco_expressions_pop_num_calc(&lctx.counter, -1, error))
+ if (!teco_expressions_pop_num_calc(&lctx.counter, -1, error))
return;
lctx.brace_level = teco_brace_level;
lctx.pass_through = teco_machine_main_eval_colon(ctx) > 0;
@@ -370,7 +417,7 @@ teco_state_start_loop_close(teco_machine_main_t *ctx, GError **error)
}
}
-/*$ ";" break
+/*$ ";" ":;" break
* [bool]; -- Conditionally break from loop
* [bool]:;
*
@@ -504,9 +551,11 @@ teco_state_start_cmdline_push(teco_machine_main_t *ctx, GError **error)
!teco_qreg_table_edit_name(&teco_qreg_table_globals, "\e", 1, error))
return;
+ const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+
teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
teco_interface_ssm(SCI_CLEARALL, 0, 0);
- teco_interface_ssm(SCI_ADDTEXT, teco_cmdline.pc, (sptr_t)teco_cmdline.str.data);
+ teco_interface_ssm(SCI_ADDTEXT, teco_cmdline.pc, (sptr_t)macro);
teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
/*
@@ -536,34 +585,6 @@ teco_state_start_cmdline_pop(teco_machine_main_t *ctx, GError **error)
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CMDLINE, "");
}
-/*$ "=" print
- * <n>= -- Show value as message
- *
- * Shows integer <n> as a message in the message line and/or
- * on the console.
- * It is currently always formatted as a decimal integer and
- * shown with the user-message severity.
- * The command fails if <n> is not given.
- */
-/**
- * @todo perhaps care about current radix
- * @todo colon-modifier to suppress line-break on console?
- */
-static void
-teco_state_start_print(teco_machine_main_t *ctx, GError **error)
-{
- if (!teco_expressions_eval(FALSE, error))
- return;
- if (!teco_expressions_args()) {
- teco_error_argexpected_set(error, "=");
- return;
- }
- teco_int_t v;
- if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
- return;
- teco_interface_msg(TECO_MSG_USER, "%" TECO_INT_FORMAT, v);
-}
-
/*$ A
* [n]A -> code -- Get character code from buffer
* -A -> code
@@ -573,8 +594,6 @@ teco_state_start_print(teco_machine_main_t *ctx, GError **error)
* This can be an ASCII <code> or Unicode codepoint
* depending on Scintilla's encoding of the current
* buffer.
- * Invalid Unicode byte sequences are reported as
- * -1 or -2.
*
* - If <n> is 0, return the <code> of the character
* pointed to by dot.
@@ -585,12 +604,11 @@ teco_state_start_print(teco_machine_main_t *ctx, GError **error)
* - If <n> is omitted, the sign prefix is implied.
*
* If the position of the queried character is off-page,
- * the command will yield an error.
- *
+ * the command will return -1.
* If the document is encoded as UTF-8 and there is
- * an incomplete sequence at the requested position,
- * -1 is returned.
- * All other invalid Unicode sequences are returned as -2.
+ * an invalid byte sequence at the requested position,
+ * -2 is returned.
+ * Incomplete byte sequences are returned as -3.
*/
static void
teco_state_start_get(teco_machine_main_t *ctx, GError **error)
@@ -603,15 +621,11 @@ teco_state_start_get(teco_machine_main_t *ctx, GError **error)
gssize get_pos = teco_interface_glyphs2bytes_relative(pos, v);
sptr_t len = teco_interface_ssm(SCI_GETLENGTH, 0, 0);
- if (get_pos < 0 || get_pos == len) {
- teco_error_range_set(error, "A");
- return;
- }
-
- teco_expressions_push(teco_interface_get_character(get_pos, len));
+ teco_expressions_push(get_pos < 0 || get_pos == len
+ ? -1 : teco_interface_get_character(get_pos, len));
}
-static teco_state_t *
+teco_state_t *
teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
{
static teco_machine_main_transition_t transitions[] = {
@@ -621,7 +635,7 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
['$'] = {&teco_state_escape},
['!'] = {&teco_state_label},
['O'] = {&teco_state_goto,
- .modifier_at = TRUE},
+ .modifier_at = TRUE, .modifier_colon = 1},
['^'] = {&teco_state_control,
.modifier_at = TRUE, .modifier_colon = 2},
['F'] = {&teco_state_fcommand,
@@ -629,7 +643,7 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
['"'] = {&teco_state_condcommand},
['E'] = {&teco_state_ecommand,
.modifier_at = TRUE, .modifier_colon = 2},
- ['I'] = {&teco_state_insert_building,
+ ['I'] = {&teco_state_insert,
.modifier_at = TRUE},
['?'] = {&teco_state_help,
.modifier_at = TRUE},
@@ -639,8 +653,10 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
.modifier_at = TRUE, .modifier_colon = 1},
['['] = {&teco_state_pushqreg},
- [']'] = {&teco_state_popqreg},
- ['G'] = {&teco_state_getqregstring},
+ [']'] = {&teco_state_popqreg,
+ .modifier_colon = 1},
+ ['G'] = {&teco_state_getqregstring,
+ .modifier_colon = 1},
['Q'] = {&teco_state_queryqreg,
.modifier_colon = 1},
['U'] = {&teco_state_setqreginteger,
@@ -650,6 +666,8 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
.modifier_colon = 1},
['X'] = {&teco_state_copytoqreg,
.modifier_at = TRUE, .modifier_colon = 1},
+ ['='] = {&teco_state_print_decimal,
+ .modifier_colon = 1},
/*
* Arithmetics
@@ -702,28 +720,25 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
.modifier_colon = 1},
['D'] = {&teco_state_start, teco_state_start_delete_chars,
.modifier_colon = 1},
- ['='] = {&teco_state_start, teco_state_start_print},
- ['A'] = {&teco_state_start, teco_state_start_get}
+ ['A'] = {&teco_state_start, teco_state_start_get},
+ ['T'] = {&teco_state_start, teco_state_start_typeout}
};
- switch (chr) {
/*
- * No-ops (same as TECO_NOOPS):
+ * Non-operational commands.
* These are explicitly not handled in teco_state_control,
* so that we can potentially reuse the upcaret notations like ^J.
*/
- case ' ':
- case '\f':
- case '\r':
- case '\n':
- case '\v':
+ if (teco_is_noop(chr)) {
if (ctx->flags.modifier_at ||
(ctx->flags.mode == TECO_MODE_NORMAL && ctx->flags.modifier_colon)) {
teco_error_modifier_set(error, chr);
return NULL;
}
return &teco_state_start;
+ }
+ switch (chr) {
/*$ 0 1 2 3 4 5 6 7 8 9 digit number
* [n]0|1|2|3|4|5|6|7|8|9 -> n*Radix+X -- Append digit
*
@@ -758,8 +773,10 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
* for beginnings of command-lines?
* It could also be used for a corresponding KEYMACRO mask.
*/
- if (teco_cmdline.effective_len == 1 && teco_cmdline.str.data[0] == '*')
+ if (teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) == 1 &&
+ teco_cmdline_ssm(SCI_GETCHARAT, 0, 0) == '*')
return &teco_state_save_cmdline;
+ /* treat as an operator */
break;
case '<':
@@ -898,20 +915,18 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
teco_ascii_toupper(chr), error);
}
-TECO_DEFINE_STATE_COMMAND(teco_state_start,
- .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */
- .is_start = TRUE,
- .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE
+TECO_DEFINE_STATE_START(teco_state_start,
+ .input_cb = (teco_state_input_cb_t)teco_state_start_input
);
-/*$ F<
+/*$ "F<" ":F<"
* F< -- Go to loop start or jump to beginning of macro
* :F<
*
* Immediately jumps to the current loop's start.
* Also works from inside conditionals.
*
- * This command behaves exactly like \fB>\fP with regard to
+ * This command behaves exactly like \fB<\fP with regard to
* colon-modifiers.
*
* Outside of loops \(em or in a macro without
@@ -945,7 +960,7 @@ teco_state_fcommand_loop_start(teco_machine_main_t *ctx, GError **error)
ctx->macro_pc = lctx->pc;
}
-/*$ F> continue
+/*$ "F>" ":F>" continue
* F> -- Go to loop end or return from macro
* :F>
*
@@ -1043,6 +1058,8 @@ teco_state_fcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error
.modifier_at = TRUE, .modifier_colon = 2},
['R'] = {&teco_state_replace_default,
.modifier_at = TRUE, .modifier_colon = 2},
+ ['N'] = {&teco_state_replace_default_all,
+ .modifier_at = TRUE, .modifier_colon = 1},
['G'] = {&teco_state_changedir,
.modifier_at = TRUE},
@@ -1065,7 +1082,9 @@ teco_state_fcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error
teco_ascii_toupper(chr), error);
}
-TECO_DEFINE_STATE_COMMAND(teco_state_fcommand);
+TECO_DEFINE_STATE_COMMAND(teco_state_fcommand,
+ .input_cb = (teco_state_input_cb_t)teco_state_fcommand_input
+);
static void
teco_undo_change_dir_action(gchar **dir, gboolean run)
@@ -1089,12 +1108,12 @@ teco_undo_change_dir_to_current(void)
}
static teco_state_t *
-teco_state_changedir_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_changedir_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
- g_autofree gchar *dir = teco_file_expand_path(str->data);
+ g_autofree gchar *dir = teco_file_expand_path(str.data);
if (!*dir) {
teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "$HOME", 5);
g_assert(qreg != NULL);
@@ -1105,7 +1124,7 @@ teco_state_changedir_done(teco_machine_main_t *ctx, const teco_string_t *str, GE
/*
* Null-characters must not occur in file names.
*/
- if (teco_string_contains(&home, '\0')) {
+ if (teco_string_contains(home, '\0')) {
teco_string_clear(&home);
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Null-character not allowed in filenames");
@@ -1157,7 +1176,9 @@ teco_state_changedir_done(teco_machine_main_t *ctx, const teco_string_t *str, GE
* String-building characters are enabled on this
* command and directories can be tab-completed.
*/
-TECO_DEFINE_STATE_EXPECTDIR(teco_state_changedir);
+TECO_DEFINE_STATE_EXPECTDIR(teco_state_changedir,
+ .expectstring.done_cb = teco_state_changedir_done
+);
static teco_state_t *
teco_state_condcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
@@ -1185,8 +1206,7 @@ teco_state_condcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **er
teco_error_argexpected_set(error, "\"");
return NULL;
}
- if (!teco_expressions_pop_num_calc(&value, 0, error))
- return NULL;
+ value = teco_expressions_pop_num(0);
break;
default:
@@ -1273,7 +1293,8 @@ teco_state_condcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **er
}
TECO_DEFINE_STATE_COMMAND(teco_state_condcommand,
- .style = SCE_SCITECO_OPERATOR
+ .style = SCE_SCITECO_OPERATOR,
+ .input_cb = (teco_state_input_cb_t)teco_state_condcommand_input
);
/*$ ^_ negate
@@ -1287,8 +1308,6 @@ TECO_DEFINE_STATE_COMMAND(teco_state_condcommand,
static void
teco_state_control_negate(teco_machine_main_t *ctx, GError **error)
{
- teco_int_t v;
-
if (!teco_expressions_eval(FALSE, error))
return;
@@ -1296,9 +1315,8 @@ teco_state_control_negate(teco_machine_main_t *ctx, GError **error)
teco_error_argexpected_set(error, "^_");
return;
}
- if (!teco_expressions_pop_num_calc(&v, 0, error))
- return;
- teco_expressions_push(~v);
+
+ teco_expressions_push(~teco_expressions_pop_num(0));
}
static void
@@ -1319,35 +1337,6 @@ teco_state_control_xor(teco_machine_main_t *ctx, GError **error)
teco_expressions_push_calc(TECO_OP_XOR, error);
}
-/*$ ^C exit
- * ^C -- Exit program immediately
- *
- * Lets the top-level macro return immediately
- * regardless of the current macro invocation frame.
- * This command is only allowed in batch mode,
- * so it is not invoked accidentally when using
- * the CTRL+C immediate editing command to
- * interrupt long running operations.
- * When using \fB^C\fP in a munged file,
- * interactive mode is never started, so it behaves
- * effectively just like \(lq-EX\fB$$\fP\(rq
- * (when executed in the top-level macro at least).
- *
- * The \fBquit\fP hook is still executed.
- */
-static void
-teco_state_control_exit(teco_machine_main_t *ctx, GError **error)
-{
- if (teco_undo_enabled) {
- g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
- "<^C> not allowed in interactive mode");
- return;
- }
-
- teco_quit_requested = TRUE;
- g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, "");
-}
-
/*$ ^O octal
* ^O -- Set radix to 8 (octal)
*/
@@ -1401,14 +1390,13 @@ teco_state_control_radix(teco_machine_main_t *ctx, GError **error)
return;
teco_expressions_push(radix);
} else {
- if (!teco_expressions_pop_num_calc(&radix, 0, error) ||
- !qreg->vtable->undo_set_integer(qreg, error) ||
- !qreg->vtable->set_integer(qreg, radix, error))
+ if (!qreg->vtable->undo_set_integer(qreg, error) ||
+ !qreg->vtable->set_integer(qreg, teco_expressions_pop_num(0), error))
return;
}
}
-/*$ ^E glyphs2bytes bytes2glyphs
+/*$ "^E" ":^E" glyphs2bytes bytes2glyphs
* glyphs^E -> bytes -- Translate between glyph and byte indexes
* bytes:^E -> glyphs
* ^E -> bytes
@@ -1455,9 +1443,7 @@ teco_state_control_glyphs2bytes(teco_machine_main_t *ctx, GError **error)
*/
res = teco_interface_ssm(colon_modified ? SCI_GETLENGTH : SCI_GETCURRENTPOS, 0, 0);
} else {
- teco_int_t pos;
- if (!teco_expressions_pop_num_calc(&pos, 0, error))
- return;
+ teco_int_t pos = teco_expressions_pop_num(0);
if (colon_modified) {
/* teco_interface_bytes2glyphs() does not check addresses */
res = 0 <= pos && pos <= teco_interface_ssm(SCI_GETLENGTH, 0, 0)
@@ -1498,8 +1484,8 @@ teco_ranges_init(void)
* The default value 0 specifies the entire matched pattern,
* while higher numbers refer to \fB^E[\fI...\fB]\fR subpatterns.
* \fB^Y\fP can also be used to return the buffer range of the
- * last text insertion by any \*(ST command (\fBI\fP, \fBEI\fP, \fB^I\fP, \fBG\fIq\fR,
- * \fB\\\fP, \fBEC\fP, \fBEN\fP, etc).
+ * last text insertion by any \*(ST command (\fBI\fP, \fB^I\fP, \fBG\fIq\fR,
+ * \fB\\\fP, \fBEC\fP, \fBEN\fP, search replacements, etc).
* In this case <n> is only allowed to be 0 or missing.
*
* For instance, \(lq^YXq\(rq copies the entire matched pattern or text
@@ -1522,8 +1508,8 @@ teco_state_control_last_range(teco_machine_main_t *ctx, GError **error)
return;
}
- teco_expressions_push(teco_interface_bytes2glyphs(teco_ranges[n].from));
- teco_expressions_push(teco_interface_bytes2glyphs(teco_ranges[n].to));
+ teco_expressions_push(teco_ranges[n].from);
+ teco_expressions_push(teco_ranges[n].to);
}
/*$ ^S
@@ -1551,17 +1537,20 @@ teco_state_control_last_length(teco_machine_main_t *ctx, GError **error)
/*
* There is little use in supporting n^S for n != 0.
* This is just for consistency with ^Y.
+ *
+ * We do not use teco_expressions_pop_num_calc(),
+ * so as not to reset the sign prefix.
*/
- if (teco_expressions_args() > 0 &&
- !teco_expressions_pop_num_calc(&n, 0, error))
+ if (!teco_expressions_eval(FALSE, error))
return;
+ if (teco_expressions_args() > 0)
+ n = teco_expressions_pop_num(0);
if (n < 0 || n >= teco_ranges_count) {
- teco_error_subpattern_set(error, "^Y");
+ teco_error_subpattern_set(error, "^S");
return;
}
- teco_expressions_push(teco_interface_bytes2glyphs(teco_ranges[n].from) -
- teco_interface_bytes2glyphs(teco_ranges[n].to));
+ teco_expressions_push(teco_ranges[n].from - teco_ranges[n].to);
}
static void TECO_DEBUG_CLEANUP
@@ -1570,6 +1559,100 @@ teco_ranges_cleanup(void)
g_free(teco_ranges);
}
+/*$ ^B date
+ * ^B -> (((year-1900)*16 + month)*32 + day) -- Retrieve date
+ *
+ * Returns the current date via the given equation.
+ */
+/*
+ * FIXME: Perhaps :^B should directly return the
+ * decoded year, month and day.
+ */
+static void
+teco_state_control_date(teco_machine_main_t *ctx, GError **error)
+{
+ GDate date;
+
+ g_date_clear(&date, 1);
+ g_date_set_time_t(&date, time(NULL));
+ teco_expressions_push(((g_date_get_year(&date)-1900)*16 + g_date_get_month(&date))*32 +
+ g_date_get_day(&date));
+}
+
+/*$ "^H" ":^H" "::^H" time timestamp
+ * ^H -> seconds since midnight -- Retrieve time of day or timestamp
+ * :^H -> seconds
+ * ::^H -> timestamp
+ *
+ * By default returns the current time in seconds since midnight (UTC).
+ *
+ * If colon-modified it returns the number of <seconds> since the Epoch,
+ * 1970-01-01 00:00:00 +0000 (UTC).
+ *
+ * If modified by two colons it returns the system's monotonic time in microseconds,
+ * which can be used as a <timestamp>.
+ */
+static void
+teco_state_control_time(teco_machine_main_t *ctx, GError **error)
+{
+ switch (teco_machine_main_eval_colon(ctx)) {
+ case 0:
+ teco_expressions_push(time(NULL) % (60*60*24));
+ break;
+ case 1:
+ teco_expressions_push(time(NULL));
+ break;
+ case 2:
+ /*
+ * NOTE: Might not be reliable if TECO_INTEGER==32.
+ */
+ teco_expressions_push(g_get_monotonic_time());
+ break;
+ default:
+ g_assert_not_reached();
+ }
+}
+
+/*$ ^W refresh sleep delay wait
+ * [n]^W -- Wait and refresh screen
+ *
+ * First sleep <n> milliseconds before refreshing the view,
+ * i.e. drawing it.
+ * By default it sleeps for 10ms.
+ * This can be added to loops to make progress visible
+ * in interactive mode.
+ * In batch mode this command is useful as a sleep command.
+ * Sleeps can of course be interrupted with CTRL+C.
+ *
+ * Since CTRL+W is an immediate editing command, you may
+ * have to type this command in upcaret mode.
+ * To enforce a complete screen redraw you can also
+ * press CTRL+L.
+ */
+static void
+teco_state_control_refresh(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t ms;
+
+ if (!teco_expressions_pop_num_calc(&ms, 10, error))
+ return;
+
+ while (ms > 0 && !teco_interface_is_interrupted()) {
+ /*
+ * UNIX' usleep() would also be interrupted by
+ * SIGINT, but polling for interruptions is
+ * probably precise enough.
+ * We need this as a fallback anyway.
+ */
+ g_usleep(MIN(ms*1000, TECO_POLL_INTERVAL));
+ ms -= TECO_POLL_INTERVAL/1000;
+ }
+
+ teco_interface_unfold();
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_refresh(FALSE);
+}
+
static teco_state_t *
teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
{
@@ -1583,6 +1666,9 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
.modifier_at = TRUE, .modifier_colon = 1},
['^'] = {&teco_state_ascii},
['['] = {&teco_state_escape},
+ ['C'] = {&teco_state_ctlc},
+ ['A'] = {&teco_state_print_string,
+ .modifier_at = TRUE, .modifier_colon = 1},
/*
* Additional numeric operations
@@ -1595,7 +1681,9 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
/*
* Commands
*/
- ['C'] = {&teco_state_start, teco_state_control_exit},
+ ['B'] = {&teco_state_start, teco_state_control_date},
+ ['H'] = {&teco_state_start, teco_state_control_time,
+ .modifier_colon = 2},
['O'] = {&teco_state_start, teco_state_control_octal},
['D'] = {&teco_state_start, teco_state_control_decimal},
['R'] = {&teco_state_start, teco_state_control_radix},
@@ -1605,7 +1693,10 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
.modifier_colon = 1},
['X'] = {&teco_state_start, teco_state_control_search_mode},
['Y'] = {&teco_state_start, teco_state_control_last_range},
- ['S'] = {&teco_state_start, teco_state_control_last_length}
+ ['S'] = {&teco_state_start, teco_state_control_last_length},
+ ['T'] = {&teco_state_start, teco_state_control_typeout,
+ .modifier_colon = 1},
+ ['W'] = {&teco_state_start, teco_state_control_refresh}
};
/*
@@ -1617,7 +1708,9 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
teco_ascii_toupper(chr), error);
}
-TECO_DEFINE_STATE_COMMAND(teco_state_control);
+TECO_DEFINE_STATE_COMMAND(teco_state_control,
+ .input_cb = (teco_state_input_cb_t)teco_state_control_input
+);
static teco_state_t *
teco_state_ascii_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
@@ -1640,7 +1733,82 @@ teco_state_ascii_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
* Note that this command can be typed CTRL+Caret or
* Caret-Caret.
*/
-TECO_DEFINE_STATE(teco_state_ascii);
+TECO_DEFINE_STATE(teco_state_ascii,
+ .input_cb = (teco_state_input_cb_t)teco_state_ascii_input
+);
+
+/*$ ^[^[ ^[$ $$ ^C terminate return
+ * [a1,a2,...]$$ -- Terminate command line or return from macro
+ * [a1,a2,...]^[$
+ * [a1,a2,...]^C
+ *
+ * Returns from the current macro invocation.
+ * This will pass control to the calling macro immediately
+ * and is thus faster than letting control reach the macro's end.
+ * Also, direct arguments to \fB$$\fP will be left on the expression
+ * stack when the macro returns.
+ * \fB$$\fP closes loops automatically and is thus safe to call
+ * from loop bodies.
+ * Furthermore, it has defined semantics when executed
+ * from within braced expressions:
+ * All braces opened in the current macro invocation will
+ * be closed and their values discarded.
+ * Only the direct arguments to \fB$$\fP will be kept.
+ *
+ * Returning from the top-level macro in batch mode
+ * will exit the program or start up interactive mode depending
+ * on whether program exit has been requested.
+ * If \fB$$\fP exits the program, any remaining numeric parameter
+ * is returned by the process as its exit status.
+ * By default, the success code is returned.
+ * \(lqEX\fB$$\fP\(rq is thus a common idiom to exit
+ * prematurely.
+ *
+ * In interactive mode, returning from the top-level macro
+ * (i.e. typing \fB$$\fP at the command line) has the
+ * effect of command line termination.
+ * The arguments to \fB$$\fP are currently not used
+ * when terminating a command line \(em the new command line
+ * will always start with a clean expression stack.
+ *
+ * \fB^C\fP cannot be typed directly on the command-line
+ * as it could be inserted accidentally after interrupting
+ * operations with CTRL+C.
+ *
+ * The first \fIescape\fP of \fB$$\fP may be typed either
+ * as an escape character (ASCII 27), in up-arrow mode
+ * (e.g. \fB^[$\fP) or as a dollar character \(em the
+ * second character must be either a real escape character
+ * or a dollar character.
+ */
+/*
+ * FIXME: Analogous to ^C^C, we could support ^[^[ typed with carets only
+ * at the expense of yet another parser state.
+ */
+static teco_state_t *
+teco_return(teco_machine_main_t *ctx, GError **error)
+{
+ g_assert(ctx->flags.mode == TECO_MODE_NORMAL);
+
+ /*
+ * This check is not crucial, but a return command would
+ * terminate the command line and it would be impossible to apply the new
+ * command line with `}` after command-line termination.
+ */
+ if (G_UNLIKELY(ctx == &teco_cmdline.machine &&
+ teco_qreg_current && !teco_string_cmp(teco_qreg_current->head.name, "\e", 1))) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Not allowed to terminate command-line while "
+ "editing command-line replacement register");
+ return NULL;
+ }
+
+ ctx->parent.current = &teco_state_start;
+ if (!teco_expressions_eval(FALSE, error))
+ return NULL;
+ teco_error_return_set(error, teco_expressions_args());
+ return NULL;
+}
/*
* The Escape state is special, as it implements
@@ -1658,52 +1826,9 @@ TECO_DEFINE_STATE(teco_state_ascii);
static teco_state_t *
teco_state_escape_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
{
- /*$ ^[^[ ^[$ $$ terminate return
- * [a1,a2,...]$$ -- Terminate command line or return from macro
- * [a1,a2,...]^[$
- *
- * Returns from the current macro invocation.
- * This will pass control to the calling macro immediately
- * and is thus faster than letting control reach the macro's end.
- * Also, direct arguments to \fB$$\fP will be left on the expression
- * stack when the macro returns.
- * \fB$$\fP closes loops automatically and is thus safe to call
- * from loop bodies.
- * Furthermore, it has defined semantics when executed
- * from within braced expressions:
- * All braces opened in the current macro invocation will
- * be closed and their values discarded.
- * Only the direct arguments to \fB$$\fP will be kept.
- *
- * Returning from the top-level macro in batch mode
- * will exit the program or start up interactive mode depending
- * on whether program exit has been requested.
- * \(lqEX\fB$$\fP\(rq is thus a common idiom to exit
- * prematurely.
- *
- * In interactive mode, returning from the top-level macro
- * (i.e. typing \fB$$\fP at the command line) has the
- * effect of command line termination.
- * The arguments to \fB$$\fP are currently not used
- * when terminating a command line \(em the new command line
- * will always start with a clean expression stack.
- *
- * The first \fIescape\fP of \fB$$\fP may be typed either
- * as an escape character (ASCII 27), in up-arrow mode
- * (e.g. \fB^[$\fP) or as a dollar character \(em the
- * second character must be either a real escape character
- * or a dollar character.
- */
- if (chr == '\e' || chr == '$') {
- if (ctx->flags.mode > TECO_MODE_NORMAL)
- return &teco_state_start;
-
- ctx->parent.current = &teco_state_start;
- if (!teco_expressions_eval(FALSE, error))
- return NULL;
- teco_error_return_set(error, teco_expressions_args());
- return NULL;
- }
+ if (chr == '\e' || chr == '$')
+ return ctx->flags.mode > TECO_MODE_NORMAL
+ ? &teco_state_start : teco_return(ctx, error);
/*
* Alternatives: ^[, <CTRL/[>, <ESC>, $ (dollar)
@@ -1743,14 +1868,100 @@ teco_state_escape_end_of_macro(teco_machine_t *ctx, GError **error)
return teco_expressions_discard_args(error);
}
-TECO_DEFINE_STATE_COMMAND(teco_state_escape,
- .end_of_macro_cb = teco_state_escape_end_of_macro,
- /*
- * The state should behave like teco_state_start
- * when it comes to function key macro masking.
+TECO_DEFINE_STATE_START(teco_state_escape,
+ .input_cb = (teco_state_input_cb_t)teco_state_escape_input,
+ .end_of_macro_cb = teco_state_escape_end_of_macro
+);
+
+/*
+ * Just like ^[, ^C actually implements a lookahead,
+ * so a ^C itself does nothing.
+ * This does not break the user experience since ^C
+ * is disallowed to type at the command-line.
+ */
+static teco_state_t *
+teco_state_ctlc_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+{
+ switch (chr) {
+ case TECO_CTL_KEY('C'): return teco_state_ctlc_control_input(ctx, 'C', error);
+ case '^': return &teco_state_ctlc_control;
+ }
+
+ return ctx->flags.mode > TECO_MODE_NORMAL
+ ? teco_state_start_input(ctx, chr, error) : teco_return(ctx, error);
+}
+
+static gboolean
+teco_state_ctlc_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (G_UNLIKELY(ctx == &teco_cmdline.machine)) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "<^C> is not allowed to terminate command-lines");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+TECO_DEFINE_STATE_START(teco_state_ctlc,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_ctlc_initial,
+ .input_cb = (teco_state_input_cb_t)teco_state_ctlc_input
+);
+
+static teco_state_t *
+teco_state_ctlc_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+{
+ /*$ ^C^C exit
+ * [n]^C^C -- Exit program immediately
+ *
+ * Lets the top-level macro return immediately
+ * regardless of the current macro invocation frame.
+ * This command is only allowed in batch mode,
+ * so it is not invoked accidentally when using
+ * the CTRL+C immediate editing command to
+ * interrupt long running operations.
+ * When using \fB^C^C\fP in a munged file,
+ * interactive mode is never started, so it behaves
+ * effectively just like \(lq-EX\fB$$\fP\(rq
+ * (when executed in the top-level macro at least).
+ *
+ * Any numeric parameter is returned by the process
+ * as its exit status.
+ * By default, the success code is returned.
+ * The \fBquit\fP hook is still executed.
+ *
+ * This command is currently disallowed in interactive mode.
+ *
+ * Note that both \(lq^C\(rq can be typed either
+ * as control codes (3) or with carets.
*/
- .is_start = TRUE,
- .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE
+ if (chr == 'c' || chr == 'C') {
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (teco_undo_enabled) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "<^C^C> not allowed in interactive mode");
+ return NULL;
+ }
+
+ if (!teco_expressions_eval(FALSE, error))
+ return NULL;
+ teco_ed |= TECO_ED_EXIT;
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, "");
+ return NULL;
+ }
+
+ return ctx->flags.mode > TECO_MODE_NORMAL
+ ? teco_state_control_input(ctx, chr, error) : teco_return(ctx, error);
+}
+
+/*
+ * This state is necessary, so that you can type ^C^C exclusively with carets.
+ * Otherwise it would be very cumbersome to cause exits with ASCII characters only.
+ */
+TECO_DEFINE_STATE_COMMAND(teco_state_ctlc_control,
+ .input_cb = (teco_state_input_cb_t)teco_state_ctlc_control_input
);
/*$ ED flags
@@ -1772,7 +1983,12 @@ TECO_DEFINE_STATE_COMMAND(teco_state_escape,
* Without any argument ED returns the current flags.
*
* Currently, the following flags are used by \*(ST:
- * .IP 4: 5
+ * .IP 2: 5
+ * Reflects whether program termination has been requested
+ * by successfully performing the \fBEX\fP command.
+ * This flag can also be used to cancel the effect of any
+ * prior \fBEX\fP.
+ * .IP 4:
* If enabled, prefer raw single-byte ANSI encoding
* for all new buffers and registers.
* This does not change the encoding of any existing
@@ -1816,6 +2032,13 @@ TECO_DEFINE_STATE_COMMAND(teco_state_escape,
* by the \(lqNerd Fonts\(rq project.
* Changes to this flag in interactive mode may not become
* effective immediately.
+ * .IP 1024:
+ * If set the default clipboard register \(lq~\(rq will refer
+ * to the primary clipboard (\(lq~P\(rq) instead of the
+ * clipboard selection (\(lq~C\(rq).
+ * .IP 2048:
+ * Enable/Disable redirection of Scintilla messages (\fBES\fP)
+ * to the command line's Scintilla view.
*
* The features controlled thus are discribed in other sections
* of this manual.
@@ -1862,7 +2085,7 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error)
* The current user interface: 1 for Curses, 2 for GTK
* (\fBread-only\fP)
* .IP 1:
- * The current numbfer of buffers: Also the numeric id
+ * The current number of buffers: Also the numeric id
* of the last buffer in the ring. This is implied if
* no argument is given, so \(lqEJ\(rq returns the number
* of buffers in the ring.
@@ -1938,20 +2161,35 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error)
* The column after the last horizontal movement.
* This is only used by \fBfnkeys.tes\fP and is similar to the Scintilla-internal
* setting \fBSCI_CHOOSECARETX\fP.
- * Unless most other settings, this is on purpose not restored on rubout,
- * so it "survives" command line replacements.
+ * Unlike most other settings, this is on purpose not restored on rubout,
+ * so it \(lqsurvives\(rq command line replacements.
+ * .IP 5:
+ * Height of the command line view in lines (1 by default).
+ * Must not be smaller than 1.
+ * .IP 6:
+ * .SCITECO_TOPIC recovery
+ * Interval in seconds for the creation of recovery files
+ * or 0 if those dumps are disabled (the default is 300 seconds).
+ * When enabled all dirty buffers are dumped to files with hash
+ * signs around the original basename (\fB#\fIfilename\fB#\fR).
+ * They are removed automatically when no longer required,
+ * but may be left around when the \*(ST crashes or terminates
+ * unexpectedly.
+ * After changing the interval, the new value may become
+ * active only after the previous interval expires.
+ * Recovery files are not dumped in batch mode.
* .
* .IP -1:
* Type of the last mouse event (\fBread-only\fP).
* One of the following values will be returned:
* .RS
- * . IP 1: 4
+ * . IP 0: 4
* Some button has been pressed
- * . IP 2:
+ * . IP 1:
* Some button has been released
- * . IP 3:
+ * . IP 2:
* Scroll up
- * . IP 4:
+ * . IP 3:
* Scroll down
* .RE
* .IP -2:
@@ -1992,23 +2230,22 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error)
EJ_BUFFERS,
EJ_MEMORY_LIMIT,
EJ_INIT_COLOR,
- EJ_CARETX
+ EJ_CARETX,
+ EJ_CMDLINE_HEIGHT,
+ EJ_RECOVERY_INTERVAL
};
static teco_int_t caret_x = 0;
teco_int_t property;
- if (!teco_expressions_eval(FALSE, error) ||
- !teco_expressions_pop_num_calc(&property, teco_num_sign, error))
+ if (!teco_expressions_pop_num_calc(&property, teco_num_sign, error))
return;
if (teco_expressions_args() > 0) {
/*
* Set property
*/
- teco_int_t value, color;
- if (!teco_expressions_pop_num_calc(&value, 0, error))
- return;
+ teco_int_t value = teco_expressions_pop_num(0);
switch (property) {
case EJ_MEMORY_LIMIT:
@@ -2027,15 +2264,36 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error)
teco_error_argexpected_set(error, "EJ");
return;
}
- if (!teco_expressions_pop_num_calc(&color, 0, error))
- return;
- teco_interface_init_color((guint)value, (guint32)color);
+ teco_interface_init_color((guint)value,
+ (guint32)teco_expressions_pop_num(0));
break;
case EJ_CARETX:
+ /* DON'T undo on rubout */
caret_x = value;
break;
+ case EJ_CMDLINE_HEIGHT:
+ if (value < 1 || value > G_MAXUINT) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid command line height %" TECO_INT_FORMAT " "
+ "for <EJ>", value);
+ return;
+ }
+ teco_undo_guint(teco_cmdline.height) = value;
+ break;
+
+ case EJ_RECOVERY_INTERVAL:
+ if (value < 0 || value > G_MAXUINT) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid recovery interval %" TECO_INT_FORMAT "s "
+ "for <EJ>", value);
+ return;
+ }
+ teco_undo_guint(teco_ring_recovery_interval) = value;
+ /* FIXME: Perhaps signal the interface to reprogram timers */
+ break;
+
default:
g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
"Cannot set property %" TECO_INT_FORMAT " "
@@ -2091,6 +2349,14 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error)
teco_expressions_push(caret_x);
break;
+ case EJ_CMDLINE_HEIGHT:
+ teco_expressions_push(teco_cmdline.height);
+ break;
+
+ case EJ_RECOVERY_INTERVAL:
+ teco_expressions_push(teco_ring_recovery_interval);
+ break;
+
default:
g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
"Invalid property %" TECO_INT_FORMAT " "
@@ -2099,7 +2365,7 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error)
}
}
-/*$ EL eol
+/*$ "EL" ":EL" EOL
* 0EL -- Set or get End of Line mode
* 13,10:EL
* 1EL
@@ -2150,11 +2416,7 @@ teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error)
teco_int_t eol_mode;
if (teco_machine_main_eval_colon(ctx) > 0) {
- teco_int_t v1, v2;
- if (!teco_expressions_pop_num_calc(&v1, 0, error))
- return;
-
- switch (v1) {
+ switch (teco_expressions_pop_num(0)) {
case '\r':
eol_mode = SC_EOL_CR;
break;
@@ -2163,9 +2425,7 @@ teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error)
eol_mode = SC_EOL_LF;
break;
}
- if (!teco_expressions_pop_num_calc(&v2, 0, error))
- return;
- if (v2 == '\r') {
+ if (teco_expressions_pop_num(0) == '\r') {
eol_mode = SC_EOL_CRLF;
break;
}
@@ -2176,8 +2436,7 @@ teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error)
return;
}
} else {
- if (!teco_expressions_pop_num_calc(&eol_mode, 0, error))
- return;
+ eol_mode = teco_expressions_pop_num(0);
switch (eol_mode) {
case SC_EOL_CRLF:
case SC_EOL_CR:
@@ -2195,6 +2454,13 @@ teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error)
undo__teco_interface_ssm(SCI_SETEOLMODE,
teco_interface_ssm(SCI_GETEOLMODE, 0, 0), 0);
teco_interface_ssm(SCI_SETEOLMODE, eol_mode, 0);
+
+ /*
+ * While the buffer contents were not changed,
+ * the result of saving the file may differ,
+ * so we still dirtify the buffer.
+ */
+ teco_ring_dirtify();
} else if (teco_machine_main_eval_colon(ctx) > 0) {
const gchar *eol_seq = teco_eol_get_seq(teco_interface_ssm(SCI_GETEOLMODE, 0, 0));
teco_expressions_push(eol_seq);
@@ -2255,7 +2521,7 @@ teco_codepage2str(guint codepage)
return NULL;
}
-/*$ EE encoding codepage charset
+/*$ "EE" ":EE" encoding codepage charset
* codepageEE -- Edit current document's encoding (codepage/charset)
* EE -> codepage
* codepage:EE
@@ -2319,10 +2585,7 @@ teco_state_ecommand_encoding(teco_machine_main_t *ctx, GError **error)
/*
* Set code page
*/
- teco_int_t new_cp;
- if (!teco_expressions_pop_num_calc(&new_cp, 0, error))
- return;
-
+ teco_int_t new_cp = teco_expressions_pop_num(0);
if (old_cp == SC_CP_UTF8 && new_cp == SC_CP_UTF8)
return;
@@ -2449,7 +2712,26 @@ teco_state_ecommand_encoding(teco_machine_main_t *ctx, GError **error)
teco_interface_ssm(SCI_GOTOPOS, teco_interface_glyphs2bytes(dot_glyphs), 0);
}
-/*$ EX exit
+/*$ EO version
+ * EO -> major*10000 + minor*100 + micro -- Get program version
+ *
+ * Return the version of \*(ST encoded into an integer.
+ */
+static void
+teco_state_ecommand_version(teco_machine_main_t *ctx, GError **error)
+{
+ /*
+ * FIXME: This is inefficient and could be done at build-time.
+ * Or we could have PACKAGE_MAJOR_VERSION, PACKAGE_MINOR_VERSION etc. macros.
+ * But then, who cares?
+ */
+ guint major, minor, micro;
+ G_GNUC_UNUSED gint rc = sscanf(PACKAGE_VERSION, "%u.%u.%u", &major, &minor, &micro);
+ g_assert(rc == 3);
+ teco_expressions_push(major*10000 + minor*100 + micro);
+}
+
+/*$ "EX" ":EX" exit quit
* [bool]EX -- Exit program
* -EX
* :EX
@@ -2486,6 +2768,10 @@ teco_state_ecommand_encoding(teco_machine_main_t *ctx, GError **error)
* \(lq:EX\fB$$\fP\(rq is nevertheless the usual interactive
* command sequence to exit while saving all modified
* buffers.
+ *
+ * The program termination request is also available in bit 2
+ * of the \fBED\fP flags, so \(lqED&2\(rq can be used to
+ * check whether EX has been successfully called.
*/
/** @fixme what if changing file after EX? will currently still exit */
static void
@@ -2498,14 +2784,22 @@ teco_state_ecommand_exit(teco_machine_main_t *ctx, GError **error)
teco_int_t v;
if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
return;
- if (teco_is_failure(v) && teco_ring_is_any_dirty()) {
- g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
- "Modified buffers exist");
+ guint id;
+ if (teco_is_failure(v) && (id = teco_ring_get_first_dirty())) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Buffer with id %u is dirty", id);
return;
}
}
- teco_undo_gboolean(teco_quit_requested) = TRUE;
+ teco_undo_int(teco_ed) |= TECO_ED_EXIT;
+}
+
+static void
+teco_state_macrofile_deprecated(teco_machine_main_t *ctx, GError **error)
+{
+ teco_interface_msg(TECO_MSG_WARNING,
+ "<EM> command is deprecated - use <EI> instead");
}
static teco_state_t *
@@ -2523,9 +2817,10 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error
.modifier_at = TRUE, .modifier_colon = 1},
['G'] = {&teco_state_egcommand,
.modifier_at = TRUE, .modifier_colon = 1},
- ['I'] = {&teco_state_insert_nobuilding,
- .modifier_at = TRUE},
- ['M'] = {&teco_state_macrofile,
+ ['I'] = {&teco_state_indirect,
+ .modifier_at = TRUE, .modifier_colon = 1},
+ /* DEPRECATED: can be repurposed */
+ ['M'] = {&teco_state_indirect, teco_state_macrofile_deprecated,
.modifier_at = TRUE, .modifier_colon = 1},
['N'] = {&teco_state_glob_pattern,
.modifier_at = TRUE, .modifier_colon = 1},
@@ -2537,6 +2832,8 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error
.modifier_at = TRUE, .modifier_colon = 1},
['W'] = {&teco_state_save_file,
.modifier_at = TRUE},
+ ['R'] = {&teco_state_read_file,
+ .modifier_at = TRUE},
/*
* Commands
@@ -2549,6 +2846,7 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error
.modifier_colon = 1},
['E'] = {&teco_state_start, teco_state_ecommand_encoding,
.modifier_colon = 1},
+ ['O'] = {&teco_state_start, teco_state_ecommand_version},
['X'] = {&teco_state_start, teco_state_ecommand_exit,
.modifier_colon = 1},
};
@@ -2560,7 +2858,9 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error
teco_ascii_toupper(chr), error);
}
-TECO_DEFINE_STATE_COMMAND(teco_state_ecommand);
+TECO_DEFINE_STATE_COMMAND(teco_state_ecommand,
+ .input_cb = (teco_state_input_cb_t)teco_state_ecommand_input
+);
gboolean
teco_state_insert_initial(teco_machine_main_t *ctx, GError **error)
@@ -2568,7 +2868,8 @@ teco_state_insert_initial(teco_machine_main_t *ctx, GError **error)
if (ctx->flags.mode > TECO_MODE_NORMAL)
return TRUE;
- teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos);
teco_undo_guint(teco_ranges_count) = 1;
/*
@@ -2622,22 +2923,21 @@ teco_state_insert_initial(teco_machine_main_t *ctx, GError **error)
undo__teco_interface_ssm(SCI_UNDO, 0, 0);
/* This is done only now because it can _theoretically_ fail. */
- for (gint i = args; i > 0; i--)
- if (!teco_expressions_pop_num_calc(NULL, 0, error))
- return FALSE;
+ for (gint i = 0; i < args; i++)
+ teco_expressions_pop_num(0);
return TRUE;
}
gboolean
-teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str,
+teco_state_insert_process(teco_machine_main_t *ctx, teco_string_t str,
gsize new_chars, GError **error)
{
g_assert(new_chars > 0);
teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
teco_interface_ssm(SCI_ADDTEXT, new_chars,
- (sptr_t)(str->data + str->len - new_chars));
+ (sptr_t)(str.data + str.len - new_chars));
teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
teco_ring_dirtify();
@@ -2648,11 +2948,13 @@ teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str,
}
teco_state_t *
-teco_state_insert_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_insert_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
- if (ctx->flags.mode == TECO_MODE_NORMAL)
- teco_undo_gsize(teco_ranges[0].to) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos);
return &teco_state_start;
}
@@ -2677,21 +2979,7 @@ teco_state_insert_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro
* may be better, since it has string building characters
* disabled.
*/
-TECO_DEFINE_STATE_INSERT(teco_state_insert_building);
-
-/*$ EI
- * [c1,c2,...]EI[text]$ -- Insert text without string building characters
- *
- * Inserts text at the current position in the current
- * document.
- * This command is identical to the \fBI\fP command,
- * except that string building characters are \fBdisabled\fP.
- * Therefore it may be beneficial when editing \*(ST
- * macros.
- */
-TECO_DEFINE_STATE_INSERT(teco_state_insert_nobuilding,
- .expectstring.string_building = FALSE
-);
+TECO_DEFINE_STATE_INSERT(teco_state_insert);
static gboolean
teco_state_insert_indent_initial(teco_machine_main_t *ctx, GError **error)
diff --git a/src/core-commands.h b/src/core-commands.h
index 523ba28..254c4a7 100644
--- a/src/core-commands.h
+++ b/src/core-commands.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -22,40 +22,65 @@
#include "parser.h"
#include "string-utils.h"
-/** non-operational characters in teco_state_start */
-#define TECO_NOOPS " \f\r\n\v"
+/** Check whether c is a non-operational command in teco_state_start */
+static inline gboolean
+teco_is_noop(gunichar c)
+{
+ return c == ' ' || c == '\f' || c == '\r' || c == '\n' || c == '\v';
+}
+
+gboolean teco_get_range_args(const gchar *cmd, gsize *from_ret, gsize *len_ret, GError **error);
+
+/* in cmdline.c */
+gboolean teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
+ gunichar key, GError **error);
+
+/**
+ * @class TECO_DEFINE_STATE_COMMAND
+ * @implements TECO_DEFINE_STATE_CASEINSENSITIVE
+ * @ingroup states
+ *
+ * Base state for everything where part of a one or two letter command
+ * is accepted.
+ */
+#define TECO_DEFINE_STATE_COMMAND(NAME, ...) \
+ TECO_DEFINE_STATE_CASEINSENSITIVE(NAME, \
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
+ teco_state_command_process_edit_cmd, \
+ .style = SCE_SCITECO_COMMAND, \
+ ##__VA_ARGS__ \
+ )
/*
* FIXME: Most of these states can probably be private/static
* as they are only referenced from teco_state_start.
*/
-TECO_DECLARE_STATE(teco_state_start);
-TECO_DECLARE_STATE(teco_state_fcommand);
+extern teco_state_t teco_state_fcommand;
void teco_undo_change_dir_to_current(void);
-TECO_DECLARE_STATE(teco_state_changedir);
+extern teco_state_t teco_state_changedir;
-TECO_DECLARE_STATE(teco_state_condcommand);
-TECO_DECLARE_STATE(teco_state_control);
-TECO_DECLARE_STATE(teco_state_ascii);
-TECO_DECLARE_STATE(teco_state_escape);
-TECO_DECLARE_STATE(teco_state_ecommand);
+extern teco_state_t teco_state_condcommand;
+extern teco_state_t teco_state_control;
+extern teco_state_t teco_state_ascii;
+extern teco_state_t teco_state_ecommand;
typedef struct {
- gsize from; /*< start position in bytes */
- gsize to; /*< end position in bytes */
+ teco_int_t from; /*< start position in glyphs */
+ teco_int_t to; /*< end position in glyphs */
} teco_range_t;
extern guint teco_ranges_count;
extern teco_range_t *teco_ranges;
gboolean teco_state_insert_initial(teco_machine_main_t *ctx, GError **error);
-gboolean teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str,
+gboolean teco_state_insert_process(teco_machine_main_t *ctx, teco_string_t str,
gsize new_chars, GError **error);
-teco_state_t *teco_state_insert_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error);
+teco_state_t *teco_state_insert_done(teco_machine_main_t *ctx, teco_string_t str, GError **error);
/* in cmdline.c */
-gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar chr, GError **error);
+gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
+ gunichar chr, GError **error);
/**
* @class TECO_DEFINE_STATE_INSERT
@@ -63,21 +88,39 @@ gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machi
* @ingroup states
*
* @note Also serves as a base class of the replace-insertion commands.
- * @fixme Generating the done_cb could be avoided if there simply were a default.
*/
#define TECO_DEFINE_STATE_INSERT(NAME, ...) \
- static teco_state_t * \
- NAME##_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) \
- { \
- return teco_state_insert_done(ctx, str, error); \
- } \
TECO_DEFINE_STATE_EXPECTSTRING(NAME, \
.initial_cb = (teco_state_initial_cb_t)teco_state_insert_initial, \
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_insert_process_edit_cmd, \
.expectstring.process_cb = teco_state_insert_process, \
+ .expectstring.done_cb = teco_state_insert_done, \
##__VA_ARGS__ \
)
-TECO_DECLARE_STATE(teco_state_insert_building);
-TECO_DECLARE_STATE(teco_state_insert_nobuilding);
-TECO_DECLARE_STATE(teco_state_insert_indent);
+extern teco_state_t teco_state_insert;
+extern teco_state_t teco_state_insert_indent;
+
+/**
+ * @class TECO_DEFINE_STATE_START
+ * @implements TECO_DEFINE_STATE_COMMAND
+ * @ingroup states
+ *
+ * Base state for everything where a new command can begin
+ * (the start state itself and all lookahead states).
+ */
+#define TECO_DEFINE_STATE_START(NAME, ...) \
+ TECO_DEFINE_STATE_COMMAND(NAME, \
+ .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */ \
+ .is_start = TRUE, \
+ .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE, \
+ ##__VA_ARGS__ \
+ )
+
+teco_state_t *teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error);
+
+extern teco_state_t teco_state_start;
+extern teco_state_t teco_state_control;
+extern teco_state_t teco_state_escape;
+extern teco_state_t teco_state_ctlc;
+extern teco_state_t teco_state_ctlc_control;
diff --git a/src/doc.c b/src/doc.c
index 4ac96c8..9bc9dc8 100644
--- a/src/doc.c
+++ b/src/doc.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -205,7 +205,8 @@ teco_doc_get_string(teco_doc_t *ctx, gchar **str, gsize *outlen, guint *codepage
gsize len = teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0);
if (str) {
*str = g_malloc(len + 1);
- teco_view_ssm(teco_qreg_view, SCI_GETTEXT, len + 1, (sptr_t)*str);
+ /* null-terminates the string */
+ teco_view_ssm(teco_qreg_view, SCI_GETTEXT, len, (sptr_t)*str);
}
if (outlen)
*outlen = len;
diff --git a/src/doc.h b/src/doc.h
index 4ecfd21..3218a70 100644
--- a/src/doc.h
+++ b/src/doc.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -33,6 +33,8 @@
*/
typedef struct teco_doc_scintilla_t teco_doc_scintilla_t;
+TECO_DECLARE_UNDO_OBJECT(doc_scintilla, teco_doc_scintilla_t *);
+
/**
* A Scintilla document.
*
@@ -108,7 +110,7 @@ void teco_doc_exchange(teco_doc_t *ctx, teco_doc_t *other);
static inline void
teco_doc_undo_exchange(teco_doc_t *ctx)
{
- teco_undo_ptr(ctx->doc);
+ teco_undo_object_doc_scintilla_push(&ctx->doc);
teco_doc_undo_reset(ctx);
}
diff --git a/src/eol.c b/src/eol.c
index e5cab2c..201e949 100644
--- a/src/eol.c
+++ b/src/eol.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/eol.h b/src/eol.h
index 0bcb28c..be5a4b1 100644
--- a/src/eol.h
+++ b/src/eol.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/error.c b/src/error.c
index 6326984..716b60b 100644
--- a/src/error.c
+++ b/src/error.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -79,7 +79,7 @@ teco_error_display_short(const GError *error)
void
teco_error_display_full(const GError *error)
{
- teco_interface_msg(TECO_MSG_ERROR, "%s", error->message);
+ teco_interface_msg_literal(TECO_MSG_ERROR, error->message, strlen(error->message));
guint nr = 0;
diff --git a/src/error.h b/src/error.h
index 61bcce6..67de4aa 100644
--- a/src/error.h
+++ b/src/error.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -42,11 +42,13 @@ typedef enum {
TECO_ERROR_SYNTAX,
TECO_ERROR_MODIFIER,
TECO_ERROR_ARGEXPECTED,
+ TECO_ERROR_LABEL,
TECO_ERROR_CODEPOINT,
TECO_ERROR_MOVE,
TECO_ERROR_WORDS,
TECO_ERROR_RANGE,
TECO_ERROR_SUBPATTERN,
+ TECO_ERROR_INVALIDBUF,
TECO_ERROR_INVALIDQREG,
TECO_ERROR_QREGOPUNSUPPORTED,
TECO_ERROR_QREGCONTAINSNULL,
@@ -91,10 +93,18 @@ teco_error_argexpected_set(GError **error, const gchar *cmd)
}
static inline void
+teco_error_label_set(GError **error, const gchar *name, gsize len)
+{
+ g_autofree gchar *label_printable = teco_string_echo(name, len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_LABEL,
+ "Label \"%s\" not found", label_printable);
+}
+
+static inline void
teco_error_codepoint_set(GError **error, const gchar *cmd)
{
g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
- "Invalid Unicode codepoint for <%s>", cmd);
+ "Invalid codepoint for <%s>", cmd);
}
static inline void
@@ -126,6 +136,13 @@ teco_error_subpattern_set(GError **error, const gchar *cmd)
}
static inline void
+teco_error_invalidbuf_set(GError **error, teco_int_t id)
+{
+ g_set_error(error, TECO_ERROR, TECO_ERROR_INVALIDBUF,
+ "Invalid buffer id %" TECO_INT_FORMAT, id);
+}
+
+static inline void
teco_error_invalidqreg_set(GError **error, const gchar *name, gsize len, gboolean local)
{
g_autofree gchar *name_printable = teco_string_echo(name, len);
diff --git a/src/expressions.c b/src/expressions.c
index f802c6e..9561c46 100644
--- a/src/expressions.c
+++ b/src/expressions.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -76,12 +76,22 @@ teco_expressions_push_int(teco_int_t number)
undo__remove_index__teco_numbers(teco_numbers->len-1);
}
+/** Peek into the numbers stack */
teco_int_t
teco_expressions_peek_num(guint index)
{
return g_array_index(teco_numbers, teco_int_t, teco_numbers->len - 1 - index);
}
+/**
+ * Pop a value from the number stack.
+ *
+ * This must only be called if you are sure that the number at the
+ * given index exists and is an argument, i.e. only after calling
+ * teco_expressions_eval() and teco_expressions_args().
+ * If you are unsure or want to imply a value,
+ * use teco_expressions_pop_num_calc() instead.
+ */
teco_int_t
teco_expressions_pop_num(guint index)
{
@@ -99,6 +109,17 @@ teco_expressions_pop_num(guint index)
return n;
}
+/**
+ * Pop an argument from the number stack.
+ *
+ * This resolves operations and allows implying values
+ * if there isn't any argument on the stack.
+ *
+ * @param ret Where to store the number
+ * @param imply The fallback value if there is no argument
+ * @param error A GError
+ * @return FALSE if an error occurred
+ */
gboolean
teco_expressions_pop_num_calc(teco_int_t *ret, teco_int_t imply, GError **error)
{
@@ -263,6 +284,13 @@ teco_expressions_calc(GError **error)
return TRUE;
}
+/**
+ * Resolve all operations on the top of the stack.
+ *
+ * @param pop_brace If TRUE this also pops the "brace" operator.
+ * @param error A GError
+ * @return FALSE if an error occurred
+ */
gboolean
teco_expressions_eval(gboolean pop_brace, GError **error)
{
@@ -287,6 +315,14 @@ teco_expressions_eval(gboolean pop_brace, GError **error)
return TRUE;
}
+/**
+ * Get number of numeric arguments on the top of the stack.
+ *
+ * @fixme You must call teco_expressions_eval() to resolve operations
+ * before this gives sensitive results.
+ * Overall it might be better to automatically call teco_expressions_eval()
+ * here or introduce a separate teco_expressions_args_calc().
+ */
guint
teco_expressions_args(void)
{
@@ -387,7 +423,7 @@ teco_expressions_clear(void)
* @param buffer The output buffer of at least TECO_EXPRESSIONS_FORMAT_LEN characters.
* The output string will be null-terminated.
* @param number The number to format.
- * @param table The local Q-Register table that contains the appropriate radix register (^R).
+ * @param qreg The radix register (^R).
* @return A pointer into buffer to the beginning of the formatted number.
*/
gchar *
diff --git a/src/expressions.h b/src/expressions.h
index 631c867..3ef0faf 100644
--- a/src/expressions.h
+++ b/src/expressions.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/file-utils.c b/src/file-utils.c
index 75bcb48..7c37b27 100644
--- a/src/file-utils.c
+++ b/src/file-utils.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -109,6 +109,14 @@ teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs)
#ifdef G_OS_UNIX
+/*
+ * NOTE: This version does not resolve symlinks to non-existing paths.
+ * It could be improved by repeating readlink() and g_canonicalize_filename(),
+ * but it would require glib v2.58.0.
+ * Alternatively we could also iteratively resolve all path components.
+ * Currently, we simply do not rely on successful canonicalization of
+ * yet non-existing paths.
+ */
gchar *
teco_file_get_absolute_path(const gchar *path)
{
@@ -137,13 +145,12 @@ teco_file_is_visible(const gchar *path)
#if GLIB_CHECK_VERSION(2,58,0)
/*
- * FIXME: This should perhaps be preferred on any platform.
- * But it will complicate preprocessing.
+ * NOTE: Does not resolve symlinks.
*/
gchar *
teco_file_get_absolute_path(const gchar *path)
{
- return g_canonicalize_filename(path, NULL);
+ return path ? g_canonicalize_filename(path, NULL) : NULL;
}
#else /* !GLIB_CHECK_VERSION(2,58,0) */
@@ -353,7 +360,7 @@ teco_file_expand_path(const gchar *path)
*/
g_auto(teco_string_t) home = {NULL, 0};
if (!qreg->vtable->get_string(qreg, &home.data, &home.len, NULL, NULL) ||
- teco_string_contains(&home, '\0'))
+ teco_string_contains(home, '\0'))
return g_strdup(path);
g_assert(home.data != NULL);
@@ -419,7 +426,7 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_
while ((cur_basename.data = (gchar *)g_dir_read_name(dir))) {
cur_basename.len = strlen(cur_basename.data);
- if (string_diff(&cur_basename, basename, basename_len) != basename_len)
+ if (string_diff(cur_basename, basename, basename_len) != basename_len)
/* basename is not a prefix of cur_basename */
continue;
@@ -453,7 +460,7 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_
other_file.data = (gchar *)g_slist_next(files)->data + filename_len;
other_file.len = strlen(other_file.data);
- gsize len = string_diff(&other_file, cur_filename + filename_len,
+ gsize len = string_diff(other_file, cur_filename + filename_len,
strlen(cur_filename) - filename_len);
if (len < prefix_len)
prefix_len = len;
diff --git a/src/file-utils.h b/src/file-utils.h
index 12a9b83..11d6650 100644
--- a/src/file-utils.h
+++ b/src/file-utils.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -32,9 +32,7 @@ void teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attr
/**
* Get absolute/full version of a possibly relative path.
* The path is tried to be canonicalized so it does
- * not contain relative components.
- * Works with existing and non-existing paths (in the latter case,
- * heuristics may be applied).
+ * not contain relative components and symlinks.
* Depending on platform and existence of the path,
* canonicalization might fail, but the path returned is
* always absolute.
diff --git a/src/glob.c b/src/glob.c
index 5e013e4..d15f601 100644
--- a/src/glob.c
+++ b/src/glob.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -38,10 +38,7 @@
#include "undo.h"
#include "glob.h"
-/*
- * FIXME: This state could be static.
- */
-TECO_DECLARE_STATE(teco_state_glob_filename);
+static teco_state_t teco_state_glob_filename;
/** @memberof teco_globber_t */
void
@@ -308,13 +305,13 @@ teco_globber_compile_pattern(const gchar *pattern)
*/
static teco_state_t *
-teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_glob_pattern_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_glob_filename;
- if (str->len > 0) {
- g_autofree gchar *filename = teco_file_expand_path(str->data);
+ if (str.len > 0) {
+ g_autofree gchar *filename = teco_file_expand_path(str.data);
teco_qreg_t *glob_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
g_assert(glob_reg != NULL);
@@ -327,7 +324,7 @@ teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str,
return &teco_state_glob_filename;
}
-/*$ EN glob
+/*$ "EN" ":EN" glob
* [type]EN[pattern]$[filename]$ -- Glob files or match filename and check file type
* [type]:EN[pattern]$[filename]$ -> Success|Failure
*
@@ -373,7 +370,7 @@ teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str,
* \fIfilename\fP does not necessarily have to exist in the
* file system for the match to succeed (unless a file type check
* is also specified).
- * For instance, \(lqENf??/\[**].c\fB$\fPfoo/bar.c\fB$\fP\(rq will
+ * For instance, \(lqENf??\[sl]*.c\fB$\fPfoo/bar.c\fB$\fP\(rq will
* always match and the string \(lqfoo/bar.c\(rq will be inserted
* (see below).
*
@@ -454,11 +451,12 @@ teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str,
* have to edit that register anyway.
*/
TECO_DEFINE_STATE_EXPECTGLOB(teco_state_glob_pattern,
- .expectstring.last = FALSE
+ .expectstring.last = FALSE,
+ .expectstring.done_cb = teco_state_glob_pattern_done
);
static teco_state_t *
-teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_glob_filename_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
@@ -470,8 +468,7 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str
teco_int_t teco_test_mode;
- if (!teco_expressions_eval(FALSE, error) ||
- !teco_expressions_pop_num_calc(&teco_test_mode, 0, error))
+ if (!teco_expressions_pop_num_calc(&teco_test_mode, 0, error))
return NULL;
switch (teco_test_mode) {
/*
@@ -498,35 +495,36 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str
if (!glob_reg->vtable->get_string(glob_reg, &pattern_str.data, &pattern_str.len,
NULL, error))
return NULL;
- if (teco_string_contains(&pattern_str, '\0')) {
+ if (teco_string_contains(pattern_str, '\0')) {
teco_error_qregcontainsnull_set(error, "_", 1, FALSE);
return NULL;
}
- if (str->len > 0) {
+ if (str.len > 0) {
/*
* Match pattern against provided file name
*/
- g_autofree gchar *filename = teco_file_expand_path(str->data);
+ g_autofree gchar *filename = teco_file_expand_path(str.data);
g_autoptr(GRegex) pattern = teco_globber_compile_pattern(pattern_str.data);
if (g_regex_match(pattern, filename, 0, NULL) &&
(teco_test_mode == 0 || g_file_test(filename, file_flags))) {
if (!colon_modified) {
- gsize len = strlen(filename);
-
- teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
- teco_undo_gsize(teco_ranges[0].to) = teco_ranges[0].from + len + 1;
- teco_undo_guint(teco_ranges_count) = 1;
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
/*
* FIXME: Filenames may contain linefeeds.
* But if we add them null-terminated, they will be relatively hard to parse.
*/
+ gsize len = strlen(filename);
filename[len] = '\n';
teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
teco_interface_ssm(SCI_ADDTEXT, len+1, (sptr_t)filename);
teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+
+ teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos);
+ teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos + len + 1);
+ teco_undo_guint(teco_ranges_count) = 1;
}
matching = TRUE;
@@ -550,16 +548,15 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str
g_auto(teco_globber_t) globber;
teco_globber_init(&globber, pattern_str.data, file_flags);
- teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
- teco_undo_gsize(teco_ranges[0].to) = teco_ranges[0].from;
- teco_undo_guint(teco_ranges_count) = 1;
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos);
teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
gchar *globbed_filename;
while ((globbed_filename = teco_globber_next(&globber))) {
gsize len = strlen(globbed_filename);
- teco_ranges[0].to += len+1;
+ pos += len+1;
/*
* FIXME: Filenames may contain linefeeds.
@@ -573,6 +570,9 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str
}
teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+
+ teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos);
+ teco_undo_guint(teco_ranges_count) = 1;
}
if (colon_modified) {
@@ -591,4 +591,6 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str
return &teco_state_start;
}
-TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_filename);
+static TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_filename,
+ .expectstring.done_cb = teco_state_glob_filename_done
+);
diff --git a/src/glob.h b/src/glob.h
index d11fbce..995437c 100644
--- a/src/glob.h
+++ b/src/glob.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -47,8 +47,9 @@ gchar *teco_globber_escape_pattern(const gchar *pattern);
GRegex *teco_globber_compile_pattern(const gchar *pattern);
/* in cmdline.c */
-gboolean teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error);
-gboolean teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error);
+gboolean teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
+ gunichar key, GError **error);
+gboolean teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error);
/**
* @interface TECO_DEFINE_STATE_EXPECTGLOB
@@ -68,4 +69,4 @@ gboolean teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, const
* Command states
*/
-TECO_DECLARE_STATE(teco_state_glob_pattern);
+extern teco_state_t teco_state_glob_pattern;
diff --git a/src/goto-commands.c b/src/goto-commands.c
index a0e6634..a9ff3c2 100644
--- a/src/goto-commands.c
+++ b/src/goto-commands.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -24,6 +24,7 @@
#include <glib.h>
#include "sciteco.h"
+#include "error.h"
#include "string-utils.h"
#include "expressions.h"
#include "parser.h"
@@ -34,17 +35,20 @@
#include "goto.h"
#include "goto-commands.h"
-TECO_DECLARE_STATE(teco_state_blockcomment);
-TECO_DECLARE_STATE(teco_state_eolcomment);
+static teco_state_t teco_state_blockcomment;
+static teco_state_t teco_state_eolcomment;
+/**
+ * In TECO_MODE_PARSE_ONLY_GOTO mode, we remain in parse-only mode
+ * until the given label is encountered.
+ */
teco_string_t teco_goto_skip_label = {NULL, 0};
-
-static gboolean
-teco_state_label_initial(teco_machine_main_t *ctx, GError **error)
-{
- memset(&ctx->goto_label, 0, sizeof(ctx->goto_label));
- return TRUE;
-}
+/**
+ * The program counter to restore if the teco_goto_skip_label
+ * is \b not found (after :Olabel$).
+ * If smaller than 0 an error is thrown instead.
+ */
+gssize teco_goto_backup_pc = -1;
/*
* NOTE: The comma is theoretically not allowed in a label
@@ -71,9 +75,10 @@ teco_state_label_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
teco_goto_table_undo_remove(&ctx->goto_table, ctx->goto_label.data, ctx->goto_label.len);
if (teco_goto_skip_label.len > 0 &&
- !teco_string_cmp(&ctx->goto_label, teco_goto_skip_label.data, teco_goto_skip_label.len)) {
+ !teco_string_cmp(ctx->goto_label, teco_goto_skip_label.data, teco_goto_skip_label.len)) {
teco_undo_string_own(teco_goto_skip_label);
memset(&teco_goto_skip_label, 0, sizeof(teco_goto_skip_label));
+ teco_undo_gssize(teco_goto_backup_pc) = -1;
if (ctx->parent.must_undo)
teco_undo_flags(ctx->flags);
@@ -108,28 +113,37 @@ teco_state_label_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
}
TECO_DEFINE_STATE(teco_state_label,
- .initial_cb = (teco_state_initial_cb_t)teco_state_label_initial,
- .style = SCE_SCITECO_LABEL
+ .style = SCE_SCITECO_LABEL,
+ .input_cb = (teco_state_input_cb_t)teco_state_label_input
);
static teco_state_t *
-teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_goto_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
+ if (!str.len) {
+ /* you can still write @O/,/, though... */
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "No labels given for <O>");
+ return NULL;
+ }
+
teco_int_t value;
- if (!teco_expressions_pop_num_calc(&value, 1, error))
+ if (!teco_expressions_pop_num_calc(&value, 0, error))
return NULL;
+ gboolean colon_modified = teco_machine_main_eval_colon(ctx) > 0;
+
/*
* Find the comma-separated substring in str indexed by `value`.
*/
teco_string_t label = {NULL, 0};
- while (value > 0) {
- label.data = label.data ? label.data+label.len+1 : str->data;
- const gchar *p = label.data ? memchr(label.data, ',', str->len - (label.data - str->data)) : NULL;
- label.len = p ? p - label.data : str->len - (label.data - str->data);
+ while (value >= 0) {
+ label.data = label.data ? label.data+label.len+1 : str.data;
+ const gchar *p = label.data ? memchr(label.data, ',', str.len - (label.data - str.data)) : NULL;
+ label.len = p ? p - label.data : str.len - (label.data - str.data);
value--;
@@ -137,19 +151,24 @@ teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError
break;
}
- if (value == 0) {
+ if (value < 0 && label.len > 0) {
gssize pc = teco_goto_table_find(&ctx->goto_table, label.data, label.len);
if (pc >= 0) {
ctx->macro_pc = pc;
- } else {
+ } else if (!ctx->goto_table.complete) {
/* skip till label is defined */
g_assert(teco_goto_skip_label.len == 0);
undo__teco_string_truncate(&teco_goto_skip_label, 0);
teco_string_init(&teco_goto_skip_label, label.data, label.len);
+ teco_undo_gssize(teco_goto_backup_pc) = colon_modified ? ctx->macro_pc : -1;
if (ctx->parent.must_undo)
teco_undo_flags(ctx->flags);
ctx->flags.mode = TECO_MODE_PARSE_ONLY_GOTO;
+ } else if (!colon_modified) {
+ /* can happen if we previously executed a colon-modified go-to */
+ teco_error_label_set(error, teco_goto_skip_label.data, teco_goto_skip_label.len);
+ return NULL;
}
}
@@ -159,38 +178,52 @@ teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError
/* in cmdline.c */
gboolean teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
gunichar chr, GError **error);
-gboolean teco_state_goto_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str,
+gboolean teco_state_goto_insert_completion(teco_machine_main_t *ctx, teco_string_t str,
GError **error);
-/*$ O
+/*$ "O" ":O" goto
* Olabel$ -- Go to label
- * [n]Olabel1[,label2,...]$
+ * :Olabel$
+ * [n]Olabel0[,label1,...]$
*
* Go to <label>.
* The simple go-to command is a special case of the
* computed go-to command.
* A comma-separated list of labels may be specified
* in the string argument.
- * The label to jump to is selected by <n> (1 is <label1>,
- * 2 is <label2>, etc.).
- * If <n> is omitted, 1 is implied.
+ * The label to jump to is selected by <n> (0 is <label0>,
+ * 1 is <label1>, etc.).
+ * If <n> is omitted, 0 is implied.
+ * Computed go-tos can be used like switch-case statements
+ * other languages.
*
* If the label selected by <n> is does not exist in the
- * list of labels, the command does nothing.
+ * list of labels or is empty, the command does nothing
+ * and execution continues normally.
* Label definitions are cached in a table, so that
* if the label to go to has already been defined, the
* go-to command will jump immediately.
* Otherwise, parsing continues until the <label>
* is defined.
* The command will yield an error if a label has
- * not been defined when the macro or command-line
- * is terminated.
- * In the latter case, the user will not be able to
- * terminate the command-line.
+ * not been defined when the macro is terminated.
+ * When jumping to a non-existent <label> in the
+ * command-line macro, you cannot practically terminate
+ * the command-line until defining the <label>.
+ *
+ * String building constructs are enabled in \fBO\fP
+ * which allows for a second kind of computed go-to,
+ * where the label name contains the value to select.
+ * When colon-modifying the \fBO\fP command, execution
+ * will continue after the command if the given <label>
+ * isn't found.
+ * This is useful to handle the \(lqdefault\(rq case
+ * when using computed go-tos of the second kind.
*/
TECO_DEFINE_STATE_EXPECTSTRING(teco_state_goto,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_goto_process_edit_cmd,
- .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_goto_insert_completion
+ .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_goto_insert_completion,
+ .expectstring.done_cb = teco_state_goto_done
);
/**
@@ -211,25 +244,34 @@ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_goto,
)
static teco_state_t *
-teco_state_blockcomment_star_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+teco_state_blockcomment_star_input(teco_machine_t *ctx, gunichar chr, GError **error)
{
return chr == '!' ? &teco_state_start : &teco_state_blockcomment;
}
-TECO_DEFINE_STATE_COMMENT(teco_state_blockcomment_star);
+static TECO_DEFINE_STATE_COMMENT(teco_state_blockcomment_star,
+ .input_cb = teco_state_blockcomment_star_input
+);
static teco_state_t *
-teco_state_blockcomment_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+teco_state_blockcomment_input(teco_machine_t *ctx, gunichar chr, GError **error)
{
return chr == '*' ? &teco_state_blockcomment_star : &teco_state_blockcomment;
}
-TECO_DEFINE_STATE_COMMENT(teco_state_blockcomment);
+static TECO_DEFINE_STATE_COMMENT(teco_state_blockcomment,
+ .input_cb = teco_state_blockcomment_input
+);
+/*
+ * `!!` line comments are inspired by TECO-64.
+ */
static teco_state_t *
-teco_state_eolcomment_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+teco_state_eolcomment_input(teco_machine_t *ctx, gunichar chr, GError **error)
{
return chr == '\n' ? &teco_state_start : &teco_state_eolcomment;
}
-TECO_DEFINE_STATE_COMMENT(teco_state_eolcomment);
+static TECO_DEFINE_STATE_COMMENT(teco_state_eolcomment,
+ .input_cb = teco_state_eolcomment_input
+);
diff --git a/src/goto-commands.h b/src/goto-commands.h
index f4f52d5..3b44168 100644
--- a/src/goto-commands.h
+++ b/src/goto-commands.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -16,10 +16,13 @@
*/
#pragma once
+#include <glib.h>
+
#include "parser.h"
#include "string-utils.h"
extern teco_string_t teco_goto_skip_label;
+extern gssize teco_goto_backup_pc;
-TECO_DECLARE_STATE(teco_state_label);
-TECO_DECLARE_STATE(teco_state_goto);
+extern teco_state_t teco_state_label;
+extern teco_state_t teco_state_goto;
diff --git a/src/goto.c b/src/goto.c
index 855c9f9..42eb964 100644
--- a/src/goto.c
+++ b/src/goto.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/goto.h b/src/goto.h
index eda823f..c514692 100644
--- a/src/goto.h
+++ b/src/goto.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -26,10 +26,14 @@
typedef struct {
teco_rb3str_tree_t tree;
+ /** Whether to generate undo tokens (unnecessary in macro invocations) */
+ guint must_undo : 1;
+
/**
- * Whether to generate undo tokens (unnecessary in macro invocations)
+ * Whether the table is guaranteed to be complete because the entire
+ * macro has already been parsed.
*/
- gboolean must_undo;
+ guint complete : 1;
} teco_goto_table_t;
/** @memberof teco_goto_table_t */
@@ -38,6 +42,7 @@ teco_goto_table_init(teco_goto_table_t *ctx, gboolean must_undo)
{
rb3_reset_tree(&ctx->tree);
ctx->must_undo = must_undo;
+ ctx->complete = FALSE;
}
gboolean teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len);
diff --git a/src/help.c b/src/help.c
index 0bbbc54..03cc595 100644
--- a/src/help.c
+++ b/src/help.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -271,7 +271,7 @@ teco_state_help_initial(teco_machine_main_t *ctx, GError **error)
}
static teco_state_t *
-teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_help_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
@@ -281,7 +281,7 @@ teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError
"Help topic must not contain null-byte");
return NULL;
}
- const gchar *topic_name = str->data ? : "";
+ const gchar *topic_name = str.data ? : "";
teco_help_topic_t *topic = teco_help_find(topic_name);
if (!topic) {
g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
@@ -316,7 +316,7 @@ teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError
/* in cmdline.c */
gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
gunichar chr, GError **error);
-gboolean teco_state_help_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str,
+gboolean teco_state_help_insert_completion(teco_machine_main_t *ctx, teco_string_t str,
GError **error);
/*$ "?" help
@@ -388,5 +388,6 @@ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_help,
.initial_cb = (teco_state_initial_cb_t)teco_state_help_initial,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_help_process_edit_cmd,
.insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_help_insert_completion,
- .expectstring.string_building = FALSE
+ .expectstring.string_building = FALSE,
+ .expectstring.done_cb = teco_state_help_done
);
diff --git a/src/help.h b/src/help.h
index 3148e01..c2accc4 100644
--- a/src/help.h
+++ b/src/help.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -27,4 +27,4 @@ gboolean teco_help_auto_complete(const gchar *topic_name, teco_string_t *insert)
* Command states
*/
-TECO_DECLARE_STATE(teco_state_help);
+extern teco_state_t teco_state_help;
diff --git a/src/interface-curses/curses-icons.c b/src/interface-curses/curses-icons.c
index 8a84abe..0e14655 100644
--- a/src/interface-curses/curses-icons.c
+++ b/src/interface-curses/curses-icons.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -24,8 +24,6 @@
#include <glib.h>
-#include <curses.h>
-
#include "sciteco.h"
#include "curses-icons.h"
@@ -364,6 +362,10 @@ teco_curses_icon_cmp(const void *a, const void *b)
gunichar
teco_curses_icons_lookup_file(const gchar *filename)
{
+ if (!filename || !*filename)
+ /* "(Unnamed)" file */
+ return 0xf1036; /* ó±€¶ */
+
g_autofree gchar *basename = g_path_get_basename(filename);
const teco_curses_icon_t *icon;
diff --git a/src/interface-curses/curses-icons.h b/src/interface-curses/curses-icons.h
index fce9d75..a12fe88 100644
--- a/src/interface-curses/curses-icons.h
+++ b/src/interface-curses/curses-icons.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/interface-curses/curses-info-popup.c b/src/interface-curses/curses-info-popup.c
index 332d434..edb6e15 100644
--- a/src/interface-curses/curses-info-popup.c
+++ b/src/interface-curses/curses-info-popup.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -19,6 +19,8 @@
#include "config.h"
#endif
+#include <string.h>
+
#include <glib.h>
#include <curses.h>
@@ -26,6 +28,7 @@
#include "list.h"
#include "string-utils.h"
#include "interface.h"
+#include "cmdline.h"
#include "curses-utils.h"
#include "curses-info-popup.h"
#include "curses-icons.h"
@@ -37,6 +40,7 @@ typedef struct {
teco_stailq_entry_t entry;
teco_popup_entry_type_t type;
+ /** entry name or empty string for the "(Unnamed)" buffer */
teco_string_t name;
gboolean highlight;
} teco_popup_entry_t;
@@ -71,7 +75,6 @@ teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_type_
static void
teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr)
{
- int cols = getmaxx(stdscr); /**! screen width */
int pad_lines; /**! pad height */
gint pad_cols; /**! entry columns */
gint pad_colwidth; /**! width per entry column */
@@ -82,10 +85,10 @@ teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr)
* Otherwise 2 characters after the entry.
*/
gint reserve = teco_ed & TECO_ED_ICONS ? 2+1 : 2;
- pad_colwidth = MIN(ctx->longest + reserve, cols - 2);
+ pad_colwidth = MIN(ctx->longest + reserve, COLS - 2);
/* pad_cols = floor((cols - 2) / pad_colwidth) */
- pad_cols = (cols - 2) / pad_colwidth;
+ pad_cols = (COLS - 2) / pad_colwidth;
/* pad_lines = ceil(length / pad_cols) */
pad_lines = (ctx->length+pad_cols-1) / pad_cols;
@@ -96,7 +99,7 @@ teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr)
* it will be drawn into the popup window which has left
* and right borders.
*/
- ctx->pad = newpad(pad_lines, cols - 2);
+ ctx->pad = newpad(pad_lines, COLS - 2);
/*
* NOTE: attr could contain A_REVERSE on monochrome terminals,
@@ -122,25 +125,32 @@ teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr)
if (entry->highlight)
wattron(ctx->pad, A_BOLD);
+ teco_string_t name = entry->name;
+ if (!name.len) {
+ name.data = TECO_UNNAMED_FILE;
+ name.len = strlen(name.data);
+ }
+
switch (entry->type) {
case TECO_POPUP_FILE:
- g_assert(!teco_string_contains(&entry->name, '\0'));
+ g_assert(!teco_string_contains(name, '\0'));
if (teco_ed & TECO_ED_ICONS) {
+ /* "(Unnamed)" buffer is looked up as "" */
teco_curses_add_wc(ctx->pad, teco_curses_icons_lookup_file(entry->name.data));
waddch(ctx->pad, ' ');
}
- teco_curses_format_filename(ctx->pad, entry->name.data, -1);
+ teco_curses_format_filename(ctx->pad, name.data, -1);
break;
case TECO_POPUP_DIRECTORY:
- g_assert(!teco_string_contains(&entry->name, '\0'));
+ g_assert(!teco_string_contains(name, '\0'));
if (teco_ed & TECO_ED_ICONS) {
teco_curses_add_wc(ctx->pad, teco_curses_icons_lookup_dir(entry->name.data));
waddch(ctx->pad, ' ');
}
- teco_curses_format_filename(ctx->pad, entry->name.data, -1);
+ teco_curses_format_filename(ctx->pad, name.data, -1);
break;
default:
- teco_curses_format_str(ctx->pad, entry->name.data, entry->name.len, -1);
+ teco_curses_format_str(ctx->pad, name.data, name.len, -1);
break;
}
@@ -157,9 +167,6 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr)
/* nothing to display */
return;
- int lines, cols; /* screen dimensions */
- getmaxyx(stdscr, lines, cols);
-
if (ctx->window)
delwin(ctx->window);
@@ -171,10 +178,10 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr)
* Popup window can cover all but one screen row.
* Another row is reserved for the top border.
*/
- gint popup_lines = MIN(pad_lines + 1, lines - 1);
+ gint popup_lines = MIN(pad_lines + 1, LINES - teco_cmdline.height);
/* window covers message, scintilla and info windows */
- ctx->window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0);
+ ctx->window = newwin(popup_lines, 0, LINES - teco_cmdline.height - popup_lines, 0);
wattrset(ctx->window, attr);
@@ -188,7 +195,7 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr)
copywin(ctx->pad, ctx->window,
ctx->pad_first_line, 0,
- 1, 1, popup_lines - 1, cols - 2, FALSE);
+ 1, 1, popup_lines - 1, COLS - 2, FALSE);
if (pad_lines <= popup_lines - 1)
/* no need for scrollbar */
@@ -200,13 +207,13 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr)
/* bar_y = floor(pad_first_line/pad_lines * (popup_lines-2)) + 1 */
gint bar_y = ctx->pad_first_line*(popup_lines-2) / pad_lines + 1;
- mvwvline(ctx->window, 1, cols-1, ACS_CKBOARD, popup_lines-2);
+ mvwvline(ctx->window, 1, COLS-1, ACS_CKBOARD, popup_lines-2);
/*
* We do not use ACS_BLOCK here since it will not
* always be drawn as a solid block (e.g. xterm).
* Instead, simply draw reverse blanks.
*/
- wmove(ctx->window, bar_y, cols-1);
+ wmove(ctx->window, bar_y, COLS-1);
wattrset(ctx->window, attr ^ A_REVERSE);
wvline(ctx->window, ' ', bar_height);
}
@@ -227,7 +234,6 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr)
const teco_string_t *
teco_curses_info_popup_getentry(teco_curses_info_popup_t *ctx, gint y, gint x)
{
- int cols = getmaxx(stdscr); /**! screen width */
gint pad_cols; /**! entry columns */
gint pad_colwidth; /**! width per entry column */
@@ -240,10 +246,10 @@ teco_curses_info_popup_getentry(teco_curses_info_popup_t *ctx, gint y, gint x)
* Otherwise 2 characters after the entry.
*/
gint reserve = teco_ed & TECO_ED_ICONS ? 2+1 : 2;
- pad_colwidth = MIN(ctx->longest + reserve, cols - 2);
+ pad_colwidth = MIN(ctx->longest + reserve, COLS - 2);
/* pad_cols = floor((cols - 2) / pad_colwidth) */
- pad_cols = (cols - 2) / pad_colwidth;
+ pad_cols = (COLS - 2) / pad_colwidth;
gint cur_col = 0;
for (teco_stailq_entry_t *cur = ctx->list.first; cur != NULL; cur = cur->next) {
@@ -265,9 +271,8 @@ teco_curses_info_popup_getentry(teco_curses_info_popup_t *ctx, gint y, gint x)
void
teco_curses_info_popup_scroll_page(teco_curses_info_popup_t *ctx)
{
- gint lines = getmaxy(stdscr);
gint pad_lines = getmaxy(ctx->pad);
- gint popup_lines = MIN(pad_lines + 1, lines - 1);
+ gint popup_lines = MIN(pad_lines + 1, LINES - teco_cmdline.height);
/* progress scroll position */
ctx->pad_first_line += popup_lines - 1;
@@ -281,9 +286,8 @@ teco_curses_info_popup_scroll_page(teco_curses_info_popup_t *ctx)
void
teco_curses_info_popup_scroll(teco_curses_info_popup_t *ctx, gint delta)
{
- gint lines = getmaxy(stdscr);
gint pad_lines = getmaxy(ctx->pad);
- gint popup_lines = MIN(pad_lines + 1, lines - 1);
+ gint popup_lines = MIN(pad_lines + 1, LINES - teco_cmdline.height);
ctx->pad_first_line = MAX(ctx->pad_first_line+delta, 0);
if (pad_lines - ctx->pad_first_line < popup_lines - 1)
diff --git a/src/interface-curses/curses-info-popup.h b/src/interface-curses/curses-info-popup.h
index d845b29..fd923e9 100644
--- a/src/interface-curses/curses-info-popup.h
+++ b/src/interface-curses/curses-info-popup.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/interface-curses/curses-utils.c b/src/interface-curses/curses-utils.c
index f94b6dc..875c332 100644
--- a/src/interface-curses/curses-utils.c
+++ b/src/interface-curses/curses-utils.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/interface-curses/curses-utils.h b/src/interface-curses/curses-utils.h
index 18cdd3d..97fc1cc 100644
--- a/src/interface-curses/curses-utils.h
+++ b/src/interface-curses/curses-utils.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -20,6 +20,9 @@
#include <curses.h>
+/** what is displayed for unnamed buffers in the info line and popups */
+#define TECO_UNNAMED_FILE "(Unnamed)"
+
guint teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width);
guint teco_curses_format_filename(WINDOW *win, const gchar *filename, gint max_width);
diff --git a/src/interface-curses/interface.c b/src/interface-curses/interface.c
index a71ca20..b1c806f 100644
--- a/src/interface-curses/interface.c
+++ b/src/interface-curses/interface.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -22,7 +22,6 @@
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
-#include <stdarg.h>
#include <unistd.h>
#include <errno.h>
@@ -108,6 +107,8 @@
#define CURSES_TTY
#endif
+//#define DEBUG
+
#ifdef G_OS_WIN32
/**
@@ -135,7 +136,7 @@ teco_console_ctrl_handler(DWORD type)
static gint teco_xterm_version(void) G_GNUC_UNUSED;
-#define UNNAMED_FILE "(Unnamed)"
+static gint teco_interface_blocking_getch(void);
/**
* Get bright variant of one of the 8 standard
@@ -163,19 +164,98 @@ static gint teco_xterm_version(void) G_GNUC_UNUSED;
#define COLOR_LCYAN COLOR_LIGHT(COLOR_CYAN)
#define COLOR_LWHITE COLOR_LIGHT(COLOR_WHITE)
+static struct {
+ /**
+ * Mapping of foreground and background curses color tuples
+ * (encoded into a pointer) to a color pair number.
+ */
+ GHashTable *pair_table;
+
+ /**
+ * Mapping of the first 16 curses color codes (that may or may not
+ * correspond with the standard terminal color codes) to
+ * Scintilla-compatible RGB values (red is LSB) to initialize after
+ * Curses startup.
+ * Negative values mean no color redefinition (keep the original
+ * palette entry).
+ */
+ gint32 color_table[16];
+
+ /**
+ * Mapping of the first 16 curses color codes to their
+ * original values for restoring them on shutdown.
+ * Unfortunately, this may not be supported on all
+ * curses ports, so this array may be unused.
+ */
+ struct {
+ gshort r, g, b;
+ } orig_color_table[16];
+
+ int stdin_orig, stdout_orig, stderr_orig;
+ SCREEN *screen;
+ FILE *screen_tty;
+
+ WINDOW *info_window;
+ enum {
+ TECO_INFO_TYPE_BUFFER = 0,
+ TECO_INFO_TYPE_QREG
+ } info_type;
+ /* current document's name or empty string for "(Unnamed)" buffer */
+ teco_string_t info_current;
+ gboolean info_dirty;
+
+ /** timer to track the recovery interval */
+ GTimer *recovery_timer;
+
+ WINDOW *msg_window;
+
+ /**
+ * Pad used exclusively for wgetch() as it will not
+ * result in unwanted wrefresh().
+ */
+ WINDOW *input_pad;
+ GQueue *input_queue;
+
+ teco_curses_info_popup_t popup;
+ gsize popup_prefix_len;
+
+ /**
+ * GError "thrown" by teco_interface_event_loop_iter().
+ * Having this in a variable avoids problems with EMScripten.
+ */
+ GError *event_loop_error;
+} teco_interface;
+
/**
- * Returns the curses `COLOR_PAIR` for the given curses foreground and background `COLOR`s.
- * This is used simply to enumerate every possible color combination.
- * Note: only 256 combinations are possible due to curses portability.
+ * Returns the curses color pair for the given curses foreground and background colors.
+ * Initializes a new pair if necessary.
+ *
+ * Scinterm no longer initializes all color pairs for all combinations of
+ * the builtin foreground and background colors.
+ * Since curses guarantees only 256 color pairs, we cannot do that either.
+ * Instead we allocate color pairs beginnig at 128 on demand
+ * (similar to what Scinterm does).
*
- * @param fg The curses foreground `COLOR`.
- * @param bg The curses background `COLOR`.
- * @return number for defining a curses `COLOR_PAIR`.
+ * @note Scinterm now also has scintilla_set_color_offsets(),
+ * so we could use the lower 127 color pairs as well.
+ *
+ * @param fg curses foreground color
+ * @param bg curses background color
+ * @return curses color pair number
*/
-static inline gshort
+static gshort
teco_color_pair(gshort fg, gshort bg)
{
- return bg * (COLORS < 16 ? 8 : 16) + fg + 1;
+ static gshort last_pair = 127;
+
+ G_STATIC_ASSERT(sizeof(gshort)*2 <= sizeof(guint));
+ gpointer key = GUINT_TO_POINTER(((guint)fg << 16) | bg);
+ gpointer value = g_hash_table_lookup(teco_interface.pair_table, key);
+ if (G_LIKELY(value != NULL))
+ return GPOINTER_TO_UINT(value);
+ init_pair(++last_pair, fg, bg);
+ g_hash_table_insert(teco_interface.pair_table, key, GUINT_TO_POINTER(last_pair));
+ return last_pair;
}
/**
@@ -333,61 +413,6 @@ teco_view_free(teco_view_t *ctx)
scintilla_delete(ctx);
}
-static struct {
- /**
- * Mapping of the first 16 curses color codes (that may or may not
- * correspond with the standard terminal color codes) to
- * Scintilla-compatible RGB values (red is LSB) to initialize after
- * Curses startup.
- * Negative values mean no color redefinition (keep the original
- * palette entry).
- */
- gint32 color_table[16];
-
- /**
- * Mapping of the first 16 curses color codes to their
- * original values for restoring them on shutdown.
- * Unfortunately, this may not be supported on all
- * curses ports, so this array may be unused.
- */
- struct {
- gshort r, g, b;
- } orig_color_table[16];
-
- int stdout_orig, stderr_orig;
- SCREEN *screen;
- FILE *screen_tty;
-
- WINDOW *info_window;
- enum {
- TECO_INFO_TYPE_BUFFER = 0,
- TECO_INFO_TYPE_QREG
- } info_type;
- teco_string_t info_current;
- gboolean info_dirty;
-
- WINDOW *msg_window;
-
- WINDOW *cmdline_window, *cmdline_pad;
- guint cmdline_len, cmdline_rubout_len;
-
- /**
- * Pad used exclusively for wgetch() as it will not
- * result in unwanted wrefresh().
- */
- WINDOW *input_pad;
- GQueue *input_queue;
-
- teco_curses_info_popup_t popup;
- gsize popup_prefix_len;
-
- /**
- * GError "thrown" by teco_interface_event_loop_iter().
- * Having this in a variable avoids problems with EMScripten.
- */
- GError *event_loop_error;
-} teco_interface;
-
static void teco_interface_init_color_safe(guint color, guint32 rgb);
static void teco_interface_restore_colors(void);
@@ -401,7 +426,6 @@ static void teco_interface_resize_all_windows(void);
static void teco_interface_set_window_title(const gchar *title);
static void teco_interface_draw_info(void);
-static void teco_interface_draw_cmdline(void);
void
teco_interface_init(void)
@@ -411,15 +435,16 @@ teco_interface_init(void)
for (guint i = 0; i < G_N_ELEMENTS(teco_interface.orig_color_table); i++)
teco_interface.orig_color_table[i].r = -1;
- teco_interface.stdout_orig = teco_interface.stderr_orig = -1;
+ teco_interface.stdin_orig = teco_interface.stdout_orig = teco_interface.stderr_orig = -1;
teco_curses_info_popup_init(&teco_interface.popup);
+ teco_cmdline_init();
/*
- * Make sure we have a string for the info line
- * even if teco_interface_info_update() is never called.
+ * The default INDIC_STRIKE wouldn't be visible.
+ * Instead we use INDIC_SQUIGGLE, which is rendered as A_UNDERLINE.
*/
- teco_string_init(&teco_interface.info_current, PACKAGE_NAME, strlen(PACKAGE_NAME));
+ teco_cmdline_ssm(SCI_INDICSETSTYLE, INDICATOR_RUBBEDOUT, INDIC_SQUIGGLE);
/*
* On all platforms except Curses/XTerm, it's
@@ -558,7 +583,7 @@ teco_interface_init_color(guint color, guint32 rgb)
((color & 0x1) << 2) | ((color & 0x4) >> 2);
#endif
- if (teco_interface.cmdline_window) {
+ if (teco_interface.input_pad) {
/* interactive mode */
if (!can_change_color())
return;
@@ -580,22 +605,46 @@ teco_interface_init_color(guint color, guint32 rgb)
static void
teco_interface_init_screen(void)
{
- teco_interface.screen_tty = g_fopen("/dev/tty", "r+");
+ teco_interface.screen_tty = g_fopen("/dev/tty", "a");
/* should never fail */
g_assert(teco_interface.screen_tty != NULL);
- teco_interface.screen = newterm(NULL, teco_interface.screen_tty, teco_interface.screen_tty);
+ /*
+ * At least on NetBSD we loose keypresses when passing in a
+ * handle for /dev/tty.
+ * We therefore redirect stdin in interactive mode.
+ * This works always if stdin was already redirected or not (isatty(0))
+ * since we are guaranteed not to read from stdin outside of curses.
+ * When returning to batch mode, we can restore the original stdin.
+ */
+ teco_interface.stdin_orig = dup(0);
+ g_assert(teco_interface.stdin_orig >= 0);
+ G_GNUC_UNUSED FILE *stdin_new = g_freopen("/dev/tty", "r", stdin);
+ g_assert(stdin_new != NULL);
+
+ teco_interface.screen = newterm(NULL, teco_interface.screen_tty, stdin);
if (G_UNLIKELY(!teco_interface.screen)) {
g_fprintf(stderr, "Error initializing interactive mode. "
"$TERM may be incorrect.\n");
exit(EXIT_FAILURE);
}
+ /* initscr() does that in ncurses */
+ def_prog_mode();
+
/*
* If stdout or stderr would go to the terminal,
* redirect it. Otherwise, they are already redirected
* (e.g. to a file) and writing to them does not
* interrupt terminal interaction.
+ *
+ * This cannot of course preserve all messages written to stdout/stderr.
+ * Only those messages written before flushing will be preserved and
+ * be visible after program termination since they are still in a user-
+ * space stdio-buffer.
+ * All messages could only be preserved if we redirected to a temporary
+ * file and replayed it afterwards. It wouldn't preserve the order of
+ * stdout vs. stderr messages.
*/
if (isatty(1)) {
teco_interface.stdout_orig = dup(1);
@@ -690,6 +739,9 @@ teco_interface_init_interactive(GError **error)
teco_interface_init_screen();
+ teco_interface.pair_table = g_hash_table_new(g_direct_hash, g_direct_equal);
+ start_color();
+
/*
* On UNIX terminals, the escape key is usually
* delivered as the escape character even though function
@@ -716,12 +768,8 @@ teco_interface_init_interactive(GError **error)
* Disables click-detection.
* If we'd want to discern PRESSED and CLICKED events,
* we'd have to emulate the same feature on GTK.
- *
- * On PDCurses/Wincon we currently rely on click detection
- * since it does not report BUTTONX_RELEASED unless also
- * moving the mouse cursor.
*/
-#if NCURSES_MOUSE_VERSION >= 2 && !defined(PDCURSES_WINCON)
+#if NCURSES_MOUSE_VERSION >= 2
mouseinterval(0);
#endif
@@ -745,8 +793,12 @@ teco_interface_init_interactive(GError **error)
leaveok(stdscr, TRUE);
teco_interface.info_window = newwin(1, 0, 0, 0);
- teco_interface.msg_window = newwin(1, 0, LINES - 2, 0);
- teco_interface.cmdline_window = newwin(0, 0, LINES - 1, 0);
+ teco_interface.msg_window = newwin(1, 0, LINES - teco_cmdline.height - 1, 0);
+
+ WINDOW *cmdline_win = teco_view_get_window(teco_cmdline.view);
+ wresize(cmdline_win, teco_cmdline.height, COLS);
+ mvwin(cmdline_win, LINES - teco_cmdline.height, 0);
+ teco_cmdline_resized(COLS);
teco_interface.input_pad = newpad(1, 1);
/*
@@ -819,10 +871,14 @@ teco_interface_restore_batch(void)
teco_interface_restore_colors();
/*
- * Restore stdout and stderr, so output goes to
+ * Restore stdin, stdout and stderr, so output goes to
* the terminal again in case we "muted" them.
*/
#ifdef CURSES_TTY
+ if (teco_interface.stdin_orig >= 0) {
+ G_GNUC_UNUSED int fd = dup2(teco_interface.stdin_orig, 0);
+ g_assert(fd == 0);
+ }
if (teco_interface.stdout_orig >= 0) {
G_GNUC_UNUSED int fd = dup2(teco_interface.stdout_orig, 1);
g_assert(fd == 1);
@@ -834,40 +890,38 @@ teco_interface_restore_batch(void)
#endif
/*
- * cmdline_window determines whether we're in batch mode.
+ * input_pad determines whether we're in batch mode.
*/
- if (teco_interface.cmdline_window) {
- delwin(teco_interface.cmdline_window);
- teco_interface.cmdline_window = NULL;
+ if (teco_interface.input_pad) {
+ delwin(teco_interface.input_pad);
+ teco_interface.input_pad = NULL;
}
}
static void
teco_interface_resize_all_windows(void)
{
- int lines, cols; /* screen dimensions */
-
- getmaxyx(stdscr, lines, cols);
-
- wresize(teco_interface.info_window, 1, cols);
+ wresize(teco_interface.info_window, 1, COLS);
wresize(teco_view_get_window(teco_interface_current_view),
- lines - 3, cols);
- wresize(teco_interface.msg_window, 1, cols);
- mvwin(teco_interface.msg_window, lines - 2, 0);
- wresize(teco_interface.cmdline_window, 1, cols);
- mvwin(teco_interface.cmdline_window, lines - 1, 0);
+ LINES - 2 - teco_cmdline.height, COLS);
+ wresize(teco_interface.msg_window, 1, COLS);
+ mvwin(teco_interface.msg_window, LINES - 1 - teco_cmdline.height, 0);
+
+ WINDOW *cmdline_win = teco_view_get_window(teco_cmdline.view);
+ wresize(cmdline_win, teco_cmdline.height, COLS);
+ mvwin(cmdline_win, LINES - teco_cmdline.height, 0);
+ teco_cmdline_resized(COLS);
teco_interface_draw_info();
teco_interface_msg_clear(); /* FIXME: use saved message */
teco_interface_popup_clear();
- teco_interface_draw_cmdline();
}
void
-teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len)
{
- if (!teco_interface.cmdline_window) { /* batch mode */
- teco_interface_stdio_vmsg(type, fmt, ap);
+ if (!teco_interface.input_pad) { /* batch mode */
+ teco_interface_stdio_msg(type, str, len);
return;
}
@@ -876,10 +930,7 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
* even in interactive mode.
*/
#if defined(PDCURSES_GUI) || defined(CURSES_TTY) || defined(NCURSES_WIN32)
- va_list aq;
- va_copy(aq, ap);
- teco_interface_stdio_vmsg(type, fmt, aq);
- va_end(aq);
+ teco_interface_stdio_msg(type, str, len);
#endif
short fg, bg;
@@ -903,27 +954,68 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
break;
}
- /*
- * NOTE: This is safe since we don't have to cancel out any A_REVERSE,
- * that could be set in the background attributes.
- */
wmove(teco_interface.msg_window, 0, 0);
- wbkgdset(teco_interface.msg_window, teco_color_attr(fg, bg));
- vw_printw(teco_interface.msg_window, fmt, ap);
- wclrtoeol(teco_interface.msg_window);
+ wattrset(teco_interface.msg_window, teco_color_attr(fg, bg));
+ teco_curses_format_str(teco_interface.msg_window, str, len, -1);
+ teco_curses_clrtobot(teco_interface.msg_window);
}
void
teco_interface_msg_clear(void)
{
- if (!teco_interface.cmdline_window) /* batch mode */
+ if (!teco_interface.input_pad) /* batch mode */
return;
short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
- wbkgdset(teco_interface.msg_window, teco_color_attr(fg, bg));
- werase(teco_interface.msg_window);
+ wmove(teco_interface.msg_window, 0, 0);
+ wattrset(teco_interface.msg_window, teco_color_attr(fg, bg));
+ teco_curses_clrtobot(teco_interface.msg_window);
+}
+
+teco_int_t
+teco_interface_getch(gboolean widechar)
+{
+ if (!teco_interface.input_pad) /* batch mode */
+ return teco_interface_stdio_getch(widechar);
+
+ teco_interface_refresh(FALSE);
+
+ /*
+ * Signal that we accept input by drawing a real cursor in the message bar.
+ */
+ wmove(teco_interface.msg_window, 0, 0);
+ curs_set(1);
+ wrefresh(teco_interface.msg_window);
+
+ gchar buf[4];
+ gint i = 0;
+ gint32 cp;
+
+ do {
+ cp = teco_interface_blocking_getch();
+ if (cp == TECO_CTL_KEY('C'))
+ teco_interrupted = TRUE;
+ if (cp == TECO_CTL_KEY('C') || cp == TECO_CTL_KEY('D')) {
+ cp = -1;
+ break;
+ }
+ if (cp < 0 || cp > 0xFF)
+ continue;
+
+ if (!widechar || !cp)
+ break;
+
+ /* doesn't work as expected when passed a null byte */
+ buf[i] = cp;
+ cp = g_utf8_get_char_validated(buf, ++i);
+ if (i >= sizeof(buf) || cp != -2)
+ i = 0;
+ } while (cp < 0);
+
+ curs_set(0);
+ return cp;
}
void
@@ -931,7 +1023,7 @@ teco_interface_show_view(teco_view_t *view)
{
teco_interface_current_view = view;
- if (!teco_interface.cmdline_window) /* batch mode */
+ if (!teco_interface.input_pad) /* batch mode */
return;
WINDOW *current_view_win = teco_view_get_window(teco_interface_current_view);
@@ -940,9 +1032,7 @@ teco_interface_show_view(teco_view_t *view)
* screen size might have changed since
* this view's WINDOW was last active
*/
- int lines, cols; /* screen dimensions */
- getmaxyx(stdscr, lines, cols);
- wresize(current_view_win, lines - 3, cols);
+ wresize(current_view_win, LINES - 2 - teco_cmdline.height, COLS);
/* Set up window position: never changes */
mvwin(current_view_win, 1, 0);
}
@@ -972,40 +1062,64 @@ teco_interface_set_window_title(const gchar *title)
#elif defined(CURSES_TTY) && defined(HAVE_TIGETSTR)
+/*
+ * Many Modern terminal emulators map the window title to
+ * the historic status line.
+ * This feature is not standardized in ncurses,
+ * so we query the terminfo database.
+ * This feature may make problems with terminal emulators
+ * that do support a status line but do not map them
+ * to the window title.
+ * Some emulators (like xterm, rxvt and many pseudo-xterms)
+ * support setting the window title via custom escape
+ * sequences and via the status line but their
+ * terminfo entry does not say so.
+ * Real XTerm can also save and restore window titles but
+ * there is not even a terminfo capability defined for this.
+ * Currently, SciTECO just leaves the title set after we quit.
+ *
+ * TODO: Once we support customizing the UI,
+ * there could be a special status line that's sent
+ * to the terminal that may be set up in the profile
+ * depending on $TERM.
+ */
static void
teco_interface_set_window_title(const gchar *title)
{
- if (!has_status_line || !to_status_line || !from_status_line)
+ static const gchar *term = NULL;
+ static const gchar *title_start = NULL;
+ static const gchar *title_end = NULL;
+
+ if (G_UNLIKELY(!term)) {
+ term = g_getenv("TERM");
+
+ title_start = to_status_line;
+ title_end = from_status_line;
+
+ if ((!title_start || !title_end) && term &&
+ (g_str_has_prefix(term, "xterm") || g_str_has_prefix(term, "rxvt"))) {
+ /*
+ * Just assume that any whitelisted $TERM has the OSC-0
+ * escape sequence or at least ignores it.
+ * This might also set the window's icon, but it's more widely
+ * used than OSC-2.
+ */
+ title_start = "\e]0;";
+ title_end = "\a";
+ }
+ }
+
+ if (!title_start || !title_end)
return;
/*
- * Modern terminal emulators map the window title to
- * the historic status line.
- * This feature is not standardized in ncurses,
- * so we query the terminfo database.
- * This feature may make problems with terminal emulators
- * that do support a status line but do not map them
- * to the window title. Some emulators (like xterm)
- * support setting the window title via custom escape
- * sequences and via the status line but their
- * terminfo entry does not say so. (xterm can also
- * save and restore window titles but there is not
- * even a terminfo capability defined for this.)
- * Taken the different emulator incompatibilites
- * it may be best to make this configurable.
- * Once we support configurable status lines,
- * there could be a special status line that's sent
- * to the terminal that may be set up in the profile
- * depending on $TERM.
- *
* NOTE: The terminfo manpage advises us to use putp()
* but on ncurses/UNIX (where terminfo is available),
* we do not let curses write to stdout.
- * NOTE: This leaves the title set after we quit.
*/
- fputs(to_status_line, teco_interface.screen_tty);
+ fputs(title_start, teco_interface.screen_tty);
fputs(title, teco_interface.screen_tty);
- fputs(from_status_line, teco_interface.screen_tty);
+ fputs(title_end, teco_interface.screen_tty);
fflush(teco_interface.screen_tty);
}
@@ -1040,26 +1154,30 @@ teco_interface_draw_info(void)
waddstr(teco_interface.info_window, PACKAGE_NAME " ");
+ teco_string_t info_current = teco_interface.info_current;
+ if (!info_current.len) {
+ info_current.data = TECO_UNNAMED_FILE;
+ info_current.len = strlen(info_current.data);
+ }
+
switch (teco_interface.info_type) {
case TECO_INFO_TYPE_QREG:
info_type_str = PACKAGE_NAME " - <QRegister> ";
teco_curses_add_wc(teco_interface.info_window,
teco_ed & TECO_ED_ICONS ? TECO_CURSES_ICONS_QREG : '-');
waddstr(teco_interface.info_window, " <QRegister> ");
- /* same formatting as in command lines */
teco_curses_format_str(teco_interface.info_window,
- teco_interface.info_current.data,
- teco_interface.info_current.len, -1);
+ info_current.data, info_current.len, -1);
break;
case TECO_INFO_TYPE_BUFFER:
info_type_str = PACKAGE_NAME " - <Buffer> ";
- g_assert(!teco_string_contains(&teco_interface.info_current, '\0'));
+ g_assert(!teco_string_contains(info_current, '\0'));
+ /* "(Unnamed)" buffer has to be looked up as "" */
teco_curses_add_wc(teco_interface.info_window,
teco_ed & TECO_ED_ICONS ? teco_curses_icons_lookup_file(teco_interface.info_current.data) : '-');
waddstr(teco_interface.info_window, " <Buffer> ");
- teco_curses_format_filename(teco_interface.info_window,
- teco_interface.info_current.data,
+ teco_curses_format_filename(teco_interface.info_window, info_current.data,
getmaxx(teco_interface.info_window) -
getcurx(teco_interface.info_window) - 1);
waddch(teco_interface.info_window, teco_interface.info_dirty ? '*' : ' ');
@@ -1075,8 +1193,7 @@ teco_interface_draw_info(void)
* Make sure the title will consist only of printable characters.
*/
g_autofree gchar *info_current_printable;
- info_current_printable = teco_string_echo(teco_interface.info_current.data,
- teco_interface.info_current.len);
+ info_current_printable = teco_string_echo(info_current.data, info_current.len);
g_autofree gchar *title = g_strconcat(info_type_str, info_current_printable,
teco_interface.info_dirty ? "*" : "", NULL);
teco_interface_set_window_title(title);
@@ -1096,123 +1213,14 @@ teco_interface_info_update_qreg(const teco_qreg_t *reg)
void
teco_interface_info_update_buffer(const teco_buffer_t *buffer)
{
- const gchar *filename = buffer->filename ? : UNNAMED_FILE;
-
teco_string_clear(&teco_interface.info_current);
- teco_string_init(&teco_interface.info_current, filename, strlen(filename));
- teco_interface.info_dirty = buffer->dirty;
+ teco_string_init(&teco_interface.info_current, buffer->filename,
+ buffer->filename ? strlen(buffer->filename) : 0);
+ teco_interface.info_dirty = buffer->state > TECO_BUFFER_CLEAN;
teco_interface.info_type = TECO_INFO_TYPE_BUFFER;
/* NOTE: drawn in teco_interface_event_loop_iter() */
}
-void
-teco_interface_cmdline_update(const teco_cmdline_t *cmdline)
-{
- /*
- * Especially important on PDCurses, which can crash
- * in newpad() when run with --fake-cmdline.
- */
- if (!teco_interface.cmdline_window) /* batch mode */
- return;
-
- /*
- * Replace entire pre-formatted command-line.
- * We don't know if it is similar to the last one,
- * so resizing makes no sense.
- * We approximate the size of the new formatted command-line,
- * wasting a few bytes for control characters and
- * multi-byte Unicode sequences.
- */
- if (teco_interface.cmdline_pad)
- delwin(teco_interface.cmdline_pad);
-
- int max_cols = 1;
- for (guint i = 0; i < cmdline->str.len; i++)
- max_cols += TECO_IS_CTL(cmdline->str.data[i]) ? 3 : 1;
- teco_interface.cmdline_pad = newpad(1, max_cols);
-
- short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
- short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
- wattrset(teco_interface.cmdline_pad, teco_color_attr(fg, bg));
-
- /* format effective command line */
- teco_interface.cmdline_len =
- teco_curses_format_str(teco_interface.cmdline_pad,
- cmdline->str.data, cmdline->effective_len, -1);
-
- /*
- * A_BOLD should result in either a bold font or a brighter
- * color both on 8 and 16 color terminals.
- * This is not quite color-scheme-agnostic, but works
- * with both the `terminal` and `solarized` themes.
- * This problem will be gone once we use a Scintilla view
- * as command line, since we can then define a style
- * for rubbed out parts of the command line which will
- * be user-configurable.
- * The attributes, supported by the terminal can theoretically
- * be queried with term_attrs().
- */
- wattron(teco_interface.cmdline_pad, A_UNDERLINE | A_BOLD);
-
- /*
- * Format rubbed-out command line.
- * NOTE: This formatting will never be truncated since we're
- * writing into the pad which is large enough.
- */
- teco_interface.cmdline_rubout_len =
- teco_curses_format_str(teco_interface.cmdline_pad, cmdline->str.data + cmdline->effective_len,
- cmdline->str.len - cmdline->effective_len, -1);
-
- /*
- * Highlight cursor after effective command line
- * FIXME: This should use SCI_GETCARETFORE().
- */
- attr_t attr = A_NORMAL;
- short pair = 0;
- if (teco_interface.cmdline_rubout_len) {
- wmove(teco_interface.cmdline_pad, 0, teco_interface.cmdline_len);
- wattr_get(teco_interface.cmdline_pad, &attr, &pair, NULL);
- wchgat(teco_interface.cmdline_pad, 1,
- (attr & (A_UNDERLINE | A_REVERSE)) ^ A_REVERSE, pair, NULL);
- } else {
- teco_interface.cmdline_len++;
- wattr_get(teco_interface.cmdline_pad, &attr, &pair, NULL);
- wattr_set(teco_interface.cmdline_pad, (attr & ~(A_UNDERLINE | A_BOLD)) ^ A_REVERSE, pair, NULL);
- waddch(teco_interface.cmdline_pad, ' ');
- }
-
- teco_interface_draw_cmdline();
-}
-
-static void
-teco_interface_draw_cmdline(void)
-{
- /* total width available for command line */
- guint total_width = getmaxx(teco_interface.cmdline_window) - 1;
-
- /* beginning of command line to show */
- guint disp_offset = teco_interface.cmdline_len -
- MIN(teco_interface.cmdline_len,
- total_width/2 + teco_interface.cmdline_len % MAX(total_width/2, 1));
- /*
- * length of command line to show
- *
- * NOTE: we do not use getmaxx(cmdline_pad) here since it may be
- * larger than the text the pad contains.
- */
- guint disp_len = MIN(total_width, teco_interface.cmdline_len +
- teco_interface.cmdline_rubout_len - disp_offset);
-
- short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
- short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
-
- wattrset(teco_interface.cmdline_window, teco_color_attr(fg, bg));
- mvwaddch(teco_interface.cmdline_window, 0, 0, '*' | A_BOLD);
- teco_curses_clrtobot(teco_interface.cmdline_window);
- copywin(teco_interface.cmdline_pad, teco_interface.cmdline_window,
- 0, disp_offset, 0, 1, 0, disp_len, FALSE);
-}
-
#if PDCURSES
/*
@@ -1243,7 +1251,7 @@ teco_interface_init_clipboard(void)
if (rc == PDC_CLIP_SUCCESS)
PDC_freeclipboard(contents);
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
}
gboolean
@@ -1307,8 +1315,10 @@ get_selection_by_name(const gchar *name)
* (everything gets passed down), but currently we
* only register the three standard registers
* "~", "~P", "~S" and "~C".
+ * (We are never called with "~", though.)
*/
- return g_ascii_tolower(*name) ? : 'c';
+ g_assert(*name != '\0');
+ return g_ascii_tolower(*name);
}
/*
@@ -1507,10 +1517,10 @@ teco_interface_init_clipboard(void)
!teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECO_CLIPBOARD_GET", 22)))
return;
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P"));
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S"));
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C"));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("P"));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("S"));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("C"));
}
gboolean
@@ -1520,7 +1530,7 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
if (teco_interface_osc52_is_enabled())
return teco_interface_osc52_set_clipboard(name, str, str_len, error);
- static const gchar *reg_name = "$SCITECO_CLIPBOARD_SET";
+ static const gchar reg_name[] = "$SCITECO_CLIPBOARD_SET";
teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, reg_name, strlen(reg_name));
if (!reg) {
@@ -1533,7 +1543,7 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
g_auto(teco_string_t) command;
if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error))
return FALSE;
- if (teco_string_contains(&command, '\0')) {
+ if (teco_string_contains(command, '\0')) {
teco_error_qregcontainsnull_set(error, reg_name, strlen(reg_name), FALSE);
return FALSE;
}
@@ -1581,7 +1591,7 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError
if (teco_interface_osc52_is_enabled())
return teco_interface_osc52_get_clipboard(name, str, len, error);
- static const gchar *reg_name = "$SCITECO_CLIPBOARD_GET";
+ static const gchar reg_name[] = "$SCITECO_CLIPBOARD_GET";
teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, reg_name, strlen(reg_name));
if (!reg) {
@@ -1594,7 +1604,7 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError
g_auto(teco_string_t) command;
if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error))
return FALSE;
- if (teco_string_contains(&command, '\0')) {
+ if (teco_string_contains(command, '\0')) {
teco_error_qregcontainsnull_set(error, reg_name, strlen(reg_name), FALSE);
return FALSE;
}
@@ -1680,7 +1690,7 @@ void
teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len,
gboolean highlight)
{
- if (teco_interface.cmdline_window)
+ if (teco_interface.input_pad)
/* interactive mode */
teco_curses_info_popup_add(&teco_interface.popup, type, name, name_len, highlight);
}
@@ -1688,7 +1698,7 @@ teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize
void
teco_interface_popup_show(gsize prefix_len)
{
- if (!teco_interface.cmdline_window)
+ if (!teco_interface.input_pad)
/* batch mode */
return;
@@ -1702,7 +1712,7 @@ teco_interface_popup_show(gsize prefix_len)
void
teco_interface_popup_scroll(void)
{
- if (!teco_interface.cmdline_window)
+ if (!teco_interface.input_pad)
/* batch mode */
return;
@@ -1757,8 +1767,8 @@ teco_interface_is_interrupted(void)
* filtering out CTRL+C.
* It's currently necessary as a fallback e.g. for PDCURSES_GUI or XCurses.
*
- * NOTE: Theoretically, this can be optimized by doing wgetch() only every X
- * microseconds like on Gtk+.
+ * NOTE: Theoretically, this can be optimized by doing wgetch() only every
+ * TECO_POLL_INTERVAL microseconds like on Gtk+.
* But this turned out to slow things down, at least on PDCurses/WinGUI.
*/
gboolean
@@ -1786,9 +1796,22 @@ teco_interface_is_interrupted(void)
#endif
-static void
-teco_interface_refresh(void)
+void
+teco_interface_refresh(gboolean force)
{
+ if (!teco_interface.input_pad)
+ /* batch mode */
+ return;
+
+#ifdef NETBSD_CURSES
+ /* works around crashes in doupdate() */
+ if (G_UNLIKELY(COLS <= 1 || LINES <= 1))
+ return;
+#endif
+
+ if (G_UNLIKELY(force))
+ clearok(curscr, TRUE);
+
/*
* Info window is updated very often which is very
* costly, especially when using PDC_set_title(),
@@ -1799,7 +1822,7 @@ teco_interface_refresh(void)
wnoutrefresh(teco_interface.info_window);
teco_view_noutrefresh(teco_interface_current_view);
wnoutrefresh(teco_interface.msg_window);
- wnoutrefresh(teco_interface.cmdline_window);
+ teco_view_noutrefresh(teco_cmdline.view);
teco_curses_info_popup_noutrefresh(&teco_interface.popup);
doupdate();
}
@@ -1813,42 +1836,50 @@ teco_interface_refresh(void)
(BUTTON1_##X | BUTTON2_##X | BUTTON3_##X | BUTTON4_##X | BUTTON5_##X)
static gboolean
-teco_interface_getmouse(GError **error)
+teco_interface_process_mevent(MEVENT *event, GError **error)
{
- MEVENT event;
-
- if (getmouse(&event) != OK)
- return TRUE;
+#ifdef DEBUG
+ g_printf("EVENT: 0x%016X -> bit %02d [%c%c%c%c%c]\n",
+ event->bstate, ffs(event->bstate)-1,
+ event->bstate & BUTTON_NUM(4) ? 'U' : ' ',
+ event->bstate & BUTTON_NUM(5) ? 'D' : ' ',
+ event->bstate & BUTTON_EVENT(PRESSED) ? 'P' : ' ',
+ event->bstate & BUTTON_EVENT(RELEASED) ? 'R' : ' ',
+ event->bstate & REPORT_MOUSE_POSITION ? 'M' : ' ');
+#endif
if (teco_curses_info_popup_is_shown(&teco_interface.popup) &&
- wmouse_trafo(teco_interface.popup.window, &event.y, &event.x, FALSE)) {
+ wmouse_trafo(teco_interface.popup.window, &event->y, &event->x, FALSE)) {
/*
* NOTE: Not all curses variants report the RELEASED event,
* but may also return REPORT_MOUSE_POSITION.
* So we might react to all button presses as well.
- * Others will still report CLICKED.
*/
- if (event.bstate & (BUTTON1_RELEASED | BUTTON1_CLICKED | REPORT_MOUSE_POSITION)) {
+ if (event->bstate & (BUTTON1_RELEASED | REPORT_MOUSE_POSITION)) {
teco_machine_t *machine = &teco_cmdline.machine.parent;
- const teco_string_t *insert = teco_curses_info_popup_getentry(&teco_interface.popup, event.y, event.x);
+ const teco_string_t *insert = teco_curses_info_popup_getentry(&teco_interface.popup,
+ event->y, event->x);
if (insert && machine->current->insert_completion_cb) {
- /* successfully clicked popup item */
+ /*
+ * Successfully clicked popup item.
+ * `insert` is the empty string for the "(Unnamed)" buffer.
+ */
const teco_string_t insert_suffix = {insert->data + teco_interface.popup_prefix_len,
insert->len - teco_interface.popup_prefix_len};
- if (!machine->current->insert_completion_cb(machine, &insert_suffix, error))
+ if (!machine->current->insert_completion_cb(machine, insert_suffix, error))
return FALSE;
teco_interface_popup_clear();
teco_interface_msg_clear();
- teco_interface_cmdline_update(&teco_cmdline);
+ teco_cmdline_update();
}
return TRUE;
}
- if (event.bstate & BUTTON_NUM(4))
+ if (event->bstate & BUTTON_NUM(4))
teco_curses_info_popup_scroll(&teco_interface.popup, -2);
- else if (event.bstate & BUTTON_NUM(5))
+ else if (event->bstate & BUTTON_NUM(5))
teco_curses_info_popup_scroll(&teco_interface.popup, +2);
short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_CALLTIP, 0));
@@ -1859,106 +1890,144 @@ teco_interface_getmouse(GError **error)
}
/*
- * Return mouse coordinates relative to the view.
- * They will be in characters, but that's what SCI_POSITIONFROMPOINT
- * expects on Scinterm anyway.
- */
- WINDOW *current = teco_view_get_window(teco_interface_current_view);
- if (!wmouse_trafo(current, &event.y, &event.x, FALSE))
- /* no event inside of current view */
- return TRUE;
-
- /*
* NOTE: There will only be one of the button bits
* set in bstate, so we don't loose information translating
* them to enums.
*
- * At least on ncurses, we don't always get a RELEASED event.
- * It instead sends only REPORT_MOUSE_POSITION,
- * so make sure not to overwrite teco_mouse.button in this case.
+ * At least on ncurses, this enables the "Normal tracking mode"
+ * which only reports PRESSEND and RELEASED events, but no mouse
+ * position tracing.
*/
- if (event.bstate & BUTTON_NUM(4))
+ if (event->bstate & BUTTON_NUM(4))
/* scroll up - there will be no RELEASED event */
teco_mouse.type = TECO_MOUSE_SCROLLUP;
- else if (event.bstate & BUTTON_NUM(5))
+ else if (event->bstate & BUTTON_NUM(5))
/* scroll down - there will be no RELEASED event */
teco_mouse.type = TECO_MOUSE_SCROLLDOWN;
- else if (event.bstate & BUTTON_EVENT(RELEASED))
+ else if (event->bstate & BUTTON_EVENT(RELEASED))
teco_mouse.type = TECO_MOUSE_RELEASED;
- else if (event.bstate & BUTTON_EVENT(PRESSED))
+ else if (event->bstate & BUTTON_EVENT(PRESSED))
teco_mouse.type = TECO_MOUSE_PRESSED;
else
- /* can also be REPORT_MOUSE_POSITION */
- teco_mouse.type = TECO_MOUSE_RELEASED;
+ return TRUE;
- teco_mouse.x = event.x;
- teco_mouse.y = event.y;
+ /*
+ * Return mouse coordinates relative to the view.
+ * They will be in characters, but that's what SCI_POSITIONFROMPOINT
+ * expects on Scinterm anyway.
+ */
+ WINDOW *current = teco_view_get_window(teco_interface_current_view);
+ if (!wmouse_trafo(current, &event->y, &event->x, FALSE))
+ /* no event inside of current view */
+ return TRUE;
- if (event.bstate & BUTTON_NUM(1))
+ teco_mouse.x = event->x;
+ teco_mouse.y = event->y;
+
+ if (event->bstate & BUTTON_NUM(1))
teco_mouse.button = 1;
- else if (event.bstate & BUTTON_NUM(2))
+ else if (event->bstate & BUTTON_NUM(2))
teco_mouse.button = 2;
- else if (event.bstate & BUTTON_NUM(3))
+ else if (event->bstate & BUTTON_NUM(3))
teco_mouse.button = 3;
- else if (!(event.bstate & REPORT_MOUSE_POSITION))
+ else if (!(event->bstate & REPORT_MOUSE_POSITION))
teco_mouse.button = -1;
teco_mouse.mods = 0;
- if (event.bstate & BUTTON_SHIFT)
+ if (event->bstate & BUTTON_SHIFT)
teco_mouse.mods |= TECO_MOUSE_SHIFT;
- if (event.bstate & BUTTON_CTRL)
+ if (event->bstate & BUTTON_CTRL)
teco_mouse.mods |= TECO_MOUSE_CTRL;
- if (event.bstate & BUTTON_ALT)
+ if (event->bstate & BUTTON_ALT)
teco_mouse.mods |= TECO_MOUSE_ALT;
- if (event.bstate & BUTTON_EVENT(CLICKED)) {
- /*
- * Click detection __should__ be disabled,
- * but some Curses implementations report them anyway.
- * This has been observed on PDCurses/WinGUI.
- * On PDCurses/Wincon we especially did not disable
- * click detection since it doesn't report
- * BUTTONX_RELEASED at all.
- * We emulate separate PRESSED/RELEASE events on those
- * platforms.
- */
- teco_mouse.type = TECO_MOUSE_PRESSED;
- if (!teco_cmdline_keymacro("MOUSE", -1, error))
- return FALSE;
- teco_mouse.type = TECO_MOUSE_RELEASED;
+#if defined(NCURSES_UNIX) && NCURSES_VERSION_PATCH < 20250913
+ /*
+ * FIXME: Some terminal emulators do not send separate
+ * middle click PRESSED and RELEASED buttons
+ * (both are sent when releasing the button).
+ * Furthermore due to ncurses bugs the order
+ * of events is arbitrary.
+ * Therefore we ignore BUTTON2_PRESSED and synthesize
+ * PRESSED and RELEASED evnts on BUTTON2_RELEASED:
+ */
+ if (teco_mouse.button == 2) {
+ if (teco_mouse.type == TECO_MOUSE_PRESSED)
+ /* ignore BUTTON2_PRESSED events */
+ return TRUE;
+ if (teco_mouse.type == TECO_MOUSE_RELEASED) {
+ teco_mouse.type = TECO_MOUSE_PRESSED;
+ if (!teco_cmdline_keymacro("MOUSE", -1, error))
+ return FALSE;
+ teco_mouse.type = TECO_MOUSE_RELEASED;
+ }
}
+#endif /* NCURSES_UNIX && NCURSES_VERSION_PATCH < 20250913 */
+
return teco_cmdline_keymacro("MOUSE", -1, error);
}
+static gboolean
+teco_interface_getmouse(GError **error)
+{
+ MEVENT event;
+
+ while (getmouse(&event) == OK)
+ if (!teco_interface_process_mevent(&event, error))
+ return FALSE;
+
+ return TRUE;
+}
+
#endif /* NCURSES_MOUSE_VERSION >= 2 */
static gint
teco_interface_blocking_getch(void)
{
+ if (!g_queue_is_empty(teco_interface.input_queue))
+ return GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue));
+
#if NCURSES_MOUSE_VERSION >= 2
-#if defined(NCURSES_VERSION) || defined(PDCURSES_WINCON)
/*
- * REPORT_MOUSE_POSITION is necessary at least on
- * ncurses, so that BUTTONX_RELEASED events are reported.
- * At least we interpret REPORT_MOUSE_POSITION
- * like BUTTONX_RELEASED.
- * It does NOT report every cursor movement, though.
- *
- * FIXME: On PDCurses/Wincon we enable it, so we at least
- * receive something that will be interpreted as
- * BUTTONX_RELEASED, although it really just reports
- * cursor movements.
- */
- static const mmask_t mmask = ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION;
-#else
- static const mmask_t mmask = ALL_MOUSE_EVENTS;
+ * FIXME: Due to an ncurses bug, we can receive bogus BUTTON3_PRESSED events after
+ * left scrolling.
+ * If we would reset the mouse mask with every wgetch(), which resets
+ * the internal button state, we would receive bogus BUTTON3_PRESSED events
+ * repeatedly.
+ * An upstream ncurses patch will probably be merged soon.
+ */
+ static gboolean old_mousekey = FALSE;
+ gboolean new_mousekey = (teco_ed & TECO_ED_MOUSEKEY) != 0;
+ if (new_mousekey != old_mousekey) {
+ old_mousekey = new_mousekey;
+ mmask_t mmask = BUTTON_EVENT(PRESSED) | BUTTON_EVENT(RELEASED);
+#ifdef __PDCURSES__
+ /*
+ * On PDCurses it's crucial NOT to mask for BUTTONX_CLICKED.
+ * Also, scroll events are not reported without the non-standard
+ * MOUSE_WHEEL_SCROLL.
+ */
+ mmask |= MOUSE_WHEEL_SCROLL;
#endif
- mousemask(teco_ed & TECO_ED_MOUSEKEY ? mmask : 0, NULL);
+ mousemask(new_mousekey ? mmask : 0, NULL);
+ }
#endif /* NCURSES_MOUSE_VERSION >= 2 */
/* no special <CTRL/C> handling */
raw();
nodelay(teco_interface.input_pad, FALSE);
+
+ /*
+ * Make sure we return when it's time to create recovery dumps.
+ */
+ if (teco_ring_recovery_interval != 0) {
+ if (G_UNLIKELY(!teco_interface.recovery_timer))
+ teco_interface.recovery_timer = g_timer_new();
+ gdouble elapsed = g_timer_elapsed(teco_interface.recovery_timer, NULL);
+ wtimeout(teco_interface.input_pad,
+ MAX((gdouble)teco_ring_recovery_interval - elapsed, 0)*1000);
+ }
+
/*
* Memory limiting is stopped temporarily, since it might otherwise
* constantly place 100% load on the CPU.
@@ -1974,6 +2043,12 @@ teco_interface_blocking_getch(void)
cbreak();
#endif
+ if (key == ERR && teco_ring_recovery_interval != 0 &&
+ g_timer_elapsed(teco_interface.recovery_timer, NULL) >= teco_ring_recovery_interval) {
+ teco_ring_dump_recovery();
+ g_timer_start(teco_interface.recovery_timer);
+ }
+
return key;
}
@@ -1995,12 +2070,11 @@ teco_interface_event_loop_iter(void)
GError **error = &teco_interface.event_loop_error;
- gint key = g_queue_is_empty(teco_interface.input_queue)
- ? teco_interface_blocking_getch()
- : GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue));
+ gint key = teco_interface_blocking_getch();
const teco_view_t *last_view = teco_interface_current_view;
sptr_t last_vpos = teco_interface_ssm(SCI_GETFIRSTVISIBLELINE, 0, 0);
+ guint last_cmdline_height = teco_cmdline.height;
switch (key) {
case ERR:
@@ -2008,10 +2082,11 @@ teco_interface_event_loop_iter(void)
return;
#ifdef KEY_RESIZE
case KEY_RESIZE:
-#ifdef __PDCURSES__
- /* NOTE: No longer necessary since PDCursesMod v4.3.3. */
- resize_term(0, 0);
-#endif
+ /*
+ * At least on PDCurses/Wincon, the hardware cursor is sometimes
+ * reactivated.
+ */
+ curs_set(0);
teco_interface_resize_all_windows();
break;
#endif
@@ -2078,9 +2153,10 @@ teco_interface_event_loop_iter(void)
* Do not auto-scroll on mouse events, so you can scroll the view manually
* in the ^KMOUSE macro, allowing dot to be outside of the view.
*/
- teco_interface_refresh();
+ teco_interface_unfold();
+ teco_interface_refresh(FALSE);
return;
-#endif
+#endif /* NCURSES_MOUSE_VERSION >= 2 */
/*
* Control keys and keys with printable representation
@@ -2125,6 +2201,10 @@ teco_interface_event_loop_iter(void)
}
}
+ if (G_UNLIKELY(teco_cmdline.height != last_cmdline_height))
+ /* command line height was changed with h,5EJ */
+ teco_interface_resize_all_windows();
+
/*
* Scintilla has been patched to avoid any automatic scrolling since that
* has been benchmarked to be a very costly operation.
@@ -2134,9 +2214,10 @@ teco_interface_event_loop_iter(void)
*/
if (teco_interface_current_view == last_view)
teco_interface_ssm(SCI_SETFIRSTVISIBLELINE, last_vpos, 0);
+ teco_interface_unfold();
teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
- teco_interface_refresh();
+ teco_interface_refresh(FALSE);
}
gboolean
@@ -2148,11 +2229,14 @@ teco_interface_event_loop(GError **error)
if (!teco_interface_init_interactive(error))
return FALSE;
- static const teco_cmdline_t empty_cmdline; // FIXME
- teco_interface_cmdline_update(&empty_cmdline);
teco_interface_msg_clear();
teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
- teco_interface_refresh();
+ /*
+ * NetBSD's Curses needs the hard refresh as it would
+ * otherwise draw the info window in the wrong row.
+ * Shouldn't cause any slowdown on ncurses.
+ */
+ teco_interface_refresh(TRUE);
#ifdef EMCURSES
PDC_emscripten_set_handler(teco_interface_event_loop_iter, TRUE);
@@ -2199,10 +2283,6 @@ teco_interface_cleanup(void)
teco_string_clear(&teco_interface.info_current);
if (teco_interface.input_queue)
g_queue_free(teco_interface.input_queue);
- if (teco_interface.cmdline_window)
- delwin(teco_interface.cmdline_window);
- if (teco_interface.cmdline_pad)
- delwin(teco_interface.cmdline_pad);
if (teco_interface.msg_window)
delwin(teco_interface.msg_window);
if (teco_interface.input_pad)
@@ -2227,4 +2307,10 @@ teco_interface_cleanup(void)
close(teco_interface.stderr_orig);
if (teco_interface.stdout_orig >= 0)
close(teco_interface.stdout_orig);
+
+ if (teco_interface.pair_table)
+ g_hash_table_destroy(teco_interface.pair_table);
+
+ if (teco_interface.recovery_timer)
+ g_timer_destroy(teco_interface.recovery_timer);
}
diff --git a/src/interface-gtk/gtk-info-popup.c b/src/interface-gtk/gtk-info-popup.c
index aaa0a65..769f772 100644
--- a/src/interface-gtk/gtk-info-popup.c
+++ b/src/interface-gtk/gtk-info-popup.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -37,6 +37,7 @@ typedef struct {
teco_stailq_entry_t entry;
teco_popup_entry_type_t type;
+ /** entry name or empty string for the "(Unnamed)" buffer */
teco_string_t name;
gboolean highlight;
} teco_popup_entry_t;
@@ -109,10 +110,10 @@ teco_gtk_info_popup_activated_cb(GtkFlowBox *box, GtkFlowBoxChild *child, gpoint
GList *entry;
for (entry = child_list; entry != NULL && !TECO_IS_GTK_LABEL(entry->data); entry = g_list_next(entry));
g_assert(entry != NULL);
- const teco_string_t *str = teco_gtk_label_get_text(TECO_GTK_LABEL(entry->data));
+ teco_string_t str = teco_gtk_label_get_text(TECO_GTK_LABEL(entry->data));
g_signal_emit(popup, teco_gtk_info_popup_clicked_signal, 0,
- str->data, (gulong)str->len);
+ str.data, (gulong)str.len);
}
static void
@@ -249,6 +250,10 @@ teco_gtk_info_popup_new(void)
GIcon *
teco_gtk_info_popup_get_icon_for_path(const gchar *path, const gchar *fallback_name)
{
+ if (!path || !*path)
+ /* "(Unnamed)" file */
+ return g_icon_new_for_string(fallback_name, NULL);
+
GIcon *icon = NULL;
g_autoptr(GFile) file = g_file_new_for_path(path);
@@ -299,7 +304,7 @@ teco_gtk_info_popup_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t type,
static void
teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t type,
- const gchar *name, gssize len, gboolean highlight)
+ const gchar *name, gsize len, gboolean highlight)
{
g_return_if_fail(self != NULL);
g_return_if_fail(TECO_IS_GTK_INFO_POPUP(self));
@@ -318,12 +323,8 @@ teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t typ
const gchar *fallback = type == TECO_POPUP_FILE ? "text-x-generic"
: "folder";
- /*
- * `name` is not guaranteed to be null-terminated.
- */
- g_autofree gchar *path = len < 0 ? g_strdup(name) : g_strndup(name, len);
-
- g_autoptr(GIcon) icon = teco_gtk_info_popup_get_icon_for_path(path, fallback);
+ /* name comes from a teco_string_t and is guaranteed to be null-terminated */
+ g_autoptr(GIcon) icon = teco_gtk_info_popup_get_icon_for_path(name, fallback);
if (icon) {
gint width, height;
gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
diff --git a/src/interface-gtk/gtk-info-popup.h b/src/interface-gtk/gtk-info-popup.h
index ad79b84..3ce8e1f 100644
--- a/src/interface-gtk/gtk-info-popup.h
+++ b/src/interface-gtk/gtk-info-popup.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/interface-gtk/gtk-label.c b/src/interface-gtk/gtk-label.c
index ef370a2..5052cdc 100644
--- a/src/interface-gtk/gtk-label.c
+++ b/src/interface-gtk/gtk-label.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -32,7 +32,9 @@
#include "gtk-label.h"
-#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16))
+#define TECO_UNNAMED_FILE "(Unnamed)"
+
+#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16))
struct _TecoGtkLabel {
GtkLabel parent_instance;
@@ -40,6 +42,7 @@ struct _TecoGtkLabel {
PangoColor fg, bg;
guint16 fg_alpha, bg_alpha;
+ /** text backing the label or empty string for "(Unnamed)" buffer */
teco_string_t string;
};
@@ -143,7 +146,6 @@ teco_gtk_label_add_highlight_attribs(PangoAttrList *attribs, PangoColor *fg, gui
* even in Pango v1.38.
* Perhaps, this has been fixed in later versions.
*/
-#if PANGO_VERSION_CHECK(1,38,0)
attr = pango_attr_foreground_alpha_new(fg_alpha);
attr->start_index = index;
attr->end_index = index + len;
@@ -153,7 +155,6 @@ teco_gtk_label_add_highlight_attribs(PangoAttrList *attribs, PangoColor *fg, gui
attr->start_index = index;
attr->end_index = index + len;
pango_attr_list_insert(attribs, attr);
-#endif
attr = pango_attr_foreground_new(fg->red, fg->green, fg->blue);
attr->start_index = index;
@@ -253,25 +254,52 @@ teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len)
teco_string_clear(&self->string);
teco_string_init(&self->string, str, len < 0 ? strlen(str) : len);
- g_autofree gchar *plaintext = NULL;
+ teco_string_t string = self->string;
+ if (!string.len) {
+ string.data = TECO_UNNAMED_FILE;
+ string.len = strlen(string.data);
+ }
- if (self->string.len > 0) {
- PangoAttrList *attribs = NULL;
+ g_autofree gchar *plaintext = NULL;
+ PangoAttrList *attribs = NULL;
- teco_gtk_label_parse_string(self->string.data, self->string.len,
- &self->fg, self->fg_alpha,
- &self->bg, self->bg_alpha,
- &attribs, &plaintext);
+ teco_gtk_label_parse_string(string.data, string.len,
+ &self->fg, self->fg_alpha,
+ &self->bg, self->bg_alpha,
+ &attribs, &plaintext);
- gtk_label_set_attributes(GTK_LABEL(self), attribs);
- pango_attr_list_unref(attribs);
- }
+ gtk_label_set_attributes(GTK_LABEL(self), attribs);
+ pango_attr_list_unref(attribs);
gtk_label_set_text(GTK_LABEL(self), plaintext);
}
-const teco_string_t *
+teco_string_t
teco_gtk_label_get_text(TecoGtkLabel *self)
{
- return &self->string;
+ return self->string;
+}
+
+/**
+ * Signal that a keypress is expected (after executing ^T)
+ * by printing the first character in reverse.
+ *
+ * @fixme This mimics the current Curses implementation.
+ * Perhaps better show an icon?
+ */
+void
+teco_gtk_label_highlight_getch(TecoGtkLabel *self)
+{
+ const gchar *plaintext = gtk_label_get_text(GTK_LABEL(self));
+ g_assert(plaintext != NULL);
+ if (!*plaintext || !strcmp(plaintext, "\u258C")) {
+ gtk_label_set_text(GTK_LABEL(self), "\u258C");
+ } else {
+ PangoAttrList *attribs = gtk_label_get_attributes(GTK_LABEL(self));
+ teco_gtk_label_add_highlight_attribs(attribs,
+ &self->fg, self->fg_alpha,
+ &self->bg, self->bg_alpha,
+ 0, 1);
+ gtk_label_set_attributes(GTK_LABEL(self), attribs);
+ }
}
diff --git a/src/interface-gtk/gtk-label.h b/src/interface-gtk/gtk-label.h
index c52d073..ad39c6e 100644
--- a/src/interface-gtk/gtk-label.h
+++ b/src/interface-gtk/gtk-label.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -27,9 +27,11 @@ G_DECLARE_FINAL_TYPE(TecoGtkLabel, teco_gtk_label, TECO, GTK_LABEL, GtkLabel)
GtkWidget *teco_gtk_label_new(const gchar *str, gssize len);
void teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len);
-const teco_string_t *teco_gtk_label_get_text(TecoGtkLabel *self);
+teco_string_t teco_gtk_label_get_text(TecoGtkLabel *self);
void teco_gtk_label_parse_string(const gchar *str, gssize len,
PangoColor *fg, guint16 fg_alpha,
PangoColor *bg, guint16 bg_alpha,
PangoAttrList **attribs, gchar **text);
+
+void teco_gtk_label_highlight_getch(TecoGtkLabel *self);
diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c
index 045f9d7..0e1507f 100644
--- a/src/interface-gtk/interface.c
+++ b/src/interface-gtk/interface.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -19,7 +19,7 @@
#include "config.h"
#endif
-#include <stdarg.h>
+#include <stdlib.h>
#include <string.h>
#include <signal.h>
@@ -28,6 +28,7 @@
#include <glib/gstdio.h>
#ifdef G_OS_UNIX
+#include <unistd.h>
#include <glib-unix.h>
#endif
@@ -61,6 +62,7 @@
//#define DEBUG
static gboolean teco_interface_busy_timeout_cb(gpointer user_data);
+static gboolean teco_interface_dump_recovery_cb(gpointer user_data);
static void teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data);
static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
GdkRectangle *allocation,
@@ -69,33 +71,22 @@ static void teco_interface_cmdline_commit_cb(GtkIMContext *context, gchar *str,
gpointer user_data);
static gboolean teco_interface_input_cb(GtkWidget *widget, GdkEvent *event,
gpointer user_data);
+static void teco_interface_scroll_cb(GtkEventControllerScroll *controller,
+ double dx, double dy, gpointer data);
static void teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len,
gpointer user_data);
static gboolean teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event,
gpointer user_data);
static gboolean teco_interface_sigterm_handler(gpointer user_data) G_GNUC_UNUSED;
+static gchar teco_interface_get_ansi_key(GdkEventKey *event);
-/**
- * Interval between polling for keypresses.
- * In other words, this is the maximum latency to detect CTRL+C interruptions.
- */
-#define TECO_POLL_INTERVAL 100000 /* microseconds */
-
-#define UNNAMED_FILE "(Unnamed)"
+#define TECO_UNNAMED_FILE "(Unnamed)"
#define USER_CSS_FILE ".teco_css"
/** printf() format for CSS RGB colors given as guint32 */
#define CSS_COLOR_FORMAT "#%06" G_GINT32_MODIFIER "X"
-/** Style used for the asterisk at the beginning of the command line */
-#define STYLE_ASTERISK 16
-
-/** Indicator number used for control characters in the command line */
-#define INDIC_CONTROLCHAR (INDIC_CONTAINER+0)
-/** Indicator number used for the rubbed out part of the command line */
-#define INDIC_RUBBEDOUT (INDIC_CONTAINER+1)
-
/** Convert Scintilla-style BGR color triple to RGB. */
static inline guint32
teco_bgr2rgb(guint32 bgr)
@@ -113,9 +104,10 @@ static struct {
TECO_INFO_TYPE_BUFFER_DIRTY,
TECO_INFO_TYPE_QREG
} info_type;
+ /* current document's name or empty string for "(Unnamed)" buffer */
teco_string_t info_current;
- gboolean no_csd;
+ gboolean no_csd, detach;
gint xembed_id;
GtkWidget *info_bar_widget;
@@ -124,11 +116,11 @@ static struct {
GtkWidget *info_name_widget;
GtkWidget *event_box_widget;
+ GtkEventController *scroll_controller;
GtkWidget *message_bar_widget;
GtkWidget *message_widget;
- teco_view_t *cmdline_view;
GtkIMContext *input_method;
GtkWidget *popup_widget;
@@ -142,6 +134,26 @@ static struct {
void
teco_interface_init(void)
{
+#ifdef G_OS_UNIX
+ if (teco_interface.detach) {
+ /*
+ * NOTE: There is also daemon() on BSD/Linux,
+ * but the following should be more portable.
+ */
+ pid_t pid = fork();
+ g_assert(pid >= 0);
+ if (pid != 0)
+ /* parent process */
+ exit(EXIT_SUCCESS);
+
+ setsid();
+
+ g_freopen("/dev/null", "r", stdin);
+ g_freopen("/dev/null", "a+", stdout);
+ g_freopen("/dev/null", "a+", stderr);
+ }
+#endif
+
/*
* gtk_init() is not necessary when using gtk_get_option_group(),
* but this will open the default display.
@@ -157,10 +169,10 @@ teco_interface_init(void)
* clipboards/selections are supported on this system,
* so we register only some default ones.
*/
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P"));
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S"));
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C"));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("P"));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("S"));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("C"));
teco_interface.event_queue = g_queue_new();
@@ -190,7 +202,7 @@ teco_interface_init(void)
*/
teco_interface.info_bar_widget = gtk_header_bar_new();
gtk_widget_set_name(teco_interface.info_bar_widget, "sciteco-info-bar");
- teco_interface.info_name_widget = teco_gtk_label_new(NULL, 0);
+ teco_interface.info_name_widget = teco_gtk_label_new("", 0);
gtk_widget_set_valign(teco_interface.info_name_widget, GTK_ALIGN_CENTER);
/* eases writing portable fallback.css that avoids CSS element names */
gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget),
@@ -230,7 +242,7 @@ teco_interface_init(void)
/*
* Overlay widget will allow overlaying the Scintilla view
* and message widgets with the info popup.
- * Therefore overlay_vbox (containing the view and popup)
+ * Therefore overlay_vbox (containing the view and message line)
* will be the main child of the overlay.
*/
GtkWidget *overlay_widget = gtk_overlay_new();
@@ -253,22 +265,30 @@ teco_interface_init(void)
gint events = gtk_widget_get_events(teco_interface.event_box_widget);
events |= GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
- GDK_SCROLL_MASK;
+ GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK;
gtk_widget_set_events(teco_interface.event_box_widget, events);
g_signal_connect(teco_interface.event_box_widget, "button-press-event",
G_CALLBACK(teco_interface_input_cb), NULL);
g_signal_connect(teco_interface.event_box_widget, "button-release-event",
G_CALLBACK(teco_interface_input_cb), NULL);
- g_signal_connect(teco_interface.event_box_widget, "scroll-event",
- G_CALLBACK(teco_interface_input_cb), NULL);
+
+ /*
+ * On some platforms only GDK_SCROLL_SMOOTH events are reported, which are hard
+ * to translate to discrete scroll events, as required by the `4EJ` API.
+ * This work is therefore delegated to a scroll controller.
+ */
+ teco_interface.scroll_controller = gtk_event_controller_scroll_new(teco_interface.event_box_widget,
+ GTK_EVENT_CONTROLLER_SCROLL_VERTICAL |
+ GTK_EVENT_CONTROLLER_SCROLL_DISCRETE);
+ g_signal_connect(teco_interface.scroll_controller, "scroll",
+ G_CALLBACK(teco_interface_scroll_cb), NULL);
teco_interface.message_bar_widget = gtk_info_bar_new();
gtk_widget_set_name(teco_interface.message_bar_widget, "sciteco-message-bar");
GtkWidget *message_bar_content =
gtk_info_bar_get_content_area(GTK_INFO_BAR(teco_interface.message_bar_widget));
- /* NOTE: Messages are always pre-canonicalized */
- teco_interface.message_widget = gtk_label_new(NULL);
+ teco_interface.message_widget = teco_gtk_label_new(NULL, 0);
/* eases writing portable fallback.css that avoids CSS element names */
gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.message_widget),
"label");
@@ -281,23 +301,9 @@ teco_interface_init(void)
gtk_container_add(GTK_CONTAINER(overlay_widget), overlay_vbox);
gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0);
- teco_interface.cmdline_view = teco_view_new();
- teco_view_setup(teco_interface.cmdline_view);
- teco_view_ssm(teco_interface.cmdline_view, SCI_SETUNDOCOLLECTION, FALSE, 0);
- teco_view_ssm(teco_interface.cmdline_view, SCI_SETVSCROLLBAR, FALSE, 0);
- teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINTYPEN, 1, SC_MARGIN_TEXT);
- teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETSTYLE, 0, STYLE_ASTERISK);
- teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINWIDTHN, 1,
- teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTWIDTH, STYLE_ASTERISK, (sptr_t)"*"));
- teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*");
- /* only required as long as we avoid ordinary character representations */
- teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_CONTROLCHAR, INDIC_ROUNDBOX);
- teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETALPHA, INDIC_CONTROLCHAR, 128);
- teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_RUBBEDOUT, INDIC_STRIKE);
- /* we will forward key events, so the view should only react to text insertion */
- teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALLCMDKEYS, 0, 0);
-
- GtkWidget *cmdline_widget = GTK_WIDGET(teco_interface.cmdline_view);
+ teco_cmdline_init();
+
+ GtkWidget *cmdline_widget = GTK_WIDGET(teco_cmdline.view);
gtk_widget_set_name(cmdline_widget, "sciteco-cmdline");
g_signal_connect(cmdline_widget, "size-allocate",
G_CALLBACK(teco_interface_cmdline_size_allocate_cb), NULL);
@@ -332,10 +338,6 @@ teco_interface_init(void)
*/
gtk_widget_set_can_focus(teco_interface.message_widget, FALSE);
gtk_widget_set_can_focus(teco_interface.info_name_widget, FALSE);
-
- teco_cmdline_t empty_cmdline;
- memset(&empty_cmdline, 0, sizeof(empty_cmdline));
- teco_interface_cmdline_update(&empty_cmdline);
}
static void
@@ -361,6 +363,11 @@ teco_interface_get_options(void)
G_OPTION_ARG_INT, &teco_interface.xembed_id,
"Embed into an existing X11 Window.", "ID"},
#endif
+#ifdef G_OS_UNIX
+ {"detach", 'd', G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, &teco_interface.detach,
+ "Detach from controlling terminal (daemonize).", NULL},
+#endif
{NULL}
};
@@ -379,7 +386,7 @@ teco_interface_get_options(void)
void teco_interface_init_color(guint color, guint32 rgb) {}
void
-teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len)
{
/*
* The message types are chosen such that there is a CSS class
@@ -395,21 +402,11 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
g_assert(type < G_N_ELEMENTS(type2gtk));
- gchar buf[256];
-
- /*
- * stdio_vmsg() leaves `ap` undefined and we are expected
- * to do the same and behave like vprintf().
- */
- va_list aq;
- va_copy(aq, ap);
- teco_interface_stdio_vmsg(type, fmt, ap);
- g_vsnprintf(buf, sizeof(buf), fmt, aq);
- va_end(aq);
+ teco_interface_stdio_msg(type, str, len);
gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget),
type2gtk[type]);
- gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), buf);
+ teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.message_widget), str, len);
if (type == TECO_MSG_ERROR)
gtk_widget_error_bell(teco_interface.window);
@@ -420,7 +417,100 @@ teco_interface_msg_clear(void)
{
gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget),
GTK_MESSAGE_QUESTION);
- gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), "");
+ teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.message_widget), "", 0);
+}
+
+static void
+teco_interface_getch_commit_cb(GtkIMContext *context, gchar *str, gpointer user_data)
+{
+ teco_int_t *cp = user_data;
+
+ /*
+ * FIXME: What if str contains several characters?
+ */
+ *cp = g_utf8_get_char_validated(str, -1);
+ g_assert(*cp >= 0);
+ gtk_main_quit();
+}
+
+static gboolean
+teco_interface_getch_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data)
+{
+ teco_int_t *cp = user_data;
+
+ g_assert(event->type == GDK_KEY_PRESS);
+
+ switch (event->key.keyval) {
+ case GDK_KEY_Escape: *cp = '\e'; break;
+ case GDK_KEY_BackSpace: *cp = TECO_CTL_KEY('H'); break;
+ case GDK_KEY_Tab: *cp = '\t'; break;
+ case GDK_KEY_Return: *cp = '\n'; break;
+ default:
+ /*
+ * NOTE: Alt-Gr key-combinations are sometimes reported as
+ * Ctrl+Alt, so we filter those out.
+ */
+ if ((event->key.state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) == GDK_CONTROL_MASK &&
+ (*cp = teco_interface_get_ansi_key(&event->key))) {
+ *cp = TECO_CTL_KEY(g_ascii_toupper(*cp));
+ switch (*cp) {
+ case TECO_CTL_KEY('C'):
+ teco_interrupted = TRUE;
+ /* fall through */
+ case TECO_CTL_KEY('D'):
+ *cp = -1;
+ }
+ break;
+ }
+
+ gtk_im_context_filter_keypress(teco_interface.input_method, &event->key);
+ return TRUE;
+ }
+
+ gtk_main_quit();
+ return TRUE;
+}
+
+teco_int_t
+teco_interface_getch(gboolean widechar)
+{
+ if (!gtk_main_level())
+ /* batch mode */
+ return teco_interface_stdio_getch(widechar);
+
+ teco_int_t cp = -1;
+ gulong key_handler, commit_handler;
+
+ /* temporarily replace the "key-press-event" and "commit" handlers */
+ g_signal_handlers_block_by_func(teco_interface.window,
+ teco_interface_input_cb, NULL);
+ key_handler = g_signal_connect(teco_interface.window, "key-press-event",
+ G_CALLBACK(teco_interface_getch_input_cb), &cp);
+ g_signal_handlers_block_by_func(teco_interface.input_method,
+ teco_interface_cmdline_commit_cb, NULL);
+ commit_handler = g_signal_connect(teco_interface.input_method, "commit",
+ G_CALLBACK(teco_interface_getch_commit_cb), &cp);
+
+ /*
+ * Highlights the first character in the label.
+ * This mimics what the Curses UI does.
+ * Is there a better way to signal that we expect input?
+ */
+ teco_gtk_label_highlight_getch(TECO_GTK_LABEL(teco_interface.message_widget));
+
+ GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window));
+ gdk_window_thaw_updates(top_window);
+
+ gtk_main();
+
+ gdk_window_freeze_updates(top_window);
+
+ g_signal_handler_disconnect(teco_interface.input_method, commit_handler);
+ g_signal_handlers_unblock_by_func(teco_interface.input_method, teco_interface_cmdline_commit_cb, NULL);
+ g_signal_handler_disconnect(teco_interface.window, key_handler);
+ g_signal_handlers_unblock_by_func(teco_interface.window, teco_interface_input_cb, NULL);
+
+ return cp;
}
void
@@ -439,8 +529,13 @@ teco_interface_refresh_info(void)
gtk_style_context_remove_class(style, "dirty");
g_auto(teco_string_t) info_current_temp;
- teco_string_init(&info_current_temp,
- teco_interface.info_current.data, teco_interface.info_current.len);
+
+ if (!teco_interface.info_current.len)
+ teco_string_init(&info_current_temp, TECO_UNNAMED_FILE, strlen(TECO_UNNAMED_FILE));
+ else
+ teco_string_init(&info_current_temp,
+ teco_interface.info_current.data, teco_interface.info_current.len);
+
if (teco_interface.info_type == TECO_INFO_TYPE_BUFFER_DIRTY)
teco_string_append_c(&info_current_temp, '*');
teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.info_name_widget),
@@ -508,92 +603,18 @@ teco_interface_info_update_qreg(const teco_qreg_t *reg)
void
teco_interface_info_update_buffer(const teco_buffer_t *buffer)
{
- const gchar *filename = buffer->filename ? : UNNAMED_FILE;
-
teco_string_clear(&teco_interface.info_current);
- teco_string_init(&teco_interface.info_current, filename, strlen(filename));
- teco_interface.info_type = buffer->dirty ? TECO_INFO_TYPE_BUFFER_DIRTY
- : TECO_INFO_TYPE_BUFFER;
-}
-
-/**
- * Insert a single character into the command line.
- *
- * @fixme
- * Control characters should be inserted verbatim since the Scintilla
- * representations of them should be preferred.
- * However, Scintilla would break the line on every CR/LF and there is
- * currently no way to prevent this.
- * Scintilla needs to be patched.
- *
- * @see teco_view_set_representations()
- * @see teco_curses_format_str()
- */
-static void
-teco_interface_cmdline_insert_c(gchar chr)
-{
- gchar buffer[3+1] = "";
-
- /*
- * NOTE: This mapping is similar to teco_view_set_representations()
- */
- switch (chr) {
- case '\e': strcpy(buffer, "$"); break;
- case '\r': strcpy(buffer, "CR"); break;
- case '\n': strcpy(buffer, "LF"); break;
- case '\t': strcpy(buffer, "TAB"); break;
- default:
- if (TECO_IS_CTL(chr)) {
- buffer[0] = '^';
- buffer[1] = TECO_CTL_ECHO(chr);
- buffer[2] = '\0';
- }
- }
-
- if (*buffer) {
- gsize len = strlen(buffer);
- teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, len, (sptr_t)buffer);
- teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_CONTROLCHAR, 0);
- teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE,
- teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - len, len);
- } else {
- teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, 1, (sptr_t)&chr);
- }
-}
-
-void
-teco_interface_cmdline_update(const teco_cmdline_t *cmdline)
-{
- /*
- * We don't know if the new command line is similar to
- * the old one, so we can just as well rebuild it.
- *
- * NOTE: teco_view_ssm() already locks the GDK lock.
- */
- teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALL, 0, 0);
-
- /* format effective command line */
- for (guint i = 0; i < cmdline->effective_len; i++)
- teco_interface_cmdline_insert_c(cmdline->str.data[i]);
-
- /* cursor should be after effective command line */
- guint pos = teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0);
- teco_view_ssm(teco_interface.cmdline_view, SCI_GOTOPOS, pos, 0);
-
- /* format rubbed out command line */
- for (guint i = cmdline->effective_len; i < cmdline->str.len; i++)
- teco_interface_cmdline_insert_c(cmdline->str.data[i]);
-
- teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_RUBBEDOUT, 0);
- teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE, pos,
- teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - pos);
-
- teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 0, 0);
+ teco_string_init(&teco_interface.info_current, buffer->filename,
+ buffer->filename ? strlen(buffer->filename) : 0);
+ teco_interface.info_type = buffer->state > TECO_BUFFER_CLEAN
+ ? TECO_INFO_TYPE_BUFFER_DIRTY : TECO_INFO_TYPE_BUFFER;
}
static GdkAtom
teco_interface_get_selection_by_name(const gchar *name)
{
+ g_assert(*name != '\0');
+
/*
* We can use gdk_atom_intern() to support arbitrary X11 selection
* names. However, since we cannot find out which selections are
@@ -602,11 +623,9 @@ teco_interface_get_selection_by_name(const gchar *name)
* Checking them here avoids expensive X server roundtrips.
*/
switch (*name) {
- case '\0': return GDK_NONE;
case 'P': return GDK_SELECTION_PRIMARY;
case 'S': return GDK_SELECTION_SECONDARY;
case 'C': return GDK_SELECTION_CLIPBOARD;
- default: break;
}
return gdk_atom_intern(name, FALSE);
@@ -767,6 +786,27 @@ teco_interface_is_interrupted(void)
return teco_interrupted != FALSE;
}
+void
+teco_interface_refresh(gboolean force)
+{
+ if (!gtk_main_level()) /* batch mode */
+ return;
+
+ if (G_UNLIKELY(force))
+ gtk_widget_queue_draw(teco_interface.window);
+
+ GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window));
+ gdk_window_thaw_updates(top_window);
+
+ /*
+ * FIXME: Why do we need two iterations to see any updates?
+ */
+ for (gint i = 0; i < 2; i++)
+ gtk_main_iteration_do(FALSE);
+
+ gdk_window_freeze_updates(top_window);
+}
+
static void
teco_interface_set_css_variables(teco_view_t *view)
{
@@ -776,40 +816,6 @@ teco_interface_set_css_variables(teco_view_t *view)
guint32 calltip_bg_color = teco_view_ssm(view, SCI_STYLEGETBACK, STYLE_CALLTIP, 0);
/*
- * FIXME: Font and colors of Scintilla views cannot be set via CSS.
- * But some day, there will be a way to send messages to the commandline view
- * from SciTECO code via ES.
- * Configuration will then be in the hands of color schemes.
- *
- * NOTE: We don't actually know apriori how large the font_size buffer should be,
- * but luckily SCI_STYLEGETFONT with a sptr==0 will return only the size.
- * This is undocumented in the Scintilla docs.
- */
- g_autofree gchar *font_name = g_malloc(teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, 0) + 1);
- teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, (sptr_t)font_name);
-
- teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_DEFAULT, default_fg_color);
- teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_DEFAULT, default_bg_color);
- teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)font_name);
- teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETSIZE, STYLE_DEFAULT,
- teco_view_ssm(view, SCI_STYLEGETSIZE, STYLE_DEFAULT, 0));
- teco_view_ssm(teco_interface.cmdline_view, SCI_STYLECLEARALL, 0, 0);
- teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_CALLTIP, calltip_fg_color);
- teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_CALLTIP, calltip_bg_color);
- teco_view_ssm(teco_interface.cmdline_view, SCI_SETCARETFORE,
- teco_view_ssm(view, SCI_GETCARETFORE, 0, 0), 0);
- /* used for the asterisk at the beginning of the command line */
- teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBOLD, STYLE_ASTERISK, TRUE);
- /* used for character representations */
- teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_CONTROLCHAR, default_fg_color);
- /* used for the rubbed out command line */
- teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_RUBBEDOUT, default_fg_color);
- /* this somehow gets reset */
- teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*");
-
- guint text_height = teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTHEIGHT, 0, 0);
-
- /*
* Generates a CSS that sets some predefined color variables.
* This effectively "exports" Scintilla styles into the CSS
* world.
@@ -835,16 +841,17 @@ teco_interface_set_css_variables(teco_view_t *view)
gtk_css_provider_load_from_data(teco_interface.css_var_provider, css, -1, NULL);
/*
- * The font and size of the commandline view might have changed,
+ * The font and size and height of the command-line view might have changed,
* so we resize it.
* This cannot be done via CSS or Scintilla messages.
- * Currently, it is always exactly one line high in order to mimic the Curses UI.
*/
- gtk_widget_set_size_request(GTK_WIDGET(teco_interface.cmdline_view), -1, text_height);
+ g_assert(teco_cmdline.height > 0);
+ gtk_widget_set_size_request(GTK_WIDGET(teco_cmdline.view), -1,
+ teco_cmdline.height*teco_cmdline_ssm(SCI_TEXTHEIGHT, 0, 0));
}
static void
-teco_interface_refresh(gboolean current_view_changed)
+teco_interface_update(gboolean current_view_changed)
{
/*
* The styles configured via Scintilla might change
@@ -1116,9 +1123,6 @@ teco_interface_handle_scroll(GdkEventScroll *event, GError **error)
{
g_assert(event->type == GDK_SCROLL);
- /*
- * FIXME: Do we have to support GDK_SCROLL_SMOOTH?
- */
switch (event->direction) {
case GDK_SCROLL_UP:
teco_mouse.type = TECO_MOUSE_SCROLLUP;
@@ -1157,7 +1161,7 @@ teco_interface_event_loop(GError **error)
if (!scitecoconfig_reg->vtable->get_string(scitecoconfig_reg,
&scitecoconfig.data, &scitecoconfig.len, NULL, error))
return FALSE;
- if (teco_string_contains(&scitecoconfig, '\0')) {
+ if (teco_string_contains(scitecoconfig, '\0')) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Null-character not allowed in filenames");
return FALSE;
@@ -1195,7 +1199,7 @@ teco_interface_event_loop(GError **error)
GTK_STYLE_PROVIDER(user_css_provider),
GTK_STYLE_PROVIDER_PRIORITY_USER);
- teco_interface_refresh(TRUE);
+ teco_interface_update(TRUE);
gtk_widget_show_all(teco_interface.window);
/* don't show popup by default */
@@ -1215,7 +1219,7 @@ teco_interface_event_loop(GError **error)
* This is not necessary on Windows since the icon included
* as a resource will be used by default.
*/
- static const gchar *icon_files[] = {
+ static const gchar *const icon_files[] = {
"sciteco-48.png", "sciteco-32.png", "sciteco-16.png"
};
GList *icon_list = NULL;
@@ -1249,6 +1253,10 @@ teco_interface_event_loop(GError **error)
g_unix_signal_add(SIGTERM, teco_interface_sigterm_handler, NULL);
#endif
+ /* the interval might have been changed in the profile */
+ g_timeout_add_seconds(teco_ring_recovery_interval,
+ teco_interface_dump_recovery_cb, NULL);
+
/* don't limit while waiting for input as this might be a busy operation */
teco_memory_stop_limiting();
@@ -1274,6 +1282,8 @@ teco_interface_cleanup(void)
if (teco_interface.window)
gtk_widget_destroy(teco_interface.window);
+ if (teco_interface.scroll_controller)
+ g_object_unref(teco_interface.scroll_controller);
scintilla_release_resources();
@@ -1305,6 +1315,20 @@ teco_interface_busy_timeout_cb(gpointer user_data)
return G_SOURCE_REMOVE;
}
+static gboolean
+teco_interface_dump_recovery_cb(gpointer user_data)
+{
+ teco_ring_dump_recovery();
+
+ /*
+ * The backup interval could have changed (6EJ).
+ * New intervals will not be effective immediately, though.
+ */
+ g_timeout_add_seconds(teco_ring_recovery_interval,
+ teco_interface_dump_recovery_cb, NULL);
+ return G_SOURCE_REMOVE;
+}
+
static void
teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data)
{
@@ -1312,17 +1336,12 @@ teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data)
teco_interface_set_cursor(widget, "text");
}
-/**
- * Called when the commandline widget is resized.
- * This should ensure that the caret jumps to the middle of the command line,
- * imitating the behaviour of the current Curses command line.
- */
+/** Called when the commandline widget is resized */
static void
teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
GdkRectangle *allocation, gpointer user_data)
{
- teco_view_ssm(teco_interface.cmdline_view, SCI_SETXCARETPOLICY,
- CARET_SLOP | CARET_EVEN, allocation->width/2);
+ teco_cmdline_resized(allocation->width);
}
static gboolean
@@ -1415,7 +1434,9 @@ teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data)
}
teco_interrupted = FALSE;
- teco_interface_refresh(teco_interface_current_view != last_view);
+ teco_interface_update(teco_interface_current_view != last_view);
+ /* always expand folds, even after mouse clicks */
+ teco_interface_unfold();
/*
* Scintilla has been patched to avoid any automatic scrolling since that
* has been benchmarked to be a very costly operation.
@@ -1458,10 +1479,34 @@ teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data)
}
static void
+teco_interface_scroll_cb(GtkEventControllerScroll *controller, double dx, double dy, gpointer data)
+{
+ /*
+ * FIXME: Using teco_interface.event_box_widget will cause crashes in
+ * teco_interface_input_cb()
+ */
+ GtkWidget *widget = teco_interface.window;
+
+ /*
+ * Emulate a GDK_SCROLL event to make use of the existing
+ * event queuing in teco_interface_input_cb().
+ */
+ g_autoptr(GdkEvent) scroll_event = gdk_event_new(GDK_SCROLL);
+ scroll_event->scroll.window = gtk_widget_get_parent_window(widget);
+ scroll_event->scroll.direction = dy > 0 ? GDK_SCROLL_DOWN : GDK_SCROLL_UP;
+ scroll_event->scroll.delta_x = dx;
+ scroll_event->scroll.delta_y = dy;
+
+ teco_interface_input_cb(widget, scroll_event, NULL);
+}
+
+static void
teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpointer user_data)
{
g_assert(len >= teco_interface.popup_prefix_len);
- const teco_string_t insert = {str+teco_interface.popup_prefix_len, len-teco_interface.popup_prefix_len};
+ /* str is an empty string for the "(Unnamed)" buffer */
+ const teco_string_t insert = {str+teco_interface.popup_prefix_len,
+ len-teco_interface.popup_prefix_len};
teco_machine_t *machine = &teco_cmdline.machine.parent;
const teco_view_t *last_view = teco_interface_current_view;
@@ -1471,12 +1516,12 @@ teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpoint
* A auto completion should never result in program termination.
*/
if (machine->current->insert_completion_cb &&
- !machine->current->insert_completion_cb(machine, &insert, NULL))
+ !machine->current->insert_completion_cb(machine, insert, NULL))
return;
teco_interface_popup_clear();
- teco_interface_cmdline_update(&teco_cmdline);
+ teco_cmdline_update();
- teco_interface_refresh(teco_interface_current_view != last_view);
+ teco_interface_update(teco_interface_current_view != last_view);
}
static gboolean
@@ -1484,7 +1529,6 @@ teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer
{
/*
* Emulate that the "close" key was pressed
- * which may then be handled by the execution thread
* which invokes the appropriate "function key macro"
* if it exists. Its default action will ensure that
* the execution thread shuts down and the main loop
@@ -1503,8 +1547,11 @@ teco_interface_sigterm_handler(gpointer user_data)
/*
* Similar to window deletion - emulate "close" key press.
*/
+ GtkWidget *widget = teco_interface.window;
+
g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS);
+ close_event->key.window = gtk_widget_get_parent_window(widget);
close_event->key.keyval = GDK_KEY_Close;
- return teco_interface_input_cb(teco_interface.window, close_event, NULL);
+ return teco_interface_input_cb(widget, close_event, NULL);
}
diff --git a/src/interface-gtk/view.c b/src/interface-gtk/view.c
index 81db3d7..3a18f33 100644
--- a/src/interface-gtk/view.c
+++ b/src/interface-gtk/view.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -102,12 +102,8 @@ teco_view_new(void)
gint events = gtk_widget_get_events(GTK_WIDGET(ctx));
events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK | GDK_TOUCH_MASK |
-#ifdef GDK_VERSION_3_18
GDK_TOUCHPAD_GESTURE_MASK |
-#endif
-#ifdef GDK_VERSION_3_22
GDK_TABLET_PAD_MASK |
-#endif
GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
gtk_widget_set_events(GTK_WIDGET(ctx), events);
diff --git a/src/interface.c b/src/interface.c
index 9ec1bed..2343a16 100644
--- a/src/interface.c
+++ b/src/interface.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -20,6 +20,7 @@
#endif
#include <stdarg.h>
+#include <string.h>
#include <stdio.h>
#include <glib.h>
@@ -36,6 +37,9 @@
//#define DEBUG
+/** minimum level of messages to print to stdout/stderr */
+teco_msg_t teco_interface_msg_level = TECO_MSG_USER;
+
teco_view_t *teco_interface_current_view = NULL;
TECO_DEFINE_UNDO_CALL(teco_interface_show_view, teco_view_t *);
@@ -81,35 +85,82 @@ teco_interface_undo_set_clipboard(const gchar *name, gchar *str, gsize len)
}
}
+void
+teco_interface_msg(teco_msg_t type, const gchar *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ g_autofree gchar *buf = g_strdup_vprintf(fmt, ap);
+ va_end(ap);
+
+ teco_interface_msg_literal(type, buf, strlen(buf));
+}
+
/**
- * Print a message to the appropriate stdio streams.
+ * Print a raw message to the appropriate stdio streams.
*
- * This method has similar semantics to `vprintf`, i.e.
- * it leaves `ap` undefined. Therefore to pass the format
- * string and arguments to another `vprintf`-like function,
- * you have to copy the arguments via `va_copy`.
+ * This deliberately does not echo (i.e. escape non-printable characters)
+ * the string. Either they are supposed to be written verbatim
+ * (TECO_MSG_USER) or are already echoed.
+ * Everything higher than TECO_MSG_USER is also terminated by LF.
+ *
+ * @fixme TECO_MSG_USER could always be flushed.
+ * This however makes the message disappear on UNIX since stdout/stderr
+ * have been redirected to /dev/null.
+ * Also it would probably be detrimental for performance in scripts
+ * that write individual characters.
+ * Perhaps we should put flushing under control of the language instead.
*/
void
-teco_interface_stdio_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+teco_interface_stdio_msg(teco_msg_t type, const gchar *str, gsize len)
{
- FILE *stream = stdout;
+ /* "user"-level messages are always printed */
+ if (type != TECO_MSG_USER && type < teco_interface_msg_level)
+ return;
switch (type) {
case TECO_MSG_USER:
+ fwrite(str, 1, len, stdout);
+ //fflush(stdout);
break;
case TECO_MSG_INFO:
- fputs("Info: ", stream);
+ g_fprintf(stdout, "Info: %.*s\n", (gint)len, str);
break;
case TECO_MSG_WARNING:
- stream = stderr;
- fputs("Warning: ", stream);
+ g_fprintf(stderr, "Warning: %.*s\n", (gint)len, str);
break;
case TECO_MSG_ERROR:
- stream = stderr;
- fputs("Error: ", stream);
+ g_fprintf(stderr, "Error: %.*s\n", (gint)len, str);
break;
}
+}
- g_vfprintf(stream, fmt, ap);
- fputc('\n', stream);
+/**
+ * Get character from stdin.
+ *
+ * @param widechar If TRUE reads one glyph encoded in UTF-8.
+ * If FALSE, returns exactly one byte.
+ * @return Codepoint or -1 in case of EOF.
+ */
+teco_int_t
+teco_interface_stdio_getch(gboolean widechar)
+{
+ gchar buf[4];
+ gint i = 0;
+ gint32 cp;
+
+ do {
+ if (G_UNLIKELY(fread(buf+i, 1, 1, stdin) < 1))
+ return -1; /* EOF */
+ if (!widechar || !buf[i])
+ return (guchar)buf[i];
+
+ /* doesn't work as expected when passed a null byte */
+ cp = g_utf8_get_char_validated(buf, ++i);
+ if (i >= sizeof(buf) || cp != -2)
+ i = 0;
+ } while (cp < 0);
+
+ return cp;
}
diff --git a/src/interface.h b/src/interface.h
index 33b094b..f196a83 100644
--- a/src/interface.h
+++ b/src/interface.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -16,7 +16,6 @@
*/
#pragma once
-#include <stdarg.h>
#include <signal.h>
#include <glib.h>
@@ -42,6 +41,12 @@
* feature.
*/
+/**
+ * Interval between polling for keypresses (if necessary).
+ * In other words, this is the maximum latency to detect CTRL+C interruptions.
+ */
+#define TECO_POLL_INTERVAL 100000 /* microseconds */
+
/** @protected */
extern teco_view_t *teco_interface_current_view;
@@ -61,18 +66,15 @@ typedef enum {
TECO_MSG_ERROR
} teco_msg_t;
+extern teco_msg_t teco_interface_msg_level;
+
/** @pure */
-void teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap);
+void teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len);
-static inline void G_GNUC_PRINTF(2, 3)
-teco_interface_msg(teco_msg_t type, const gchar *fmt, ...)
-{
- va_list ap;
+void teco_interface_msg(teco_msg_t type, const gchar *fmt, ...) G_GNUC_PRINTF(2, 3);
- va_start(ap, fmt);
- teco_interface_vmsg(type, fmt, ap);
- va_end(ap);
-}
+/** @pure */
+teco_int_t teco_interface_getch(gboolean widechar);
/** @pure */
void teco_interface_msg_clear(void);
@@ -93,6 +95,14 @@ teco_interface_ssm(unsigned int iMessage, uptr_t wParam, sptr_t lParam)
*/
void undo__teco_interface_ssm(unsigned int, uptr_t, sptr_t);
+/** Expand folds, so that dot is always visible. */
+static inline void
+teco_interface_unfold(void)
+{
+ sptr_t dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_interface_ssm(SCI_ENSUREVISIBLE, teco_interface_ssm(SCI_LINEFROMPOSITION, dot, 0), 0);
+}
+
/** @pure */
void teco_interface_info_update_qreg(const teco_qreg_t *reg);
/** @pure */
@@ -108,9 +118,6 @@ void undo__teco_interface_info_update_qreg(const teco_qreg_t *);
void undo__teco_interface_info_update_buffer(const teco_buffer_t *);
/** @pure */
-void teco_interface_cmdline_update(const teco_cmdline_t *cmdline);
-
-/** @pure */
gboolean teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
GError **error);
void teco_interface_undo_set_clipboard(const gchar *name, gchar *str, gsize len);
@@ -128,7 +135,17 @@ typedef enum {
TECO_POPUP_DIRECTORY
} teco_popup_entry_type_t;
-/** @pure */
+/**
+ * Add entry to popup.
+ *
+ * @param type Entry type
+ * @param name
+ * Name string of the entry or NULL for "(Unnamed)".
+ * It is not necessarily null-terminated.
+ * @param name_len Length of string in name
+ * @param highlight Whether to highlight the entry
+ * @pure
+ */
void teco_interface_popup_add(teco_popup_entry_type_t type,
const gchar *name, gsize name_len, gboolean highlight);
/** @pure */
@@ -145,7 +162,7 @@ gboolean teco_interface_is_interrupted(void);
typedef struct {
enum {
- TECO_MOUSE_PRESSED = 1,
+ TECO_MOUSE_PRESSED = 0,
TECO_MOUSE_RELEASED,
TECO_MOUSE_SCROLLUP,
TECO_MOUSE_SCROLLDOWN
@@ -172,7 +189,13 @@ gboolean teco_interface_event_loop(GError **error);
* Interfacing to the external SciTECO world
*/
/** @protected */
-void teco_interface_stdio_vmsg(teco_msg_t type, const gchar *fmt, va_list ap);
+void teco_interface_stdio_msg(teco_msg_t type, const gchar *str, gsize len);
+
+/** @protected */
+teco_int_t teco_interface_stdio_getch(gboolean widechar);
+
+/** @protected */
+void teco_interface_refresh(gboolean force);
/** @pure */
void teco_interface_cleanup(void);
diff --git a/src/lexer.c b/src/lexer.c
index 5e6202d..25ea8f2 100644
--- a/src/lexer.c
+++ b/src/lexer.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -26,6 +26,7 @@
#include "sciteco.h"
#include "view.h"
#include "parser.h"
+#include "core-commands.h"
#include "lexer.h"
static teco_style_t
@@ -37,16 +38,21 @@ teco_lexer_getstyle(teco_view_t *view, teco_machine_main_t *machine,
/*
* FIXME: At least this special workaround for numbers might be
* unnecessary once we get a special parser state for parsing numbers.
- *
- * FIXME: What about ^* and ^/?
- * They are currently highlighted as commands.
*/
if (machine->parent.current->keymacro_mask & TECO_KEYMACRO_MASK_START &&
chr <= 0xFF) {
if (g_ascii_isdigit(chr))
style = SCE_SCITECO_NUMBER;
- else if (strchr("+-*/#&", chr))
+ else if (strchr(",+-*/#&()", chr))
style = SCE_SCITECO_OPERATOR;
+ } else if (machine->parent.current == &teco_state_control) {
+ /*
+ * Two-character operators must always begin with caret
+ * They get a separate style, so we can extend it back to
+ * the caret in teco_lexter_step.
+ */
+ if (strchr("*/#", chr))
+ style = SCE_SCITECO_OPERATOR2;
}
/*
@@ -126,13 +132,40 @@ teco_lexer_step(teco_view_t *view, teco_machine_main_t *machine,
machine->macro_pc = g_utf8_next_char(macro+machine->macro_pc) - macro;
gunichar escape_char = machine->expectstring.machine.escape_char;
+ guint fold_level = SC_FOLDLEVELBASE+machine->expectstring.nesting-1+
+ (escape_char == '{' ? 1 : 0);
+
style = teco_lexer_getstyle(view, machine, chr);
/*
+ * Apply folding. This currently folds only {...} string arguments
+ * and all its embedded braces.
+ * We could fold loops and IF-statements as well, but that would
+ * require manually keeping track of the nesting in parse-only mode,
+ * which should better be in the parser itself.
+ *
+ * FIXME: You cannot practically disable folding via properties.
+ */
+ if (teco_view_ssm(view, SCI_GETPROPERTYINT, (uptr_t)"fold", TRUE)) {
+ guint next_fold_level = SC_FOLDLEVELBASE+machine->expectstring.nesting-1+
+ (machine->expectstring.machine.escape_char == '{' ? 1 : 0);
+
+ if (next_fold_level > fold_level)
+ /* `chr` opened a {...} string argument */
+ teco_view_ssm(view, SCI_SETFOLDLEVEL, *cur_line,
+ fold_level | SC_FOLDLEVELHEADERFLAG);
+ else if (!*cur_col)
+ teco_view_ssm(view, SCI_SETFOLDLEVEL, *cur_line, fold_level);
+ }
+
+ /*
* Optionally style @^Uq{ ... } contents like macro definitions.
* The curly braces will be styled like regular commands.
*
- * FIXME: This will not work with nested macro definitions.
+ * FIXME: This works only for top-level macro definitions,
+ * not for nested definitions.
+ * FIXME: The macrodef_machine's end-of-macro callback could be used
+ * to detect and highlight an error on the closing `}`.
* FIXME: This cannot currently be disabled, not even with SCI_SETPROPERTY.
* We could only map it to an ED flag or
* rewrite the lexer against the ILexer5 interface, which requires C++.
@@ -147,8 +180,9 @@ teco_lexer_step(teco_view_t *view, teco_machine_main_t *machine,
/*
* True comments begin with `!*` or `!!`, but only the second character gets
* the correct style by default, so we extend it backwards.
+ * The same is true for two-letter operators.
*/
- if (style == SCE_SCITECO_COMMENT)
+ if (style == SCE_SCITECO_COMMENT || style == SCE_SCITECO_OPERATOR2)
old_pc--;
teco_view_ssm(view, SCI_STARTSTYLING, start+old_pc, 0);
@@ -185,22 +219,35 @@ teco_lexer_style(teco_view_t *view, gsize end)
gsize start = teco_view_ssm(view, SCI_GETENDSTYLED, 0, 0);
guint start_line = teco_view_ssm(view, SCI_LINEFROMPOSITION, start, 0);
- gint start_col = 0;
/*
* The line state stores the laster character (column) in bytes,
* that starts from a fresh parser state.
* It's -1 if the line does not have a clean parser state.
- * Therefore we search for the first line before `start` that has a
- * known clean parser state.
+ * If the cached position on start_line does not fit our needs,
+ * we backtrack and search in previous lines
+ * for a known clean parser state.
+ *
+ * NOTE: It's crucial to consider the line state of the first possible
+ * line since we might be styling for a single-line command line view.
+ *
+ * FIXME: During rubout of regular commands we will frequently have the
+ * situation that the cached line state points after the last styled position
+ * forcing us to restyle the entire command line macro.
+ * If this turns out to be problematic, we might detect that
+ * view == teco_cmdline.view and inspect teco_cmdline.machine.
*/
- if (start_line > 0) {
+ gint start_col = teco_view_ssm(view, SCI_GETLINESTATE, start_line, 0);
+ if (start_col > start - teco_view_ssm(view, SCI_POSITIONFROMLINE, start_line, 0))
+ /* we are asked to style __before__ the last known start state */
+ start_col = -1;
+ if (start_col < 0 && start_line > 0) {
do
start_line--;
while ((start_col = teco_view_ssm(view, SCI_GETLINESTATE, start_line, 0)) < 0 &&
start_line > 0);
- start_col = MAX(start_col, 0);
}
+ start_col = MAX(start_col, 0);
start = teco_view_ssm(view, SCI_POSITIONFROMLINE, start_line, 0) + start_col;
g_assert(end > start);
diff --git a/src/lexer.h b/src/lexer.h
index 2b011be..e7073c2 100644
--- a/src/lexer.h
+++ b/src/lexer.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -25,12 +25,14 @@ typedef enum {
SCE_SCITECO_DEFAULT = 0,
SCE_SCITECO_COMMAND = 1,
SCE_SCITECO_OPERATOR = 2,
- SCE_SCITECO_QREG = 3,
- SCE_SCITECO_STRING = 4,
- SCE_SCITECO_NUMBER = 5,
- SCE_SCITECO_LABEL = 6,
- SCE_SCITECO_COMMENT = 7,
- SCE_SCITECO_INVALID = 8
+ /** two-character operators */
+ SCE_SCITECO_OPERATOR2 = 3,
+ SCE_SCITECO_QREG = 4,
+ SCE_SCITECO_STRING = 5,
+ SCE_SCITECO_NUMBER = 6,
+ SCE_SCITECO_LABEL = 7,
+ SCE_SCITECO_COMMENT = 8,
+ SCE_SCITECO_INVALID = 9
} teco_style_t;
void teco_lexer_style(teco_view_t *view, gsize end);
diff --git a/src/list.h b/src/list.h
index 2a7ab91..fc4b247 100644
--- a/src/list.h
+++ b/src/list.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/main.c b/src/main.c
index 05f8c41..4d46817 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -30,17 +30,24 @@
#include <glib/gprintf.h>
#include <glib/gstdio.h>
+#ifdef G_OS_WIN32
+#include <fcntl.h>
+#include <io.h>
+#endif
+
#ifdef HAVE_SYS_CAPSICUM_H
#include <sys/capsicum.h>
#endif
#include "sciteco.h"
+#include "expressions.h"
#include "file-utils.h"
#include "cmdline.h"
#include "interface.h"
#include "parser.h"
#include "goto.h"
#include "qreg.h"
+#include "view.h"
#include "ring.h"
#include "undo.h"
#include "error.h"
@@ -107,6 +114,10 @@ teco_get_default_config_path(void)
#endif
+static gboolean teco_show_version = FALSE;
+static gboolean teco_quiet = FALSE;
+static gboolean teco_stdin = FALSE;
+static gboolean teco_stdout = FALSE;
static gchar *teco_eval_macro = NULL;
static gboolean teco_mung_file = FALSE;
static gboolean teco_mung_profile = TRUE;
@@ -118,6 +129,14 @@ static gchar *
teco_process_options(gchar ***argv)
{
static const GOptionEntry option_entries[] = {
+ {"version", 'v', 0, G_OPTION_ARG_NONE, &teco_show_version,
+ "Show version"},
+ {"quiet", 'q', 0, G_OPTION_ARG_NONE, &teco_quiet,
+ "Don't print any non-user-level messages to stdout"},
+ {"stdin", 'i', 0, G_OPTION_ARG_NONE, &teco_stdin,
+ "Read stdin into the unnamed buffer"},
+ {"stdout", 'o', 0, G_OPTION_ARG_NONE, &teco_stdout,
+ "Print current buffer to stdout before program termination"},
{"eval", 'e', 0, G_OPTION_ARG_STRING, &teco_eval_macro,
"Evaluate macro", "macro"},
{"mung", 'm', 0, G_OPTION_ARG_NONE, &teco_mung_file,
@@ -150,7 +169,7 @@ teco_process_options(gchar ***argv)
g_option_context_set_description(
options,
"Bug reports should go to <" PACKAGE_BUGREPORT "> or "
- "<" PACKAGE_URL ">."
+ "<rhaberkorn@fmsbw.de>."
);
g_option_context_add_main_entries(options, option_entries, NULL);
@@ -186,6 +205,15 @@ teco_process_options(gchar ***argv)
exit(EXIT_FAILURE);
}
+ if (teco_show_version) {
+ puts(PACKAGE_VERSION);
+ exit(EXIT_SUCCESS);
+ }
+
+ if (teco_quiet)
+ /* warnings and errors will still be printed to stderr */
+ teco_interface_msg_level = TECO_MSG_WARNING;
+
if ((*argv)[0] && !g_strcmp0((*argv)[1], "-S")) {
/* translate -S to --, this is always passed down */
(*argv)[1][1] = '-';
@@ -337,6 +365,21 @@ main(int argc, char **argv)
#endif
{
g_autoptr(GError) error = NULL;
+ teco_int_t ret = EXIT_SUCCESS;
+
+#ifdef G_OS_WIN32
+ /*
+ * Windows might by default perform EOL translations, especially
+ * when writing to stdout, i.e. translate LF to CRLF.
+ * This would break at the very least --stdout, where you are
+ * expected to get the linebreaks configured on the current buffer via EL.
+ * It would also break binary filters on Windows.
+ * Since printing LF to the console is safe nowadays, we just do that
+ * globally.
+ */
+ for (gint fd = 0; fd <= 2; fd++)
+ _setmode(fd, _O_BINARY);
+#endif
#ifdef DEBUG_PAUSE
/* Windows debugging hack (see above) */
@@ -417,15 +460,20 @@ main(int argc, char **argv)
* DEC TECO has them in the global table, though.
*/
/* search string and status register */
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("_", 1));
+ teco_qreg_table_insert_unique(&teco_qreg_table_globals,
+ teco_qreg_plain_new("_", 1));
/* replacement string register */
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("-", 1));
+ teco_qreg_table_insert_unique(&teco_qreg_table_globals,
+ teco_qreg_plain_new("-", 1));
/* current document's dot (":") */
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_dot_new());
+ teco_qreg_table_insert_unique(&teco_qreg_table_globals,
+ teco_qreg_dot_new());
/* current buffer name and number ("*") */
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_bufferinfo_new());
+ teco_qreg_table_insert_unique(&teco_qreg_table_globals,
+ teco_qreg_bufferinfo_new());
/* current working directory ("$") */
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_workingdir_new());
+ teco_qreg_table_insert_unique(&teco_qreg_table_globals,
+ teco_qreg_workingdir_new());
/* environment defaults and registers */
teco_initialize_environment();
@@ -438,15 +486,37 @@ main(int argc, char **argv)
}
/*
- * Add remaining arguments to unnamed buffer.
+ * Load stdin into the unnamed buffer.
+ * This will also perform EOL normalization.
+ * This is not done automatically when isatty(0) == 0
+ * since you might want to read from stdin manually (^T).
+ * Loading stdin also won't work if the stream is infinite.
*
- * FIXME: This is not really robust since filenames may contain linefeeds.
- * Also, the Unnamed Buffer should be kept empty for piping.
- * Therefore, it would be best to store the arguments in Q-Regs, e.g. $0,$1,$2...
+ * NOTE: The profile hasn't run yet, so it cannot guess the
+ * documents encoding. This should therefore be done by the profile
+ * for any preexisting unnamed buffer.
+ */
+ if (teco_stdin) {
+ if (!teco_view_load_from_stdin(teco_ring_current->view, TRUE, &error))
+ goto cleanup;
+
+ if (teco_interface_ssm(SCI_GETLENGTH, 0, 0) > 0)
+ teco_ring_dirtify();
+ }
+
+ /*
+ * Initialize the commandline-argument Q-registers (^Ax).
*/
- for (gint i = 1; argv_utf8[i]; i++) {
- teco_interface_ssm(SCI_APPENDTEXT, strlen(argv_utf8[i]), (sptr_t)argv_utf8[i]);
- teco_interface_ssm(SCI_APPENDTEXT, 1, (sptr_t)"\n");
+ for (guint i = 0; argv_utf8[i]; i++) {
+ gchar buf[32+1];
+ gint len = g_snprintf(buf, sizeof(buf), "\1%u", i);
+ g_assert(len < sizeof(buf));
+
+ teco_qreg_t *qreg = teco_qreg_plain_new(buf, len);
+ teco_qreg_table_insert_unique(&teco_qreg_table_globals, qreg);
+ if (!qreg->vtable->set_string(qreg, argv_utf8[i], strlen(argv_utf8[i]),
+ teco_default_codepage(), &error))
+ goto cleanup;
}
/*
@@ -461,7 +531,8 @@ main(int argc, char **argv)
}
g_clear_error(&error);
- if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
+ if (!teco_expressions_pop_num_calc(&ret, EXIT_SUCCESS, &error) ||
+ !teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
goto cleanup;
goto cleanup;
}
@@ -498,8 +569,10 @@ main(int argc, char **argv)
goto cleanup;
g_clear_error(&error);
- if (teco_quit_requested) {
- if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
+ if (teco_ed & TECO_ED_EXIT) {
+ /* exit was requested using the EX command */
+ if (!teco_expressions_pop_num_calc(&ret, EXIT_SUCCESS, &error) ||
+ !teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
goto cleanup;
goto cleanup;
}
@@ -509,7 +582,7 @@ main(int argc, char **argv)
* If munged file didn't quit, switch into interactive mode
*/
/* commandline replacement string register */
- teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("\e", 1));
+ teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_plain_new("\e", 1));
teco_undo_enabled = TRUE;
teco_ring_set_scintilla_undo(TRUE);
@@ -553,8 +626,13 @@ main(int argc, char **argv)
goto cleanup;
cleanup:
- if (error != NULL)
+ if (!error && teco_stdout)
+ teco_view_save_to_stdout(teco_ring_current->view, &error);
+
+ if (error != NULL) {
teco_error_display_full(error);
+ ret = EXIT_FAILURE;
+ }
#ifndef NDEBUG
teco_ring_cleanup();
@@ -562,8 +640,9 @@ cleanup:
teco_qreg_table_clear(&teco_qreg_table_globals);
teco_qreg_stack_clear();
teco_view_free(teco_qreg_view);
+ teco_cmdline_cleanup();
#endif
teco_interface_cleanup();
- return error ? EXIT_FAILURE : EXIT_SUCCESS;
+ return ret;
}
diff --git a/src/memory.c b/src/memory.c
index ea056bc..d8de483 100644
--- a/src/memory.c
+++ b/src/memory.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -251,13 +251,19 @@
* It is of course possible to query the program's RSS via OS APIs.
* This has long been avoided because it is naturally platform-dependant and
* some of the APIs have proven to be too slow for frequent polling.
+ * Also, this will only reliably work if malloc_trim(0) does what it's
+ * supposed to do.
*
* - Windows has GetProcessMemoryInfo() which is quite slow.
* When polled on a separate thread, the slow down is very acceptable.
+ * - POSIX has getrusage().
+ * __Its performance on different OS is still untested!__
+ * It reports in different units on different systems, see
+ * mimalloc/src/prim/unix/prim.c.
+ * The maxrss field does not shrink even with a working malloc_trim().
* - OS X has task_info().
* __Its performance is still untested!__
* - FreeBSD has sysctl().
- * __Its performance is still untested!__
* - Linux has no APIs but /proc/self/statm.
* Reading it is naturally very slow, but at least of constant time.
* When polled on a separate thread, the slow down is very acceptable.
@@ -298,6 +304,10 @@ static guint teco_memory_usage = 0;
*/
#ifdef REPLACE_MALLOC
+#ifndef G_ATOMIC_LOCK_FREE
+#warning "malloc() replacement will be very slow!"
+#endif
+
void * __attribute__((used))
malloc(size_t size)
{
@@ -471,10 +481,8 @@ teco_memory_get_usage(void)
/*
* Practically only for FreeBSD.
*
- * The malloc replacement via dlmalloc also works on FreeBSD,
- * but this implementation has been benchmarked to be up to 4 times faster
- * (but only if we poll in a separate thread).
- * On the downside, this will of course be less precise.
+ * Since FreeBSD supports dlmalloc(), which is generally faster than
+ * jemalloc with a poll thread, this is not usually required.
*/
static gsize
teco_memory_get_usage(void)
diff --git a/src/memory.h b/src/memory.h
index ae7b506..9826073 100644
--- a/src/memory.h
+++ b/src/memory.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/move-commands.c b/src/move-commands.c
index 6324131..45afc4e 100644
--- a/src/move-commands.c
+++ b/src/move-commands.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -36,7 +36,7 @@
#include "core-commands.h"
#include "move-commands.h"
-/*$ J jump
+/*$ "J" ":J" jump
* [position]J -- Go to position in buffer
* [position]:J -> Success|Failure
*
@@ -90,7 +90,7 @@ teco_move_chars(teco_int_t n)
return TECO_SUCCESS;
}
-/*$ C move
+/*$ "C" ":C" move
* [n]C -- Move dot <n> characters
* -C
* [n]:C -> Success|Failure
@@ -117,7 +117,7 @@ teco_state_start_move(teco_machine_main_t *ctx, GError **error)
}
}
-/*$ R reverse
+/*$ "R" ":R" reverse
* [n]R -- Move dot <n> characters backwards
* -R
* [n]:R -> Success|Failure
@@ -158,7 +158,7 @@ teco_move_lines(teco_int_t n)
return TECO_SUCCESS;
}
-/*$ L line
+/*$ "L" ":L" line
* [n]L -- Move dot <n> lines forwards
* -L
* [n]:L -> Success|Failure
@@ -193,7 +193,7 @@ teco_state_start_line(teco_machine_main_t *ctx, GError **error)
}
}
-/*$ B backwards
+/*$ "B" ":B" backwards
* [n]B -- Move dot <n> lines backwards
* -B
* [n]:B -> Success|Failure
@@ -276,7 +276,7 @@ teco_find_words(gsize *pos, teco_int_t n, gboolean end_of_word)
/*
* FIXME: Is this safe or do we have to look up Unicode code points?
*/
- if ((!teco_string_contains(&wchars, *p)) == skip_word) {
+ if ((!teco_string_contains(wchars, *p)) == skip_word) {
if (skip_word == end_of_word)
break;
skip_word = !skip_word;
@@ -314,7 +314,7 @@ teco_find_words(gsize *pos, teco_int_t n, gboolean end_of_word)
/*
* FIXME: Is this safe or do we have to look up Unicode code points?
*/
- if ((!teco_string_contains(&wchars, p[-1])) == skip_word) {
+ if ((!teco_string_contains(wchars, p[-1])) == skip_word) {
if (skip_word != end_of_word)
break;
skip_word = !skip_word;
@@ -328,7 +328,7 @@ teco_find_words(gsize *pos, teco_int_t n, gboolean end_of_word)
return TRUE;
}
-/*$ W word
+/*$ "W" ":W" "@W" ":@W" word
* [n]W -- Move dot <n> words forwards
* -W
* [n]:W -> Success|Failure
@@ -353,7 +353,7 @@ teco_find_words(gsize *pos, teco_int_t n, gboolean end_of_word)
* buffer, the command yields an error.
* If colon-modified it instead returns a condition code.
*/
-/*$ P
+/*$ "P" ":P" "@P" ":@P"
* [n]P -- Move dot <n> words backwards
* -P
* [n]:P -> Success|Failure
@@ -401,7 +401,7 @@ teco_state_start_words(teco_machine_main_t *ctx, const gchar *cmd, gint factor,
return &teco_state_start;
}
-/*$ V
+/*$ "V" ":V" "@V" ":@V"
* [n]V -- Delete words forwards
* -V
* [n]:V -> Success|Failure
@@ -417,7 +417,7 @@ teco_state_start_words(teco_machine_main_t *ctx, const gchar *cmd, gint factor,
* \(lq@V\(rq is especially useful to remove the remainder of the
* current word.
*/
-/*$ Y
+/*$ "Y" ":Y" "@Y" ":@Y"
* [n]Y -- Delete word backwards
* -Y
* [n]:Y -> Success|Failure
@@ -544,7 +544,7 @@ teco_state_start_kill(teco_machine_main_t *ctx, const gchar *cmd, gboolean by_li
return TRUE;
}
-/*$ K kill
+/*$ "K" ":K" kill
* [n]K -- Kill lines
* -K
* from,to K
@@ -572,7 +572,7 @@ teco_state_start_kill_lines(teco_machine_main_t *ctx, GError **error)
teco_state_start_kill(ctx, "K", TRUE, error);
}
-/*$ D delete
+/*$ "D" ":D" delete
* [n]D -- Delete characters
* -D
* from,to D
@@ -600,7 +600,7 @@ teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error)
teco_state_start_kill(ctx, "D", FALSE, error);
}
-/*$ ^Q lines2glyphs glyphs2lines
+/*$ "^Q" ":^Q" lines2glyphs glyphs2lines
* [n]^Q -> glyphs -- Convert between lines and glyph lengths or positions
* [position]:^Q -> line
*
@@ -620,21 +620,16 @@ teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error)
void
teco_state_control_lines2glyphs(teco_machine_main_t *ctx, GError **error)
{
- if (!teco_expressions_eval(FALSE, error))
- return;
-
if (teco_machine_main_eval_colon(ctx)) {
gssize pos;
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+
if (!teco_expressions_args()) {
pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
} else {
- teco_int_t v;
-
- if (!teco_expressions_pop_num_calc(&v, 0, error))
- return;
-
- pos = teco_interface_glyphs2bytes(v);
+ pos = teco_interface_glyphs2bytes(teco_expressions_pop_num(0));
if (pos < 0) {
teco_error_range_set(error, "^Q");
return;
diff --git a/src/move-commands.h b/src/move-commands.h
index 1f32151..cc92961 100644
--- a/src/move-commands.h
+++ b/src/move-commands.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/parser.c b/src/parser.c
index c1d22b2..747249d 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -19,6 +19,7 @@
#include "config.h"
#endif
+#include <errno.h>
#include <string.h>
#include <glib.h>
@@ -80,7 +81,7 @@ teco_machine_input(teco_machine_t *ctx, gunichar chr, GError **error)
gboolean
teco_state_end_of_macro(teco_machine_t *ctx, GError **error)
{
- g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_SYNTAX,
"Unterminated command");
return FALSE;
}
@@ -161,9 +162,7 @@ gboolean
teco_execute_macro(const gchar *macro, gsize macro_len,
teco_qreg_table_t *qreg_table_locals, GError **error)
{
- const teco_string_t str = {(gchar *)macro, macro_len};
-
- if (!teco_string_validate_utf8(&str)) {
+ if (!teco_string_validate_utf8((teco_string_t){(gchar *)macro, macro_len})) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
"Invalid UTF-8 byte sequence in macro");
return FALSE;
@@ -185,41 +184,60 @@ teco_execute_macro(const gchar *macro, gsize macro_len,
GError *tmp_error = NULL;
- if (!teco_machine_main_step(&macro_machine, macro, macro_len, &tmp_error)) {
- if (!g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) {
- /* passes ownership of tmp_error */
- g_propagate_error(error, tmp_error);
- goto error_cleanup;
+ for (;;) {
+ if (!teco_machine_main_step(&macro_machine, macro, macro_len, &tmp_error)) {
+ if (!g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) {
+ /* passes ownership of tmp_error */
+ g_propagate_error(error, tmp_error);
+ goto error_cleanup;
+ }
+ g_error_free(tmp_error);
+
+ /*
+ * Macro returned - handle like regular
+ * end of macro, even though some checks
+ * are unnecessary here.
+ * macro_pc will still point to the return PC.
+ */
+ g_assert(macro_machine.parent.current == &teco_state_start);
+
+ /*
+ * Discard all braces, except the current one.
+ */
+ if (!teco_expressions_brace_return(parent_brace_level, teco_error_return_args, error))
+ goto error_cleanup;
+
+ /*
+ * Clean up the loop stack.
+ * We are allowed to return in loops.
+ * NOTE: This does not have to be undone.
+ */
+ g_array_remove_range(teco_loop_stack, macro_machine.loop_stack_fp,
+ teco_loop_stack->len - macro_machine.loop_stack_fp);
}
- g_error_free(tmp_error);
- /*
- * Macro returned - handle like regular
- * end of macro, even though some checks
- * are unnecessary here.
- * macro_pc will still point to the return PC.
- */
- g_assert(macro_machine.parent.current == &teco_state_start);
+ if (G_LIKELY(teco_goto_backup_pc < 0))
+ break;
- /*
- * Discard all braces, except the current one.
- */
- if (!teco_expressions_brace_return(parent_brace_level, teco_error_return_args, error))
- goto error_cleanup;
+ /* continue after :Olabel$ */
+ macro_machine.macro_pc = teco_goto_backup_pc;
+ /* macro could have ended in a "lookahead" state */
+ macro_machine.parent.current = &teco_state_start;
- /*
- * Clean up the loop stack.
- * We are allowed to return in loops.
- * NOTE: This does not have to be undone.
- */
- g_array_remove_range(teco_loop_stack, macro_machine.loop_stack_fp,
- teco_loop_stack->len - macro_machine.loop_stack_fp);
+ teco_undo_string_own(teco_goto_skip_label);
+ memset(&teco_goto_skip_label, 0, sizeof(teco_goto_skip_label));
+ teco_undo_gssize(teco_goto_backup_pc) = -1;
+
+ if (macro_machine.parent.must_undo)
+ teco_undo_flags(macro_machine.flags);
+ macro_machine.flags.mode = TECO_MODE_NORMAL;
+
+ /* no need to reparse everything in the future */
+ macro_machine.goto_table.complete = TRUE;
}
if (G_UNLIKELY(teco_goto_skip_label.len > 0)) {
- g_autofree gchar *label_printable = teco_string_echo(teco_goto_skip_label.data, teco_goto_skip_label.len);
- g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
- "Label \"%s\" not found", label_printable);
+ teco_error_label_set(error, teco_goto_skip_label.data, teco_goto_skip_label.len);
goto error_attach;
}
@@ -385,6 +403,8 @@ teco_machine_main_clear(teco_machine_main_t *ctx)
teco_goto_table_clear(&ctx->goto_table);
teco_string_clear(&ctx->expectstring.string);
teco_machine_stringbuilding_clear(&ctx->expectstring.machine);
+ teco_string_clear(&ctx->goto_label);
+ teco_machine_qregspec_free(ctx->expectqreg);
}
/** Append string to result with case folding. */
@@ -394,9 +414,6 @@ teco_machine_stringbuilding_append(teco_machine_stringbuilding_t *ctx, const gch
g_assert(ctx->result != NULL);
switch (ctx->mode) {
- case TECO_STRINGBUILDING_MODE_NORMAL:
- teco_string_append(ctx->result, str, len);
- break;
case TECO_STRINGBUILDING_MODE_UPPER: {
g_autofree gchar *folded = ctx->codepage == SC_CP_UTF8
? g_utf8_strup(str, len) : g_ascii_strup(str, len);
@@ -409,46 +426,91 @@ teco_machine_stringbuilding_append(teco_machine_stringbuilding_t *ctx, const gch
teco_string_append(ctx->result, folded, strlen(folded));
break;
}
+ default:
+ teco_string_append(ctx->result, str, len);
+ break;
}
}
-/*
- * FIXME: All teco_state_stringbuilding_* states could be static?
+/**
+ * Append codepoint to result string with case folding.
+ *
+ * This also takes the target encoding into account and checks the value
+ * range accordingly.
+ *
+ * @return FALSE if the codepoint is not valid in the target encoding.
*/
+static gboolean
+teco_machine_stringbuilding_append_c(teco_machine_stringbuilding_t *ctx, teco_int_t value)
+{
+ g_assert(ctx->result != NULL);
+
+ if (ctx->codepage == SC_CP_UTF8) {
+ if (value < 0 || !g_unichar_validate(value))
+ return FALSE;
+ switch (ctx->mode) {
+ case TECO_STRINGBUILDING_MODE_UPPER:
+ value = g_unichar_toupper(value);
+ break;
+ case TECO_STRINGBUILDING_MODE_LOWER:
+ value = g_unichar_tolower(value);
+ break;
+ }
+ teco_string_append_wc(ctx->result, value);
+ } else {
+ if (value < 0 || value > 0xFF)
+ return FALSE;
+ switch (ctx->mode) {
+ case TECO_STRINGBUILDING_MODE_UPPER:
+ value = g_ascii_toupper(value);
+ break;
+ case TECO_STRINGBUILDING_MODE_LOWER:
+ value = g_ascii_tolower(value);
+ break;
+ }
+ teco_string_append_c(ctx->result, value);
+ }
+
+ return TRUE;
+}
+
static teco_state_t *teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx,
gunichar chr, GError **error);
-TECO_DECLARE_STATE(teco_state_stringbuilding_ctl);
+static teco_state_t teco_state_stringbuilding_ctl;
static teco_state_t *teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx,
gunichar chr, GError **error);
-TECO_DECLARE_STATE(teco_state_stringbuilding_escaped);
+static teco_state_t teco_state_stringbuilding_escaped;
-TECO_DECLARE_STATE(teco_state_stringbuilding_lower);
-TECO_DECLARE_STATE(teco_state_stringbuilding_upper);
+static teco_state_t teco_state_stringbuilding_lower;
+static teco_state_t teco_state_stringbuilding_upper;
-TECO_DECLARE_STATE(teco_state_stringbuilding_ctle);
-TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_num);
-TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_u);
-TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_q);
-TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_quote);
-TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_n);
+static teco_state_t teco_state_stringbuilding_ctle;
+static teco_state_t teco_state_stringbuilding_ctle_num;
+static teco_state_t teco_state_stringbuilding_ctle_u;
+static teco_state_t teco_state_stringbuilding_ctle_code;
+static teco_state_t teco_state_stringbuilding_ctle_q;
+static teco_state_t teco_state_stringbuilding_ctle_quote;
+static teco_state_t teco_state_stringbuilding_ctle_n;
static teco_state_t *
teco_state_stringbuilding_start_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
{
- switch (chr) {
- case '^':
- return &teco_state_stringbuilding_ctl;
- case TECO_CTL_KEY('^'):
- /*
- * Ctrl+^ is inserted verbatim as code 30.
- * Otherwise it would expand to a single caret
- * just like caret+caret (^^).
- */
- break;
- default:
- if (TECO_IS_CTL(chr))
- return teco_state_stringbuilding_ctl_input(ctx, TECO_CTL_ECHO(chr), error);
+ if (ctx->mode != TECO_STRINGBUILDING_MODE_DISABLED) {
+ switch (chr) {
+ case '^':
+ return &teco_state_stringbuilding_ctl;
+ case TECO_CTL_KEY('^'):
+ /*
+ * Ctrl+^ is inserted verbatim as code 30.
+ * Otherwise it would expand to a single caret
+ * just like caret+caret (^^).
+ */
+ break;
+ default:
+ if (TECO_IS_CTL(chr))
+ return teco_state_stringbuilding_ctl_input(ctx, TECO_CTL_ECHO(chr), error);
+ }
}
return teco_state_stringbuilding_escaped_input(ctx, chr, error);
@@ -457,14 +519,15 @@ teco_state_stringbuilding_start_input(teco_machine_stringbuilding_t *ctx, gunich
/* in cmdline.c */
gboolean teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
-gboolean teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, const teco_string_t *str, GError **error);
-
-TECO_DEFINE_STATE(teco_state_stringbuilding_start,
- .is_start = TRUE,
- .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)
- teco_state_stringbuilding_start_process_edit_cmd,
- .insert_completion_cb = (teco_state_insert_completion_cb_t)
- teco_state_stringbuilding_insert_completion
+gboolean teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, teco_string_t str, GError **error);
+
+static TECO_DEFINE_STATE(teco_state_stringbuilding_start,
+ .is_start = TRUE,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_start_input,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)
+ teco_state_stringbuilding_start_process_edit_cmd,
+ .insert_completion_cb = (teco_state_insert_completion_cb_t)
+ teco_state_stringbuilding_insert_completion
);
static teco_state_t *
@@ -481,6 +544,11 @@ teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar
* be abolished altogether.
*/
break;
+ case 'P':
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->mode);
+ ctx->mode = TECO_STRINGBUILDING_MODE_DISABLED;
+ return &teco_state_stringbuilding_start;
case 'Q':
case 'R': return &teco_state_stringbuilding_escaped;
case 'V': return &teco_state_stringbuilding_lower;
@@ -509,7 +577,9 @@ teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctl);
+static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctl,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctl_input,
+);
static teco_state_t *
teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -523,8 +593,6 @@ teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, guni
* is that we don't try to casefold non-ANSI characters in single-byte mode.
*/
switch (ctx->mode) {
- case TECO_STRINGBUILDING_MODE_NORMAL:
- break;
case TECO_STRINGBUILDING_MODE_UPPER:
chr = ctx->codepage == SC_CP_UTF8 || chr < 0x80
? g_unichar_toupper(chr) : chr;
@@ -543,7 +611,8 @@ teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, guni
gboolean teco_state_stringbuilding_escaped_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
-TECO_DEFINE_STATE(teco_state_stringbuilding_escaped,
+static TECO_DEFINE_STATE(teco_state_stringbuilding_escaped,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_escaped_input,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)
teco_state_stringbuilding_escaped_process_edit_cmd
);
@@ -569,7 +638,9 @@ teco_state_stringbuilding_lower_ctl_input(teco_machine_stringbuilding_t *ctx, gu
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_lower_ctl);
+static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_lower_ctl,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_lower_ctl_input
+);
static teco_state_t *
teco_state_stringbuilding_lower_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -587,7 +658,9 @@ teco_state_stringbuilding_lower_input(teco_machine_stringbuilding_t *ctx, gunich
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE(teco_state_stringbuilding_lower);
+static TECO_DEFINE_STATE(teco_state_stringbuilding_lower,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_lower_input
+);
static teco_state_t *
teco_state_stringbuilding_upper_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -610,7 +683,9 @@ teco_state_stringbuilding_upper_ctl_input(teco_machine_stringbuilding_t *ctx, gu
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_upper_ctl);
+static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_upper_ctl,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_upper_ctl_input
+);
static teco_state_t *
teco_state_stringbuilding_upper_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -628,7 +703,9 @@ teco_state_stringbuilding_upper_input(teco_machine_stringbuilding_t *ctx, gunich
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE(teco_state_stringbuilding_upper);
+static TECO_DEFINE_STATE(teco_state_stringbuilding_upper,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_upper_input
+);
static teco_state_t *
teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -638,6 +715,7 @@ teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gunicha
switch (teco_ascii_toupper(chr)) {
case '\\': next = &teco_state_stringbuilding_ctle_num; break;
case 'U': next = &teco_state_stringbuilding_ctle_u; break;
+ case '<': next = &teco_state_stringbuilding_ctle_code; break;
case 'Q': next = &teco_state_stringbuilding_ctle_q; break;
case '@': next = &teco_state_stringbuilding_ctle_quote; break;
case 'N': next = &teco_state_stringbuilding_ctle_n; break;
@@ -660,7 +738,9 @@ teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gunicha
return next;
}
-TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctle);
+static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctle,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_input
+);
/* in cmdline.c */
gboolean teco_state_stringbuilding_qreg_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
@@ -711,7 +791,9 @@ teco_state_stringbuilding_ctle_num_input(teco_machine_stringbuilding_t *ctx, gun
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_num);
+static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_num,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_num_input
+);
static teco_state_t *
teco_state_stringbuilding_ctle_u_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -736,47 +818,73 @@ teco_state_stringbuilding_ctle_u_input(teco_machine_stringbuilding_t *ctx, gunic
if (!qreg->vtable->get_integer(qreg, &value, error))
return NULL;
- if (ctx->codepage == SC_CP_UTF8) {
- if (value < 0 || !g_unichar_validate(value))
- goto error_codepoint;
- switch (ctx->mode) {
- case TECO_STRINGBUILDING_MODE_NORMAL:
- break;
- case TECO_STRINGBUILDING_MODE_UPPER:
- value = g_unichar_toupper(value);
- break;
- case TECO_STRINGBUILDING_MODE_LOWER:
- value = g_unichar_tolower(value);
- break;
+ if (!teco_machine_stringbuilding_append_c(ctx, value)) {
+ g_autofree gchar *name_printable = teco_string_echo(qreg->head.name.data, qreg->head.name.len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
+ "Q-Register \"%s\" does not contain a valid codepoint", name_printable);
+ return NULL;
+ }
+
+ return &teco_state_stringbuilding_start;
+}
+
+static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_u,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_u_input
+);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_code_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
+{
+ if (chr == '>') {
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ if (!ctx->code.data) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
+ "Invalid empty ^E<> specified");
+ return NULL;
}
- teco_string_append_wc(ctx->result, value);
- } else {
- if (value < 0 || value > 0xFF)
- goto error_codepoint;
- switch (ctx->mode) {
- case TECO_STRINGBUILDING_MODE_NORMAL:
- break;
- case TECO_STRINGBUILDING_MODE_UPPER:
- value = g_ascii_toupper(value);
- break;
- case TECO_STRINGBUILDING_MODE_LOWER:
- value = g_ascii_tolower(value);
- break;
+
+ /*
+ * FIXME: Once we support hexadecimal constants in the SciTECO
+ * language itself, we might support this syntax as well.
+ * Or should we perhaps always consider the current radix?
+ */
+ gchar *endp = ctx->code.data;
+ errno = 0;
+ gint64 code = g_ascii_strtoll(ctx->code.data, &endp, 0);
+ if (errno || endp - ctx->code.data != ctx->code.len ||
+ !teco_machine_stringbuilding_append_c(ctx, code)) {
+ /* will also catch embedded nulls */
+ g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
+ "Invalid code ^E<%s> specified", ctx->code.data);
+ return NULL;
}
- teco_string_append_c(ctx->result, value);
+
+ if (ctx->parent.must_undo)
+ teco_undo_string_own(ctx->code);
+ else
+ teco_string_clear(&ctx->code);
+ memset(&ctx->code, 0, sizeof(ctx->code));
+
+ return &teco_state_stringbuilding_start;
}
- return &teco_state_stringbuilding_start;
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_ctle_code;
-error_codepoint: {
- g_autofree gchar *name_printable = teco_string_echo(qreg->head.name.data, qreg->head.name.len);
- g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT,
- "Q-Register \"%s\" does not contain a valid codepoint", name_printable);
- return NULL;
-}
+ if (ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->code, ctx->code.len);
+ teco_string_append_wc(&ctx->code, chr);
+
+ return &teco_state_stringbuilding_ctle_code;
}
-TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_u);
+static TECO_DEFINE_STATE(teco_state_stringbuilding_ctle_code,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_code_input
+);
static teco_state_t *
teco_state_stringbuilding_ctle_q_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -804,7 +912,9 @@ teco_state_stringbuilding_ctle_q_input(teco_machine_stringbuilding_t *ctx, gunic
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_q);
+static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_q,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_q_input
+);
static teco_state_t *
teco_state_stringbuilding_ctle_quote_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -836,7 +946,7 @@ teco_state_stringbuilding_ctle_quote_input(teco_machine_stringbuilding_t *ctx, g
* in command line arguments anyway.
* Otherwise, we'd have to implement our own POSIX shell escape function.
*/
- if (teco_string_contains(&str, '\0')) {
+ if (teco_string_contains(str, '\0')) {
teco_error_qregcontainsnull_set(error, qreg->head.name.data, qreg->head.name.len,
table != &teco_qreg_table_globals);
return NULL;
@@ -847,7 +957,9 @@ teco_state_stringbuilding_ctle_quote_input(teco_machine_stringbuilding_t *ctx, g
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_quote);
+static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_quote,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_quote_input
+);
static teco_state_t *
teco_state_stringbuilding_ctle_n_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error)
@@ -872,7 +984,7 @@ teco_state_stringbuilding_ctle_n_input(teco_machine_stringbuilding_t *ctx, gunic
g_auto(teco_string_t) str = {NULL, 0};
if (!qreg->vtable->get_string(qreg, &str.data, &str.len, NULL, error))
return NULL;
- if (teco_string_contains(&str, '\0')) {
+ if (teco_string_contains(str, '\0')) {
teco_error_qregcontainsnull_set(error, qreg->head.name.data, qreg->head.name.len,
table != &teco_qreg_table_globals);
return NULL;
@@ -884,7 +996,9 @@ teco_state_stringbuilding_ctle_n_input(teco_machine_stringbuilding_t *ctx, gunic
return &teco_state_stringbuilding_start;
}
-TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_n);
+static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_n,
+ .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_n_input
+);
void
teco_machine_stringbuilding_init(teco_machine_stringbuilding_t *ctx, gunichar escape_char,
@@ -922,6 +1036,11 @@ teco_machine_stringbuilding_escape(teco_machine_stringbuilding_t *ctx, const gch
for (guint i = 0; i < len; ) {
gunichar chr = g_utf8_get_char(str+i);
+ /*
+ * NOTE: We support both `[` and `{`, so this works for autocompleting
+ * long Q-register specifications as well.
+ * This may therefore insert unnecessary ^Q, but they won't hurt.
+ */
if (g_unichar_toupper(chr) == ctx->escape_char ||
(ctx->escape_char == '[' && chr == ']') ||
(ctx->escape_char == '{' && chr == '}'))
@@ -939,8 +1058,8 @@ teco_machine_stringbuilding_escape(teco_machine_stringbuilding_t *ctx, const gch
void
teco_machine_stringbuilding_clear(teco_machine_stringbuilding_t *ctx)
{
- if (ctx->machine_qregspec)
- teco_machine_qregspec_free(ctx->machine_qregspec);
+ teco_machine_qregspec_free(ctx->machine_qregspec);
+ teco_string_clear(&ctx->code);
}
gboolean
@@ -958,30 +1077,28 @@ teco_state_expectstring_input(teco_machine_main_t *ctx, gunichar chr, GError **e
teco_state_t *current = ctx->parent.current;
/*
- * String termination handling
+ * Ignore whitespace immediately after @-modified commands.
+ * This is inspired by TECO-64.
+ * The alternative would have been to throw an error,
+ * as allowing whitespace escape_chars is harmful.
*/
- if (ctx->flags.modifier_at) {
- if (current->expectstring.last)
- /* also clears the "@" modifier flag */
- teco_machine_main_eval_at(ctx);
+ if (ctx->flags.modifier_at && teco_is_noop(chr))
+ return current;
+ /*
+ * String termination handling
+ */
+ if (teco_machine_main_eval_at(ctx)) {
/*
- * FIXME: Exclude setting at least whitespace characters as the
- * new string escape character to avoid accidental errors?
- *
* FIXME: Should we perhaps restrict case folding escape characters
* to the ANSI range (teco_ascii_toupper())?
- * This would be faster than case folding each and every character
+ * This would be faster than case folding almost all characters
* of a string argument to check against the escape char.
*/
- switch (ctx->expectstring.machine.escape_char) {
- case '\e':
- case '{':
- if (ctx->parent.must_undo)
- teco_undo_gunichar(ctx->expectstring.machine.escape_char);
- ctx->expectstring.machine.escape_char = g_unichar_toupper(chr);
- return current;
- }
+ if (ctx->parent.must_undo)
+ teco_undo_gunichar(ctx->expectstring.machine.escape_char);
+ ctx->expectstring.machine.escape_char = g_unichar_toupper(chr);
+ return current;
}
/*
@@ -1019,11 +1136,11 @@ teco_state_expectstring_input(teco_machine_main_t *ctx, gunichar chr, GError **e
* so they may do their main activity in process_cb().
*/
if (ctx->expectstring.insert_len && current->expectstring.process_cb &&
- !current->expectstring.process_cb(ctx, &ctx->expectstring.string,
+ !current->expectstring.process_cb(ctx, ctx->expectstring.string,
ctx->expectstring.insert_len, error))
return NULL;
- teco_state_t *next = current->expectstring.done_cb(ctx, &ctx->expectstring.string, error);
+ teco_state_t *next = current->expectstring.done_cb(ctx, ctx->expectstring.string, error);
if (ctx->parent.must_undo)
teco_undo_string_own(ctx->expectstring.string);
@@ -1035,6 +1152,14 @@ teco_state_expectstring_input(teco_machine_main_t *ctx, gunichar chr, GError **e
if (ctx->parent.must_undo)
teco_undo_gunichar(ctx->expectstring.machine.escape_char);
ctx->expectstring.machine.escape_char = '\e';
+ } else if (ctx->expectstring.machine.escape_char == '{') {
+ /*
+ * Makes sure that after all but the last string argument,
+ * the escape character is reset, as in @FR{foo}{bar}.
+ */
+ if (ctx->parent.must_undo)
+ teco_undo_flags(ctx->flags);
+ ctx->flags.modifier_at = TRUE;
}
ctx->expectstring.nesting = 1;
@@ -1090,7 +1215,7 @@ teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error)
/* never calls process_cb() in parse-only mode */
if (ctx->expectstring.insert_len && current->expectstring.process_cb &&
- !current->expectstring.process_cb(ctx, &ctx->expectstring.string,
+ !current->expectstring.process_cb(ctx, ctx->expectstring.string,
ctx->expectstring.insert_len, error))
return FALSE;
@@ -1102,10 +1227,10 @@ teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error)
}
gboolean
-teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str,
+teco_state_expectfile_process(teco_machine_main_t *ctx, teco_string_t str,
gsize new_chars, GError **error)
{
- g_assert(str->data != NULL);
+ g_assert(str.data != NULL);
/*
* Null-chars must not occur in filename/path strings and at some point
@@ -1114,7 +1239,7 @@ teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str
* Doing it here ensures that teco_file_expand_path() can be safely called
* from the done_cb().
*/
- if (memchr(str->data + str->len - new_chars, '\0', new_chars)) {
+ if (memchr(str.data + str.len - new_chars, '\0', new_chars)) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Null-character not allowed in filenames");
return FALSE;
diff --git a/src/parser.h b/src/parser.h
index 5477150..0c389cc 100644
--- a/src/parser.h
+++ b/src/parser.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -75,7 +75,9 @@ void undo__remove_index__teco_loop_stack(guint);
* FIXME: Maybe use TECO_DECLARE_VTABLE_METHOD()?
*/
typedef const struct {
+ /** whether string building characters are enabled by default */
guint string_building : 1;
+ /** whether this string argument is the last of the command */
guint last : 1;
/**
@@ -83,7 +85,7 @@ typedef const struct {
*
* Can be NULL if no interactive feedback is required.
*/
- gboolean (*process_cb)(teco_machine_main_t *ctx, const teco_string_t *str,
+ gboolean (*process_cb)(teco_machine_main_t *ctx, teco_string_t str,
gsize new_chars, GError **error);
/**
@@ -91,7 +93,7 @@ typedef const struct {
* Commands that don't give interactive feedback can use this callback
* to perform their main processing.
*/
- teco_state_t *(*done_cb)(teco_machine_main_t *ctx, const teco_string_t *str, GError **error);
+ teco_state_t *(*done_cb)(teco_machine_main_t *ctx, teco_string_t str, GError **error);
} teco_state_expectstring_t;
typedef const struct {
@@ -108,7 +110,7 @@ typedef gboolean (*teco_state_refresh_cb_t)(teco_machine_t *ctx, GError **error)
typedef gboolean (*teco_state_end_of_macro_cb_t)(teco_machine_t *ctx, GError **error);
typedef gboolean (*teco_state_process_edit_cmd_cb_t)(teco_machine_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
-typedef gboolean (*teco_state_insert_completion_cb_t)(teco_machine_t *ctx, const teco_string_t *str, GError **error);
+typedef gboolean (*teco_state_insert_completion_cb_t)(teco_machine_t *ctx, teco_string_t str, GError **error);
typedef enum {
TECO_KEYMACRO_MASK_START = (1 << 0),
@@ -246,15 +248,18 @@ gboolean teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent
* @implements teco_state_t
* @ingroup states
*
- * @todo Should we eliminate required callbacks, this could be turned into a
- * struct initializer TECO_INIT_STATE() and TECO_DECLARE_STATE() would become pointless.
- * This would also ease declaring static states.
+ * Base class of all states.
+ *
+ * Since states are constant, you can append static assertions for required callbacks
+ * and other conditions.
+ * You should use TECO_ASSERT_SAFE(), but it won't be checked on all supported compilers.
+ * You should not put anything in front of the definition, though, so you
+ * can write `static TECO_DEFINE_STATE(...)`.
*/
#define TECO_DEFINE_STATE(NAME, ...) \
/** @ingroup states */ \
teco_state_t NAME = { \
.initial_cb = NULL, /* do nothing */ \
- .input_cb = (teco_state_input_cb_t)NAME##_input, /* always required */ \
.refresh_cb = NULL, /* do nothing */ \
.end_of_macro_cb = teco_state_end_of_macro, \
.process_edit_cmd_cb = teco_state_process_edit_cmd, \
@@ -263,11 +268,8 @@ gboolean teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent
.keymacro_mask = TECO_KEYMACRO_MASK_DEFAULT, \
.style = SCE_SCITECO_DEFAULT, \
##__VA_ARGS__ \
- }
-
-/** @ingroup states */
-#define TECO_DECLARE_STATE(NAME) \
- extern teco_state_t NAME
+ }; \
+ TECO_ASSERT_SAFE(NAME.input_cb != NULL)
/* in cmdline.c */
gboolean teco_state_caseinsensitive_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gunichar chr, GError **error);
@@ -332,7 +334,8 @@ gboolean teco_machine_input(teco_machine_t *ctx, gunichar chr, GError **error);
typedef enum {
TECO_STRINGBUILDING_MODE_NORMAL = 0,
TECO_STRINGBUILDING_MODE_UPPER,
- TECO_STRINGBUILDING_MODE_LOWER
+ TECO_STRINGBUILDING_MODE_LOWER,
+ TECO_STRINGBUILDING_MODE_DISABLED
} teco_stringbuilding_mode_t;
/**
@@ -383,6 +386,13 @@ typedef struct teco_machine_stringbuilding_t {
* the buffer's or Q-Register's encoding.
*/
guint codepage;
+
+ /**
+ * String to collect code from `^E<...>` constructs.
+ * This could waste some memory for string arguments with nested Q-Reg specs,
+ * but we better keep it here than adding another global variable.
+ */
+ teco_string_t code;
} teco_machine_stringbuilding_t;
void teco_machine_stringbuilding_init(teco_machine_stringbuilding_t *ctx, gunichar escape_char,
@@ -484,6 +494,9 @@ struct teco_machine_main_t {
* This is tracked even in parse-only mode.
*/
guint modifier_at : 1;
+
+ /** whether <EB> command accepts a filename */
+ guint allow_filename : 1;
} flags;
/** The nesting level of braces */
@@ -506,17 +519,19 @@ struct teco_machine_main_t {
/*
* teco_state_t-dependent state.
*
- * Some of these cannot be used concurrently and are therefore
- * grouped into unions.
- * We could further optimize memory usage by dynamically allocating
- * some of these structures on demand.
+ * Some cannot theoretically be used at the same time
+ * but it's hard to prevent memory leaks if putting them into
+ * a common union.
*/
- teco_machine_expectstring_t expectstring;
- union {
- teco_string_t goto_label;
- teco_machine_qregspec_t *expectqreg;
- teco_machine_scintilla_t scintilla;
- };
+ teco_machine_expectstring_t expectstring;
+ /**
+ * State machine for parsing Q-reg specifications.
+ * This could theoretically be inlined, but it would introduce
+ * a recursive dependency between qreg.h and parser.h.
+ */
+ teco_machine_qregspec_t *expectqreg;
+ teco_string_t goto_label;
+ teco_machine_scintilla_t scintilla;
};
typedef struct teco_machine_main_flags_t teco_machine_main_flags_t;
@@ -584,7 +599,7 @@ gboolean teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **erro
/* in cmdline.c */
gboolean teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
-gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str,
+gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, teco_string_t str,
GError **error);
/**
@@ -595,18 +610,11 @@ gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, con
* Super-class for states accepting string arguments
* Opaquely cares about alternative-escape characters,
* string building commands and accumulation into a string
- *
- * @note Generating the input_cb could be avoided if there were a default
- * implementation.
*/
#define TECO_DEFINE_STATE_EXPECTSTRING(NAME, ...) \
- static teco_state_t * \
- NAME##_input(teco_machine_main_t *ctx, gunichar chr, GError **error) \
- { \
- return teco_state_expectstring_input(ctx, chr, error); \
- } \
TECO_DEFINE_STATE(NAME, \
.initial_cb = (teco_state_initial_cb_t)teco_state_expectstring_initial, \
+ .input_cb = (teco_state_input_cb_t)teco_state_expectstring_input, \
.refresh_cb = (teco_state_refresh_cb_t)teco_state_expectstring_refresh, \
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
teco_state_expectstring_process_edit_cmd, \
@@ -616,17 +624,17 @@ gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, con
.style = SCE_SCITECO_STRING, \
.expectstring.string_building = TRUE, \
.expectstring.last = TRUE, \
- .expectstring.process_cb = NULL, /* do nothing */ \
- .expectstring.done_cb = NAME##_done, /* always required */ \
+ .expectstring.process_cb = NULL, /* do nothing */ \
##__VA_ARGS__ \
- )
+ ); \
+ TECO_ASSERT_SAFE(NAME.expectstring.done_cb != NULL)
-gboolean teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str,
+gboolean teco_state_expectfile_process(teco_machine_main_t *ctx, teco_string_t str,
gsize new_chars, GError **error);
/* in cmdline.c */
gboolean teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error);
-gboolean teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error);
+gboolean teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error);
/**
* @interface TECO_DEFINE_STATE_EXPECTFILE
@@ -645,7 +653,7 @@ gboolean teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, const
/* in cmdline.c */
gboolean teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error);
-gboolean teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error);
+gboolean teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error);
/**
* @interface TECO_DEFINE_STATE_EXPECTDIR
diff --git a/src/qreg-commands.c b/src/qreg-commands.c
index a4019a0..4ede403 100644
--- a/src/qreg-commands.c
+++ b/src/qreg-commands.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -39,8 +39,8 @@ teco_state_expectqreg_initial(teco_machine_main_t *ctx, GError **error)
teco_state_t *current = ctx->parent.current;
/*
- * NOTE: We have to allocate a new instance always since `expectqreg`
- * is part of an union.
+ * NOTE: This could theoretically be allocated once in
+ * teco_machine_main_init(), but we'd have to set the type here anyway.
*/
ctx->expectqreg = teco_machine_qregspec_new(current->expectqreg.type, ctx->qreg_table_locals,
ctx->parent.must_undo);
@@ -54,8 +54,8 @@ teco_state_expectqreg_input(teco_machine_main_t *ctx, gunichar chr, GError **err
{
teco_state_t *current = ctx->parent.current;
- teco_qreg_t *qreg;
- teco_qreg_table_t *table;
+ teco_qreg_t *qreg = NULL;
+ teco_qreg_table_t *table = NULL;
switch (teco_machine_qregspec_input(ctx->expectqreg, chr,
ctx->flags.mode == TECO_MODE_NORMAL ? &qreg : NULL, &table, error)) {
@@ -69,7 +69,7 @@ teco_state_expectqreg_input(teco_machine_main_t *ctx, gunichar chr, GError **err
/*
* NOTE: ctx->expectqreg is preserved since we may want to query it from follow-up
- * states. This means, it must usually be stored manually in got_register_cb() via:
+ * states. This means, it must usually be reset manually in got_register_cb() via:
* teco_state_expectqreg_reset(ctx);
*/
return current->expectqreg.got_register_cb(ctx, qreg, table, error);
@@ -91,7 +91,9 @@ teco_state_pushqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
* Save Q-Register <q> contents on the global Q-Register push-down
* stack.
*/
-TECO_DEFINE_STATE_EXPECTQREG(teco_state_pushqreg);
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_pushqreg,
+ .expectqreg.got_register_cb = teco_state_pushqreg_got_register
+);
static teco_state_t *
teco_state_popqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
@@ -99,25 +101,37 @@ teco_state_popqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
{
teco_state_expectqreg_reset(ctx);
- return ctx->flags.mode == TECO_MODE_NORMAL &&
- !teco_qreg_stack_pop(qreg, error) ? NULL : &teco_state_start;
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (!teco_machine_main_eval_colon(ctx))
+ return !teco_qreg_stack_pop(qreg, error) ? NULL : &teco_state_start;
+ teco_expressions_push(teco_bool(teco_qreg_stack_pop(qreg, NULL)));
+ return &teco_state_start;
}
-/*$ "]" "]q" pop
+/*$ "]" "]q" ":]q" pop
* ]q -- Restore Q-Register
+ * :]q -> Success|Failure
*
* Restore Q-Register <q> by replacing its contents
* with the contents of the register saved on top of
* the Q-Register push-down stack.
* The stack entry is popped.
*
+ * When colon-modified, \fB]\fP returns a success boolean
+ * (-1) if there was a register to pop.
+ * If the stack was empty, a failure boolean (0) is returned
+ * instead of throwing an error.
+ *
* In interactive mode, the original contents of <q>
* are not immediately reclaimed but are kept in memory
* to support rubbing out the command.
* Memory is reclaimed on command-line termination.
*/
TECO_DEFINE_STATE_EXPECTQREG(teco_state_popqreg,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_popqreg_got_register
);
static teco_state_t *
@@ -131,11 +145,12 @@ teco_state_eqcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
}
TECO_DEFINE_STATE_EXPECTQREG(teco_state_eqcommand,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_eqcommand_got_register
);
static teco_state_t *
-teco_state_loadqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_loadqreg_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_qreg_t *qreg;
teco_qreg_table_t *table;
@@ -146,9 +161,9 @@ teco_state_loadqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GEr
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
- if (str->len > 0) {
+ if (str.len > 0) {
/* Load file into Q-Register */
- g_autofree gchar *filename = teco_file_expand_path(str->data);
+ g_autofree gchar *filename = teco_file_expand_path(str.data);
if (!qreg->vtable->load(qreg, filename, error))
return NULL;
} else {
@@ -176,7 +191,9 @@ teco_state_loadqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GEr
* Undefined Q-Registers will be defined.
* The command fails if <file> could not be read.
*/
-TECO_DEFINE_STATE_EXPECTFILE(teco_state_loadqreg);
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_loadqreg,
+ .expectstring.done_cb = teco_state_loadqreg_done
+);
static teco_state_t *
teco_state_epctcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
@@ -188,10 +205,12 @@ teco_state_epctcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
return &teco_state_saveqreg;
}
-TECO_DEFINE_STATE_EXPECTQREG(teco_state_epctcommand);
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_epctcommand,
+ .expectqreg.got_register_cb = teco_state_epctcommand_got_register
+);
static teco_state_t *
-teco_state_saveqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_saveqreg_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
teco_qreg_t *qreg;
@@ -201,7 +220,7 @@ teco_state_saveqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GEr
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
- g_autofree gchar *filename = teco_file_expand_path(str->data);
+ g_autofree gchar *filename = teco_file_expand_path(str.data);
return qreg->vtable->save(qreg, filename, error) ? &teco_state_start : NULL;
}
@@ -220,7 +239,9 @@ teco_state_saveqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GEr
* File names may also be tab-completed and string building
* characters are enabled by default.
*/
-TECO_DEFINE_STATE_EXPECTFILE(teco_state_saveqreg);
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_saveqreg,
+ .expectstring.done_cb = teco_state_saveqreg_done
+);
static gboolean
teco_state_queryqreg_initial(teco_machine_main_t *ctx, GError **error)
@@ -273,9 +294,7 @@ teco_state_queryqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
if (teco_expressions_args() > 0) {
/* Query character from Q-Register string */
- teco_int_t pos;
- if (!teco_expressions_pop_num_calc(&pos, 0, error))
- return NULL;
+ teco_int_t pos = teco_expressions_pop_num(0);
if (pos < 0) {
teco_error_range_set(error, "Q");
return NULL;
@@ -297,24 +316,24 @@ teco_state_queryqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
return &teco_state_start;
}
-/*$ Q Qq query
+/*$ "Q" "Qq" ":Qq" query
* Qq -> n -- Query Q-Register existence, its integer or string characters
* -Qq -> -n
- * <position>Qq -> character
+ * <position>Qq -> code
* :Qq -> -1 | size
*
* Without any arguments, get and return the integer-part of
* Q-Register <q>.
*
- * With one argument, return the <character> code at <position>
+ * With one argument, return the character <code> at <position>
* from the string-part of Q-Register <q>.
* Positions are handled like buffer positions \(em they
* begin at 0 up to the length of the string minus 1.
- * An error is thrown for invalid positions.
+ * -1 is returned for invalid positions.
* If <q> is encoded as UTF-8 and there is
- * an incomplete sequence at the requested position,
- * -1 is returned.
- * All other invalid Unicode sequences are returned as -2.
+ * an invalid byte sequence at the requested position,
+ * -2 is returned.
+ * Incomplete UTF-8 byte sequences are returned as -3.
* Both non-colon-modified forms of Q require register <q>
* to be defined and fail otherwise.
*
@@ -337,7 +356,8 @@ teco_state_queryqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
* boolean.
*/
TECO_DEFINE_STATE_EXPECTQREG(teco_state_queryqreg,
- .initial_cb = (teco_state_initial_cb_t)teco_state_queryqreg_initial
+ .initial_cb = (teco_state_initial_cb_t)teco_state_queryqreg_initial,
+ .expectqreg.got_register_cb = teco_state_queryqreg_got_register
);
static teco_state_t *
@@ -351,12 +371,13 @@ teco_state_ctlucommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
}
TECO_DEFINE_STATE_EXPECTQREG(teco_state_ctlucommand,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_ctlucommand_got_register
);
static teco_state_t *
teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx,
- const teco_string_t *str, GError **error)
+ teco_string_t str, GError **error)
{
teco_qreg_t *qreg;
@@ -378,43 +399,46 @@ teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx,
return NULL;
g_autofree gchar *buffer = NULL;
+ const gchar *start;
gsize len = 0;
if (codepage == SC_CP_UTF8) {
- /* the glib docs wrongly claim that one character can take 6 bytes */
- buffer = g_malloc(4*args);
+ /* 4 bytes should be enough for UTF-8, but we better follow the documentation */
+ start = buffer = g_malloc(args*6);
+
for (gint i = args; i > 0; i--) {
- teco_int_t v;
- if (!teco_expressions_pop_num_calc(&v, 0, error))
- return NULL;
- if (v < 0 || !g_unichar_validate(v)) {
+ teco_int_t chr = teco_expressions_peek_num(i-1);
+ if (chr < 0 || !g_unichar_validate(chr)) {
teco_error_codepoint_set(error, "^U");
return NULL;
}
- len += g_unichar_to_utf8(v, buffer+len);
+ len += g_unichar_to_utf8(chr, buffer+len);
}
+ /* we pop only now since we had to peek in reverse order */
+ for (gint i = 0; i < args; i++)
+ teco_expressions_pop_num(0);
} else {
buffer = g_malloc(args);
- for (gint i = args; i > 0; i--) {
- teco_int_t v;
- if (!teco_expressions_pop_num_calc(&v, 0, error))
- return NULL;
- if (v < 0 || v > 0xFF) {
+
+ for (gint i = 0; i < args; i++) {
+ teco_int_t chr = teco_expressions_pop_num(0);
+ if (chr < 0 || chr > 0xFF) {
teco_error_codepoint_set(error, "^U");
return NULL;
}
- buffer[len++] = v;
+ buffer[args-(++len)] = chr;
}
+ start = buffer+args-len;
}
if (colon_modified) {
/* append to register */
- if (!qreg->vtable->append_string(qreg, buffer, len, error))
+ if (!qreg->vtable->append_string(qreg, start, len, error))
return NULL;
} else {
/* set register */
if (!qreg->vtable->undo_set_string(qreg, error) ||
- !qreg->vtable->set_string(qreg, buffer, len,
+ !qreg->vtable->set_string(qreg, start, len,
codepage, error))
return NULL;
}
@@ -422,12 +446,12 @@ teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx,
if (args > 0 || colon_modified) {
/* append to register */
- if (!qreg->vtable->append_string(qreg, str->data, str->len, error))
+ if (!qreg->vtable->append_string(qreg, str.data, str.len, error))
return NULL;
} else {
/* set register */
if (!qreg->vtable->undo_set_string(qreg, error) ||
- !qreg->vtable->set_string(qreg, str->data, str->len,
+ !qreg->vtable->set_string(qreg, str.data, str.len,
teco_default_codepage(), error))
return NULL;
}
@@ -435,7 +459,7 @@ teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx,
return &teco_state_start;
}
-/*$ ^Uq
+/*$ "^Uq" ":^Uq" "set string" append
* [c1,c2,...]^Uq[string]$ -- Set or append to Q-Register string without string building
* [c1,c2,...]:^Uq[string]$
*
@@ -462,7 +486,8 @@ teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx,
* is desired.
*/
TECO_DEFINE_STATE_EXPECTSTRING(teco_state_setqregstring_nobuilding,
- .expectstring.string_building = FALSE
+ .expectstring.string_building = FALSE,
+ .expectstring.done_cb = teco_state_setqregstring_nobuilding_done
);
static teco_state_t *
@@ -476,7 +501,8 @@ teco_state_eucommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
}
TECO_DEFINE_STATE_EXPECTQREG(teco_state_eucommand,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_eucommand_got_register
);
static gboolean
@@ -499,13 +525,7 @@ teco_state_setqregstring_building_initial(teco_machine_main_t *ctx, GError **err
return TRUE;
}
-static teco_state_t *
-teco_state_setqregstring_building_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
-{
- return teco_state_setqregstring_nobuilding_done(ctx, str, error);
-}
-
-/*$ EU EUq
+/*$ "EU" "EUq" ":EUq"
* [c1,c2,...]EUq[string]$ -- Set or append to Q-Register string with string building characters
* [c1,c2,...]:EUq[string]$
*
@@ -517,7 +537,8 @@ teco_state_setqregstring_building_done(teco_machine_main_t *ctx, const teco_stri
*/
TECO_DEFINE_STATE_EXPECTSTRING(teco_state_setqregstring_building,
.initial_cb = (teco_state_initial_cb_t)teco_state_setqregstring_building_initial,
- .expectstring.string_building = TRUE
+ .expectstring.string_building = TRUE,
+ .expectstring.done_cb = teco_state_setqregstring_nobuilding_done
);
static teco_state_t *
@@ -534,9 +555,12 @@ teco_state_getqregstring_got_register(teco_machine_main_t *ctx, teco_qreg_t *qre
if (!qreg->vtable->get_string(qreg, &str.data, &str.len, NULL, error))
return NULL;
- teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
- teco_undo_gsize(teco_ranges[0].to) = teco_ranges[0].from + str.len;
- teco_undo_guint(teco_ranges_count) = 1;
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_interface_msg_literal(TECO_MSG_USER, str.data, str.len);
+ return &teco_state_start;
+ }
+
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
if (str.len > 0) {
teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
@@ -548,17 +572,28 @@ teco_state_getqregstring_got_register(teco_machine_main_t *ctx, teco_qreg_t *qre
undo__teco_interface_ssm(SCI_UNDO, 0, 0);
}
+ teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos);
+ teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos + str.len);
+ teco_undo_guint(teco_ranges_count) = 1;
+
return &teco_state_start;
}
-/*$ G Gq get
- * Gq -- Insert Q-Register string
+/*$ G Gq get paste
+ * Gq -- Insert or print Q-Register string
+ * :Gq
*
* Inserts the string of Q-Register <q> into the buffer
* at its current position.
+ * If colon-modified prints the string as a message
+ * (i.e. to the terminal and/or in the message area) instead
+ * of modifying the current buffer.
+ *
* Specifying an undefined <q> yields an error.
*/
-TECO_DEFINE_STATE_EXPECTQREG(teco_state_getqregstring);
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_getqregstring,
+ .expectqreg.got_register_cb = teco_state_getqregstring_got_register
+);
static teco_state_t *
teco_state_setqreginteger_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
@@ -590,7 +625,7 @@ teco_state_setqreginteger_got_register(teco_machine_main_t *ctx, teco_qreg_t *qr
return &teco_state_start;
}
-/*$ U Uq
+/*$ "U" "Uq" ":Uq" set
* nUq -- Set Q-Register integer
* -Uq
* [n]:Uq -> Success|Failure
@@ -607,7 +642,8 @@ teco_state_setqreginteger_got_register(teco_machine_main_t *ctx, teco_qreg_t *qr
* The register is defined if it does not exist.
*/
TECO_DEFINE_STATE_EXPECTQREG(teco_state_setqreginteger,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_setqreginteger_got_register
);
static teco_state_t *
@@ -641,7 +677,8 @@ teco_state_increaseqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg
* <q> will be defined if it does not exist.
*/
TECO_DEFINE_STATE_EXPECTQREG(teco_state_increaseqreg,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_increaseqreg_got_register
);
static teco_state_t *
@@ -674,7 +711,7 @@ teco_state_macro_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
return &teco_state_start;
}
-/*$ M Mq eval
+/*$ "M" "Mq" ":Mq" call eval macro
* Mq -- Execute macro
* :Mq
*
@@ -700,15 +737,17 @@ teco_state_macro_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
* (as reported by \fBEE\fP), its contents must be and are checked to be in
* valid UTF-8.
*/
-TECO_DEFINE_STATE_EXPECTQREG(teco_state_macro);
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_macro,
+ .expectqreg.got_register_cb = teco_state_macro_got_register
+);
static teco_state_t *
-teco_state_macrofile_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_indirect_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
- g_autofree gchar *filename = teco_file_expand_path(str->data);
+ g_autofree gchar *filename = teco_file_expand_path(str.data);
if (teco_machine_main_eval_colon(ctx) > 0) {
/* don't create new local Q-Registers if colon modifier is given */
@@ -725,9 +764,9 @@ teco_state_macrofile_done(teco_machine_main_t *ctx, const teco_string_t *str, GE
return &teco_state_start;
}
-/*$ EM
- * EMfile$ -- Execute macro from file
- * :EMfile$
+/*$ "EI" ":EI" indirect include
+ * EIfile$ -- Execute from indirect command file
+ * :EIfile$
*
* Read the file with name <file> into memory and execute its contents
* as a macro.
@@ -738,7 +777,9 @@ teco_state_macrofile_done(teco_machine_main_t *ctx, const teco_string_t *str, GE
* As all \*(ST code, the contents of <file> must be in valid UTF-8
* even if operating in the \(lqdefault ANSI\(rq mode as configured by \fBED\fP.
*/
-TECO_DEFINE_STATE_EXPECTFILE(teco_state_macrofile);
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_indirect,
+ .expectstring.done_cb = teco_state_indirect_done
+);
static teco_state_t *
teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
@@ -757,39 +798,10 @@ teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
- gssize from, len; /* in bytes */
+ gsize from, len;
- if (!teco_expressions_eval(FALSE, error))
+ if (!teco_get_range_args("X", &from, &len, error))
return NULL;
- if (teco_expressions_args() <= 1) {
- teco_int_t line;
-
- from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
- if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error))
- return NULL;
- line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0);
-
- if (!teco_validate_line(line)) {
- teco_error_range_set(error, "X");
- return NULL;
- }
-
- len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from;
-
- if (len < 0) {
- from += len;
- len *= -1;
- }
- } else {
- gssize to = teco_interface_glyphs2bytes(teco_expressions_pop_num(0));
- from = teco_interface_glyphs2bytes(teco_expressions_pop_num(0));
- len = to - from;
-
- if (len < 0 || from < 0 || to < 0) {
- teco_error_range_set(error, "X");
- return NULL;
- }
- }
/*
* NOTE: This does not use SCI_GETRANGEPOINTER+SCI_GETGAPPOSITION
@@ -837,7 +849,7 @@ teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
return &teco_state_start;
}
-/*$ X Xq
+/*$ "X" "Xq" ":Xq" "@Xq" ":@Xq" copy extract
* [lines]Xq -- Copy into or append or cut to Q-Register
* -Xq
* from,toXq
@@ -870,5 +882,6 @@ teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
* Register <q> will be created if it is undefined.
*/
TECO_DEFINE_STATE_EXPECTQREG(teco_state_copytoqreg,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_copytoqreg_got_register
);
diff --git a/src/qreg-commands.h b/src/qreg-commands.h
index f6ad82a..51f792b 100644
--- a/src/qreg-commands.h
+++ b/src/qreg-commands.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -40,7 +40,7 @@ teco_state_t *teco_state_expectqreg_input(teco_machine_main_t *ctx, gunichar chr
/* in cmdline.c */
gboolean teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
-gboolean teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str,
+gboolean teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, teco_string_t str,
GError **error);
/**
@@ -51,48 +51,44 @@ gboolean teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, const
* Super class for states accepting Q-Register specifications.
*/
#define TECO_DEFINE_STATE_EXPECTQREG(NAME, ...) \
- static teco_state_t * \
- NAME##_input(teco_machine_main_t *ctx, gunichar chr, GError **error) \
- { \
- return teco_state_expectqreg_input(ctx, chr, error); \
- } \
TECO_DEFINE_STATE(NAME, \
.initial_cb = (teco_state_initial_cb_t)teco_state_expectqreg_initial, \
+ .input_cb = (teco_state_input_cb_t)teco_state_expectqreg_input, \
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
teco_state_expectqreg_process_edit_cmd, \
.insert_completion_cb = (teco_state_insert_completion_cb_t) \
teco_state_expectqreg_insert_completion, \
.style = SCE_SCITECO_QREG, \
.expectqreg.type = TECO_QREG_REQUIRED, \
- .expectqreg.got_register_cb = NAME##_got_register, /* always required */ \
##__VA_ARGS__ \
- )
+ ); \
+ TECO_ASSERT_SAFE(NAME.expectqreg.got_register_cb != NULL)
/*
* FIXME: Some of these states are referenced only in qreg-commands.c,
* so they should be moved there?
*/
-TECO_DECLARE_STATE(teco_state_pushqreg);
-TECO_DECLARE_STATE(teco_state_popqreg);
+extern teco_state_t teco_state_pushqreg;
+extern teco_state_t teco_state_popqreg;
-TECO_DECLARE_STATE(teco_state_eqcommand);
-TECO_DECLARE_STATE(teco_state_loadqreg);
+extern teco_state_t teco_state_eqcommand;
+extern teco_state_t teco_state_loadqreg;
-TECO_DECLARE_STATE(teco_state_epctcommand);
-TECO_DECLARE_STATE(teco_state_saveqreg);
+extern teco_state_t teco_state_epctcommand;
+extern teco_state_t teco_state_saveqreg;
-TECO_DECLARE_STATE(teco_state_queryqreg);
+extern teco_state_t teco_state_queryqreg;
-TECO_DECLARE_STATE(teco_state_ctlucommand);
-TECO_DECLARE_STATE(teco_state_setqregstring_nobuilding);
-TECO_DECLARE_STATE(teco_state_eucommand);
-TECO_DECLARE_STATE(teco_state_setqregstring_building);
+extern teco_state_t teco_state_ctlucommand;
+extern teco_state_t teco_state_setqregstring_nobuilding;
+extern teco_state_t teco_state_eucommand;
+extern teco_state_t teco_state_setqregstring_building;
-TECO_DECLARE_STATE(teco_state_getqregstring);
-TECO_DECLARE_STATE(teco_state_setqreginteger);
-TECO_DECLARE_STATE(teco_state_increaseqreg);
+extern teco_state_t teco_state_getqregstring;
+extern teco_state_t teco_state_setqreginteger;
+extern teco_state_t teco_state_increaseqreg;
-TECO_DECLARE_STATE(teco_state_macro);
-TECO_DECLARE_STATE(teco_state_macrofile);
+extern teco_state_t teco_state_macro;
+extern teco_state_t teco_state_indirect;
-TECO_DECLARE_STATE(teco_state_copytoqreg);
+extern teco_state_t teco_state_copytoqreg;
diff --git a/src/qreg.c b/src/qreg.c
index 8990210..4cf92f0 100644
--- a/src/qreg.c
+++ b/src/qreg.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -39,6 +39,7 @@
#include "ring.h"
#include "eol.h"
#include "error.h"
+#include "rb3str.h"
#include "qreg.h"
/**
@@ -239,18 +240,12 @@ teco_qreg_plain_get_character(teco_qreg_t *qreg, teco_int_t position,
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);
+ *chr = off >= 0 && off != len ? teco_view_get_character(teco_qreg_view, off, len) : -1;
if (teco_qreg_current)
teco_doc_edit(&teco_qreg_current->string, 0);
- return ret;
+ return TRUE;
}
static teco_int_t
@@ -340,7 +335,7 @@ teco_qreg_plain_load(teco_qreg_t *qreg, const gchar *filename, GError **error)
* So if loading fails, teco_qreg_current will be
* made the current document again.
*/
- if (!teco_view_load(teco_qreg_view, filename, error))
+ if (!teco_view_load(teco_qreg_view, filename, TRUE, error))
return FALSE;
if (teco_qreg_current)
@@ -492,6 +487,19 @@ teco_qreg_external_edit(teco_qreg_t *qreg, GError **error)
}
static gboolean
+teco_qreg_external_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ g_auto(teco_string_t) buf = {NULL, 0};
+ guint codepage;
+
+ if (!qreg->vtable->undo_set_string(qreg, error) ||
+ !qreg->vtable->get_string(qreg, &buf.data, &buf.len, &codepage, error))
+ return FALSE;
+ teco_string_append(&buf, str, len);
+ return qreg->vtable->set_string(qreg, buf.data, buf.len, codepage, error);
+}
+
+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};
@@ -527,9 +535,8 @@ teco_qreg_external_get_character(teco_qreg_t *qreg, teco_int_t position,
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;
+ *chr = -1;
+ return TRUE;
}
const gchar *p = g_utf8_offset_to_pointer(str.data, position);
@@ -610,6 +617,7 @@ teco_qreg_external_save(teco_qreg_t *qreg, const gchar *filename, GError **error
.exchange_string = teco_qreg_external_exchange_string, \
.undo_exchange_string = teco_qreg_external_undo_exchange_string, \
.edit = teco_qreg_external_edit, \
+ .append_string = teco_qreg_external_append_string, \
.get_character = teco_qreg_external_get_character, \
.get_length = teco_qreg_external_get_length, \
.load = teco_qreg_external_load, \
@@ -642,6 +650,8 @@ teco_qreg_bufferinfo_get_integer(teco_qreg_t *qreg, teco_int_t *ret, GError **er
/*
* FIXME: Something could be implemented here. There are 2 possibilities:
* Either it renames the current buffer, or opens a file (alternative to EB).
+ * Should we implement it, we can probably remove the append_string
+ * implementation below.
*/
static gboolean
teco_qreg_bufferinfo_set_string(teco_qreg_t *qreg, const gchar *str, gsize len,
@@ -722,7 +732,7 @@ teco_qreg_workingdir_set_string(teco_qreg_t *qreg, const gchar *str, gsize len,
g_auto(teco_string_t) dir;
teco_string_init(&dir, str, len);
- if (teco_string_contains(&dir, '\0')) {
+ if (teco_string_contains(dir, '\0')) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
"Directory contains null-character");
return FALSE;
@@ -747,17 +757,6 @@ teco_qreg_workingdir_undo_set_string(teco_qreg_t *qreg, GError **error)
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)
@@ -791,7 +790,6 @@ 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
);
@@ -801,19 +799,27 @@ teco_qreg_workingdir_new(void)
* 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.
+ * Most logical would be ".", but it is also ugly to write in practice.
* Perhaps "@"...
*/
return teco_qreg_new(&vtable, "$", 1);
}
+static inline const gchar *
+teco_qreg_clipboard_get_name(const teco_qreg_t *qreg)
+{
+ g_assert(1 <= qreg->head.name.len && qreg->head.name.len <= 2 &&
+ *qreg->head.name.data == '~');
+ if (qreg->head.name.len > 1)
+ return qreg->head.name.data+1;
+ return teco_ed & TECO_ED_CLIP_PRIMARY ? "P" : "C";
+}
+
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;
+ const gchar *clipboard_name = teco_qreg_clipboard_get_name(qreg);
if (teco_ed & TECO_ED_AUTOEOL) {
/*
@@ -849,17 +855,6 @@ teco_qreg_clipboard_set_string(teco_qreg_t *qreg, const gchar *str, gsize len,
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)
{
@@ -873,8 +868,7 @@ teco_qreg_clipboard_undo_set_string(teco_qreg_t *qreg, GError **error)
if (!teco_undo_enabled)
return TRUE;
- g_assert(!teco_string_contains(&qreg->head.name, '\0'));
- const gchar *clipboard_name = qreg->head.name.data + 1;
+ const gchar *clipboard_name = teco_qreg_clipboard_get_name(qreg);
/*
* Ownership of str is passed to the undo token.
@@ -892,8 +886,7 @@ 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;
+ const gchar *clipboard_name = teco_qreg_clipboard_get_name(qreg);
if (!(teco_ed & TECO_ED_AUTOEOL))
/*
@@ -937,8 +930,7 @@ teco_qreg_clipboard_get_string(teco_qreg_t *qreg, gchar **str, gsize *len,
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;
+ const gchar *clipboard_name = teco_qreg_clipboard_get_name(qreg);
g_auto(teco_string_t) str = {NULL, 0};
@@ -954,13 +946,18 @@ 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));
+ /*
+ * Register "~" is the default clipboard, which defaults to "~C".
+ * This is configurable via the integer cell.
+ */
+ if (qreg->head.name.len == 1)
+ qreg->integer = 'C';
return qreg;
}
@@ -974,9 +971,9 @@ teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo)
/* general purpose registers */
for (gchar q = 'A'; q <= 'Z'; q++)
- teco_qreg_table_insert(table, teco_qreg_plain_new(&q, sizeof(q)));
+ teco_qreg_table_insert_unique(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)));
+ teco_qreg_table_insert_unique(table, teco_qreg_plain_new(&q, sizeof(q)));
}
/** @memberof teco_qreg_table_t */
@@ -986,10 +983,10 @@ teco_qreg_table_init_locals(teco_qreg_table_t *table, gboolean must_undo)
teco_qreg_table_init(table, must_undo);
/* search mode ("^X") */
- teco_qreg_table_insert(table, teco_qreg_plain_new("\x18", 1));
+ teco_qreg_table_insert_unique(table, teco_qreg_plain_new("\x18", 1));
/* numeric radix ("^R") */
table->radix = teco_qreg_radix_new();
- teco_qreg_table_insert(table, table->radix);
+ teco_qreg_table_insert_unique(table, table->radix);
}
static inline void
@@ -1110,7 +1107,7 @@ teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error)
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;
+ const teco_string_t name = cur->head.name;
/*
* Ignore the "$" register (not an environment
@@ -1118,7 +1115,7 @@ teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error)
* name contains "=" or null (not allowed in environment
* variable names).
*/
- if (name->len == 1 ||
+ if (name.len == 1 ||
teco_string_contains(name, '=') || teco_string_contains(name, '\0'))
continue;
@@ -1127,16 +1124,16 @@ teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error)
g_strfreev(envp);
return NULL;
}
- if (teco_string_contains(&value, '\0')) {
+ 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);
+ name.data);
return NULL;
}
/* more efficient than g_environ_setenv() */
- *p++ = g_strconcat(name->data+1, "=", value.data, NULL);
+ *p++ = g_strconcat(name.data+1, "=", value.data, NULL);
}
*p = NULL;
@@ -1336,16 +1333,16 @@ teco_ed_hook(teco_ed_hook_t type, GError **error)
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"
+ static const gchar *const type2name[] = {
+ [TECO_ED_HOOK_ADD] = "ADD",
+ [TECO_ED_HOOK_EDIT] = "EDIT",
+ [TECO_ED_HOOK_CLOSE] = "CLOSE",
+ [TECO_ED_HOOK_QUIT] = "QUIT"
};
error_add_frame:
- g_assert(0 <= type-1 && type-1 < G_N_ELEMENTS(type2name));
- teco_error_add_frame_edhook(type2name[type-1]);
+ g_assert(0 <= type && type < G_N_ELEMENTS(type2name));
+ teco_error_add_frame_edhook(type2name[type]);
return FALSE;
}
@@ -1380,15 +1377,12 @@ TECO_DEFINE_UNDO_SCALAR(teco_machine_qregspec_flags_t);
#define teco_undo_qregspec_flags(VAR) \
(*teco_undo_object_teco_machine_qregspec_flags_t_push(&(VAR)))
-/*
- * 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_caret);
-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;
+static teco_state_t teco_state_qregspec_start_global;
+static teco_state_t teco_state_qregspec_caret;
+static teco_state_t teco_state_qregspec_firstchar;
+static teco_state_t teco_state_qregspec_secondchar;
+static teco_state_t teco_state_qregspec_string;
static teco_state_t *teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx,
gunichar chr, GError **error);
@@ -1416,7 +1410,7 @@ teco_state_qregspec_done(teco_machine_qregspec_t *ctx, GError **error)
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_insert_unique(ctx->result_table, ctx->result);
teco_qreg_table_undo_remove(ctx->result);
}
break;
@@ -1445,11 +1439,12 @@ teco_state_qregspec_start_input(teco_machine_qregspec_t *ctx, gunichar chr, GErr
/* in cmdline.c */
gboolean teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
-gboolean teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str,
+gboolean teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, teco_string_t str,
GError **error);
-TECO_DEFINE_STATE(teco_state_qregspec_start,
+static TECO_DEFINE_STATE(teco_state_qregspec_start,
.is_start = TRUE,
+ .input_cb = (teco_state_input_cb_t)teco_state_qregspec_start_input,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd,
.insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_qregspec_insert_completion
);
@@ -1485,7 +1480,8 @@ teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx, gunichar ch
* 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,
+static TECO_DEFINE_STATE(teco_state_qregspec_start_global,
+ .input_cb = (teco_state_input_cb_t)teco_state_qregspec_start_global_input,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
);
@@ -1506,7 +1502,9 @@ teco_state_qregspec_caret_input(teco_machine_qregspec_t *ctx, gunichar chr, GErr
return teco_state_qregspec_done(ctx, error);
}
-TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_qregspec_caret);
+static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_qregspec_caret,
+ .input_cb = (teco_state_input_cb_t)teco_state_qregspec_caret_input
+);
static teco_state_t *
teco_state_qregspec_firstchar_input(teco_machine_qregspec_t *ctx, gunichar chr, GError **error)
@@ -1522,7 +1520,8 @@ teco_state_qregspec_firstchar_input(teco_machine_qregspec_t *ctx, gunichar chr,
return &teco_state_qregspec_secondchar;
}
-TECO_DEFINE_STATE(teco_state_qregspec_firstchar,
+static TECO_DEFINE_STATE(teco_state_qregspec_firstchar,
+ .input_cb = (teco_state_input_cb_t)teco_state_qregspec_firstchar_input,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
);
@@ -1540,7 +1539,8 @@ teco_state_qregspec_secondchar_input(teco_machine_qregspec_t *ctx, gunichar chr,
return teco_state_qregspec_done(ctx, error);
}
-TECO_DEFINE_STATE(teco_state_qregspec_secondchar,
+static TECO_DEFINE_STATE(teco_state_qregspec_secondchar,
+ .input_cb = (teco_state_input_cb_t)teco_state_qregspec_secondchar_input,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
);
@@ -1587,10 +1587,11 @@ teco_state_qregspec_string_input(teco_machine_qregspec_t *ctx, gunichar chr, GEr
/* 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);
-gboolean teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str,
+gboolean teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, teco_string_t str,
GError **error);
-TECO_DEFINE_STATE(teco_state_qregspec_string,
+static TECO_DEFINE_STATE(teco_state_qregspec_string,
+ .input_cb = (teco_state_input_cb_t)teco_state_qregspec_string_input,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_string_process_edit_cmd,
.insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_qregspec_string_insert_completion
);
diff --git a/src/qreg.h b/src/qreg.h
index 7a9e13c..c5e9461 100644
--- a/src/qreg.h
+++ b/src/qreg.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -18,6 +18,8 @@
#include <glib.h>
+//#include <rb3ptr.h>
+
#include "sciteco.h"
#include "view.h"
#include "doc.h"
@@ -43,6 +45,8 @@ extern teco_view_t *teco_qreg_view;
* teco_qreg_set_integer_t set_integer;
* ...
* teco_qreg_set_integer(qreg, 23, error);
+ *
+ * But this probably won't work. Perhaps use the X-macro pattern.
*/
typedef const struct {
gboolean (*set_integer)(teco_qreg_t *qreg, teco_int_t value, GError **error);
@@ -156,7 +160,13 @@ struct teco_qreg_table_t {
void teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo);
void teco_qreg_table_init_locals(teco_qreg_table_t *table, gboolean must_undo);
-/** @memberof teco_qreg_table_t */
+/**
+ * Insert Q-Register into table.
+ *
+ * @return If non-NULL a register with the same name as qreg already
+ * existed in table. In this case qreg is __not__ automatically freed.
+ * @memberof teco_qreg_table_t
+ */
static inline teco_qreg_t *
teco_qreg_table_insert(teco_qreg_table_t *table, teco_qreg_t *qreg)
{
@@ -165,6 +175,35 @@ teco_qreg_table_insert(teco_qreg_table_t *table, teco_qreg_t *qreg)
}
/** @memberof teco_qreg_table_t */
+static inline void
+teco_qreg_table_insert_unique(teco_qreg_table_t *table, teco_qreg_t *qreg)
+{
+ G_GNUC_UNUSED teco_qreg_t *found = teco_qreg_table_insert(table, qreg);
+ g_assert(found == NULL);
+}
+
+/**
+ * Insert Q-register into table, possibly replacing a register with the same name.
+ *
+ * This is useful for initializing Q-registers late when the user could have
+ * already created one in the profile.
+ *
+ * @param table Table to insert into
+ * @param qreg Q-Register to insert
+ *
+ * @memberof teco_qreg_table_t
+ */
+static inline void
+teco_qreg_table_replace(teco_qreg_table_t *table, teco_qreg_t *qreg)
+{
+ teco_qreg_t *found = teco_qreg_table_insert(table, qreg);
+ if (found) {
+ rb3_replace(&found->head.head, &qreg->head.head);
+ teco_qreg_free(found);
+ }
+}
+
+/** @memberof teco_qreg_table_t */
static inline teco_qreg_t *
teco_qreg_table_find(teco_qreg_table_t *table, const gchar *name, gsize len)
{
@@ -200,7 +239,7 @@ gboolean teco_qreg_stack_pop(teco_qreg_t *qreg, GError **error);
void teco_qreg_stack_clear(void);
typedef enum {
- TECO_ED_HOOK_ADD = 1,
+ TECO_ED_HOOK_ADD = 0,
TECO_ED_HOOK_EDIT,
TECO_ED_HOOK_CLOSE,
TECO_ED_HOOK_QUIT
diff --git a/src/rb3str.c b/src/rb3str.c
index 276b624..f4c16fa 100644
--- a/src/rb3str.c
+++ b/src/rb3str.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -34,13 +34,13 @@
static gint
teco_rb3str_cmp(const teco_rb3str_head_t *head, const teco_string_t *data)
{
- return teco_string_cmp(&head->key, data->data, data->len);
+ return teco_string_cmp(head->key, data->data, data->len);
}
static gint
teco_rb3str_casecmp(const teco_rb3str_head_t *head, const teco_string_t *data)
{
- return teco_string_casecmp(&head->key, data->data, data->len);
+ return teco_string_casecmp(head->key, data->data, data->len);
}
/** @memberof teco_rb3str_tree_t */
@@ -113,7 +113,7 @@ teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive,
guint prefixed_entries = 0;
for (teco_rb3str_head_t *cur = teco_rb3str_nfind(tree, case_sensitive, str, str_len);
- cur && cur->key.len >= str_len && diff(&cur->key, str, str_len) == str_len;
+ cur && cur->key.len >= str_len && diff(cur->key, str, str_len) == str_len;
cur = teco_rb3str_get_next(cur)) {
if (restrict_len && g_utf8_strlen(cur->key.data, cur->key.len) != restrict_len)
continue;
@@ -122,7 +122,7 @@ teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive,
first = cur;
prefix_len = cur->key.len - str_len;
} else {
- gsize len = diff(&cur->key, first->key.data, first->key.len) - str_len;
+ gsize len = diff(cur->key, first->key.data, first->key.len) - str_len;
if (len < prefix_len)
prefix_len = len;
}
@@ -134,7 +134,7 @@ teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive,
teco_string_init(insert, first->key.data + str_len, prefix_len);
} else if (prefixed_entries > 1) {
for (teco_rb3str_head_t *cur = first;
- cur && cur->key.len >= str_len && diff(&cur->key, str, str_len) == str_len;
+ cur && cur->key.len >= str_len && diff(cur->key, str, str_len) == str_len;
cur = teco_rb3str_get_next(cur)) {
if (restrict_len && g_utf8_strlen(cur->key.data, cur->key.len) != restrict_len)
continue;
diff --git a/src/rb3str.h b/src/rb3str.h
index 466cf90..00d1791 100644
--- a/src/rb3str.h
+++ b/src/rb3str.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
diff --git a/src/ring.c b/src/ring.c
index b7a75d1..2445718 100644
--- a/src/ring.c
+++ b/src/ring.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -21,6 +21,7 @@
#include <glib.h>
#include <glib/gprintf.h>
+#include <glib/gstdio.h>
#include <Scintilla.h>
@@ -55,7 +56,8 @@ teco_buffer_set_filename(teco_buffer_t *ctx, const gchar *filename)
gchar *resolved = teco_file_get_absolute_path(filename);
g_free(ctx->filename);
ctx->filename = resolved;
- teco_interface_info_update(ctx);
+ if (ctx == teco_ring_current && !teco_qreg_current)
+ teco_interface_info_update(ctx);
}
/** @memberof teco_buffer_t */
@@ -74,16 +76,23 @@ teco_buffer_undo_edit(teco_buffer_t *ctx)
}
/** @private @memberof teco_buffer_t */
+static inline gchar *
+teco_buffer_get_recovery(teco_buffer_t *ctx)
+{
+ g_autofree gchar *dirname = g_path_get_dirname(ctx->filename);
+ g_autofree gchar *basename = g_path_get_basename(ctx->filename);
+ return g_strconcat(dirname, G_DIR_SEPARATOR_S, "#", basename, "#", NULL);
+}
+
+/** @private @memberof teco_buffer_t */
static gboolean
teco_buffer_load(teco_buffer_t *ctx, const gchar *filename, GError **error)
{
- if (!teco_view_load(ctx->view, filename, error))
+ if (!teco_view_load(ctx->view, filename, TRUE, error))
return FALSE;
-#if 0 /* NOTE: currently buffer cannot be dirty */
- undo__teco_interface_info_update_buffer(ctx);
- teco_undo_gboolean(ctx->dirty) = FALSE;
-#endif
+ /* currently buffer cannot be dirty */
+ g_assert(ctx->state == TECO_BUFFER_CLEAN);
teco_buffer_set_filename(ctx, filename);
return TRUE;
@@ -107,8 +116,15 @@ teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error)
* Undirtify
* NOTE: info update is performed by set_filename()
*/
- undo__teco_interface_info_update_buffer(ctx);
- teco_undo_gboolean(ctx->dirty) = FALSE;
+ if (ctx == teco_ring_current && !teco_qreg_current)
+ undo__teco_interface_info_update_buffer(ctx);
+ if (ctx->state > TECO_BUFFER_DIRTY_NO_DUMP) {
+ g_autofree gchar *filename_recovery = teco_buffer_get_recovery(ctx);
+ g_unlink(filename_recovery);
+ /* on rubout, we do not restore the recovery file */
+ ctx->state = TECO_BUFFER_DIRTY_NO_DUMP;
+ }
+ teco_undo_guint(ctx->state) = TECO_BUFFER_CLEAN;
/*
* FIXME: necessary also if the filename was not specified but the file
@@ -127,6 +143,11 @@ teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error)
static inline void
teco_buffer_free(teco_buffer_t *ctx)
{
+ if (ctx->state > TECO_BUFFER_DIRTY_NO_DUMP) {
+ g_autofree gchar *filename_recovery = teco_buffer_get_recovery(ctx);
+ g_unlink(filename_recovery);
+ }
+
teco_view_free(ctx->view);
g_free(ctx->filename);
g_free(ctx);
@@ -151,7 +172,7 @@ teco_ring_last(void)
}
static void
-teco_undo_ring_edit_action(teco_buffer_t **buffer, gboolean run)
+teco_undo_ring_reinsert_action(teco_buffer_t **buffer, gboolean run)
{
if (run) {
/*
@@ -162,22 +183,19 @@ teco_undo_ring_edit_action(teco_buffer_t **buffer, gboolean run)
teco_tailq_insert_before((*buffer)->entry.next, &(*buffer)->entry);
else
teco_tailq_insert_tail(&teco_ring_head, &(*buffer)->entry);
-
- teco_ring_current = *buffer;
- teco_buffer_edit(*buffer);
} else {
teco_buffer_free(*buffer);
}
}
-/*
- * Emitted after a buffer close
- * The pointer is the only remaining reference to the buffer!
+/**
+ * Insert buffer during undo (for closing buffers).
+ * Ownership of the buffer is passed to the undo token.
*/
static void
-teco_undo_ring_edit(teco_buffer_t *buffer)
+teco_undo_ring_reinsert(teco_buffer_t *buffer)
{
- teco_buffer_t **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_ring_edit_action,
+ teco_buffer_t **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_ring_reinsert_action,
sizeof(buffer));
if (ctx)
*ctx = buffer;
@@ -223,27 +241,57 @@ teco_ring_find_by_id(teco_int_t id)
return NULL;
}
+static void
+teco_ring_undirtify(void)
+{
+ if (teco_ring_current->state > TECO_BUFFER_DIRTY_NO_DUMP) {
+ g_autofree gchar *filename_recovery = teco_buffer_get_recovery(teco_ring_current);
+ g_unlink(filename_recovery);
+ }
+
+ teco_ring_current->state = TECO_BUFFER_CLEAN;
+ teco_interface_info_update(teco_ring_current);
+}
+
+TECO_DEFINE_UNDO_CALL(teco_ring_undirtify);
+
void
teco_ring_dirtify(void)
{
- if (teco_qreg_current || teco_ring_current->dirty)
+ if (teco_qreg_current)
return;
- undo__teco_interface_info_update_buffer(teco_ring_current);
- teco_undo_gboolean(teco_ring_current->dirty) = TRUE;
- teco_interface_info_update(teco_ring_current);
+ switch ((teco_buffer_state_t)teco_ring_current->state) {
+ case TECO_BUFFER_CLEAN:
+ teco_ring_current->state = TECO_BUFFER_DIRTY_NO_DUMP;
+ teco_interface_info_update(teco_ring_current);
+ undo__teco_ring_undirtify();
+ break;
+ case TECO_BUFFER_DIRTY_NO_DUMP:
+ case TECO_BUFFER_DIRTY_OUTDATED_DUMP:
+ break;
+ case TECO_BUFFER_DIRTY_RECENT_DUMP:
+ teco_ring_current->state = TECO_BUFFER_DIRTY_OUTDATED_DUMP;
+ /* set to TECO_BUFFER_DIRTY_OUTDATED_DUMP on rubout */
+ teco_undo_guint(teco_ring_current->state);
+ break;
+ }
}
-gboolean
-teco_ring_is_any_dirty(void)
+/** Get id of first dirty buffer, or otherwise 0 */
+guint
+teco_ring_get_first_dirty(void)
{
+ guint id = 1;
+
for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) {
teco_buffer_t *buffer = (teco_buffer_t *)cur;
- if (buffer->dirty)
- return TRUE;
+ if (buffer->state > TECO_BUFFER_CLEAN)
+ return id;
+ id++;
}
- return FALSE;
+ return 0;
}
gboolean
@@ -252,13 +300,72 @@ teco_ring_save_all_dirty_buffers(GError **error)
for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) {
teco_buffer_t *buffer = (teco_buffer_t *)cur;
/* NOTE: Will fail for a dirty unnamed file */
- if (buffer->dirty && !teco_buffer_save(buffer, NULL, error))
+ if (buffer->state > TECO_BUFFER_CLEAN &&
+ !teco_buffer_save(buffer, NULL, error))
return FALSE;
}
return TRUE;
}
+/**
+ * Recovery creation interval in seconds or 0 if disabled.
+ * It's not currently enforced in batch mode.
+ */
+guint teco_ring_recovery_interval = 5*60;
+
+/**
+ * Create recovery files for all dirty buffers.
+ *
+ * Should be called by the interface every teco_ring_recovery_interval seconds.
+ * This does not generate or expect undo tokens, so it can be called
+ * even when idlying.
+ */
+void
+teco_ring_dump_recovery(void)
+{
+ g_assert(teco_ring_recovery_interval > 0);
+
+ for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) {
+ teco_buffer_t *buffer = (teco_buffer_t *)cur;
+ /* already dumped buffers don't have to be written again */
+ if (buffer->state != TECO_BUFFER_DIRTY_NO_DUMP &&
+ buffer->state != TECO_BUFFER_DIRTY_OUTDATED_DUMP)
+ continue;
+
+ /*
+ * Dirty unnamed buffers cannot be backed up.
+ * FIXME: Perhaps they should be dumped under ~/#UNNAMED#?
+ */
+ if (!buffer->filename)
+ continue;
+
+ g_autofree gchar *filename_recovery = teco_buffer_get_recovery(buffer);
+
+ g_autoptr(GIOChannel) channel = g_io_channel_new_file(filename_recovery, "w", NULL);
+ if (!channel)
+ continue;
+
+ /*
+ * teco_view_save_to_channel() expects a buffered and blocking channel.
+ */
+ g_io_channel_set_encoding(channel, NULL, NULL);
+ g_io_channel_set_buffered(channel, TRUE);
+
+ /*
+ * This does not use teco_view_save_to_file() since we must not
+ * emit undo tokens.
+ *
+ * FIXME: Errors are silently ignored.
+ * Should we log warnings instead?
+ */
+ if (!teco_view_save_to_channel(buffer->view, channel, NULL))
+ continue;
+
+ buffer->state = TECO_BUFFER_DIRTY_RECENT_DUMP;
+ }
+}
+
gboolean
teco_ring_edit_by_name(const gchar *filename, GError **error)
{
@@ -306,8 +413,7 @@ teco_ring_edit_by_id(teco_int_t id, GError **error)
{
teco_buffer_t *buffer = teco_ring_find(id);
if (!buffer) {
- g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
- "Invalid buffer id %" TECO_INT_FORMAT, id);
+ teco_error_invalidbuf_set(error, id);
return FALSE;
}
@@ -320,7 +426,7 @@ teco_ring_edit_by_id(teco_int_t id, GError **error)
}
static void
-teco_ring_close_buffer(teco_buffer_t *buffer)
+teco_ring_remove_buffer(teco_buffer_t *buffer)
{
teco_tailq_remove(&teco_ring_head, &buffer->entry);
@@ -333,32 +439,48 @@ teco_ring_close_buffer(teco_buffer_t *buffer)
"Removed unnamed file from the ring.");
}
-TECO_DEFINE_UNDO_CALL(teco_ring_close_buffer, teco_buffer_t *);
+TECO_DEFINE_UNDO_CALL(teco_ring_remove_buffer, teco_buffer_t *);
+/**
+ * Close the given buffer.
+ * Executes close hooks and changes the current buffer if necessary.
+ * It already pushes undo tokens.
+ */
gboolean
-teco_ring_close(GError **error)
+teco_ring_close(teco_buffer_t *buffer, GError **error)
{
- teco_buffer_t *buffer = teco_ring_current;
+ if (buffer == teco_ring_current) {
+ if (!teco_ed_hook(TECO_ED_HOOK_CLOSE, error))
+ return FALSE;
- if (!teco_ed_hook(TECO_ED_HOOK_CLOSE, error))
- return FALSE;
- teco_ring_close_buffer(buffer);
- teco_ring_current = teco_buffer_next(buffer) ? : teco_buffer_prev(buffer);
- /* Transfer responsibility to the undo token object. */
- teco_undo_ring_edit(buffer);
+ teco_ring_undo_edit();
+ teco_ring_remove_buffer(buffer);
+
+ teco_ring_current = teco_buffer_next(buffer) ? : teco_buffer_prev(buffer);
+ if (!teco_ring_current) {
+ /* edit new unnamed buffer */
+ if (!teco_ring_edit_by_name(NULL, error))
+ return FALSE;
+ } else {
+ teco_buffer_edit(teco_ring_current);
+ if (!teco_ed_hook(TECO_ED_HOOK_EDIT, error))
+ return FALSE;
+ }
+ } else {
+ teco_ring_remove_buffer(buffer);
+ }
- if (!teco_ring_current)
- return teco_ring_edit_by_name(NULL, error);
+ /* transfer responsibility to the undo token object */
+ teco_undo_ring_reinsert(buffer);
- teco_buffer_edit(teco_ring_current);
- return teco_ed_hook(TECO_ED_HOOK_EDIT, error);
+ return TRUE;
}
void
teco_ring_undo_close(void)
{
undo__teco_buffer_free(teco_ring_current);
- undo__teco_ring_close_buffer(teco_ring_current);
+ undo__teco_ring_remove_buffer(teco_ring_current);
}
void
@@ -387,13 +509,6 @@ teco_ring_cleanup(void)
* Command states
*/
-/*
- * FIXME: Should be part of the teco_machine_main_t?
- * Unfortunately, we cannot just merge initial() with done(),
- * since we want to react immediately to xEB without waiting for $.
- */
-static gboolean allow_filename = FALSE;
-
static gboolean
teco_state_edit_file_initial(teco_machine_main_t *ctx, GError **error)
{
@@ -404,18 +519,17 @@ teco_state_edit_file_initial(teco_machine_main_t *ctx, GError **error)
if (!teco_expressions_pop_num_calc(&id, -1, error))
return FALSE;
- allow_filename = TRUE;
+ ctx->flags.allow_filename = TRUE;
if (id == 0) {
- for (teco_buffer_t *cur = teco_ring_first(); cur; cur = teco_buffer_next(cur)) {
- const gchar *filename = cur->filename ? : "(Unnamed)";
- teco_interface_popup_add(TECO_POPUP_FILE, filename,
- strlen(filename), cur == teco_ring_current);
- }
+ for (teco_buffer_t *cur = teco_ring_first(); cur; cur = teco_buffer_next(cur))
+ teco_interface_popup_add(TECO_POPUP_FILE, cur->filename,
+ cur->filename ? strlen(cur->filename) : 0,
+ cur == teco_ring_current);
teco_interface_popup_show(0);
} else if (id > 0) {
- allow_filename = FALSE;
+ ctx->flags.allow_filename = FALSE;
if (!teco_current_doc_undo_edit(error) ||
!teco_ring_edit(id, error))
return FALSE;
@@ -424,24 +538,35 @@ teco_state_edit_file_initial(teco_machine_main_t *ctx, GError **error)
return TRUE;
}
+gboolean
+teco_state_edit_file_process(teco_machine_main_t *ctx, teco_string_t str,
+ gsize new_chars, GError **error)
+{
+ g_assert(new_chars > 0);
+
+ if (!ctx->flags.allow_filename) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "If a buffer is selected by id, the <EB> "
+ "string argument must be empty");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
static teco_state_t *
-teco_state_edit_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_edit_file_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
- if (!allow_filename) {
- if (str->len > 0) {
- g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
- "If a buffer is selected by id, the <EB> "
- "string argument must be empty");
- return NULL;
- }
-
+ if (!ctx->flags.allow_filename) {
+ /* process_cb() already throws error if str.len > 0 */
+ g_assert(str.len == 0);
return &teco_state_start;
}
- g_autofree gchar *filename = teco_file_expand_path(str->data);
+ g_autofree gchar *filename = teco_file_expand_path(str.data);
if (teco_globber_is_pattern(filename)) {
g_auto(teco_globber_t) globber;
teco_globber_init(&globber, filename, G_FILE_TEST_IS_REGULAR);
@@ -465,7 +590,7 @@ teco_state_edit_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE
/*$ EB edit
* [n]EB[file]$ -- Open or edit file
- * nEB$
+ * [n]EB$
*
* Opens or edits the file with name <file>.
* If <file> is not in the buffer ring it is opened,
@@ -518,45 +643,60 @@ teco_state_edit_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE
* ecetera.
*/
TECO_DEFINE_STATE_EXPECTGLOB(teco_state_edit_file,
- .initial_cb = (teco_state_initial_cb_t)teco_state_edit_file_initial
+ .initial_cb = (teco_state_initial_cb_t)teco_state_edit_file_initial,
+ .expectstring.process_cb = teco_state_edit_file_process,
+ .expectstring.done_cb = teco_state_edit_file_done
);
static teco_state_t *
-teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_save_file_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
- g_autofree gchar *filename = teco_file_expand_path(str->data);
- if (teco_qreg_current) {
- if (!teco_qreg_current->vtable->save(teco_qreg_current, filename, error))
- return NULL;
- } else {
- if (!teco_buffer_save(teco_ring_current, *filename ? filename : NULL, error))
+ if (!teco_expressions_eval(FALSE, error))
+ return NULL;
+
+ g_autofree gchar *filename = teco_file_expand_path(str.data);
+
+ /*
+ * This is like implying teco_ring_get_id(teco_ring_current)
+ * but avoids the O(n) ring iterations.
+ */
+ teco_buffer_t *buffer = teco_ring_current;
+ if (teco_expressions_args() > 0) {
+ teco_int_t id = teco_expressions_pop_num(0);
+ buffer = teco_ring_find(id);
+ if (!buffer) {
+ teco_error_invalidbuf_set(error, id);
return NULL;
+ }
+ } else if (teco_qreg_current) {
+ return !teco_qreg_current->vtable->save(teco_qreg_current, filename, error)
+ ? NULL : &teco_state_start;
}
- return &teco_state_start;
+ return !teco_buffer_save(buffer, *filename ? filename : NULL, error) ? NULL : &teco_state_start;
}
/*$ EW write save
- * EW$ -- Save current buffer or Q-Register
- * EWfile$
+ * EW$ -- Save buffer or Q-Register
+ * [n]EW[file]$
*
- * Saves the current buffer to disk.
+ * Saves the chosen buffer with id <n> to disk
+ * By default, the current buffer is saved.
* If the buffer was dirty, it will be clean afterwards.
* If the string argument <file> is not empty,
* the buffer is saved with the specified file name
* and is renamed in the ring.
*
- * The EW command also works if the current document
- * is a Q-Register, i.e. a Q-Register is edited.
- * In this case, the string contents of the current
- * Q-Register are saved to <file>.
+ * If the current document is a Q-Register and <n> is not given,
+ * the string contents of the current Q-Register are saved to <file>
+ * (cf. \fBE%\fIq\fR command)..
* Q-Registers have no notion of associated file names,
- * so <file> must be always specified.
+ * so <file> must be always specified in this case.
*
- * In interactive mode, EW is executed immediately and
+ * In interactive mode, \fBEW\fP is executed immediately and
* may be rubbed out.
* In order to support that, \*(ST creates so called
* save point files.
@@ -568,9 +708,9 @@ teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE
* Save point files are always created in the same directory
* as the original file to ensure that no copying of the file
* on disk is necessary but only a rename of the file.
- * When rubbing out the EW command, \*(ST restores the latest
+ * When rubbing out the \fBEW\fP command, \*(ST restores the latest
* save point file by moving (renaming) it back to its
- * original path \(em also not requiring any on-disk copying.
+ * original path -- also not requiring any on-disk copying.
* \*(ST is impossible to crash, but just in case it still
* does it may leave behind these save point files which
* must be manually deleted by the user.
@@ -580,62 +720,119 @@ teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE
* File names may also be tab-completed and string building
* characters are enabled by default.
*/
-TECO_DEFINE_STATE_EXPECTFILE(teco_state_save_file);
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_save_file,
+ .expectstring.done_cb = teco_state_save_file_done
+);
+
+static teco_state_t *
+teco_state_read_file_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
+{
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ g_autofree gchar *filename = teco_file_expand_path(str.data);
+ /* FIXME: Add wrapper to interface.h? */
+ if (!teco_view_load(teco_interface_current_view, filename, FALSE, error))
+ return NULL;
+
+ if (teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0) != pos) {
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+ }
+
+ return &teco_state_start;
+}
+
+/*$ ER read
+ * ER<file>$ -- Read and insert file into current buffer
+ *
+ * Reads and inserts the given <file> into the current buffer or Q-Register at dot.
+ * Dot is left immediately after the given file.
+ */
+/*
+ * NOTE: Video TECO allows glob patterns as an argument.
+ */
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_read_file,
+ .expectstring.done_cb = teco_state_read_file_done
+);
-/*$ EF close
- * [bool]EF -- Remove buffer from ring
+/*$ "EF" ":EF" close
+ * [n]EF -- Remove buffer from ring
* -EF
- * :EF
+ * [n]:EF
*
* Removes buffer from buffer ring, effectively
* closing it.
- * If the buffer is dirty (modified), EF will yield
+ * The optional argument <n> specifies the id of the buffer
+ * to close -- by default the current buffer will be closed.
+ * If the selected buffer is dirty (modified), \fBEF\fP will yield
* an error.
- * <bool> may be a specified to enforce closing dirty
- * buffers.
- * If it is a Failure condition boolean (negative),
- * the buffer will be closed unconditionally.
- * If <bool> is absent, the sign prefix (1 or -1) will
- * be implied, so \(lq-EF\(rq will always close the buffer.
+ * If <n> is negative (success boolean), buffer <-n> will be closed
+ * even if it is dirty.
+ * \(lq-EF\(rq will force-close the current buffer.
*
- * When colon-modified, <bool> is ignored and \fBEF\fP
- * will save the buffer before closing.
+ * When colon-modified, the selected buffer is saved before closing.
* The file is always written, unlike \(lq:EX\(rq which
* saves only dirty buffers.
* This can fail of course, e.g. when called on the unnamed
* buffer.
*
- * It is noteworthy that EF will be executed immediately in
+ * It is noteworthy that \fBEF\fP will be executed immediately in
* interactive mode but can be rubbed out at a later time
* to reopen the file.
* Closed files are kept in memory until the command line
* is terminated.
+ *
+ * Close and edit hooks are only executed when closing the current buffer.
*/
void
teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error)
{
- if (teco_qreg_current) {
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+
+ /*
+ * This is like implying teco_num_sign*teco_ring_get_id(teco_ring_current)
+ * but avoids the O(n) ring iterations.
+ */
+ teco_buffer_t *buffer;
+ gboolean force;
+ if (teco_expressions_args() > 0) {
+ teco_int_t id = teco_expressions_pop_num(0);
+ buffer = teco_ring_find(ABS(id));
+ if (!buffer) {
+ teco_error_invalidbuf_set(error, ABS(id));
+ return;
+ }
+ force = id < 0;
+ } else if (teco_qreg_current) {
+ /*
+ * TODO: Should perhaps remove the register like FQq.
+ */
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,
"Q-Register \"%s\" currently edited", name_printable);
return;
+ } else {
+ buffer = teco_ring_current;
+ force = teco_num_sign < 0;
+ teco_set_num_sign(1);
}
if (teco_machine_main_eval_colon(ctx) > 0) {
- if (!teco_buffer_save(teco_ring_current, NULL, error))
- return;
- } else {
- teco_int_t v;
- if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
- return;
- if (teco_is_failure(v) && teco_ring_current->dirty) {
- g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
- "Buffer \"%s\" is dirty",
- teco_ring_current->filename ? : "(Unnamed)");
+ if (!teco_buffer_save(buffer, NULL, error))
return;
- }
+ } else if (!force && buffer->state > TECO_BUFFER_CLEAN) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Buffer \"%s\" is dirty",
+ buffer->filename ? : "(Unnamed)");
+ return;
}
- teco_ring_close(error);
+ teco_ring_close(buffer, error);
}
diff --git a/src/ring.h b/src/ring.h
index 34293b9..e45434d 100644
--- a/src/ring.h
+++ b/src/ring.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -25,13 +25,29 @@
#include "parser.h"
#include "list.h"
+typedef enum {
+ /** buffer is freshly opened or saved */
+ TECO_BUFFER_CLEAN = 0,
+ /** buffer modified, but a recovery file does not yet exist */
+ TECO_BUFFER_DIRTY_NO_DUMP,
+ /** buffer modified, recovery file outdated */
+ TECO_BUFFER_DIRTY_OUTDATED_DUMP,
+ /** buffer modified and recovery file is up to date */
+ TECO_BUFFER_DIRTY_RECENT_DUMP
+} teco_buffer_state_t;
+
typedef struct teco_buffer_t {
teco_tailq_entry_t entry;
teco_view_t *view;
gchar *filename;
- gboolean dirty;
+
+ /**
+ * A teco_buffer_state_t.
+ * This is still a guint, so you can call teco_undo_guint().
+ */
+ guint state;
} teco_buffer_t;
/** @memberof teco_buffer_t */
@@ -67,9 +83,13 @@ teco_buffer_t *teco_ring_find_by_id(teco_int_t id);
teco_int_t : teco_ring_find_by_id)(X))
void teco_ring_dirtify(void);
-gboolean teco_ring_is_any_dirty(void);
+guint teco_ring_get_first_dirty(void);
gboolean teco_ring_save_all_dirty_buffers(GError **error);
+extern guint teco_ring_recovery_interval;
+
+void teco_ring_dump_recovery(void);
+
gboolean teco_ring_edit_by_name(const gchar *filename, GError **error);
gboolean teco_ring_edit_by_id(teco_int_t id, GError **error);
@@ -86,7 +106,7 @@ teco_ring_undo_edit(void)
teco_buffer_undo_edit(teco_ring_current);
}
-gboolean teco_ring_close(GError **error);
+gboolean teco_ring_close(teco_buffer_t *buffer, GError **error);
void teco_ring_undo_close(void);
void teco_ring_set_scintilla_undo(gboolean state);
@@ -97,8 +117,9 @@ void teco_ring_cleanup(void);
* Command states
*/
-TECO_DECLARE_STATE(teco_state_edit_file);
-TECO_DECLARE_STATE(teco_state_save_file);
+extern teco_state_t teco_state_edit_file;
+extern teco_state_t teco_state_save_file;
+extern teco_state_t teco_state_read_file;
void teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error);
diff --git a/src/sciteco.h b/src/sciteco.h
index 4868303..16dba69 100644
--- a/src/sciteco.h
+++ b/src/sciteco.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -25,10 +25,12 @@
#if TECO_INTEGER == 32
typedef gint32 teco_int_t;
-#define TECO_INT_FORMAT G_GINT32_FORMAT
+#define TECO_INT_MODIFIER G_GINT32_MODIFIER
+#define TECO_INT_FORMAT G_GINT32_FORMAT
#elif TECO_INTEGER == 64
typedef gint64 teco_int_t;
-#define TECO_INT_FORMAT G_GINT64_FORMAT
+#define TECO_INT_MODIFIER G_GINT64_MODIFIER
+#define TECO_INT_FORMAT G_GINT64_FORMAT
#else
#error Invalid TECO integer storage size
#endif
@@ -85,6 +87,7 @@ teco_is_failure(teco_bool_t x)
* This is not a bitfield, since it is set from SciTECO.
*/
enum {
+ TECO_ED_EXIT = (1 << 1),
TECO_ED_DEFAULT_ANSI = (1 << 2),
TECO_ED_AUTOCASEFOLD = (1 << 3),
TECO_ED_AUTOEOL = (1 << 4),
@@ -92,7 +95,9 @@ enum {
TECO_ED_MOUSEKEY = (1 << 6),
TECO_ED_SHELLEMU = (1 << 7),
TECO_ED_OSC52 = (1 << 8),
- TECO_ED_ICONS = (1 << 9)
+ TECO_ED_ICONS = (1 << 9),
+ TECO_ED_CLIP_PRIMARY = (1 << 10),
+ TECO_ED_MINIBUF_SSM = (1 << 11)
};
/* in main.c */
@@ -112,6 +117,16 @@ extern volatile sig_atomic_t teco_interrupted;
*/
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FILE, fclose);
+/**
+ * A "safe" compile-time assertion, which also passes if the expression is not constant.
+ *
+ * Can be useful since different compilers have different ideas about what's a constant expression.
+ * In particular GCC does not treat `static const` objects as constant (in the way it qualifies
+ * for _Static_assert()), while constexpr is only available since C23.
+ */
+#define TECO_ASSERT_SAFE(EXPR) \
+ G_STATIC_ASSERT(!__builtin_constant_p(EXPR) || (EXPR))
+
/*
* BEWARE DRAGONS!
*/
diff --git a/src/search.c b/src/search.c
index 7fcf10e..856d079 100644
--- a/src/search.c
+++ b/src/search.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -56,7 +56,7 @@ TECO_DEFINE_UNDO_SCALAR(teco_search_parameters_t);
*/
static teco_search_parameters_t teco_search_parameters;
-/*$ ^X search-mode
+/*$ "^X" "search mode"
* mode^X -- Set or get search mode flag
* -^X
* ^X -> mode
@@ -111,8 +111,7 @@ teco_state_search_initial(teco_machine_main_t *ctx, GError **error)
return FALSE;
if (teco_expressions_args()) {
/* TODO: optional count argument? */
- if (!teco_expressions_pop_num_calc(&v1, 0, error))
- return FALSE;
+ v1 = teco_expressions_pop_num(0);
if (v1 <= v2) {
teco_search_parameters.count = 1;
teco_search_parameters.from = teco_interface_glyphs2bytes(v1);
@@ -538,7 +537,7 @@ teco_pattern2regexp(teco_string_t *pattern, teco_machine_qregspec_t *qreg_machin
if (state == TECO_SEARCH_STATE_ALT)
teco_string_append_c(&re, ')');
- g_assert(!teco_string_contains(&re, '\0'));
+ g_assert(!teco_string_contains(re, '\0'));
return g_steal_pointer(&re.data) ? : g_strdup("");
}
@@ -548,6 +547,16 @@ TECO_DEFINE_UNDO_OBJECT_OWN(ranges, teco_range_t *, g_free);
#define teco_undo_ranges_own(VAR) \
(*teco_undo_object_ranges_push(&(VAR)))
+/**
+ * Extract the ranges of the given GMatchInfo.
+ *
+ * @param match_info The result of g_regex_match().
+ * @param offset The beginning of the match operation in bytes.
+ * Match results will be relative to this offset.
+ * @param count Where to store the number of ranges (subpatterns).
+ * @returns Ranges (subpatterns) in absolute byte positions.
+ * They \b must still be converted to glyph positions afterwards.
+ */
static teco_range_t *
teco_get_ranges(const GMatchInfo *match_info, gsize offset, guint *count)
{
@@ -661,7 +670,7 @@ teco_do_search(GRegex *re, gsize from, gsize to, gint *count, GError **error)
matched[i].ranges = NULL;
}
- for (int i = 0; i < matched_num; i++)
+ for (gint i = 0; i < matched_num; i++)
g_free(matched[i].ranges);
}
@@ -671,14 +680,23 @@ teco_do_search(GRegex *re, gsize from, gsize to, gint *count, GError **error)
teco_undo_guint(teco_ranges_count) = num_ranges;
g_assert(teco_ranges_count > 0);
- teco_interface_ssm(SCI_SETSEL, matched_ranges[0].from, matched_ranges[0].to);
+ teco_interface_ssm(SCI_SETSEL, teco_ranges[0].from, teco_ranges[0].to);
+
+ /*
+ * teco_get_ranges() returned byte positions,
+ * while everything else expects glyph offsets.
+ */
+ for (guint i = 0; i < teco_ranges_count; i++) {
+ teco_ranges[i].from = teco_interface_bytes2glyphs(teco_ranges[i].from);
+ teco_ranges[i].to = teco_interface_bytes2glyphs(teco_ranges[i].to);
+ }
}
return TRUE;
}
static gboolean
-teco_state_search_process(teco_machine_main_t *ctx, const teco_string_t *str, gsize new_chars, GError **error)
+teco_state_search_process(teco_machine_main_t *ctx, teco_string_t str, gsize new_chars, GError **error)
{
/* FIXME: Should G_REGEX_OPTIMIZE be added under certain circumstances? */
GRegexCompileFlags flags = G_REGEX_MULTILINE | G_REGEX_DOTALL;
@@ -723,10 +741,9 @@ teco_state_search_process(teco_machine_main_t *ctx, const teco_string_t *str, gs
qreg_machine = teco_machine_qregspec_new(TECO_QREG_REQUIRED, ctx->qreg_table_locals, FALSE);
g_autoptr(GRegex) re = NULL;
- teco_string_t pattern = *str;
g_autofree gchar *re_pattern;
/* NOTE: teco_pattern2regexp() modifies str pointer */
- re_pattern = teco_pattern2regexp(&pattern, qreg_machine,
+ re_pattern = teco_pattern2regexp(&str, qreg_machine,
ctx->expectstring.machine.codepage, FALSE, error);
if (!re_pattern)
return FALSE;
@@ -811,7 +828,7 @@ failure:
}
static teco_state_t *
-teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_search_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
@@ -819,14 +836,14 @@ teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro
teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
g_assert(search_reg != NULL);
- if (str->len > 0) {
+ if (str.len > 0) {
/* workaround: preserve selection (also on rubout) */
gint anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0);
if (teco_current_doc_must_undo())
undo__teco_interface_ssm(SCI_SETANCHOR, anchor, 0);
if (!search_reg->vtable->undo_set_string(search_reg, error) ||
- !search_reg->vtable->set_string(search_reg, str->data, str->len,
+ !search_reg->vtable->set_string(search_reg, str.data, str.len,
teco_default_codepage(), error))
return NULL;
@@ -835,7 +852,7 @@ teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro
g_auto(teco_string_t) search_str = {NULL, 0};
if (!search_reg->vtable->get_string(search_reg, &search_str.data, &search_str.len,
NULL, error) ||
- !teco_state_search_process(ctx, &search_str, search_str.len, error))
+ !teco_state_search_process(ctx, search_str, search_str.len, error))
return NULL;
}
@@ -864,11 +881,10 @@ teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro
TECO_DEFINE_STATE_EXPECTSTRING(NAME, \
.initial_cb = (teco_state_initial_cb_t)teco_state_search_initial, \
.expectstring.process_cb = teco_state_search_process, \
- .expectstring.done_cb = NAME##_done, \
##__VA_ARGS__ \
)
-/*$ S search pattern compare
+/*$ "S" ":S" "::S" search pattern compare
* [n]S[pattern]$ -- Search for pattern
* -S[pattern]$
* from,toS[pattern]$
@@ -938,7 +954,9 @@ teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro
* Changing the <pattern> results in the search being reperformed
* from the beginning.
*/
-TECO_DEFINE_STATE_SEARCH(teco_state_search);
+TECO_DEFINE_STATE_SEARCH(teco_state_search,
+ .expectstring.done_cb = teco_state_search_done
+);
static gboolean
teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error)
@@ -958,8 +976,7 @@ teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error)
return FALSE;
if (teco_expressions_args()) {
/* TODO: optional count argument? */
- if (!teco_expressions_pop_num_calc(&v1, 0, error))
- return FALSE;
+ v1 = teco_expressions_pop_num(0);
if (v1 <= v2) {
teco_search_parameters.count = 1;
teco_search_parameters.from_buffer = teco_ring_find(v1);
@@ -998,17 +1015,20 @@ teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error)
}
static teco_state_t *
-teco_state_search_all_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_search_all_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
- if (ctx->flags.mode <= TECO_MODE_NORMAL &&
- (!teco_state_search_done(ctx, str, error) ||
- !teco_ed_hook(TECO_ED_HOOK_EDIT, error)))
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ const teco_buffer_t *curbuf = teco_ring_current;
+ if (!teco_state_search_done(ctx, str, error) ||
+ (teco_ring_current != curbuf && !teco_ed_hook(TECO_ED_HOOK_EDIT, error)))
return NULL;
return &teco_state_start;
}
-/*$ N
+/*$ "N" ":N" "search all"
* [n]N[pattern]$ -- Search over buffer-boundaries
* -N[pattern]$
* from,toN[pattern]$
@@ -1054,11 +1074,12 @@ teco_state_search_all_done(teco_machine_main_t *ctx, const teco_string_t *str, G
* This is probably not very useful in practice, so it's not documented.
*/
TECO_DEFINE_STATE_SEARCH(teco_state_search_all,
- .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial
+ .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial,
+ .expectstring.done_cb = teco_state_search_all_done
);
static teco_state_t *
-teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_search_kill_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
@@ -1080,13 +1101,15 @@ teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str,
if (teco_search_parameters.dot < dot) {
/* kill forwards */
sptr_t anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0);
- gsize len = anchor - teco_search_parameters.dot;
+ teco_int_t len_glyphs = teco_interface_bytes2glyphs(anchor) -
+ teco_interface_bytes2glyphs(teco_search_parameters.dot);
if (teco_current_doc_must_undo())
undo__teco_interface_ssm(SCI_GOTOPOS, dot, 0);
teco_interface_ssm(SCI_GOTOPOS, anchor, 0);
- teco_interface_ssm(SCI_DELETERANGE, teco_search_parameters.dot, len);
+ teco_interface_ssm(SCI_DELETERANGE, teco_search_parameters.dot,
+ anchor - teco_search_parameters.dot);
/* NOTE: An undo action is not always created. */
if (teco_current_doc_must_undo() &&
@@ -1095,8 +1118,8 @@ teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str,
/* fix up ranges (^Y) */
for (guint i = 0; i < teco_ranges_count; i++) {
- teco_ranges[i].from -= len;
- teco_ranges[i].to -= len;
+ teco_ranges[i].from -= len_glyphs;
+ teco_ranges[i].to -= len_glyphs;
}
} else {
/* kill backwards */
@@ -1113,7 +1136,7 @@ teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str,
return &teco_state_start;
}
-/*$ FK
+/*$ "FK" ":FK"
* FK[pattern]$ -- Delete up to occurrence of pattern
* [n]FK[pattern]$
* -FK[pattern]$
@@ -1135,10 +1158,12 @@ teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str,
/*
* ::FK is possible but doesn't make much sense, so it's undocumented.
*/
-TECO_DEFINE_STATE_SEARCH(teco_state_search_kill);
+TECO_DEFINE_STATE_SEARCH(teco_state_search_kill,
+ .expectstring.done_cb = teco_state_search_kill_done
+);
static teco_state_t *
-teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_search_delete_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
@@ -1164,7 +1189,7 @@ teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str
return &teco_state_start;
}
-/*$ FD
+/*$ "FD" ":FD" "::FD"
* [n]FD[pattern]$ -- Delete occurrence of pattern
* -FD[pattern]$
* from,toFD[pattern]$
@@ -1178,37 +1203,50 @@ teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str
* Searches for <pattern> just like the regular search command
* (\fBS\fP) but when found deletes the entire occurrence.
*/
-TECO_DEFINE_STATE_SEARCH(teco_state_search_delete);
+TECO_DEFINE_STATE_SEARCH(teco_state_search_delete,
+ .expectstring.done_cb = teco_state_search_delete_done
+);
static gboolean
teco_state_replace_insert_initial(teco_machine_main_t *ctx, GError **error)
{
- if (ctx->flags.mode == TECO_MODE_NORMAL)
- teco_machine_stringbuilding_set_codepage(&ctx->expectstring.machine,
- teco_interface_get_codepage());
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ /*
+ * Overwrites teco_ranges set by the preceding search.
+ * FIXME: Wastes undo tokens in teco_do_search().
+ * Perhaps make this configurable in the state.
+ */
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos);
+ teco_undo_guint(teco_ranges_count) = 1;
+
+ /*
+ * Current document's encoding determines the behaviour of
+ * string building constructs.
+ */
+ teco_machine_stringbuilding_set_codepage(&ctx->expectstring.machine,
+ teco_interface_get_codepage());
return TRUE;
}
-/*
- * FIXME: Could be static
- */
-TECO_DEFINE_STATE_INSERT(teco_state_replace_insert,
+static TECO_DEFINE_STATE_INSERT(teco_state_replace_insert,
.initial_cb = (teco_state_initial_cb_t)teco_state_replace_insert_initial
);
static teco_state_t *
-teco_state_replace_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_replace_ignore_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
return &teco_state_start;
}
-/*
- * FIXME: Could be static
- */
-TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_ignore);
+static TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_ignore,
+ .expectstring.done_cb = teco_state_replace_ignore_done
+);
static teco_state_t *
-teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_replace_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_replace_ignore;
@@ -1225,7 +1263,7 @@ teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GErr
: &teco_state_replace_ignore;
}
-/*$ FS
+/*$ "FS" ":FS" "::FS"
* [n]FS[pattern]$[string]$ -- Search and replace
* -FS[pattern]$[string]$
* from,toFS[pattern]$[string]$
@@ -1248,16 +1286,12 @@ teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GErr
* immediately and interactively.
*/
TECO_DEFINE_STATE_SEARCH(teco_state_replace,
- .expectstring.last = FALSE
+ .expectstring.last = FALSE,
+ .expectstring.done_cb = teco_state_replace_done
);
-/*
- * FIXME: TECO_DEFINE_STATE_INSERT() already defines a done_cb(),
- * so we had to name this differently.
- * Perhaps it simply shouldn't define it.
- */
static teco_state_t *
-teco_state_replace_default_insert_done_overwrite(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_replace_default_insert_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
@@ -1265,55 +1299,53 @@ teco_state_replace_default_insert_done_overwrite(teco_machine_main_t *ctx, const
teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1);
g_assert(replace_reg != NULL);
- if (str->len > 0) {
+ if (str.len > 0) {
if (!replace_reg->vtable->undo_set_string(replace_reg, error) ||
- !replace_reg->vtable->set_string(replace_reg, str->data, str->len,
+ !replace_reg->vtable->set_string(replace_reg, str.data, str.len,
teco_default_codepage(), error))
return NULL;
} else {
g_auto(teco_string_t) replace_str = {NULL, 0};
if (!replace_reg->vtable->get_string(replace_reg, &replace_str.data, &replace_str.len,
NULL, error) ||
- (replace_str.len > 0 && !teco_state_insert_process(ctx, &replace_str, replace_str.len, error)))
+ (replace_str.len > 0 && !teco_state_insert_process(ctx, replace_str, replace_str.len, error)))
return NULL;
}
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos);
return &teco_state_start;
}
-/*
- * FIXME: Could be static
- */
-TECO_DEFINE_STATE_INSERT(teco_state_replace_default_insert,
- .initial_cb = NULL,
- .expectstring.done_cb = teco_state_replace_default_insert_done_overwrite
+static TECO_DEFINE_STATE_INSERT(teco_state_replace_default_insert,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_replace_insert_initial,
+ .expectstring.done_cb = teco_state_replace_default_insert_done
);
static teco_state_t *
-teco_state_replace_default_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_replace_default_ignore_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL ||
- !str->len)
+ !str.len)
return &teco_state_start;
teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1);
g_assert(replace_reg != NULL);
if (!replace_reg->vtable->undo_set_string(replace_reg, error) ||
- !replace_reg->vtable->set_string(replace_reg, str->data, str->len,
+ !replace_reg->vtable->set_string(replace_reg, str.data, str.len,
teco_default_codepage(), error))
return NULL;
return &teco_state_start;
}
-/*
- * FIXME: Could be static
- */
-TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_default_ignore);
+static TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_default_ignore,
+ .expectstring.done_cb = teco_state_replace_default_ignore_done
+);
static teco_state_t *
-teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_replace_default_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_replace_default_ignore;
@@ -1330,7 +1362,7 @@ teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *s
: &teco_state_replace_default_ignore;
}
-/*$ FR
+/*$ "FR" ":FR" "::FR" search-replace
* [n]FR[pattern]$[string]$ -- Search and replace with default
* -FR[pattern]$[string]$
* from,toFR[pattern]$[string]$
@@ -1352,5 +1384,51 @@ teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *s
* register is implied instead.
*/
TECO_DEFINE_STATE_SEARCH(teco_state_replace_default,
- .expectstring.last = FALSE
+ .expectstring.last = FALSE,
+ .expectstring.done_cb = teco_state_replace_default_done
+);
+
+static teco_state_t *
+teco_state_replace_default_all_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
+{
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return &teco_state_replace_default_ignore;
+
+ const teco_buffer_t *curbuf = teco_ring_current;
+ teco_state_t *state = teco_state_replace_default_done(ctx, str, error);
+ if (!state || (curbuf != teco_ring_current && !teco_ed_hook(TECO_ED_HOOK_EDIT, error)))
+ return NULL;
+
+ return state;
+}
+
+/*$ "FN" ":FN" "::FN" "search-replace all"
+ * [n]FN[pattern]$[string]$ -- Search and replace with default over buffer-boundaries
+ * -FN[pattern]$[string]$
+ * from,toFN[pattern]$[string]$
+ * [n]:FN[pattern]$[string]$ -> Success|Failure
+ * -:FN[pattern]$[string]$ -> Success|Failure
+ * from,to:FN[pattern]$[string]$ -> Success|Failure
+ * [n]::FN[pattern]$[string]$ -> Success|Failure
+ * -::FN[pattern]$[string]$ -> Success|Failure
+ * from,to::FN[pattern]$[string]$ -> Success|Failure
+ *
+ * The \fBFN\fP command is similar to the \fBFR\fP command
+ * but will continue to search for occurrences of <pattern> when the
+ * end or beginning of the current buffer is reached.
+ * It searches for <pattern> just like the search over buffer-boundaries
+ * command (\fBN\fP) and replaces the occurrence with <string>
+ * similar to what \fBFR\fP does.
+ * If <string> is empty the string in the global replacement
+ * register is implied instead.
+ *
+ * \fBFN\fP also differs from \fBFR\fP in the interpretation of two arguments.
+ * Using two arguments the search will be bounded between the
+ * buffer with number <from>, up to the buffer with number
+ * <to>.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_replace_default_all,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial,
+ .expectstring.last = FALSE,
+ .expectstring.done_cb = teco_state_replace_default_all_done
);
diff --git a/src/search.h b/src/search.h
index 621fdd1..9bd62f7 100644
--- a/src/search.h
+++ b/src/search.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -22,9 +22,10 @@
void teco_state_control_search_mode(teco_machine_main_t *ctx, GError **error);
-TECO_DECLARE_STATE(teco_state_search);
-TECO_DECLARE_STATE(teco_state_search_all);
-TECO_DECLARE_STATE(teco_state_search_kill);
-TECO_DECLARE_STATE(teco_state_search_delete);
-TECO_DECLARE_STATE(teco_state_replace);
-TECO_DECLARE_STATE(teco_state_replace_default);
+extern teco_state_t teco_state_search;
+extern teco_state_t teco_state_search_all;
+extern teco_state_t teco_state_search_kill;
+extern teco_state_t teco_state_search_delete;
+extern teco_state_t teco_state_replace;
+extern teco_state_t teco_state_replace_default;
+extern teco_state_t teco_state_replace_default_all;
diff --git a/src/spawn.c b/src/spawn.c
index d51dbb1..61718fd 100644
--- a/src/spawn.c
+++ b/src/spawn.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -129,7 +129,7 @@ teco_parse_shell_command_line(const gchar *cmdline, GError **error)
teco_string_t comspec;
if (!reg->vtable->get_string(reg, &comspec.data, &comspec.len, NULL, error))
return NULL;
- if (teco_string_contains(&comspec, '\0')) {
+ if (teco_string_contains(comspec, '\0')) {
teco_string_clear(&comspec);
teco_error_qregcontainsnull_set(error, "$COMSPEC", 8, FALSE);
return NULL;
@@ -150,7 +150,7 @@ teco_parse_shell_command_line(const gchar *cmdline, GError **error)
teco_string_t shell;
if (!reg->vtable->get_string(reg, &shell.data, &shell.len, NULL, error))
return NULL;
- if (teco_string_contains(&shell, '\0')) {
+ if (teco_string_contains(shell, '\0')) {
teco_string_clear(&shell);
teco_error_qregcontainsnull_set(error, "$SHELL", 6, FALSE);
return NULL;
@@ -204,7 +204,7 @@ teco_state_execute_initial(teco_machine_main_t *ctx, GError **error)
teco_int_t line;
teco_spawn_ctx.from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
- if (!teco_expressions_pop_num_calc(&line, 0, error))
+ if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error))
return FALSE;
line += teco_interface_ssm(SCI_LINEFROMPOSITION, teco_spawn_ctx.from, 0);
teco_spawn_ctx.to = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0);
@@ -219,18 +219,13 @@ teco_state_execute_initial(teco_machine_main_t *ctx, GError **error)
break;
}
- default: {
+ default:
/* pipe and replace character range */
- teco_int_t from, to;
- if (!teco_expressions_pop_num_calc(&to, 0, error) ||
- !teco_expressions_pop_num_calc(&from, 0, error))
- return FALSE;
- teco_spawn_ctx.from = teco_interface_glyphs2bytes(from);
- teco_spawn_ctx.to = teco_interface_glyphs2bytes(to);
+ teco_spawn_ctx.to = teco_interface_glyphs2bytes(teco_expressions_pop_num(0));
+ teco_spawn_ctx.from = teco_interface_glyphs2bytes(teco_expressions_pop_num(0));
rc = teco_bool(teco_spawn_ctx.from <= teco_spawn_ctx.to &&
teco_spawn_ctx.from >= 0 && teco_spawn_ctx.to >= 0);
}
- }
if (teco_is_failure(rc)) {
if (!teco_machine_main_eval_colon(ctx)) {
@@ -247,7 +242,7 @@ teco_state_execute_initial(teco_machine_main_t *ctx, GError **error)
}
static teco_state_t *
-teco_state_execute_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_execute_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
/*
* NOTE: With G_SPAWN_LEAVE_DESCRIPTORS_OPEN and without G_SPAWN_SEARCH_PATH_FROM_ENVP,
@@ -290,13 +285,13 @@ teco_state_execute_done(teco_machine_main_t *ctx, const teco_string_t *str, GErr
}
#endif
- if (!str->len || teco_string_contains(str, '\0')) {
+ if (!str.len || teco_string_contains(str, '\0')) {
g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
"Command line must not be empty or contain null-bytes");
goto gerror;
}
- argv = teco_parse_shell_command_line(str->data, error);
+ argv = teco_parse_shell_command_line(str.data, error);
if (!argv)
goto gerror;
@@ -420,8 +415,9 @@ teco_state_execute_done(teco_machine_main_t *ctx, const teco_string_t *str, GErr
teco_interface_ssm(SCI_DELETERANGE, teco_spawn_ctx.from,
teco_spawn_ctx.to - teco_spawn_ctx.from);
- teco_undo_gsize(teco_ranges[0].from) = teco_spawn_ctx.from;
- teco_undo_gsize(teco_ranges[0].to) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(teco_spawn_ctx.from);
+ teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos);
teco_undo_guint(teco_ranges_count) = 1;
}
teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
@@ -490,7 +486,7 @@ cleanup:
/* in cmdline.c */
gboolean teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error);
-/*$ EC pipe filter
+/*$ "EC" :EC" pipe filter
* ECcommand$ -- Execute operating system command and filter buffer contents
* linesECcommand$
* -ECcommand$
@@ -609,7 +605,8 @@ gboolean teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_mach
*/
TECO_DEFINE_STATE_EXPECTSTRING(teco_state_execute,
.initial_cb = (teco_state_initial_cb_t)teco_state_execute_initial,
- .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_execute_process_edit_cmd
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_execute_process_edit_cmd,
+ .expectstring.done_cb = teco_state_execute_done
);
static teco_state_t *
@@ -623,7 +620,7 @@ teco_state_egcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
return &teco_state_execute;
}
-/*$ EG EGq
+/*$ "EG" "EGq" ":EGq"
* EGq command$ -- Set Q-Register to output of operating system command
* linesEGq command$
* -EGq command$
@@ -651,7 +648,8 @@ teco_state_egcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
* The register <q> is defined if it does not already exist.
*/
TECO_DEFINE_STATE_EXPECTQREG(teco_state_egcommand,
- .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT,
+ .expectqreg.got_register_cb = teco_state_egcommand_got_register
);
/*
diff --git a/src/spawn.h b/src/spawn.h
index ef210e9..09764bd 100644
--- a/src/spawn.h
+++ b/src/spawn.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -18,5 +18,5 @@
#include "parser.h"
-TECO_DECLARE_STATE(teco_state_execute);
-TECO_DECLARE_STATE(teco_state_egcommand);
+extern teco_state_t teco_state_execute;
+extern teco_state_t teco_state_egcommand;
diff --git a/src/stdio-commands.c b/src/stdio-commands.c
new file mode 100644
index 0000000..abb6566
--- /dev/null
+++ b/src/stdio-commands.c
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2012-2026 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 <glib.h>
+
+#include "sciteco.h"
+#include "parser.h"
+#include "error.h"
+#include "undo.h"
+#include "expressions.h"
+#include "interface.h"
+#include "cmdline.h"
+#include "core-commands.h"
+#include "stdio-commands.h"
+
+/**
+ * Check whether we are executing directly from the end of the command line.
+ * This works \b only when invoked from the initial_cb.
+ */
+static inline gboolean
+teco_cmdline_is_executing(teco_machine_main_t *ctx)
+{
+ return ctx == &teco_cmdline.machine &&
+ ctx->macro_pc == teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0);
+}
+
+static gboolean is_executing = FALSE;
+
+/**
+ * Print number from stack in the given radix.
+ *
+ * It must be popped manually, so we can call it multiple times
+ * on the same number.
+ */
+static gboolean
+teco_print(teco_machine_main_t *ctx, guint radix, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+ if (!teco_expressions_args()) {
+ teco_error_argexpected_set(error, "=");
+ return FALSE;
+ }
+
+ /*
+ * teco_expressions_format() cannot easily be used
+ * to format __unsigned__ integers.
+ */
+ const gchar *fmt = "%" TECO_INT_MODIFIER "d";
+ switch (radix) {
+ case 8: fmt = "%" TECO_INT_MODIFIER "o"; break;
+ case 16: fmt = "%" TECO_INT_MODIFIER "X"; break;
+ }
+ gchar buf[32];
+ gint len = g_snprintf(buf, sizeof(buf), fmt, teco_expressions_peek_num(0));
+ g_assert(len > 0);
+ if (!teco_machine_main_eval_colon(ctx))
+ buf[len++] = '\n';
+
+ teco_interface_msg_literal(TECO_MSG_USER, buf, len);
+ return TRUE;
+}
+
+/*$ "=" "==" "===" ":=" ":==" ":===" "print number"
+ * <n>= -- Print integer as message
+ * <n>==
+ * <n>===
+ * <n>:=
+ * <n>:==
+ * <n>:===
+ *
+ * Shows integer <n> as a message in the message line and/or
+ * on the console.
+ * One \(lq=\(rq formats the integer as a signed decimal number,
+ * \(lq==\(rq formats as an unsigned octal number and
+ * \(lq===\(rq as an unsigned hexadecimal number.
+ * It is logged with the user-message severity.
+ * The command fails if <n> is not given.
+ *
+ * A noteworthy quirk is that \(lq==\(rq and \(lq===\(rq
+ * will print 2 or 3 numbers in succession when executed
+ * from interactive mode at the end of the command line
+ * in order to guarantee immediate feedback.
+ *
+ * If you want to print multiple values from the stack,
+ * you have to put the \(lq=\(rq into a pass-through loop
+ * or separate the commands with
+ * whitespace (e.g. \(lq^Y= =\(rq).
+ *
+ * If colon-modified the number is printed without a trailing
+ * linefeed.
+ */
+/*
+ * In order to imitate TECO-11 closely, we apply the lookahead
+ * strategy -- `=` and `==` are not executed immediately but only
+ * when a non-`=` character is parsed (cf. `$$` and `^C^C`).
+ * However, this would be very annoying during interactive
+ * execution, therefore we still print the number immediately
+ * and perhaps multiple times:
+ * Typing `===` prints the number first in decimal,
+ * then octal and finally in hexadecimal.
+ * This won't happen e.g. in a loop that is closed on the command-line.
+ */
+static teco_state_t teco_state_print_octal;
+
+static gboolean
+teco_state_print_decimal_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return TRUE;
+ is_executing = teco_cmdline_is_executing(ctx);
+ if (G_LIKELY(!is_executing))
+ return TRUE;
+ /*
+ * Interactive invocation:
+ * don't yet pop number as we may have to print it repeatedly
+ */
+ return teco_print(ctx, 10, error);
+}
+
+static teco_state_t *
+teco_state_print_decimal_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+{
+ if (chr == '=')
+ return &teco_state_print_octal;
+
+ if (ctx->flags.mode == TECO_MODE_NORMAL) {
+ if (G_LIKELY(!is_executing) && !teco_print(ctx, 10, error))
+ return NULL;
+ teco_expressions_pop_num(0);
+ }
+ return teco_state_start_input(ctx, chr, error);
+}
+
+/*
+ * Due to the deferred nature of `=`,
+ * it is valid to end in this state as well.
+ */
+static gboolean
+teco_state_print_decimal_end_of_macro(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return TRUE;
+ if (G_UNLIKELY(is_executing))
+ return TRUE;
+ if (!teco_print(ctx, 10, error))
+ return FALSE;
+ teco_expressions_pop_num(0);
+ return TRUE;
+}
+
+TECO_DEFINE_STATE_START(teco_state_print_decimal,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_print_decimal_initial,
+ .input_cb = (teco_state_input_cb_t)teco_state_print_decimal_input,
+ .end_of_macro_cb = (teco_state_end_of_macro_cb_t)
+ teco_state_print_decimal_end_of_macro
+);
+
+static gboolean
+teco_state_print_octal_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return TRUE;
+ is_executing = teco_cmdline_is_executing(ctx);
+ if (G_LIKELY(!is_executing))
+ return TRUE;
+ /*
+ * Interactive invocation:
+ * don't yet pop number as we may have to print it repeatedly
+ */
+ return teco_print(ctx, 8, error);
+}
+
+static teco_state_t *
+teco_state_print_octal_input(teco_machine_main_t *ctx, gunichar chr, GError **error)
+{
+ if (chr == '=') {
+ if (ctx->flags.mode == TECO_MODE_NORMAL) {
+ if (!teco_print(ctx, 16, error))
+ return NULL;
+ teco_expressions_pop_num(0);
+ }
+ return &teco_state_start;
+ }
+
+ if (ctx->flags.mode == TECO_MODE_NORMAL) {
+ if (G_LIKELY(!is_executing) && !teco_print(ctx, 8, error))
+ return NULL;
+ teco_expressions_pop_num(0);
+ }
+ return teco_state_start_input(ctx, chr, error);
+}
+
+/*
+ * Due to the deferred nature of `==`,
+ * it is valid to end in this state as well.
+ */
+static gboolean
+teco_state_print_octal_end_of_macro(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return TRUE;
+ if (G_UNLIKELY(is_executing))
+ return TRUE;
+ if (!teco_print(ctx, 8, error))
+ return FALSE;
+ teco_expressions_pop_num(0);
+ return TRUE;
+}
+
+static TECO_DEFINE_STATE_START(teco_state_print_octal,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_print_octal_initial,
+ .input_cb = (teco_state_input_cb_t)teco_state_print_octal_input,
+ .end_of_macro_cb = (teco_state_end_of_macro_cb_t)
+ teco_state_print_octal_end_of_macro
+);
+
+static gboolean
+teco_state_print_string_initial(teco_machine_main_t *ctx, GError **error)
+{
+ /*
+ * ^A differs from all other string-taking commands in having
+ * a default ^A escape char.
+ */
+ if (ctx->parent.must_undo)
+ teco_undo_gunichar(ctx->expectstring.machine.escape_char);
+ ctx->expectstring.machine.escape_char = TECO_CTL_KEY('A');
+
+ if (ctx->flags.mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ teco_machine_stringbuilding_set_codepage(&ctx->expectstring.machine,
+ teco_machine_main_eval_colon(ctx)
+ ? SC_CHARSET_ANSI : teco_default_codepage());
+ return TRUE;
+}
+
+static teco_state_t *
+teco_state_print_string_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
+{
+ teco_interface_msg_literal(TECO_MSG_USER, str.data, str.len);
+ return &teco_state_start;
+}
+
+/*$ "^A" ":^A" print "print string"
+ * ^A<string>^A -- Print string as message
+ * @^A/string/
+ * :^A<string>^A
+ *
+ * Print <string> as a message, i.e. in the message line
+ * in interactive mode and if possible on the terminal (stdout) as well.
+ *
+ * \fB^A\fP differs from all other commands in the way <string>
+ * is terminated.
+ * It is terminated by ^A (CTRL+A, ASCII 1) by default.
+ * While the initial \fB^A\fP can be written with upcarets,
+ * the terminating ^A must always be ASCII 1.
+ * You can however overwrite the <string> terminator as usual
+ * by \fB@\fP-modifying the command.
+ *
+ * String-building characters are enabled for this command.
+ * \fB^A\fP outputs strings in the default codepage,
+ * but when colon modified raw ANSI encoding is enforced.
+ */
+/*
+ * NOTE: Codepage is among other things important for
+ * ^EUq, ^E<...> and case folding.
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_print_string,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_print_string_initial,
+ .expectstring.done_cb = teco_state_print_string_done
+);
+
+/*$ T type typeout
+ * [lines]T -- Type out buffer contents as messages
+ * -T
+ * from,toT
+ *
+ * Type out the next or previous number of <lines> from the buffer
+ * as a message, i.e. in the message line in interactive mode
+ * and if possible on the terminal (stdout) as well.
+ * If <lines> is omitted, the sign prefix is implied.
+ * If two arguments are specified, the characters beginning
+ * at position <from> up to the character at position <to>
+ * are copied.
+ *
+ * The semantics of the arguments is analogous to the \fBK\fP
+ * command's arguments.
+ */
+void
+teco_state_start_typeout(teco_machine_main_t *ctx, GError **error)
+{
+ gsize from, len;
+
+ if (!teco_get_range_args("T", &from, &len, error))
+ return;
+
+ /*
+ * NOTE: This may remove the buffer gap since we need a consecutive
+ * piece of memory to log as a single message.
+ * FIXME: In batch mode even this could theoretically be avoided.
+ * Need to add a function like teco_interface_is_batch().
+ * Still, this is probably more efficient than using a temporary
+ * allocation with SCI_GETTEXTRANGEFULL.
+ */
+ const gchar *str = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, from, len);
+ teco_interface_msg_literal(TECO_MSG_USER, str, len);
+}
+
+/*$ "^T" ":^T" "typeout glyph" "get char"
+ * <c1,c2,...>^T -- Type out the numeric arguments as a message or get character from user
+ * <c1,c2,...>:^T
+ * ^T -> codepoint
+ * :^T -> byte
+ *
+ * Types out characters for all the values
+ * on the argument stack (interpreted as codepoints) as messages,
+ * i.e. in the message line in interactive mode
+ * and if possible on the terminal (stdout) as well.
+ * It does so in the order of the arguments, i.e.
+ * <c1> is inserted before <c2>, ecetera.
+ * By default the codepoints are expected to be in the default
+ * codepage, but you can force raw ANSI encoding (for arbitrary
+ * bytes) by colon-modifying the command.
+ *
+ * When called without any argument, \fB^T\fP reads a key from the
+ * user (or from stdin) and returns the corresponding codepoint.
+ * If the default encoding is UTF-8, this will not work
+ * for function keys.
+ * If the default encoding is raw ANSI or if the command is
+ * colon-modified, \fB^T\fP returns raw bytes.
+ * When run in batch mode, this will return whatever byte is
+ * delivered by the attached terminal.
+ * In case stdin is closed, -1 is returned.
+ * In interactive mode, pressing CTRL+D or CTRL+C will also
+ * return -1.
+ */
+void
+teco_state_control_typeout(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+
+ gboolean utf8 = !teco_machine_main_eval_colon(ctx) &&
+ teco_default_codepage() == SC_CP_UTF8;
+
+ guint args = teco_expressions_args();
+ if (!args) {
+ teco_expressions_push(teco_interface_getch(utf8));
+ return;
+ }
+
+ if (!utf8) {
+ /* assume raw ANSI byte output */
+ g_autofree gchar *buf = g_malloc(args);
+ gchar *p = buf+args;
+
+ for (gint i = 0; i < args; i++) {
+ teco_int_t chr = teco_expressions_pop_num(0);
+ if (chr < 0 || chr > 0xFF) {
+ teco_error_codepoint_set(error, "^T");
+ return;
+ }
+ *--p = chr;
+ }
+
+ teco_interface_msg_literal(TECO_MSG_USER, p, args);
+ return;
+ }
+
+ /* 4 bytes should be enough for UTF-8, but we better follow the documentation */
+ g_autofree gchar *buf = g_malloc(args*6);
+ gchar *p = buf;
+
+ for (gint i = args; i > 0; i--) {
+ teco_int_t chr = teco_expressions_peek_num(i-1);
+ if (chr < 0 || !g_unichar_validate(chr)) {
+ teco_error_codepoint_set(error, "^T");
+ return;
+ }
+ p += g_unichar_to_utf8(chr, p);
+ }
+ /* we pop only now since we had to peek in reverse order */
+ for (gint i = 0; i < args; i++)
+ teco_expressions_pop_num(0);
+
+ teco_interface_msg_literal(TECO_MSG_USER, buf, p-buf);
+}
diff --git a/src/stdio-commands.h b/src/stdio-commands.h
new file mode 100644
index 0000000..cf04b34
--- /dev/null
+++ b/src/stdio-commands.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2012-2026 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/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include "parser.h"
+
+void teco_state_start_typeout(teco_machine_main_t *ctx, GError **error);
+void teco_state_control_typeout(teco_machine_main_t *ctx, GError **error);
+
+/*
+ * Command states
+ */
+extern teco_state_t teco_state_print_decimal;
+extern teco_state_t teco_state_print_string;
diff --git a/src/string-utils.c b/src/string-utils.c
index 10e34a8..e9dd148 100644
--- a/src/string-utils.c
+++ b/src/string-utils.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -98,12 +98,12 @@ teco_string_get_coord(const gchar *str, gsize off, guint *pos, guint *line, guin
* @memberof teco_string_t
*/
gsize
-teco_string_diff(const teco_string_t *a, const gchar *b, gsize b_len)
+teco_string_diff(teco_string_t a, const gchar *b, gsize b_len)
{
gsize len = 0;
- while (len < a->len && len < b_len &&
- a->data[len] == b[len])
+ while (len < a.len && len < b_len &&
+ a.data[len] == b[len])
len++;
return len;
@@ -124,12 +124,12 @@ teco_string_diff(const teco_string_t *a, const gchar *b, gsize b_len)
* @memberof teco_string_t
*/
gsize
-teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len)
+teco_string_casediff(teco_string_t a, const gchar *b, gsize b_len)
{
gsize len = 0;
- while (len < a->len && len < b_len) {
- gunichar a_chr = g_utf8_get_char(a->data+len);
+ while (len < a.len && len < b_len) {
+ gunichar a_chr = g_utf8_get_char(a.data+len);
gunichar b_chr = g_utf8_get_char(b+len);
if (g_unichar_tolower(a_chr) != g_unichar_tolower(b_chr))
break;
@@ -141,36 +141,36 @@ teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len)
/** @memberof teco_string_t */
gint
-teco_string_cmp(const teco_string_t *a, const gchar *b, gsize b_len)
+teco_string_cmp(teco_string_t a, const gchar *b, gsize b_len)
{
- for (guint i = 0; i < a->len; i++) {
+ for (guint i = 0; i < a.len; i++) {
if (i == b_len)
/* b is a prefix of a */
return 1;
- gint ret = (gint)a->data[i] - (gint)b[i];
+ gint ret = (gint)a.data[i] - (gint)b[i];
if (ret != 0)
/* a and b have a common prefix of length i */
return ret;
}
- return a->len == b_len ? 0 : -1;
+ return a.len == b_len ? 0 : -1;
}
/** @memberof teco_string_t */
gint
-teco_string_casecmp(const teco_string_t *a, const gchar *b, gsize b_len)
+teco_string_casecmp(teco_string_t a, const gchar *b, gsize b_len)
{
- for (guint i = 0; i < a->len; i++) {
+ for (guint i = 0; i < a.len; i++) {
if (i == b_len)
/* b is a prefix of a */
return 1;
- gint ret = (gint)g_ascii_tolower(a->data[i]) - (gint)g_ascii_tolower(b[i]);
+ gint ret = (gint)g_ascii_tolower(a.data[i]) - (gint)g_ascii_tolower(b[i]);
if (ret != 0)
/* a and b have a common prefix of length i */
return ret;
}
- return a->len == b_len ? 0 : -1;
+ return a.len == b_len ? 0 : -1;
}
/**
@@ -184,22 +184,20 @@ teco_string_casecmp(const teco_string_t *a, const gchar *b, gsize b_len)
* @memberof teco_string_t
*/
const gchar *
-teco_string_last_occurrence(const teco_string_t *str, const gchar *chars)
+teco_string_last_occurrence(teco_string_t str, const gchar *chars)
{
- teco_string_t ret = *str;
-
- if (!ret.len)
+ if (!str.len)
return NULL;
do {
- gint i = teco_string_rindex(&ret, *chars);
+ gint i = teco_string_rindex(str, *chars);
if (i >= 0) {
- ret.data += i+1;
- ret.len -= i+1;
+ str.data += i+1;
+ str.len -= i+1;
}
} while (*chars++);
- return ret.data;
+ return str.data;
}
TECO_DEFINE_UNDO_CALL(teco_string_truncate, teco_string_t *, gsize);
diff --git a/src/string-utils.h b/src/string-utils.h
index 2491d07..a1eda4e 100644
--- a/src/string-utils.h
+++ b/src/string-utils.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -63,6 +63,9 @@ teco_strv_remove(gchar **strv, guint i)
* to functions expecting traditional null-terminated C strings if you can
* guarantee that it contains no null-character other than the trailing one.
*
+ * Since string objects are just two words, they can and should be passed by
+ * value if the callee doesn't have to modify it.
+ *
* @warning For consistency with C idioms the underlying character type is
* `char`, which might be signed!
* Accessing individual characters may yield signed integers and that sign
@@ -102,7 +105,7 @@ teco_string_init(teco_string_t *target, const gchar *str, gsize len)
static inline void
teco_string_init_chunk(teco_string_t *target, const gchar *str, gssize len, GStringChunk *chunk)
{
- target->data = g_string_chunk_insert_len(chunk, str, len);
+ target->data = g_string_chunk_insert_len(chunk, str ? : "", len);
target->len = len;
}
@@ -164,19 +167,19 @@ gchar *teco_string_echo(const gchar *str, gsize len);
void teco_string_get_coord(const gchar *str, gsize off, guint *pos, guint *line, guint *column);
-typedef gsize (*teco_string_diff_t)(const teco_string_t *a, const gchar *b, gsize b_len);
-gsize teco_string_diff(const teco_string_t *a, const gchar *b, gsize b_len);
-gsize teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len);
+typedef gsize (*teco_string_diff_t)(teco_string_t a, const gchar *b, gsize b_len);
+gsize teco_string_diff(teco_string_t a, const gchar *b, gsize b_len);
+gsize teco_string_casediff(teco_string_t a, const gchar *b, gsize b_len);
-typedef gint (*teco_string_cmp_t)(const teco_string_t *a, const gchar *b, gsize b_len);
-gint teco_string_cmp(const teco_string_t *a, const gchar *b, gsize b_len);
-gint teco_string_casecmp(const teco_string_t *a, const gchar *b, gsize b_len);
+typedef gint (*teco_string_cmp_t)(teco_string_t a, const gchar *b, gsize b_len);
+gint teco_string_cmp(teco_string_t a, const gchar *b, gsize b_len);
+gint teco_string_casecmp(teco_string_t a, const gchar *b, gsize b_len);
/** @memberof teco_string_t */
static inline gboolean
-teco_string_contains(const teco_string_t *str, gchar chr)
+teco_string_contains(teco_string_t str, gchar chr)
{
- return str->data && memchr(str->data, chr, str->len);
+ return str.data && memchr(str.data, chr, str.len);
}
/**
@@ -188,26 +191,26 @@ teco_string_contains(const teco_string_t *str, gchar chr)
* @memberof teco_string_t
*/
static inline gint
-teco_string_rindex(const teco_string_t *str, gchar chr)
+teco_string_rindex(teco_string_t str, gchar chr)
{
gint i;
- for (i = str->len-1; i >= 0 && str->data[i] != chr; i--);
+ for (i = str.len-1; i >= 0 && str.data[i] != chr; i--);
return i;
}
-const gchar *teco_string_last_occurrence(const teco_string_t *str, const gchar *chars);
+const gchar *teco_string_last_occurrence(teco_string_t str, const gchar *chars);
/**
* Validate whether string consists exclusively of valid UTF-8, but accept null bytes.
* @note there is g_utf8_validate_len() in Glib 2.60
*/
static inline gboolean
-teco_string_validate_utf8(const teco_string_t *str)
+teco_string_validate_utf8(teco_string_t str)
{
- const gchar *p = str->data;
- while (!g_utf8_validate(p, str->len - (p - str->data), &p) && !*p)
+ const gchar *p = str.data;
+ while (!g_utf8_validate(p, str.len - (p - str.data), &p) && !*p)
p++;
- return p - str->data == str->len;
+ return p - str.data == str.len;
}
/** @memberof teco_string_t */
diff --git a/src/symbols-extract.tes b/src/symbols-extract.tes
index 9a8a270..6c7ef05 100755
--- a/src/symbols-extract.tes
+++ b/src/symbols-extract.tes
@@ -6,23 +6,23 @@
0,2EJ !* FIXME: Memory limiting is too slow *!
-:EMQ[$SCITECOPATH]/getopt.tes
-EMQ[$SCITECOPATH]/string.tes
+:EIQ[$SCITECOPATH]/getopt.tes
+EIQ[$SCITECOPATH]/string.tes
!* read commandline arguments *!
[getopt.p]
-[optstring]p:n: M[getopt]"F (0/0) '
-LR 0X#ou 2LR 0X#in HK
+[optstring]p:n:
+M[getopt]U#ou Q#ou"< Invalid command-line^J 1 ' Q#ou+1U#in
!* copy all defines in input file beginning with prefix *!
-EBN#in <S#defineS[[Q[getopt.p]]M ]; 1:Xa 10:a> EF
+EBN[\#in] <S#defineS[[Q[getopt.p]]M ]; 1:Xa 10:a> EF
!* sort all defines *!
Ga ZJB 0,.M[qsort] J
!* format as C/C++ array *!
I/*
- * AUTOGENERATED FROM Q#in
+ * AUTOGENERATED FROM Q[\#in]
* DO NOT EDIT
*/
#ifdef HAVE_CONFIG_H
@@ -31,7 +31,7 @@ I/*
#include <glib.h>
-#include "Q#in"
+#include "Q[\#in]"
#include "sciteco.h"
#include "symbols.h"
@@ -56,6 +56,6 @@ teco_cmdline_cleanup(void)

!* write output file *!
-2EL EWQ#ou
+2EL EWQ[\#ou]
EX
diff --git a/src/symbols.c b/src/symbols.c
index 4028b7e..dd5856e 100644
--- a/src/symbols.c
+++ b/src/symbols.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -38,6 +38,7 @@
#include "undo.h"
#include "expressions.h"
#include "interface.h"
+#include "cmdline.h"
#include "symbols.h"
teco_symbol_list_t teco_symbol_list_scintilla = {NULL, 0};
@@ -138,7 +139,7 @@ teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, tec
glist_str.data = (gchar *)glist->data + symbol_len;
glist_str.len = strlen(glist_str.data);
- gsize len = teco_string_casediff(&glist_str, (gchar *)entry->data + symbol_len,
+ gsize len = teco_string_casediff(glist_str, (gchar *)entry->data + symbol_len,
strlen(entry->data) - symbol_len);
if (!prefix_len || len < prefix_len)
prefix_len = len;
@@ -166,13 +167,17 @@ teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, tec
* Command states
*/
-/*
- * FIXME: This state could be static.
- */
-TECO_DECLARE_STATE(teco_state_scintilla_lparam);
+static inline sptr_t
+teco_scintilla_ssm(unsigned int iMessage, uptr_t wParam, sptr_t lParam)
+{
+ return teco_view_ssm(teco_ed & TECO_ED_MINIBUF_SSM ? teco_cmdline.view : teco_interface_current_view,
+ iMessage, wParam, lParam);
+}
+
+static teco_state_t teco_state_scintilla_lparam;
static gboolean
-teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_string_t *str, GError **error)
+teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, teco_string_t str, GError **error)
{
if (teco_string_contains(str, '\0')) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
@@ -180,7 +185,7 @@ teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_str
return FALSE;
}
- g_auto(GStrv) symbols = g_strsplit(str->data, ",", -1);
+ g_auto(GStrv) symbols = g_strsplit(str.data, ",", -1);
if (!symbols[0])
return TRUE;
@@ -212,7 +217,7 @@ teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_str
}
static teco_state_t *
-teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_scintilla_lparam;
@@ -226,12 +231,10 @@ teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t
teco_undo_scintilla_message(ctx->scintilla);
memset(&ctx->scintilla, 0, sizeof(ctx->scintilla));
- if ((str->len > 0 && !teco_scintilla_parse_symbols(&ctx->scintilla, str, error)) ||
+ if ((str.len > 0 && !teco_scintilla_parse_symbols(&ctx->scintilla, str, error)) ||
!teco_expressions_eval(FALSE, error))
return NULL;
- teco_int_t value;
-
if (!ctx->scintilla.iMessage) {
if (!teco_expressions_args()) {
g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
@@ -239,9 +242,7 @@ teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t
return NULL;
}
- if (!teco_expressions_pop_num_calc(&value, 0, error))
- return NULL;
- ctx->scintilla.iMessage = value;
+ ctx->scintilla.iMessage = teco_expressions_pop_num(0);
}
return &teco_state_scintilla_lparam;
@@ -250,7 +251,7 @@ teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t
/* in cmdline.c */
gboolean teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx,
gunichar key, GError **error);
-gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str,
+gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, teco_string_t str,
GError **error);
/*$ ES scintilla message
@@ -320,6 +321,13 @@ gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx
* second string argument of \fBES\fP, i.e. it allows you
* to look up style ids by name.
*
+ * By default Scintilla messages are sent to the current buffer's
+ * view or the Q-register view \(em there is only one view for
+ * all Q-registers.
+ * If bit 11 is set in the \fBED\fP flags, the messages will be
+ * sent to the command-line view instead, which allows you to
+ * set up \*(ST syntax highlighting and other styles.
+ *
* .BR Warning :
* Almost all Scintilla messages may be dispatched using
* this command.
@@ -344,17 +352,18 @@ gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx
TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_symbols,
.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_scintilla_symbols_process_edit_cmd,
.insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_scintilla_symbols_insert_completion,
- .expectstring.last = FALSE
+ .expectstring.last = FALSE,
+ .expectstring.done_cb = teco_state_scintilla_symbols_done
);
#ifdef HAVE_LEXILLA
static gpointer
-teco_create_lexer(const teco_string_t *str, GError **error)
+teco_create_lexer(teco_string_t str, GError **error)
{
CreateLexerFn CreateLexerFn = CreateLexer;
- const gchar *lexer = memchr(str->data ? : "", '\0', str->len);
+ const gchar *lexer = memchr(str.data ? : "", '\0', str.len);
if (lexer) {
/* external lexer */
lexer++;
@@ -363,7 +372,7 @@ teco_create_lexer(const teco_string_t *str, GError **error)
* NOTE: The same module can be opened multiple times.
* They are internally reference counted.
*/
- GModule *module = g_module_open(str->data, G_MODULE_BIND_LAZY);
+ GModule *module = g_module_open(str.data, G_MODULE_BIND_LAZY);
if (!module) {
teco_error_module_set(error, "Error opening lexer module");
return NULL;
@@ -385,7 +394,7 @@ teco_create_lexer(const teco_string_t *str, GError **error)
*
* FIXME: In Scintillua distributions, the lexers are usually contained in the
* same directory as the prebuilt shared libraries.
- * Perhaps we should default scintillua.lexers to the dirname in str->data?
+ * Perhaps we should default scintillua.lexers to the dirname in str.data?
*/
teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECO_SCINTILLUA_LEXERS", 26);
if (reg) {
@@ -397,7 +406,7 @@ teco_create_lexer(const teco_string_t *str, GError **error)
}
} else {
/* Lexilla lexer */
- lexer = str->data ? : "";
+ lexer = str.data ? : "";
}
ILexer5 *ret = CreateLexerFn(lexer);
@@ -413,9 +422,9 @@ teco_create_lexer(const teco_string_t *str, GError **error)
#else /* !HAVE_LEXILLA */
static gpointer
-teco_create_lexer(const teco_string_t *str, GError **error)
+teco_create_lexer(teco_string_t str, GError **error)
{
- g_autofree gchar *str_printable = teco_string_echo(str->data, str->len);
+ g_autofree gchar *str_printable = teco_string_echo(str.data, str.len);
g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
"Cannot load lexer \"%s\": Lexilla disabled", str_printable);
return NULL;
@@ -424,7 +433,7 @@ teco_create_lexer(const teco_string_t *str, GError **error)
#endif
static teco_state_t *
-teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, teco_string_t str, GError **error)
{
if (ctx->flags.mode > TECO_MODE_NORMAL)
return &teco_state_start;
@@ -446,10 +455,10 @@ teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *
/*
* FIXME: Should we cache the name to style id?
*/
- guint count = teco_interface_ssm(SCI_GETNAMEDSTYLES, 0, 0);
+ guint count = teco_scintilla_ssm(SCI_GETNAMEDSTYLES, 0, 0);
for (guint id = 0; id < count; id++) {
gchar style[128] = "";
- teco_interface_ssm(SCI_NAMEOFSTYLE, id, (sptr_t)style);
+ teco_scintilla_ssm(SCI_NAMEOFSTYLE, id, (sptr_t)style);
if (!teco_string_cmp(str, style, strlen(style))) {
teco_expressions_push(id);
return &teco_state_start;
@@ -457,13 +466,13 @@ teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *
}
g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
- "Style name \"%s\" not found.", str->data ? : "");
+ "Style name \"%s\" not found.", str.data ? : "");
return NULL;
} else if (ctx->scintilla.iMessage == SCI_SETILEXER) {
lParam = (sptr_t)teco_create_lexer(str, error);
if (!lParam)
return NULL;
- } else if (str->len > 0) {
+ } else if (str.len > 0) {
/*
* Theoretically, Scintilla could use null bytes from the string specified.
* This could only be the case for messages where the string length is
@@ -473,13 +482,13 @@ teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *
* which unlocks useful messages like
* SCI_SETREPRESENTATIONS and SCI_SETPROPERTY.
*/
- const gchar *p = memchr(str->data, '\0', str->len);
+ const gchar *p = memchr(str.data, '\0', str.len);
if (p) {
- ctx->scintilla.wParam = (uptr_t)str->data;
- if (str->len > p - str->data + 1)
+ ctx->scintilla.wParam = (uptr_t)str.data;
+ if (str.len > p - str.data + 1)
lParam = (sptr_t)(p+1);
} else {
- lParam = (sptr_t)str->data;
+ lParam = (sptr_t)str.data;
}
}
@@ -495,10 +504,12 @@ teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *
lParam = v;
}
- teco_expressions_push(teco_interface_ssm(ctx->scintilla.iMessage,
+ teco_expressions_push(teco_scintilla_ssm(ctx->scintilla.iMessage,
ctx->scintilla.wParam, lParam));
return &teco_state_start;
}
-TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_lparam);
+static TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_lparam,
+ .expectstring.done_cb = teco_state_scintilla_lparam_done
+);
diff --git a/src/symbols.h b/src/symbols.h
index 1d0af12..c7db610 100644
--- a/src/symbols.h
+++ b/src/symbols.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -61,4 +61,4 @@ extern teco_symbol_list_t teco_symbol_list_scilexer;
* Command states
*/
-TECO_DECLARE_STATE(teco_state_scintilla_symbols);
+extern teco_state_t teco_state_scintilla_symbols;
diff --git a/src/undo.c b/src/undo.c
index c8b22ab..18f90c5 100644
--- a/src/undo.c
+++ b/src/undo.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -34,6 +34,7 @@ TECO_DEFINE_UNDO_SCALAR(gunichar);
TECO_DEFINE_UNDO_SCALAR(gint);
TECO_DEFINE_UNDO_SCALAR(guint);
TECO_DEFINE_UNDO_SCALAR(gsize);
+TECO_DEFINE_UNDO_SCALAR(gssize);
TECO_DEFINE_UNDO_SCALAR(teco_int_t);
TECO_DEFINE_UNDO_SCALAR(gboolean);
TECO_DEFINE_UNDO_SCALAR(gconstpointer);
diff --git a/src/undo.h b/src/undo.h
index 459fdb0..476ca45 100644
--- a/src/undo.h
+++ b/src/undo.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -182,6 +182,9 @@ TECO_DECLARE_UNDO_SCALAR(guint);
TECO_DECLARE_UNDO_SCALAR(gsize);
#define teco_undo_gsize(VAR) (*teco_undo_object_gsize_push(&(VAR)))
+TECO_DECLARE_UNDO_SCALAR(gssize);
+#define teco_undo_gssize(VAR) (*teco_undo_object_gssize_push(&(VAR)))
+
TECO_DECLARE_UNDO_SCALAR(teco_int_t);
#define teco_undo_int(VAR) (*teco_undo_object_teco_int_t_push(&(VAR)))
diff --git a/src/view.c b/src/view.c
index 71d74e2..a522d1c 100644
--- a/src/view.c
+++ b/src/view.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -145,11 +145,18 @@ teco_view_setup(teco_view_t *ctx)
TECO_DEFINE_UNDO_CALL(teco_view_ssm, teco_view_t *, unsigned int, uptr_t, sptr_t);
-/** @memberof teco_view_t */
+/**
+ * Configure typical TECO representations for control characters.
+ *
+ * You may have to SCI_SETVIEWEOL(TRUE) to see the CR and LF characters.
+ * In order to see the TAB character use SCI_SETTABDRAWMODE(SCTD_CONTROLCHAR).
+ *
+ * @memberof teco_view_t
+ */
void
teco_view_set_representations(teco_view_t *ctx)
{
- static const char *reps[] = {
+ static const gchar reps[][4] = {
"^@", "^A", "^B", "^C", "^D", "^E", "^F", "^G",
"^H", "TAB" /* ^I */, "LF" /* ^J */, "^K", "^L", "CR" /* ^M */, "^N", "^O",
"^P", "^Q", "^R", "^S", "^T", "^U", "^V", "^W",
@@ -198,16 +205,22 @@ teco_view_set_representations(teco_view_t *ctx)
*
* @param ctx The view to load.
* @param channel Channel to read from.
+ * @param clear Whether to completely replace document
+ * (leaving dot at the beginning of the document) or insert at dot
+ * (leaving dot at the end of the insertion).
* @param error A GError.
* @return FALSE in case of a GError.
*
* @memberof teco_view_t
*/
gboolean
-teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **error)
+teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel,
+ gboolean clear, GError **error)
{
gboolean ret = TRUE;
+ unsigned int message = SCI_ADDTEXT;
+
g_auto(teco_eol_reader_t) reader;
teco_eol_reader_init_gio(&reader, channel);
@@ -225,22 +238,27 @@ teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **erro
SC_LINECHARACTERINDEX_UTF32, 0);
teco_view_ssm(ctx, SCI_BEGINUNDOACTION, 0, 0);
- teco_view_ssm(ctx, SCI_CLEARALL, 0, 0);
+ if (clear) {
+ teco_view_ssm(ctx, SCI_CLEARALL, 0, 0);
- /*
- * Preallocate memory based on the file size.
- * May waste a few bytes if file contains DOS EOLs
- * and EOL translation is enabled, but is faster.
- * NOTE: g_io_channel_unix_get_fd() should report the correct fd
- * on Windows, too.
- */
- struct stat stat_buf = {.st_size = 0};
- if (!fstat(g_io_channel_unix_get_fd(channel), &stat_buf) &&
- stat_buf.st_size > 0) {
- ret = teco_memory_check(stat_buf.st_size, error);
- if (!ret)
- goto cleanup;
- teco_view_ssm(ctx, SCI_ALLOCATE, stat_buf.st_size, 0);
+ /*
+ * Preallocate memory based on the file size.
+ * May waste a few bytes if file contains DOS EOLs
+ * and EOL translation is enabled, but is faster.
+ * NOTE: g_io_channel_unix_get_fd() should report the correct fd
+ * on Windows, too.
+ */
+ struct stat stat_buf = {.st_size = 0};
+ if (!fstat(g_io_channel_unix_get_fd(channel), &stat_buf) &&
+ stat_buf.st_size > 0) {
+ ret = teco_memory_check(stat_buf.st_size, error);
+ if (!ret)
+ goto cleanup;
+ teco_view_ssm(ctx, SCI_ALLOCATE, stat_buf.st_size, 0);
+ }
+
+ /* keep dot at beginning of document */
+ message = SCI_APPENDTEXT;
}
for (;;) {
@@ -258,7 +276,7 @@ teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **erro
if (rc == G_IO_STATUS_EOF)
break;
- teco_view_ssm(ctx, SCI_APPENDTEXT, str.len, (sptr_t)str.data);
+ teco_view_ssm(ctx, message, str.len, (sptr_t)str.data);
/*
* Even if we checked initially, knowing the file size,
@@ -285,7 +303,7 @@ teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **erro
* If it is enabled but the stream does not contain any
* EOL characters, the platform default is still assumed.
*/
- if (reader.eol_style >= 0)
+ if (clear && reader.eol_style >= 0)
teco_view_ssm(ctx, SCI_SETEOLMODE, reader.eol_style, 0);
if (reader.eol_style_inconsistent)
@@ -303,12 +321,21 @@ cleanup:
}
/**
- * Load view's document from file.
+ * Load file into view's document.
+ *
+ * @param ctx The view to load.
+ * @param filename File name to read
+ * @param clear Whether to completely replace document
+ * (leaving dot at the beginning of the document) or insert at dot
+ * (leaving dot at the end of the insertion).
+ * @param error A GError.
+ * @return FALSE in case of a GError.
*
* @memberof teco_view_t
*/
gboolean
-teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error)
+teco_view_load_from_file(teco_view_t *ctx, const gchar *filename,
+ gboolean clear, GError **error)
{
g_autoptr(GIOChannel) channel = g_io_channel_new_file(filename, "r", error);
if (!channel)
@@ -322,7 +349,7 @@ teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error
g_io_channel_set_encoding(channel, NULL, NULL);
g_io_channel_set_buffered(channel, FALSE);
- if (!teco_view_load_from_channel(ctx, channel, error)) {
+ if (!teco_view_load_from_channel(ctx, channel, clear, error)) {
g_prefix_error(error, "Error reading file \"%s\": ", filename);
return FALSE;
}
@@ -330,6 +357,44 @@ teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error
return TRUE;
}
+/**
+ * Load stdin until EOF into view's document.
+ *
+ * @param ctx The view to load.
+ * @param clear Whether to completely replace document
+ * (leaving dot at the beginning of the document) or insert at dot
+ * (leaving dot at the end of the insertion).
+ * @param error A GError.
+ * @return FALSE in case of a GError.
+ *
+ * @memberof teco_view_t
+ */
+gboolean
+teco_view_load_from_stdin(teco_view_t *ctx, gboolean clear, GError **error)
+{
+#ifdef G_OS_WIN32
+ g_autoptr(GIOChannel) channel = g_io_channel_win32_new_fd(0);
+#else
+ g_autoptr(GIOChannel) channel = g_io_channel_unix_new(0);
+#endif
+ g_assert(channel != NULL);
+
+ /*
+ * The file loading algorithm does not need buffered
+ * streams, so disabling buffering should increase
+ * performance (slightly).
+ */
+ g_io_channel_set_encoding(channel, NULL, NULL);
+ g_io_channel_set_buffered(channel, FALSE);
+
+ if (!teco_view_load_from_channel(ctx, channel, clear, error)) {
+ g_prefix_error(error, "Error reading stdin: ");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
#if 0
/*
@@ -401,13 +466,10 @@ teco_undo_restore_savepoint_push(gchar *savepoint, const gchar *filename)
static void
teco_make_savepoint(const gchar *filename)
{
- gchar savepoint_basename[FILENAME_MAX];
-
g_autofree gchar *basename = g_path_get_basename(filename);
- g_snprintf(savepoint_basename, sizeof(savepoint_basename),
- ".teco-%d-%s~", savepoint_id, basename);
g_autofree gchar *dirname = g_path_get_dirname(filename);
- gchar *savepoint = g_build_filename(dirname, savepoint_basename, NULL);
+ gchar *savepoint = g_strdup_printf("%s%c.teco-%d-%s~", dirname, G_DIR_SEPARATOR,
+ savepoint_id, basename);
if (g_rename(filename, savepoint)) {
teco_interface_msg(TECO_MSG_WARNING,
@@ -446,6 +508,13 @@ teco_undo_remove_file_push(const gchar *filename)
strcpy(ctx, filename);
}
+/**
+ * Save the view's document to the given IO channel.
+ *
+ * @note This must not emit undo tokens as it is also used by teco_ring_dump_recovery().
+ *
+ * @memberof teco_view_t
+ */
gboolean
teco_view_save_to_channel(teco_view_t *ctx, GIOChannel *channel, GError **error)
{
@@ -485,16 +554,16 @@ teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error)
file_stat.st_gid = -1;
#endif
teco_file_attributes_t attributes = TECO_FILE_INVALID_ATTRIBUTES;
+ gboolean undo_remove_file = FALSE;
if (teco_undo_enabled) {
- if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
+ undo_remove_file = !g_file_test(filename, G_FILE_TEST_IS_REGULAR);
+ if (!undo_remove_file) {
#ifdef G_OS_UNIX
g_stat(filename, &file_stat);
#endif
attributes = teco_file_get_attributes(filename);
teco_make_savepoint(filename);
- } else {
- teco_undo_remove_file_push(filename);
}
}
@@ -503,6 +572,18 @@ teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error)
if (!channel)
return FALSE;
+ if (undo_remove_file) {
+ /*
+ * The file is new, so has to be removed on undo.
+ * If `filename` is a symlink, it's crucial to resolve it now,
+ * since early canonicalization may have failed (for non-existent
+ * path segments).
+ * Now, `filename` is guaranteed to exist.
+ */
+ g_autofree gchar *filename_canon = teco_file_get_absolute_path(filename);
+ teco_undo_remove_file_push(filename_canon);
+ }
+
/*
* teco_view_save_to_channel() expects a buffered and blocking channel
*/
@@ -511,6 +592,7 @@ teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error)
if (!teco_view_save_to_channel(ctx, channel, error)) {
g_prefix_error(error, "Error writing file \"%s\": ", filename);
+ /* file might also be removed (in interactive mode) */
return FALSE;
}
@@ -534,6 +616,31 @@ teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error)
return TRUE;
}
+/** @memberof teco_view_t */
+gboolean
+teco_view_save_to_stdout(teco_view_t *ctx, GError **error)
+{
+#ifdef G_OS_WIN32
+ g_autoptr(GIOChannel) channel = g_io_channel_win32_new_fd(1);
+#else
+ g_autoptr(GIOChannel) channel = g_io_channel_unix_new(1);
+#endif
+ g_assert(channel != NULL);
+
+ /*
+ * teco_view_save_to_channel() expects a buffered and blocking channel
+ */
+ g_io_channel_set_encoding(channel, NULL, NULL);
+ g_io_channel_set_buffered(channel, TRUE);
+
+ if (!teco_view_save_to_channel(ctx, channel, error)) {
+ g_prefix_error(error, "Error writing to stdout: ");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
/**
* Convert a glyph index to a byte offset as used by Scintilla.
*
@@ -628,8 +735,8 @@ teco_view_glyphs2bytes_relative(teco_view_t *ctx, gsize pos, teco_int_t n)
* @param pos The glyph's byte position
* @param len The length of the document in bytes
* @return The requested codepoint.
- * In UTF-8 encoded documents, this might be -1 (incomplete sequence)
- * or -2 (invalid byte sequence).
+ * In UTF-8 encoded documents, this might be -2 (invalid byte sequence)
+ * or -3 (incomplete sequence).
*/
teco_int_t
teco_view_get_character(teco_view_t *ctx, gsize pos, gsize len)
@@ -653,12 +760,15 @@ teco_view_get_character(teco_view_t *ctx, gsize pos, gsize len)
* or repeatedly calling SCI_GETCHARAT.
*/
teco_view_ssm(ctx, SCI_GETTEXTRANGEFULL, 0, (sptr_t)&range);
+ if (!*buf)
+ return 0;
/*
* 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.
*/
- return *buf ? (gint32)g_utf8_get_char_validated(buf, -1) : 0;
+ gint32 rc = g_utf8_get_char_validated(buf, -1);
+ return rc < 0 ? rc-1 : rc;
}
void
diff --git a/src/view.h b/src/view.h
index 7776e5b..4c57eb8 100644
--- a/src/view.h
+++ b/src/view.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2025 Robin Haberkorn
+ * Copyright (C) 2012-2026 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
@@ -52,20 +52,27 @@ teco_view_set_scintilla_undo(teco_view_t *ctx, gboolean state)
teco_view_ssm(ctx, SCI_SETUNDOCOLLECTION, state, 0);
}
-gboolean teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **error);
-gboolean teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error);
+gboolean teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel,
+ gboolean clear, GError **error);
+gboolean teco_view_load_from_file(teco_view_t *ctx, const gchar *filename,
+ gboolean clear, GError **error);
+gboolean teco_view_load_from_stdin(teco_view_t *ctx, gboolean clear, GError **error);
/** @memberof teco_view_t */
-#define teco_view_load(CTX, FROM, ERROR) \
+#define teco_view_load(CTX, FROM, CLEAR, ERROR) \
(_Generic((FROM), GIOChannel * : teco_view_load_from_channel, \
- const gchar * : teco_view_load_from_file)((CTX), (FROM), (ERROR)))
+ gchar * : teco_view_load_from_file, \
+ const gchar * : teco_view_load_from_file)((CTX), (FROM), \
+ (CLEAR), (ERROR)))
gboolean teco_view_save_to_channel(teco_view_t *ctx, GIOChannel *channel, GError **error);
gboolean teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error);
+gboolean teco_view_save_to_stdout(teco_view_t *ctx, GError **error);
/** @memberof teco_view_t */
#define teco_view_save(CTX, TO, ERROR) \
(_Generic((TO), GIOChannel * : teco_view_save_to_channel, \
+ gchar * : teco_view_save_to_file, \
const gchar * : teco_view_save_to_file)((CTX), (TO), (ERROR)))
/** @pure @memberof teco_view_t */