diff options
Diffstat (limited to 'src')
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, µ); + 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; @@ -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; @@ -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); } @@ -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 @@ -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. @@ -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 +); @@ -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; @@ -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 @@ -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); @@ -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 ); @@ -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); @@ -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 @@ -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(¯o_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(¯o_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; @@ -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 ); @@ -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 @@ -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); } @@ -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; @@ -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); @@ -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))) @@ -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 @@ -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 */ |
