/*
* Copyright (C) 2012-2025 Robin Haberkorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include
#include
#include "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);
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, 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
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_reinsert_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);
} else {
teco_buffer_free(*buffer);
}
}
/**
* Insert buffer during undo (for closing buffers).
* Ownership of the buffer is passed to the undo token.
*/
static void
teco_undo_ring_reinsert(teco_buffer_t *buffer)
{
teco_buffer_t **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_ring_reinsert_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_table_current = NULL;
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_table_current = NULL;
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_remove_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_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(teco_buffer_t *buffer, GError **error)
{
if (buffer == teco_ring_current) {
if (!teco_ed_hook(TECO_ED_HOOK_CLOSE, error))
return FALSE;
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);
}
/* transfer responsibility to the undo token object */
teco_undo_ring_reinsert(buffer);
return TRUE;
}
void
teco_ring_undo_close(void)
{
undo__teco_buffer_free(teco_ring_current);
undo__teco_ring_remove_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->flags.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(0);
} 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->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 "
"string argument must be empty");
return NULL;
}
return &teco_state_start;
}
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_current_doc_undo_edit(error) &&
teco_ring_edit(globbed_filename, error);
g_free(globbed_filename);
if (!rc)
return NULL;
}
} else {
if (!teco_current_doc_undo_edit(error) ||
!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 .
* If 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.
* 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.
* Note that you can also click on these entries \(em
* if mouse support is enabled \(em to immediately switch
* to any file in the buffer ring just like with any
* other popup.
*
* 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).
*
* does not have to exist on disk.
* In this case, an empty buffer is created and its
* name is guessed from .
* 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 is greater than zero, the string argument
* must be empty.
* Instead selects a buffer from the ring to edit.
* A value of 1 denotes the first buffer, 2 the second,
* ecetera.
*/
TECO_DEFINE_STATE_EXPECTGLOB(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->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))
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 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 .
* Q-Registers have no notion of associated file names,
* so 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 is the name of the saved file and 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);
static teco_state_t *
teco_state_read_file_done(teco_machine_main_t *ctx, const 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$ -- Read and insert file into current buffer
*
* Reads and inserts the given 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);
/*$ EF close
* [n]EF -- Remove buffer from ring
* -EF
* [n]:EF
*
* Removes buffer from buffer ring, effectively
* closing it.
* The optional argument 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.
* If 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, 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 \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_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;
if (!teco_expressions_pop_num_calc(&id, 0, error))
return;
buffer = teco_ring_find(ABS(id));
force = id < 0;
} else {
buffer = teco_ring_current;
force = teco_num_sign < 0;
teco_set_num_sign(1);
}
if (buffer == teco_ring_current && teco_qreg_current) {
const teco_string_t *name = &teco_qreg_current->head.name;
g_autofree gchar *name_printable = teco_string_echo(name->data, name->len);
g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
"Q-Register \"%s\" currently edited", name_printable);
return;
}
if (teco_machine_main_eval_colon(ctx) > 0) {
if (!teco_buffer_save(buffer, NULL, error))
return;
} else {
if (!force && buffer->dirty) {
g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
"Buffer \"%s\" is dirty",
buffer->filename ? : "(Unnamed)");
return;
}
}
teco_ring_close(buffer, error);
}