aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/view.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/view.c')
-rw-r--r--src/view.c439
1 files changed, 439 insertions, 0 deletions
diff --git a/src/view.c b/src/view.c
new file mode 100644
index 0000000..e61c1a6
--- /dev/null
+++ b/src/view.c
@@ -0,0 +1,439 @@
+/*
+ * 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 <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+#include "file-utils.h"
+#include "string-utils.h"
+#include "interface.h"
+#include "undo.h"
+#include "error.h"
+#include "qreg.h"
+#include "eol.h"
+#include "view.h"
+
+/** @memberof teco_view_t */
+void
+teco_view_setup(teco_view_t *ctx)
+{
+ /*
+ * Start with or without undo collection,
+ * depending on teco_undo_enabled.
+ */
+ teco_view_ssm(ctx, SCI_SETUNDOCOLLECTION, teco_undo_enabled, 0);
+
+ teco_view_ssm(ctx, SCI_SETFOCUS, TRUE, 0);
+
+ /*
+ * Some Scintilla implementations show the horizontal
+ * scroll bar by default.
+ * Ensure it is never displayed by default.
+ */
+ teco_view_ssm(ctx, SCI_SETHSCROLLBAR, FALSE, 0);
+
+ /*
+ * Only margin 1 is given a width by default.
+ * To provide a minimalist default view, it is disabled.
+ */
+ teco_view_ssm(ctx, SCI_SETMARGINWIDTHN, 1, 0);
+
+ /*
+ * Set some basic styles in order to provide
+ * a consistent look across UIs if no profile
+ * is used. This makes writing UI-agnostic profiles
+ * and color schemes easier.
+ * FIXME: Some settings like fonts should probably
+ * be set per UI (i.e. Scinterm doesn't use it,
+ * GTK might try to use a system-wide default
+ * monospaced font).
+ */
+ teco_view_ssm(ctx, SCI_SETCARETSTYLE, CARETSTYLE_BLOCK, 0);
+ teco_view_ssm(ctx, SCI_SETCARETPERIOD, 0, 0);
+ teco_view_ssm(ctx, SCI_SETCARETFORE, 0xFFFFFF, 0);
+
+ teco_view_ssm(ctx, SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF);
+ teco_view_ssm(ctx, SCI_STYLESETBACK, STYLE_DEFAULT, 0x000000);
+ teco_view_ssm(ctx, SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)"Courier");
+ teco_view_ssm(ctx, SCI_STYLECLEARALL, 0, 0);
+
+ /*
+ * FIXME: The line number background is apparently not
+ * affected by SCI_STYLECLEARALL
+ */
+ teco_view_ssm(ctx, SCI_STYLESETBACK, STYLE_LINENUMBER, 0x000000);
+
+ /*
+ * Use white as the default background color
+ * for call tips. Necessary since this style is also
+ * used for popup windows and we need to provide a sane
+ * default if no color-scheme is applied (and --no-profile).
+ */
+ teco_view_ssm(ctx, SCI_STYLESETFORE, STYLE_CALLTIP, 0x000000);
+ teco_view_ssm(ctx, SCI_STYLESETBACK, STYLE_CALLTIP, 0xFFFFFF);
+}
+
+TECO_DEFINE_UNDO_CALL(teco_view_ssm, teco_view_t *, unsigned int, uptr_t, sptr_t);
+
+/** @memberof teco_view_t */
+void
+teco_view_set_representations(teco_view_t *ctx)
+{
+ static const char *reps[] = {
+ "^@", "^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",
+ "^X", "^Y", "^Z", "$" /* ^[ */, "^\\", "^]", "^^", "^_"
+ };
+
+ for (guint cc = 0; cc < G_N_ELEMENTS(reps); cc++) {
+ gchar buf[] = {(gchar)cc, '\0'};
+ teco_view_ssm(ctx, SCI_SETREPRESENTATION, (uptr_t)buf, (sptr_t)reps[cc]);
+ }
+}
+
+TECO_DEFINE_UNDO_CALL(teco_view_set_representations, teco_view_t *);
+
+/**
+ * Loads the view's document by reading all data from
+ * a GIOChannel.
+ * The EOL style is guessed from the channel's data
+ * (if AUTOEOL is enabled).
+ * This assumes that the channel is blocking.
+ * Also it tries to guess the size of the file behind
+ * channel in order to preallocate memory in Scintilla.
+ *
+ * Any error reading the GIOChannel is propagated as
+ * an exception.
+ *
+ * @param ctx The view to load.
+ * @param channel Channel to read from.
+ * @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_ssm(ctx, SCI_BEGINUNDOACTION, 0, 0);
+ 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)
+ teco_view_ssm(ctx, SCI_ALLOCATE, stat_buf.st_size, 0);
+
+ g_auto(teco_eol_reader_t) reader;
+ teco_eol_reader_init_gio(&reader, channel);
+
+ for (;;) {
+ /*
+ * NOTE: We don't have to free this data since teco_eol_reader_gio_convert()
+ * will point it into its internal buffer.
+ */
+ teco_string_t str;
+
+ GIOStatus rc = teco_eol_reader_convert(&reader, &str.data, &str.len, error);
+ if (rc == G_IO_STATUS_ERROR) {
+ teco_view_ssm(ctx, SCI_ENDUNDOACTION, 0, 0);
+ return FALSE;
+ }
+ if (rc == G_IO_STATUS_EOF)
+ break;
+
+ teco_view_ssm(ctx, SCI_APPENDTEXT, str.len, (sptr_t)str.data);
+ }
+
+ /*
+ * EOL-style guessed.
+ * Save it as the buffer's EOL mode, so save()
+ * can restore the original EOL-style.
+ * If auto-EOL-translation is disabled, this cannot
+ * have been guessed and the buffer's EOL mode should
+ * have a platform default.
+ * If it is enabled but the stream does not contain any
+ * EOL characters, the platform default is still assumed.
+ */
+ if (reader.eol_style >= 0)
+ teco_view_ssm(ctx, SCI_SETEOLMODE, reader.eol_style, 0);
+
+ if (reader.eol_style_inconsistent)
+ teco_interface_msg(TECO_MSG_WARNING,
+ "Inconsistent EOL styles normalized");
+
+ teco_view_ssm(ctx, SCI_ENDUNDOACTION, 0, 0);
+ return TRUE;
+}
+
+/**
+ * Load view's document from file.
+ *
+ * @memberof teco_view_t
+ */
+gboolean
+teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error)
+{
+ g_autoptr(GIOChannel) channel = g_io_channel_new_file(filename, "r", error);
+ if (!channel)
+ return FALSE;
+
+ /*
+ * 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, error)) {
+ g_prefix_error(error, "Error reading file \"%s\": ", filename);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+#if 0
+
+/*
+ * TODO: on UNIX it may be better to open() the current file, unlink() it
+ * and keep the file descriptor in the undo token.
+ * When the operation is undone, the file descriptor's contents are written to
+ * the file (which should be efficient enough because it is written to the same
+ * filesystem). This way we could avoid messing around with save point files.
+ */
+
+#else
+
+static gint savepoint_id = 0;
+
+typedef struct {
+#ifdef G_OS_WIN32
+ teco_file_attributes_t orig_attrs;
+#endif
+
+ gchar *savepoint;
+ gchar filename[];
+} teco_undo_restore_savepoint_t;
+
+static void
+teco_undo_restore_savepoint_action(teco_undo_restore_savepoint_t *ctx, gboolean run)
+{
+ if (!run) {
+ g_unlink(ctx->savepoint);
+ } else if (!g_rename(ctx->savepoint, ctx->filename)) {
+#ifdef G_OS_WIN32
+ if (ctx->orig_attrs != TECO_FILE_INVALID_ATTRIBUTES)
+ teco_file_set_attributes(ctx->filename, ctx->orig_attrs);
+#endif
+ } else {
+ teco_interface_msg(TECO_MSG_WARNING,
+ "Unable to restore save point file \"%s\"",
+ ctx->savepoint);
+ }
+
+ g_free(ctx->savepoint);
+ savepoint_id--;
+}
+
+static void
+teco_undo_restore_savepoint_push(gchar *savepoint, const gchar *filename)
+{
+ teco_undo_restore_savepoint_t *ctx;
+
+ ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_restore_savepoint_action,
+ sizeof(*ctx) + strlen(filename) + 1);
+ if (ctx) {
+ ctx->savepoint = savepoint;
+ strcpy(ctx->filename, filename);
+
+#ifdef G_OS_WIN32
+ ctx->orig_attrs = teco_file_get_attributes(filename);
+ if (ctx->orig_attrs != TECO_FILE_INVALID_ATTRIBUTES)
+ teco_file_set_attributes(savepoint,
+ ctx->orig_attrs | FILE_ATTRIBUTE_HIDDEN);
+#endif
+ } else {
+ g_unlink(savepoint);
+ g_free(savepoint);
+ savepoint_id--;
+ }
+}
+
+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);
+
+ if (g_rename(filename, savepoint)) {
+ teco_interface_msg(TECO_MSG_WARNING,
+ "Unable to create save point file \"%s\"",
+ savepoint);
+ g_free(savepoint);
+ return;
+ }
+ savepoint_id++;
+
+ /*
+ * NOTE: passes ownership of savepoint string to undo token.
+ */
+ teco_undo_restore_savepoint_push(savepoint, filename);
+}
+
+#endif
+
+/*
+ * NOTE: Does not simply undo__g_unlink() since `filename` needs to be
+ * memory managed.
+ */
+static void
+sciteo_undo_remove_file_action(gchar *filename, gboolean run)
+{
+ if (run)
+ g_unlink(filename);
+}
+
+static inline void
+teco_undo_remove_file_push(const gchar *filename)
+{
+ gchar *ctx = teco_undo_push_size((teco_undo_action_t)sciteo_undo_remove_file_action,
+ strlen(filename)+1);
+ if (ctx)
+ strcpy(ctx, filename);
+}
+
+gboolean
+teco_view_save_to_channel(teco_view_t *ctx, GIOChannel *channel, GError **error)
+{
+ g_auto(teco_eol_writer_t) writer;
+ teco_eol_writer_init_gio(&writer, teco_view_ssm(ctx, SCI_GETEOLMODE, 0, 0), channel);
+
+ /* write part of buffer before gap */
+ sptr_t gap = teco_view_ssm(ctx, SCI_GETGAPPOSITION, 0, 0);
+ if (gap > 0) {
+ const gchar *buffer = (const gchar *)teco_view_ssm(ctx, SCI_GETRANGEPOINTER, 0, gap);
+ gssize bytes_written = teco_eol_writer_convert(&writer, buffer, gap, error);
+ if (bytes_written < 0)
+ return FALSE;
+ g_assert(bytes_written == (gsize)gap);
+ }
+
+ /* write part of buffer after gap */
+ gsize size = teco_view_ssm(ctx, SCI_GETLENGTH, 0, 0) - gap;
+ if (size > 0) {
+ const gchar *buffer = (const gchar *)teco_view_ssm(ctx, SCI_GETRANGEPOINTER, gap, (sptr_t)size);
+ gssize bytes_written = teco_eol_writer_convert(&writer, buffer, size, error);
+ if (bytes_written < 0)
+ return FALSE;
+ g_assert(bytes_written == size);
+ }
+
+ return TRUE;
+}
+
+/** @memberof teco_view_t */
+gboolean
+teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error)
+{
+#ifdef G_OS_UNIX
+ GStatBuf file_stat;
+ file_stat.st_uid = -1;
+ file_stat.st_gid = -1;
+#endif
+ teco_file_attributes_t attributes = TECO_FILE_INVALID_ATTRIBUTES;
+
+ if (teco_undo_enabled) {
+ if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
+#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);
+ }
+ }
+
+ /* leaves access mode intact if file still exists */
+ g_autoptr(GIOChannel) channel = g_io_channel_new_file(filename, "w", error);
+ if (!channel)
+ return FALSE;
+
+ /*
+ * 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 file \"%s\": ", filename);
+ return FALSE;
+ }
+
+ /* if file existed but has been renamed, restore attributes */
+ if (attributes != TECO_FILE_INVALID_ATTRIBUTES)
+ teco_file_set_attributes(filename, attributes);
+#ifdef G_OS_UNIX
+ /*
+ * only a good try to inherit owner since process user must have
+ * CHOWN capability traditionally reserved to root only.
+ * FIXME: We should probably fall back to another save point
+ * strategy.
+ */
+ if (fchown(g_io_channel_unix_get_fd(channel),
+ file_stat.st_uid, file_stat.st_gid))
+ teco_interface_msg(TECO_MSG_WARNING,
+ "Unable to preserve owner of \"%s\": %s",
+ filename, g_strerror(errno));
+#endif
+
+ return TRUE;
+}