diff options
Diffstat (limited to 'src/ring.c')
| -rw-r--r-- | src/ring.c | 431 |
1 files changed, 314 insertions, 117 deletions
@@ -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); } |
