aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/ring.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ring.c')
-rw-r--r--src/ring.c580
1 files changed, 580 insertions, 0 deletions
diff --git a/src/ring.c b/src/ring.c
new file mode 100644
index 0000000..f9cf41f
--- /dev/null
+++ b/src/ring.c
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2012-2021 Robin Haberkorn
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+#include "file-utils.h"
+#include "interface.h"
+#include "view.h"
+#include "undo.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "expressions.h"
+#include "qreg.h"
+#include "glob.h"
+#include "error.h"
+#include "list.h"
+#include "ring.h"
+
+/** @private @static @memberof teco_buffer_t */
+static teco_buffer_t *
+teco_buffer_new(void)
+{
+ teco_buffer_t *ctx = g_new0(teco_buffer_t, 1);
+ ctx->view = teco_view_new();
+ teco_view_setup(ctx->view);
+ /* only have to do this once: */
+ teco_view_set_representations(ctx->view);
+ return ctx;
+}
+
+/** @private @memberof teco_buffer_t */
+static void
+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);
+}
+
+/** @memberof teco_buffer_t */
+void
+teco_buffer_edit(teco_buffer_t *ctx)
+{
+ teco_interface_show_view(ctx->view);
+ teco_interface_info_update(ctx);
+}
+/** @memberof teco_buffer_t */
+void
+teco_buffer_undo_edit(teco_buffer_t *ctx)
+{
+ undo__teco_interface_info_update_buffer(ctx);
+ undo__teco_interface_show_view(ctx->view);
+}
+
+/** @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))
+ return FALSE;
+
+#if 0 /* NOTE: currently buffer cannot be dirty */
+ undo__teco_interface_info_update_buffer(ctx);
+ teco_undo_gboolean(ctx->dirty) = FALSE;
+#endif
+
+ teco_buffer_set_filename(ctx, filename);
+ return TRUE;
+}
+
+/** @private @memberof teco_buffer_t */
+static gboolean
+teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error)
+{
+ if (!filename && !ctx->filename) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Cannot save the unnamed file "
+ "without providing a file name");
+ return FALSE;
+ }
+
+ if (!teco_view_save(ctx->view, filename ? : ctx->filename, error))
+ return FALSE;
+
+ /*
+ * Undirtify
+ * NOTE: info update is performed by set_filename()
+ */
+ undo__teco_interface_info_update_buffer(ctx);
+ teco_undo_gboolean(ctx->dirty) = FALSE;
+
+ /*
+ * FIXME: necessary also if the filename was not specified but the file
+ * is (was) new, in order to canonicalize the filename.
+ * May be circumvented by cananonicalizing without requiring the file
+ * name to exist (like readlink -f)
+ * NOTE: undo_info_update is already called above
+ */
+ teco_undo_cstring(ctx->filename);
+ teco_buffer_set_filename(ctx, filename ? : ctx->filename);
+
+ return TRUE;
+}
+
+/** @private @memberof teco_buffer_t */
+static inline void
+teco_buffer_free(teco_buffer_t *ctx)
+{
+ teco_view_free(ctx->view);
+ g_free(ctx->filename);
+ g_free(ctx);
+}
+
+TECO_DEFINE_UNDO_CALL(teco_buffer_free, teco_buffer_t *);
+
+static teco_tailq_entry_t teco_ring_head = TECO_TAILQ_HEAD_INITIALIZER(&teco_ring_head);
+
+teco_buffer_t *teco_ring_current = NULL;
+
+teco_buffer_t *
+teco_ring_first(void)
+{
+ return (teco_buffer_t *)teco_ring_head.first;
+}
+
+teco_buffer_t *
+teco_ring_last(void)
+{
+ return (teco_buffer_t *)teco_ring_head.last->prev->next;
+}
+
+static void
+teco_undo_ring_edit_action(teco_buffer_t **buffer, gboolean run)
+{
+ if (run) {
+ /*
+ * assumes that buffer still has correct prev/next
+ * pointers
+ */
+ if (teco_buffer_next(*buffer))
+ 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!
+ */
+static void
+teco_undo_ring_edit(teco_buffer_t *buffer)
+{
+ teco_buffer_t **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_ring_edit_action,
+ sizeof(buffer));
+ if (ctx)
+ *ctx = buffer;
+ else
+ teco_buffer_free(buffer);
+}
+
+teco_int_t
+teco_ring_get_id(teco_buffer_t *buffer)
+{
+ teco_int_t ret = 1;
+
+ for (teco_tailq_entry_t *cur = teco_ring_head.first;
+ cur != &buffer->entry;
+ cur = cur->next)
+ ret++;
+
+ return ret;
+}
+
+teco_buffer_t *
+teco_ring_find_by_name(const gchar *filename)
+{
+ g_autofree gchar *resolved = teco_file_get_absolute_path(filename);
+
+ for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) {
+ teco_buffer_t *buffer = (teco_buffer_t *)cur;
+ if (!g_strcmp0(buffer->filename, resolved))
+ return buffer;
+ }
+
+ return NULL;
+}
+
+teco_buffer_t *
+teco_ring_find_by_id(teco_int_t id)
+{
+ for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) {
+ if (!--id)
+ return (teco_buffer_t *)cur;
+ }
+
+ return NULL;
+}
+
+void
+teco_ring_dirtify(void)
+{
+ if (teco_qreg_current || teco_ring_current->dirty)
+ 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);
+}
+
+gboolean
+teco_ring_is_any_dirty(void)
+{
+ 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;
+ }
+
+ return FALSE;
+}
+
+gboolean
+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))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_ring_edit_by_name(const gchar *filename, GError **error)
+{
+ teco_buffer_t *buffer = teco_ring_find(filename);
+
+ teco_qreg_current = NULL;
+ if (buffer) {
+ teco_ring_current = buffer;
+ teco_buffer_edit(buffer);
+
+ return teco_ed_hook(TECO_ED_HOOK_EDIT, error);
+ }
+
+ buffer = teco_buffer_new();
+ teco_tailq_insert_tail(&teco_ring_head, &buffer->entry);
+
+ teco_ring_current = buffer;
+ teco_ring_undo_close();
+
+ teco_buffer_edit(buffer);
+ if (filename && g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
+ if (!teco_buffer_load(buffer, filename, error))
+ return FALSE;
+
+ teco_interface_msg(TECO_MSG_INFO,
+ "Added file \"%s\" to ring", filename);
+ } else {
+ teco_buffer_set_filename(buffer, filename);
+
+ if (filename)
+ teco_interface_msg(TECO_MSG_INFO,
+ "Added new file \"%s\" to ring",
+ filename);
+ else
+ teco_interface_msg(TECO_MSG_INFO,
+ "Added new unnamed file to ring.");
+ }
+
+ return teco_ed_hook(TECO_ED_HOOK_ADD, error);
+}
+
+gboolean
+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);
+ return FALSE;
+ }
+
+ teco_qreg_current = NULL;
+ teco_ring_current = buffer;
+ teco_buffer_edit(buffer);
+
+ return teco_ed_hook(TECO_ED_HOOK_EDIT, error);
+}
+
+static void
+teco_ring_close_buffer(teco_buffer_t *buffer)
+{
+ teco_tailq_remove(&teco_ring_head, &buffer->entry);
+
+ if (buffer->filename)
+ teco_interface_msg(TECO_MSG_INFO,
+ "Removed file \"%s\" from the ring",
+ buffer->filename);
+ else
+ teco_interface_msg(TECO_MSG_INFO,
+ "Removed unnamed file from the ring.");
+}
+
+TECO_DEFINE_UNDO_CALL(teco_ring_close_buffer, teco_buffer_t *);
+
+gboolean
+teco_ring_close(GError **error)
+{
+ teco_buffer_t *buffer = teco_ring_current;
+
+ 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);
+
+ if (!teco_ring_current)
+ return teco_ring_edit_by_name(NULL, error);
+
+ teco_buffer_edit(teco_ring_current);
+ return teco_ed_hook(TECO_ED_HOOK_EDIT, error);
+}
+
+void
+teco_ring_undo_close(void)
+{
+ undo__teco_buffer_free(teco_ring_current);
+ undo__teco_ring_close_buffer(teco_ring_current);
+}
+
+void
+teco_ring_set_scintilla_undo(gboolean state)
+{
+ for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) {
+ teco_buffer_t *buffer = (teco_buffer_t *)cur;
+ teco_view_set_scintilla_undo(buffer->view, state);
+ }
+}
+
+void
+teco_ring_cleanup(void)
+{
+ teco_tailq_entry_t *next;
+
+ for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = next) {
+ next = cur->next;
+ teco_buffer_free((teco_buffer_t *)cur);
+ }
+
+ teco_ring_head = TECO_TAILQ_HEAD_INITIALIZER(&teco_ring_head);
+}
+
+/*
+ * 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)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ teco_int_t id;
+ if (!teco_expressions_pop_num_calc(&id, -1, error))
+ return FALSE;
+
+ 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);
+ }
+
+ teco_interface_popup_show();
+ } else if (id > 0) {
+ allow_filename = FALSE;
+ if (!teco_current_doc_undo_edit(error) ||
+ !teco_ring_edit(id, error))
+ 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)
+{
+ if (ctx->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;
+ }
+
+ return &teco_state_start;
+ }
+
+ if (!teco_current_doc_undo_edit(error))
+ return NULL;
+
+ 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);
+
+ gchar *globbed_filename;
+ while ((globbed_filename = teco_globber_next(&globber))) {
+ gboolean rc = teco_ring_edit(globbed_filename, error);
+ g_free(globbed_filename);
+ if (!rc)
+ return NULL;
+ }
+ } else {
+ if (!teco_ring_edit_by_name(*filename ? filename : NULL, error))
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ EB edit
+ * [n]EB[file]$ -- Open or edit file
+ * nEB$
+ *
+ * Opens or edits the file with name <file>.
+ * If <file> is not in the buffer ring it is opened,
+ * added to the ring and set as the currently edited
+ * buffer.
+ * If it already exists in the ring, it is merely
+ * made the current file.
+ * <file> may be omitted in which case the default
+ * unnamed buffer is created/edited.
+ * If an argument is specified as 0, EB will additionally
+ * display the buffer ring contents in the window's popup
+ * area.
+ * Naturally this only has any effect in interactive
+ * mode.
+ *
+ * <file> may also be a glob pattern, in which case
+ * all regular files matching the pattern are opened/edited.
+ * Globbing is performed exactly the same as the
+ * \fBEN\fP command does.
+ * Also refer to the section called
+ * .B Glob Patterns
+ * for more details.
+ *
+ * File names of buffers in the ring are normalized
+ * by making them absolute.
+ * Any comparison on file names is performed using
+ * guessed or actual absolute file paths, so that
+ * one file may be referred to in many different ways
+ * (paths).
+ *
+ * <file> does not have to exist on disk.
+ * In this case, an empty buffer is created and its
+ * name is guessed from <file>.
+ * When the newly created buffer is first saved,
+ * the file is created on disk and the buffer's name
+ * will be updated to the absolute path of the file
+ * on disk.
+ *
+ * File names may also be tab-completed and string building
+ * characters are enabled by default.
+ *
+ * If <n> is greater than zero, the string argument
+ * must be empty.
+ * Instead <n> selects a buffer from the ring to edit.
+ * A value of 1 denotes the first buffer, 2 the second,
+ * ecetera.
+ */
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_edit_file,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_edit_file_initial
+);
+
+static teco_state_t *
+teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->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_save(teco_qreg_current, filename, error))
+ return NULL;
+ } else {
+ if (!teco_buffer_save(teco_ring_current, *filename ? filename : NULL, error))
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ EW write save
+ * EW$ -- Save current buffer or Q-Register
+ * EWfile$
+ *
+ * Saves the current buffer to disk.
+ * 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>.
+ * Q-Registers have no notion of associated file names,
+ * so <file> must be always specified.
+ *
+ * In interactive mode, EW is executed immediately and
+ * may be rubbed out.
+ * In order to support that, \*(ST creates so called
+ * save point files.
+ * It does not merely overwrite existing files when saving
+ * but moves them to save point files instead.
+ * Save point files are called \(lq.teco-\fIn\fP-\fIfilename\fP~\(rq,
+ * where <filename> is the name of the saved file and <n> is
+ * a number that is increased with every save operation.
+ * 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
+ * save point file by moving (renaming) it back to its
+ * original path \(em 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.
+ * Otherwise save point files are deleted on command line
+ * termination.
+ *
+ * File names may also be tab-completed and string building
+ * characters are enabled by default.
+ */
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_save_file);