aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am100
-rw-r--r--src/cmdline.c1058
-rw-r--r--src/cmdline.cpp1043
-rw-r--r--src/cmdline.h145
-rw-r--r--src/core-commands.c2510
-rw-r--r--src/core-commands.h71
-rw-r--r--src/doc.c209
-rw-r--r--src/doc.h115
-rw-r--r--src/document.cpp122
-rw-r--r--src/document.h141
-rw-r--r--src/eol.c461
-rw-r--r--src/eol.cpp359
-rw-r--r--src/eol.h180
-rw-r--r--src/error.c173
-rw-r--r--src/error.cpp171
-rw-r--r--src/error.h316
-rw-r--r--src/expressions.c384
-rw-r--r--src/expressions.cpp277
-rw-r--r--src/expressions.h399
-rw-r--r--src/file-utils.c343
-rw-r--r--src/file-utils.h (renamed from src/ioview.h)68
-rw-r--r--src/glob.c (renamed from src/glob.cpp)251
-rw-r--r--src/glob.h60
-rw-r--r--src/goto-commands.c171
-rw-r--r--src/goto-commands.h (renamed from src/symbols-minimal.cpp)21
-rw-r--r--src/goto.c182
-rw-r--r--src/goto.cpp193
-rw-r--r--src/goto.h144
-rw-r--r--src/help.c (renamed from src/help.cpp)319
-rw-r--r--src/help.h78
-rw-r--r--src/interface-curses/Makefile.am11
-rw-r--r--src/interface-curses/curses-info-popup.c211
-rw-r--r--src/interface-curses/curses-info-popup.cpp219
-rw-r--r--src/interface-curses/curses-info-popup.h96
-rw-r--r--src/interface-curses/curses-utils.c (renamed from src/interface-curses/curses-utils.cpp)38
-rw-r--r--src/interface-curses/curses-utils.h22
-rw-r--r--src/interface-curses/interface-curses.h199
-rw-r--r--src/interface-curses/interface.c (renamed from src/interface-curses/interface-curses.cpp)1044
-rw-r--r--src/interface-gtk/Makefile.am26
-rw-r--r--src/interface-gtk/fallback.css51
-rw-r--r--src/interface-gtk/gtk-info-popup.gob331
-rw-r--r--src/interface-gtk/gtkflowbox.c4795
-rw-r--r--src/interface-gtk/gtkflowbox.h180
-rw-r--r--src/interface-gtk/interface-gtk.cpp1132
-rw-r--r--src/interface-gtk/interface-gtk.h179
-rw-r--r--src/interface-gtk/interface.c1203
-rw-r--r--src/interface-gtk/teco-gtk-info-popup.gob446
-rw-r--r--src/interface-gtk/teco-gtk-label.gob (renamed from src/interface-gtk/gtk-canonicalized-label.gob)113
-rw-r--r--src/interface.c120
-rw-r--r--src/interface.cpp180
-rw-r--r--src/interface.h446
-rw-r--r--src/ioview.cpp512
-rw-r--r--src/list.h94
-rw-r--r--src/main.c (renamed from src/main.cpp)393
-rw-r--r--src/memory.c672
-rw-r--r--src/memory.cpp350
-rw-r--r--src/memory.h76
-rw-r--r--src/parser.c902
-rw-r--r--src/parser.cpp2883
-rw-r--r--src/parser.h845
-rw-r--r--src/qreg-commands.c760
-rw-r--r--src/qreg-commands.h90
-rw-r--r--src/qreg.c1542
-rw-r--r--src/qreg.h238
-rw-r--r--src/qregisters.cpp1680
-rw-r--r--src/qregisters.h666
-rw-r--r--src/rb3str.c150
-rw-r--r--src/rb3str.h69
-rw-r--r--src/rbtree.cpp90
-rw-r--r--src/rbtree.h205
-rw-r--r--src/ring.c580
-rw-r--r--src/ring.cpp461
-rw-r--r--src/ring.h266
-rw-r--r--src/scintilla.c349
-rw-r--r--src/scintilla.h64
-rw-r--r--src/sciteco.h159
-rw-r--r--src/search.c1130
-rw-r--r--src/search.cpp946
-rw-r--r--src/search.h130
-rw-r--r--src/spawn.c667
-rw-r--r--src/spawn.cpp662
-rw-r--r--src/spawn.h67
-rw-r--r--src/string-utils.c185
-rw-r--r--src/string-utils.cpp87
-rw-r--r--src/string-utils.h178
-rwxr-xr-xsrc/symbols-extract.tes22
-rw-r--r--src/symbols.cpp75
-rw-r--r--src/symbols.h69
-rw-r--r--src/undo.c163
-rw-r--r--src/undo.cpp104
-rw-r--r--src/undo.h399
-rw-r--r--src/view.c439
-rw-r--r--src/view.h75
93 files changed, 18705 insertions, 21895 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index 3589fdf..a2990d8 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -11,11 +11,17 @@ endif
include $(top_srcdir)/bootstrap.am
include $(top_srcdir)/scintilla.am
-AM_CXXFLAGS = -Wall -Wno-char-subscripts
+# FIXME: Common flags should be in configure.ac
+AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value
+AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr
+
+# NOTE: This may be necessary to ensure that malloc() overriding
+# works. It may prevent elimination of unused functions, though.
+AM_LDFLAGS = -rdynamic
if STATIC_EXECUTABLES
# AM_LDFLAGS are libtool flags, NOT compiler/linker flags
-AM_LDFLAGS = -all-static
+AM_LDFLAGS += -all-static
endif
BUILT_SOURCES =
@@ -26,43 +32,55 @@ dist_noinst_SCRIPTS = symbols-extract.tes
EXTRA_DIST = sciteco.html
noinst_LTLIBRARIES = libsciteco-base.la
-libsciteco_base_la_SOURCES = main.cpp sciteco.h \
- memory.cpp memory.h \
- string-utils.cpp string-utils.h \
- error.cpp error.h \
- cmdline.cpp cmdline.h \
- undo.cpp undo.h \
- expressions.cpp expressions.h \
- document.cpp document.h \
- eol.cpp eol.h \
- ioview.cpp ioview.h \
- qregisters.cpp qregisters.h \
- ring.cpp ring.h \
- parser.cpp parser.h \
- search.cpp search.h \
- spawn.cpp spawn.h \
- glob.cpp glob.h \
- goto.cpp goto.h \
- help.cpp help.h \
- rbtree.cpp rbtree.h \
- symbols.cpp symbols.h \
- interface.cpp interface.h
+libsciteco_base_la_SOURCES = main.c sciteco.h list.h \
+ memory.c memory.h \
+ string-utils.c string-utils.h \
+ file-utils.c file-utils.h \
+ error.c error.h \
+ cmdline.c cmdline.h \
+ undo.c undo.h \
+ expressions.c expressions.h \
+ doc.c doc.h \
+ eol.c eol.h \
+ qreg.c qreg.h \
+ qreg-commands.c qreg-commands.h \
+ ring.c ring.h \
+ parser.c parser.h \
+ core-commands.c core-commands.h \
+ search.c search.h \
+ spawn.c spawn.h \
+ glob.c glob.h \
+ goto.c goto.h \
+ goto-commands.c goto-commands.h \
+ help.c help.h \
+ rb3str.c rb3str.h \
+ scintilla.c scintilla.h \
+ view.c view.h \
+ interface.c interface.h
# NOTE: We cannot link in Scintilla (static library) into
# a libtool convenience library
-libsciteco_base_la_LIBADD = $(LIBSCITECO_INTERFACE)
+libsciteco_base_la_LIBADD = $(LIBSCITECO_INTERFACE) \
+ $(top_builddir)/contrib/dlmalloc/libdlmalloc.la \
+ $(top_builddir)/contrib/rb3ptr/librb3ptr.la
if BOOTSTRAP
noinst_PROGRAMS = sciteco-minimal
-symbols-scintilla.cpp symbols-scilexer.cpp : sciteco-minimal$(EXEEXT)
+sciteco_minimal_SOURCES =
+symbols-scintilla.c symbols-scilexer.c : sciteco-minimal$(EXEEXT)
endif
-sciteco_minimal_SOURCES = symbols-minimal.cpp
sciteco_minimal_LDADD = libsciteco-base.la \
@SCINTILLA_PATH@/bin/scintilla.a
+# Scintilla is unfortunately still written in C++, so we must force
+# Automake to use the C++ linker when linking the binaries.
+# The following hack is actually advocated in the Automake manual.
+nodist_EXTRA_sciteco_minimal_SOURCES = fuck-this-shit.cpp
bin_PROGRAMS = sciteco
sciteco_SOURCES =
-nodist_sciteco_SOURCES = symbols-scintilla.cpp symbols-scilexer.cpp
+nodist_sciteco_SOURCES = symbols-scintilla.c symbols-scilexer.c
sciteco_LDADD = $(sciteco_minimal_LDADD)
+# see above
+nodist_EXTRA_sciteco_SOURCES = fuck-this-shit.cpp
# For MinGW: Compile in resource (contains the icon)
if WIN32
@@ -72,28 +90,14 @@ sciteco_SOURCES += sciteco.rc
endif
CLEANFILES = $(BUILT_SOURCES) \
- symbols-scintilla.cpp symbols-scilexer.cpp
+ symbols-scintilla.c symbols-scilexer.c
-symbols-scintilla.cpp : @SCINTILLA_PATH@/include/Scintilla.h \
- symbols-extract.tes
+symbols-scintilla.c : @SCINTILLA_PATH@/include/Scintilla.h \
+ symbols-extract.tes
$(SCITECO_MINIMAL) -m -- @srcdir@/symbols-extract.tes \
- -p "SCI_" -n scintilla $@ $<
+ -p "SCI_" -n teco_symbol_list_scintilla $@ $<
-symbols-scilexer.cpp : @SCINTILLA_PATH@/include/SciLexer.h \
- symbols-extract.tes
+symbols-scilexer.c : @SCINTILLA_PATH@/include/SciLexer.h \
+ symbols-extract.tes
$(SCITECO_MINIMAL) -m -- @srcdir@/symbols-extract.tes \
- -p "SCLEX_,SCE_" -n scilexer $@ $<
-
-# This installs a wrapper script to libexecdir to be used as
-# the SciTECO interpreter in Hash-Bang lines.
-# It makes sure that option parsing is disabled for all
-# script arguments which is necessary for builds against Glib < 2.44.
-# NOTE: When we raise the Glib requirement to 2.44, the sciteco-wrapper
-# workaround can be removed completely.
-libexec_SCRIPTS = sciteco-wrapper
-CLEANFILES += $(libexec_SCRIPTS)
-
-.PHONY: sciteco-wrapper
-sciteco-wrapper:
- printf '#!/bin/sh\nOPT=$$1\nshift\nexec %s "$$OPT" -- $$@' \
- "$(SCITECO_INSTALLED)" >$@
+ -p "SCLEX_,SCE_" -n teco_symbol_list_scilexer $@ $<
diff --git a/src/cmdline.c b/src/cmdline.c
new file mode 100644
index 0000000..85cbbdd
--- /dev/null
+++ b/src/cmdline.c
@@ -0,0 +1,1058 @@
+/*
+ * 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 <string.h>
+#include <signal.h>
+
+#ifdef HAVE_MALLOC_H
+#include <malloc.h>
+#endif
+#ifdef HAVE_MALLOC_NP_H
+#include <malloc_np.h>
+#endif
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "file-utils.h"
+#include "interface.h"
+#include "view.h"
+#include "expressions.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "qreg-commands.h"
+#include "qreg.h"
+#include "ring.h"
+#include "goto.h"
+#include "help.h"
+#include "undo.h"
+#include "scintilla.h"
+#include "spawn.h"
+#include "eol.h"
+#include "error.h"
+#include "qreg.h"
+#include "cmdline.h"
+
+#if defined(HAVE_MALLOC_TRIM) && !defined(HAVE_DECL_MALLOC_TRIM)
+int malloc_trim(size_t pad);
+#endif
+
+#define TECO_DEFAULT_BREAK_CHARS " \t\v\r\n\f<>,;@"
+
+teco_cmdline_t teco_cmdline = {};
+
+/*
+ * FIXME: Should this be here?
+ * Should perhaps rather be in teco_machine_main_t or teco_cmdline_t.
+ */
+gboolean teco_quit_requested = FALSE;
+
+/** Last terminated command line */
+static teco_string_t teco_last_cmdline = {NULL, 0};
+
+/**
+ * Insert string into command line and execute
+ * it immediately.
+ * It already handles command line replacement (TECO_ERROR_CMDLINE).
+ *
+ * @param data String to insert.
+ * NULL inserts a character from the previously
+ * rubbed out command line (rubin).
+ * @param len Length of string to insert.
+ * @param error A GError.
+ * @return FALSE to throw a GError
+ */
+/*
+ * FIXME: Passing data == NULL to perform a rubin is inelegant.
+ * Better make teco_cmdline_rubin() a proper function.
+ * FIXME: The inner loop should be factored out.
+ */
+gboolean
+teco_cmdline_insert(const gchar *data, gsize len, GError **error)
+{
+ const teco_string_t src = {(gchar *)data, len};
+ teco_string_t old_cmdline = {NULL, 0};
+ guint repl_pc = 0;
+
+ teco_cmdline.machine.macro_pc = teco_cmdline.pc = teco_cmdline.effective_len;
+
+ if (!data) {
+ if (teco_cmdline.effective_len < teco_cmdline.str.len)
+ teco_cmdline.effective_len++;
+ } else {
+ if (!teco_string_cmp(&src, teco_cmdline.str.data + teco_cmdline.effective_len,
+ teco_cmdline.str.len - teco_cmdline.effective_len)) {
+ teco_cmdline.effective_len += len;
+ } else {
+ if (teco_cmdline.effective_len < teco_cmdline.str.len)
+ /* automatically disable immediate editing modifier */
+ 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;
+ }
+ }
+
+ /*
+ * Parse/execute characters, one at a time so
+ * undo tokens get emitted for the corresponding characters.
+ */
+ while (teco_cmdline.pc < teco_cmdline.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 (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_CMDLINE)) {
+ /*
+ * Result of command line replacement (}):
+ * Exchange command lines, avoiding deep copying
+ */
+ teco_qreg_t *cmdline_reg = teco_qreg_table_find(&teco_qreg_table_globals, "\e", 1);
+ teco_string_t new_cmdline;
+
+ if (!cmdline_reg->vtable->get_string(cmdline_reg, &new_cmdline.data, &new_cmdline.len, error))
+ return FALSE;
+
+ /*
+ * Search for first differing character in old and
+ * 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_undo_pop(teco_cmdline.pc);
+
+ g_assert(old_cmdline.len == 0);
+ old_cmdline = teco_cmdline.str;
+ teco_cmdline.str = new_cmdline;
+ teco_cmdline.effective_len = new_cmdline.len;
+ teco_cmdline.machine.macro_pc = repl_pc = teco_cmdline.pc;
+
+ continue;
+ }
+
+ if (tmp_error->domain != TECO_ERROR || tmp_error->code < TECO_ERROR_CMDLINE) {
+ teco_error_add_frame_toplevel();
+ teco_error_display_short(tmp_error);
+
+ if (old_cmdline.len > 0) {
+ /*
+ * Error during command-line replacement.
+ * Replay previous command-line.
+ * This avoids deep copying.
+ */
+ teco_undo_pop(repl_pc);
+
+ teco_string_clear(&teco_cmdline.str);
+ teco_cmdline.str = old_cmdline;
+ teco_cmdline.machine.macro_pc = teco_cmdline.pc = repl_pc;
+
+ /* rubout cmdline replacement command */
+ teco_cmdline.effective_len--;
+ continue;
+ }
+ }
+
+ /* error is handled in teco_cmdline_keypress_c() */
+ g_propagate_error(error, g_steal_pointer(&tmp_error));
+ return FALSE;
+ }
+
+ teco_cmdline.pc++;
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_cmdline_keypress_c(gchar key, GError **error)
+{
+ teco_machine_t *machine = &teco_cmdline.machine.parent;
+ g_autoptr(GError) tmp_error = NULL;
+
+ /*
+ * Cleanup messages,etc...
+ */
+ teco_interface_msg_clear();
+
+ /*
+ * Process immediate editing commands, inserting
+ * characters as necessary into the command line.
+ */
+ if (!machine->current->process_edit_cmd_cb(machine, NULL, key, &tmp_error)) {
+ if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) {
+ /*
+ * Return from top-level macro, results
+ * in command line termination.
+ * The return "arguments" are currently
+ * ignored.
+ */
+ g_assert(machine->current == &teco_state_start);
+
+ teco_interface_popup_clear();
+
+ if (teco_quit_requested) {
+ /* cought by user interface */
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, "");
+ return FALSE;
+ }
+
+ teco_undo_clear();
+ /* also empties all Scintilla undo buffers */
+ teco_ring_set_scintilla_undo(TRUE);
+ teco_view_set_scintilla_undo(teco_qreg_view, TRUE);
+ /*
+ * FIXME: Reset main machine?
+ */
+ teco_goto_table_clear(&teco_cmdline.machine.goto_table);
+ teco_expressions_clear();
+ 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;
+ } else {
+ /*
+ * NOTE: Error message already displayed in
+ * teco_cmdline_insert().
+ *
+ * Undo tokens may have been emitted
+ * (or had to be) before the exception
+ * is thrown. They must be executed so
+ * as if the character had never been
+ * inserted.
+ */
+ teco_undo_pop(teco_cmdline.pc);
+ teco_cmdline.effective_len = teco_cmdline.pc;
+ /* program counter could be messed up */
+ teco_cmdline.machine.macro_pc = teco_cmdline.effective_len;
+ }
+
+#ifdef HAVE_MALLOC_TRIM
+ /*
+ * Undo stacks can grow very large - sometimes large enough to
+ * make the system swap and become unresponsive.
+ * This shrinks the program break after lots of memory has
+ * been freed, reducing the virtual memory size and aiding
+ * in recovering from swapping issues.
+ *
+ * This is particularily important with some memory limiting backends
+ * after hitting the memory limit* as otherwise the program's resident
+ * size won't shrink and it would be impossible to recover.
+ */
+ if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN) ||
+ g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_MEMLIMIT))
+ malloc_trim(0);
+#endif
+ }
+
+ /*
+ * Echo command line
+ */
+ teco_interface_cmdline_update(&teco_cmdline);
+ return TRUE;
+}
+
+gboolean
+teco_cmdline_fnmacro(const gchar *name, GError **error)
+{
+ /*
+ * NOTE: It should be safe to allocate on the stack since
+ * there are only a limited number of possible function key macros.
+ */
+ gchar macro_name[1 + strlen(name)];
+ macro_name[0] = TECO_CTL_KEY('F');
+ memcpy(macro_name+1, name, sizeof(macro_name)-1);
+
+ teco_qreg_t *macro_reg;
+
+ if (teco_ed & TECO_ED_FNKEYS &&
+ (macro_reg = teco_qreg_table_find(&teco_qreg_table_globals, macro_name, sizeof(macro_name)))) {
+ teco_int_t macro_mask;
+ if (!macro_reg->vtable->get_integer(macro_reg, &macro_mask, error))
+ return FALSE;
+
+ if (macro_mask & teco_cmdline.machine.parent.current->fnmacro_mask)
+ return TRUE;
+
+ g_auto(teco_string_t) macro_str = {NULL, 0};
+ return macro_reg->vtable->get_string(macro_reg, &macro_str.data, &macro_str.len, error) &&
+ teco_cmdline_keypress(macro_str.data, macro_str.len, error);
+ }
+
+ /*
+ * Most function key macros have no default action,
+ * except "CLOSE" which quits the application
+ * (this may loose unsaved data but is better than
+ * not doing anything if the user closes the window).
+ * NOTE: Doing the check here is less efficient than
+ * doing it in the UI implementations, but defines
+ * the default actions centrally.
+ * Also, fnmacros are only handled after key presses.
+ */
+ if (!strcmp(name, "CLOSE")) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, "");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_cmdline_cleanup(void)
+{
+ teco_machine_main_clear(&teco_cmdline.machine);
+ teco_string_clear(&teco_cmdline.str);
+ teco_string_clear(&teco_last_cmdline);
+}
+#endif
+
+/*
+ * Commandline key processing.
+ *
+ * These are all the implementations of teco_state_process_edit_cmd_cb_t.
+ * It makes sense to use state callbacks for key processing, as it is
+ * largely state-dependant; but it defines interactive-mode-only
+ * behaviour which can be kept isolated from the rest of the states'
+ * implementation.
+ */
+
+gboolean
+teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ switch (key) {
+ case '\n': /* insert EOL sequence */
+ teco_interface_popup_clear();
+
+ if (teco_ed & TECO_ED_AUTOEOL) {
+ if (!teco_cmdline_insert("\n", 1, error))
+ return FALSE;
+ } else {
+ const gchar *eol = teco_eol_get_seq(teco_interface_ssm(SCI_GETEOLMODE, 0, 0));
+ if (!teco_cmdline_insert(eol, strlen(eol), error))
+ return FALSE;
+ }
+ return TRUE;
+
+ case TECO_CTL_KEY('G'): /* toggle immediate editing modifier */
+ teco_interface_popup_clear();
+
+ teco_cmdline.modifier_enabled = !teco_cmdline.modifier_enabled;
+ teco_interface_msg(TECO_MSG_INFO,
+ "Immediate editing modifier is now %s.",
+ teco_cmdline.modifier_enabled ? "enabled" : "disabled");
+ return TRUE;
+
+ case TECO_CTL_KEY('H'): /* rubout/reinsert character */
+ teco_interface_popup_clear();
+
+ if (teco_cmdline.modifier_enabled) {
+ /* re-insert character */
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+ } else {
+ /* rubout character */
+ teco_cmdline_rubout();
+ }
+ return TRUE;
+
+ case TECO_CTL_KEY('W'): /* rubout/reinsert command */
+ teco_interface_popup_clear();
+
+ if (teco_cmdline.modifier_enabled) {
+ /* reinsert command */
+ do {
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+ } while (!ctx->current->is_start &&
+ teco_cmdline.effective_len < teco_cmdline.str.len);
+ } else {
+ /* rubout command */
+ do
+ teco_cmdline_rubout();
+ while (!ctx->current->is_start);
+ }
+ return TRUE;
+
+#ifdef SIGTSTP
+ case TECO_CTL_KEY('Z'):
+ /*
+ * <CTL/Z> does not raise signal if handling of
+ * special characters temporarily disabled in terminal
+ * (Curses), or command-line is detached from
+ * terminal (GTK+).
+ * This does NOT change the state of the popup window.
+ */
+ raise(SIGTSTP);
+ return TRUE;
+#endif
+ }
+
+ teco_interface_popup_clear();
+ return teco_cmdline_insert(&key, sizeof(key), error);
+}
+
+gboolean
+teco_state_caseinsensitive_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ if (teco_ed & TECO_ED_AUTOCASEFOLD)
+ /* will not modify non-letter keys */
+ key = g_ascii_islower(key) ? g_ascii_toupper(key)
+ : g_ascii_tolower(key);
+
+ return teco_state_process_edit_cmd(ctx, parent_ctx, key, error);
+}
+
+/*
+ * NOTE: The wordchars are null-terminated, so the null byte
+ * is always considered to be a non-wordchar.
+ */
+static inline gboolean
+teco_is_wordchar(const gchar *wordchars, gchar c)
+{
+ return c != '\0' && strchr(wordchars, c);
+}
+
+gboolean
+teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
+ gchar key, GError **error)
+{
+ teco_state_t *current = ctx->parent.current;
+
+ switch (key) {
+ case TECO_CTL_KEY('W'): { /* rubout/reinsert word */
+ teco_interface_popup_clear();
+
+ g_autofree gchar *wchars = g_malloc(teco_interface_ssm(SCI_GETWORDCHARS, 0, 0));
+ teco_interface_ssm(SCI_GETWORDCHARS, 0, (sptr_t)wchars);
+
+ if (teco_cmdline.modifier_enabled) {
+ /* reinsert word chars */
+ while (ctx->parent.current == current &&
+ teco_cmdline.effective_len < teco_cmdline.str.len &&
+ teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len]))
+ 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_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len]))
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+
+ return TRUE;
+ }
+
+ /*
+ * FIXME: In parse-only mode (ctx->result == NULL), we only
+ * get the default behaviour of teco_state_process_edit_cmd().
+ * This may not be a real-life issue serious enough to maintain
+ * a result string even in parse-only mode.
+ *
+ * FIXME: Does not properly rubout string-building commands at the
+ * start of the string argument -- ctx->result->len is not
+ * a valid indicator of argument emptyness.
+ * Since it chains to teco_state_process_edit_cmd() we will instead
+ * rubout the entire command.
+ */
+ if (ctx->result && ctx->result->len > 0) {
+ gboolean is_wordchar = teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]);
+ teco_cmdline_rubout();
+ if (ctx->parent.current != current) {
+ /* rub out string building command */
+ while (ctx->result->len > 0 && ctx->parent.current != current)
+ teco_cmdline_rubout();
+ return TRUE;
+ }
+
+ /*
+ * rubout non-word chars
+ * FIXME: This might rub out part of string building commands, e.g. "EQ[A] ^W"
+ */
+ if (!is_wordchar) {
+ while (ctx->result->len > 0 &&
+ !teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ teco_cmdline_rubout();
+ }
+
+ /* rubout word chars */
+ while (ctx->result->len > 0 &&
+ teco_is_wordchar(wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]))
+ teco_cmdline_rubout();
+
+ return TRUE;
+ }
+
+ /*
+ * Otherwise, the entire string command will be rubbed out.
+ */
+ break;
+ }
+
+ case TECO_CTL_KEY('U'): /* rubout/reinsert entire string */
+ teco_interface_popup_clear();
+
+ if (teco_cmdline.modifier_enabled) {
+ /* reinsert string */
+ while (ctx->parent.current == current &&
+ teco_cmdline.effective_len < teco_cmdline.str.len)
+ if (!teco_cmdline_rubin(error))
+ return FALSE;
+
+ return TRUE;
+ }
+
+ /*
+ * FIXME: In parse only mode (ctx->result == NULL),
+ * this will chain to teco_state_process_edit_cmd() and rubout
+ * only a single character.
+ */
+ if (ctx->result) {
+ /* rubout string */
+ while (ctx->result->len > 0)
+ teco_cmdline_rubout();
+ return TRUE;
+ }
+
+ break;
+
+ case '\t': { /* autocomplete file name */
+ /*
+ * FIXME: Does not autocomplete in parse-only mode (ctx->result == NULL).
+ */
+ if (!teco_cmdline.modifier_enabled || !ctx->result)
+ break;
+
+ /*
+ * TODO: In insertion commands, we can autocomplete
+ * the string at the buffer cursor.
+ */
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ 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);
+ teco_machine_stringbuilding_escape(ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ' ');
+
+ if (!teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error))
+ return FALSE;
+
+ /* may be reset if there was a rubbed out command line */
+ teco_cmdline.modifier_enabled = TRUE;
+
+ return TRUE;
+ }
+ }
+
+ /*
+ * Chaining to the parent (embedding) state machine's handler
+ * makes sure that ^W at the beginning of the string argument
+ * rubs out the entire string command.
+ */
+ return teco_state_process_edit_cmd(parent_ctx, NULL, key, error);
+}
+
+gboolean
+teco_state_stringbuilding_qreg_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
+ gchar chr, GError **error)
+{
+ g_assert(ctx->machine_qregspec != NULL);
+ /* We downcast since teco_machine_qregspec_t is private in qreg.c */
+ teco_machine_t *machine = (teco_machine_t *)ctx->machine_qregspec;
+ return machine->current->process_edit_cmd_cb(machine, &ctx->parent, chr, error);
+}
+
+gboolean
+teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* insert <TAB> indention */
+ if (teco_cmdline.modifier_enabled || teco_interface_ssm(SCI_GETUSETABS, 0, 0))
+ break;
+
+ teco_interface_popup_clear();
+
+ /* insert soft tabs */
+ gint spaces = teco_interface_ssm(SCI_GETTABWIDTH, 0, 0);
+ gint pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ spaces -= teco_interface_ssm(SCI_GETCOLUMN, pos, 0) % spaces;
+
+ while (spaces--)
+ if (!teco_cmdline_insert(" ", 1, error))
+ return FALSE;
+
+ return TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case TECO_CTL_KEY('W'): /* rubout/reinsert file names including directories */
+ teco_interface_popup_clear();
+
+ 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]))
+ 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_rubin(error))
+ return FALSE;
+
+ return TRUE;
+ }
+
+ if (ctx->expectstring.string.len > 0) {
+ /* rubout directory separator */
+ if (G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len-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]))
+ teco_cmdline_rubout();
+
+ return TRUE;
+ }
+
+ /*
+ * Rub out entire command instead of rubbing out nothing.
+ */
+ break;
+
+ case '\t': { /* autocomplete file name */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ /* null-byte not allowed in file names */
+ return TRUE;
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_file_auto_complete(ctx->expectstring.string.data, G_FILE_TEST_EXISTS, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous && ctx->expectstring.nesting == 1)
+ teco_string_append_c(&new_chars_escaped,
+ ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
+
+ return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error);
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete directory */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ /* null-byte not allowed in file names */
+ return TRUE;
+
+ /*
+ * FIXME: We might terminate the command in case of leaf directories.
+ */
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ teco_file_auto_complete(ctx->expectstring.string.data, G_FILE_TEST_IS_DIR, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+
+ return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error);
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ g_assert(ctx->expectqreg != NULL);
+ /*
+ * NOTE: teco_machine_qregspec_t is private, so we downcast to teco_machine_t.
+ * Otherwise, we'd have to move this callback into qreg.c.
+ */
+ teco_state_t *expectqreg_current = ((teco_machine_t *)ctx->expectqreg)->current;
+ return expectqreg_current->process_edit_cmd_cb((teco_machine_t *)ctx->expectqreg, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ switch (key) {
+ case '\t': { /* autocomplete Q-Register name */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ /*
+ * NOTE: This is only for short Q-Register specifications,
+ * so there is no escaping.
+ */
+ g_auto(teco_string_t) new_chars;
+ teco_machine_qregspec_auto_complete(ctx, &new_chars);
+
+ return new_chars.len ? teco_cmdline_insert(new_chars.data, new_chars.len, error) : TRUE;
+ }
+ }
+
+ /*
+ * We chain to the parent (embedding) state machine's handler
+ * since rubout could otherwise rubout the command, invalidating
+ * the state machine. In particular ^W would crash.
+ * This also makes sure that commands like <EQ> are completely
+ * rub out via ^W.
+ */
+ return teco_state_process_edit_cmd(parent_ctx, NULL, key, error);
+}
+
+gboolean
+teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = teco_machine_qregspec_get_stringbuilding(ctx);
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: teco_machine_qregspec_t is private, so we downcast to teco_machine_t.
+ * Otherwise, we'd have to move this callback into qreg.c.
+ *
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, (teco_machine_t *)ctx, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete Q-Register name */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_machine_qregspec_auto_complete(ctx, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ']');
+
+ return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, (teco_machine_t *)ctx, key, error);
+}
+
+gboolean
+teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete file name */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ /*
+ * In the EC command, <TAB> completes files just like ^T
+ *
+ * TODO: Implement shell-command completion by iterating
+ * executables in $PATH
+ */
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ 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);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ' ');
+
+ return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error);
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete Scintilla symbol */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ 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;
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_symbol_list_auto_complete(list, symbol, &new_chars);
+ /*
+ * FIXME: Does not escape `,`. Also, <^Q,> is not allowed currently?
+ */
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ',');
+
+ return teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error);
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete goto label */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ teco_string_t label = ctx->expectstring.string;
+ gint i = teco_string_rindex(&label, ',');
+ if (i >= 0) {
+ label.data += i+1;
+ label.len -= i+1;
+ }
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_goto_table_auto_complete(&ctx->goto_table, label.data, label.len, &new_chars);
+ /*
+ * FIXME: This does not escape `,`. Cannot be escaped via ^Q currently?
+ */
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous)
+ teco_string_append_c(&new_chars_escaped, ',');
+
+ return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+gboolean
+teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error)
+{
+ teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine;
+ teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current;
+
+ /*
+ * NOTE: We don't just define teco_state_stringbuilding_start_process_edit_cmd(),
+ * as it would be hard to subclass/overwrite for different main machine states.
+ */
+ if (!stringbuilding_current->is_start)
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+
+ switch (key) {
+ case '\t': { /* autocomplete help term */
+ if (teco_cmdline.modifier_enabled)
+ break;
+
+ if (teco_interface_popup_is_shown()) {
+ /* cycle through popup pages */
+ teco_interface_popup_show();
+ return TRUE;
+ }
+
+ if (teco_string_contains(&ctx->expectstring.string, '\0'))
+ /* help term must not contain null-byte */
+ return TRUE;
+
+ g_auto(teco_string_t) new_chars, new_chars_escaped;
+ gboolean unambiguous = teco_help_auto_complete(ctx->expectstring.string.data, &new_chars);
+ teco_machine_stringbuilding_escape(stringbuilding_ctx, new_chars.data, new_chars.len, &new_chars_escaped);
+ if (unambiguous && ctx->expectstring.nesting == 1)
+ teco_string_append_c(&new_chars_escaped,
+ ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char);
+
+ return new_chars_escaped.len ? teco_cmdline_insert(new_chars_escaped.data, new_chars_escaped.len, error) : TRUE;
+ }
+ }
+
+ return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error);
+}
+
+/*
+ * Command states
+ */
+
+static teco_state_t *
+teco_state_save_cmdline_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode != TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (!qreg->vtable->undo_set_string(qreg, error) ||
+ !qreg->vtable->set_string(qreg, teco_last_cmdline.data, teco_last_cmdline.len, error))
+ return NULL;
+
+ return &teco_state_start;
+}
+
+/*$ *q
+ * *q -- Save last command line
+ *
+ * Only at the very beginning of a command-line, this command
+ * may be used to save the last command line as a string in
+ * Q-Register <q>.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_save_cmdline,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);
diff --git a/src/cmdline.cpp b/src/cmdline.cpp
deleted file mode 100644
index 9262e27..0000000
--- a/src/cmdline.cpp
+++ /dev/null
@@ -1,1043 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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
-
-#ifdef HAVE_MALLOC_H
-#include <malloc.h>
-#endif
-
-#include <string.h>
-#include <signal.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include "sciteco.h"
-#include "string-utils.h"
-#include "interface.h"
-#include "expressions.h"
-#include "parser.h"
-#include "qregisters.h"
-#include "ring.h"
-#include "ioview.h"
-#include "goto.h"
-#include "help.h"
-#include "undo.h"
-#include "symbols.h"
-#include "spawn.h"
-#include "glob.h"
-#include "error.h"
-#include "cmdline.h"
-
-extern "C" {
-#if defined(HAVE_MALLOC_TRIM) && !HAVE_DECL_MALLOC_TRIM
-int malloc_trim(size_t pad);
-#endif
-}
-
-namespace SciTECO {
-
-static gchar *filename_complete(const gchar *filename, gchar completed = ' ',
- GFileTest file_test = G_FILE_TEST_EXISTS);
-static gchar *symbol_complete(SymbolList &list, const gchar *symbol,
- gchar completed = ' ');
-
-static const gchar *last_occurrence(const gchar *str,
- const gchar *chars = " \t\v\r\n\f<>,;@");
-static inline gboolean filename_is_dir(const gchar *filename);
-
-/** Current command line. */
-Cmdline cmdline;
-
-/** Last terminated command line */
-static Cmdline last_cmdline;
-
-/**
- * Specifies whether the immediate editing modifier
- * is enabled/disabled.
- * It can be toggled with the ^G immediate editing command
- * and influences the undo/redo direction and function of the
- * TAB key.
- */
-static bool modifier_enabled = false;
-
-bool quit_requested = false;
-
-namespace States {
- StateSaveCmdline save_cmdline;
-}
-
-#if 0
-Cmdline *
-copy(void) const
-{
- Cmdline *c = new Cmdline();
-
- if (str)
- c->str = g_memdup(str, len+rubout_len);
- c->len = len;
- c->rubout_len = rubout_len;
-
- return c;
-}
-#endif
-
-/**
- * Throws a command line based on the command line
- * replacement register.
- * It is catched by Cmdline::keypress() to actually
- * perform the command line update.
- */
-void
-Cmdline::replace(void)
-{
- QRegister *cmdline_reg = QRegisters::globals[CTL_KEY_ESC_STR];
- /* use heap object to avoid copy constructors etc. */
- Cmdline *new_cmdline = new Cmdline();
-
- /* FIXME: does not handle null bytes */
- new_cmdline->str = cmdline_reg->get_string();
- new_cmdline->len = strlen(new_cmdline->str);
- new_cmdline->rubout_len = 0;
-
- /*
- * Search for first differing character in old and
- * new command line. This avoids unnecessary rubouts
- * and insertions when the command line is updated.
- */
- for (new_cmdline->pc = 0;
- new_cmdline->pc < len && new_cmdline->pc < new_cmdline->len &&
- str[new_cmdline->pc] == new_cmdline->str[new_cmdline->pc];
- new_cmdline->pc++);
-
- throw new_cmdline;
-}
-
-/**
- * Insert string into command line and execute
- * it immediately.
- * It already handles command line replacement and will
- * only throw SciTECO::Error.
- *
- * @param src String to insert (null-terminated).
- * NULL inserts a character from the previously
- * rubbed out command line.
- */
-void
-Cmdline::insert(const gchar *src)
-{
- Cmdline old_cmdline;
- guint repl_pc = 0;
-
- macro_pc = pc = len;
-
- if (!src) {
- if (rubout_len) {
- len++;
- rubout_len--;
- }
- } else {
- size_t src_len = strlen(src);
-
- if (src_len <= rubout_len && !strncmp(str+len, src, src_len)) {
- len += src_len;
- rubout_len -= src_len;
- } else {
- if (rubout_len)
- /* automatically disable immediate editing modifier */
- modifier_enabled = false;
-
- String::append(str, len, src);
- len += src_len;
- rubout_len = 0;
- }
- }
-
- /*
- * Parse/execute characters, one at a time so
- * undo tokens get emitted for the corresponding characters.
- */
- while (pc < len) {
- try {
- Execute::step(str, pc+1);
- } catch (Cmdline *new_cmdline) {
- /*
- * Result of command line replacement (}):
- * Exchange command lines, avoiding
- * deep copying
- */
- undo.pop(new_cmdline->pc);
-
- old_cmdline = *this;
- *this = *new_cmdline;
- new_cmdline->str = NULL;
- macro_pc = repl_pc = pc;
-
- delete new_cmdline;
- continue;
- } catch (Error &error) {
- error.add_frame(new Error::ToplevelFrame());
- error.display_short();
-
- if (old_cmdline.str) {
- /*
- * Error during command-line replacement.
- * Replay previous command-line.
- * This avoids deep copying.
- */
- undo.pop(repl_pc);
-
- g_free(str);
- *this = old_cmdline;
- old_cmdline.str = NULL;
- macro_pc = pc = repl_pc;
-
- /* rubout cmdline replacement command */
- len--;
- rubout_len++;
- continue;
- }
-
- /* error is handled in Cmdline::keypress() */
- throw;
- }
-
- pc++;
- }
-}
-
-void
-Cmdline::keypress(gchar key)
-{
- /*
- * Cleanup messages,etc...
- */
- interface.msg_clear();
-
- /*
- * Process immediate editing commands, inserting
- * characters as necessary into the command line.
- */
- try {
- States::current->process_edit_cmd(key);
- } catch (Return) {
- /*
- * Return from top-level macro, results
- * in command line termination.
- * The return "arguments" are currently
- * ignored.
- */
- g_assert(States::current == &States::start);
-
- interface.popup_clear();
-
- if (quit_requested)
- /* cought by user interface */
- throw Quit();
-
- undo.clear();
- /* also empties all Scintilla undo buffers */
- ring.set_scintilla_undo(true);
- QRegisters::view.set_scintilla_undo(true);
- Goto::table->clear();
- expressions.clear();
- loop_stack.clear();
-
- last_cmdline = *this;
- str = NULL;
- len = rubout_len = 0;
-
-#ifdef HAVE_MALLOC_TRIM
- /*
- * Glibc/Linux-only optimization: Undo stacks can grow very
- * large - sometimes large enough to make the system
- * swap and become unresponsive.
- * This shrink the program break after lots of memory has
- * been freed, reducing the virtual memory size and aiding
- * in recovering from swapping issues.
- */
- malloc_trim(0);
-#endif
- } catch (Error &error) {
- /*
- * NOTE: Error message already displayed in
- * Cmdline::insert().
- *
- * Undo tokens may have been emitted
- * (or had to be) before the exception
- * is thrown. They must be executed so
- * as if the character had never been
- * inserted.
- */
- undo.pop(pc);
- rubout_len += len-pc;
- len = pc;
- /* program counter could be messed up */
- macro_pc = len;
- }
-
- /*
- * Echo command line
- */
- interface.cmdline_update(this);
-}
-
-void
-Cmdline::fnmacro(const gchar *name)
-{
- gchar macro_name[1 + strlen(name) + 1];
- QRegister *reg;
- gchar *macro;
-
- if (!(Flags::ed & Flags::ED_FNKEYS))
- /* function key macros disabled */
- goto default_action;
-
- macro_name[0] = CTL_KEY('F');
- g_strlcpy(macro_name + 1, name, sizeof(macro_name) - 1);
-
- reg = QRegisters::globals[macro_name];
- if (!reg)
- /* macro undefined */
- goto default_action;
-
- if (reg->get_integer() & States::current->get_fnmacro_mask())
- return;
-
- macro = reg->get_string();
- try {
- keypress(macro);
- } catch (...) {
- /* could be "Quit" for instance */
- g_free(macro);
- throw;
- }
- g_free(macro);
-
- return;
-
- /*
- * Most function key macros have no default action,
- * except "CLOSE" which quits the application
- * (this may loose unsaved data but is better than
- * not doing anything if the user closes the window).
- * NOTE: Doing the check here is less efficient than
- * doing it in the UI implementations, but defines
- * the default actions centrally.
- * Also, fnmacros are only handled after key presses.
- */
-default_action:
- if (!strcmp(name, "CLOSE"))
- throw Quit();
-}
-
-static gchar *
-filename_complete(const gchar *filename, gchar completed,
- GFileTest file_test)
-{
- gchar *filename_expanded;
- gsize filename_len;
- gchar *dirname, *basename, dir_sep;
- gsize dirname_len;
- const gchar *cur_basename;
-
- GDir *dir;
- GSList *files = NULL;
- guint files_len = 0;
- gchar *insert = NULL;
- gsize prefix_len = 0;
-
- if (Globber::is_pattern(filename))
- return NULL;
-
- filename_expanded = expand_path(filename);
- filename_len = strlen(filename_expanded);
-
- /*
- * Derive base and directory names.
- * We do not use g_path_get_basename() or g_path_get_dirname()
- * since we need strict suffixes and prefixes of filename
- * in order to construct paths of entries in dirname
- * that are suitable for auto completion.
- */
- dirname_len = file_get_dirname_len(filename_expanded);
- dirname = g_strndup(filename_expanded, dirname_len);
- basename = filename_expanded + dirname_len;
-
- dir = g_dir_open(dirname_len ? dirname : ".", 0, NULL);
- if (!dir) {
- g_free(dirname);
- g_free(filename_expanded);
- return NULL;
- }
-
- /*
- * On Windows, both forward and backslash
- * directory separators are allowed in directory
- * names passed to glib.
- * To imitate glib's behaviour, we use
- * the last valid directory separator in `filename_expanded`
- * to generate new separators.
- * This also allows forward-slash auto-completion
- * on Windows.
- */
- dir_sep = dirname_len ? dirname[dirname_len-1]
- : G_DIR_SEPARATOR;
-
- while ((cur_basename = g_dir_read_name(dir))) {
- gchar *cur_filename;
-
- if (!g_str_has_prefix(cur_basename, basename))
- continue;
-
- /*
- * dirname contains any directory separator,
- * so g_strconcat() works here.
- */
- cur_filename = g_strconcat(dirname, cur_basename, NIL);
-
- /*
- * NOTE: This avoids g_file_test() for G_FILE_TEST_EXISTS
- * since the file we process here should always exist.
- */
- if ((!*basename && !file_is_visible(cur_filename)) ||
- (file_test != G_FILE_TEST_EXISTS &&
- !g_file_test(cur_filename, file_test))) {
- g_free(cur_filename);
- continue;
- }
-
- if (file_test == G_FILE_TEST_IS_DIR ||
- g_file_test(cur_filename, G_FILE_TEST_IS_DIR))
- String::append(cur_filename, dir_sep);
-
- files = g_slist_prepend(files, cur_filename);
-
- if (g_slist_next(files)) {
- const gchar *other_file = (gchar *)g_slist_next(files)->data;
- gsize len = String::diff(other_file + filename_len,
- cur_filename + filename_len);
- if (len < prefix_len)
- prefix_len = len;
- } else {
- prefix_len = strlen(cur_filename + filename_len);
- }
-
- files_len++;
- }
- if (prefix_len > 0)
- insert = g_strndup((gchar *)files->data + filename_len, prefix_len);
-
- g_dir_close(dir);
- g_free(dirname);
- g_free(filename_expanded);
-
- if (!insert && files_len > 1) {
- files = g_slist_sort(files, (GCompareFunc)g_strcmp0);
-
- for (GSList *file = files; file; file = g_slist_next(file)) {
- InterfaceCurrent::PopupEntryType type;
- bool is_buffer = false;
-
- if (filename_is_dir((gchar *)file->data)) {
- type = InterfaceCurrent::POPUP_DIRECTORY;
- } else {
- type = InterfaceCurrent::POPUP_FILE;
- /* FIXME: inefficient */
- is_buffer = ring.find((gchar *)file->data);
- }
-
- interface.popup_add(type, (gchar *)file->data,
- is_buffer);
- }
-
- interface.popup_show();
- } else if (completed && files_len == 1 &&
- !filename_is_dir((gchar *)files->data)) {
- /*
- * FIXME: If we are completing only directories,
- * we can theoretically insert the completed character
- * after directories without subdirectories
- */
- String::append(insert, completed);
- }
-
- g_slist_free_full(files, g_free);
-
- return insert;
-}
-
-static gchar *
-symbol_complete(SymbolList &list, const gchar *symbol, gchar completed)
-{
- GList *glist;
- guint glist_len = 0;
- gchar *insert = NULL;
- gsize symbol_len;
- gsize prefix_len = 0;
-
- if (!symbol)
- symbol = "";
- symbol_len = strlen(symbol);
-
- glist = list.get_glist();
- if (!glist)
- return NULL;
- glist = g_list_copy(glist);
- if (!glist)
- return NULL;
- /* NOTE: element data must not be freed */
-
- for (GList *entry = g_list_first(glist), *next = g_list_next(entry);
- entry != NULL;
- entry = next, next = entry ? g_list_next(entry) : NULL) {
- if (!g_str_has_prefix((gchar *)entry->data, symbol)) {
- glist = g_list_delete_link(glist, entry);
- continue;
- }
-
- gsize len = String::diff((gchar *)glist->data + symbol_len,
- (gchar *)entry->data + symbol_len);
- if (!prefix_len || len < prefix_len)
- prefix_len = len;
-
- glist_len++;
- }
- if (prefix_len > 0)
- insert = g_strndup((gchar *)glist->data + symbol_len, prefix_len);
-
- if (!insert && glist_len > 1) {
- for (GList *entry = g_list_first(glist);
- entry != NULL;
- entry = g_list_next(entry)) {
- interface.popup_add(InterfaceCurrent::POPUP_PLAIN,
- (gchar *)entry->data);
- }
-
- interface.popup_show();
- } else if (glist_len == 1) {
- String::append(insert, completed);
- }
-
- g_list_free(glist);
-
- return insert;
-}
-
-/*
- * Commandline key processing.
- *
- * These are all the implementations of State::process_edit_cmd().
- * It makes sense to use virtual methods for key processing, as it is
- * largely state-dependant; but it defines interactive-mode-only
- * behaviour which can be kept isolated from the rest of the states'
- * implementation.
- */
-
-void
-State::process_edit_cmd(gchar key)
-{
- switch (key) {
- case '\n': /* insert EOL sequence */
- interface.popup_clear();
-
- if (Flags::ed & Flags::ED_AUTOEOL)
- cmdline.insert("\n");
- else
- cmdline.insert(get_eol_seq(interface.ssm(SCI_GETEOLMODE)));
- return;
-
- case CTL_KEY('G'): /* toggle immediate editing modifier */
- interface.popup_clear();
-
- modifier_enabled = !modifier_enabled;
- interface.msg(InterfaceCurrent::MSG_INFO,
- "Immediate editing modifier is now %s.",
- modifier_enabled ? "enabled" : "disabled");
- return;
-
- case CTL_KEY('H'): /* rubout/reinsert character */
- interface.popup_clear();
-
- if (modifier_enabled)
- /* re-insert character */
- cmdline.insert();
- else
- /* rubout character */
- cmdline.rubout();
- return;
-
- case CTL_KEY('W'): /* rubout/reinsert command */
- interface.popup_clear();
-
- if (modifier_enabled) {
- /* reinsert command */
- do
- cmdline.insert();
- while (!States::is_start() && cmdline.rubout_len);
- } else {
- /* rubout command */
- do
- cmdline.rubout();
- while (!States::is_start());
- }
- return;
-
-#ifdef SIGTSTP
- case CTL_KEY('Z'):
- /*
- * <CTL/Z> does not raise signal if handling of
- * special characters temporarily disabled in terminal
- * (Curses), or command-line is detached from
- * terminal (GTK+).
- * This does NOT change the state of the popup window.
- */
- raise(SIGTSTP);
- return;
-#endif
- }
-
- interface.popup_clear();
- cmdline.insert(key);
-}
-
-void
-StateCaseInsensitive::process_edit_cmd(gchar key)
-{
- if (Flags::ed & Flags::ED_AUTOCASEFOLD)
- /* will not modify non-letter keys */
- key = g_ascii_islower(key) ? g_ascii_toupper(key)
- : g_ascii_tolower(key);
-
- State::process_edit_cmd(key);
-}
-
-void
-StateExpectString::process_edit_cmd(gchar key)
-{
- switch (key) {
- case CTL_KEY('W'): { /* rubout/reinsert word */
- interface.popup_clear();
-
- gchar wchars[interface.ssm(SCI_GETWORDCHARS)];
- interface.ssm(SCI_GETWORDCHARS, 0, (sptr_t)wchars);
-
- if (modifier_enabled) {
- /* reinsert word chars */
- while (States::current == this && cmdline.rubout_len &&
- strchr(wchars, cmdline.str[cmdline.len]))
- cmdline.insert();
-
- /* reinsert non-word chars */
- while (States::current == this && cmdline.rubout_len &&
- !strchr(wchars, cmdline.str[cmdline.len]))
- cmdline.insert();
- return;
- }
-
- if (strings[0] && *strings[0]) {
- /* rubout non-word chars */
- while (strings[0] && *strings[0] &&
- !strchr(wchars, cmdline.str[cmdline.len-1]))
- cmdline.rubout();
-
- /* rubout word chars */
- while (strings[0] && *strings[0] &&
- strchr(wchars, cmdline.str[cmdline.len-1]))
- cmdline.rubout();
- return;
- }
-
- /*
- * Otherwise, the entire command string will
- * be rubbed out.
- */
- break;
- }
-
- case CTL_KEY('U'): /* rubout/reinsert string */
- interface.popup_clear();
-
- if (modifier_enabled) {
- /* reinsert string */
- while (States::current == this && cmdline.rubout_len)
- cmdline.insert();
- } else {
- /* rubout string */
- while (strings[0] && *strings[0])
- cmdline.rubout();
- }
- return;
-
- case '\t': /* autocomplete file name */
- if (modifier_enabled) {
- /*
- * TODO: In insertion commands, we can autocomplete
- * the string at the buffer cursor.
- */
- /* autocomplete filename using string argument */
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- const gchar *filename = last_occurrence(strings[0]);
- gchar *new_chars = filename_complete(filename);
-
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
-
- /* may be reset if there was a rubbed out command line */
- modifier_enabled = true;
- return;
- }
-
- if (machine.qregspec_machine) {
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- gchar *new_chars = machine.qregspec_machine->auto_complete();
-
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
- return;
- }
- break;
- }
-
- State::process_edit_cmd(key);
-}
-
-void
-StateInsert::process_edit_cmd(gchar key)
-{
- gint spaces;
-
- switch (key) {
- case '\t': /* insert <TAB> indention */
- if (modifier_enabled || interface.ssm(SCI_GETUSETABS))
- break;
-
- interface.popup_clear();
-
- /* insert soft tabs */
- spaces = interface.ssm(SCI_GETTABWIDTH);
- spaces -= interface.ssm(SCI_GETCOLUMN,
- interface.ssm(SCI_GETCURRENTPOS)) % spaces;
-
- while (spaces--)
- cmdline.insert(' ');
- return;
- }
-
- StateExpectString::process_edit_cmd(key);
-}
-
-void
-StateExpectFile::process_edit_cmd(gchar key)
-{
- gchar *new_chars;
-
- switch (key) {
- case CTL_KEY('W'): /* rubout/reinsert file names including directories */
- interface.popup_clear();
-
- if (modifier_enabled) {
- /* reinsert one level of file name */
- while (States::current == this && cmdline.rubout_len &&
- !G_IS_DIR_SEPARATOR(cmdline.str[cmdline.len]))
- cmdline.insert();
-
- /* reinsert final directory separator */
- if (States::current == this && cmdline.rubout_len &&
- G_IS_DIR_SEPARATOR(cmdline.str[cmdline.len]))
- cmdline.insert();
- return;
- }
-
- if (strings[0] && *strings[0]) {
- /* rubout directory separator */
- if (strings[0] && *strings[0] &&
- G_IS_DIR_SEPARATOR(cmdline.str[cmdline.len-1]))
- cmdline.rubout();
-
- /* rubout one level of file name */
- while (strings[0] && *strings[0] &&
- !G_IS_DIR_SEPARATOR(cmdline.str[cmdline.len-1]))
- cmdline.rubout();
- return;
- }
-
- /*
- * Rub out entire command instead of
- * rubbing out nothing.
- */
- break;
-
- case '\t': /* autocomplete file name */
- if (modifier_enabled)
- break;
-
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- new_chars = filename_complete(strings[0],
- escape_char == '{' ? '\0' : escape_char);
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
- return;
- }
-
- StateExpectString::process_edit_cmd(key);
-}
-
-void
-StateExpectDir::process_edit_cmd(gchar key)
-{
- gchar *new_chars;
-
- switch (key) {
- case '\t': /* autocomplete directory */
- if (modifier_enabled)
- break;
-
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- new_chars = filename_complete(strings[0], '\0',
- G_FILE_TEST_IS_DIR);
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
- return;
- }
-
- StateExpectFile::process_edit_cmd(key);
-}
-
-void
-StateExpectQReg::process_edit_cmd(gchar key)
-{
- gchar *new_chars;
-
- switch (key) {
- case '\t': /* autocomplete Q-Register name */
- if (modifier_enabled)
- break;
-
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- new_chars = machine.auto_complete();
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
- return;
- }
-
- State::process_edit_cmd(key);
-}
-
-void
-StateExecuteCommand::process_edit_cmd(gchar key)
-{
- gchar *new_chars;
-
- switch (key) {
- case '\t': /* autocomplete symbol or file name */
- if (modifier_enabled)
- break;
-
- /*
- * In the EC command, <TAB> completes files just like ^T
- * TODO: Implement shell-command completion by iterating
- * executables in $PATH
- */
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- new_chars = filename_complete(last_occurrence(strings[0]));
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
- return;
- }
-
- StateExpectString::process_edit_cmd(key);
-}
-
-void
-StateScintilla_symbols::process_edit_cmd(gchar key)
-{
- switch (key) {
- case '\t': { /* autocomplete Scintilla symbol */
- if (modifier_enabled)
- break;
-
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- const gchar *symbol = last_occurrence(strings[0], ",");
- SymbolList &list = symbol == strings[0]
- ? Symbols::scintilla
- : Symbols::scilexer;
- gchar *new_chars = symbol_complete(list, symbol, ',');
-
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
- return;
- }
- }
-
- StateExpectString::process_edit_cmd(key);
-}
-
-void
-StateGotoCmd::process_edit_cmd(gchar key)
-{
- switch (key) {
- case '\t': { /* autocomplete goto label */
- if (modifier_enabled)
- break;
-
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- const gchar *label = last_occurrence(strings[0], ",");
- gchar *new_chars = Goto::table->auto_complete(label);
-
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
- return;
- }
- }
-
- StateExpectString::process_edit_cmd(key);
-}
-
-void
-StateGetHelp::process_edit_cmd(gchar key)
-{
- switch (key) {
- case '\t': { /* autocomplete help term */
- if (modifier_enabled)
- break;
-
- if (interface.popup_is_shown()) {
- /* cycle through popup pages */
- interface.popup_show();
- return;
- }
-
- gchar complete = escape_char == '{' ? '\0' : escape_char;
- gchar *new_chars = help_index.auto_complete(strings[0], complete);
-
- if (new_chars)
- cmdline.insert(new_chars);
- g_free(new_chars);
- return;
- }
- }
-
- StateExpectString::process_edit_cmd(key);
-}
-
-/*
- * Command states
- */
-
-/*$ *q
- * *q -- Save last command line
- *
- * Only at the very beginning of a command-line, this command
- * may be used to save the last command line as a string in
- * Q-Register <q>.
- */
-State *
-StateSaveCmdline::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::start);
- reg->undo_set_string();
- reg->set_string(last_cmdline.str, last_cmdline.len);
- return &States::start;
-}
-
-/*
- * Auxiliary functions
- */
-
-static const gchar *
-last_occurrence(const gchar *str, const gchar *chars)
-{
- if (!str)
- return NULL;
-
- while (*chars) {
- const gchar *p = strrchr(str, *chars++);
- if (p)
- str = p+1;
- }
-
- return str;
-}
-
-static inline gboolean
-filename_is_dir(const gchar *filename)
-{
- gchar c;
-
- if (!*filename)
- return false;
-
- c = filename[strlen(filename)-1];
- return G_IS_DIR_SEPARATOR(c);
-}
-
-} /* namespace SciTECO */
diff --git a/src/cmdline.h b/src/cmdline.h
index 66e1829..0c61dea 100644
--- a/src/cmdline.h
+++ b/src/cmdline.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,103 +14,84 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __CMDLINE_H
-#define __CMDLINE_H
+#pragma once
#include <glib.h>
-#include "memory.h"
+#include "sciteco.h"
+#include "string-utils.h"
#include "parser.h"
-#include "qregisters.h"
#include "undo.h"
-namespace SciTECO {
+typedef struct {
+ /**
+ * State machine used for interactive mode (commandline macro).
+ * It is initialized on-demand in main.c.
+ * This is a global variable instead of being passed down the call stack
+ * since some process_edit_cmd_cb will be for nested state machines
+ * but we must "step" only the toplevel state machine.
+ */
+ teco_machine_main_t machine;
-/*
- * NOTE: Some of the members (esp. insert() and rubout())
- * have to be public, so that State::process_edit_cmd()
- * implementations can access it.
- * Otherwise, we'd have to list all implementations as
- * friend methods, which is inelegant.
- */
-extern class Cmdline : public Object {
-public:
/**
- * String containing the current command line.
- * It is not null-terminated and contains the effective
- * command-line up to cmdline_len followed by the recently rubbed-out
- * command-line of length cmdline_rubout_len.
+ * String containing the current command line
+ * (both effective and rubbed out).
*/
- gchar *str;
- /** Effective command line length */
- gsize len;
- /** Length of the rubbed out command line */
- gsize rubout_len;
+ 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).
+ */
+ gsize effective_len;
+
/** Program counter within the command-line macro */
guint pc;
- Cmdline() : str(NULL), len(0), rubout_len(0), pc(0) {}
- inline
- ~Cmdline()
- {
- g_free(str);
- }
-
- inline gchar
- operator [](guint i) const
- {
- return str[i];
- }
-
- void keypress(gchar key);
- inline void
- keypress(const gchar *keys)
- {
- while (*keys)
- keypress(*keys++);
- }
-
- void fnmacro(const gchar *name);
-
- void replace(void) G_GNUC_NORETURN;
-
- inline void
- rubout(void)
- {
- if (len) {
- undo.pop(--len);
- rubout_len++;
- }
- }
-
- void insert(const gchar *src = NULL);
- inline void
- insert(gchar key)
- {
- gchar src[] = {key, '\0'};
- insert(src);
- }
-} cmdline;
-
-extern bool quit_requested;
+ /**
+ * Specifies whether the immediate editing modifier
+ * is enabled/disabled.
+ * It can be toggled with the ^G immediate editing command
+ * and influences the undo/redo direction and function of the
+ * TAB key.
+ */
+ gboolean modifier_enabled;
+} teco_cmdline_t;
-/*
- * Command states
- */
+extern teco_cmdline_t teco_cmdline;
+
+gboolean teco_cmdline_insert(const gchar *data, gsize len, GError **error);
-class StateSaveCmdline : public StateExpectQReg {
-public:
- StateSaveCmdline() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
+static inline gboolean
+teco_cmdline_rubin(GError **error)
+{
+ return teco_cmdline_insert(NULL, 0, error);
+}
-private:
- State *got_register(QRegister *reg);
-};
+gboolean teco_cmdline_keypress_c(gchar key, GError **error);
-namespace States {
- extern StateSaveCmdline save_cmdline;
+static inline gboolean
+teco_cmdline_keypress(const gchar *str, gsize len, GError **error)
+{
+ for (guint i = 0; i < len; i++)
+ if (!teco_cmdline_keypress_c(str[i], error))
+ return FALSE;
+ return TRUE;
}
-} /* namespace SciTECO */
+gboolean teco_cmdline_fnmacro(const gchar *name, GError **error);
+
+static inline void
+teco_cmdline_rubout(void)
+{
+ if (teco_cmdline.effective_len)
+ teco_undo_pop(--teco_cmdline.effective_len);
+}
+
+extern gboolean teco_quit_requested;
+
+/*
+ * Command states
+ */
-#endif
+TECO_DECLARE_STATE(teco_state_save_cmdline);
diff --git a/src/core-commands.c b/src/core-commands.c
new file mode 100644
index 0000000..4c5d176
--- /dev/null
+++ b/src/core-commands.c
@@ -0,0 +1,2510 @@
+/*
+ * 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 <string.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "file-utils.h"
+#include "interface.h"
+#include "undo.h"
+#include "expressions.h"
+#include "ring.h"
+#include "parser.h"
+#include "scintilla.h"
+#include "search.h"
+#include "spawn.h"
+#include "glob.h"
+#include "help.h"
+#include "cmdline.h"
+#include "error.h"
+#include "memory.h"
+#include "eol.h"
+#include "qreg.h"
+#include "qreg-commands.h"
+#include "goto-commands.h"
+#include "core-commands.h"
+
+static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gchar chr, GError **error);
+
+/*
+ * NOTE: This needs some extra code in teco_state_start_input().
+ */
+static void
+teco_state_start_mul(teco_machine_main_t *ctx, GError **error)
+{
+ teco_expressions_push_calc(TECO_OP_MUL, error);
+}
+
+static void
+teco_state_start_div(teco_machine_main_t *ctx, GError **error)
+{
+ teco_expressions_push_calc(TECO_OP_DIV, error);
+}
+
+static void
+teco_state_start_plus(teco_machine_main_t *ctx, GError **error)
+{
+ teco_expressions_push_calc(TECO_OP_ADD, error);
+}
+
+static void
+teco_state_start_minus(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_args())
+ teco_set_num_sign(-teco_num_sign);
+ else
+ teco_expressions_push_calc(TECO_OP_SUB, error);
+}
+
+static void
+teco_state_start_and(teco_machine_main_t *ctx, GError **error)
+{
+ teco_expressions_push_calc(TECO_OP_AND, error);
+}
+
+static void
+teco_state_start_or(teco_machine_main_t *ctx, GError **error)
+{
+ teco_expressions_push_calc(TECO_OP_OR, error);
+}
+
+static void
+teco_state_start_brace_open(teco_machine_main_t *ctx, GError **error)
+{
+ if (teco_num_sign < 0) {
+ teco_set_num_sign(1);
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+ teco_expressions_push(-1);
+ if (!teco_expressions_push_calc(TECO_OP_MUL, error))
+ return;
+ }
+ teco_expressions_brace_open();
+}
+
+static void
+teco_state_start_brace_close(teco_machine_main_t *ctx, GError **error)
+{
+ teco_expressions_brace_close(error);
+}
+
+static void
+teco_state_start_comma(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+ teco_expressions_push_op(TECO_OP_NEW);
+}
+
+/*$ "." dot
+ * \&. -> dot -- Return buffer position
+ *
+ * \(lq.\(rq pushes onto the stack, the current
+ * position (also called <dot>) of the currently
+ * selected buffer or Q-Register.
+ */
+static void
+teco_state_start_dot(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+ teco_expressions_push(teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0));
+}
+
+/*$ Z size
+ * Z -> size -- Return buffer size
+ *
+ * Pushes onto the stack, the size of the currently selected
+ * buffer or Q-Register.
+ * This is value is also the buffer position of the document's
+ * end.
+ */
+static void
+teco_state_start_zed(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+ teco_expressions_push(teco_interface_ssm(SCI_GETLENGTH, 0, 0));
+}
+
+/*$ H
+ * H -> 0,Z -- Return range for entire buffer
+ *
+ * Pushes onto the stack the integer 0 (position of buffer
+ * beginning) and the current buffer's size.
+ * It is thus often equivalent to the expression
+ * \(lq0,Z\(rq, or more generally \(lq(0,Z)\(rq.
+ */
+static void
+teco_state_start_range(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+ teco_expressions_push(0);
+ teco_expressions_push(teco_interface_ssm(SCI_GETLENGTH, 0, 0));
+}
+
+/*$ "\\"
+ * n\\ -- Insert or read ASCII numbers
+ * \\ -> n
+ *
+ * Backslash pops a value from the stack, formats it
+ * according to the current radix and inserts it in the
+ * current buffer or Q-Register at dot.
+ * If <n> is omitted (empty stack), it does the reverse -
+ * it reads from the current buffer position an integer
+ * in the current radix and pushes it onto the stack.
+ * Dot is not changed when reading integers.
+ *
+ * In other words, the command serializes or deserializes
+ * integers as ASCII characters.
+ */
+static void
+teco_state_start_backslash(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, 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);
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ teco_interface_ssm(SCI_ADDTEXT, strlen(str), (sptr_t)str);
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+ } else {
+ uptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ gchar c = (gchar)teco_interface_ssm(SCI_GETCHARAT, pos, 0);
+ teco_int_t v = 0;
+ gint sign = 1;
+
+ if (c == '-') {
+ pos++;
+ sign = -1;
+ }
+
+ for (;;) {
+ c = teco_ascii_toupper((gchar)teco_interface_ssm(SCI_GETCHARAT, pos, 0));
+ if (c >= '0' && c <= '0' + MIN(teco_radix, 10) - 1)
+ v = (v*teco_radix) + (c - '0');
+ else if (c >= 'A' &&
+ c <= 'A' + MIN(teco_radix - 10, 26) - 1)
+ v = (v*teco_radix) + 10 + (c - 'A');
+ else
+ break;
+
+ pos++;
+ }
+
+ teco_expressions_push(sign * v);
+ }
+}
+
+/*
+ * NOTE: This needs some extra code in teco_state_start_input().
+ */
+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))
+ return;
+ lctx.pass_through = teco_machine_main_eval_colon(ctx);
+
+ if (lctx.counter) {
+ /*
+ * Non-colon modified, we add implicit
+ * braces, so loop body won't see parameters.
+ * Colon modified, loop starts can be used
+ * to process stack elements which is symmetric
+ * to ":>".
+ */
+ if (!lctx.pass_through)
+ teco_expressions_brace_open();
+
+ lctx.pc = ctx->macro_pc;
+ g_array_append_val(teco_loop_stack, lctx);
+ undo__remove_index__teco_loop_stack(teco_loop_stack->len-1);
+ } else {
+ /* skip to end of loop */
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_PARSE_ONLY_LOOP;
+ }
+}
+
+/*
+ * NOTE: This needs some extra code in teco_state_start_input().
+ */
+static void
+teco_state_start_loop_close(teco_machine_main_t *ctx, GError **error)
+{
+ if (teco_loop_stack->len <= ctx->loop_stack_fp) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Loop end without corresponding "
+ "loop start command");
+ return;
+ }
+
+ teco_loop_context_t *lctx = &g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1);
+ gboolean colon_modified = teco_machine_main_eval_colon(ctx);
+
+ /*
+ * Colon-modified loop ends can be used to
+ * aggregate values on the stack.
+ * A non-colon modified ">" behaves like ":>"
+ * for pass-through loop starts, though.
+ */
+ if (!lctx->pass_through) {
+ if (colon_modified) {
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+ teco_expressions_push_op(TECO_OP_NEW);
+ } else if (!teco_expressions_discard_args(error)) {
+ return;
+ }
+ }
+
+ if (lctx->counter == 1) {
+ /* this was the last loop iteration */
+ if (!lctx->pass_through &&
+ !teco_expressions_brace_close(error))
+ return;
+ undo__insert_val__teco_loop_stack(teco_loop_stack->len-1, *lctx);
+ g_array_remove_index(teco_loop_stack, teco_loop_stack->len-1);
+ } else {
+ /*
+ * Repeat loop:
+ * NOTE: One undo token per iteration could
+ * be avoided by saving the original counter
+ * in the teco_loop_context_t.
+ * We do however optimize the case of infinite loops
+ * because the loop counter does not have to be
+ * updated.
+ */
+ ctx->macro_pc = lctx->pc;
+ if (lctx->counter >= 0) {
+ if (ctx->parent.must_undo)
+ teco_undo_int(lctx->counter);
+ lctx->counter--;
+ }
+ }
+}
+
+/*$ ";" break
+ * [bool]; -- Conditionally break from loop
+ * [bool]:;
+ *
+ * Breaks from the current inner-most loop if <bool>
+ * signifies failure (non-negative value).
+ * If colon-modified, breaks from the loop if <bool>
+ * signifies success (negative value).
+ *
+ * If the condition code cannot be popped from the stack,
+ * the global search register's condition integer
+ * is implied instead.
+ * This way, you may break on search success/failures
+ * without colon-modifying the search command (or at a
+ * later point).
+ *
+ * Executing \(lq;\(rq outside of iterations in the current
+ * macro invocation level yields an error. It is thus not
+ * possible to let a macro break a caller's loop.
+ */
+static void
+teco_state_start_break(teco_machine_main_t *ctx, GError **error)
+{
+ if (teco_loop_stack->len <= ctx->loop_stack_fp) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "<;> only allowed in iterations");
+ return;
+ }
+
+ teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(reg != NULL);
+ teco_int_t v;
+ if (!reg->vtable->get_integer(reg, &v, error))
+ return;
+
+ teco_bool_t rc;
+ if (!teco_expressions_pop_num_calc(&rc, v, error))
+ return;
+ if (teco_machine_main_eval_colon(ctx))
+ rc = ~rc;
+
+ if (teco_is_success(rc))
+ return;
+
+ teco_loop_context_t lctx = g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1);
+ g_array_remove_index(teco_loop_stack, teco_loop_stack->len-1);
+
+ if (!teco_expressions_discard_args(error))
+ return;
+ if (!lctx.pass_through &&
+ !teco_expressions_brace_close(error))
+ return;
+
+ undo__insert_val__teco_loop_stack(teco_loop_stack->len, lctx);
+
+ /* skip to end of loop */
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_PARSE_ONLY_LOOP;
+}
+
+/*$ "{" "}"
+ * { -- Edit command line
+ * }
+ *
+ * The opening curly bracket is a powerful command
+ * to edit command lines but has very simple semantics.
+ * It copies the current commandline into the global
+ * command line editing register (called Escape, i.e.
+ * ASCII 27) and edits this register.
+ * The curly bracket itself is not copied.
+ *
+ * The command line may then be edited using any
+ * \*(ST command or construct.
+ * You may switch between the command line editing
+ * register and other registers or buffers.
+ * The user will then usually reapply (called update)
+ * the current command-line.
+ *
+ * The closing curly bracket will update the current
+ * command-line with the contents of the global command
+ * line editing register.
+ * To do so it merely rubs-out the current command-line
+ * up to the first changed character and inserts
+ * all characters following from the updated command
+ * line into the command stream.
+ * To prevent the undesired rubout of the entire
+ * command-line, the replacement command ("}") is only
+ * allowed when the replacement register currently edited
+ * since it will otherwise be usually empty.
+ *
+ * .B Note:
+ * - Command line editing only works on command lines,
+ * but not arbitrary macros.
+ * It is therefore not available in batch mode and
+ * will yield an error if used.
+ * - Command line editing commands may be safely used
+ * from macro invocations.
+ * Such macros are called command line editing macros.
+ * - A command line update from a macro invocation will
+ * always yield to the outer-most macro level (i.e.
+ * the command line macro).
+ * Code following the update command in the macro
+ * will thus never be executed.
+ * - As a safe-guard against command line trashing due
+ * to erroneous changes at the beginning of command
+ * lines, a backup mechanism is implemented:
+ * If the updated command line yields an error at
+ * any command during the update, the original
+ * command line will be restored with an algorithm
+ * similar to command line updating and the update
+ * command will fail instead.
+ * That way it behaves like any other command that
+ * yields an error:
+ * The character resulting in the update is rejected
+ * by the command line input subsystem.
+ * - In the rare case that an aforementioned command line
+ * backup fails, the commands following the erroneous
+ * character will not be inserted again (will be lost).
+ */
+static void
+teco_state_start_cmdline_push(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_undo_enabled) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Command-line editing only possible in "
+ "interactive mode");
+ return;
+ }
+
+ if (!teco_current_doc_undo_edit(error) ||
+ !teco_qreg_table_edit_name(&teco_qreg_table_globals, "\e", 1, error))
+ return;
+
+ 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_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+
+ /* must always support undo on global register */
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+}
+
+static void
+teco_state_start_cmdline_pop(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_undo_enabled) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Command-line editing only possible in "
+ "interactive mode");
+ return;
+ }
+ if (teco_qreg_current != teco_qreg_table_find(&teco_qreg_table_globals, "\e", 1)) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Command-line replacement only allowed when "
+ "editing the replacement register");
+ return;
+ }
+
+ /* replace cmdline in the outer macro environment */
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CMDLINE, "");
+}
+
+/*$ J jump
+ * [position]J -- Go to position in buffer
+ * [position]:J -> Success|Failure
+ *
+ * Sets dot to <position>.
+ * If <position> is omitted, 0 is implied and \(lqJ\(rq will
+ * go to the beginning of the buffer.
+ *
+ * If <position> is outside the range of the buffer, the
+ * command yields an error.
+ * If colon-modified, the command will instead return a
+ * condition boolean signalling whether the position could
+ * be changed or not.
+ */
+static void
+teco_state_start_jump(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+
+ if (!teco_expressions_pop_num_calc(&v, 0, error))
+ return;
+
+ if (teco_validate_pos(v)) {
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_GOTOPOS,
+ teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0);
+ teco_interface_ssm(SCI_GOTOPOS, v, 0);
+
+ if (teco_machine_main_eval_colon(ctx))
+ teco_expressions_push(TECO_SUCCESS);
+ } else if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(TECO_FAILURE);
+ } else {
+ teco_error_move_set(error, "J");
+ return;
+ }
+}
+
+static teco_bool_t
+teco_move_chars(teco_int_t n)
+{
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ if (!teco_validate_pos(pos + n))
+ return TECO_FAILURE;
+
+ teco_interface_ssm(SCI_GOTOPOS, pos + n, 0);
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0);
+
+ return TECO_SUCCESS;
+}
+
+/*$ C move
+ * [n]C -- Move dot <n> characters
+ * -C
+ * [n]:C -> Success|Failure
+ *
+ * Adds <n> to dot. 1 or -1 is implied if <n> is omitted.
+ * Fails if <n> would move dot off-page.
+ * The colon modifier results in a success-boolean being
+ * returned instead.
+ */
+static void
+teco_state_start_move(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+
+ if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
+ return;
+
+ teco_bool_t rc = teco_move_chars(v);
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(rc);
+ } else if (teco_is_failure(rc)) {
+ teco_error_move_set(error, "C");
+ return;
+ }
+}
+
+/*$ R reverse
+ * [n]R -- Move dot <n> characters backwards
+ * -R
+ * [n]:R -> Success|Failure
+ *
+ * Subtracts <n> from dot.
+ * It is equivalent to \(lq-nC\(rq.
+ */
+static void
+teco_state_start_reverse(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+
+ if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
+ return;
+
+ teco_bool_t rc = teco_move_chars(-v);
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(rc);
+ } else if (teco_is_failure(rc)) {
+ teco_error_move_set(error, "R");
+ return;
+ }
+}
+
+static teco_bool_t
+teco_move_lines(teco_int_t n)
+{
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ sptr_t line = teco_interface_ssm(SCI_LINEFROMPOSITION, pos, 0) + n;
+
+ if (!teco_validate_line(line))
+ return TECO_FAILURE;
+
+ teco_interface_ssm(SCI_GOTOLINE, line, 0);
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0);
+
+ return TECO_SUCCESS;
+}
+
+/*$ L line
+ * [n]L -- Move dot <n> lines forwards
+ * -L
+ * [n]:L -> Success|Failure
+ *
+ * Move dot to the beginning of the line specified
+ * relatively to the current line.
+ * Therefore a value of 0 for <n> goes to the
+ * beginning of the current line, 1 will go to the
+ * next line, -1 to the previous line etc.
+ * If <n> is omitted, 1 or -1 is implied depending on
+ * the sign prefix.
+ *
+ * If <n> would move dot off-page, the command yields
+ * an error.
+ * The colon-modifer results in a condition boolean
+ * being returned instead.
+ */
+static void
+teco_state_start_line(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+
+ if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
+ return;
+
+ teco_bool_t rc = teco_move_lines(v);
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(rc);
+ } else if (teco_is_failure(rc)) {
+ teco_error_move_set(error, "L");
+ return;
+ }
+}
+
+/*$ B backwards
+ * [n]B -- Move dot <n> lines backwards
+ * -B
+ * [n]:B -> Success|Failure
+ *
+ * Move dot to the beginning of the line <n>
+ * lines before the current one.
+ * It is equivalent to \(lq-nL\(rq.
+ */
+static void
+teco_state_start_back(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+
+ if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
+ return;
+
+ teco_bool_t rc = teco_move_lines(-v);
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(rc);
+ } else if (teco_is_failure(rc)) {
+ teco_error_move_set(error, "B");
+ return;
+ }
+}
+
+/*$ W word
+ * [n]W -- Move dot by words
+ * -W
+ * [n]:W -> Success|Failure
+ *
+ * Move dot <n> words forward.
+ * - If <n> is positive, dot is positioned at the beginning
+ * of the word <n> words after the current one.
+ * - If <n> is negative, dot is positioned at the end
+ * of the word <n> words before the current one.
+ * - If <n> is zero, dot is not moved.
+ *
+ * \(lqW\(rq uses Scintilla's definition of a word as
+ * configurable using the
+ * .B SCI_SETWORDCHARS
+ * message.
+ *
+ * Otherwise, the command's behaviour is analogous to
+ * the \(lqC\(rq command.
+ */
+static void
+teco_state_start_word(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+ if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
+ return;
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ /*
+ * FIXME: would be nice to do this with constant amount of
+ * editor messages. E.g. by using custom algorithm accessing
+ * the internal document buffer.
+ */
+ unsigned int msg = SCI_WORDRIGHTEND;
+ if (v < 0) {
+ v *= -1;
+ msg = SCI_WORDLEFTEND;
+ }
+ while (v--) {
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ teco_interface_ssm(msg, 0, 0);
+ if (pos == teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0))
+ break;
+ }
+ if (v < 0) {
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0);
+ if (teco_machine_main_eval_colon(ctx))
+ teco_expressions_push(TECO_SUCCESS);
+ } else {
+ teco_interface_ssm(SCI_GOTOPOS, pos, 0);
+ if (!teco_machine_main_eval_colon(ctx)) {
+ teco_error_move_set(error, "W");
+ return;
+ }
+ teco_expressions_push(TECO_FAILURE);
+ }
+}
+
+static teco_bool_t
+teco_delete_words(teco_int_t n)
+{
+ sptr_t pos, size;
+
+ if (!n)
+ return TECO_SUCCESS;
+
+ pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ size = teco_interface_ssm(SCI_GETLENGTH, 0, 0);
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ /*
+ * FIXME: would be nice to do this with constant amount of
+ * editor messages. E.g. by using custom algorithm accessing
+ * the internal document buffer.
+ */
+ if (n > 0) {
+ while (n--) {
+ sptr_t size = teco_interface_ssm(SCI_GETLENGTH, 0, 0);
+ teco_interface_ssm(SCI_DELWORDRIGHTEND, 0, 0);
+ if (size == teco_interface_ssm(SCI_GETLENGTH, 0, 0))
+ break;
+ }
+ } else {
+ n *= -1;
+ while (n--) {
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ //teco_interface_ssm(SCI_DELWORDLEFTEND, 0, 0);
+ teco_interface_ssm(SCI_WORDLEFTEND, 0, 0);
+ if (pos == teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0))
+ break;
+ teco_interface_ssm(SCI_DELWORDRIGHTEND, 0, 0);
+ }
+ }
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+
+ if (n >= 0) {
+ if (size != teco_interface_ssm(SCI_GETLENGTH, 0, 0)) {
+ teco_interface_ssm(SCI_UNDO, 0, 0);
+ teco_interface_ssm(SCI_GOTOPOS, pos, 0);
+ }
+ return TECO_FAILURE;
+ }
+
+ undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0);
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+ teco_ring_dirtify();
+
+ return TECO_SUCCESS;
+}
+
+/*$ V
+ * [n]V -- Delete words forward
+ * -V
+ * [n]:V -> Success|Failure
+ *
+ * Deletes the next <n> words until the end of the
+ * n'th word after the current one.
+ * If <n> is negative, deletes up to end of the
+ * n'th word before the current one.
+ * If <n> is omitted, 1 or -1 is implied depending on the
+ * sign prefix.
+ *
+ * It uses Scintilla's definition of a word as configurable
+ * using the
+ * .B SCI_SETWORDCHARS
+ * message.
+ *
+ * If the words to delete extend beyond the range of the
+ * buffer, the command yields an error.
+ * If colon-modified it instead returns a condition code.
+ */
+static void
+teco_state_start_delete_words(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+
+ if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
+ return;
+
+ teco_bool_t rc = teco_delete_words(v);
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(rc);
+ } else if (teco_is_failure(rc)) {
+ teco_error_words_set(error, "V");
+ return;
+ }
+}
+
+/*$ Y
+ * [n]Y -- Delete word backwards
+ * -Y
+ * [n]:Y -> Success|Failure
+ *
+ * Delete <n> words backward.
+ * <n>Y is equivalent to \(lq-nV\(rq.
+ */
+static void
+teco_state_start_delete_words_back(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+
+ if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
+ return;
+
+ teco_bool_t rc = teco_delete_words(-v);
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(rc);
+ } else if (teco_is_failure(rc)) {
+ teco_error_words_set(error, "Y");
+ return;
+ }
+}
+
+/*$ "=" 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);
+}
+
+static gboolean
+teco_state_start_kill(teco_machine_main_t *ctx, const gchar *cmd, gboolean by_lines, GError **error)
+{
+ teco_bool_t rc;
+ teco_int_t from, len;
+
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+
+ if (teco_expressions_args() <= 1) {
+ from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ if (by_lines) {
+ teco_int_t line;
+ if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error))
+ return FALSE;
+ line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0);
+ len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from;
+ rc = teco_bool(teco_validate_line(line));
+ } else {
+ if (!teco_expressions_pop_num_calc(&len, teco_num_sign, error))
+ return FALSE;
+ rc = teco_bool(teco_validate_pos(from + len));
+ }
+ if (len < 0) {
+ len *= -1;
+ from -= len;
+ }
+ } else {
+ teco_int_t to = teco_expressions_pop_num(0);
+ from = teco_expressions_pop_num(0);
+ len = to - from;
+ rc = teco_bool(len >= 0 && teco_validate_pos(from) &&
+ teco_validate_pos(to));
+ }
+
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(rc);
+ } else if (teco_is_failure(rc)) {
+ teco_error_range_set(error, cmd);
+ return FALSE;
+ }
+
+ if (len == 0 || teco_is_failure(rc))
+ return TRUE;
+
+ if (teco_current_doc_must_undo()) {
+ sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0);
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+ }
+
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ teco_interface_ssm(SCI_DELETERANGE, from, len);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ return TRUE;
+}
+
+/*$ K kill
+ * [n]K -- Kill lines
+ * -K
+ * from,to K
+ * [n]:K -> Success|Failure
+ * from,to:K -> Success|Failure
+ *
+ * Deletes characters up to the beginning of the
+ * line <n> lines after or before the current one.
+ * If <n> is 0, \(lqK\(rq will delete up to the beginning
+ * of the current line.
+ * If <n> is omitted, the sign prefix will be implied.
+ * So to delete the entire line regardless of the position
+ * in it, one can use \(lq0KK\(rq.
+ *
+ * If the deletion is beyond the buffer's range, the command
+ * will yield an error unless it has been colon-modified
+ * so it returns a condition code.
+ *
+ * If two arguments <from> and <to> are available, the
+ * command is synonymous to <from>,<to>D.
+ */
+static void
+teco_state_start_kill_lines(teco_machine_main_t *ctx, GError **error)
+{
+ teco_state_start_kill(ctx, "K", TRUE, error);
+}
+
+/*$ D delete
+ * [n]D -- Delete characters
+ * -D
+ * from,to D
+ * [n]:D -> Success|Failure
+ * from,to:D -> Success|Failure
+ *
+ * If <n> is positive, the next <n> characters (up to and
+ * character .+<n>) are deleted.
+ * If <n> is negative, the previous <n> characters are
+ * deleted.
+ * If <n> is omitted, the sign prefix will be implied.
+ *
+ * If two arguments can be popped from the stack, the
+ * command will delete the characters with absolute
+ * position <from> up to <to> from the current buffer.
+ *
+ * If the character range to delete is beyond the buffer's
+ * range, the command will yield an error unless it has
+ * been colon-modified so it returns a condition code
+ * instead.
+ */
+static void
+teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error)
+{
+ teco_state_start_kill(ctx, "D", FALSE, error);
+}
+
+/*$ A
+ * [n]A -> code -- Get character code from buffer
+ * -A -> code
+ *
+ * Returns the character <code> of the character
+ * <n> relative to dot from the buffer.
+ * This can be an ASCII <code> or Unicode codepoint
+ * depending on Scintilla's encoding of the current
+ * buffer.
+ * - If <n> is 0, return the <code> of the character
+ * pointed to by dot.
+ * - If <n> is 1, return the <code> of the character
+ * immediately after dot.
+ * - If <n> is -1, return the <code> of the character
+ * immediately preceding dot, ecetera.
+ * - 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.
+ */
+/** @todo does Scintilla really return code points??? */
+static void
+teco_state_start_get(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+ if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error))
+ return;
+ v += teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ /*
+ * NOTE: We cannot use teco_validate_pos() here since
+ * the end of the buffer is not a valid position for <A>.
+ */
+ if (v < 0 || v >= teco_interface_ssm(SCI_GETLENGTH, 0, 0)) {
+ teco_error_range_set(error, "A");
+ return;
+ }
+ teco_expressions_push(teco_interface_ssm(SCI_GETCHARAT, v, 0));
+}
+
+static teco_state_t *
+teco_state_start_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ static teco_machine_main_transition_t transitions[] = {
+ /*
+ * Simple transitions
+ */
+ ['$'] = {&teco_state_escape},
+ ['!'] = {&teco_state_label},
+ ['O'] = {&teco_state_goto},
+ ['^'] = {&teco_state_control},
+ ['F'] = {&teco_state_fcommand},
+ ['"'] = {&teco_state_condcommand},
+ ['E'] = {&teco_state_ecommand},
+ ['I'] = {&teco_state_insert_building},
+ ['?'] = {&teco_state_help},
+ ['S'] = {&teco_state_search},
+ ['N'] = {&teco_state_search_all},
+
+ ['['] = {&teco_state_pushqreg},
+ [']'] = {&teco_state_popqreg},
+ ['G'] = {&teco_state_getqregstring},
+ ['Q'] = {&teco_state_queryqreg},
+ ['U'] = {&teco_state_setqreginteger},
+ ['%'] = {&teco_state_increaseqreg},
+ ['M'] = {&teco_state_macro},
+ ['X'] = {&teco_state_copytoqreg},
+
+ /*
+ * Arithmetics
+ */
+ ['*'] = {&teco_state_start, teco_state_start_mul},
+ ['/'] = {&teco_state_start, teco_state_start_div},
+ ['+'] = {&teco_state_start, teco_state_start_plus},
+ ['-'] = {&teco_state_start, teco_state_start_minus},
+ ['&'] = {&teco_state_start, teco_state_start_and},
+ ['#'] = {&teco_state_start, teco_state_start_or},
+ ['('] = {&teco_state_start, teco_state_start_brace_open},
+ [')'] = {&teco_state_start, teco_state_start_brace_close},
+ [','] = {&teco_state_start, teco_state_start_comma},
+
+ ['.'] = {&teco_state_start, teco_state_start_dot},
+ ['Z'] = {&teco_state_start, teco_state_start_zed},
+ ['H'] = {&teco_state_start, teco_state_start_range},
+ ['\\'] = {&teco_state_start, teco_state_start_backslash},
+
+ /*
+ * Control Structures (loops)
+ */
+ ['<'] = {&teco_state_start, teco_state_start_loop_open},
+ ['>'] = {&teco_state_start, teco_state_start_loop_close},
+ [';'] = {&teco_state_start, teco_state_start_break},
+
+ /*
+ * Command-line Editing
+ */
+ ['{'] = {&teco_state_start, teco_state_start_cmdline_push},
+ ['}'] = {&teco_state_start, teco_state_start_cmdline_pop},
+
+ /*
+ * Commands
+ */
+ ['J'] = {&teco_state_start, teco_state_start_jump},
+ ['C'] = {&teco_state_start, teco_state_start_move},
+ ['R'] = {&teco_state_start, teco_state_start_reverse},
+ ['L'] = {&teco_state_start, teco_state_start_line},
+ ['B'] = {&teco_state_start, teco_state_start_back},
+ ['W'] = {&teco_state_start, teco_state_start_word},
+ ['V'] = {&teco_state_start, teco_state_start_delete_words},
+ ['Y'] = {&teco_state_start, teco_state_start_delete_words_back},
+ ['='] = {&teco_state_start, teco_state_start_print},
+ ['K'] = {&teco_state_start, teco_state_start_kill_lines},
+ ['D'] = {&teco_state_start, teco_state_start_delete_chars},
+ ['A'] = {&teco_state_start, teco_state_start_get}
+ };
+
+ switch (chr) {
+ /*
+ * No-ops:
+ * 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':
+ return &teco_state_start;
+
+ /*$ 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
+ *
+ * Integer constants in \*(ST may be thought of and are
+ * technically sequences of single-digit commands.
+ * These commands take one argument from the stack
+ * (0 is implied), multiply it with the current radix
+ * (2, 8, 10, 16, ...), add the digit's value and
+ * return the resultant integer.
+ *
+ * The command-like semantics of digits may be abused
+ * in macros, for instance to append digits to computed
+ * integers.
+ * It is not an error to append a digit greater than the
+ * current radix - this may be changed in the future.
+ */
+ case '0' ... '9':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ teco_expressions_add_digit(chr);
+ return &teco_state_start;
+
+ case '*':
+ /*
+ * Special save last commandline command
+ *
+ * FIXME: Maybe, there should be a special teco_state_t
+ * for beginnings of command-lines?
+ * It could also be used for a corresponding FNMACRO mask.
+ */
+ if (teco_cmdline.effective_len == 1 && teco_cmdline.str.data[0] == '*')
+ return &teco_state_save_cmdline;
+ break;
+
+ case '<':
+ if (ctx->mode != TECO_MODE_PARSE_ONLY_LOOP)
+ break;
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nest_level);
+ ctx->nest_level++;
+ return &teco_state_start;
+
+ case '>':
+ if (ctx->mode != TECO_MODE_PARSE_ONLY_LOOP)
+ break;
+ if (!ctx->nest_level) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_NORMAL;
+ } else {
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nest_level);
+ ctx->nest_level--;
+ }
+ return &teco_state_start;
+
+ /*
+ * Control Structures (conditionals)
+ */
+ case '|':
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ if (ctx->mode == TECO_MODE_PARSE_ONLY_COND && !ctx->nest_level)
+ ctx->mode = TECO_MODE_NORMAL;
+ else if (ctx->mode == TECO_MODE_NORMAL)
+ /* skip to end of conditional; skip ELSE-part */
+ ctx->mode = TECO_MODE_PARSE_ONLY_COND;
+ return &teco_state_start;
+
+ case '\'':
+ switch (ctx->mode) {
+ case TECO_MODE_PARSE_ONLY_COND:
+ case TECO_MODE_PARSE_ONLY_COND_FORCE:
+ if (!ctx->nest_level) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_NORMAL;
+ } else {
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nest_level);
+ ctx->nest_level--;
+ }
+ break;
+ default:
+ break;
+ }
+ return &teco_state_start;
+
+ /*
+ * Modifiers
+ */
+ case '@':
+ /*
+ * @ modifier has syntactic significance, so set it even
+ * in PARSE_ONLY* modes
+ */
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->modifier_at = TRUE;
+ return &teco_state_start;
+
+ case ':':
+ if (ctx->mode == TECO_MODE_NORMAL) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->modifier_colon = TRUE;
+ }
+ return &teco_state_start;
+
+ default:
+ /*
+ * <CTRL/x> commands implemented in teco_state_control
+ */
+ if (TECO_IS_CTL(chr))
+ return teco_state_control_input(ctx, TECO_CTL_ECHO(chr), error);
+ }
+
+ return teco_machine_main_transition_input(ctx, transitions, G_N_ELEMENTS(transitions),
+ teco_ascii_toupper(chr), error);
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_start,
+ .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */
+ .is_start = TRUE,
+ .fnmacro_mask = TECO_FNMACRO_MASK_START
+);
+
+/*$ F<
+ * F< -- Go to loop start or jump to beginning of macro
+ *
+ * Immediately jumps to the current loop's start.
+ * Also works from inside conditionals.
+ *
+ * Outside of loops \(em or in a macro without
+ * a loop \(em this jumps to the beginning of the macro.
+ */
+static void
+teco_state_fcommand_loop_start(teco_machine_main_t *ctx, GError **error)
+{
+ /* FIXME: what if in brackets? */
+ if (!teco_expressions_discard_args(error))
+ return;
+
+ ctx->macro_pc = teco_loop_stack->len > ctx->loop_stack_fp
+ ? g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1).pc : -1;
+}
+
+/*$ F> continue
+ * F> -- Go to loop end
+ * :F>
+ *
+ * Jumps to the current loop's end.
+ * If the loop has remaining iterations or runs indefinitely,
+ * the jump is performed immediately just as if \(lq>\(rq
+ * had been executed.
+ * If the loop has reached its last iteration, \*(ST will
+ * parse until the loop end command has been found and control
+ * resumes after the end of the loop.
+ *
+ * In interactive mode, if the loop is incomplete and must
+ * be exited, you can type in the loop's remaining commands
+ * without them being executed (but they are parsed).
+ *
+ * When colon-modified, \fB:F>\fP behaves like \fB:>\fP
+ * and allows numbers to be aggregated on the stack.
+ *
+ * Calling \fBF>\fP outside of a loop at the current
+ * macro invocation level will throw an error.
+ */
+static void
+teco_state_fcommand_loop_end(teco_machine_main_t *ctx, GError **error)
+{
+ guint old_len = teco_loop_stack->len;
+
+ /*
+ * NOTE: This is almost identical to the normal
+ * loop end since we don't really want to or need to
+ * parse till the end of the loop.
+ */
+ g_assert(error != NULL);
+ teco_state_start_loop_close(ctx, error);
+ if (*error)
+ return;
+
+ if (teco_loop_stack->len < old_len) {
+ /* skip to end of loop */
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_PARSE_ONLY_LOOP;
+ }
+}
+
+/*$ "F'"
+ * F\' -- Jump to end of conditional
+ */
+static void
+teco_state_fcommand_cond_end(teco_machine_main_t *ctx, GError **error)
+{
+ /* skip to end of conditional, also including any else-clause */
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_PARSE_ONLY_COND_FORCE;
+}
+
+/*$ F|
+ * F| -- Jump to else-part of conditional
+ *
+ * Jump to else-part of conditional or end of
+ * conditional (only if invoked from inside the
+ * condition's else-part).
+ */
+static void
+teco_state_fcommand_cond_else(teco_machine_main_t *ctx, GError **error)
+{
+ /* skip to ELSE-part or end of conditional */
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_PARSE_ONLY_COND;
+}
+
+static teco_state_t *
+teco_state_fcommand_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ static teco_machine_main_transition_t transitions[] = {
+ /*
+ * Simple transitions
+ */
+ ['K'] = {&teco_state_search_kill},
+ ['D'] = {&teco_state_search_delete},
+ ['S'] = {&teco_state_replace},
+ ['R'] = {&teco_state_replace_default},
+ ['G'] = {&teco_state_changedir},
+
+ /*
+ * Loop Flow Control
+ */
+ ['<'] = {&teco_state_start, teco_state_fcommand_loop_start},
+ ['>'] = {&teco_state_start, teco_state_fcommand_loop_end},
+
+ /*
+ * Conditional Flow Control
+ */
+ ['\''] = {&teco_state_start, teco_state_fcommand_cond_end},
+ ['|'] = {&teco_state_start, teco_state_fcommand_cond_else}
+ };
+
+ return teco_machine_main_transition_input(ctx, transitions, G_N_ELEMENTS(transitions),
+ teco_ascii_toupper(chr), error);
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_fcommand);
+
+static void
+teco_undo_change_dir_action(gchar **dir, gboolean run)
+{
+ /*
+ * Changing the directory on rub-out may fail.
+ * This is handled silently.
+ */
+ if (run)
+ g_chdir(*dir);
+ g_free(*dir);
+}
+
+void
+teco_undo_change_dir_to_current(void)
+{
+ gchar **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_change_dir_action,
+ sizeof(gchar *));
+ if (ctx)
+ *ctx = g_get_current_dir();
+}
+
+static teco_state_t *
+teco_state_changedir_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 *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);
+ teco_string_t home;
+ if (!qreg->vtable->get_string(qreg, &home.data, &home.len, error))
+ return NULL;
+
+ /*
+ * Null-characters must not occur in file names.
+ */
+ 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");
+ return NULL;
+ }
+
+ g_free(dir);
+ dir = home.data;
+ }
+
+ teco_undo_change_dir_to_current();
+
+ if (g_chdir(dir)) {
+ /* FIXME: Is errno usable on Windows here? */
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Cannot change working directory to \"%s\"", dir);
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ FG cd change-dir folder-go
+ * FG[directory]$ -- Change working directory
+ *
+ * Changes the process' current working directory
+ * to <directory> which affects all subsequent
+ * operations on relative file names like
+ * tab-completions.
+ * It is also inherited by external processes spawned
+ * via \fBEC\fP and \fBEG\fP.
+ *
+ * If <directory> is omitted, the working directory
+ * is changed to the current user's home directory
+ * as set by the \fBHOME\fP environment variable
+ * (i.e. its corresponding \(lq$HOME\(rq environment
+ * register).
+ * This variable is always initialized by \*(ST
+ * (see \fBsciteco\fP(1)).
+ * Therefore the expression \(lqFG\fB$\fP\(rq is
+ * exactly equivalent to both \(lqFG~\fB$\fP\(rq and
+ * \(lqFG^EQ[$HOME]\fB$\fP\(rq.
+ *
+ * The current working directory is also mapped to
+ * the special global Q-Register \(lq$\(rq (dollar sign)
+ * which may be used retrieve the current working directory.
+ *
+ * String-building characters are enabled on this
+ * command and directories can be tab-completed.
+ */
+TECO_DEFINE_STATE_EXPECTDIR(teco_state_changedir);
+
+static teco_state_t *
+teco_state_condcommand_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ teco_int_t value = 0;
+ gboolean result = TRUE;
+
+ switch (ctx->mode) {
+ case TECO_MODE_PARSE_ONLY_COND:
+ case TECO_MODE_PARSE_ONLY_COND_FORCE:
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nest_level);
+ ctx->nest_level++;
+ break;
+
+ case TECO_MODE_NORMAL:
+ if (!teco_expressions_eval(FALSE, error))
+ return NULL;
+
+ if (chr == '~')
+ /* don't pop value for ~ conditionals */
+ break;
+
+ if (!teco_expressions_args()) {
+ teco_error_argexpected_set(error, "\"");
+ return NULL;
+ }
+ if (!teco_expressions_pop_num_calc(&value, 0, error))
+ return NULL;
+ break;
+
+ default:
+ break;
+ }
+
+ switch (teco_ascii_toupper(chr)) {
+ case '~':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = !teco_expressions_args();
+ break;
+ case 'A':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = g_ascii_isalpha((gchar)value);
+ break;
+ case 'C':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = g_ascii_isalnum((gchar)value) ||
+ value == '.' || value == '$' || value == '_';
+ break;
+ case 'D':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = g_ascii_isdigit((gchar)value);
+ break;
+ case 'I':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = G_IS_DIR_SEPARATOR((gchar)value);
+ break;
+ case 'S':
+ case 'T':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = teco_is_success(value);
+ break;
+ case 'F':
+ case 'U':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = teco_is_failure(value);
+ break;
+ case 'E':
+ case '=':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = value == 0;
+ break;
+ case 'G':
+ case '>':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = value > 0;
+ break;
+ case 'L':
+ case '<':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = value < 0;
+ break;
+ case 'N':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = value != 0;
+ break;
+ case 'R':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = g_ascii_isalnum((gchar)value);
+ break;
+ case 'V':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = g_ascii_islower((gchar)value);
+ break;
+ case 'W':
+ if (ctx->mode == TECO_MODE_NORMAL)
+ result = g_ascii_isupper((gchar)value);
+ break;
+ default:
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid conditional type \"%c\"", chr);
+ return NULL;
+ }
+
+ if (!result) {
+ /* skip to ELSE-part or end of conditional */
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_PARSE_ONLY_COND;
+ }
+
+ return &teco_state_start;
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_condcommand);
+
+/*$ ^_ negate
+ * n^_ -> ~n -- Binary negation
+ *
+ * Binary negates (complements) <n> and returns
+ * the result.
+ * Binary complements are often used to negate
+ * \*(ST booleans.
+ */
+static void
+teco_state_control_negate(teco_machine_main_t *ctx, GError **error)
+{
+ teco_int_t v;
+
+ if (!teco_expressions_args()) {
+ teco_error_argexpected_set(error, "^_");
+ return;
+ }
+ if (!teco_expressions_pop_num_calc(&v, 0, error))
+ return;
+ teco_expressions_push(~v);
+}
+
+static void
+teco_state_control_pow(teco_machine_main_t *ctx, GError **error)
+{
+ teco_expressions_push_calc(TECO_OP_POW, error);
+}
+
+static void
+teco_state_control_mod(teco_machine_main_t *ctx, GError **error)
+{
+ teco_expressions_push_calc(TECO_OP_MOD, error);
+}
+
+static void
+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)
+ */
+static void
+teco_state_control_octal(teco_machine_main_t *ctx, GError **error)
+{
+ teco_set_radix(8);
+}
+
+/*$ ^D decimal
+ * ^D -- Set radix to 10 (decimal)
+ */
+static void
+teco_state_control_decimal(teco_machine_main_t *ctx, GError **error)
+{
+ teco_set_radix(10);
+}
+
+/*$ ^R radix
+ * radix^R -- Set and get radix
+ * ^R -> radix
+ *
+ * Set current radix to arbitrary value <radix>.
+ * If <radix> is omitted, the command instead
+ * returns the current radix.
+ */
+static void
+teco_state_control_radix(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+ if (!teco_expressions_args()) {
+ teco_expressions_push(teco_radix);
+ } else {
+ teco_int_t v;
+ if (!teco_expressions_pop_num_calc(&v, 0, error))
+ return;
+ teco_set_radix(v);
+ }
+}
+
+static teco_state_t *
+teco_state_control_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ static teco_machine_main_transition_t transitions[] = {
+ /*
+ * Simple transitions
+ */
+ ['I'] = {&teco_state_insert_indent},
+ ['U'] = {&teco_state_ctlucommand},
+ ['^'] = {&teco_state_ascii},
+ ['['] = {&teco_state_escape},
+
+ /*
+ * Additional numeric operations
+ */
+ ['_'] = {&teco_state_start, teco_state_control_negate},
+ ['*'] = {&teco_state_start, teco_state_control_pow},
+ ['/'] = {&teco_state_start, teco_state_control_mod},
+ ['#'] = {&teco_state_start, teco_state_control_xor},
+
+ /*
+ * Commands
+ */
+ ['C'] = {&teco_state_start, teco_state_control_exit},
+ ['O'] = {&teco_state_start, teco_state_control_octal},
+ ['D'] = {&teco_state_start, teco_state_control_decimal},
+ ['R'] = {&teco_state_start, teco_state_control_radix}
+ };
+
+ /*
+ * FIXME: Should we return a special syntax error in case of failure?
+ * Currently you get error messages like 'Syntax error "F"' for ^F.
+ * The easiest way around would be g_prefix_error(error, "Control command");
+ */
+ return teco_machine_main_transition_input(ctx, transitions, G_N_ELEMENTS(transitions),
+ teco_ascii_toupper(chr), error);
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_control);
+
+static teco_state_t *
+teco_state_ascii_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ if (ctx->mode == TECO_MODE_NORMAL)
+ teco_expressions_push(chr);
+
+ return &teco_state_start;
+}
+
+/*$ ^^ ^^c
+ * ^^c -> n -- Get ASCII code of character
+ *
+ * Returns the ASCII code of the character <c>
+ * that is part of the command.
+ * Can be used in place of integer constants for improved
+ * readability.
+ * For instance ^^A will return 65.
+ *
+ * Note that this command can be typed CTRL+Caret or
+ * Caret-Caret.
+ */
+TECO_DEFINE_STATE(teco_state_ascii);
+
+/*
+ * The Escape state is special, as it implements
+ * a kind of "lookahead" for the ^[ command (discard all
+ * arguments).
+ * It is not executed immediately as usual in SciTECO
+ * but only if not followed by an escape character.
+ * This is necessary since $$ is the macro return
+ * and command-line termination command and it must not
+ * discard arguments.
+ * Deferred execution of ^[ is possible since it does
+ * not have any visible side-effects - its effects can
+ * only be seen when executing the following command.
+ */
+static teco_state_t *
+teco_state_escape_input(teco_machine_main_t *ctx, gchar 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->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;
+ }
+
+ /*
+ * Alternatives: ^[, <CTRL/[>, <ESC>, $ (dollar)
+ */
+ /*$ ^[ $ escape discard
+ * $ -- Discard all arguments
+ * ^[
+ *
+ * Pops and discards all values from the stack that
+ * might otherwise be used as arguments to following
+ * commands.
+ * Therefore it stops popping on stack boundaries like
+ * they are introduced by arithmetic brackets or loops.
+ *
+ * Note that ^[ is usually typed using the Escape key.
+ * CTRL+[ however is possible as well and equivalent to
+ * Escape in every manner.
+ * The up-arrow notation however is processed like any
+ * ordinary command and only works at the begining of
+ * a command.
+ * Additionally, this command may be written as a single
+ * dollar character.
+ */
+ if (ctx->mode == TECO_MODE_NORMAL &&
+ !teco_expressions_discard_args(error))
+ return NULL;
+ return teco_state_start_input(ctx, chr, error);
+}
+
+static gboolean
+teco_state_escape_end_of_macro(teco_machine_t *ctx, GError **error)
+{
+ /*
+ * Due to the deferred nature of ^[,
+ * it is valid to end in the "escape" state.
+ */
+ return teco_expressions_discard_args(error);
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(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.
+ */
+ .is_start = TRUE,
+ .fnmacro_mask = TECO_FNMACRO_MASK_START
+);
+
+/*$ EF close
+ * [bool]EF -- Remove buffer from ring
+ * -EF
+ *
+ * Removes buffer from buffer ring, effectively
+ * closing it.
+ * If the buffer is dirty (modified), EF 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.
+ *
+ * It is noteworthy that EF 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.
+ */
+static void
+teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error)
+{
+ if (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;
+ }
+
+ 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)");
+ return;
+ }
+
+ teco_ring_close(error);
+}
+
+/*$ ED flags
+ * flags ED -- Set and get ED-flags
+ * [off,]on ED
+ * ED -> flags
+ *
+ * With arguments, the command will set the \fBED\fP flags.
+ * <flags> is a bitmap of flags to set.
+ * Specifying one argument to set the flags is a special
+ * case of specifying two arguments that allow to control
+ * which flags to enable/disable.
+ * <off> is a bitmap of flags to disable (set to 0 in ED
+ * flags) and <on> is a bitmap of flags that is ORed into
+ * the flags variable.
+ * If <off> is omitted, the value 0^_ is implied.
+ * In otherwords, all flags are turned off before turning
+ * on the <on> flags.
+ * Without any argument ED returns the current flags.
+ *
+ * Currently, the following flags are used by \*(ST:
+ * - 8: Enable/disable automatic folding of case-insensitive
+ * command characters during interactive key translation.
+ * The case of letter keys is inverted, so one or two
+ * character commands will typically be inserted upper-case,
+ * but you can still press Shift to insert lower-case letters.
+ * Case-insensitive Q-Register specifications are not
+ * case folded.
+ * This is thought to improve the readability of the command
+ * line macro.
+ * - 16: Enable/disable automatic translation of end of
+ * line sequences to and from line feed.
+ * Disabling this flag allows 8-bit clean loading and saving
+ * of files.
+ * - 32: Enable/Disable buffer editing hooks
+ * (via execution of macro in global Q-Register \(lqED\(rq)
+ * - 64: Enable/Disable function key macros
+ * - 128: Enable/Disable enforcement of UNIX98
+ * \(lq/bin/sh\(rq emulation for operating system command
+ * executions
+ * - 256: Enable/Disable \fBxterm\fP(1) clipboard support.
+ * Should only be enabled if XTerm allows the
+ * \fIGetSelection\fP and \fISetSelection\fP window
+ * operations.
+ *
+ * The features controlled thus are discribed in other sections
+ * of this manual.
+ *
+ * The default value of the \fBED\fP flags is 16
+ * (only automatic EOL translation enabled).
+ */
+static void
+teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+ if (!teco_expressions_args()) {
+ teco_expressions_push(teco_ed);
+ } else {
+ teco_int_t on, off;
+ if (!teco_expressions_pop_num_calc(&on, 0, error) ||
+ !teco_expressions_pop_num_calc(&off, ~(teco_int_t)0, error))
+ return;
+ teco_undo_int(teco_ed) = (teco_ed & ~off) | on;
+ }
+}
+
+/*$ EJ properties
+ * [key]EJ -> value -- Get and set system properties
+ * -EJ -> value
+ * value,keyEJ
+ * rgb,color,3EJ
+ *
+ * This command may be used to get and set system
+ * properties.
+ * With one argument, it retrieves a numeric property
+ * identified by \fIkey\fP.
+ * If \fIkey\fP is omitted, the prefix sign is implied
+ * (1 or -1).
+ * With two arguments, it sets property \fIkey\fP to
+ * \fIvalue\fP and returns nothing. Some property \fIkeys\fP
+ * may require more than one value. Properties may be
+ * write-only or read-only.
+ *
+ * The following property keys are defined:
+ * .IP 0 4
+ * The current user interface: 1 for Curses, 2 for GTK
+ * (\fBread-only\fP)
+ * .IP 1
+ * The current numbfer 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.
+ * (\fBread-only\fP)
+ * .IP 2
+ * The current memory limit in bytes.
+ * This limit helps to prevent dangerous out-of-memory
+ * conditions (e.g. resulting from infinite loops) by
+ * constantly sampling the memory requirements of \*(ST.
+ * Note that not all platforms support precise measurements
+ * of the current memory usage \(em \*(ST will fall back
+ * to an approximation which might be less than the actual
+ * usage on those platforms.
+ * Memory limiting is effective in batch and interactive mode.
+ * Commands which would exceed that limit will fail instead
+ * allowing users to recover in interactive mode, e.g. by
+ * terminating the command line.
+ * When getting, a zero value indicates that memory limiting is
+ * disabled.
+ * Setting a value less than or equal to 0 as in
+ * \(lq0,2EJ\(rq disables the limit.
+ * \fBWarning:\fP Disabling memory limiting may provoke
+ * out-of-memory errors in long running or infinite loops
+ * (interactive mode) that result in abnormal program
+ * termination.
+ * Setting a new limit may fail if the current memory
+ * requirements are too large for the new limit \(em if
+ * this happens you may have to clear your command-line
+ * first.
+ * Memory limiting is enabled by default.
+ * .IP 3
+ * This \fBwrite-only\fP property allows redefining the
+ * first 16 entries of the terminal color palette \(em a
+ * feature required by some
+ * color schemes when using the Curses user interface.
+ * When setting this property, you are making a request
+ * to define the terminal \fIcolor\fP as the Scintilla-compatible
+ * RGB color value given in the \fIrgb\fP parameter.
+ * \fIcolor\fP must be a value between 0 and 15
+ * corresponding to black, red, green, yellow, blue, magenta,
+ * cyan, white, bright black, bright red, etc. in that order.
+ * The \fIrgb\fP value has the format 0xBBGGRR, i.e. the red
+ * component is the least-significant byte and all other bytes
+ * are ignored.
+ * Note that on curses, RGB color values sent to Scintilla
+ * are actually mapped to these 16 colors by the Scinterm port
+ * and may represent colors with no resemblance to the \(lqRGB\(rq
+ * value used (depending on the current palette) \(em they should
+ * instead be viewed as placeholders for 16 standard terminal
+ * color codes.
+ * Please refer to the Scinterm manual for details on the allowed
+ * \(lqRGB\(rq values and how they map to terminal colors.
+ * This command provides a crude way to request exact RGB colors
+ * for the first 16 terminal colors.
+ * The color definition may be queued or be completely ignored
+ * on other user interfaces and no feedback is given
+ * if it fails. In fact feedback cannot be given reliably anyway.
+ * Note that on 8 color terminals, only the first 8 colors
+ * can be redefined (if you are lucky).
+ * Note that due to restrictions of most terminal emulators
+ * and some curses implementations, this command simply will not
+ * restore the original palette entry or request
+ * when rubbed out and should generally only be used in
+ * \fIbatch-mode\fP \(em typically when loading a color scheme.
+ * For the same reasons \(em even though \*(ST tries hard to
+ * restore the original palette on exit \(em palette changes may
+ * persist after \*(ST terminates on most terminal emulators on Unix.
+ * The only emulator which will restore their default palette
+ * on exit the author is aware of is \fBxterm\fP(1) and
+ * the Linux console driver.
+ * You have been warned. Good luck.
+ */
+static void
+teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error)
+{
+ enum {
+ EJ_USER_INTERFACE = 0,
+ EJ_BUFFERS,
+ EJ_MEMORY_LIMIT,
+ EJ_INIT_COLOR
+ };
+
+ teco_int_t property;
+ if (!teco_expressions_eval(FALSE, error) ||
+ !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;
+
+ switch (property) {
+ case EJ_MEMORY_LIMIT:
+ if (!teco_memory_set_limit(MAX(0, value), error))
+ return;
+ break;
+
+ case EJ_INIT_COLOR:
+ if (value < 0 || value >= 16) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid color code %" TECO_INT_FORMAT " "
+ "specified for <EJ>", value);
+ return;
+ }
+ if (!teco_expressions_args()) {
+ 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);
+ break;
+
+ default:
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Cannot set property %" TECO_INT_FORMAT " "
+ "for <EJ>", property);
+ return;
+ }
+
+ return;
+ }
+
+ /*
+ * Get property
+ */
+ switch (property) {
+ case EJ_USER_INTERFACE:
+ /*
+ * FIXME: Replace INTERFACE_* macros with
+ * teco_interface_id()?
+ */
+#ifdef INTERFACE_CURSES
+ teco_expressions_push(1);
+#elif defined(INTERFACE_GTK)
+ teco_expressions_push(2);
+#else
+#error Missing value for current interface!
+#endif
+ break;
+
+ case EJ_BUFFERS:
+ teco_expressions_push(teco_ring_get_id(teco_ring_last()));
+ break;
+
+ case EJ_MEMORY_LIMIT:
+ teco_expressions_push(teco_memory_limit);
+ break;
+
+ default:
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid property %" TECO_INT_FORMAT " "
+ "for <EJ>", property);
+ return;
+ }
+}
+
+/*$ EL eol
+ * 0EL -- Set or get End of Line mode
+ * 13,10:EL
+ * 1EL
+ * 13:EL
+ * 2EL
+ * 10:EL
+ * EL -> 0 | 1 | 2
+ * :EL -> 13,10 | 13 | 10
+ *
+ * Sets or gets the current document's End Of Line (EOL) mode.
+ * This is a thin wrapper around Scintilla's
+ * \fBSCI_SETEOLMODE\fP and \fBSCI_GETEOLMODE\fP messages but is
+ * shorter to type and supports restoring the EOL mode upon rubout.
+ * Like the Scintilla message, <EL> does \fBnot\fP change the
+ * characters in the current document.
+ * If automatic EOL translation is activated (which is the default),
+ * \*(ST will however use this information when saving files or
+ * writing to external processes.
+ *
+ * With one argument, the EOL mode is set according to these
+ * constants:
+ * .IP 0 4
+ * Carriage return (ASCII 13), followed by line feed (ASCII 10).
+ * This is the default EOL mode on DOS/Windows.
+ * .IP 1
+ * Carriage return (ASCII 13).
+ * The default EOL mode on old Mac OS systems.
+ * .IP 2
+ * Line feed (ASCII 10).
+ * The default EOL mode on POSIX/UNIX systems.
+ *
+ * In its colon-modified form, the EOL mode is set according
+ * to the EOL characters on the expression stack.
+ * \*(ST will only pop as many values as are necessary to
+ * determine the EOL mode.
+ *
+ * Without arguments, the current EOL mode is returned.
+ * When colon-modified, the current EOL mode's character sequence
+ * is pushed onto the expression stack.
+ */
+static void
+teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return;
+
+ if (teco_expressions_args() > 0) {
+ teco_int_t eol_mode;
+
+ if (teco_machine_main_eval_colon(ctx)) {
+ teco_int_t v1, v2;
+ if (!teco_expressions_pop_num_calc(&v1, 0, error))
+ return;
+
+ switch (v1) {
+ case '\r':
+ eol_mode = SC_EOL_CR;
+ break;
+ case '\n':
+ if (!teco_expressions_args()) {
+ eol_mode = SC_EOL_LF;
+ break;
+ }
+ if (!teco_expressions_pop_num_calc(&v2, 0, error))
+ return;
+ if (v2 == '\r') {
+ eol_mode = SC_EOL_CRLF;
+ break;
+ }
+ /* fall through */
+ default:
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid EOL sequence for <EL>");
+ return;
+ }
+ } else {
+ if (!teco_expressions_pop_num_calc(&eol_mode, 0, error))
+ return;
+ switch (eol_mode) {
+ case SC_EOL_CRLF:
+ case SC_EOL_CR:
+ case SC_EOL_LF:
+ break;
+ default:
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid EOL mode %" TECO_INT_FORMAT " for <EL>",
+ eol_mode);
+ return;
+ }
+ }
+
+ undo__teco_interface_ssm(SCI_SETEOLMODE,
+ teco_interface_ssm(SCI_GETEOLMODE, 0, 0), 0);
+ teco_interface_ssm(SCI_SETEOLMODE, eol_mode, 0);
+ } else if (teco_machine_main_eval_colon(ctx)) {
+ const gchar *eol_seq = teco_eol_get_seq(teco_interface_ssm(SCI_GETEOLMODE, 0, 0));
+ teco_expressions_push(eol_seq);
+ } else {
+ teco_expressions_push(teco_interface_ssm(SCI_GETEOLMODE, 0, 0));
+ }
+}
+
+/*$ EX exit
+ * [bool]EX -- Exit program
+ * -EX
+ * :EX
+ *
+ * Exits \*(ST, or rather requests program termination
+ * at the end of the top-level macro.
+ * Therefore instead of exiting immediately which
+ * could be annoying in interactive mode, EX will
+ * result in program termination only when the command line
+ * is terminated.
+ * This allows EX to be rubbed out and used in macros.
+ * The usual command to exit \*(ST in interactive mode
+ * is thus \(lqEX\fB$$\fP\(rq.
+ * In batch mode EX will exit the program if control
+ * reaches the end of the munged file \(em instead of
+ * starting up interactive mode.
+ *
+ * If any buffer is dirty (modified), EX will yield
+ * an error.
+ * When specifying <bool> as a success/truth condition
+ * boolean, EX will not check whether there are modified
+ * buffers and will always succeed.
+ * If <bool> is omitted, the sign prefix is implied
+ * (1 or -1).
+ * In other words \(lq-EX\fB$$\fP\(rq is the usual
+ * interactive command sequence to discard all unsaved
+ * changes and exit.
+ *
+ * When colon-modified, <bool> is ignored and EX
+ * will instead immediately try to save all modified buffers \(em
+ * this can of course be reversed using rubout.
+ * Saving all buffers can fail, e.g. if the unnamed file
+ * is modified or if there is an IO error.
+ * \(lq:EX\fB$$\fP\(rq is nevertheless the usual interactive
+ * command sequence to exit while saving all modified
+ * buffers.
+ */
+/** @fixme what if changing file after EX? will currently still exit */
+static void
+teco_state_ecommand_exit(teco_machine_main_t *ctx, GError **error)
+{
+ if (teco_machine_main_eval_colon(ctx)) {
+ if (!teco_ring_save_all_dirty_buffers(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_is_any_dirty()) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Modified buffers exist");
+ return;
+ }
+ }
+
+ teco_undo_gboolean(teco_quit_requested) = TRUE;
+}
+
+static teco_state_t *
+teco_state_ecommand_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ static teco_machine_main_transition_t transitions[] = {
+ /*
+ * Simple Transitions
+ */
+ ['%'] = {&teco_state_epctcommand},
+ ['B'] = {&teco_state_edit_file},
+ ['C'] = {&teco_state_execute},
+ ['G'] = {&teco_state_egcommand},
+ ['I'] = {&teco_state_insert_nobuilding},
+ ['M'] = {&teco_state_macrofile},
+ ['N'] = {&teco_state_glob_pattern},
+ ['S'] = {&teco_state_scintilla_symbols},
+ ['Q'] = {&teco_state_eqcommand},
+ ['U'] = {&teco_state_eucommand},
+ ['W'] = {&teco_state_save_file},
+
+ /*
+ * Commands
+ */
+ ['F'] = {&teco_state_start, teco_state_ecommand_close},
+ ['D'] = {&teco_state_start, teco_state_ecommand_flags},
+ ['J'] = {&teco_state_start, teco_state_ecommand_properties},
+ ['L'] = {&teco_state_start, teco_state_ecommand_eol},
+ ['X'] = {&teco_state_start, teco_state_ecommand_exit}
+ };
+
+ /*
+ * FIXME: Should we return a special syntax error in case of failure?
+ */
+ return teco_machine_main_transition_input(ctx, transitions, G_N_ELEMENTS(transitions),
+ teco_ascii_toupper(chr), error);
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_ecommand);
+
+gboolean
+teco_state_insert_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+ guint args = teco_expressions_args();
+ if (!args)
+ return TRUE;
+
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ for (int i = args; i > 0; i--) {
+ gchar chr = (gchar)teco_expressions_peek_num(i-1);
+ teco_interface_ssm(SCI_ADDTEXT, 1, (sptr_t)&chr);
+ }
+ for (int i = args; i > 0; i--)
+ if (!teco_expressions_pop_num_calc(NULL, 0, error))
+ return FALSE;
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+
+ return TRUE;
+}
+
+gboolean
+teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str,
+ gsize new_chars, GError **error)
+{
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ teco_interface_ssm(SCI_ADDTEXT, new_chars,
+ (sptr_t)(str->data + str->len - new_chars));
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+
+ return TRUE;
+}
+
+/*
+ * NOTE: cannot support VideoTECO's <n>I because
+ * beginning and end of strings must be determined
+ * syntactically
+ */
+/*$ I insert
+ * [c1,c2,...]I[text]$ -- Insert text with string building characters
+ *
+ * First inserts characters for all the values
+ * on the argument stack (interpreted as codepoints).
+ * It does so in the order of the arguments, i.e.
+ * <c1> is inserted before <c2>, ecetera.
+ * Secondly, the command inserts <text>.
+ * In interactive mode, <text> is inserted interactively.
+ *
+ * String building characters are \fBenabled\fP for the
+ * I command.
+ * When editing \*(ST macros, using the \fBEI\fP command
+ * 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
+);
+
+static gboolean
+teco_state_insert_indent_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ if (!teco_state_insert_initial(ctx, error))
+ return FALSE;
+
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ if (teco_interface_ssm(SCI_GETUSETABS, 0, 0)) {
+ teco_interface_ssm(SCI_ADDTEXT, 1, (sptr_t)"\t");
+ } else {
+ gint len = teco_interface_ssm(SCI_GETTABWIDTH, 0, 0);
+
+ len -= teco_interface_ssm(SCI_GETCOLUMN,
+ teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0) % len;
+
+ gchar spaces[len];
+
+ memset(spaces, ' ', sizeof(spaces));
+ teco_interface_ssm(SCI_ADDTEXT, sizeof(spaces), (sptr_t)spaces);
+ }
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+
+ return TRUE;
+}
+
+/*
+ * Alternatives: ^i, ^I, <CTRL/I>, <TAB>
+ */
+/*$ ^I indent
+ * [char,...]^I[text]$ -- Insert with leading indention
+ *
+ * ^I (usually typed using the Tab key), first inserts
+ * all the chars on the stack into the buffer, then indention
+ * characters (one tab or multiple spaces) and eventually
+ * the optional <text> is inserted interactively.
+ * It is thus a derivate of the \fBI\fP (insertion) command.
+ *
+ * \*(ST uses Scintilla settings to determine the indention
+ * characters.
+ * If tab use is enabled with the \fBSCI_SETUSETABS\fP message,
+ * a single tab character is inserted.
+ * Tab use is enabled by default.
+ * Otherwise, a number of spaces is inserted up to the
+ * next tab stop so that the command's <text> argument
+ * is inserted at the beginning of the next tab stop.
+ * The size of the tab stops is configured by the
+ * \fBSCI_SETTABWIDTH\fP Scintilla message (8 by default).
+ * In combination with \*(ST's use of the tab key as an
+ * immediate editing command for all insertions, this
+ * implements support for different insertion styles.
+ * The Scintilla settings apply to the current Scintilla
+ * document and are thus local to the currently edited
+ * buffer or Q-Register.
+ *
+ * However for the same reason, the ^I command is not
+ * fully compatible with classic TECO which \fIalways\fP
+ * inserts a single tab character and should not be used
+ * for the purpose of inserting single tabs in generic
+ * macros.
+ * To insert a single tab character reliably, the idioms
+ * \(lq9I$\(rq or \(lqI^I$\(rq may be used.
+ *
+ * Like the I command, ^I has string building characters
+ * \fBenabled\fP.
+ */
+TECO_DEFINE_STATE_INSERT(teco_state_insert_indent,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_insert_indent_initial
+);
diff --git a/src/core-commands.h b/src/core-commands.h
new file mode 100644
index 0000000..c5a8ee0
--- /dev/null
+++ b/src/core-commands.h
@@ -0,0 +1,71 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include "sciteco.h"
+#include "parser.h"
+#include "string-utils.h"
+
+/*
+ * 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);
+
+void teco_undo_change_dir_to_current(void);
+TECO_DECLARE_STATE(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);
+
+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,
+ gsize new_chars, GError **error);
+
+/* in cmdline.c */
+gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error);
+
+/**
+ * @class TECO_DEFINE_STATE_INSERT
+ * @implements TECO_DEFINE_STATE_EXPECTSTRING
+ * @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_start; /* nothing to be done when done */ \
+ } \
+ 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, \
+ ##__VA_ARGS__ \
+ )
+
+TECO_DECLARE_STATE(teco_state_insert_building);
+TECO_DECLARE_STATE(teco_state_insert_nobuilding);
+TECO_DECLARE_STATE(teco_state_insert_indent);
diff --git a/src/doc.c b/src/doc.c
new file mode 100644
index 0000000..41acf40
--- /dev/null
+++ b/src/doc.c
@@ -0,0 +1,209 @@
+/*
+ * 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 <Scintilla.h>
+
+#include "sciteco.h"
+#include "view.h"
+#include "undo.h"
+#include "qreg.h"
+#include "doc.h"
+
+static inline teco_doc_scintilla_t *
+teco_doc_get_scintilla(teco_doc_t *ctx)
+{
+ if (G_UNLIKELY(!ctx->doc))
+ ctx->doc = (teco_doc_scintilla_t *)teco_view_ssm(teco_qreg_view, SCI_CREATEDOCUMENT, 0, 0);
+ return ctx->doc;
+}
+
+/** @memberof teco_doc_t */
+void
+teco_doc_edit(teco_doc_t *ctx)
+{
+ /*
+ * FIXME: SCI_SETREPRESENTATION does not redraw
+ * the screen - also that would be very slow.
+ * Since SCI_SETDOCPOINTER resets the representation
+ * (this should probably be fixed in Scintilla),
+ * the screen is garbled since the layout cache
+ * is calculated with the default representations.
+ * We work around this by temporarily disabling the
+ * layout cache.
+ */
+ gint old_mode = teco_view_ssm(teco_qreg_view, SCI_GETLAYOUTCACHE, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_SETLAYOUTCACHE, SC_CACHE_NONE, 0);
+
+ teco_view_ssm(teco_qreg_view, SCI_SETDOCPOINTER, 0,
+ (sptr_t)teco_doc_get_scintilla(ctx));
+ teco_view_ssm(teco_qreg_view, SCI_SETFIRSTVISIBLELINE, ctx->first_line, 0);
+ teco_view_ssm(teco_qreg_view, SCI_SETXOFFSET, ctx->xoffset, 0);
+ teco_view_ssm(teco_qreg_view, SCI_SETSEL, ctx->anchor, (sptr_t)ctx->dot);
+
+ /*
+ * Default TECO-style character representations.
+ * They are reset on EVERY SETDOCPOINTER call by Scintilla.
+ */
+ teco_view_set_representations(teco_qreg_view);
+
+ teco_view_ssm(teco_qreg_view, SCI_SETLAYOUTCACHE, old_mode, 0);
+}
+
+/** @memberof teco_doc_t */
+void
+teco_doc_undo_edit(teco_doc_t *ctx)
+{
+ /*
+ * FIXME: see above in teco_doc_edit()
+ */
+ undo__teco_view_ssm(teco_qreg_view, SCI_SETLAYOUTCACHE,
+ teco_view_ssm(teco_qreg_view, SCI_GETLAYOUTCACHE, 0, 0), 0);
+
+ undo__teco_view_set_representations(teco_qreg_view);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_SETSEL, ctx->anchor, (sptr_t)ctx->dot);
+ undo__teco_view_ssm(teco_qreg_view, SCI_SETXOFFSET, ctx->xoffset, 0);
+ undo__teco_view_ssm(teco_qreg_view, SCI_SETFIRSTVISIBLELINE, ctx->first_line, 0);
+ undo__teco_view_ssm(teco_qreg_view, SCI_SETDOCPOINTER, 0,
+ (sptr_t)teco_doc_get_scintilla(ctx));
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_SETLAYOUTCACHE, SC_CACHE_NONE, 0);
+}
+
+/** @memberof teco_doc_t */
+void
+teco_doc_set_string(teco_doc_t *ctx, const gchar *str, gsize len)
+{
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_reset(ctx);
+ teco_doc_edit(ctx);
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_APPENDTEXT, len, (sptr_t)(str ? : ""));
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+}
+
+/** @memberof teco_doc_t */
+void
+teco_doc_undo_set_string(teco_doc_t *ctx)
+{
+ /*
+ * Necessary, so that upon rubout the
+ * string's parameters are restored.
+ */
+ teco_doc_update(ctx, teco_qreg_view);
+
+ if (teco_qreg_current && teco_qreg_current->must_undo) // FIXME
+ teco_doc_undo_edit(&teco_qreg_current->string);
+
+ teco_doc_undo_reset(ctx);
+ undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0);
+
+ teco_doc_undo_edit(ctx);
+}
+
+/**
+ * Get a document as a string.
+ *
+ * @param ctx The document.
+ * @param str Pointer to a variable to hold the return string.
+ * It can be NULL if you are interested only in the string's length.
+ * Strings must be freed via g_free().
+ * @param len Where to store the string's length (mandatory).
+ *
+ * @see teco_qreg_vtable_t::get_string()
+ * @memberof teco_doc_t
+ */
+void
+teco_doc_get_string(teco_doc_t *ctx, gchar **str, gsize *len)
+{
+ if (!ctx->doc) {
+ if (str)
+ *str = NULL;
+ *len = 0;
+ return;
+ }
+
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(ctx);
+
+ *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);
+ }
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+}
+
+/** @memberof teco_doc_t */
+void
+teco_doc_update_from_view(teco_doc_t *ctx, teco_view_t *from)
+{
+ ctx->anchor = teco_view_ssm(from, SCI_GETANCHOR, 0, 0);
+ ctx->dot = teco_view_ssm(from, SCI_GETCURRENTPOS, 0, 0);
+ ctx->first_line = teco_view_ssm(from, SCI_GETFIRSTVISIBLELINE, 0, 0);
+ ctx->xoffset = teco_view_ssm(from, SCI_GETXOFFSET, 0, 0);
+}
+
+/** @memberof teco_doc_t */
+void
+teco_doc_update_from_doc(teco_doc_t *ctx, const teco_doc_t *from)
+{
+ ctx->anchor = from->anchor;
+ ctx->dot = from->dot;
+ ctx->first_line = from->first_line;
+ ctx->xoffset = from->xoffset;
+}
+
+/**
+ * Only for teco_qreg_stack_pop() which does some clever
+ * exchanging of document data (without any deep copying)
+ *
+ * @memberof teco_doc_t
+ */
+void
+teco_doc_exchange(teco_doc_t *ctx, teco_doc_t *other)
+{
+ teco_doc_t temp;
+ memcpy(&temp, ctx, sizeof(temp));
+ memcpy(ctx, other, sizeof(*ctx));
+ memcpy(other, &temp, sizeof(*other));
+}
+
+/** @memberof teco_doc_t */
+void
+teco_doc_clear(teco_doc_t *ctx)
+{
+ if (ctx->doc)
+ teco_view_ssm(teco_qreg_view, SCI_RELEASEDOCUMENT, 0, (sptr_t)ctx->doc);
+}
diff --git a/src/doc.h b/src/doc.h
new file mode 100644
index 0000000..471d16a
--- /dev/null
+++ b/src/doc.h
@@ -0,0 +1,115 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+#include <string.h>
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+#include "view.h"
+#include "undo.h"
+
+/**
+ * Scintilla document type.
+ * The struct is never defined and only exists for improved
+ * type safety.
+ */
+typedef struct teco_doc_scintilla_t teco_doc_scintilla_t;
+
+/**
+ * A Scintilla document.
+ *
+ * Also contains other attributes required to restore
+ * the overall editor state when loading it into a Scintilla view.
+ */
+typedef struct {
+ /**
+ * Underlying Scintilla document.
+ * It is created on demand in teco_doc_maybe_create_document(),
+ * so that we don't waste memory on integer-only Q-Registers.
+ */
+ teco_doc_scintilla_t *doc;
+
+ /*
+ * The so called "parameters".
+ * Updated/restored only when required
+ */
+ gint anchor, dot;
+ gint first_line, xoffset;
+} teco_doc_t;
+
+/** @memberof teco_doc_t */
+static inline void
+teco_doc_init(teco_doc_t *ctx)
+{
+ memset(ctx, 0, sizeof(*ctx));
+}
+
+void teco_doc_edit(teco_doc_t *ctx);
+void teco_doc_undo_edit(teco_doc_t *ctx);
+
+void teco_doc_set_string(teco_doc_t *ctx, const gchar *str, gsize len);
+void teco_doc_undo_set_string(teco_doc_t *ctx);
+
+void teco_doc_get_string(teco_doc_t *ctx, gchar **str, gsize *len);
+
+void teco_doc_update_from_view(teco_doc_t *ctx, teco_view_t *from);
+void teco_doc_update_from_doc(teco_doc_t *ctx, const teco_doc_t *from);
+
+/** @memberof teco_doc_t */
+#define teco_doc_update(CTX, FROM) \
+ (_Generic((FROM), teco_view_t * : teco_doc_update_from_view, \
+ teco_doc_t * : teco_doc_update_from_doc, \
+ const teco_doc_t * : teco_doc_update_from_doc)((CTX), (FROM)))
+
+/** @memberof teco_doc_t */
+static inline void
+teco_doc_reset(teco_doc_t *ctx)
+{
+ ctx->anchor = ctx->dot = 0;
+ ctx->first_line = ctx->xoffset = 0;
+}
+
+/** @memberof teco_doc_t */
+static inline void
+teco_doc_undo_reset(teco_doc_t *ctx)
+{
+ /*
+ * NOTE: Could be rolled into one function
+ * and called with teco_undo_call() if we really
+ * wanted to save more memory.
+ */
+ teco_undo_gint(ctx->anchor);
+ teco_undo_gint(ctx->dot);
+ teco_undo_gint(ctx->first_line);
+ teco_undo_gint(ctx->xoffset);
+}
+
+void teco_doc_exchange(teco_doc_t *ctx, teco_doc_t *other);
+
+/** @memberof teco_doc_t */
+static inline void
+teco_doc_undo_exchange(teco_doc_t *ctx)
+{
+ teco_undo_ptr(ctx->doc);
+ teco_doc_undo_reset(ctx);
+}
+
+void teco_doc_clear(teco_doc_t *ctx);
diff --git a/src/document.cpp b/src/document.cpp
deleted file mode 100644
index e3b183e..0000000
--- a/src/document.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <Scintilla.h>
-
-#include "sciteco.h"
-#include "interface.h"
-#include "undo.h"
-#include "document.h"
-
-namespace SciTECO {
-
-void
-Document::edit(ViewCurrent &view)
-{
- /*
- * FIXME: SCI_SETREPRESENTATION does not redraw
- * the screen - also that would be very slow.
- * Since SCI_SETDOCPOINTER resets the representation
- * (this should probably be fixed in Scintilla),
- * the screen is garbled since the layout cache
- * is calculated with the default representations.
- * We work around this by temporarily disabling the
- * layout cache.
- */
- gint old_mode = view.ssm(SCI_GETLAYOUTCACHE);
-
- maybe_create_document();
-
- view.ssm(SCI_SETLAYOUTCACHE, SC_CACHE_NONE);
-
- view.ssm(SCI_SETDOCPOINTER, 0, (sptr_t)doc);
- view.ssm(SCI_SETFIRSTVISIBLELINE, first_line);
- view.ssm(SCI_SETXOFFSET, xoffset);
- view.ssm(SCI_SETSEL, anchor, (sptr_t)dot);
-
- /*
- * Default TECO-style character representations.
- * They are reset on EVERY SETDOCPOINTER call by Scintilla.
- */
- view.set_representations();
-
- view.ssm(SCI_SETLAYOUTCACHE, old_mode);
-}
-
-void
-Document::undo_edit(ViewCurrent &view)
-{
- maybe_create_document();
-
- /*
- * FIXME: see above in Document::edit()
- */
- view.undo_ssm(SCI_SETLAYOUTCACHE,
- view.ssm(SCI_GETLAYOUTCACHE));
-
- view.undo_set_representations();
-
- view.undo_ssm(SCI_SETSEL, anchor, (sptr_t)dot);
- view.undo_ssm(SCI_SETXOFFSET, xoffset);
- view.undo_ssm(SCI_SETFIRSTVISIBLELINE, first_line);
- view.undo_ssm(SCI_SETDOCPOINTER, 0, (sptr_t)doc);
-
- view.undo_ssm(SCI_SETLAYOUTCACHE, SC_CACHE_NONE);
-}
-
-void
-Document::update(ViewCurrent &view)
-{
- anchor = view.ssm(SCI_GETANCHOR);
- dot = view.ssm(SCI_GETCURRENTPOS);
- first_line = view.ssm(SCI_GETFIRSTVISIBLELINE);
- xoffset = view.ssm(SCI_GETXOFFSET);
-}
-
-/*
- * Only for QRegisterStack::pop() which does some clever
- * exchanging of document data (without any deep copying)
- */
-void
-Document::exchange(Document &other)
-{
- SciDoc temp_doc = doc;
- gint temp_anchor = anchor;
- gint temp_dot = dot;
- gint temp_first_line = first_line;
- gint temp_xoffset = xoffset;
-
- doc = other.doc;
- anchor = other.anchor;
- dot = other.dot;
- first_line = other.first_line;
- xoffset = other.xoffset;
-
- other.doc = temp_doc;
- other.anchor = temp_anchor;
- other.dot = temp_dot;
- other.first_line = temp_first_line;
- other.xoffset = temp_xoffset;
-}
-
-} /* namespace SciTECO */
diff --git a/src/document.h b/src/document.h
deleted file mode 100644
index f132745..0000000
--- a/src/document.h
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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/>.
- */
-
-#ifndef __DOCUMENT_H
-#define __DOCUMENT_H
-
-#include <glib.h>
-
-#include <Scintilla.h>
-
-#include "sciteco.h"
-#include "memory.h"
-#include "interface.h"
-#include "undo.h"
-
-namespace SciTECO {
-
-/*
- * Classes
- */
-
-class Document : public Object {
- typedef const void *SciDoc;
- SciDoc doc;
-
- /*
- * The so called "parameters".
- * Updated/restored only when required
- */
- gint anchor, dot;
- gint first_line, xoffset;
-
-public:
- Document() : doc(NULL)
- {
- reset();
- }
- virtual ~Document()
- {
- /*
- * Cannot release document here, since we must
- * do it on the same view that created it.
- * We also cannot call get_create_document_view()
- * since it is virtual.
- * So we must demand that deriving classes call
- * release_document() from their destructors.
- */
- g_assert(doc == NULL);
- }
-
- inline bool
- is_initialized(void)
- {
- return doc != NULL;
- }
-
- void edit(ViewCurrent &view);
- void undo_edit(ViewCurrent &view);
-
- void update(ViewCurrent &view);
- inline void
- update(const Document &from)
- {
- anchor = from.anchor;
- dot = from.dot;
- first_line = from.first_line;
- xoffset = from.xoffset;
- }
-
- inline void
- reset(void)
- {
- anchor = dot = 0;
- first_line = xoffset = 0;
- }
- inline void
- undo_reset(void)
- {
- undo.push_var(anchor);
- undo.push_var(dot);
- undo.push_var(first_line);
- undo.push_var(xoffset);
- }
-
- void exchange(Document &other);
- inline void
- undo_exchange(void)
- {
- undo.push_var(doc);
- undo_reset();
- }
-
-protected:
- inline void
- release_document(void)
- {
- if (is_initialized()) {
- ViewCurrent &view = get_create_document_view();
- view.ssm(SCI_RELEASEDOCUMENT, 0, (sptr_t)doc);
- doc = NULL;
- }
- }
-
-private:
- /*
- * Must be implemented by derived class.
- * Documents must be released on the same view
- * as they were created.
- * Since we do not want to save this view
- * per document, it must instead be returned by
- * this method.
- */
- virtual ViewCurrent &get_create_document_view(void) = 0;
-
- inline void
- maybe_create_document(void)
- {
- if (!is_initialized()) {
- ViewCurrent &view = get_create_document_view();
- doc = (SciDoc)view.ssm(SCI_CREATEDOCUMENT);
- }
- }
-};
-
-} /* namespace SciTECO */
-
-#endif
diff --git a/src/eol.c b/src/eol.c
new file mode 100644
index 0000000..44ad021
--- /dev/null
+++ b/src/eol.c
@@ -0,0 +1,461 @@
+/*
+ * 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 <string.h>
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+#include "eol.h"
+
+const gchar *
+teco_eol_get_seq(gint eol_mode)
+{
+ switch (eol_mode) {
+ case SC_EOL_CRLF:
+ return "\r\n";
+ case SC_EOL_CR:
+ return "\r";
+ case SC_EOL_LF:
+ default:
+ return "\n";
+ }
+}
+
+static inline void
+teco_eol_reader_init(teco_eol_reader_t *ctx)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->eol_style = -1;
+}
+
+static GIOStatus
+teco_eol_reader_read_gio(teco_eol_reader_t *ctx, gsize *read_len, GError **error)
+{
+ return g_io_channel_read_chars(ctx->gio.channel, ctx->gio.buffer,
+ sizeof(ctx->gio.buffer),
+ read_len, error);
+}
+
+/** @memberof teco_eol_reader_t */
+void
+teco_eol_reader_init_gio(teco_eol_reader_t *ctx, GIOChannel *channel)
+{
+ teco_eol_reader_init(ctx);
+ ctx->read_cb = teco_eol_reader_read_gio;
+
+ teco_eol_reader_set_channel(ctx, channel);
+}
+
+static GIOStatus
+teco_eol_reader_read_mem(teco_eol_reader_t *ctx, gsize *read_len, GError **error)
+{
+ *read_len = ctx->mem.len;
+ ctx->mem.len = 0;
+ /*
+ * On the first call, returns G_IO_STATUS_NORMAL,
+ * later G_IO_STATUS_EOF.
+ */
+ return *read_len != 0 ? G_IO_STATUS_NORMAL : G_IO_STATUS_EOF;
+}
+
+/** @memberof teco_eol_reader_t */
+void
+teco_eol_reader_init_mem(teco_eol_reader_t *ctx, gchar *buffer, gsize len)
+{
+ teco_eol_reader_init(ctx);
+ ctx->read_cb = teco_eol_reader_read_mem;
+
+ ctx->mem.buffer = buffer;
+ ctx->mem.len = len;
+}
+
+/**
+ * Read data with automatic EOL translation.
+ *
+ * This gets the next data block from the converter
+ * implementation, performs EOL translation (if enabled)
+ * in a more or less efficient manner and returns
+ * a chunk of EOL-normalized data.
+ *
+ * Since the underlying data source may have to be
+ * queried repeatedly and because the EOL Reader avoids
+ * reassembling the EOL-normalized data by returning
+ * references into the modified data source, it is
+ * necessary to call this function repeatedly until
+ * it returns G_IO_STATUS_EOF.
+ *
+ * @param ctx The EOL Reader object.
+ * @param ret Location to store a pointer to the converted chunk.
+ * The EOL-converted data is NOT null-terminated.
+ * @param data_len A pointer to the length of the converted chunk.
+ * @param error A GError.
+ * @return The status of the conversion.
+ *
+ * @memberof teco_eol_reader_t
+ */
+GIOStatus
+teco_eol_reader_convert(teco_eol_reader_t *ctx, gchar **ret, gsize *data_len, GError **error)
+{
+ gchar *buffer = ctx->read_cb == teco_eol_reader_read_gio ? ctx->gio.buffer : ctx->mem.buffer;
+
+ if (ctx->last_char < 0) {
+ /* a CRLF was last translated */
+ ctx->block_len++;
+ ctx->last_char = '\n';
+ }
+ ctx->offset += ctx->block_len;
+
+ if (ctx->offset == ctx->read_len) {
+ ctx->offset = 0;
+
+ switch (ctx->read_cb(ctx, &ctx->read_len, error)) {
+ case G_IO_STATUS_ERROR:
+ return G_IO_STATUS_ERROR;
+
+ case G_IO_STATUS_EOF:
+ if (ctx->last_char == '\r') {
+ /*
+ * Very last character read is CR.
+ * If this is the only EOL so far, the
+ * EOL style is MAC.
+ * This is also executed if auto-eol is disabled
+ * but it doesn't hurt.
+ */
+ if (ctx->eol_style < 0)
+ ctx->eol_style = SC_EOL_CR;
+ else if (ctx->eol_style != SC_EOL_CR)
+ ctx->eol_style_inconsistent = TRUE;
+ }
+
+ return G_IO_STATUS_EOF;
+
+ case G_IO_STATUS_NORMAL:
+ case G_IO_STATUS_AGAIN:
+ break;
+ }
+
+ if (!(teco_ed & TECO_ED_AUTOEOL)) {
+ /*
+ * No EOL translation - always return entire
+ * buffer
+ */
+ *data_len = ctx->block_len = ctx->read_len;
+ *ret = buffer;
+ return G_IO_STATUS_NORMAL;
+ }
+ }
+
+ /*
+ * Return data with automatic EOL translation.
+ * Every EOL sequence is normalized to LF and
+ * the first sequence determines the documents
+ * EOL style.
+ * This loop is executed for every byte of the
+ * file/stream, so it was important to optimize
+ * it. Specifically, the number of returns
+ * is minimized by keeping a pointer to
+ * the beginning of a block of data in the buffer
+ * which already has LFs (offset).
+ * Mac EOLs can be converted to UNIX EOLs directly
+ * in the buffer.
+ * So if their EOLs are consistent, the function
+ * will return one block for the entire buffer.
+ * When reading a file with DOS EOLs, there will
+ * be one call per line which is significantly slower.
+ */
+ for (guint i = ctx->offset; i < ctx->read_len; i++) {
+ switch (buffer[i]) {
+ case '\n':
+ if (ctx->last_char == '\r') {
+ if (ctx->eol_style < 0)
+ ctx->eol_style = SC_EOL_CRLF;
+ else if (ctx->eol_style != SC_EOL_CRLF)
+ ctx->eol_style_inconsistent = TRUE;
+
+ /*
+ * Return block. CR has already
+ * been made LF in `buffer`.
+ */
+ *data_len = ctx->block_len = i-ctx->offset;
+ /* next call will skip the CR */
+ ctx->last_char = -1;
+ *ret = buffer + ctx->offset;
+ return G_IO_STATUS_NORMAL;
+ }
+
+ if (ctx->eol_style < 0)
+ ctx->eol_style = SC_EOL_LF;
+ else if (ctx->eol_style != SC_EOL_LF)
+ ctx->eol_style_inconsistent = TRUE;
+ /*
+ * No conversion necessary and no need to
+ * return block yet.
+ */
+ ctx->last_char = '\n';
+ break;
+
+ case '\r':
+ if (ctx->last_char == '\r') {
+ if (ctx->eol_style < 0)
+ ctx->eol_style = SC_EOL_CR;
+ else if (ctx->eol_style != SC_EOL_CR)
+ ctx->eol_style_inconsistent = TRUE;
+ }
+
+ /*
+ * Convert CR to LF in `buffer`.
+ * This way more than one line using
+ * Mac EOLs can be returned at once.
+ */
+ buffer[i] = '\n';
+ ctx->last_char = '\r';
+ break;
+
+ default:
+ if (ctx->last_char == '\r') {
+ if (ctx->eol_style < 0)
+ ctx->eol_style = SC_EOL_CR;
+ else if (ctx->eol_style != SC_EOL_CR)
+ ctx->eol_style_inconsistent = TRUE;
+ }
+ ctx->last_char = buffer[i];
+ break;
+ }
+ }
+
+ /*
+ * Return remaining block.
+ * With UNIX/MAC EOLs, this will usually be the
+ * entire `buffer`
+ */
+ *data_len = ctx->block_len = ctx->read_len-ctx->offset;
+ *ret = buffer + ctx->offset;
+ return G_IO_STATUS_NORMAL;
+}
+
+/** @memberof teco_eol_reader_t */
+GIOStatus
+teco_eol_reader_convert_all(teco_eol_reader_t *ctx, gchar **ret, gsize *out_len, GError **error)
+{
+ gsize buffer_len = ctx->read_cb == teco_eol_reader_read_gio
+ ? sizeof(ctx->gio.buffer) : ctx->mem.len;
+
+ /*
+ * NOTE: Doesn't use teco_string_t to make use of GString's
+ * preallocation feature.
+ */
+ GString *str = g_string_sized_new(buffer_len);
+
+ for (;;) {
+ gchar *data;
+ gsize data_len;
+
+ GIOStatus rc = teco_eol_reader_convert(ctx, &data, &data_len, error);
+ if (rc == G_IO_STATUS_ERROR) {
+ g_string_free(str, TRUE);
+ return G_IO_STATUS_ERROR;
+ }
+ if (rc == G_IO_STATUS_EOF)
+ break;
+
+ g_string_append_len(str, data, data_len);
+ }
+
+ if (out_len)
+ *out_len = str->len;
+ *ret = g_string_free(str, FALSE);
+ return G_IO_STATUS_NORMAL;
+}
+
+/** @memberof teco_eol_reader_t */
+void
+teco_eol_reader_clear(teco_eol_reader_t *ctx)
+{
+ if (ctx->read_cb == teco_eol_reader_read_gio && ctx->gio.channel)
+ g_io_channel_unref(ctx->gio.channel);
+}
+
+static inline void
+teco_eol_writer_init(teco_eol_writer_t *ctx, gint eol_mode)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->eol_seq = teco_eol_get_seq(eol_mode);
+ ctx->eol_seq_len = strlen(ctx->eol_seq);
+}
+
+static gssize
+teco_eol_writer_write_gio(teco_eol_writer_t *ctx, const gchar *buffer, gsize buffer_len, GError **error)
+{
+ gsize bytes_written;
+
+ switch (g_io_channel_write_chars(ctx->gio.channel, buffer, buffer_len,
+ &bytes_written, error)) {
+ case G_IO_STATUS_ERROR:
+ return -1;
+ case G_IO_STATUS_EOF:
+ case G_IO_STATUS_NORMAL:
+ case G_IO_STATUS_AGAIN:
+ break;
+ }
+
+ return bytes_written;
+}
+
+/** @memberof teco_eol_writer_t */
+void
+teco_eol_writer_init_gio(teco_eol_writer_t *ctx, gint eol_mode, GIOChannel *channel)
+{
+ teco_eol_writer_init(ctx, eol_mode);
+ ctx->write_cb = teco_eol_writer_write_gio;
+ teco_eol_writer_set_channel(ctx, channel);
+}
+
+static gssize
+teco_eol_writer_write_mem(teco_eol_writer_t *ctx, const gchar *buffer, gsize buffer_len, GError **error)
+{
+ g_string_append_len(ctx->mem.str, buffer, buffer_len);
+ return buffer_len;
+}
+
+/**
+ * @note Currently uses GString instead of teco_string_t to allow making use
+ * of preallocation.
+ * On the other hand GString has a higher overhead.
+ *
+ * @memberof teco_eol_writer_t
+ */
+void
+teco_eol_writer_init_mem(teco_eol_writer_t *ctx, gint eol_mode, GString *str)
+{
+ teco_eol_writer_init(ctx, eol_mode);
+ ctx->write_cb = teco_eol_writer_write_mem;
+ ctx->mem.str = str;
+}
+
+/**
+ * Perform EOL-normalization on a buffer (if enabled) and
+ * pass it to the underlying data sink.
+ *
+ * This can be called repeatedly to transform a larger
+ * document - the buffer provided does not have to be
+ * well-formed with regard to EOL sequences.
+ *
+ * @param ctx The EOL Reader object.
+ * @param buffer The buffer to convert.
+ * @param buffer_len The length of the data in buffer.
+ * @param error A GError.
+ * @return The number of bytes consumed/converted from buffer.
+ * A value smaller than 0 is returned in case of errors.
+ *
+ * @memberof teco_eol_writer_t
+ */
+gssize
+teco_eol_writer_convert(teco_eol_writer_t *ctx, const gchar *buffer, gsize buffer_len, GError **error)
+{
+ if (!(teco_ed & TECO_ED_AUTOEOL))
+ /*
+ * Write without EOL-translation:
+ * `state` is not required
+ * NOTE: This throws in case of errors
+ */
+ return ctx->write_cb(ctx, buffer, buffer_len, error);
+
+ /*
+ * Write to stream with EOL-translation.
+ * The document's EOL mode tells us what was guessed
+ * when its content was read in (presumably from a file)
+ * but might have been changed manually by the user.
+ * NOTE: This code assumes that the output stream is
+ * buffered, since otherwise it would be slower
+ * (has been benchmarked).
+ * NOTE: The loop is executed for every character
+ * in `buffer` and has been optimized for minimal
+ * function (i.e. GIOChannel) calls.
+ */
+ guint i = 0;
+ gsize bytes_written = 0;
+ if (ctx->state == TECO_EOL_STATE_WRITE_LF) {
+ /* complete writing a CRLF sequence */
+ gssize rc = ctx->write_cb(ctx, "\n", 1, error);
+ if (rc < 1)
+ /* nothing written or error */
+ return rc;
+ ctx->state = TECO_EOL_STATE_START;
+ bytes_written++;
+ i++;
+ }
+
+ guint block_start = i;
+ gssize block_written;
+ while (i < buffer_len) {
+ switch (buffer[i]) {
+ case '\n':
+ if (ctx->last_c == '\r') {
+ /* EOL sequence already written */
+ bytes_written++;
+ block_start = i+1;
+ break;
+ }
+ /* fall through */
+ case '\r':
+ block_written = ctx->write_cb(ctx, buffer+block_start, i-block_start, error);
+ if (block_written < 0)
+ return -1;
+ bytes_written += block_written;
+ if (block_written < i-block_start)
+ return bytes_written;
+
+ block_written = ctx->write_cb(ctx, ctx->eol_seq, ctx->eol_seq_len, error);
+ if (block_written < 0)
+ return -1;
+ if (block_written == 0)
+ return bytes_written;
+ if (block_written < ctx->eol_seq_len) {
+ /* incomplete EOL seq - we have written CR of CRLF */
+ ctx->state = TECO_EOL_STATE_WRITE_LF;
+ return bytes_written;
+ }
+ bytes_written++;
+
+ block_start = i+1;
+ break;
+ }
+
+ ctx->last_c = buffer[i++];
+ }
+
+ /*
+ * Write out remaining block (i.e. line)
+ */
+ gssize rc = ctx->write_cb(ctx, buffer+block_start, buffer_len-block_start, error);
+ return rc < 0 ? -1 : bytes_written + rc;
+}
+
+/** @memberof teco_eol_writer_t */
+void
+teco_eol_writer_clear(teco_eol_writer_t *ctx)
+{
+ if (ctx->write_cb == teco_eol_writer_write_gio && ctx->gio.channel)
+ g_io_channel_unref(ctx->gio.channel);
+}
diff --git a/src/eol.cpp b/src/eol.cpp
deleted file mode 100644
index 2dea3ef..0000000
--- a/src/eol.cpp
+++ /dev/null
@@ -1,359 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 "error.h"
-#include "eol.h"
-
-namespace SciTECO {
-
-/**
- * Read data with automatic EOL translation.
- *
- * This gets the next data block from the converter
- * implementation, performs EOL translation (if enabled)
- * in a more or less efficient manner and returns
- * a chunk of EOL-normalized data.
- *
- * Since the underlying data source may have to be
- * queried repeatedly and because EOLReader avoids
- * reassembling the EOL-normalized data by returning
- * references into the modified data source, it is
- * necessary to call this function repeatedly until
- * it returns NULL.
- *
- * Errors reading the data source are propagated
- * (as exceptions).
- *
- * @param data_len The length of the data chunk returned
- * by this function. Set on return.
- * @return A pointer to a chunk of EOL-normalized
- * data of length data_len.
- * It is NOT null-terminated.
- * NULL is returned when all data has been converted.
- */
-const gchar *
-EOLReader::convert(gsize &data_len)
-{
- if (last_char < 0) {
- /* a CRLF was last translated */
- block_len++;
- last_char = '\n';
- }
- offset += block_len;
-
- if (offset == read_len) {
- offset = 0;
-
- /*
- * NOTE: This throws in case of errors
- */
- if (!this->read(buffer, read_len)) {
- /* EOF */
- if (last_char == '\r') {
- /*
- * Very last character read is CR.
- * If this is the only EOL so far, the
- * EOL style is MAC.
- * This is also executed if auto-eol is disabled
- * but it doesn't hurt.
- */
- if (eol_style < 0)
- eol_style = SC_EOL_CR;
- else if (eol_style != SC_EOL_CR)
- eol_style_inconsistent = TRUE;
- }
-
- return NULL;
- }
-
- if (!(Flags::ed & Flags::ED_AUTOEOL)) {
- /*
- * No EOL translation - always return entire
- * buffer
- */
- data_len = block_len = read_len;
- return buffer;
- }
- }
-
- /*
- * Return data with automatic EOL translation.
- * Every EOL sequence is normalized to LF and
- * the first sequence determines the documents
- * EOL style.
- * This loop is executed for every byte of the
- * file/stream, so it was important to optimize
- * it. Specifically, the number of returns
- * is minimized by keeping a pointer to
- * the beginning of a block of data in the buffer
- * which already has LFs (offset).
- * Mac EOLs can be converted to UNIX EOLs directly
- * in the buffer.
- * So if their EOLs are consistent, the function
- * will return one block for the entire buffer.
- * When reading a file with DOS EOLs, there will
- * be one call per line which is significantly slower.
- */
- for (guint i = offset; i < read_len; i++) {
- switch (buffer[i]) {
- case '\n':
- if (last_char == '\r') {
- if (eol_style < 0)
- eol_style = SC_EOL_CRLF;
- else if (eol_style != SC_EOL_CRLF)
- eol_style_inconsistent = TRUE;
-
- /*
- * Return block. CR has already
- * been made LF in `buffer`.
- */
- data_len = block_len = i-offset;
- /* next call will skip the CR */
- last_char = -1;
- return buffer + offset;
- }
-
- if (eol_style < 0)
- eol_style = SC_EOL_LF;
- else if (eol_style != SC_EOL_LF)
- eol_style_inconsistent = TRUE;
- /*
- * No conversion necessary and no need to
- * return block yet.
- */
- last_char = '\n';
- break;
-
- case '\r':
- if (last_char == '\r') {
- if (eol_style < 0)
- eol_style = SC_EOL_CR;
- else if (eol_style != SC_EOL_CR)
- eol_style_inconsistent = TRUE;
- }
-
- /*
- * Convert CR to LF in `buffer`.
- * This way more than one line using
- * Mac EOLs can be returned at once.
- */
- buffer[i] = '\n';
- last_char = '\r';
- break;
-
- default:
- if (last_char == '\r') {
- if (eol_style < 0)
- eol_style = SC_EOL_CR;
- else if (eol_style != SC_EOL_CR)
- eol_style_inconsistent = TRUE;
- }
- last_char = buffer[i];
- break;
- }
- }
-
- /*
- * Return remaining block.
- * With UNIX/MAC EOLs, this will usually be the
- * entire `buffer`
- */
- data_len = block_len = read_len-offset;
- return buffer + offset;
-}
-
-bool
-EOLReaderGIO::read(gchar *buffer, gsize &read_len)
-{
- GError *error = NULL;
-
- switch (g_io_channel_read_chars(channel, buffer,
- sizeof(EOLReaderGIO::buffer),
- &read_len, &error)) {
- case G_IO_STATUS_ERROR:
- throw GlibError(error);
- case G_IO_STATUS_EOF:
- return false;
- case G_IO_STATUS_NORMAL:
- case G_IO_STATUS_AGAIN:
- break;
- }
-
- return true;
-}
-
-bool
-EOLReaderMem::read(gchar *buffer, gsize &read_len)
-{
- read_len = buffer_len;
- buffer_len = 0;
- /*
- * On the first call, returns true,
- * later false (no more data).
- */
- return read_len != 0;
-}
-
-/*
- * This could be in EOLReader as well, but this way, we
- * make use of the buffer_len to avoid unnecessary allocations.
- */
-gchar *
-EOLReaderMem::convert_all(gsize *out_len)
-{
- GString *str = g_string_sized_new(buffer_len);
- const gchar *data;
- gsize data_len;
-
- try {
- while ((data = convert(data_len)))
- g_string_append_len(str, data, data_len);
- } catch (...) {
- g_string_free(str, TRUE);
- throw; /* forward */
- }
-
- if (out_len)
- *out_len = str->len;
- return g_string_free(str, FALSE);
-}
-
-/**
- * Perform EOL-normalization on a buffer (if enabled) and
- * pass it to the underlying data sink.
- *
- * This can be called repeatedly to transform a larger
- * document - the buffer provided does not have to be
- * well-formed with regard to EOL sequences.
- *
- * @param buffer The buffer to convert.
- * @param buffer_len The length of the data in buffer.
- * @return The number of bytes consumed/converted from buffer.
- */
-gsize
-EOLWriter::convert(const gchar *buffer, gsize buffer_len)
-{
- gsize bytes_written;
- guint i = 0;
- guint block_start;
- gsize block_written;
-
- if (!(Flags::ed & Flags::ED_AUTOEOL))
- /*
- * Write without EOL-translation:
- * `state` is not required
- * NOTE: This throws in case of errors
- */
- return this->write(buffer, buffer_len);
-
- /*
- * Write to stream with EOL-translation.
- * The document's EOL mode tells us what was guessed
- * when its content was read in (presumably from a file)
- * but might have been changed manually by the user.
- * NOTE: This code assumes that the output stream is
- * buffered, since otherwise it would be slower
- * (has been benchmarked).
- * NOTE: The loop is executed for every character
- * in `buffer` and has been optimized for minimal
- * function (i.e. GIOChannel) calls.
- */
- bytes_written = 0;
- if (state == STATE_WRITE_LF) {
- /* complete writing a CRLF sequence */
- if (this->write("\n", 1) < 1)
- return 0;
- state = STATE_START;
- bytes_written++;
- i++;
- }
-
- block_start = i;
- while (i < buffer_len) {
- switch (buffer[i]) {
- case '\n':
- if (last_c == '\r') {
- /* EOL sequence already written */
- bytes_written++;
- block_start = i+1;
- break;
- }
- /* fall through */
- case '\r':
- block_written = this->write(buffer+block_start, i-block_start);
- bytes_written += block_written;
- if (block_written < i-block_start)
- return bytes_written;
-
- block_written = this->write(eol_seq, eol_seq_len);
- if (block_written == 0)
- return bytes_written;
- if (block_written < eol_seq_len) {
- /* incomplete EOL seq - we have written CR of CRLF */
- state = STATE_WRITE_LF;
- return bytes_written;
- }
- bytes_written++;
-
- block_start = i+1;
- break;
- }
-
- last_c = buffer[i++];
- }
-
- /*
- * Write out remaining block (i.e. line)
- */
- bytes_written += this->write(buffer+block_start, buffer_len-block_start);
- return bytes_written;
-}
-
-gsize
-EOLWriterGIO::write(const gchar *buffer, gsize buffer_len)
-{
- gsize bytes_written;
- GError *error = NULL;
-
- switch (g_io_channel_write_chars(channel, buffer, buffer_len,
- &bytes_written, &error)) {
- case G_IO_STATUS_ERROR:
- throw GlibError(error);
- case G_IO_STATUS_EOF:
- case G_IO_STATUS_NORMAL:
- case G_IO_STATUS_AGAIN:
- break;
- }
-
- return bytes_written;
-}
-
-gsize
-EOLWriterMem::write(const gchar *buffer, gsize buffer_len)
-{
- g_string_append_len(str, buffer, buffer_len);
- return buffer_len;
-}
-
-} /* namespace SciTECO */
diff --git a/src/eol.h b/src/eol.h
index 7d6c527..4a0c144 100644
--- a/src/eol.h
+++ b/src/eol.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,148 +14,106 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __EOL_H
-#define __EOL_H
-
-#include <string.h>
+#pragma once
#include <glib.h>
#include "sciteco.h"
-#include "memory.h"
-namespace SciTECO {
+const gchar *teco_eol_get_seq(gint eol_mode);
+
+typedef struct teco_eol_reader_t teco_eol_reader_t;
-class EOLReader : public Object {
- gchar *buffer;
+struct teco_eol_reader_t {
gsize read_len;
guint offset;
gsize block_len;
gint last_char;
-public:
gint eol_style;
gboolean eol_style_inconsistent;
- EOLReader(gchar *_buffer)
- : buffer(_buffer),
- read_len(0), offset(0), block_len(0),
- last_char(0), eol_style(-1),
- eol_style_inconsistent(FALSE) {}
- virtual ~EOLReader() {}
-
- const gchar *convert(gsize &data_len);
-
-protected:
- virtual bool read(gchar *buffer, gsize &read_len) = 0;
+ GIOStatus (*read_cb)(teco_eol_reader_t *ctx, gsize *read_len, GError **error);
+
+ /*
+ * NOTE: This wastes some bytes for "memory" readers,
+ * but avoids inheritance.
+ */
+ union {
+ struct {
+ gchar buffer[1024];
+ GIOChannel *channel;
+ } gio;
+
+ struct {
+ gchar *buffer;
+ gsize len;
+ } mem;
+ };
};
-class EOLReaderGIO : public EOLReader {
- gchar buffer[1024];
- GIOChannel *channel;
-
- bool read(gchar *buffer, gsize &read_len);
-
-public:
- EOLReaderGIO(GIOChannel *_channel = NULL)
- : EOLReader(buffer), channel(NULL)
- {
- set_channel(_channel);
- }
-
- inline void
- set_channel(GIOChannel *_channel = NULL)
- {
- if (channel)
- g_io_channel_unref(channel);
- channel = _channel;
- if (channel)
- g_io_channel_ref(channel);
- }
-
- ~EOLReaderGIO()
- {
- set_channel();
- }
-};
+void teco_eol_reader_init_gio(teco_eol_reader_t *ctx, GIOChannel *channel);
+void teco_eol_reader_init_mem(teco_eol_reader_t *ctx, gchar *buffer, gsize len);
-class EOLReaderMem : public EOLReader {
- gsize buffer_len;
+/** @memberof teco_eol_reader_t */
+static inline void
+teco_eol_reader_set_channel(teco_eol_reader_t *ctx, GIOChannel *channel)
+{
+ if (ctx->gio.channel)
+ g_io_channel_unref(ctx->gio.channel);
+ ctx->gio.channel = channel;
+ if (ctx->gio.channel)
+ g_io_channel_ref(ctx->gio.channel);
+}
- bool read(gchar *buffer, gsize &read_len);
+GIOStatus teco_eol_reader_convert(teco_eol_reader_t *ctx, gchar **ret, gsize *data_len, GError **error);
+GIOStatus teco_eol_reader_convert_all(teco_eol_reader_t *ctx, gchar **ret, gsize *out_len, GError **error);
-public:
- EOLReaderMem(gchar *buffer, gsize _buffer_len)
- : EOLReader(buffer), buffer_len(_buffer_len) {}
+void teco_eol_reader_clear(teco_eol_reader_t *ctx);
- gchar *convert_all(gsize *out_len = NULL);
-};
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_eol_reader_t, teco_eol_reader_clear);
+
+typedef struct teco_eol_writer_t teco_eol_writer_t;
-class EOLWriter : public Object {
+struct teco_eol_writer_t {
enum {
- STATE_START = 0,
- STATE_WRITE_LF
+ TECO_EOL_STATE_START = 0,
+ TECO_EOL_STATE_WRITE_LF
} state;
gchar last_c;
const gchar *eol_seq;
gsize eol_seq_len;
-public:
- EOLWriter(gint eol_mode) : state(STATE_START), last_c('\0')
- {
- eol_seq = get_eol_seq(eol_mode);
- eol_seq_len = strlen(eol_seq);
- }
- virtual ~EOLWriter() {}
+ gssize (*write_cb)(teco_eol_writer_t *ctx, const gchar *buffer, gsize buffer_len, GError **error);
- gsize convert(const gchar *buffer, gsize buffer_len);
+ union {
+ struct {
+ GIOChannel *channel;
+ } gio;
-protected:
- virtual gsize write(const gchar *buffer, gsize buffer_len) = 0;
+ struct {
+ GString *str;
+ } mem;
+ };
};
-class EOLWriterGIO : public EOLWriter {
- GIOChannel *channel;
-
- gsize write(const gchar *buffer, gsize buffer_len);
-
-public:
- EOLWriterGIO(gint eol_mode)
- : EOLWriter(eol_mode), channel(NULL) {}
-
- EOLWriterGIO(GIOChannel *_channel, gint eol_mode)
- : EOLWriter(eol_mode), channel(NULL)
- {
- set_channel(_channel);
- }
-
- inline void
- set_channel(GIOChannel *_channel = NULL)
- {
- if (channel)
- g_io_channel_unref(channel);
- channel = _channel;
- if (channel)
- g_io_channel_ref(channel);
- }
-
- ~EOLWriterGIO()
- {
- set_channel();
- }
-};
+void teco_eol_writer_init_gio(teco_eol_writer_t *ctx, gint eol_mode, GIOChannel *channel);
+void teco_eol_writer_init_mem(teco_eol_writer_t *ctx, gint eol_mode, GString *str);
-class EOLWriterMem : public EOLWriter {
- GString *str;
+/** @memberof teco_eol_writer_t */
+static inline void
+teco_eol_writer_set_channel(teco_eol_writer_t *ctx, GIOChannel *channel)
+{
+ if (ctx->gio.channel)
+ g_io_channel_unref(ctx->gio.channel);
+ ctx->gio.channel = channel;
+ if (ctx->gio.channel)
+ g_io_channel_ref(ctx->gio.channel);
+}
- gsize write(const gchar *buffer, gsize buffer_len);
-
-public:
- EOLWriterMem(GString *_str, gint eol_mode)
- : EOLWriter(eol_mode), str(_str) {}
-};
+gssize teco_eol_writer_convert(teco_eol_writer_t *ctx, const gchar *buffer,
+ gsize buffer_len, GError **error);
-} /* namespace SciTECO */
+void teco_eol_writer_clear(teco_eol_writer_t *ctx);
-#endif
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_eol_writer_t, teco_eol_writer_clear);
diff --git a/src/error.c b/src/error.c
new file mode 100644
index 0000000..6a0e10f
--- /dev/null
+++ b/src/error.c
@@ -0,0 +1,173 @@
+/*
+ * 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 <string.h>
+
+#include <glib.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "interface.h"
+#include "list.h"
+#include "error.h"
+
+guint teco_error_return_args = 0;
+
+/*
+ * FIXME: Does this have to be stored in teco_machine_main_t?
+ * Probably becomes clear once we implement error handling by macros.
+ */
+guint teco_error_pos = 0, teco_error_line = 0, teco_error_column = 0;
+
+void
+teco_error_set_coord(const gchar *str, guint pos)
+{
+ teco_error_pos = pos;
+ teco_string_get_coord(str, pos, &teco_error_line, &teco_error_column);
+}
+
+typedef enum {
+ TECO_FRAME_QREG,
+ TECO_FRAME_FILE,
+ TECO_FRAME_EDHOOK,
+ TECO_FRAME_TOPLEVEL
+} teco_frame_type_t;
+
+typedef struct teco_frame_t {
+ teco_stailq_entry_t entry;
+
+ teco_frame_type_t type;
+
+ guint pos, line, column;
+
+ /*
+ * NOTE: This is currently sufficient to describe all
+ * frame types. Otherwise, add an union.
+ */
+ gchar name[];
+} teco_frame_t;
+
+/**
+ * List of teco_frame_t describing the stack frames.
+ *
+ * Stack frames are collected deliberately unformatted
+ * since there are future applications where displaying
+ * a stack frame will not be necessary (e.g. error handled
+ * by SciTECO macro).
+ * Preformatting all stack frames would be very costly.
+ */
+static teco_stailq_head_t teco_frames = TECO_STAILQ_HEAD_INITIALIZER(&teco_frames);
+
+void
+teco_error_display_short(const GError *error)
+{
+ teco_interface_msg(TECO_MSG_ERROR, "%s (at %d)",
+ error->message, teco_error_pos);
+}
+
+void
+teco_error_display_full(const GError *error)
+{
+ teco_interface_msg(TECO_MSG_ERROR, "%s", error->message);
+
+ guint nr = 0;
+
+ for (teco_stailq_entry_t *cur = teco_frames.first; cur != NULL; cur = cur->next) {
+ teco_frame_t *frame = (teco_frame_t *)cur;
+
+ switch (frame->type) {
+ case TECO_FRAME_QREG:
+ teco_interface_msg(TECO_MSG_INFO,
+ "#%d in Q-Register \"%s\" at %d (%d:%d)",
+ nr, frame->name, frame->pos, frame->line, frame->column);
+ break;
+ case TECO_FRAME_FILE:
+ teco_interface_msg(TECO_MSG_INFO,
+ "#%d in file \"%s\" at %d (%d:%d)",
+ nr, frame->name, frame->pos, frame->line, frame->column);
+ break;
+ case TECO_FRAME_EDHOOK:
+ teco_interface_msg(TECO_MSG_INFO,
+ "#%d in \"%s\" hook execution",
+ nr, frame->name);
+ break;
+ case TECO_FRAME_TOPLEVEL:
+ teco_interface_msg(TECO_MSG_INFO,
+ "#%d in toplevel macro at %d (%d:%d)",
+ nr, frame->pos, frame->line, frame->column);
+ break;
+ }
+
+ nr++;
+ }
+}
+
+static teco_frame_t *
+teco_error_add_frame(teco_frame_type_t type, gsize size)
+{
+ teco_frame_t *frame = g_malloc(sizeof(teco_frame_t) + size);
+ frame->type = type;
+ frame->pos = teco_error_pos;
+ frame->line = teco_error_line;
+ frame->column = teco_error_column;
+ teco_stailq_insert_tail(&teco_frames, &frame->entry);
+
+ return frame;
+}
+
+void
+teco_error_add_frame_qreg(const gchar *name, gsize len)
+{
+ g_autofree gchar *name_printable = teco_string_echo(name, len);
+ teco_frame_t *frame = teco_error_add_frame(TECO_FRAME_QREG, strlen(name_printable) + 1);
+ strcpy(frame->name, name_printable);
+}
+
+void
+teco_error_add_frame_file(const gchar *name)
+{
+ teco_frame_t *frame = teco_error_add_frame(TECO_FRAME_FILE, strlen(name) + 1);
+ strcpy(frame->name, name);
+}
+
+void
+teco_error_add_frame_edhook(const gchar *type)
+{
+ teco_frame_t *frame = teco_error_add_frame(TECO_FRAME_EDHOOK, strlen(type) + 1);
+ strcpy(frame->name, type);
+}
+
+void
+teco_error_add_frame_toplevel(void)
+{
+ teco_error_add_frame(TECO_FRAME_TOPLEVEL, 0);
+}
+
+#ifndef NDEBUG
+__attribute__((destructor))
+#endif
+void
+teco_error_clear_frames(void)
+{
+ teco_stailq_entry_t *entry;
+ while ((entry = teco_stailq_remove_head(&teco_frames)))
+ g_free(entry);
+}
diff --git a/src/error.cpp b/src/error.cpp
deleted file mode 100644
index f960a54..0000000
--- a/src/error.cpp
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <stdarg.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include "sciteco.h"
-#include "interface.h"
-#include "error.h"
-
-namespace SciTECO {
-
-Error::Frame *
-Error::QRegFrame::copy() const
-{
- Frame *frame = new QRegFrame(name);
-
- frame->pos = pos;
- frame->line = line;
- frame->column = column;
-
- return frame;
-}
-
-void
-Error::QRegFrame::display(gint nr)
-{
- interface.msg(InterfaceCurrent::MSG_INFO,
- "#%d in Q-Register \"%s\" at %d (%d:%d)",
- nr, name, pos, line, column);
-}
-
-Error::Frame *
-Error::FileFrame::copy() const
-{
- Frame *frame = new FileFrame(name);
-
- frame->pos = pos;
- frame->line = line;
- frame->column = column;
-
- return frame;
-}
-
-void
-Error::FileFrame::display(gint nr)
-{
- interface.msg(InterfaceCurrent::MSG_INFO,
- "#%d in file \"%s\" at %d (%d:%d)",
- nr, name, pos, line, column);
-}
-
-Error::Frame *
-Error::EDHookFrame::copy() const
-{
- /* coordinates do not matter */
- return new EDHookFrame(type);
-}
-
-void
-Error::EDHookFrame::display(gint nr)
-{
- interface.msg(InterfaceCurrent::MSG_INFO,
- "#%d in \"%s\" hook execution",
- nr, type);
-}
-
-Error::Frame *
-Error::ToplevelFrame::copy() const
-{
- Frame *frame = new ToplevelFrame();
-
- frame->pos = pos;
- frame->line = line;
- frame->column = column;
-
- return frame;
-}
-
-void
-Error::ToplevelFrame::display(gint nr)
-{
- interface.msg(InterfaceCurrent::MSG_INFO,
- "#%d in toplevel macro at %d (%d:%d)",
- nr, pos, line, column);
-}
-
-Error::Error(const gchar *fmt, ...)
- : frames(NULL), pos(0), line(0), column(0)
-{
- va_list ap;
-
- va_start(ap, fmt);
- description = g_strdup_vprintf(fmt, ap);
- va_end(ap);
-}
-
-Error::Error(const Error &inst)
- : description(g_strdup(inst.description)),
- pos(inst.pos), line(inst.line), column(inst.column)
-{
- /* shallow copy of the frames */
- frames = g_slist_copy(inst.frames);
-
- for (GSList *cur = frames; cur; cur = g_slist_next(cur)) {
- Frame *frame = (Frame *)cur->data;
- cur->data = frame->copy();
- }
-}
-
-void
-Error::add_frame(Frame *frame)
-{
- frame->pos = pos;
- frame->line = line;
- frame->column = column;
-
- frames = g_slist_prepend(frames, frame);
-}
-
-void
-Error::display_short(void)
-{
- interface.msg(InterfaceCurrent::MSG_ERROR,
- "%s (at %d)", description, pos);
-}
-
-void
-Error::display_full(void)
-{
- gint nr = 0;
-
- interface.msg(InterfaceCurrent::MSG_ERROR, "%s", description);
-
- frames = g_slist_reverse(frames);
- for (GSList *cur = frames; cur; cur = g_slist_next(cur)) {
- Frame *frame = (Frame *)cur->data;
-
- frame->display(nr++);
- }
-}
-
-Error::~Error()
-{
- g_free(description);
- for (GSList *cur = frames; cur; cur = g_slist_next(cur))
- delete (Frame *)cur->data;
- g_slist_free(frames);
-}
-
-} /* namespace SciTECO */
diff --git a/src/error.h b/src/error.h
index a12a76b..16136b9 100644
--- a/src/error.h
+++ b/src/error.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,205 +14,135 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __ERROR_H
-#define __ERROR_H
-
-#include <exception>
-#include <typeinfo>
+#pragma once
#include <glib.h>
-#include <glib/gprintf.h>
#include "sciteco.h"
-#include "memory.h"
#include "string-utils.h"
-namespace SciTECO {
-
-/**
- * Thrown as exception to signify that program
- * should be terminated.
- */
-class Quit : public Object {};
-
-/**
- * Thrown as exception to cause a macro to
- * return or a command-line termination.
+/*
+ * FIXME: Introducing a second error quark might be useful to distinguish
+ * errors that can be cought by SciTECO macros from errors that must always
+ * propagate (TECO_ERROR_QUIT, TECO_ERROR_RETURN).
+ * On the other hand, these error codes will probably soon become obsolete
+ * when the SciTECO call stack no longer corresponds with the C callstack.
*/
-class Return : public Object {
-public:
- guint args;
-
- Return(guint _args = 0) : args(_args) {}
-};
-
-class Error : public Object {
- GSList *frames;
-
-public:
- gchar *description;
- gint pos;
- gint line, column;
-
- class Frame : public Object {
- public:
- gint pos;
- gint line, column;
-
- virtual Frame *copy() const = 0;
- virtual ~Frame() {}
-
- virtual void display(gint nr) = 0;
- };
-
- class QRegFrame : public Frame {
- gchar *name;
-
- public:
- QRegFrame(const gchar *_name)
- : name(g_strdup(_name)) {}
-
- Frame *copy() const;
-
- ~QRegFrame()
- {
- g_free(name);
- }
-
- void display(gint nr);
- };
-
- class FileFrame : public Frame {
- gchar *name;
-
- public:
- FileFrame(const gchar *_name)
- : name(g_strdup(_name)) {}
-
- Frame *copy() const;
-
- ~FileFrame()
- {
- g_free(name);
- }
-
- void display(gint nr);
- };
-
- class EDHookFrame : public Frame {
- gchar *type;
-
- public:
- EDHookFrame(const gchar *_type)
- : type(g_strdup(_type)) {}
-
- Frame *copy() const;
-
- ~EDHookFrame()
- {
- g_free(type);
- }
-
- void display(gint nr);
- };
-
- class ToplevelFrame : public Frame {
- public:
- Frame *copy() const;
-
- void display(gint nr);
- };
-
- Error(const gchar *fmt, ...) G_GNUC_PRINTF(2, 3);
- Error(const Error &inst);
- ~Error();
-
- inline void
- set_coord(const gchar *str, gint _pos)
- {
- pos = _pos;
- String::get_coord(str, pos, line, column);
- }
-
- void add_frame(Frame *frame);
-
- void display_short(void);
- void display_full(void);
-};
+#define TECO_ERROR (g_quark_from_static_string("sciteco-error-quark"))
-class StdError : public Error {
-public:
- StdError(const gchar *type, const std::exception &error)
- : Error("%s: %s", type, error.what()) {}
- StdError(const std::exception &error)
- : Error("%s: %s", typeid(error).name(), error.what()) {}
-};
+typedef enum {
+ /** Default (catch-all) error code */
+ TECO_ERROR_FAILED = 0,
-class GlibError : public Error {
-public:
- /**
- * Construct error for glib's GError.
- * Ownership of the error's resources is passed
- * the GlibError object.
+ /*
+ * FIXME: Subsume all these errors under TECO_ERROR_SYNTAX or TECO_ERROR_FAIL?
+ * They will mainly be different in their error message.
*/
- GlibError(GError *gerror)
- : Error("%s", gerror->message)
- {
- g_error_free(gerror);
- }
-};
-
-class SyntaxError : public Error {
-public:
- SyntaxError(gchar chr)
- : Error("Syntax error \"%c\" (%d)", chr, chr) {}
-};
-
-class ArgExpectedError : public Error {
-public:
- ArgExpectedError(const gchar *cmd)
- : Error("Argument expected for <%s>", cmd) {}
- ArgExpectedError(gchar cmd)
- : Error("Argument expected for <%c>", cmd) {}
-};
-
-class MoveError : public Error {
-public:
- MoveError(const gchar *cmd)
- : Error("Attempt to move pointer off page with <%s>",
- cmd) {}
- MoveError(gchar cmd)
- : Error("Attempt to move pointer off page with <%c>",
- cmd) {}
-};
-
-class RangeError : public Error {
-public:
- RangeError(const gchar *cmd)
- : Error("Invalid range specified for <%s>", cmd) {}
- RangeError(gchar cmd)
- : Error("Invalid range specified for <%c>", cmd) {}
-};
-
-class InvalidQRegError : public Error {
-public:
- InvalidQRegError(const gchar *name, bool local = false)
- : Error("Invalid Q-Register \"%s%s\"",
- local ? "." : "", name) {}
- InvalidQRegError(gchar name, bool local = false)
- : Error("Invalid Q-Register \"%s%c\"",
- local ? "." : "", name) {}
-};
-
-class QRegOpUnsupportedError : public Error {
-public:
- QRegOpUnsupportedError(const gchar *name, bool local = false)
- : Error("Operation unsupported on "
- "Q-Register \"%s%s\"",
- local ? "." : "", name) {}
-};
-
-} /* namespace SciTECO */
-
-#endif
+ TECO_ERROR_SYNTAX,
+ TECO_ERROR_ARGEXPECTED,
+ TECO_ERROR_MOVE,
+ TECO_ERROR_WORDS,
+ TECO_ERROR_RANGE,
+ TECO_ERROR_INVALIDQREG,
+ TECO_ERROR_QREGOPUNSUPPORTED,
+ TECO_ERROR_QREGCONTAINSNULL,
+ TECO_ERROR_MEMLIMIT,
+
+ /** Interrupt current operation */
+ TECO_ERROR_INTERRUPTED,
+
+ /** Thrown to signal command line replacement */
+ TECO_ERROR_CMDLINE = 0x80,
+ /** Thrown as exception to cause a macro to return or a command-line termination. */
+ TECO_ERROR_RETURN,
+ /** Thrown as exception to signify that program should be terminated. */
+ TECO_ERROR_QUIT
+} teco_error_t;
+
+static inline void
+teco_error_syntax_set(GError **error, gchar chr)
+{
+ g_set_error(error, TECO_ERROR, TECO_ERROR_SYNTAX,
+ "Syntax error \"%c\" (%d)", chr, chr);
+}
+
+static inline void
+teco_error_argexpected_set(GError **error, const gchar *cmd)
+{
+ g_set_error(error, TECO_ERROR, TECO_ERROR_ARGEXPECTED,
+ "Argument expected for <%s>", cmd);
+}
+
+static inline void
+teco_error_move_set(GError **error, const gchar *cmd)
+{
+ g_set_error(error, TECO_ERROR, TECO_ERROR_MOVE,
+ "Attempt to move pointer off page with <%s>", cmd);
+}
+
+static inline void
+teco_error_words_set(GError **error, const gchar *cmd)
+{
+ g_set_error(error, TECO_ERROR, TECO_ERROR_MOVE,
+ "Not enough words to delete with <%s>", cmd);
+}
+
+static inline void
+teco_error_range_set(GError **error, const gchar *cmd)
+{
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Invalid range specified for <%s>", cmd);
+}
+
+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);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_INVALIDQREG,
+ "Invalid %sQ-Register \"%s\"", local ? "local " : "", name_printable);
+}
+
+static inline void
+teco_error_qregopunsupported_set(GError **error, const gchar *name, gsize len, gboolean local)
+{
+ g_autofree gchar *name_printable = teco_string_echo(name, len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_QREGOPUNSUPPORTED,
+ "Operation unsupported on %sQ-Register \"%s\"", local ? "local " : "", name_printable);
+}
+
+static inline void
+teco_error_qregcontainsnull_set(GError **error, const gchar *name, gsize len, gboolean local)
+{
+ g_autofree gchar *name_printable = teco_string_echo(name, len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_QREGCONTAINSNULL,
+ "%sQ-Register \"%s\" contains null-byte", local ? "Local " : "", name_printable);
+}
+
+static inline void
+teco_error_interrupted_set(GError **error)
+{
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_INTERRUPTED, "Interrupted");
+}
+
+extern guint teco_error_return_args;
+
+static inline void
+teco_error_return_set(GError **error, guint args)
+{
+ teco_error_return_args = args;
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_RETURN, "");
+}
+
+extern guint teco_error_pos, teco_error_line, teco_error_column;
+
+void teco_error_set_coord(const gchar *str, guint pos);
+
+void teco_error_display_short(const GError *error);
+void teco_error_display_full(const GError *error);
+
+void teco_error_add_frame_qreg(const gchar *name, gsize len);
+void teco_error_add_frame_file(const gchar *name);
+void teco_error_add_frame_edhook(const gchar *type);
+void teco_error_add_frame_toplevel(void);
+
+void teco_error_clear_frames(void);
diff --git a/src/expressions.c b/src/expressions.c
new file mode 100644
index 0000000..9a00fee
--- /dev/null
+++ b/src/expressions.c
@@ -0,0 +1,384 @@
+/*
+ * 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 "sciteco.h"
+#include "error.h"
+#include "undo.h"
+#include "expressions.h"
+
+/*
+ * Number and operator stacks are static, so
+ * they can be passed to the undo token constructors.
+ * This is OK since we're currently singleton.
+ */
+static GArray *teco_numbers;
+
+TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(teco_numbers, teco_int_t);
+TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(teco_numbers);
+
+static GArray *teco_operators;
+
+TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(teco_operators, teco_operator_t);
+TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(teco_operators);
+
+static gboolean teco_expressions_calc(GError **error);
+
+static void __attribute__((constructor))
+teco_expressions_init(void)
+{
+ teco_numbers = g_array_sized_new(FALSE, FALSE, sizeof(teco_int_t), 1024);
+ teco_operators = g_array_sized_new(FALSE, FALSE, sizeof(teco_operator_t), 1024);
+}
+
+/** Get operator precedence */
+static inline gint
+teco_expressions_precedence(teco_operator_t op)
+{
+ return op >> 4;
+}
+
+gint teco_num_sign = 1;
+gint teco_radix = 10;
+
+void
+teco_expressions_push_int(teco_int_t number)
+{
+ while (teco_operators->len > 0 && teco_expressions_peek_op(0) == TECO_OP_NEW)
+ teco_expressions_pop_op(0);
+
+ teco_expressions_push_op(TECO_OP_NUMBER);
+
+ if (teco_num_sign < 0) {
+ teco_set_num_sign(1);
+ number *= -1;
+ }
+
+ g_array_append_val(teco_numbers, number);
+ undo__remove_index__teco_numbers(teco_numbers->len-1);
+}
+
+teco_int_t
+teco_expressions_peek_num(guint index)
+{
+ return g_array_index(teco_numbers, teco_int_t, teco_numbers->len - 1 - index);
+}
+
+teco_int_t
+teco_expressions_pop_num(guint index)
+{
+ teco_int_t n = 0;
+ teco_operator_t op = teco_expressions_pop_op(0);
+
+ g_assert(op == TECO_OP_NUMBER);
+
+ if (teco_numbers->len > 0) {
+ n = teco_expressions_peek_num(index);
+ undo__insert_val__teco_numbers(teco_numbers->len - 1 - index, n);
+ g_array_remove_index(teco_numbers, teco_numbers->len - 1 - index);
+ }
+
+ return n;
+}
+
+gboolean
+teco_expressions_pop_num_calc(teco_int_t *ret, teco_int_t imply, GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+ if (teco_num_sign < 0)
+ teco_set_num_sign(1);
+
+ teco_int_t v = teco_expressions_args() > 0 ? teco_expressions_pop_num(0) : imply;
+ if (ret)
+ *ret = v;
+ return TRUE;
+}
+
+void
+teco_expressions_add_digit(gchar digit)
+{
+ teco_int_t n = teco_expressions_args() > 0 ? teco_expressions_pop_num(0) : 0;
+
+ teco_expressions_push(n*teco_radix + (n < 0 ? -1 : 1)*(digit - '0'));
+}
+
+void
+teco_expressions_push_op(teco_operator_t op)
+{
+ g_array_append_val(teco_operators, op);
+ undo__remove_index__teco_operators(teco_operators->len-1);
+}
+
+gboolean
+teco_expressions_push_calc(teco_operator_t op, GError **error)
+{
+ gint first = teco_expressions_first_op();
+
+ /* calculate if op has lower precedence than op on stack */
+ if (first >= 0 &&
+ teco_expressions_precedence(op) <= teco_expressions_precedence(teco_expressions_peek_op(first)) &&
+ !teco_expressions_calc(error))
+ return FALSE;
+
+ teco_expressions_push_op(op);
+ return TRUE;
+}
+
+teco_operator_t
+teco_expressions_peek_op(guint index)
+{
+ return g_array_index(teco_operators, teco_operator_t, teco_operators->len - 1 - index);
+}
+
+teco_operator_t
+teco_expressions_pop_op(guint index)
+{
+ teco_operator_t op = TECO_OP_NIL;
+
+ if (teco_operators->len > 0) {
+ op = teco_expressions_peek_op(index);
+ undo__insert_val__teco_operators(teco_operators->len - 1 - index, op);
+ g_array_remove_index(teco_operators, teco_operators->len - 1 - index);
+ }
+
+ return op;
+}
+
+static gboolean
+teco_expressions_calc(GError **error)
+{
+ teco_int_t result;
+
+ if (!teco_operators->len || teco_expressions_peek_op(0) != TECO_OP_NUMBER) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Missing right operand");
+ return FALSE;
+ }
+ teco_int_t vright = teco_expressions_pop_num(0);
+ teco_operator_t op = teco_expressions_pop_op(0);
+ if (!teco_operators->len || teco_expressions_peek_op(0) != TECO_OP_NUMBER) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Missing left operand");
+ return FALSE;
+ }
+ teco_int_t vleft = teco_expressions_pop_num(0);
+
+ switch (op) {
+ case TECO_OP_POW:
+ for (result = 1; vright--; result *= vleft);
+ break;
+ case TECO_OP_MUL:
+ result = vleft * vright;
+ break;
+ case TECO_OP_DIV:
+ if (!vright) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Division by zero");
+ return FALSE;
+ }
+ result = vleft / vright;
+ break;
+ case TECO_OP_MOD:
+ if (!vright) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Remainder of division by zero");
+ return FALSE;
+ }
+ result = vleft % vright;
+ break;
+ case TECO_OP_ADD:
+ result = vleft + vright;
+ break;
+ case TECO_OP_SUB:
+ result = vleft - vright;
+ break;
+ case TECO_OP_AND:
+ result = vleft & vright;
+ break;
+ case TECO_OP_XOR:
+ result = vleft ^ vright;
+ break;
+ case TECO_OP_OR:
+ result = vleft | vright;
+ break;
+ default:
+ /* shouldn't happen */
+ g_assert_not_reached();
+ }
+
+ teco_expressions_push(result);
+ return TRUE;
+}
+
+gboolean
+teco_expressions_eval(gboolean pop_brace, GError **error)
+{
+ for (;;) {
+ gint n = teco_expressions_first_op();
+ if (n < 0)
+ break;
+
+ teco_operator_t op = teco_expressions_peek_op(n);
+ if (op == TECO_OP_BRACE) {
+ if (pop_brace)
+ teco_expressions_pop_op(n);
+ break;
+ }
+ if (n < 1)
+ break;
+
+ if (!teco_expressions_calc(error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+guint
+teco_expressions_args(void)
+{
+ guint n = 0;
+
+ while (n < teco_operators->len && teco_expressions_peek_op(n) == TECO_OP_NUMBER)
+ n++;
+
+ return n;
+}
+
+gint
+teco_expressions_first_op(void)
+{
+ for (guint i = 0; i < teco_operators->len; i++) {
+ switch (teco_expressions_peek_op(i)) {
+ case TECO_OP_NUMBER:
+ case TECO_OP_NEW:
+ break;
+ default:
+ return i;
+ }
+ }
+
+ return -1; /* no operator */
+}
+
+gboolean
+teco_expressions_discard_args(GError **error)
+{
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+ for (guint i = teco_expressions_args(); i; i--)
+ if (!teco_expressions_pop_num_calc(NULL, 0, error))
+ return FALSE;
+ return TRUE;
+}
+
+/** The nesting level of braces */
+guint teco_brace_level = 0;
+
+void
+teco_expressions_brace_open(void)
+{
+ teco_expressions_push_op(TECO_OP_BRACE);
+ teco_undo_guint(teco_brace_level)++;
+}
+
+gboolean
+teco_expressions_brace_return(guint keep_braces, guint args, GError **error)
+{
+ /*
+ * FIXME: Allocating on the stack might be dangerous.
+ */
+ teco_int_t return_numbers[args];
+
+ for (guint i = args; i; i--)
+ return_numbers[i-1] = teco_expressions_pop_num(0);
+
+ teco_undo_guint(teco_brace_level);
+
+ while (teco_brace_level > keep_braces) {
+ if (!teco_expressions_discard_args(error) ||
+ !teco_expressions_eval(TRUE, error))
+ return FALSE;
+ teco_brace_level--;
+ }
+
+ for (guint i = 0; i < args; i++)
+ teco_expressions_push(return_numbers[i]);
+
+ return TRUE;
+}
+
+gboolean
+teco_expressions_brace_close(GError **error)
+{
+ if (!teco_brace_level) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Missing opening brace");
+ return FALSE;
+ }
+ teco_undo_guint(teco_brace_level)--;
+ return teco_expressions_eval(TRUE, error);
+}
+
+void
+teco_expressions_clear(void)
+{
+ g_array_set_size(teco_numbers, 0);
+ g_array_set_size(teco_operators, 0);
+ teco_brace_level = 0;
+}
+
+/**
+ * Format a TECO integer as the `\` command would.
+ *
+ * @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.
+ * @return A pointer into buffer to the beginning of the formatted number.
+ */
+gchar *
+teco_expressions_format(gchar *buffer, teco_int_t number)
+{
+ gchar *p = buffer + TECO_EXPRESSIONS_FORMAT_LEN;
+
+ teco_int_t v = ABS(number);
+
+ *--p = '\0';
+ do {
+ *--p = '0' + (v % teco_radix);
+ if (*p > '9')
+ *p += 'A' - '9' - 1;
+ } while ((v /= teco_radix));
+ if (number < 0)
+ *--p = '-';
+
+ return p;
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_expressions_cleanup(void)
+{
+ g_array_free(teco_numbers, TRUE);
+ g_array_free(teco_operators, TRUE);
+}
+#endif
diff --git a/src/expressions.cpp b/src/expressions.cpp
deleted file mode 100644
index 7ccdd31..0000000
--- a/src/expressions.cpp
+++ /dev/null
@@ -1,277 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 "error.h"
-#include "expressions.h"
-
-namespace SciTECO {
-
-Expressions expressions;
-Expressions::NumberStack Expressions::numbers;
-Expressions::OperatorStack Expressions::operators;
-
-tecoInt
-Expressions::push(tecoInt number)
-{
- while (operators.items() && operators.peek() == OP_NEW)
- pop_op();
-
- push(OP_NUMBER);
-
- if (num_sign < 0) {
- set_num_sign(1);
- number *= -1;
- }
-
- NumberStack::undo_pop<numbers>();
- return numbers.push(number);
-}
-
-tecoInt
-Expressions::pop_num(guint index)
-{
- tecoInt n = 0;
- Operator op = pop_op();
-
- g_assert(op == OP_NUMBER);
-
- if (numbers.items()) {
- n = numbers.pop(index);
- NumberStack::undo_push<numbers>(n, index);
- }
-
- return n;
-}
-
-tecoInt
-Expressions::pop_num_calc(guint index, tecoInt imply)
-{
- eval();
- if (num_sign < 0)
- set_num_sign(1);
-
- return args() > 0 ? pop_num(index) : imply;
-}
-
-tecoInt
-Expressions::add_digit(gchar digit)
-{
- tecoInt n = args() > 0 ? pop_num() : 0;
-
- return push(n*radix + (n < 0 ? -1 : 1)*(digit - '0'));
-}
-
-Expressions::Operator
-Expressions::push(Expressions::Operator op)
-{
- OperatorStack::undo_pop<operators>();
- return operators.push(op);
-}
-
-Expressions::Operator
-Expressions::push_calc(Expressions::Operator op)
-{
- gint first = first_op();
-
- /* calculate if op has lower precedence than op on stack */
- if (first >= 0 &&
- precedence(op) <= precedence(operators.peek(first)))
- calc();
-
- return push(op);
-}
-
-Expressions::Operator
-Expressions::pop_op(guint index)
-{
- Operator op = OP_NIL;
-
- if (operators.items()) {
- op = operators.pop(index);
- OperatorStack::undo_push<operators>(op, index);
- }
-
- return op;
-}
-
-void
-Expressions::calc(void)
-{
- tecoInt result;
-
- tecoInt vright;
- Operator op;
- tecoInt vleft;
-
- if (!operators.items() || operators.peek() != OP_NUMBER)
- throw Error("Missing right operand");
- vright = pop_num();
- op = pop_op();
- if (!operators.items() || operators.peek() != OP_NUMBER)
- throw Error("Missing left operand");
- vleft = pop_num();
-
- switch (op) {
- case OP_POW:
- for (result = 1; vright--; result *= vleft);
- break;
- case OP_MUL:
- result = vleft * vright;
- break;
- case OP_DIV:
- if (!vright)
- throw Error("Division by zero");
- result = vleft / vright;
- break;
- case OP_MOD:
- if (!vright)
- throw Error("Remainder of division by zero");
- result = vleft % vright;
- break;
- case OP_ADD:
- result = vleft + vright;
- break;
- case OP_SUB:
- result = vleft - vright;
- break;
- case OP_AND:
- result = vleft & vright;
- break;
- case OP_XOR:
- result = vleft ^ vright;
- break;
- case OP_OR:
- result = vleft | vright;
- break;
- default:
- /* shouldn't happen */
- g_assert_not_reached();
- }
-
- push(result);
-}
-
-void
-Expressions::eval(bool pop_brace)
-{
- for (;;) {
- gint n = first_op();
- Operator op;
-
- if (n < 0)
- break;
-
- op = operators.peek(n);
- if (op == OP_BRACE) {
- if (pop_brace)
- pop_op(n);
- break;
- }
- if (n < 1)
- break;
-
- calc();
- }
-}
-
-guint
-Expressions::args(void)
-{
- guint n = 0;
- guint items = operators.items();
-
- while (n < items && operators.peek(n) == OP_NUMBER)
- n++;
-
- return n;
-}
-
-gint
-Expressions::first_op(void)
-{
- guint items = operators.items();
-
- for (guint i = 0; i < items; i++) {
- switch (operators.peek(i)) {
- case OP_NUMBER:
- case OP_NEW:
- break;
- default:
- return i;
- }
- }
-
- return -1; /* no operator */
-}
-
-void
-Expressions::discard_args(void)
-{
- eval();
- for (guint i = args(); i; i--)
- pop_num_calc();
-}
-
-void
-Expressions::brace_return(guint keep_braces, guint args)
-{
- tecoInt return_numbers[args];
-
- for (guint i = args; i; i--)
- return_numbers[i-1] = pop_num();
-
- undo.push_var(brace_level);
-
- while (brace_level > keep_braces) {
- discard_args();
- eval(true);
- brace_level--;
- }
-
- for (guint i = 0; i < args; i++)
- push(return_numbers[i]);
-}
-
-const gchar *
-Expressions::format(tecoInt number)
-{
- /* maximum length if radix = 2 */
- static gchar buf[1+sizeof(number)*8+1];
- gchar *p = buf + sizeof(buf);
-
- tecoInt v = ABS(number);
-
- *--p = '\0';
- do {
- *--p = '0' + (v % radix);
- if (*p > '9')
- *p += 'A' - '9' - 1;
- } while ((v /= radix));
- if (number < 0)
- *--p = '-';
-
- return p;
-}
-
-} /* namespace SciTECO */
diff --git a/src/expressions.h b/src/expressions.h
index bdd683c..6ff8af4 100644
--- a/src/expressions.h
+++ b/src/expressions.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,300 +14,149 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __EXPRESSIONS_H
-#define __EXPRESSIONS_H
+#pragma once
#include <glib.h>
-#include "memory.h"
+#include "sciteco.h"
#include "undo.h"
-#include "error.h"
-
-namespace SciTECO {
-
-template <typename Type>
-class ValueStack : public Object {
- /*
- * NOTE: Since value stacks are usually singleton,
- * we pass them as a template parameter, saving space
- * in the undo token.
- */
- template <ValueStack<Type> &stack>
- class UndoTokenPush : public UndoToken {
- Type value;
- guint index;
-
- public:
- UndoTokenPush(Type _value, guint _index = 0)
- : value(_value), index(_index) {}
-
- void
- run(void)
- {
- stack.push(value, index);
- }
- };
-
- template <ValueStack<Type> &stack>
- class UndoTokenPop : public UndoToken {
- guint index;
-
- public:
- UndoTokenPop(guint _index = 0)
- : index(_index) {}
-
- void
- run(void)
- {
- stack.pop(index);
- }
- };
-
- /** Beginning of stack area */
- Type *stack;
- /** End of stack area */
- Type *stack_top;
-
- /** Pointer to top element on stack */
- Type *sp;
-
-public:
- ValueStack(gsize size = 1024)
- {
- stack = new Type[size];
- /* stack grows to smaller addresses */
- sp = stack_top = stack+size;
- }
-
- ~ValueStack()
- {
- delete[] stack;
- }
-
- inline guint
- items(void)
- {
- return stack_top - sp;
- }
-
- inline Type &
- push(Type value, guint index = 0)
- {
- if (G_UNLIKELY(sp == stack))
- throw Error("Stack overflow");
-
- /* reserve space for new element */
- sp--;
- g_assert(items() > index);
-
- /* move away elements after index (index > 0) */
- for (guint i = 0; i < index; i++)
- sp[i] = sp[i+1];
-
- return sp[index] = value;
- }
-
- template <ValueStack<Type> &stack>
- static inline void
- undo_push(Type value, guint index = 0)
- {
- undo.push<UndoTokenPush<stack>>(value, index);
- }
-
- inline Type
- pop(guint index = 0)
- {
- /* peek() already asserts */
- Type v = peek(index);
-
- /* elements after index are moved to index (index > 0) */
- while (index--)
- sp[index+1] = sp[index];
-
- /* free space of element to pop */
- sp++;
-
- return v;
- }
-
- template <ValueStack<Type> &stack>
- static inline void
- undo_pop(guint index = 0)
- {
- undo.push<UndoTokenPop<stack>>(index);
- }
-
- inline Type &
- peek(guint index = 0)
- {
- g_assert(items() > index);
-
- return sp[index];
- }
-
- /** Clear all but `keep_items` items. */
- inline void
- clear(guint keep_items = 0)
- {
- g_assert(keep_items <= items());
-
- sp = stack_top - keep_items;
- }
-};
/**
- * Arithmetic expression stacks
+ * Defines a function undo__insert_val__ARRAY() to insert a value into
+ * a fixed GArray.
+ *
+ * @note
+ * This optimizes undo token memory consumption under the assumption
+ * that ARRAY is a global object that does not have to be stored in
+ * the undo tokens.
+ * Otherwise, you could simply undo__g_array_insert_val(...).
+ *
+ * @fixme
+ * If we only ever use INDEX == ARRAY->len, we might simplify this
+ * to undo__append_val__ARRAY().
*/
-extern class Expressions : public Object {
-public:
- /**
- * Operator type.
- * The enumeration value divided by 16 represents
- * its precedence (small values mean low precedence).
- * In other words, the value's lower nibble is
- * reserved for enumerating operators of the
- * same precedence.
- */
- enum Operator {
- /*
- * Pseudo operators
- */
- OP_NIL = 0x00,
- OP_NEW,
- OP_BRACE,
- OP_NUMBER,
- /*
- * Real operators
- */
- OP_POW = 0x60, // ^*
- OP_MOD = 0x50, // ^/
- OP_DIV, // /
- OP_MUL, // *
- OP_SUB = 0x40, // -
- OP_ADD, // +
- OP_AND = 0x30, // &
- OP_XOR = 0x20, // ^#
- OP_OR = 0x10 // #
- };
+#define TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(ARRAY, TYPE) \
+ static inline void \
+ insert_val__##ARRAY(guint index, TYPE value) \
+ { \
+ g_array_insert_val(ARRAY, index, value); \
+ } \
+ TECO_DEFINE_UNDO_CALL(insert_val__##ARRAY, guint, TYPE)
-private:
- /** Get operator precedence */
- inline gint
- precedence(Operator op)
- {
- return op >> 4;
- }
+/**
+ * Defines a function undo__remove_index__ARRAY() to remove a value from
+ * a fixed GArray.
+ *
+ * @note
+ * See TECO_DEFINE_ARRAY_UNDO_INSERT_VAL().
+ * undo__g_array_remove_index(...) would also be possible.
+ *
+ * @fixme
+ * If we only ever use INDEX == ARRAY->len-1, we might simplify this
+ * to undo__pop__ARRAY().
+ */
+#define TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(ARRAY) \
+ static inline void \
+ remove_index__##ARRAY(guint index) \
+ { \
+ g_array_remove_index(ARRAY, index); \
+ } \
+ TECO_DEFINE_UNDO_CALL(remove_index__##ARRAY, guint)
+/**
+ * Operator type.
+ * The enumeration value divided by 16 represents
+ * its precedence (small values mean low precedence).
+ * In other words, the value's lower nibble is
+ * reserved for enumerating operators of the
+ * same precedence.
+ */
+typedef enum {
/*
- * Number and operator stacks are static, so
- * they can be passed to the undo token constructors.
- * This is OK since Expression is singleton.
+ * Pseudo operators
*/
- typedef ValueStack<tecoInt> NumberStack;
- static NumberStack numbers;
-
- typedef ValueStack<Operator> OperatorStack;
- static OperatorStack operators;
-
-public:
- Expressions() : num_sign(1), radix(10), brace_level(0) {}
-
- gint num_sign;
- inline void
- set_num_sign(gint sign)
- {
- undo.push_var(num_sign) = sign;
- }
-
- gint radix;
- inline void
- set_radix(gint r)
- {
- undo.push_var(radix) = r;
- }
-
- tecoInt push(tecoInt number);
-
- /**
- * Push characters of a C-string.
- * Could be overloaded on push(tecoInt)
- * but this confuses GCC.
+ TECO_OP_NIL = 0x00,
+ TECO_OP_NEW,
+ TECO_OP_BRACE,
+ TECO_OP_NUMBER,
+ /*
+ * Real operators
*/
- inline void
- push_str(const gchar *str)
- {
- while (*str)
- push(*str++);
- }
-
- inline tecoInt
- peek_num(guint index = 0)
- {
- return numbers.peek(index);
- }
- tecoInt pop_num(guint index = 0);
- tecoInt pop_num_calc(guint index, tecoInt imply);
- inline tecoInt
- pop_num_calc(guint index = 0)
- {
- return pop_num_calc(index, num_sign);
- }
+ TECO_OP_POW = 0x60, // ^*
+ TECO_OP_MOD = 0x50, // ^/
+ TECO_OP_DIV, // /
+ TECO_OP_MUL, // *
+ TECO_OP_SUB = 0x40, // -
+ TECO_OP_ADD, // +
+ TECO_OP_AND = 0x30, // &
+ TECO_OP_XOR = 0x20, // ^#
+ TECO_OP_OR = 0x10 // #
+} teco_operator_t;
+
+extern gint teco_num_sign;
+
+static inline void
+teco_set_num_sign(gint sign)
+{
+ teco_undo_gint(teco_num_sign) = sign;
+}
+
+extern gint teco_radix;
+
+static inline void
+teco_set_radix(gint r)
+{
+ teco_undo_gint(teco_radix) = r;
+}
+
+void teco_expressions_push_int(teco_int_t number);
+
+/** Push characters of a C-string. */
+static inline void
+teco_expressions_push_str(const gchar *str)
+{
+ while (*str)
+ teco_expressions_push_int(*str++);
+}
+
+teco_int_t teco_expressions_peek_num(guint index);
+teco_int_t teco_expressions_pop_num(guint index);
+gboolean teco_expressions_pop_num_calc(teco_int_t *ret, teco_int_t imply, GError **error);
+
+void teco_expressions_add_digit(gchar digit);
+
+void teco_expressions_push_op(teco_operator_t op);
+gboolean teco_expressions_push_calc(teco_operator_t op, GError **error);
- tecoInt add_digit(gchar digit);
-
- Operator push(Operator op);
- Operator push_calc(Operator op);
- inline Operator
- peek_op(guint index = 0)
- {
- return operators.peek(index);
- }
- Operator pop_op(guint index = 0);
-
- void eval(bool pop_brace = false);
-
- guint args(void);
-
- void discard_args(void);
+/*
+ * FIXME: Does not work for TECO_OP_* constants as they are treated like int.
+ */
+#define teco_expressions_push(X) \
+ (_Generic((X), default : teco_expressions_push_int, \
+ char * : teco_expressions_push_str, \
+ const char * : teco_expressions_push_str)(X))
- /** The nesting level of braces */
- guint brace_level;
+teco_operator_t teco_expressions_peek_op(guint index);
+teco_operator_t teco_expressions_pop_op(guint index);
- inline void
- brace_open(void)
- {
- push(OP_BRACE);
- undo.push_var(brace_level)++;
- }
+gboolean teco_expressions_eval(gboolean pop_brace, GError **error);
- void brace_return(guint keep_braces, guint args = 0);
+guint teco_expressions_args(void);
- inline void
- brace_close(void)
- {
- if (!brace_level)
- throw Error("Missing opening brace");
- undo.push_var(brace_level)--;
- eval(true);
- }
+gint teco_expressions_first_op(void);
- inline void
- clear(void)
- {
- numbers.clear();
- operators.clear();
- brace_level = 0;
- }
+gboolean teco_expressions_discard_args(GError **error);
- const gchar *format(tecoInt number);
+extern guint teco_brace_level;
-private:
- void calc(void);
+void teco_expressions_brace_open(void);
+gboolean teco_expressions_brace_return(guint keep_braces, guint args, GError **error);
+gboolean teco_expressions_brace_close(GError **error);
- gint first_op(void);
-} expressions;
+void teco_expressions_clear(void);
-} /* namespace SciTECO */
+/** Maximum size required to format a number if teco_radix == 2 */
+#define TECO_EXPRESSIONS_FORMAT_LEN \
+ (1 + sizeof(teco_int_t)*8 + 1)
-#endif
+gchar *teco_expressions_format(gchar *buffer, teco_int_t number);
diff --git a/src/file-utils.c b/src/file-utils.c
new file mode 100644
index 0000000..4948787
--- /dev/null
+++ b/src/file-utils.c
@@ -0,0 +1,343 @@
+/*
+ * 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/gstdio.h>
+
+#include "sciteco.h"
+#include "qreg.h"
+#include "glob.h"
+#include "interface.h"
+#include "string-utils.h"
+#include "file-utils.h"
+
+#ifdef G_OS_WIN32
+
+/*
+ * NOTE: File attributes are represented as DWORDs in the Win32 API
+ * which should be equivalent to guint32.
+ */
+G_STATIC_ASSERT(sizeof(DWORD) == sizeof(teco_file_attributes_t));
+/*
+ * NOTE: Invalid file attributes should be represented by 0xFFFFFFFF.
+ */
+G_STATIC_ASSERT(INVALID_FILE_ATTRIBUTES == TECO_FILE_INVALID_ATTRIBUTES);
+
+teco_file_attributes_t
+teco_file_get_attributes(const gchar *filename)
+{
+ return GetFileAttributes((LPCTSTR)filename);
+}
+
+void
+teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs)
+{
+ SetFileAttributes((LPCTSTR)filename, attrs);
+}
+
+gchar *
+teco_file_get_absolute_path(const gchar *path)
+{
+ TCHAR buf[MAX_PATH];
+ return path && GetFullPathName(path, sizeof(buf), buf, NULL) ? g_strdup(buf) : NULL;
+}
+
+gboolean
+teco_file_is_visible(const gchar *path)
+{
+ return !(GetFileAttributes((LPCTSTR)path) & FILE_ATTRIBUTE_HIDDEN);
+}
+
+#else /* !G_OS_WIN32 */
+
+teco_file_attributes_t
+teco_file_get_attributes(const gchar *filename)
+{
+ struct stat buf;
+ return g_stat(filename, &buf) ? TECO_FILE_INVALID_ATTRIBUTES : buf.st_mode;
+}
+
+void
+teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs)
+{
+ g_chmod(filename, attrs);
+}
+
+#ifdef G_OS_UNIX
+
+gchar *
+teco_file_get_absolute_path(const gchar *path)
+{
+ gchar buf[PATH_MAX];
+
+ if (!path)
+ return NULL;
+ if (realpath(path, buf))
+ return g_strdup(buf);
+ if (g_path_is_absolute(path))
+ return g_strdup(path);
+
+ g_autofree gchar *cwd = g_get_current_dir();
+ return g_build_filename(cwd, path, NULL);
+}
+
+gboolean
+teco_file_is_visible(const gchar *path)
+{
+ g_autofree gchar *basename = g_path_get_basename(path);
+ return *basename != '.';
+}
+
+#else /* !G_OS_UNIX */
+
+#if GLIB_CHECK_VERSION(2,58,0)
+
+/*
+ * FIXME: This should perhaps be preferred on any platform.
+ * But it will complicate preprocessing.
+ */
+gchar *
+teco_file_get_absolute_path(const gchar *path)
+{
+ return g_canonicalize_filename(path, NULL);
+}
+
+#else /* !GLIB_CHECK_VERSION(2,58,0) */
+
+/*
+ * This will never canonicalize relative paths.
+ * I.e. the absolute path will often contain
+ * relative components, even if `path` exists.
+ * The only exception would be a simple filename
+ * not containing any "..".
+ */
+gchar *
+teco_file_get_absolute_path(const gchar *path)
+{
+ if (!path)
+ return NULL;
+ if (g_path_is_absolute(path))
+ return g_strdup(path);
+
+ g_autofree gchar *cwd = g_get_current_dir();
+ return g_build_filename(cwd, path, NULL);
+}
+
+#endif /* !GLIB_CHECK_VERSION(2,58,0) */
+
+/*
+ * There's no platform-independent way to determine if a file
+ * is visible/hidden, so we just assume that all files are
+ * visible.
+ */
+gboolean
+teco_file_is_visible(const gchar *path)
+{
+ return TRUE;
+}
+
+#endif /* !G_OS_UNIX */
+
+#endif /* !G_OS_WIN32 */
+
+/**
+ * Perform tilde expansion on a file name or path.
+ *
+ * This supports only strings with a "~" prefix.
+ * A user name after "~" is not supported.
+ * The $HOME environment variable/register is used to retrieve
+ * the current user's home directory.
+ */
+gchar *
+teco_file_expand_path(const gchar *path)
+{
+ if (!path)
+ return g_strdup("");
+
+ if (path[0] != '~' || (path[1] && !G_IS_DIR_SEPARATOR(path[1])))
+ return g_strdup(path);
+
+ /*
+ * $HOME should not have a trailing directory separator since
+ * it is canonicalized to an absolute path at startup,
+ * but this ensures that a proper path is constructed even if
+ * it does (e.g. $HOME is changed later on).
+ *
+ * FIXME: In the future, it might be possible to remove the entire register.
+ */
+ teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "$HOME", 5);
+ g_assert(qreg != NULL);
+
+ /*
+ * Getting the string should not possible to fail.
+ * The $HOME register should not contain any null-bytes on startup,
+ * but it may have been changed later on.
+ */
+ g_auto(teco_string_t) home = {NULL, 0};
+ if (!qreg->vtable->get_string(qreg, &home.data, &home.len, NULL) ||
+ teco_string_contains(&home, '\0'))
+ return g_strdup(path);
+
+ return g_build_filename(home.data, path+1, NULL);
+}
+
+/**
+ * Auto-complete a filename/directory.
+ *
+ * @param filename The filename to auto-complete or NULL.
+ * @param file_test Restrict completion to files matching the test.
+ * If G_FILE_TEST_EXISTS, both files and directories are completed.
+ * If G_FILE_TEST_IS_DIR, only directories will be completed.
+ * @param insert String to initialize with the autocompletion.
+ * @return TRUE if the completion was unambiguous (eg. command can be terminated).
+ */
+gboolean
+teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_t *insert)
+{
+ memset(insert, 0, sizeof(*insert));
+
+ if (teco_globber_is_pattern(filename))
+ return FALSE;
+
+ g_autofree gchar *filename_expanded = teco_file_expand_path(filename);
+ gsize filename_len = strlen(filename_expanded);
+
+ /*
+ * Derive base and directory names.
+ * We do not use g_path_get_basename() or g_path_get_dirname()
+ * since we need strict suffixes and prefixes of filename
+ * in order to construct paths of entries in dirname
+ * that are suitable for auto completion.
+ */
+ gsize dirname_len = teco_file_get_dirname_len(filename_expanded);
+ g_autofree gchar *dirname = g_strndup(filename_expanded, dirname_len);
+ gchar *basename = filename_expanded + dirname_len;
+
+ g_autoptr(GDir) dir = g_dir_open(dirname_len ? dirname : ".", 0, NULL);
+ if (!dir)
+ return FALSE;
+
+ /*
+ * On Windows, both forward and backslash
+ * directory separators are allowed in directory
+ * names passed to glib.
+ * To imitate glib's behaviour, we use
+ * the last valid directory separator in `filename_expanded`
+ * to generate new separators.
+ * This also allows forward-slash auto-completion
+ * on Windows.
+ */
+ const gchar *dir_sep = dirname_len ? dirname + dirname_len - 1
+ : G_DIR_SEPARATOR_S;
+
+ GSList *files = NULL;
+ guint files_len = 0;
+ gsize prefix_len = 0;
+
+ const gchar *cur_basename;
+ while ((cur_basename = g_dir_read_name(dir))) {
+ if (!g_str_has_prefix(cur_basename, basename))
+ continue;
+
+ /*
+ * NOTE: `dirname` contains any directory separator, so strcat() works here.
+ * Reserving one byte at the end of the filename ensures we can easily
+ * append the directory separator without reallocations.
+ */
+ gchar *cur_filename = g_malloc(strlen(dirname)+strlen(cur_basename)+2);
+ strcat(strcpy(cur_filename, dirname), cur_basename);
+
+ /*
+ * NOTE: This avoids g_file_test() for G_FILE_TEST_EXISTS
+ * since the file we process here should always exist.
+ */
+ if ((!*basename && !teco_file_is_visible(cur_filename)) ||
+ (file_test != G_FILE_TEST_EXISTS &&
+ !g_file_test(cur_filename, file_test))) {
+ g_free(cur_filename);
+ continue;
+ }
+
+ if (file_test == G_FILE_TEST_IS_DIR ||
+ g_file_test(cur_filename, G_FILE_TEST_IS_DIR))
+ strcat(cur_filename, dir_sep);
+
+ files = g_slist_prepend(files, cur_filename);
+
+ if (g_slist_next(files)) {
+ teco_string_t other_file;
+ other_file.data = (gchar *)g_slist_next(files)->data + filename_len;
+ other_file.len = strlen(other_file.data);
+
+ gsize len = teco_string_diff(&other_file, cur_filename + filename_len,
+ strlen(cur_filename) - filename_len);
+ if (len < prefix_len)
+ prefix_len = len;
+ } else {
+ prefix_len = strlen(cur_filename + filename_len);
+ }
+
+ files_len++;
+ }
+
+ if (prefix_len > 0) {
+ teco_string_init(insert, (gchar *)files->data + filename_len, prefix_len);
+ } else if (files_len > 1) {
+ files = g_slist_sort(files, (GCompareFunc)g_strcmp0);
+
+ for (GSList *file = files; file; file = g_slist_next(file)) {
+ teco_popup_entry_type_t type = TECO_POPUP_DIRECTORY;
+ gboolean is_buffer = FALSE;
+
+ if (!teco_file_is_dir((gchar *)file->data)) {
+ type = TECO_POPUP_FILE;
+ /* FIXME: inefficient */
+ is_buffer = teco_ring_find((gchar *)file->data) != NULL;
+ }
+
+ teco_interface_popup_add(type, (gchar *)file->data,
+ strlen((gchar *)file->data), is_buffer);
+ }
+
+ teco_interface_popup_show();
+ }
+
+ /*
+ * FIXME: If we are completing only directories,
+ * we can theoretically insert the completed character
+ * after directories without subdirectories.
+ */
+ gboolean unambiguous = files_len == 1 && !teco_file_is_dir((gchar *)files->data);
+ g_slist_free_full(files, g_free);
+ return unambiguous;
+}
diff --git a/src/ioview.h b/src/file-utils.h
index 2baa21f..496e881 100644
--- a/src/ioview.h
+++ b/src/file-utils.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,27 +14,20 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __IOVIEW_H
-#define __IOVIEW_H
+#pragma once
#include <string.h>
#include <glib.h>
-#include <glib/gstdio.h>
-#include <glib/gprintf.h>
#include "sciteco.h"
-#include "interface.h"
-#include "undo.h"
-
-namespace SciTECO {
+#include "string-utils.h"
-/*
- * Auxiliary functions
- */
+typedef guint32 teco_file_attributes_t;
+#define TECO_FILE_INVALID_ATTRIBUTES G_MAXUINT32
-gchar *expand_path(const gchar *path);
+teco_file_attributes_t teco_file_get_attributes(const gchar *filename);
+void teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs);
/**
* Get absolute/full version of a possibly relative path.
@@ -49,7 +42,7 @@ gchar *expand_path(const gchar *path);
* @param path Possibly relative path name.
* @return Newly-allocated absolute path name.
*/
-gchar *get_absolute_path(const gchar *path);
+gchar *teco_file_get_absolute_path(const gchar *path);
/**
* Normalize path or file name.
@@ -64,7 +57,7 @@ gchar *get_absolute_path(const gchar *path);
* may be ignored.
*/
static inline gchar *
-normalize_path(gchar *path)
+teco_file_normalize_path(gchar *path)
{
#if G_DIR_SEPARATOR != '/'
return g_strdelimit(path, G_DIR_SEPARATOR_S, '/');
@@ -73,7 +66,9 @@ normalize_path(gchar *path)
#endif
}
-bool file_is_visible(const gchar *path);
+gboolean teco_file_is_visible(const gchar *path);
+
+gchar *teco_file_expand_path(const gchar *path);
/**
* This gets the length of a file name's directory
@@ -87,7 +82,7 @@ bool file_is_visible(const gchar *path);
* the last used directory separator in the file name.
*/
static inline gsize
-file_get_dirname_len(const gchar *path)
+teco_file_get_dirname_len(const gchar *path)
{
gsize len = 0;
@@ -98,33 +93,14 @@ file_get_dirname_len(const gchar *path)
return len;
}
-class IOView : public ViewCurrent {
- class UndoTokenRemoveFile : public UndoToken {
- gchar *filename;
-
- public:
- UndoTokenRemoveFile(const gchar *_filename)
- : filename(g_strdup(_filename)) {}
- ~UndoTokenRemoveFile()
- {
- g_free(filename);
- }
-
- void
- run(void)
- {
- g_unlink(filename);
- }
- };
-
-public:
- void load(GIOChannel *channel);
- void load(const gchar *filename);
-
- void save(GIOChannel *channel);
- void save(const gchar *filename);
-};
+static inline gboolean
+teco_file_is_dir(const gchar *filename)
+{
+ if (!*filename)
+ return FALSE;
-} /* namespace SciTECO */
+ gchar c = filename[strlen(filename)-1];
+ return G_IS_DIR_SEPARATOR(c);
+}
-#endif
+gboolean teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_t *insert);
diff --git a/src/glob.cpp b/src/glob.c
index e6b5bd4..f6810c2 100644
--- a/src/glob.cpp
+++ b/src/glob.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -26,25 +26,28 @@
#include <glib/gstdio.h>
#include "sciteco.h"
+#include "string-utils.h"
+#include "file-utils.h"
#include "interface.h"
#include "parser.h"
+#include "core-commands.h"
#include "expressions.h"
-#include "qregisters.h"
+#include "qreg.h"
#include "ring.h"
-#include "ioview.h"
+#include "error.h"
#include "glob.h"
-namespace SciTECO {
-
-namespace States {
- StateGlob_pattern glob_pattern;
- StateGlob_filename glob_filename;
-}
+/*
+ * FIXME: This state could be static.
+ */
+TECO_DECLARE_STATE(teco_state_glob_filename);
-Globber::Globber(const gchar *pattern, GFileTest _test)
- : test(_test)
+/** @memberof teco_globber_t */
+void
+teco_globber_init(teco_globber_t *ctx, const gchar *pattern, GFileTest test)
{
- gsize dirname_len;
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->test = test;
/*
* This finds the directory component including
@@ -55,40 +58,39 @@ Globber::Globber(const gchar *pattern, GFileTest _test)
* file names with the exact same directory
* prefix as the input pattern.
*/
- dirname_len = file_get_dirname_len(pattern);
- dirname = g_strndup(pattern, dirname_len);
+ gsize dirname_len = teco_file_get_dirname_len(pattern);
+ ctx->dirname = g_strndup(pattern, dirname_len);
- dir = g_dir_open(*dirname ? dirname : ".", 0, NULL);
- /* if dirname does not exist, dir may be NULL */
+ ctx->dir = g_dir_open(*ctx->dirname ? ctx->dirname : ".", 0, NULL);
+ /* if dirname does not exist, the result may be NULL */
- Globber::pattern = compile_pattern(pattern + dirname_len);
+ ctx->pattern = teco_globber_compile_pattern(pattern + dirname_len);
}
+/** @memberof teco_globber_t */
gchar *
-Globber::next(void)
+teco_globber_next(teco_globber_t *ctx)
{
const gchar *basename;
- if (!dir)
+ if (!ctx->dir)
return NULL;
- while ((basename = g_dir_read_name(dir))) {
- gchar *filename;
-
- if (!g_regex_match(pattern, basename, (GRegexMatchFlags)0, NULL))
+ while ((basename = g_dir_read_name(ctx->dir))) {
+ if (!g_regex_match(ctx->pattern, basename, 0, NULL))
continue;
/*
* As dirname includes the directory separator,
* we can simply concatenate dirname with basename.
*/
- filename = g_strconcat(dirname, basename, NIL);
+ gchar *filename = g_strconcat(ctx->dirname, basename, NULL);
/*
* No need to perform file test for EXISTS since
* g_dir_read_name() will only return existing entries
*/
- if (test == G_FILE_TEST_EXISTS || g_file_test(filename, test))
+ if (ctx->test == G_FILE_TEST_EXISTS || g_file_test(filename, ctx->test))
return filename;
g_free(filename);
@@ -97,17 +99,20 @@ Globber::next(void)
return NULL;
}
-Globber::~Globber()
+/** @memberof teco_globber_t */
+void
+teco_globber_clear(teco_globber_t *ctx)
{
- if (pattern)
- g_regex_unref(pattern);
- if (dir)
- g_dir_close(dir);
- g_free(dirname);
+ if (ctx->pattern)
+ g_regex_unref(ctx->pattern);
+ if (ctx->dir)
+ g_dir_close(ctx->dir);
+ g_free(ctx->dirname);
}
+/** @static @memberof teco_globber_t */
gchar *
-Globber::escape_pattern(const gchar *pattern)
+teco_globber_escape_pattern(const gchar *pattern)
{
gsize escaped_len = 1;
gchar *escaped, *pout;
@@ -129,7 +134,7 @@ Globber::escape_pattern(const gchar *pattern)
break;
}
}
- pout = escaped = (gchar *)g_malloc(escaped_len);
+ pout = escaped = g_malloc(escaped_len);
while (*pattern) {
switch (*pattern) {
@@ -163,13 +168,12 @@ Globber::escape_pattern(const gchar *pattern)
* @param pattern The pattern to compile.
* @return A new compiled regular expression object.
* Always non-NULL. Unref after use.
+ *
+ * @static @memberof teco_globber_t
*/
GRegex *
-Globber::compile_pattern(const gchar *pattern)
+teco_globber_compile_pattern(const gchar *pattern)
{
- gchar *pattern_regex, *pout;
- GRegex *pattern_compiled;
-
enum {
STATE_WILDCARD,
STATE_CLASS_START,
@@ -187,7 +191,8 @@ Globber::compile_pattern(const gchar *pattern)
* might be arbitrary user input and we must avoid
* stack overflows at all costs.
*/
- pout = pattern_regex = (gchar *)g_malloc(strlen(pattern)*2 + 1 + 1);
+ g_autofree gchar *pattern_regex = g_malloc(strlen(pattern)*2 + 1 + 1);
+ gchar *pout = pattern_regex;
while (*pattern) {
if (state == STATE_WILDCARD) {
@@ -277,16 +282,14 @@ Globber::compile_pattern(const gchar *pattern)
*pout++ = '$';
*pout = '\0';
- pattern_compiled = g_regex_new(pattern_regex,
- (GRegexCompileFlags)(G_REGEX_DOTALL | G_REGEX_ANCHORED),
- (GRegexMatchFlags)0, NULL);
+ GRegex *pattern_compiled = g_regex_new(pattern_regex,
+ G_REGEX_DOTALL | G_REGEX_ANCHORED, 0, NULL);
/*
* Since the regex is generated from patterns that are
* always valid, there must be no syntactic error.
*/
g_assert(pattern_compiled != NULL);
- g_free(pattern_regex);
return pattern_compiled;
}
@@ -294,6 +297,25 @@ Globber::compile_pattern(const gchar *pattern)
* Command States
*/
+static teco_state_t *
+teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_glob_filename;
+
+ 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);
+ if (!glob_reg->vtable->undo_set_string(glob_reg, error) ||
+ !glob_reg->vtable->set_string(glob_reg, filename, strlen(filename), error))
+ return NULL;
+ }
+
+ return &teco_state_glob_filename;
+}
+
/*$ EN glob
* [type]EN[pattern]$[filename]$ -- Glob files or match filename and check file type
* [type]:EN[pattern]$[filename]$ -> Success|Failure
@@ -347,7 +369,7 @@ Globber::compile_pattern(const gchar *pattern)
* By default, if EN is not colon-modified, the result of
* globbing or file name matching is inserted into the current
* document, at the current position.
- * A linefeed is inserted after every file name, i.e.
+ * The file names will be separated by line feeds, i.e.
* every matching file will be on its own line.
*
* EN may be colon-modified to avoid any text insertion.
@@ -420,37 +442,26 @@ Globber::compile_pattern(const gchar *pattern)
* when they should be in a register, the user will
* have to edit that register anyway.
*/
-State *
-StateGlob_pattern::got_file(const gchar *filename)
-{
- BEGIN_EXEC(&States::glob_filename);
-
- if (*filename) {
- QRegister *glob_reg = QRegisters::globals["_"];
-
- glob_reg->undo_set_string();
- glob_reg->set_string(filename);
- }
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_pattern,
+ .expectstring.last = FALSE
+);
- return &States::glob_filename;
-}
-
-State *
-StateGlob_filename::got_file(const gchar *filename)
+static teco_state_t *
+teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
{
- BEGIN_EXEC(&States::start);
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
- tecoInt teco_test_mode;
GFileTest file_flags = G_FILE_TEST_EXISTS;
- bool matching = false;
- bool colon_modified = eval_colon();
+ gboolean matching = FALSE;
+ gboolean colon_modified = teco_machine_main_eval_colon(ctx);
- QRegister *glob_reg = QRegisters::globals["_"];
- gchar *pattern_str;
+ teco_int_t teco_test_mode;
- expressions.eval();
- teco_test_mode = expressions.pop_num_calc(0, 0);
+ if (!teco_expressions_eval(FALSE, error) ||
+ !teco_expressions_pop_num_calc(&teco_test_mode, 0, error))
+ return NULL;
switch (teco_test_mode) {
/*
* 0 means, no file testing.
@@ -464,91 +475,99 @@ StateGlob_filename::got_file(const gchar *filename)
case 4: file_flags = G_FILE_TEST_IS_EXECUTABLE; break;
case 5: file_flags = G_FILE_TEST_EXISTS; break;
default:
- throw Error("Invalid file test %" TECO_INTEGER_FORMAT
- " for <EN>", teco_test_mode);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Invalid file test %" TECO_INT_FORMAT " for <EN>",
+ teco_test_mode);
+ return NULL;
}
- pattern_str = glob_reg->get_string();
+ teco_qreg_t *glob_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(glob_reg != NULL);
+ g_auto(teco_string_t) pattern_str = {NULL, 0};
+ if (!glob_reg->vtable->get_string(glob_reg, &pattern_str.data, &pattern_str.len, error))
+ return NULL;
+ if (teco_string_contains(&pattern_str, '\0')) {
+ teco_error_qregcontainsnull_set(error, "_", 1, FALSE);
+ return NULL;
+ }
- if (*filename) {
+ if (str->len > 0) {
/*
* Match pattern against provided file name
*/
- GRegex *pattern = Globber::compile_pattern(pattern_str);
+ 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, (GRegexMatchFlags)0, NULL) &&
- (!teco_test_mode || g_file_test(filename, file_flags))) {
+ if (g_regex_match(pattern, filename, 0, NULL) &&
+ (teco_test_mode == 0 || g_file_test(filename, file_flags))) {
if (!colon_modified) {
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_ADDTEXT, strlen(filename),
- (sptr_t)filename);
- interface.ssm(SCI_ADDTEXT, 1, (sptr_t)"\n");
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
+ /*
+ * FIXME: Filenames may contain linefeeds.
+ * But if we add them null-terminated, they will be relatively hard to parse.
+ */
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ teco_interface_ssm(SCI_ADDTEXT, strlen(filename),
+ (sptr_t)filename);
+ teco_interface_ssm(SCI_ADDTEXT, 1, (sptr_t)"\n");
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
}
- matching = true;
+ matching = TRUE;
}
-
- g_regex_unref(pattern);
} else if (colon_modified) {
/*
* Match pattern against directory contents (globbing),
- * returning SUCCESS if at least one file matches
+ * returning TECO_SUCCESS if at least one file matches
*/
- Globber globber(pattern_str, file_flags);
- gchar *globbed_filename = globber.next();
+ g_auto(teco_globber_t) globber;
- matching = globbed_filename != NULL;
+ teco_globber_init(&globber, pattern_str.data, file_flags);
+ g_autofree gchar *globbed_filename = teco_globber_next(&globber);
- g_free(globbed_filename);
+ matching = globbed_filename != NULL;
} else {
/*
* Match pattern against directory contents (globbing),
- * inserting all matching file names (linefeed-terminated)
+ * inserting all matching file names (null-byte-terminated)
*/
- Globber globber(pattern_str, file_flags);
-
- gchar *globbed_filename;
+ g_auto(teco_globber_t) globber;
+ teco_globber_init(&globber, pattern_str.data, file_flags);
- interface.ssm(SCI_BEGINUNDOACTION);
-
- while ((globbed_filename = globber.next())) {
- size_t len = strlen(globbed_filename);
- /* overwrite trailing null */
- globbed_filename[len] = '\n';
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ gchar *globbed_filename;
+ while ((globbed_filename = teco_globber_next(&globber))) {
/*
- * FIXME: Once we're 8-bit clean, we should
- * add the filenames null-terminated
- * (there may be linebreaks in filename).
+ * FIXME: Filenames may contain linefeeds.
+ * But if we add them null-terminated, they will be relatively hard to parse.
*/
- interface.ssm(SCI_ADDTEXT, len+1,
- (sptr_t)globbed_filename);
+ teco_interface_ssm(SCI_ADDTEXT, strlen(globbed_filename),
+ (sptr_t)globbed_filename);
+ teco_interface_ssm(SCI_ADDTEXT, 1, (sptr_t)"\n");
g_free(globbed_filename);
- matching = true;
+ matching = TRUE;
}
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
}
- g_free(pattern_str);
-
if (colon_modified) {
- expressions.push(TECO_BOOL(matching));
+ teco_expressions_push(teco_bool(matching));
} else if (matching) {
/* text has been inserted */
- ring.dirtify();
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
+ teco_ring_dirtify();
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
}
- glob_reg->undo_set_integer();
- glob_reg->set_integer(TECO_BOOL(matching));
+ if (!glob_reg->vtable->undo_set_integer(glob_reg, error) ||
+ !glob_reg->vtable->set_integer(glob_reg, teco_bool(matching), error))
+ return NULL;
- return &States::start;
+ return &teco_state_start;
}
-} /* namespace SciTECO */
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_filename);
diff --git a/src/glob.h b/src/glob.h
index aca1a30..db17c99 100644
--- a/src/glob.h
+++ b/src/glob.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,66 +14,40 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __GLOB_H
-#define __GLOB_H
+#pragma once
#include <string.h>
#include <glib.h>
-#include <glib/gstdio.h>
#include "sciteco.h"
-#include "memory.h"
#include "parser.h"
-namespace SciTECO {
-
-class Globber : public Object {
+typedef struct {
GFileTest test;
gchar *dirname;
GDir *dir;
GRegex *pattern;
+} teco_globber_t;
-public:
- Globber(const gchar *pattern,
- GFileTest test = G_FILE_TEST_EXISTS);
- ~Globber();
+void teco_globber_init(teco_globber_t *ctx, const gchar *pattern, GFileTest test);
+gchar *teco_globber_next(teco_globber_t *ctx);
+void teco_globber_clear(teco_globber_t *ctx);
- gchar *next(void);
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_globber_t, teco_globber_clear);
- static inline bool
- is_pattern(const gchar *str)
- {
- return str && strpbrk(str, "*?[") != NULL;
- }
+/** @static @memberof teco_globber_t */
+static inline gboolean
+teco_globber_is_pattern(const gchar *str)
+{
+ return str && strpbrk(str, "*?[") != NULL;
+}
- static gchar *escape_pattern(const gchar *pattern);
- static GRegex *compile_pattern(const gchar *pattern);
-};
+gchar *teco_globber_escape_pattern(const gchar *pattern);
+GRegex *teco_globber_compile_pattern(const gchar *pattern);
/*
* Command states
*/
-class StateGlob_pattern : public StateExpectFile {
-public:
- StateGlob_pattern() : StateExpectFile(true, false) {}
-
-private:
- State *got_file(const gchar *filename);
-};
-
-class StateGlob_filename : public StateExpectFile {
-private:
- State *got_file(const gchar *filename);
-};
-
-namespace States {
- extern StateGlob_pattern glob_pattern;
- extern StateGlob_filename glob_filename;
-}
-
-} /* namespace SciTECO */
-
-#endif
+TECO_DECLARE_STATE(teco_state_glob_pattern);
diff --git a/src/goto-commands.c b/src/goto-commands.c
new file mode 100644
index 0000000..792b4e3
--- /dev/null
+++ b/src/goto-commands.c
@@ -0,0 +1,171 @@
+/*
+ * 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 <string.h>
+
+#include <glib.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "expressions.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "undo.h"
+#include "goto.h"
+#include "goto-commands.h"
+
+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;
+}
+
+/*
+ * NOTE: The comma is theoretically not allowed in a label
+ * (see <O> syntax), but is accepted anyway since labels
+ * are historically used as comments.
+ *
+ * TODO: Add support for "true" comments of the form !* ... *!
+ * This would be almost trivial to implement, but if we don't
+ * want any (even temporary) overhead for comments at all, we need
+ * to add a new parser state.
+ * I'm unsure whether !-signs should be allowed within comments.
+ */
+static teco_state_t *
+teco_state_label_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ if (chr == '!') {
+ /*
+ * NOTE: If the label already existed, its PC will be restored
+ * on rubout.
+ * Otherwise, the label will be removed (PC == -1).
+ */
+ gint existing_pc = teco_goto_table_set(&ctx->goto_table, ctx->goto_label.data,
+ ctx->goto_label.len, ctx->macro_pc);
+ if (ctx->parent.must_undo)
+ teco_goto_table_undo_set(&ctx->goto_table, ctx->goto_label.data, ctx->goto_label.len, existing_pc);
+
+ if (teco_goto_skip_label.len > 0 &&
+ !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));
+
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_NORMAL;
+ }
+
+ if (ctx->parent.must_undo)
+ teco_undo_string_own(ctx->goto_label);
+ else
+ teco_string_clear(&ctx->goto_label);
+ return &teco_state_start;
+ }
+
+ if (ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->goto_label, ctx->goto_label.len);
+ teco_string_append_c(&ctx->goto_label, chr);
+ return &teco_state_label;
+}
+
+TECO_DEFINE_STATE(teco_state_label,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_label_initial
+);
+
+static teco_state_t *
+teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_int_t value;
+ if (!teco_expressions_pop_num_calc(&value, 1, error))
+ return NULL;
+
+ /*
+ * 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 = memchr(label.data, ',', str->len - (label.data - str->data));
+ label.len = p ? p - label.data : str->len - (label.data - str->data);
+
+ value--;
+
+ if (!p)
+ break;
+ }
+
+ if (value == 0) {
+ gint pc = teco_goto_table_find(&ctx->goto_table, label.data, label.len);
+
+ if (pc >= 0) {
+ ctx->macro_pc = pc;
+ } else {
+ /* 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);
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->mode = TECO_MODE_PARSE_ONLY_GOTO;
+ }
+ }
+
+ return &teco_state_start;
+}
+
+/* in cmdline.c */
+gboolean teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error);
+
+/*$ O
+ * Olabel$ -- Go to label
+ * [n]Olabel1[,label2,...]$
+ *
+ * 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.
+ *
+ * If the label selected by <n> is does not exist in the
+ * list of labels, the command does nothing.
+ * 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.
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_goto,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_goto_process_edit_cmd
+);
diff --git a/src/symbols-minimal.cpp b/src/goto-commands.h
index 1582979..792a4e4 100644
--- a/src/symbols-minimal.cpp
+++ b/src/goto-commands.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,19 +14,12 @@
* 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
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
+#include "parser.h"
+#include "string-utils.h"
-#include "sciteco.h"
-#include "symbols.h"
+extern teco_string_t teco_goto_skip_label;
-namespace SciTECO {
-
-namespace Symbols {
- SymbolList scintilla;
- SymbolList scilexer;
-}
-
-} /* namespace SciTECO */
+TECO_DECLARE_STATE(teco_state_label);
+TECO_DECLARE_STATE(teco_state_goto);
diff --git a/src/goto.c b/src/goto.c
new file mode 100644
index 0000000..38717f3
--- /dev/null
+++ b/src/goto.c
@@ -0,0 +1,182 @@
+/*
+ * 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 <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "undo.h"
+#include "rb3str.h"
+#include "goto.h"
+
+//#define DEBUG
+
+/** @extends teco_rb3str_head_t */
+typedef struct {
+ teco_rb3str_head_t head;
+ gint pc;
+} teco_goto_label_t;
+
+/** @private @static @memberof teco_goto_label_t */
+static teco_goto_label_t *
+teco_goto_label_new(const gchar *name, gsize len, gint pc)
+{
+ teco_goto_label_t *label = g_new0(teco_goto_label_t, 1);
+ teco_string_init(&label->head.name, name, len);
+ label->pc = pc;
+ return label;
+}
+
+/** @private @memberof teco_goto_label_t */
+static inline void
+teco_goto_label_free(teco_goto_label_t *label)
+{
+ teco_string_clear(&label->head.name);
+ g_free(label);
+}
+
+/*
+ * FIXME: Most of these methods could be static since
+ * they are only called from goto.c.
+ */
+
+#ifdef DEBUG
+static void
+teco_goto_table_dump(teco_goto_table_t *ctx)
+{
+ for (rb3_head *cur = rb3_get_min(&ctx->tree);
+ cur != NULL;
+ cur = rb3_get_next(cur)) {
+ teco_goto_label_t *label = (teco_goto_label_t *)cur;
+ g_autofree *label_printable;
+ label_printable = teco_string_echo(cur->head.key.data, cur->head.key.len);
+
+ g_printf("table[\"%s\"] = %d\n", label_printable, label->pc);
+ }
+ g_printf("---END---\n");
+}
+#endif
+
+/** @memberof teco_goto_table_t */
+gint
+teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len)
+{
+ gint existing_pc = -1;
+
+ teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len);
+ if (label) {
+ existing_pc = label->pc;
+ rb3_unlink_and_rebalance(&label->head.head);
+ teco_goto_label_free(label);
+ }
+
+ return existing_pc;
+}
+
+/** @memberof teco_goto_table_t */
+gint
+teco_goto_table_find(teco_goto_table_t *ctx, const gchar *name, gsize len)
+{
+ teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len);
+ return label ? label->pc : -1;
+}
+
+/** @memberof teco_goto_table_t */
+gint
+teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gint pc)
+{
+ if (pc < 0)
+ return teco_goto_table_remove(ctx, name, len);
+
+ gint existing_pc = -1;
+
+ teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len);
+ if (label) {
+ existing_pc = label->pc;
+ label->pc = pc;
+ } else {
+ label = teco_goto_label_new(name, len, pc);
+ teco_rb3str_insert(&ctx->tree, TRUE, &label->head);
+ }
+
+#ifdef DEBUG
+ teco_goto_table_dump(ctx);
+#endif
+
+ return existing_pc;
+}
+
+/*
+ * NOTE: We don't simply TECO_DEFINE_UNDO_CALL(), so we can store `name`
+ * as part of the undo token.
+ * If it would be a temporary pointer, TECO_DEFINE_UNDO_CALL() wouldn't
+ * do anyway.
+ */
+typedef struct {
+ teco_goto_table_t *table;
+ gint pc;
+ gsize len;
+ gchar name[];
+} teco_goto_table_undo_set_t;
+
+static void
+teco_goto_table_undo_set_action(teco_goto_table_undo_set_t *ctx, gboolean run)
+{
+ if (run) {
+ teco_goto_table_set(ctx->table, ctx->name, ctx->len, ctx->pc);
+#ifdef DEBUG
+ teco_goto_table_dump(ctx->table);
+#endif
+ }
+}
+
+/** @memberof teco_goto_table_t */
+void
+teco_goto_table_undo_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gint pc)
+{
+ if (!ctx->must_undo)
+ return;
+
+ teco_goto_table_undo_set_t *token;
+ token = teco_undo_push_size((teco_undo_action_t)teco_goto_table_undo_set_action,
+ sizeof(*token) + len);
+ if (token) {
+ token->table = ctx;
+ token->pc = pc;
+ token->len = len;
+ memcpy(token->name, name, len);
+ }
+}
+
+/** @memberof teco_goto_table_t */
+void
+teco_goto_table_clear(teco_goto_table_t *ctx)
+{
+ struct rb3_head *cur;
+
+ while ((cur = rb3_get_root(&ctx->tree))) {
+ rb3_unlink_and_rebalance(cur);
+ teco_goto_label_free((teco_goto_label_t *)cur);
+ }
+}
diff --git a/src/goto.cpp b/src/goto.cpp
deleted file mode 100644
index 14a4655..0000000
--- a/src/goto.cpp
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 "sciteco.h"
-#include "string-utils.h"
-#include "expressions.h"
-#include "parser.h"
-#include "undo.h"
-#include "goto.h"
-
-namespace SciTECO {
-
-namespace States {
- StateLabel label;
- StateGotoCmd gotocmd;
-}
-
-namespace Goto {
- GotoTable *table = NULL;
- gchar *skip_label = NULL;
-}
-
-gint
-GotoTable::remove(const gchar *name)
-{
- gint existing_pc = -1;
-
- Label *existing = (Label *)RBTreeString::find(name);
-
- if (existing) {
- existing_pc = existing->pc;
- RBTreeString::remove(existing);
- delete existing;
- }
-
- return existing_pc;
-}
-
-gint
-GotoTable::find(const gchar *name)
-{
- Label *existing = (Label *)RBTreeString::find(name);
-
- return existing ? existing->pc : -1;
-}
-
-gint
-GotoTable::set(const gchar *name, gint pc)
-{
- if (pc < 0)
- return remove(name);
-
- gint existing_pc = -1;
- Label *existing = (Label *)RBTreeString::find(name);
-
- if (existing) {
- existing_pc = existing->pc;
- g_free(existing->name);
- existing->name = g_strdup(name);
- existing->pc = pc;
- } else {
- RBTree::insert(new Label(name, pc));
- }
-
-#ifdef DEBUG
- dump();
-#endif
-
- return existing_pc;
-}
-
-#ifdef DEBUG
-void
-GotoTable::dump(void)
-{
- for (Label *cur = (Label *)min();
- cur != NULL;
- cur = (Label *)cur->next())
- g_printf("table[\"%s\"] = %d\n", cur->name, cur->pc);
- g_printf("---END---\n");
-}
-#endif
-
-/*
- * Command states
- */
-
-StateLabel::StateLabel() : State()
-{
- transitions['\0'] = this;
-}
-
-State *
-StateLabel::custom(gchar chr)
-{
- if (chr == '!') {
- Goto::table->undo_set(strings[0],
- Goto::table->set(strings[0], macro_pc));
-
- if (!g_strcmp0(strings[0], Goto::skip_label)) {
- g_free(undo.push_str(Goto::skip_label));
- Goto::skip_label = NULL;
-
- undo.push_var(mode) = MODE_NORMAL;
- }
-
- g_free(undo.push_str(strings[0]));
- strings[0] = NULL;
-
- return &States::start;
- }
-
- String::append(undo.push_str(strings[0]), chr);
- return this;
-}
-
-/*$ O
- * Olabel$ -- Go to label
- * [n]Olabel1[,label2,...]$
- *
- * 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, the sign prefix is implied.
- *
- * If the label selected by <n> is does not exist in the
- * list of labels, the command does nothing.
- * 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.
- */
-State *
-StateGotoCmd::done(const gchar *str)
-{
- tecoInt value;
- gchar **labels;
-
- BEGIN_EXEC(&States::start);
-
- value = expressions.pop_num_calc();
- labels = g_strsplit(str, ",", -1);
-
- if (value > 0 && value <= (tecoInt)g_strv_length(labels) &&
- *labels[value-1]) {
- gint pc = Goto::table->find(labels[value-1]);
-
- if (pc >= 0) {
- macro_pc = pc;
- } else {
- /* skip till label is defined */
- undo.push_str(Goto::skip_label);
- Goto::skip_label = g_strdup(labels[value-1]);
- undo.push_var(mode) = MODE_PARSE_ONLY_GOTO;
- }
- }
-
- g_strfreev(labels);
- return &States::start;
-}
-
-} /* namespace SciTECO */
diff --git a/src/goto.h b/src/goto.h
index c45ac27..4c54847 100644
--- a/src/goto.h
+++ b/src/goto.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,131 +14,45 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __GOTO_H
-#define __GOTO_H
-
-#include <string.h>
+#pragma once
#include <glib.h>
-#include <glib/gprintf.h>
#include "sciteco.h"
-#include "memory.h"
-#include "parser.h"
-#include "undo.h"
-#include "rbtree.h"
-
-namespace SciTECO {
-
-class GotoTable : private RBTreeString, public Object {
- class UndoTokenSet : public UndoToken {
- GotoTable *table;
-
- gchar *name;
- gint pc;
-
- public:
- UndoTokenSet(GotoTable *_table, const gchar *_name, gint _pc = -1)
- : table(_table), name(g_strdup(_name)), pc(_pc) {}
- ~UndoTokenSet()
- {
- g_free(name);
- }
+#include "string-utils.h"
+#include "rb3str.h"
- void
- run(void)
- {
- table->set(name, pc);
-#ifdef DEBUG
- table->dump();
-#endif
- }
- };
+/** @extends teco_rb3str_tree_t */
+typedef struct {
+ teco_rb3str_tree_t tree;
- class Label : public RBTreeString::RBEntryOwnString {
- public:
- gint pc;
-
- Label(const gchar *name, gint _pc = -1)
- : RBEntryOwnString(name), pc(_pc) {}
- };
-
- /*
- * whether to generate UndoTokens (unnecessary in macro invocations)
+ /**
+ * Whether to generate undo tokens (unnecessary in macro invocations)
*/
- bool must_undo;
-
-public:
- GotoTable(bool _undo = true) : must_undo(_undo) {}
-
- ~GotoTable()
- {
- clear();
- }
-
- gint remove(const gchar *name);
- gint find(const gchar *name);
-
- gint set(const gchar *name, gint pc);
- inline void
- undo_set(const gchar *name, gint pc = -1)
- {
- if (must_undo)
- undo.push<UndoTokenSet>(this, name, pc);
- }
-
- inline void
- clear(void)
- {
- Label *cur;
-
- while ((cur = (Label *)root()))
- delete (Label *)RBTreeString::remove(cur);
- }
-
- inline gchar *
- auto_complete(const gchar *name, gchar completed = ',')
- {
- return RBTreeString::auto_complete(name, completed);
- }
-
-#ifdef DEBUG
- void dump(void);
-#endif
-};
-
-namespace Goto {
- extern GotoTable *table;
- extern gchar *skip_label;
+ gboolean must_undo;
+} teco_goto_table_t;
+
+/** @memberof teco_goto_table_t */
+static inline void
+teco_goto_table_init(teco_goto_table_t *ctx, gboolean must_undo)
+{
+ rb3_reset_tree(&ctx->tree);
+ ctx->must_undo = must_undo;
}
-/*
- * Command states
- */
-
-class StateLabel : public State {
-public:
- StateLabel();
+gint teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len);
-private:
- State *custom(gchar chr);
-};
+gint teco_goto_table_find(teco_goto_table_t *ctx, const gchar *name, gsize len);
-class StateGotoCmd : public StateExpectString {
-private:
- State *done(const gchar *str);
+gint teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gint pc);
+void teco_goto_table_undo_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gint pc);
-protected:
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-namespace States {
- extern StateLabel label;
- extern StateGotoCmd gotocmd;
+/** @memberof teco_goto_table_t */
+static inline gboolean
+teco_goto_table_auto_complete(teco_goto_table_t *ctx, const gchar *str, gsize len,
+ teco_string_t *insert)
+{
+ return teco_rb3str_auto_complete(&ctx->tree, TRUE, str, len, 0, insert);
}
-} /* namespace SciTECO */
-
-#endif
+void teco_goto_table_clear(teco_goto_table_t *ctx);
diff --git a/src/help.cpp b/src/help.c
index e40e85e..fe6df1d 100644
--- a/src/help.cpp
+++ b/src/help.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -21,7 +21,7 @@
#include <stdio.h>
#include <stdlib.h>
-#include <errno.h>
+#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
@@ -29,48 +29,89 @@
#include "sciteco.h"
#include "string-utils.h"
+#include "error.h"
#include "parser.h"
-#include "qregisters.h"
+#include "core-commands.h"
+#include "qreg.h"
#include "ring.h"
#include "interface.h"
+#include "rb3str.h"
#include "help.h"
-namespace SciTECO {
+static void teco_help_set(const gchar *topic_name, const gchar *filename, teco_int_t pos);
-HelpIndex help_index;
+static GStringChunk *teco_help_chunk = NULL;
-namespace States {
- StateGetHelp gethelp;
+/** @extends teco_rb3str_head_t */
+typedef struct {
+ teco_rb3str_head_t head;
+
+ teco_int_t pos;
+ gchar filename[];
+} teco_help_topic_t;
+
+/** @static @memberof teco_help_topic_t */
+static teco_help_topic_t *
+teco_help_topic_new(const gchar *topic_name, const gchar *filename, teco_int_t pos)
+{
+ /*
+ * Topics are inserted only once into the RB tree, so we can store
+ * the strings in a GStringChunk.
+ *
+ * FIXME: The same should be true for teco_help_topic_t object itself.
+ * It could be allocated via a stack allocator.
+ */
+ teco_help_topic_t *topic = g_malloc0(sizeof(teco_help_topic_t) + strlen(filename) + 1);
+ teco_string_init_chunk(&topic->head.name, topic_name, strlen(topic_name), teco_help_chunk);
+ topic->pos = pos;
+ strcpy(topic->filename, filename);
+ return topic;
}
-void
-HelpIndex::load(void)
+/** @memberof teco_help_topic_t */
+static inline void
+teco_help_topic_free(teco_help_topic_t *ctx)
{
- gchar *lib_path;
- gchar *women_path;
- GDir *women_dir;
- const gchar *basename;
+ /*
+ * NOTE: The topic name is allocated via GStringChunk and can only be
+ * be deallocated together.
+ */
+ g_free(ctx);
+}
+
+static teco_rb3str_tree_t teco_help_tree;
- if (G_LIKELY(min() != NULL))
+static gboolean
+teco_help_init(GError **error)
+{
+ if (G_LIKELY(teco_help_chunk != NULL))
/* already loaded */
- return;
+ return TRUE;
- lib_path = QRegisters::globals["$SCITECOPATH"]->get_string();
- women_path = g_build_filename(lib_path, "women", NIL);
- g_free(lib_path);
+ teco_help_chunk = g_string_chunk_new(32);
+ rb3_reset_tree(&teco_help_tree);
- women_dir = g_dir_open(women_path, 0, NULL);
- if (!women_dir) {
- g_free(women_path);
- return;
- }
+ teco_qreg_t *lib_reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECOPATH", 12);
+ g_assert(lib_reg != NULL);
+ g_auto(teco_string_t) lib_path = {NULL, 0};
+ if (!lib_reg->vtable->get_string(lib_reg, &lib_path.data, &lib_path.len, error))
+ return FALSE;
+ /*
+ * FIXME: lib_path may contain null-bytes.
+ * It's not clear how to deal with this.
+ */
+ g_autofree gchar *women_path = g_build_filename(lib_path.data, "women", NULL);
- while ((basename = g_dir_read_name(women_dir))) {
- gchar *filename, *filename_tec;
- FILE *file;
- gchar buffer[1024];
- gchar *topic;
+ /*
+ * FIXME: We might want to gracefully handle only the G_FILE_ERROR_NOENT
+ * error and propagate all other errors?
+ */
+ g_autoptr(GDir) women_dir = g_dir_open(women_path, 0, NULL);
+ if (!women_dir)
+ return TRUE;
+ const gchar *basename;
+ while ((basename = g_dir_read_name(women_dir))) {
if (!g_str_has_suffix(basename, ".woman"))
continue;
@@ -78,10 +119,9 @@ HelpIndex::load(void)
* Open the corresponding SciTECO macro to read
* its first line.
*/
- filename = g_build_filename(women_path, basename, NIL);
- filename_tec = g_strconcat(filename, ".tec", NIL);
- file = g_fopen(filename_tec, "r");
- g_free(filename_tec);
+ g_autofree gchar *filename = g_build_filename(women_path, basename, NULL);
+ g_autofree gchar *filename_tec = g_strconcat(filename, ".tec", NULL);
+ g_autoptr(FILE) file = g_fopen(filename_tec, "r");
if (!file) {
/*
* There might simply be no support script for
@@ -89,10 +129,8 @@ HelpIndex::load(void)
* In this case we create a topic using the filename
* without an extension.
*/
- topic = g_strndup(basename, strlen(basename)-6);
- set(topic, filename);
- g_free(topic);
- g_free(filename);
+ g_autofree gchar *topic = g_strndup(basename, strlen(basename)-6);
+ teco_help_set(topic, filename, 0);
continue;
}
@@ -101,25 +139,25 @@ HelpIndex::load(void)
* header containing the position to topic index.
* Every topic will be on its own line and they are unlikely
* to be very long, so we can use fgets() here.
+ *
* NOTE: Since we haven't opened with the "b" flag,
* fgets() will translate linebreaks to LF even on
* MSVCRT (Windows).
*/
+ gchar buffer[1024];
if (!fgets(buffer, sizeof(buffer), file) ||
!g_str_has_prefix(buffer, "!*")) {
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Missing or invalid topic line in womanpage script \"%s\"",
- filename);
- g_free(filename);
+ teco_interface_msg(TECO_MSG_WARNING,
+ "Missing or invalid topic line in womanpage script \"%s\"",
+ filename);
continue;
}
/* skip opening comment */
- topic = buffer+2;
+ gchar *topic = buffer+2;
do {
gchar *endptr;
- tecoInt pos = strtoul(topic, &endptr, 10);
- gsize len;
+ teco_int_t pos = strtoul(topic, &endptr, 10);
/*
* This also breaks at the last line of the
@@ -131,26 +169,20 @@ HelpIndex::load(void)
/*
* Strip the likely LF at the end of the line.
*/
- len = strlen(endptr)-1;
+ gsize len = strlen(endptr)-1;
if (G_LIKELY(endptr[len] == '\n'))
endptr[len] = '\0';
- set(endptr+1, filename, pos);
+ teco_help_set(endptr+1, filename, pos);
} while ((topic = fgets(buffer, sizeof(buffer), file)));
-
- fclose(file);
- g_free(filename);
}
- g_dir_close(women_dir);
- g_free(women_path);
+ return TRUE;
}
-HelpIndex::Topic *
-HelpIndex::find(const gchar *name)
+static inline teco_help_topic_t *
+teco_help_find(const gchar *topic_name)
{
- Topic *ret;
-
/*
* The topic index contains printable characters
* only (to avoid having to perform string building
@@ -159,24 +191,16 @@ HelpIndex::find(const gchar *name)
* Therefore, we expand control characters in the
* look-up string to their printable forms.
*/
- gchar *term = String::canonicalize_ctl(name);
-
- ret = (Topic *)RBTreeStringCase::find(term);
-
- g_free(term);
- return ret;
+ g_autofree gchar *term = teco_string_echo(topic_name, strlen(topic_name));
+ return (teco_help_topic_t *)teco_rb3str_find(&teco_help_tree, FALSE, term, strlen(term));
}
-void
-HelpIndex::set(const gchar *name, const gchar *filename, tecoInt pos)
+static void
+teco_help_set(const gchar *topic_name, const gchar *filename, teco_int_t pos)
{
- Topic *topic = new Topic(name, filename, pos);
- Topic *existing;
-
- existing = (Topic *)RBTree<RBEntryString>::find(topic);
+ teco_help_topic_t *topic;
+ teco_help_topic_t *existing = teco_help_find(topic_name);
if (existing) {
- gchar *basename;
-
if (!strcmp(existing->filename, filename)) {
/*
* A topic with the same name already exists
@@ -186,28 +210,113 @@ HelpIndex::set(const gchar *name, const gchar *filename, tecoInt pos)
* FIXME: Perhaps make it unique again!?
*/
existing->pos = pos;
- delete topic;
return;
}
/* in another file -> make name unique */
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Topic collision: \"%s\" defined in \"%s\" and \"%s\"",
- name, existing->filename, filename);
-
- String::append(topic->name, ":");
- basename = g_path_get_basename(filename);
- String::append(topic->name, basename);
- g_free(basename);
+ teco_interface_msg(TECO_MSG_WARNING,
+ "Topic collision: \"%s\" defined in \"%s\" and \"%s\"",
+ topic_name, existing->filename, filename);
+
+ g_autofree gchar *basename = g_path_get_basename(filename);
+ g_autofree gchar *unique_name = g_strconcat(topic_name, ":", basename, NULL);
+ topic = teco_help_topic_new(unique_name, filename, pos);
+ } else {
+ topic = teco_help_topic_new(topic_name, filename, pos);
}
- RBTree::insert(topic);
+ teco_rb3str_insert(&teco_help_tree, FALSE, &topic->head);
+}
+
+gboolean
+teco_help_auto_complete(const gchar *topic_name, teco_string_t *insert)
+{
+ return teco_rb3str_auto_complete(&teco_help_tree, FALSE, topic_name,
+ topic_name ? strlen(topic_name) : 0, 0, insert);
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_help_cleanup(void)
+{
+ if (!teco_help_chunk)
+ /* not initialized */
+ return;
+ g_string_chunk_free(teco_help_chunk);
+
+ struct rb3_head *cur;
+
+ while ((cur = rb3_get_root(&teco_help_tree))) {
+ rb3_unlink_and_rebalance(cur);
+ teco_help_topic_free((teco_help_topic_t *)cur);
+ }
}
+#endif
/*
* Command states
*/
+static gboolean
+teco_state_help_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ /*
+ * The help-index is populated on demand,
+ * so we start up quicker and batch mode does
+ * not depend on the availability of the standard
+ * library.
+ */
+ return teco_help_init(error);
+}
+
+static teco_state_t *
+teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (teco_string_contains(str, '\0')) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Help topic must not contain null-byte");
+ return NULL;
+ }
+ teco_help_topic_t *topic = teco_help_find(str->data);
+ if (!topic) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Topic \"%s\" not found", str->data);
+ return NULL;
+ }
+
+ teco_ring_undo_edit();
+ /*
+ * ED hooks with the default lexer framework
+ * will usually load the styling SciTECO script
+ * when editing the buffer for the first time.
+ */
+ if (!teco_ring_edit(topic->filename, error))
+ return NULL;
+
+ /*
+ * Make sure the topic is visible.
+ * We do need undo tokens for this (even though
+ * the buffer is removed on rubout if the woman
+ * page is viewed first) since we might browse
+ * multiple topics in the same buffer without
+ * closing it first.
+ */
+ undo__teco_interface_ssm(SCI_GOTOPOS,
+ teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0);
+ teco_interface_ssm(SCI_GOTOPOS, topic->pos, 0);
+
+ return &teco_state_start;
+}
+
+/* in cmdline.c */
+gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error);
+
/*$ "?" help
* ?[topic]$ -- Get help for topic
*
@@ -263,7 +372,7 @@ HelpIndex::set(const gchar *name, const gchar *filename, tecoInt pos)
* \fIgrosciteco\fP formatter and the \fIsciteco.tmac\fP
* GNU troff macros.
* When using womanpages generated by \fIgrosciteco\fP,
- * help topics can be defined using the \fBSCITECO_TOPIC\fP
+ * help topics can be defined using the \fBTECO_TOPIC\fP
* Troff macro.
* This flexible system allows \*(ST to access internal
* and third-party help files written in plain-text or
@@ -273,50 +382,8 @@ HelpIndex::set(const gchar *name, const gchar *filename, tecoInt pos)
*
* The \fB?\fP command does not have string building enabled.
*/
-void
-StateGetHelp::initial(void)
-{
- /*
- * The help-index is populated on demand,
- * so we start up quicker and batch mode does
- * not depend on the availability of the standard
- * library.
- */
- help_index.load();
-}
-
-State *
-StateGetHelp::done(const gchar *str)
-{
- HelpIndex::Topic *topic;
-
- BEGIN_EXEC(&States::start);
-
- topic = help_index.find(str);
- if (!topic)
- throw Error("Topic \"%s\" not found", str);
-
- ring.undo_edit();
- /*
- * ED hooks with the default lexer framework
- * will usually load the styling SciTECO script
- * when editing the buffer for the first time.
- */
- ring.edit(topic->filename);
-
- /*
- * Make sure the topic is visible.
- * We do need undo tokens for this (even though
- * the buffer is removed on rubout if the woman
- * page is viewed first) since we might browse
- * multiple topics in the same buffer without
- * closing it first.
- */
- interface.undo_ssm(SCI_GOTOPOS,
- interface.ssm(SCI_GETCURRENTPOS));
- interface.ssm(SCI_GOTOPOS, topic->pos);
-
- return &States::start;
-}
-
-} /* namespace SciTECO */
+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,
+ .expectstring.string_building = FALSE
+);
diff --git a/src/help.h b/src/help.h
index 6287f4d..2859a03 100644
--- a/src/help.h
+++ b/src/help.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,85 +14,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __HELP_H
-#define __HELP_H
-
-#include <string.h>
+#pragma once
#include <glib.h>
-#include <glib/gprintf.h>
-#include "sciteco.h"
-#include "memory.h"
+#include "string-utils.h"
#include "parser.h"
-#include "undo.h"
-#include "rbtree.h"
-
-namespace SciTECO {
-
-class HelpIndex : private RBTreeStringCase, public Object {
-public:
- class Topic : public RBTreeStringCase::RBEntryOwnString {
- public:
- gchar *filename;
- tecoInt pos;
-
- Topic(const gchar *name, const gchar *_filename = NULL, tecoInt _pos = 0)
- : RBEntryOwnString(name),
- filename(_filename ? g_strdup(_filename) : NULL),
- pos(_pos) {}
- ~Topic()
- {
- g_free(filename);
- }
- };
-
- ~HelpIndex()
- {
- Topic *cur;
-
- while ((cur = (Topic *)root()))
- delete (Topic *)remove(cur);
- }
-
- void load(void);
- Topic *find(const gchar *name);
-
- void set(const gchar *name, const gchar *filename,
- tecoInt pos = 0);
-
- inline gchar *
- auto_complete(const gchar *name, gchar completed = '\0')
- {
- return RBTreeStringCase::auto_complete(name, completed);
- }
-};
-
-extern HelpIndex help_index;
+gboolean teco_help_auto_complete(const gchar *topic_name, teco_string_t *insert);
/*
* Command states
*/
-class StateGetHelp : public StateExpectString {
-public:
- StateGetHelp() : StateExpectString(false) {}
-
-private:
- void initial(void);
- State *done(const gchar *str);
-
-protected:
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-namespace States {
- extern StateGetHelp gethelp;
-}
-
-} /* namespace SciTECO */
-
-#endif
+TECO_DECLARE_STATE(teco_state_help);
diff --git a/src/interface-curses/Makefile.am b/src/interface-curses/Makefile.am
index 01e68ae..14fc920 100644
--- a/src/interface-curses/Makefile.am
+++ b/src/interface-curses/Makefile.am
@@ -1,8 +1,9 @@
-AM_CPPFLAGS += -I$(top_srcdir)/src
+AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr \
+ -I$(top_srcdir)/src
-AM_CXXFLAGS = -Wall -Wno-char-subscripts
+AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value
noinst_LTLIBRARIES = libsciteco-interface.la
-libsciteco_interface_la_SOURCES = interface-curses.cpp interface-curses.h \
- curses-utils.cpp curses-utils.h \
- curses-info-popup.cpp curses-info-popup.h
+libsciteco_interface_la_SOURCES = interface.c \
+ curses-utils.c curses-utils.h \
+ curses-info-popup.c curses-info-popup.h
diff --git a/src/interface-curses/curses-info-popup.c b/src/interface-curses/curses-info-popup.c
new file mode 100644
index 0000000..7d661a2
--- /dev/null
+++ b/src/interface-curses/curses-info-popup.c
@@ -0,0 +1,211 @@
+/*
+ * 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 <curses.h>
+
+#include "list.h"
+#include "string-utils.h"
+#include "interface.h"
+#include "curses-utils.h"
+#include "curses-info-popup.h"
+
+/*
+ * FIXME: This is redundant with teco-gtk-info-popup.gob.
+ */
+typedef struct {
+ teco_stailq_entry_t entry;
+
+ teco_popup_entry_type_t type;
+ teco_string_t name;
+ gboolean highlight;
+} teco_popup_entry_t;
+
+void
+teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_type_t type,
+ const gchar *name, gsize name_len, gboolean highlight)
+{
+ if (G_UNLIKELY(!ctx->chunk))
+ ctx->chunk = g_string_chunk_new(32);
+
+ /*
+ * FIXME: Test with g_slice_new()...
+ * It could however cause problems upon command-line termination
+ * and may not be measurably faster.
+ */
+ teco_popup_entry_t *entry = g_new(teco_popup_entry_t, 1);
+ entry->type = type;
+ /*
+ * Popup entries aren't removed individually, so we can
+ * more efficiently store them via GStringChunk.
+ */
+ teco_string_init_chunk(&entry->name, name, name_len, ctx->chunk);
+ entry->highlight = highlight;
+
+ teco_stailq_insert_tail(&ctx->list, &entry->entry);
+
+ ctx->longest = MAX(ctx->longest, (gint)name_len);
+ ctx->length++;
+}
+
+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 */
+
+ /* reserve 2 spaces between columns */
+ pad_colwidth = MIN(ctx->longest + 2, cols - 2);
+
+ /* pad_cols = floor((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;
+
+ /*
+ * Render the entire autocompletion list into a pad
+ * which can be higher than the physical screen.
+ * The pad uses two columns less than the screen since
+ * it will be drawn into the popup window which has left
+ * and right borders.
+ */
+ ctx->pad = newpad(pad_lines, cols - 2);
+
+ wbkgd(ctx->pad, ' ' | attr);
+
+ /*
+ * cur_col is the row currently written.
+ * It does not wrap but grows indefinitely.
+ * Therefore the real current row is (cur_col % popup_cols)
+ */
+ gint cur_col = 0;
+ for (teco_stailq_entry_t *cur = ctx->list.first; cur != NULL; cur = cur->next) {
+ teco_popup_entry_t *entry = (teco_popup_entry_t *)cur;
+ gint cur_line = cur_col/pad_cols + 1;
+
+ wmove(ctx->pad, cur_line-1,
+ (cur_col % pad_cols)*pad_colwidth);
+
+ wattrset(ctx->pad, entry->highlight ? A_BOLD : A_NORMAL);
+
+ switch (entry->type) {
+ case TECO_POPUP_FILE:
+ case TECO_POPUP_DIRECTORY:
+ g_assert(!teco_string_contains(&entry->name, '\0'));
+ teco_curses_format_filename(ctx->pad, entry->name.data, -1);
+ break;
+ default:
+ teco_curses_format_str(ctx->pad, entry->name.data, entry->name.len, -1);
+ break;
+ }
+
+ cur_col++;
+ }
+}
+
+void
+teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr)
+{
+ if (!ctx->length)
+ /* nothing to display */
+ return;
+
+ int lines, cols; /* screen dimensions */
+ getmaxyx(stdscr, lines, cols);
+
+ if (ctx->window)
+ delwin(ctx->window);
+
+ if (!ctx->pad)
+ teco_curses_info_popup_init_pad(ctx, attr);
+ gint pad_lines = getmaxy(ctx->pad);
+
+ /*
+ * 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);
+
+ /* window covers message, scintilla and info windows */
+ ctx->window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0);
+
+ wbkgdset(ctx->window, ' ' | attr);
+
+ wborder(ctx->window,
+ ACS_VLINE,
+ ACS_VLINE, /* may be overwritten with scrollbar */
+ ACS_HLINE,
+ ' ', /* no bottom line */
+ ACS_ULCORNER, ACS_URCORNER,
+ ACS_VLINE, ACS_VLINE);
+
+ copywin(ctx->pad, ctx->window,
+ ctx->pad_first_line, 0,
+ 1, 1, popup_lines - 1, cols - 2, FALSE);
+
+ if (pad_lines <= popup_lines - 1)
+ /* no need for scrollbar */
+ return;
+
+ /* bar_height = ceil((popup_lines-1)/pad_lines * (popup_lines-2)) */
+ gint bar_height = ((popup_lines-1)*(popup_lines-2) + pad_lines-1) /
+ pad_lines;
+ /* 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);
+ /*
+ * 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);
+ wattron(ctx->window, A_REVERSE);
+ wvline(ctx->window, ' ', bar_height);
+
+ /* progress scroll position */
+ ctx->pad_first_line += popup_lines - 1;
+ /* wrap on last shown page */
+ ctx->pad_first_line %= pad_lines;
+ if (pad_lines - ctx->pad_first_line < popup_lines - 1)
+ /* show last page */
+ ctx->pad_first_line = pad_lines - (popup_lines - 1);
+}
+
+void
+teco_curses_info_popup_clear(teco_curses_info_popup_t *ctx)
+{
+ if (ctx->window)
+ delwin(ctx->window);
+ if (ctx->pad)
+ delwin(ctx->pad);
+ if (ctx->chunk)
+ g_string_chunk_free(ctx->chunk);
+
+ teco_stailq_entry_t *entry;
+ while ((entry = teco_stailq_remove_head(&ctx->list)))
+ g_free(entry);
+
+ teco_curses_info_popup_init(ctx);
+}
diff --git a/src/interface-curses/curses-info-popup.cpp b/src/interface-curses/curses-info-popup.cpp
deleted file mode 100644
index 487f1b7..0000000
--- a/src/interface-curses/curses-info-popup.cpp
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <string.h>
-
-#include <glib.h>
-
-#include <curses.h>
-
-#include "curses-utils.h"
-#include "curses-info-popup.h"
-
-namespace SciTECO {
-
-void
-CursesInfoPopup::add(PopupEntryType type,
- const gchar *name, bool highlight)
-{
- size_t name_len = strlen(name);
- Entry *entry = (Entry *)g_malloc(sizeof(Entry) + name_len + 1);
-
- entry->type = type;
- entry->highlight = highlight;
- strcpy(entry->name, name);
-
- longest = MAX(longest, (gint)name_len);
- length++;
-
- /*
- * Entries are added in reverse (constant time for GSList),
- * so they will later have to be reversed.
- */
- list = g_slist_prepend(list, entry);
-}
-
-void
-CursesInfoPopup::init_pad(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 */
-
- gint cur_col;
-
- /* reserve 2 spaces between columns */
- pad_colwidth = MIN(longest + 2, cols - 2);
-
- /* pad_cols = floor((cols - 2) / pad_colwidth) */
- pad_cols = (cols - 2) / pad_colwidth;
- /* pad_lines = ceil(length / pad_cols) */
- pad_lines = (length+pad_cols-1) / pad_cols;
-
- /*
- * Render the entire autocompletion list into a pad
- * which can be higher than the physical screen.
- * The pad uses two columns less than the screen since
- * it will be drawn into the popup window which has left
- * and right borders.
- */
- pad = newpad(pad_lines, cols - 2);
-
- wbkgd(pad, ' ' | attr);
-
- /*
- * cur_col is the row currently written.
- * It does not wrap but grows indefinitely.
- * Therefore the real current row is (cur_col % popup_cols)
- */
- cur_col = 0;
- for (GSList *cur = list; cur != NULL; cur = g_slist_next(cur)) {
- Entry *entry = (Entry *)cur->data;
- gint cur_line = cur_col/pad_cols + 1;
-
- wmove(pad, cur_line-1,
- (cur_col % pad_cols)*pad_colwidth);
-
- wattrset(pad, entry->highlight ? A_BOLD : A_NORMAL);
-
- switch (entry->type) {
- case POPUP_FILE:
- case POPUP_DIRECTORY:
- Curses::format_filename(pad, entry->name);
- break;
- default:
- Curses::format_str(pad, entry->name);
- break;
- }
-
- cur_col++;
- }
-}
-
-void
-CursesInfoPopup::show(attr_t attr)
-{
- int lines, cols; /* screen dimensions */
- gint pad_lines;
- gint popup_lines;
- gint bar_height, bar_y;
-
- if (!length)
- /* nothing to display */
- return;
-
- getmaxyx(stdscr, lines, cols);
-
- if (window)
- delwin(window);
- else
- /* reverse list only once */
- list = g_slist_reverse(list);
-
- if (!pad)
- init_pad(attr);
- pad_lines = getmaxy(pad);
-
- /*
- * Popup window can cover all but one screen row.
- * Another row is reserved for the top border.
- */
- popup_lines = MIN(pad_lines + 1, lines - 1);
-
- /* window covers message, scintilla and info windows */
- window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0);
-
- wbkgdset(window, ' ' | attr);
-
- wborder(window,
- ACS_VLINE,
- ACS_VLINE, /* may be overwritten with scrollbar */
- ACS_HLINE,
- ' ', /* no bottom line */
- ACS_ULCORNER, ACS_URCORNER,
- ACS_VLINE, ACS_VLINE);
-
- copywin(pad, window,
- pad_first_line, 0,
- 1, 1, popup_lines - 1, cols - 2, FALSE);
-
- if (pad_lines <= popup_lines - 1)
- /* no need for scrollbar */
- return;
-
- /* bar_height = ceil((popup_lines-1)/pad_lines * (popup_lines-2)) */
- bar_height = ((popup_lines-1)*(popup_lines-2) + pad_lines-1) /
- pad_lines;
- /* bar_y = floor(pad_first_line/pad_lines * (popup_lines-2)) + 1 */
- bar_y = pad_first_line*(popup_lines-2) / pad_lines + 1;
-
- mvwvline(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(window, bar_y, cols-1);
- wattron(window, A_REVERSE);
- wvline(window, ' ', bar_height);
-
- /* progress scroll position */
- pad_first_line += popup_lines - 1;
- /* wrap on last shown page */
- pad_first_line %= pad_lines;
- if (pad_lines - pad_first_line < popup_lines - 1)
- /* show last page */
- pad_first_line = pad_lines - (popup_lines - 1);
-}
-
-void
-CursesInfoPopup::clear(void)
-{
- g_slist_free_full(list, g_free);
- list = NULL;
- length = 0;
- longest = 0;
-
- pad_first_line = 0;
-
- if (window) {
- delwin(window);
- window = NULL;
- }
-
- if (pad) {
- delwin(pad);
- pad = NULL;
- }
-}
-
-CursesInfoPopup::~CursesInfoPopup()
-{
- if (window)
- delwin(window);
- if (pad)
- delwin(pad);
- if (list)
- g_slist_free_full(list, g_free);
-}
-
-} /* namespace SciTECO */
diff --git a/src/interface-curses/curses-info-popup.h b/src/interface-curses/curses-info-popup.h
index af09cb4..d911182 100644
--- a/src/interface-curses/curses-info-popup.h
+++ b/src/interface-curses/curses-info-popup.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,78 +14,52 @@
* 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
-#ifndef __CURSES_INFO_POPUP_H
-#define __CURSES_INFO_POPUP_H
+#include <string.h>
#include <glib.h>
#include <curses.h>
-#include "memory.h"
+#include "list.h"
+#include "interface.h"
-namespace SciTECO {
+typedef struct {
+ WINDOW *window; /**! window showing part of pad */
+ WINDOW *pad; /**! full-height entry list */
-class CursesInfoPopup : public Object {
-public:
- /**
- * @bug This is identical to the type defined in
- * interface.h. But for the sake of abstraction
- * we cannot access it here (or in gtk-info-popup
- * for that matter).
- */
- enum PopupEntryType {
- POPUP_PLAIN,
- POPUP_FILE,
- POPUP_DIRECTORY
- };
+ teco_stailq_head_t list; /**! list of popup entries */
+ gint longest; /**! size of longest entry */
+ gint length; /**! total number of popup entries */
-private:
- WINDOW *window; /**! window showing part of pad */
- WINDOW *pad; /**! full-height entry list */
+ gint pad_first_line; /**! first line in pad to show */
- struct Entry {
- PopupEntryType type;
- bool highlight;
- gchar name[];
- };
+ GStringChunk *chunk; /**! string chunk for all popup entry names */
+} teco_curses_info_popup_t;
- GSList *list; /**! list of popup entries */
- gint longest; /**! size of longest entry */
- gint length; /**! total number of popup entries */
+static inline void
+teco_curses_info_popup_init(teco_curses_info_popup_t *ctx)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->list = TECO_STAILQ_HEAD_INITIALIZER(&ctx->list);
+}
- gint pad_first_line; /**! first line in pad to show */
+void teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_type_t type,
+ const gchar *name, gsize name_len, gboolean highlight);
-public:
- CursesInfoPopup() : window(NULL), pad(NULL),
- list(NULL), longest(0), length(0),
- pad_first_line(0) {}
+void teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr);
+static inline bool
+teco_curses_info_popup_is_shown(teco_curses_info_popup_t *ctx)
+{
+ return ctx->window != NULL;
+}
- void add(PopupEntryType type,
- const gchar *name, bool highlight = false);
+static inline void
+teco_curses_info_popup_noutrefresh(teco_curses_info_popup_t *ctx)
+{
+ if (ctx->window)
+ wnoutrefresh(ctx->window);
+}
- void show(attr_t attr);
- inline bool
- is_shown(void)
- {
- return window != NULL;
- }
-
- void clear(void);
-
- inline void
- noutrefresh(void)
- {
- if (window)
- wnoutrefresh(window);
- }
-
- ~CursesInfoPopup();
-
-private:
- void init_pad(attr_t attr);
-};
-
-} /* namespace SciTECO */
-
-#endif
+void teco_curses_info_popup_clear(teco_curses_info_popup_t *ctx);
diff --git a/src/interface-curses/curses-utils.cpp b/src/interface-curses/curses-utils.c
index f5d5c8c..ace5795 100644
--- a/src/interface-curses/curses-utils.cpp
+++ b/src/interface-curses/curses-utils.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -29,19 +29,14 @@
#include "string-utils.h"
#include "curses-utils.h"
-namespace SciTECO {
-
gsize
-Curses::format_str(WINDOW *win, const gchar *str,
- gssize len, gint max_width)
+teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width)
{
int old_x, old_y;
gint chars_added = 0;
getyx(win, old_y, old_x);
- if (len < 0)
- len = strlen(str);
if (max_width < 0)
max_width = getmaxx(win) - old_x;
@@ -51,7 +46,7 @@ Curses::format_str(WINDOW *win, const gchar *str,
* View::set_representations()
*/
switch (*str) {
- case CTL_KEY_ESC:
+ case '\e':
chars_added++;
if (chars_added > max_width)
goto truncate;
@@ -80,12 +75,12 @@ Curses::format_str(WINDOW *win, const gchar *str,
waddch(win, 'B' | A_REVERSE);
break;
default:
- if (IS_CTL(*str)) {
+ if (TECO_IS_CTL(*str)) {
chars_added += 2;
if (chars_added > max_width)
goto truncate;
waddch(win, '^' | A_REVERSE);
- waddch(win, CTL_ECHO(*str) | A_REVERSE);
+ waddch(win, TECO_CTL_ECHO(*str) | A_REVERSE);
} else {
chars_added++;
if (chars_added > max_width)
@@ -114,29 +109,29 @@ truncate:
}
gsize
-Curses::format_filename(WINDOW *win, const gchar *filename,
- gint max_width)
+teco_curses_format_filename(WINDOW *win, const gchar *filename,
+ gint max_width)
{
int old_x = getcurx(win);
- gchar *filename_canon = String::canonicalize_ctl(filename);
- size_t filename_len = strlen(filename_canon);
+ g_autofree gchar *filename_printable = teco_string_echo(filename, strlen(filename));
+ size_t filename_len = strlen(filename_printable);
if (max_width < 0)
max_width = getmaxx(win) - old_x;
if (filename_len <= (size_t)max_width) {
- waddstr(win, filename_canon);
+ waddstr(win, filename_printable);
} else {
- const gchar *keep_post = filename_canon + filename_len -
+ const gchar *keep_post = filename_printable + filename_len -
max_width + 3;
#ifdef G_OS_WIN32
- const gchar *keep_pre = g_path_skip_root(filename_canon);
+ const gchar *keep_pre = g_path_skip_root(filename_printable);
if (keep_pre) {
- waddnstr(win, filename_canon,
- keep_pre - filename_canon);
- keep_post += keep_pre - filename_canon;
+ waddnstr(win, filename_printable,
+ keep_pre - filename_printable);
+ keep_post += keep_pre - filename_printable;
}
#endif
wattron(win, A_UNDERLINE | A_BOLD);
@@ -145,8 +140,5 @@ Curses::format_filename(WINDOW *win, const gchar *filename,
waddstr(win, keep_post);
}
- g_free(filename_canon);
return getcurx(win) - old_x;
}
-
-} /* namespace SciTECO */
diff --git a/src/interface-curses/curses-utils.h b/src/interface-curses/curses-utils.h
index 778f39e..3a681a4 100644
--- a/src/interface-curses/curses-utils.h
+++ b/src/interface-curses/curses-utils.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,26 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __CURSES_UTILS_H
-#define __CURSES_UTILS_H
+#pragma once
#include <glib.h>
#include <curses.h>
-namespace SciTECO {
-
-namespace Curses {
-
-gsize format_str(WINDOW *win, const gchar *str,
- gssize len = -1, gint max_width = -1);
-
-gsize format_filename(WINDOW *win, const gchar *filename,
- gint max_width = -1);
-
-} /* namespace Curses */
-
-} /* namespace SciTECO */
+gsize teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width);
-#endif
+gsize teco_curses_format_filename(WINDOW *win, const gchar *filename, gint max_width);
diff --git a/src/interface-curses/interface-curses.h b/src/interface-curses/interface-curses.h
deleted file mode 100644
index 32fff1d..0000000
--- a/src/interface-curses/interface-curses.h
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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/>.
- */
-
-#ifndef __INTERFACE_CURSES_H
-#define __INTERFACE_CURSES_H
-
-#include <stdarg.h>
-
-#include <glib.h>
-
-#include <curses.h>
-
-#include <Scintilla.h>
-#include <ScintillaTerm.h>
-
-#include "interface.h"
-#include "curses-info-popup.h"
-
-namespace SciTECO {
-
-typedef class ViewCurses : public View<ViewCurses> {
- Scintilla *sci;
-
-public:
- ViewCurses() : sci(NULL) {}
-
- /* implementation of View::initialize() */
- void initialize_impl(void);
-
- inline ~ViewCurses()
- {
- /*
- * NOTE: This deletes/frees the view's
- * curses WINDOW, despite of what old versions
- * of the Scinterm documentation claim.
- */
- if (sci)
- scintilla_delete(sci);
- }
-
- inline void
- noutrefresh(void)
- {
- scintilla_noutrefresh(sci);
- }
-
- inline void
- refresh(void)
- {
- scintilla_refresh(sci);
- }
-
- inline WINDOW *
- get_window(void)
- {
- return scintilla_get_window(sci);
- }
-
- /* implementation of View::ssm() */
- inline sptr_t
- ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0)
- {
- return scintilla_send_message(sci, iMessage, wParam, lParam);
- }
-} ViewCurrent;
-
-typedef class InterfaceCurses : public Interface<InterfaceCurses, ViewCurses> {
- /**
- * 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 {
- short r, g, b;
- } orig_color_table[16];
-
- int stdout_orig, stderr_orig;
- SCREEN *screen;
- FILE *screen_tty;
-
- WINDOW *info_window;
- enum {
- INFO_TYPE_BUFFER = 0,
- INFO_TYPE_QREGISTER
- } info_type;
- gchar *info_current;
-
- WINDOW *msg_window;
-
- WINDOW *cmdline_window, *cmdline_pad;
- gsize cmdline_len, cmdline_rubout_len;
-
- CursesInfoPopup popup;
-
-public:
- InterfaceCurses();
- ~InterfaceCurses();
-
- /* override of Interface::init() */
- void init(void);
-
- /* override of Interface::init_color() */
- void init_color(guint color, guint32 rgb);
-
- /* implementation of Interface::vmsg() */
- void vmsg_impl(MessageType type, const gchar *fmt, va_list ap);
- /* override of Interface::msg_clear() */
- void msg_clear(void);
-
- /* implementation of Interface::show_view() */
- void show_view_impl(ViewCurses *view);
-
- /* implementation of Interface::info_update() */
- void info_update_impl(const QRegister *reg);
- void info_update_impl(const Buffer *buffer);
-
- /* implementation of Interface::cmdline_update() */
- void cmdline_update_impl(const Cmdline *cmdline);
-
- /* override of Interface::set_clipboard() */
- void set_clipboard(const gchar *name,
- const gchar *str = NULL, gssize str_len = -1);
- /* override of Interface::get_clipboard() */
- gchar *get_clipboard(const gchar *name, gsize *str_len = NULL);
-
- /* implementation of Interface::popup_add() */
- inline void
- popup_add_impl(PopupEntryType type,
- const gchar *name, bool highlight = false)
- {
- /* FIXME: The enum casting is dangerous */
- if (cmdline_window)
- /* interactive mode */
- popup.add((CursesInfoPopup::PopupEntryType)type,
- name, highlight);
- }
-
- /* implementation of Interface::popup_show() */
- void popup_show_impl(void);
- /* implementation of Interface::popup_is_shown() */
- inline bool
- popup_is_shown_impl(void)
- {
- return popup.is_shown();
- }
-
- /* implementation of Interface::popup_clear() */
- void popup_clear_impl(void);
-
- /* main entry point (implementation) */
- void event_loop_impl(void);
-
-private:
- void init_color_safe(guint color, guint32 rgb);
- void restore_colors(void);
-
- void init_screen(void);
- void init_interactive(void);
- void restore_batch(void);
-
- void init_clipboard(void);
-
- void resize_all_windows(void);
-
- void set_window_title(const gchar *title);
- void draw_info(void);
- void draw_cmdline(void);
-
- friend void event_loop_iter();
-} InterfaceCurrent;
-
-} /* namespace SciTECO */
-
-#endif
diff --git a/src/interface-curses/interface-curses.cpp b/src/interface-curses/interface.c
index a06fe30..821581b 100644
--- a/src/interface-curses/interface-curses.cpp
+++ b/src/interface-curses/interface.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -27,6 +27,15 @@
#include <locale.h>
#include <errno.h>
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#endif
+
+#ifdef EMSCRIPTEN
+#include <emscripten.h>
+#endif
+
#include <glib.h>
#include <glib/gprintf.h>
#include <glib/gstdio.h>
@@ -45,26 +54,17 @@
#include <Scintilla.h>
#include <ScintillaTerm.h>
-#ifdef EMSCRIPTEN
-#include <emscripten.h>
-#endif
-
#include "sciteco.h"
#include "string-utils.h"
#include "cmdline.h"
-#include "qregisters.h"
+#include "qreg.h"
#include "ring.h"
#include "error.h"
-#include "interface.h"
-#include "interface-curses.h"
#include "curses-utils.h"
#include "curses-info-popup.h"
-
-#ifdef HAVE_WINDOWS_H
-/* here it shouldn't cause conflicts with other headers */
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#endif
+#include "view.h"
+#include "memory.h"
+#include "interface.h"
/**
* Whether we have PDCurses-only routines:
@@ -99,18 +99,11 @@
#endif
#ifdef NCURSES_VERSION
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
-/**
- * Whether we're on ncurses/UNIX.
- * Haiku has a UNIX-like terminal and is largely
- * POSIX compliant, so we can handle it like a
- * UNIX ncurses.
- */
+#ifdef G_OS_UNIX
+/** Whether we're on ncurses/UNIX. */
#define NCURSES_UNIX
#elif defined(G_OS_WIN32)
-/**
- * Whether we're on ncurses/win32 console
- */
+/** Whether we're on ncurses/win32 console */
#define NCURSES_WIN32
#endif
#endif
@@ -123,10 +116,6 @@
#define CURSES_TTY
#endif
-namespace SciTECO {
-
-extern "C" {
-
/*
* PDCurses/win32a by default assigns functions to certain
* keys like CTRL+V, CTRL++, CTRL+- and CTRL+=.
@@ -147,9 +136,6 @@ int PDC_set_function_key(const unsigned function, const int new_key);
#define FUNCTION_KEY_CHOOSE_FONT 4
#endif
-static void scintilla_notify(Scintilla *sci, int idFrom,
- void *notify, void *user_data);
-
#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32)
/**
@@ -161,11 +147,11 @@ static void scintilla_notify(Scintilla *sci, int idFrom,
* separate thread.
*/
static BOOL WINAPI
-console_ctrl_handler(DWORD type)
+teco_console_ctrl_handler(DWORD type)
{
switch (type) {
case CTRL_C_EVENT:
- sigint_occurred = TRUE;
+ teco_sigint_occurred = TRUE;
return TRUE;
}
@@ -174,9 +160,7 @@ console_ctrl_handler(DWORD type)
#endif
-} /* extern "C" */
-
-static gint xterm_version(void) G_GNUC_UNUSED;
+static gint teco_xterm_version(void) G_GNUC_UNUSED;
#define UNNAMED_FILE "(Unnamed)"
@@ -223,12 +207,12 @@ static gint xterm_version(void) G_GNUC_UNUSED;
* for each component).
*/
static inline void
-rgb2curses(guint32 rgb, short &r, short &g, short &b)
+teco_rgb2curses_triple(guint32 rgb, gshort *r, gshort *g, gshort *b)
{
/* NOTE: We could also use 200/51 */
- r = ((rgb & 0x0000FF) >> 0)*1000/0xFF;
- g = ((rgb & 0x00FF00) >> 8)*1000/0xFF;
- b = ((rgb & 0xFF0000) >> 16)*1000/0xFF;
+ *r = ((rgb & 0x0000FF) >> 0)*1000/0xFF;
+ *g = ((rgb & 0x00FF00) >> 8)*1000/0xFF;
+ *b = ((rgb & 0xFF0000) >> 16)*1000/0xFF;
}
/**
@@ -240,8 +224,8 @@ rgb2curses(guint32 rgb, short &r, short &g, short &b)
* It is equivalent to Scinterm's internal `term_color`
* function.
*/
-static short
-rgb2curses(guint32 rgb)
+static gshort
+teco_rgb2curses(guint32 rgb)
{
switch (rgb) {
case 0x000000: return COLOR_BLACK;
@@ -266,20 +250,19 @@ rgb2curses(guint32 rgb)
}
static gint
-xterm_version(void)
+teco_xterm_version(void)
{
static gint xterm_patch = -2;
- const gchar *term = g_getenv("TERM");
- const gchar *xterm_version;
-
/*
* The XTerm patch level (version) is cached.
*/
- if (xterm_patch != -2)
+ if (G_LIKELY(xterm_patch != -2))
return xterm_patch;
xterm_patch = -1;
+ const gchar *term = g_getenv("TERM");
+
if (!term || !g_str_has_prefix(term, "xterm"))
/* no XTerm */
return -1;
@@ -290,7 +273,7 @@ xterm_version(void)
* XTERM_VERSION however should be sufficient to tell
* whether we are running under a real XTerm.
*/
- xterm_version = g_getenv("XTERM_VERSION");
+ const gchar *xterm_version = g_getenv("XTERM_VERSION");
if (!xterm_version)
/* no XTerm */
return -1;
@@ -305,32 +288,121 @@ xterm_version(void)
return xterm_patch;
}
-void
-ViewCurses::initialize_impl(void)
+/*
+ * NOTE: The teco_view_t pointer is reused to directly
+ * point to the Scintilla object.
+ * This saves one heap object per view.
+ */
+
+static void
+teco_view_scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data)
+{
+ teco_interface_process_notify(notify);
+}
+
+teco_view_t *
+teco_view_new(void)
+{
+ return (teco_view_t *)scintilla_new(teco_view_scintilla_notify);
+}
+
+static inline void
+teco_view_noutrefresh(teco_view_t *ctx)
+{
+ scintilla_noutrefresh((Scintilla *)ctx);
+}
+
+static inline WINDOW *
+teco_view_get_window(teco_view_t *ctx)
+{
+ return scintilla_get_window((Scintilla *)ctx);
+}
+
+sptr_t
+teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam)
{
- sci = scintilla_new(scintilla_notify);
- setup();
+ return scintilla_send_message((Scintilla *)ctx, iMessage, wParam, lParam);
}
-InterfaceCurses::InterfaceCurses() : stdout_orig(-1), stderr_orig(-1),
- screen(NULL),
- screen_tty(NULL),
- info_window(NULL),
- info_type(INFO_TYPE_BUFFER),
- info_current(NULL),
- msg_window(NULL),
- cmdline_window(NULL), cmdline_pad(NULL),
- cmdline_len(0), cmdline_rubout_len(0)
+void
+teco_view_free(teco_view_t *ctx)
{
- for (guint i = 0; i < G_N_ELEMENTS(color_table); i++)
- color_table[i] = -1;
- for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++)
- orig_color_table[i].r = -1;
+ scintilla_delete((Scintilla *)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;
+
+ WINDOW *msg_window;
+
+ WINDOW *cmdline_window, *cmdline_pad;
+ gsize cmdline_len, cmdline_rubout_len;
+
+ teco_curses_info_popup_t popup;
+
+ /**
+ * 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);
+
+static void teco_interface_init_screen(void);
+static gboolean teco_interface_init_interactive(GError **error);
+static void teco_interface_restore_batch(void);
+
+static void teco_interface_init_clipboard(void);
+
+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
-InterfaceCurses::init(void)
+teco_interface_init(void)
{
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.color_table); i++)
+ teco_interface.color_table[i] = -1;
+ 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_curses_info_popup_init(&teco_interface.popup);
+
/*
* We must register this handler to handle
* asynchronous interruptions via CTRL+C
@@ -338,40 +410,45 @@ InterfaceCurses::init(void)
* have won't do.
*/
#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32)
- SetConsoleCtrlHandler(console_ctrl_handler, TRUE);
+ SetConsoleCtrlHandler(teco_console_ctrl_handler, TRUE);
#endif
/*
* Make sure we have a string for the info line
- * even if info_update() is never called.
+ * even if teco_interface_info_update() is never called.
*/
- info_current = g_strdup(PACKAGE_NAME);
+ teco_string_init(&teco_interface.info_current, PACKAGE_NAME, strlen(PACKAGE_NAME));
/*
* On all platforms except Curses/XTerm, it's
* safe to initialize the clipboards now.
*/
#ifndef CURSES_TTY
- init_clipboard();
+ teco_interface_init_clipboard();
#endif
}
-void
-InterfaceCurses::init_color_safe(guint color, guint32 rgb)
+GOptionGroup *
+teco_interface_get_options(void)
{
- short r, g, b;
+ return NULL;
+}
+static void
+teco_interface_init_color_safe(guint color, guint32 rgb)
+{
#ifdef PDCURSES_WIN32
- if (orig_color_table[color].r < 0) {
+ if (teco_interface.orig_color_table[color].r < 0) {
color_content((short)color,
- &orig_color_table[color].r,
- &orig_color_table[color].g,
- &orig_color_table[color].b);
+ &teco_interface.orig_color_table[color].r,
+ &teco_interface.orig_color_table[color].g,
+ &teco_interface.orig_color_table[color].b);
}
#endif
- rgb2curses(rgb, r, g, b);
- ::init_color((short)color, r, g, b);
+ gshort r, g, b;
+ teco_rgb2curses_triple(rgb, &r, &g, &b);
+ init_color((short)color, r, g, b);
}
#ifdef PDCURSES_WIN32
@@ -381,29 +458,29 @@ InterfaceCurses::init_color_safe(guint color, guint32 rgb)
* the real console color palette - or at least the default
* palette when the console started.
*/
-void
-InterfaceCurses::restore_colors(void)
+static void
+teco_interface_restore_colors(void)
{
if (!can_change_color())
return;
- for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++) {
- if (orig_color_table[i].r < 0)
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.orig_color_table); i++) {
+ if (teco_interface.orig_color_table[i].r < 0)
continue;
- ::init_color((short)i,
- orig_color_table[i].r,
- orig_color_table[i].g,
- orig_color_table[i].b);
+ init_color((short)i,
+ teco_interface.orig_color_table[i].r,
+ teco_interface.orig_color_table[i].g,
+ teco_interface.orig_color_table[i].b);
}
}
#elif defined(CURSES_TTY)
/*
- * FIXME: On UNIX/ncurses init_color_safe() __may__ change the
- * terminal's palette permanently and there does not appear to be
- * any portable way of restoring the original one.
+ * FIXME: On UNIX/ncurses teco_interface_init_color_safe() __may__
+ * change the terminal's palette permanently and there does not
+ * appear to be any portable way of restoring the original one.
* Curses has color_content(), but there is actually no terminal
* that allows querying the current palette and so color_content()
* will return bogus "default" values and only for the first 8 colors.
@@ -424,23 +501,23 @@ InterfaceCurses::restore_colors(void)
* already properly restored on endwin().
* Welcome in Curses hell.
*/
-void
-InterfaceCurses::restore_colors(void)
+static void
+teco_interface_restore_colors(void)
{
- if (xterm_version() < 0)
+ if (teco_xterm_version() < 0)
return;
/*
* Looks like a real XTerm
*/
- fputs(CTL_KEY_ESC_STR "]104\a", screen_tty);
- fflush(screen_tty);
+ fputs("\e]104\a", teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
}
#else /* !PDCURSES_WIN32 && !CURSES_TTY */
-void
-InterfaceCurses::restore_colors(void)
+static void
+teco_interface_restore_colors(void)
{
/*
* No way to restore the palette, or it's
@@ -451,9 +528,9 @@ InterfaceCurses::restore_colors(void)
#endif
void
-InterfaceCurses::init_color(guint color, guint32 rgb)
+teco_interface_init_color(guint color, guint32 rgb)
{
- if (color >= G_N_ELEMENTS(color_table))
+ if (color >= G_N_ELEMENTS(teco_interface.color_table))
return;
#if defined(__PDCURSES__) && !defined(PDC_RGB)
@@ -469,12 +546,12 @@ InterfaceCurses::init_color(guint color, guint32 rgb)
((color & 0x1) << 2) | ((color & 0x4) >> 2);
#endif
- if (cmdline_window) {
+ if (teco_interface.cmdline_window) {
/* interactive mode */
if (!can_change_color())
return;
- init_color_safe(color, rgb);
+ teco_interface_init_color_safe(color, rgb);
} else {
/*
* batch mode: store colors,
@@ -482,21 +559,21 @@ InterfaceCurses::init_color(guint color, guint32 rgb)
* which is called by Scinterm when interactive
* mode is initialized
*/
- color_table[color] = (gint32)rgb;
+ teco_interface.color_table[color] = (gint32)rgb;
}
}
#ifdef CURSES_TTY
-void
-InterfaceCurses::init_screen(void)
+static void
+teco_interface_init_screen(void)
{
- screen_tty = g_fopen("/dev/tty", "r+");
+ teco_interface.screen_tty = g_fopen("/dev/tty", "r+");
/* should never fail */
- g_assert(screen_tty != NULL);
+ g_assert(teco_interface.screen_tty != NULL);
- screen = newterm(NULL, screen_tty, screen_tty);
- if (!screen) {
+ teco_interface.screen = newterm(NULL, teco_interface.screen_tty, teco_interface.screen_tty);
+ if (!teco_interface.screen) {
g_fprintf(stderr, "Error initializing interactive mode. "
"$TERM may be incorrect.\n");
exit(EXIT_FAILURE);
@@ -509,25 +586,23 @@ InterfaceCurses::init_screen(void)
* interrupt terminal interaction.
*/
if (isatty(1)) {
- FILE *stdout_new;
- stdout_orig = dup(1);
- g_assert(stdout_orig >= 0);
- stdout_new = g_freopen("/dev/null", "a+", stdout);
+ teco_interface.stdout_orig = dup(1);
+ g_assert(teco_interface.stdout_orig >= 0);
+ FILE *stdout_new = g_freopen("/dev/null", "a+", stdout);
g_assert(stdout_new != NULL);
}
if (isatty(2)) {
- FILE *stderr_new;
- stderr_orig = dup(2);
- g_assert(stderr_orig >= 0);
- stderr_new = g_freopen("/dev/null", "a+", stderr);
+ teco_interface.stderr_orig = dup(2);
+ g_assert(teco_interface.stderr_orig >= 0);
+ FILE *stderr_new = g_freopen("/dev/null", "a+", stderr);
g_assert(stderr_new != NULL);
}
}
#elif defined(XCURSES)
-void
-InterfaceCurses::init_screen(void)
+static void
+teco_interface_init_screen(void)
{
const char *argv[] = {PACKAGE_NAME, NULL};
@@ -551,16 +626,16 @@ InterfaceCurses::init_screen(void)
#else
-void
-InterfaceCurses::init_screen(void)
+static void
+teco_interface_init_screen(void)
{
initscr();
}
#endif
-void
-InterfaceCurses::init_interactive(void)
+static gboolean
+teco_interface_init_interactive(GError **error)
{
/*
* Curses accesses many environment variables
@@ -569,7 +644,8 @@ InterfaceCurses::init_interactive(void)
* environment before initscr()/newterm().
* This is safe to do here since there are no threads.
*/
- QRegisters::globals.update_environ();
+ if (!teco_qreg_table_set_environ(&teco_qreg_table_globals, error))
+ return FALSE;
/*
* On UNIX terminals, the escape key is usually
@@ -625,41 +701,41 @@ InterfaceCurses::init_interactive(void)
/* for displaying UTF-8 characters properly */
setlocale(LC_CTYPE, "");
- init_screen();
+ teco_interface_init_screen();
cbreak();
noecho();
/* Scintilla draws its own cursor */
curs_set(0);
- info_window = newwin(1, 0, 0, 0);
+ teco_interface.info_window = newwin(1, 0, 0, 0);
- msg_window = newwin(1, 0, LINES - 2, 0);
+ teco_interface.msg_window = newwin(1, 0, LINES - 2, 0);
- cmdline_window = newwin(0, 0, LINES - 1, 0);
- keypad(cmdline_window, TRUE);
+ teco_interface.cmdline_window = newwin(0, 0, LINES - 1, 0);
+ keypad(teco_interface.cmdline_window, TRUE);
#ifdef EMCURSES
- nodelay(cmdline_window, TRUE);
+ nodelay(teco_interface.cmdline_window, TRUE);
#endif
/*
* Will also initialize Scinterm, Curses color pairs
* and resizes the current view.
*/
- if (current_view)
- show_view(current_view);
+ if (teco_interface_current_view)
+ teco_interface_show_view(teco_interface_current_view);
/*
* Only now it's safe to redefine the 16 default colors.
*/
if (can_change_color()) {
- for (guint i = 0; i < G_N_ELEMENTS(color_table); i++) {
+ for (guint i = 0; i < G_N_ELEMENTS(teco_interface.color_table); i++) {
/*
* init_color() may still fail if COLORS < 16
*/
- if (color_table[i] >= 0)
- init_color_safe(i, (guint32)color_table[i]);
+ if (teco_interface.color_table[i] >= 0)
+ teco_interface_init_color_safe(i, (guint32)teco_interface.color_table[i]);
}
}
@@ -670,12 +746,14 @@ InterfaceCurses::init_interactive(void)
* with stdout.
*/
#ifdef CURSES_TTY
- init_clipboard();
+ teco_interface_init_clipboard();
#endif
+
+ return TRUE;
}
-void
-InterfaceCurses::restore_batch(void)
+static void
+teco_interface_restore_batch(void)
{
/*
* Set window title to a reasonable default,
@@ -685,7 +763,7 @@ InterfaceCurses::restore_batch(void)
* is necessary.
*/
#if defined(CURSES_TTY) && defined(HAVE_TIGETSTR)
- set_window_title(g_getenv("TERM") ? : "");
+ teco_interface_set_window_title(g_getenv("TERM") ? : "");
#endif
/*
@@ -693,61 +771,59 @@ InterfaceCurses::restore_batch(void)
* (i.e. return to batch mode)
*/
endwin();
- restore_colors();
+ teco_interface_restore_colors();
/*
* Restore stdout and stderr, so output goes to
* the terminal again in case we "muted" them.
*/
#ifdef CURSES_TTY
- if (stdout_orig >= 0) {
- int fd = dup2(stdout_orig, 1);
+ if (teco_interface.stdout_orig >= 0) {
+ int fd = dup2(teco_interface.stdout_orig, 1);
g_assert(fd == 1);
}
- if (stderr_orig >= 0) {
- int fd = dup2(stderr_orig, 2);
+ if (teco_interface.stderr_orig >= 0) {
+ int fd = dup2(teco_interface.stderr_orig, 2);
g_assert(fd == 2);
}
#endif
/*
- * See vmsg_impl(): It looks at msg_win to determine
+ * See teco_interface_vmsg(): It looks at msg_window to determine
* whether we're in batch mode.
*/
- if (msg_window) {
- delwin(msg_window);
- msg_window = NULL;
+ if (teco_interface.msg_window) {
+ delwin(teco_interface.msg_window);
+ teco_interface.msg_window = NULL;
}
}
-void
-InterfaceCurses::resize_all_windows(void)
+static void
+teco_interface_resize_all_windows(void)
{
int lines, cols; /* screen dimensions */
getmaxyx(stdscr, lines, cols);
- wresize(info_window, 1, cols);
- wresize(current_view->get_window(),
+ wresize(teco_interface.info_window, 1, cols);
+ wresize(teco_view_get_window(teco_interface_current_view),
lines - 3, cols);
- wresize(msg_window, 1, cols);
- mvwin(msg_window, lines - 2, 0);
- wresize(cmdline_window, 1, cols);
- mvwin(cmdline_window, lines - 1, 0);
-
- draw_info();
- msg_clear(); /* FIXME: use saved message */
- popup_clear();
- draw_cmdline();
+ 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);
+
+ teco_interface_draw_info();
+ teco_interface_msg_clear(); /* FIXME: use saved message */
+ teco_interface_popup_clear();
+ teco_interface_draw_cmdline();
}
void
-InterfaceCurses::vmsg_impl(MessageType type, const gchar *fmt, va_list ap)
+teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
{
- short fg, bg;
-
- if (!msg_window) { /* batch mode */
- stdio_vmsg(type, fmt, ap);
+ if (!teco_interface.msg_window) { /* batch mode */
+ teco_interface_stdio_vmsg(type, fmt, ap);
return;
}
@@ -759,67 +835,65 @@ InterfaceCurses::vmsg_impl(MessageType type, const gchar *fmt, va_list ap)
defined(CURSES_TTY) || defined(NCURSES_WIN32)
va_list aq;
va_copy(aq, ap);
- stdio_vmsg(type, fmt, aq);
+ teco_interface_stdio_vmsg(type, fmt, aq);
va_end(aq);
#endif
- fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
+ short fg, bg;
+
+ fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
switch (type) {
default:
- case MSG_USER:
- bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
+ case TECO_MSG_USER:
+ bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
break;
- case MSG_INFO:
+ case TECO_MSG_INFO:
bg = COLOR_GREEN;
break;
- case MSG_WARNING:
+ case TECO_MSG_WARNING:
bg = COLOR_YELLOW;
break;
- case MSG_ERROR:
+ case TECO_MSG_ERROR:
bg = COLOR_RED;
beep();
break;
}
- wmove(msg_window, 0, 0);
- wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
- vw_printw(msg_window, fmt, ap);
- wclrtoeol(msg_window);
+ wmove(teco_interface.msg_window, 0, 0);
+ wbkgdset(teco_interface.msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ vw_printw(teco_interface.msg_window, fmt, ap);
+ wclrtoeol(teco_interface.msg_window);
}
void
-InterfaceCurses::msg_clear(void)
+teco_interface_msg_clear(void)
{
- short fg, bg;
-
- if (!msg_window) /* batch mode */
+ if (!teco_interface.msg_window) /* batch mode */
return;
- fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
- bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
+ 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(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
- werase(msg_window);
+ wbkgdset(teco_interface.msg_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ werase(teco_interface.msg_window);
}
void
-InterfaceCurses::show_view_impl(ViewCurses *view)
+teco_interface_show_view(teco_view_t *view)
{
- int lines, cols; /* screen dimensions */
- WINDOW *current_view_win;
-
- current_view = view;
+ teco_interface_current_view = view;
- if (!cmdline_window) /* batch mode */
+ if (!teco_interface.cmdline_window) /* batch mode */
return;
- current_view_win = current_view->get_window();
+ WINDOW *current_view_win = teco_view_get_window(teco_interface_current_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);
/* Set up window position: never changes */
@@ -828,8 +902,8 @@ InterfaceCurses::show_view_impl(ViewCurses *view)
#if PDCURSES
-void
-InterfaceCurses::set_window_title(const gchar *title)
+static void
+teco_interface_set_window_title(const gchar *title)
{
static gchar *last_title = NULL;
@@ -851,8 +925,8 @@ InterfaceCurses::set_window_title(const gchar *title)
#elif defined(CURSES_TTY) && defined(HAVE_TIGETSTR)
-void
-InterfaceCurses::set_window_title(const gchar *title)
+static void
+teco_interface_set_window_title(const gchar *title)
{
if (!has_status_line || !to_status_line || !from_status_line)
return;
@@ -882,30 +956,26 @@ InterfaceCurses::set_window_title(const gchar *title)
* we do not let curses write to stdout.
* NOTE: This leaves the title set after we quit.
*/
- fputs(to_status_line, screen_tty);
- fputs(title, screen_tty);
- fputs(from_status_line, screen_tty);
- fflush(screen_tty);
+ fputs(to_status_line, teco_interface.screen_tty);
+ fputs(title, teco_interface.screen_tty);
+ fputs(from_status_line, teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
}
#else
-void
-InterfaceCurses::set_window_title(const gchar *title)
+static void
+teco_interface_set_window_title(const gchar *title)
{
/* no way to set window title */
}
#endif
-void
-InterfaceCurses::draw_info(void)
+static void
+teco_interface_draw_info(void)
{
- short fg, bg;
- const gchar *info_type_str;
- gchar *info_current_canon, *title;
-
- if (!info_window) /* batch mode */
+ if (!teco_interface.info_window) /* batch mode */
return;
/*
@@ -913,69 +983,75 @@ InterfaceCurses::draw_info(void)
* the current buffer's STYLE_DEFAULT.
* The same style is used for MSG_USER messages.
*/
- fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
- bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+
+ wmove(teco_interface.info_window, 0, 0);
+ wbkgdset(teco_interface.info_window, ' ' | SCI_COLOR_ATTR(fg, bg));
- wmove(info_window, 0, 0);
- wbkgdset(info_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ const gchar *info_type_str;
- switch (info_type) {
- case INFO_TYPE_QREGISTER:
+ switch (teco_interface.info_type) {
+ case TECO_INFO_TYPE_QREG:
info_type_str = PACKAGE_NAME " - <QRegister> ";
- waddstr(info_window, info_type_str);
+ waddstr(teco_interface.info_window, info_type_str);
/* same formatting as in command lines */
- Curses::format_str(info_window, info_current);
+ teco_curses_format_str(teco_interface.info_window,
+ teco_interface.info_current.data,
+ teco_interface.info_current.len, -1);
break;
- case INFO_TYPE_BUFFER:
+ case TECO_INFO_TYPE_BUFFER:
info_type_str = PACKAGE_NAME " - <Buffer> ";
- waddstr(info_window, info_type_str);
- Curses::format_filename(info_window, info_current);
+ waddstr(teco_interface.info_window, info_type_str);
+ g_assert(!teco_string_contains(&teco_interface.info_current, '\0'));
+ teco_curses_format_filename(teco_interface.info_window,
+ teco_interface.info_current.data, -1);
break;
default:
g_assert_not_reached();
}
- wclrtoeol(info_window);
+ wclrtoeol(teco_interface.info_window);
/*
* Make sure the title will consist only of printable
* characters
*/
- info_current_canon = String::canonicalize_ctl(info_current);
- title = g_strconcat(info_type_str, info_current_canon, NIL);
- g_free(info_current_canon);
- set_window_title(title);
- g_free(title);
+ g_autofree gchar *info_current_printable;
+ info_current_printable = teco_string_echo(teco_interface.info_current.data,
+ teco_interface.info_current.len);
+ g_autofree gchar *title = g_strconcat(info_type_str, info_current_printable, NULL);
+ teco_interface_set_window_title(title);
}
void
-InterfaceCurses::info_update_impl(const QRegister *reg)
+teco_interface_info_update_qreg(const teco_qreg_t *reg)
{
- g_free(info_current);
- /* NOTE: will contain control characters */
- info_type = INFO_TYPE_QREGISTER;
- info_current = g_strdup(reg->name);
- /* NOTE: drawn in event_loop_iter() */
+ teco_string_clear(&teco_interface.info_current);
+ teco_string_init(&teco_interface.info_current,
+ reg->head.name.data, reg->head.name.len);
+ teco_interface.info_type = TECO_INFO_TYPE_QREG;
+ /* NOTE: drawn in teco_interface_event_loop_iter() */
}
void
-InterfaceCurses::info_update_impl(const Buffer *buffer)
+teco_interface_info_update_buffer(const teco_buffer_t *buffer)
{
- g_free(info_current);
- info_type = INFO_TYPE_BUFFER;
- info_current = g_strconcat(buffer->filename ? : UNNAMED_FILE,
- buffer->dirty ? "*" : " ", NIL);
- /* NOTE: drawn in event_loop_iter() */
+ 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_string_append_c(&teco_interface.info_current,
+ buffer->dirty ? '*' : ' ');
+ teco_interface.info_type = TECO_INFO_TYPE_BUFFER;
+ /* NOTE: drawn in teco_interface_event_loop_iter() */
}
void
-InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline)
+teco_interface_cmdline_update(const teco_cmdline_t *cmdline)
{
- short fg, bg;
- int max_cols = 1;
-
/*
* Replace entire pre-formatted command-line.
* We don't know if it is similar to the last one,
@@ -983,18 +1059,22 @@ InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline)
* We approximate the size of the new formatted command-line,
* wasting a few bytes for control characters.
*/
- if (cmdline_pad)
- delwin(cmdline_pad);
- for (guint i = 0; i < cmdline->len+cmdline->rubout_len; i++)
- max_cols += IS_CTL((*cmdline)[i]) ? 3 : 1;
- cmdline_pad = newpad(1, max_cols);
+ if (teco_interface.cmdline_pad)
+ delwin(teco_interface.cmdline_pad);
- fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
- bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
- wcolor_set(cmdline_pad, SCI_COLOR_PAIR(fg, bg), NULL);
+ 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));
+ wcolor_set(teco_interface.cmdline_pad, SCI_COLOR_PAIR(fg, bg), NULL);
/* format effective command line */
- cmdline_len = Curses::format_str(cmdline_pad, cmdline->str, cmdline->len);
+ 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
@@ -1006,61 +1086,61 @@ InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline)
* for rubbed out parts of the command line which will
* be user-configurable.
*/
- wattron(cmdline_pad, A_UNDERLINE | A_BOLD);
+ 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.
*/
- cmdline_rubout_len = Curses::format_str(cmdline_pad, cmdline->str + cmdline->len,
- cmdline->rubout_len);
+ 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 */
- if (cmdline_rubout_len) {
- attr_t attr;
- short pair;
+ if (teco_interface.cmdline_rubout_len) {
+ attr_t attr = 0;
+ short pair = 0;
- wmove(cmdline_pad, 0, cmdline_len);
- wattr_get(cmdline_pad, &attr, &pair, NULL);
- wchgat(cmdline_pad, 1,
+ 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, pair, NULL);
} else {
- cmdline_len++;
- wattroff(cmdline_pad, A_UNDERLINE | A_BOLD);
- waddch(cmdline_pad, ' ' | A_REVERSE);
+ teco_interface.cmdline_len++;
+ wattroff(teco_interface.cmdline_pad, A_UNDERLINE | A_BOLD);
+ waddch(teco_interface.cmdline_pad, ' ' | A_REVERSE);
}
- draw_cmdline();
+ teco_interface_draw_cmdline();
}
-void
-InterfaceCurses::draw_cmdline(void)
+static void
+teco_interface_draw_cmdline(void)
{
- short fg, bg;
/* total width available for command line */
- guint total_width = getmaxx(cmdline_window) - 1;
- /* beginning of command line to show */
- guint disp_offset;
- /* length of command line to show */
- guint disp_len;
+ guint total_width = getmaxx(teco_interface.cmdline_window) - 1;
- disp_offset = cmdline_len -
- MIN(cmdline_len,
- total_width/2 + cmdline_len % MAX(total_width/2, 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.
*/
- disp_len = MIN(total_width, cmdline_len+cmdline_rubout_len - disp_offset);
+ guint disp_len = MIN(total_width, teco_interface.cmdline_len +
+ teco_interface.cmdline_rubout_len - disp_offset);
- fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT));
- bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT));
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0));
- wbkgdset(cmdline_window, ' ' | SCI_COLOR_ATTR(fg, bg));
- werase(cmdline_window);
- mvwaddch(cmdline_window, 0, 0, '*' | A_BOLD);
- copywin(cmdline_pad, cmdline_window,
+ wbkgdset(teco_interface.cmdline_window, ' ' | SCI_COLOR_ATTR(fg, bg));
+ werase(teco_interface.cmdline_window);
+ mvwaddch(teco_interface.cmdline_window, 0, 0, '*' | A_BOLD);
+ copywin(teco_interface.cmdline_pad, teco_interface.cmdline_window,
0, disp_offset, 0, 1, 0, disp_len, FALSE);
}
@@ -1073,12 +1153,11 @@ InterfaceCurses::draw_cmdline(void)
* it corresponds to the X11 PRIMARY, SECONDARY or
* CLIPBOARD selections.
*/
-void
-InterfaceCurses::init_clipboard(void)
+static void
+teco_interface_init_clipboard(void)
{
char *contents;
long length;
- int rc;
/*
* Even on PDCurses, while the clipboard functions are
@@ -1089,71 +1168,68 @@ InterfaceCurses::init_clipboard(void)
* This could be done at compile time, but this way is more
* generic (albeit inefficient).
*/
- rc = PDC_getclipboard(&contents, &length);
+ int rc = PDC_getclipboard(&contents, &length);
if (rc == PDC_CLIP_ACCESS_ERROR)
return;
if (rc == PDC_CLIP_SUCCESS)
PDC_freeclipboard(contents);
- QRegisters::globals.insert(new QRegisterClipboard());
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new(""));
}
-void
-InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len)
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, GError **error)
{
- int rc;
-
- if (str) {
- if (str_len < 0)
- str_len = strlen(str);
-
- rc = PDC_setclipboard(str, str_len);
- } else {
- rc = PDC_clearclipboard();
+ int rc = str ? PDC_setclipboard(str, str_len) : PDC_clearclipboard();
+ if (rc != PDC_CLIP_SUCCESS) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Error %d copying to clipboard", rc);
+ return FALSE;
}
- if (rc != PDC_CLIP_SUCCESS)
- throw Error("Error %d copying to clipboard", rc);
+ return TRUE;
}
-gchar *
-InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
{
char *contents;
long length = 0;
- int rc;
- gchar *str;
/*
* NOTE: It is undefined whether we can pass in NULL for length.
*/
- rc = PDC_getclipboard(&contents, &length);
- if (str_len)
- *str_len = length;
+ int rc = PDC_getclipboard(&contents, &length);
+ *len = length;
if (rc == PDC_CLIP_EMPTY)
- return NULL;
- if (rc != PDC_CLIP_SUCCESS)
- throw Error("Error %d retrieving clipboard", rc);
+ return TRUE;
+ if (rc != PDC_CLIP_SUCCESS) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Error %d retrieving clipboard", rc);
+ return FALSE;
+ }
/*
* PDCurses defines its own free function and there is no
* way to find out which allocator was used.
* We must therefore copy the memory to be on the safe side.
- * At least we can null-terminate the return string in the
- * process (PDCurses does not guarantee that either).
+ * At least, the result is guaranteed to be null-terminated
+ * and thus teco_string_t-compatible
+ * (PDCurses does not guarantee that either).
*/
- str = (gchar *)g_malloc(length + 1);
- memcpy(str, contents, length);
- str[length] = '\0';
+ if (str) {
+ *str = memcpy(g_malloc(length + 1), contents, length);
+ (*str)[length] = '\0';
+ }
PDC_freeclipboard(contents);
- return str;
+ return TRUE;
}
#elif defined(CURSES_TTY)
-void
-InterfaceCurses::init_clipboard(void)
+static void
+teco_interface_init_clipboard(void)
{
/*
* At least on XTerm, there are escape sequences
@@ -1167,13 +1243,13 @@ InterfaceCurses::init_clipboard(void)
* not register the clipboard registers if they aren't.
* Therefore, a special XTerm clipboard ED flag an be set by the user.
*/
- if (!(Flags::ed & Flags::ED_XTERM_CLIPBOARD) || xterm_version() < 203)
+ if (!(teco_ed & TECO_ED_XTERM_CLIPBOARD) || teco_xterm_version() < 203)
return;
- QRegisters::globals.insert(new QRegisterClipboard());
- QRegisters::globals.insert(new QRegisterClipboard("P"));
- QRegisters::globals.insert(new QRegisterClipboard("S"));
- QRegisters::globals.insert(new QRegisterClipboard("C"));
+ 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"));
}
static inline gchar
@@ -1189,27 +1265,21 @@ get_selection_by_name(const gchar *name)
return g_ascii_tolower(*name) ? : 'c';
}
-void
-InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len)
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
+ GError **error)
{
+ fputs("\e]52;", teco_interface.screen_tty);
+ fputc(get_selection_by_name(name), teco_interface.screen_tty);
+ fputc(';', teco_interface.screen_tty);
+
/*
* Enough space for 1024 Base64-encoded bytes.
*/
gchar buffer[(1024 / 3) * 4 + 4];
gsize out_len;
-
/* g_base64_encode_step() state: */
- gint state = 0;
- gint save = 0;
-
- fputs(CTL_KEY_ESC_STR "]52;", screen_tty);
- fputc(get_selection_by_name(name), screen_tty);
- fputc(';', screen_tty);
-
- if (!str)
- str_len = 0;
- else if (str_len < 0)
- str_len = strlen(str);
+ gint state = 0, save = 0;
while (str_len > 0) {
gsize step_len = MIN(1024, str_len);
@@ -1221,41 +1291,32 @@ InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_l
out_len = g_base64_encode_step((const guchar *)str,
step_len, FALSE,
buffer, &state, &save);
- fwrite(buffer, 1, out_len, screen_tty);
+ fwrite(buffer, 1, out_len, teco_interface.screen_tty);
str_len -= step_len;
str += step_len;
}
out_len = g_base64_encode_close(FALSE, buffer, &state, &save);
- fwrite(buffer, 1, out_len, screen_tty);
+ fwrite(buffer, 1, out_len, teco_interface.screen_tty);
- fputc('\a', screen_tty);
- fflush(screen_tty);
+ fputc('\a', teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
+
+ return TRUE;
}
-gchar *
-InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
{
/*
- * Space for storing one group of decoded Base64 characters
- * and the OSC-52 response.
- */
- gchar buffer[MAX(3, 7)];
- GString *str_base64;
-
- /* g_base64_decode_step() state: */
- gint state = 0;
- guint save = 0;
-
- /*
* Query the clipboard -- XTerm will reply with the
* OSC-52 command that would set the current selection.
*/
- fputs(CTL_KEY_ESC_STR "]52;", screen_tty);
- fputc(get_selection_by_name(name), screen_tty);
- fputs(";?\a", screen_tty);
- fflush(screen_tty);
+ fputs("\e]52;", teco_interface.screen_tty);
+ fputc(get_selection_by_name(name), teco_interface.screen_tty);
+ fputs(";?\a", teco_interface.screen_tty);
+ fflush(teco_interface.screen_tty);
/*
* It is very well possible that the XTerm clipboard
@@ -1277,22 +1338,32 @@ InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
if (getch() == ERR) {
/* timeout */
cbreak();
- throw Error("Timed out reading XTerm clipboard");
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Timed out reading XTerm clipboard");
+ return FALSE;
}
}
- str_base64 = g_string_new("");
+ GString *str_base64 = g_string_new("");
+ /* g_base64_decode_step() state: */
+ gint state = 0;
+ guint save = 0;
for (;;) {
- gchar c;
- gsize out_len;
+ /*
+ * Space for storing one group of decoded Base64 characters
+ * and the OSC-52 response.
+ */
+ gchar buffer[MAX(3, 7)];
- c = (gchar)getch();
+ gchar c = (gchar)getch();
if (c == ERR) {
/* timeout */
cbreak();
g_string_free(str_base64, TRUE);
- throw Error("Timed out reading XTerm clipboard");
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Timed out reading XTerm clipboard");
+ return FALSE;
}
if (c == '\a')
break;
@@ -1301,30 +1372,29 @@ InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
* This could be simplified using sscanf() and
* g_base64_decode(), but we avoid one allocation
* to get the entire Base64 string.
- * (Also to allow for timeouts, we must should
+ * (Also to allow for timeouts, we should
* read character-wise using getch() anyway.)
*/
- out_len = g_base64_decode_step(&c, sizeof(c),
- (guchar *)buffer,
- &state, &save);
+ gsize out_len = g_base64_decode_step(&c, sizeof(c),
+ (guchar *)buffer,
+ &state, &save);
g_string_append_len(str_base64, buffer, out_len);
}
cbreak();
- if (str_len)
- *str_len = str_base64->len;
+ if (str)
+ *str = str_base64->str;
+ *len = str_base64->len;
- /*
- * If the clipboard answer is empty, return NULL.
- */
- return g_string_free(str_base64, str_base64->len == 0);
+ g_string_free(str_base64, !str);
+ return TRUE;
}
#else
-void
-InterfaceCurses::init_clipboard(void)
+static void
+teco_interface_init_clipboard(void)
{
/*
* No native clipboard support, so no clipboard Q-Regs are
@@ -1332,37 +1402,55 @@ InterfaceCurses::init_clipboard(void)
*/
}
-void
-InterfaceCurses::set_clipboard(const gchar *name, const gchar *str, gssize str_len)
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len,
+ GError **error)
{
- throw Error("Setting clipboard unsupported");
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Setting clipboard unsupported");
+ return FALSE;
}
-gchar *
-InterfaceCurses::get_clipboard(const gchar *name, gsize *str_len)
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
{
- throw Error("Getting clipboard unsupported");
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Getting clipboard unsupported");
+ return FALSE;
}
#endif /* !__PDCURSES__ && !CURSES_TTY */
void
-InterfaceCurses::popup_show_impl(void)
+teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len,
+ gboolean highlight)
{
- short fg, bg;
+ if (teco_interface.cmdline_window)
+ /* interactive mode */
+ teco_curses_info_popup_add(&teco_interface.popup, type, name, name_len, highlight);
+}
- if (!cmdline_window)
+void
+teco_interface_popup_show(void)
+{
+ if (!teco_interface.cmdline_window)
/* batch mode */
return;
- fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_CALLTIP));
- bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_CALLTIP));
+ short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_CALLTIP, 0));
+ short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_CALLTIP, 0));
- popup.show(SCI_COLOR_ATTR(fg, bg));
+ teco_curses_info_popup_show(&teco_interface.popup, SCI_COLOR_ATTR(fg, bg));
+}
+
+gboolean
+teco_interface_popup_is_shown(void)
+{
+ return teco_curses_info_popup_is_shown(&teco_interface.popup);
}
void
-InterfaceCurses::popup_clear_impl(void)
+teco_interface_popup_clear(void)
{
#ifdef __PDCURSES__
/*
@@ -1374,31 +1462,35 @@ InterfaceCurses::popup_clear_impl(void)
* Actually we would expect this to be necessary on any curses,
* but ncurses doesn't require this.
*/
- if (popup.is_shown()) {
- touchwin(info_window);
- touchwin(msg_window);
+ if (teco_curses_info_popup_is_shown(&teco_interface.popup)) {
+ touchwin(teco_interface.info_window);
+ touchwin(teco_interface.msg_window);
}
#endif
- popup.clear();
+ teco_curses_info_popup_clear(&teco_interface.popup);
+ teco_curses_info_popup_init(&teco_interface.popup);
+}
+
+gboolean
+teco_interface_is_interrupted(void)
+{
+ return teco_sigint_occurred != FALSE;
}
/**
* One iteration of the event loop.
*
- * This is a global function, so it may
- * be used as an Emscripten callback.
+ * This is a global function, so it may be used as an asynchronous Emscripten callback.
+ * While this function cannot directly throw GErrors,
+ * it can set teco_interface.event_loop_error.
*
- * @bug
- * Can probably be defined as a static method,
- * so we can avoid declaring it a fried function of
- * InterfaceCurses.
+ * @fixme Thrown errors should be somehow caught when building for EMScripten as well.
+ * Perhaps in a goto-block.
*/
void
-event_loop_iter()
+teco_interface_event_loop_iter(void)
{
- int key;
-
/*
* On PDCurses/win32, raw() and cbreak() does
* not disable and enable CTRL+C handling properly.
@@ -1426,7 +1518,7 @@ event_loop_iter()
* escape sequences.
*/
#ifdef NCURSES_UNIX
- keypad(interface.cmdline_window, Flags::ed & Flags::ED_FNKEYS);
+ keypad(teco_interface.cmdline_window, teco_ed & TECO_ED_FNKEYS);
#endif
/* no special <CTRL/C> handling */
@@ -1434,9 +1526,15 @@ event_loop_iter()
#ifdef PDCURSES_WIN32
SetConsoleMode(console_hnd, console_mode & ~ENABLE_PROCESSED_INPUT);
#endif
- key = wgetch(interface.cmdline_window);
+ /*
+ * Memory limiting is stopped temporarily, since it might otherwise
+ * constantly place 100% load on the CPU.
+ */
+ teco_memory_stop_limiting();
+ int key = wgetch(teco_interface.cmdline_window);
+ teco_memory_start_limiting();
/* allow asynchronous interruptions on <CTRL/C> */
- sigint_occurred = FALSE;
+ teco_sigint_occurred = FALSE;
noraw(); /* FIXME: necessary because of NCURSES_WIN32 bug */
cbreak();
#ifdef PDCURSES_WIN32
@@ -1451,10 +1549,10 @@ event_loop_iter()
#if PDCURSES
resize_term(0, 0);
#endif
- interface.resize_all_windows();
+ teco_interface_resize_all_windows();
break;
#endif
- case CTL_KEY('H'):
+ case TECO_CTL_KEY('H'):
case 0x7F: /* ^? */
case KEY_BACKSPACE:
/*
@@ -1465,18 +1563,25 @@ event_loop_iter()
* backspace.
* In SciTECO backspace is normalized to ^H.
*/
- cmdline.keypress(CTL_KEY('H'));
+ if (!teco_cmdline_keypress_c(TECO_CTL_KEY('H'),
+ &teco_interface.event_loop_error))
+ return;
break;
case KEY_ENTER:
case '\r':
case '\n':
- cmdline.keypress('\n');
+ if (!teco_cmdline_keypress_c('\n', &teco_interface.event_loop_error))
+ return;
break;
/*
* Function key macros
*/
-#define FN(KEY) case KEY_##KEY: cmdline.fnmacro(#KEY); break
+#define FN(KEY) \
+ case KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(#KEY, &teco_interface.event_loop_error)) \
+ return; \
+ break
#define FNS(KEY) FN(KEY); FN(S##KEY)
FN(DOWN); FN(UP); FNS(LEFT); FNS(RIGHT);
FNS(HOME);
@@ -1485,7 +1590,9 @@ event_loop_iter()
g_snprintf(macro_name, sizeof(macro_name),
"F%d", key - KEY_F0);
- cmdline.fnmacro(macro_name);
+ if (!teco_cmdline_fnmacro(macro_name,
+ &teco_interface.event_loop_error))
+ return;
break;
}
FNS(DC);
@@ -1503,8 +1610,9 @@ event_loop_iter()
* Control keys and keys with printable representation
*/
default:
- if (key <= 0xFF)
- cmdline.keypress((gchar)key);
+ if (key <= 0xFF &&
+ !teco_cmdline_keypress_c(key, &teco_interface.event_loop_error))
+ return;
}
/*
@@ -1513,37 +1621,38 @@ event_loop_iter()
* so we redraw it here, where the overhead does
* not matter much.
*/
- interface.draw_info();
- wnoutrefresh(interface.info_window);
- interface.current_view->noutrefresh();
- wnoutrefresh(interface.msg_window);
- wnoutrefresh(interface.cmdline_window);
- interface.popup.noutrefresh();
+ teco_interface_draw_info();
+ wnoutrefresh(teco_interface.info_window);
+ teco_view_noutrefresh(teco_interface_current_view);
+ wnoutrefresh(teco_interface.msg_window);
+ wnoutrefresh(teco_interface.cmdline_window);
+ teco_curses_info_popup_noutrefresh(&teco_interface.popup);
doupdate();
}
-void
-InterfaceCurses::event_loop_impl(void)
+gboolean
+teco_interface_event_loop(GError **error)
{
- static const Cmdline empty_cmdline;
+ static const teco_cmdline_t empty_cmdline; // FIXME
/*
* Initialize Curses for interactive mode
*/
- init_interactive();
+ if (!teco_interface_init_interactive(error))
+ return FALSE;
/* initial refresh */
- draw_info();
- wnoutrefresh(info_window);
- current_view->noutrefresh();
- msg_clear();
- wnoutrefresh(msg_window);
- cmdline_update(&empty_cmdline);
- wnoutrefresh(cmdline_window);
+ teco_interface_draw_info();
+ wnoutrefresh(teco_interface.info_window);
+ teco_view_noutrefresh(teco_interface_current_view);
+ teco_interface_msg_clear();
+ wnoutrefresh(teco_interface.msg_window);
+ teco_interface_cmdline_update(&empty_cmdline);
+ wnoutrefresh(teco_interface.cmdline_window);
doupdate();
#ifdef EMCURSES
- PDC_emscripten_set_handler(event_loop_iter, TRUE);
+ PDC_emscripten_set_handler(teco_interface_event_loop_iter, TRUE);
/*
* We must not block emscripten's main loop,
* instead event_loop_iter() is called asynchronously.
@@ -1556,28 +1665,41 @@ InterfaceCurses::event_loop_impl(void)
*/
emscripten_exit_with_live_runtime();
#else
- try {
- for (;;)
- event_loop_iter();
- } catch (Quit) {
- /* SciTECO termination (e.g. EX$$) */
+ while (!teco_interface.event_loop_error)
+ teco_interface_event_loop_iter();
+
+ /*
+ * The error needs to be propagated only if this is
+ * NOT a SciTECO termination (e.g. EX$$)
+ */
+ if (!g_error_matches(teco_interface.event_loop_error,
+ TECO_ERROR, TECO_ERROR_QUIT)) {
+ g_propagate_error(error, g_steal_pointer(&teco_interface.event_loop_error));
+ return FALSE;
}
+ g_clear_error(&teco_interface.event_loop_error);
- restore_batch();
+ teco_interface_restore_batch();
#endif
+
+ return TRUE;
}
-InterfaceCurses::~InterfaceCurses()
+void
+teco_interface_cleanup(void)
{
- if (info_window)
- delwin(info_window);
- g_free(info_current);
- if (cmdline_window)
- delwin(cmdline_window);
- if (cmdline_pad)
- delwin(cmdline_pad);
- if (msg_window)
- delwin(msg_window);
+ if (teco_interface.event_loop_error)
+ g_error_free(teco_interface.event_loop_error);
+
+ if (teco_interface.info_window)
+ delwin(teco_interface.info_window);
+ teco_string_clear(&teco_interface.info_current);
+ 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);
/*
* PDCurses (win32) crashes if initscr() wasn't called.
@@ -1586,28 +1708,16 @@ InterfaceCurses::~InterfaceCurses()
* instead.
*/
#ifndef XCURSES
- if (info_window && !isendwin())
+ if (teco_interface.info_window && !isendwin())
endwin();
#endif
- if (screen)
- delscreen(screen);
- if (screen_tty)
- fclose(screen_tty);
- if (stderr_orig >= 0)
- close(stderr_orig);
- if (stdout_orig >= 0)
- close(stdout_orig);
-}
-
-/*
- * Callbacks
- */
-
-static void
-scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data)
-{
- interface.process_notify((SCNotification *)notify);
+ if (teco_interface.screen)
+ delscreen(teco_interface.screen);
+ if (teco_interface.screen_tty)
+ fclose(teco_interface.screen_tty);
+ if (teco_interface.stderr_orig >= 0)
+ close(teco_interface.stderr_orig);
+ if (teco_interface.stdout_orig >= 0)
+ close(teco_interface.stdout_orig);
}
-
-} /* namespace SciTECO */
diff --git a/src/interface-gtk/Makefile.am b/src/interface-gtk/Makefile.am
index 825c5d3..af26519 100644
--- a/src/interface-gtk/Makefile.am
+++ b/src/interface-gtk/Makefile.am
@@ -1,22 +1,18 @@
-AM_CPPFLAGS += -I$(top_srcdir)/src
+AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr \
+ -I$(top_srcdir)/src
-AM_CFLAGS = -Wall
-AM_CXXFLAGS = -Wall -Wno-char-subscripts
+AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value
-EXTRA_DIST = gtk-info-popup.gob \
- gtk-canonicalized-label.gob
-BUILT_SOURCES = gtk-info-popup.c \
- gtk-info-popup.h gtk-info-popup-private.h \
- gtk-canonicalized-label.c \
- gtk-canonicalized-label.h
+EXTRA_DIST = teco-gtk-info-popup.gob \
+ teco-gtk-label.gob
+BUILT_SOURCES = teco-gtk-info-popup.c teco-gtk-info-popup.h \
+ teco-gtk-info-popup-private.h \
+ teco-gtk-label.c teco-gtk-label.h
noinst_LTLIBRARIES = libsciteco-interface.la
-libsciteco_interface_la_SOURCES = interface-gtk.cpp interface-gtk.h
-if GTK_FLOW_BOX_FALLBACK
-libsciteco_interface_la_SOURCES += gtkflowbox.c gtkflowbox.h
-endif
-nodist_libsciteco_interface_la_SOURCES = gtk-info-popup.c \
- gtk-canonicalized-label.c
+libsciteco_interface_la_SOURCES = interface.c
+nodist_libsciteco_interface_la_SOURCES = teco-gtk-info-popup.c \
+ teco-gtk-label.c
dist_pkgdata_DATA = fallback.css
diff --git a/src/interface-gtk/fallback.css b/src/interface-gtk/fallback.css
index c8f5431..90ad4ed 100644
--- a/src/interface-gtk/fallback.css
+++ b/src/interface-gtk/fallback.css
@@ -6,6 +6,13 @@
* This may cause problems with your current Gtk theme.
* You can copy this file to $SCITECOCONFIG/.teco_css
* to fix it up or add other style customizations.
+ * You could of course also import it using
+ * @import "/usr/share/sciteco/fallback.css";
+ *
+ * NOTE: Avoid using CSS element names like GtkLabel
+ * since Gtk switched from type names to custom names
+ * in Gtk+ v3.20 and it is impossible/cumbersome to
+ * write a CSS compatible with both.
*/
/*
@@ -17,14 +24,12 @@
* - type-label: The label showing the current document type
* - name-label: THe label showing the current document name
*/
-.info-qregister,
-.info-buffer {
+.info-buffer, .info-qregister {
background-color: @sciteco_default_fg_color;
background-image: none;
}
-.info-qregister GtkLabel,
-.info-buffer GtkLabel {
+.info-buffer *, .info-qregister * {
color: @sciteco_default_bg_color;
text-shadow: none;
}
@@ -39,11 +44,6 @@
}
/*
- * Scintilla views
- */
-ScintillaObject {}
-
-/*
* The message bar (#sciteco-message-bar).
*
* The "question" class refers to G_MESSAGE_QUESTION.
@@ -51,25 +51,32 @@ ScintillaObject {}
* reason that there is no class for G_MESSAGE_OTHER that
* we could use for styling.
*/
-#sciteco-message-bar.question {
- background-color: @sciteco_default_fg_color;
+#sciteco-message-bar .label {
+ color: @sciteco_default_bg_color;
+ text-shadow: none;
+}
+
+#sciteco-message-bar {
background-image: none;
}
-#sciteco-message-bar.question GtkLabel {
- color: @sciteco_default_bg_color;
- text-shadow: none;
+#sciteco-message-bar.question {
+ background-color: @sciteco_default_fg_color;
+}
+#sciteco-message-bar.info {
+ background-color: green;
+}
+#sciteco-message-bar.error {
+ background-color: yellow;
+}
+#sciteco-message-bar.error {
+ background-color: red;
}
/*
* The command line area (#sciteco-cmdline)
*/
-#sciteco-cmdline {
- color: @sciteco_default_fg_color;
- text-shadow: none;
- background-color: @sciteco_default_bg_color;
- background-image: none;
-}
+#sciteco-cmdline {}
/*
* The autocompletion popup (#sciteco-info-popup).
@@ -81,11 +88,11 @@ ScintillaObject {}
background-image: none;
}
-#sciteco-info-popup GtkLabel {
+#sciteco-info-popup .label {
color: @sciteco_calltip_fg_color;
text-shadow: none;
}
-#sciteco-info-popup .highlight GtkLabel {
+#sciteco-info-popup .highlight .label {
font-weight: bold;
}
diff --git a/src/interface-gtk/gtk-info-popup.gob b/src/interface-gtk/gtk-info-popup.gob
deleted file mode 100644
index edc6612..0000000
--- a/src/interface-gtk/gtk-info-popup.gob
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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/>.
- */
-
-requires 2.0.20
-
-%ctop{
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include <math.h>
-
-#include <glib/gprintf.h>
-
-#ifndef HAVE_GTK_FLOW_BOX_NEW
-#include "gtkflowbox.h"
-#endif
-
-#include "gtk-canonicalized-label.h"
-%}
-
-%h{
-#include <gtk/gtk.h>
-#include <gdk/gdk.h>
-
-#include <gio/gio.h>
-%}
-
-enum GTK_INFO_POPUP {
- PLAIN,
- FILE,
- DIRECTORY
-} Gtk:Info:Popup:Entry:Type;
-
-/*
- * NOTE: Deriving from GtkEventBox ensures that we can
- * set a background on the entire popup widget.
- */
-class Gtk:Info:Popup from Gtk:Event:Box {
- public GtkAdjustment *hadjustment;
- public GtkAdjustment *vadjustment;
-
- private GtkWidget *flow_box;
-
- init(self)
- {
- GtkWidget *box, *viewport;
-
- /*
- * A box containing a viewport and scrollbar will
- * "emulate" a scrolled window.
- * We cannot use a scrolled window since it ignores
- * the preferred height of its viewport which breaks
- * height-for-width management.
- */
- box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
-
- self->hadjustment = gtk_adjustment_new(0, 0, 0, 0, 0, 0);
- self->vadjustment = gtk_adjustment_new(0, 0, 0, 0, 0, 0);
-
- GtkWidget *scrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL,
- self->vadjustment);
- /* show/hide the scrollbar dynamically */
- g_signal_connect(self->vadjustment, "changed",
- G_CALLBACK(self_vadjustment_changed), scrollbar);
-
- self->_priv->flow_box = gtk_flow_box_new();
- /* take as little height as necessary */
- gtk_orientable_set_orientation(GTK_ORIENTABLE(self->_priv->flow_box),
- GTK_ORIENTATION_HORIZONTAL);
- //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(self->_priv->flow_box), TRUE);
- /* this for focus handling only, not for scrolling */
- gtk_flow_box_set_hadjustment(GTK_FLOW_BOX(self->_priv->flow_box),
- self->hadjustment);
- gtk_flow_box_set_vadjustment(GTK_FLOW_BOX(self->_priv->flow_box),
- self->vadjustment);
-
- viewport = gtk_viewport_new(self->hadjustment, self->vadjustment);
- gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
- gtk_container_add(GTK_CONTAINER(viewport), self->_priv->flow_box);
-
- gtk_box_pack_start(GTK_BOX(box), viewport, TRUE, TRUE, 0);
- gtk_box_pack_start(GTK_BOX(box), scrollbar, FALSE, FALSE, 0);
- gtk_widget_show_all(box);
-
- /*
- * NOTE: Everything shown except the top-level container.
- * Therefore a gtk_widget_show() is enough to show our popup.
- */
- gtk_container_add(GTK_CONTAINER(self), box);
- }
-
- /**
- * Allocate position in an overlay.
- *
- * This function can be used as the "get-child-position" signal
- * handler of a GtkOverlay in order to position the popup at the
- * bottom of the overlay's main child, spanning the entire width.
- * In contrast to the GtkOverlay's default allocation schemes,
- * this makes sure that the widget will not be larger than the
- * main child, so the popup properly scrolls when becoming too large
- * in height.
- *
- * @param user_data unused by this callback
- */
- public gboolean
- get_position_in_overlay(Gtk:Overlay *overlay, Gtk:Widget *widget,
- Gdk:Rectangle *allocation, gpointer user_data)
- {
- GtkWidget *main_child = gtk_bin_get_child(GTK_BIN(overlay));
- GtkAllocation main_child_alloc;
- gint natural_height;
-
- gtk_widget_get_allocation(main_child, &main_child_alloc);
- gtk_widget_get_preferred_height_for_width(widget,
- main_child_alloc.width,
- NULL, &natural_height);
-
- /*
- * FIXME: Probably due to some bug in the height-for-width
- * calculation of Gtk (at least in 3.10 or in the GtkFlowBox
- * fallback included with SciTECO), the natural height
- * is a bit too small to accommodate the entire GtkFlowBox,
- * resulting in the GtkViewport always scrolling.
- * This hack fixes it up in a NONPORTABLE manner.
- */
- natural_height += 5;
-
- allocation->width = main_child_alloc.width;
- allocation->height = MIN(natural_height, main_child_alloc.height);
- allocation->x = 0;
- allocation->y = main_child_alloc.height - allocation->height;
-
- return TRUE;
- }
-
- /*
- * Adapted from GtkScrolledWindow's gtk_scrolled_window_scroll_event()
- * since the viewport does not react to scroll events.
- * This is registered for our container widget instead of only for
- * GtkViewport since this is what GtkScrolledWindow does.
- * FIXME: May need to handle non-delta scrolling, i.e. GDK_SCROLL_UP
- * and GDK_SCROLL_DOWN.
- */
- override (Gtk:Widget) gboolean
- scroll_event(Gtk:Widget *widget, Gdk:Event:Scroll *event)
- {
- Self *self = SELF(widget);
- gdouble delta_x, delta_y;
-
- if (gdk_event_get_scroll_deltas((GdkEvent *)event,
- &delta_x, &delta_y)) {
- GtkAdjustment *adj = self->vadjustment;
- gdouble page_size = gtk_adjustment_get_page_size(adj);
- gdouble scroll_unit = pow(page_size, 2.0 / 3.0);
- gdouble new_value;
-
- new_value = CLAMP(gtk_adjustment_get_value(adj) + delta_y * scroll_unit,
- gtk_adjustment_get_lower(adj),
- gtk_adjustment_get_upper(adj) -
- gtk_adjustment_get_page_size(adj));
-
- gtk_adjustment_set_value(adj, new_value);
-
- return TRUE;
- }
-
- return FALSE;
- }
-
- private void
- vadjustment_changed(Gtk:Adjustment *vadjustment, gpointer user_data)
- {
- GtkWidget *scrollbar = GTK_WIDGET(user_data);
-
- /*
- * This shows/hides the widget using opacity instead of using
- * gtk_widget_set_visibility() since the latter would influence
- * size allocations. A widget with opacity 0 keeps its size.
- */
- gtk_widget_set_opacity(scrollbar,
- gtk_adjustment_get_upper(vadjustment) -
- gtk_adjustment_get_lower(vadjustment) >
- gtk_adjustment_get_page_size(vadjustment) ? 1 : 0);
- }
-
- public GtkWidget *
- new(void)
- {
- Self *widget = GET_NEW;
- return GTK_WIDGET(widget);
- }
-
- public GIcon *
- get_icon_for_path(const gchar *path, const gchar *fallback_name)
- {
- GFile *file;
- GFileInfo *info;
- GIcon *icon = NULL;
-
- file = g_file_new_for_path(path);
- info = g_file_query_info(file, "standard::icon", 0, NULL, NULL);
- if (info) {
- icon = g_file_info_get_icon(info);
- g_object_ref(icon);
- g_object_unref(info);
- } else {
- /* fall back to standard icon, but this can still return NULL! */
- icon = g_icon_new_for_string(fallback_name, NULL);
- }
- g_object_unref(file);
-
- return icon;
- }
-
- public void
- add(self, Gtk:Info:Popup:Entry:Type type,
- const gchar *name, gboolean highlight)
- {
- GtkWidget *hbox;
- GtkWidget *label;
-
- hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
- if (highlight)
- gtk_style_context_add_class(gtk_widget_get_style_context(hbox),
- "highlight");
-
- if (type == GTK_INFO_POPUP_FILE || type == GTK_INFO_POPUP_DIRECTORY) {
- const gchar *fallback = type == GTK_INFO_POPUP_FILE ? "text-x-generic"
- : "folder";
- GIcon *icon;
-
- icon = self_get_icon_for_path(name, fallback);
- if (icon) {
- GtkWidget *image;
-
- image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU);
- g_object_unref(icon);
- gtk_box_pack_start(GTK_BOX(hbox), image,
- FALSE, FALSE, 0);
- }
- }
-
- label = gtk_canonicalized_label_new(name);
- gtk_widget_set_halign(label, GTK_ALIGN_START);
- gtk_widget_set_valign(label, GTK_ALIGN_CENTER);
-
- /*
- * FIXME: This makes little sense once we've got mouse support.
- * But for the time being, it's a useful setting.
- */
- gtk_label_set_selectable(GTK_LABEL(label), TRUE);
-
- switch (type) {
- case GTK_INFO_POPUP_PLAIN:
- gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_START);
- break;
- case GTK_INFO_POPUP_FILE:
- case GTK_INFO_POPUP_DIRECTORY:
- gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
- break;
- }
-
- gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
-
- gtk_widget_show_all(hbox);
- gtk_container_add(GTK_CONTAINER(self->_priv->flow_box), hbox);
- }
-
- public void
- scroll_page(self)
- {
- GtkAdjustment *adj = self->vadjustment;
- gdouble new_value;
-
- if (gtk_adjustment_get_value(adj) + gtk_adjustment_get_page_size(adj) ==
- gtk_adjustment_get_upper(adj)) {
- /* wrap and scroll back to the top */
- new_value = gtk_adjustment_get_lower(adj);
- } else {
- /* scroll one page */
- GList *child_list;
-
- new_value = gtk_adjustment_get_value(adj) +
- gtk_adjustment_get_page_size(adj);
-
- /*
- * Adjust this so only complete entries are shown.
- * Effectively, this rounds down to the line height.
- */
- child_list = gtk_container_get_children(GTK_CONTAINER(self->_priv->flow_box));
- if (child_list) {
- new_value -= (gint)new_value %
- gtk_widget_get_allocated_height(GTK_WIDGET(child_list->data));
- g_list_free(child_list);
- }
-
- /* clip to the maximum possible value */
- new_value = MIN(new_value, gtk_adjustment_get_upper(adj));
- }
-
- gtk_adjustment_set_value(adj, new_value);
- }
-
- public void
- clear(self)
- {
- GList *children;
-
- children = gtk_container_get_children(GTK_CONTAINER(self->_priv->flow_box));
- for (GList *cur = g_list_first(children);
- cur != NULL;
- cur = g_list_next(cur))
- gtk_widget_destroy(GTK_WIDGET(cur->data));
- g_list_free(children);
- }
-}
diff --git a/src/interface-gtk/gtkflowbox.c b/src/interface-gtk/gtkflowbox.c
deleted file mode 100644
index 1a5c2e9..0000000
--- a/src/interface-gtk/gtkflowbox.c
+++ /dev/null
@@ -1,4795 +0,0 @@
-/*
- * Copyright (C) 2007-2010 Openismus GmbH
- * Copyright (C) 2013 Red Hat, Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Library General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This library 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
- * Library General Public License for more details.
- *
- * You should have received a copy of the GNU Library General Public
- * License along with this library; if not, see <http://www.gnu.org/licenses/>.
- *
- * Authors:
- * Tristan Van Berkom <tristanvb@openismus.com>
- * Matthias Clasen <mclasen@redhat.com>
- * William Jon McCann <jmccann@redhat.com>
- */
-
-/* Preamble {{{1 */
-
-/**
- * SECTION:gtkflowbox
- * @Short_Description: A container that allows reflowing its children
- * @Title: GtkFlowBox
- *
- * A GtkFlowBox positions child widgets in sequence according to its
- * orientation.
- *
- * For instance, with the horizontal orientation, the widgets will be
- * arranged from left to right, starting a new row under the previous
- * row when necessary. Reducing the width in this case will require more
- * rows, so a larger height will be requested.
- *
- * Likewise, with the vertical orientation, the widgets will be arranged
- * from top to bottom, starting a new column to the right when necessary.
- * Reducing the height will require more columns, so a larger width will
- * be requested.
- *
- * The children of a GtkFlowBox can be dynamically sorted and filtered.
- *
- * Although a GtkFlowBox must have only #GtkFlowBoxChild children,
- * you can add any kind of widget to it via gtk_container_add(), and
- * a GtkFlowBoxChild widget will automatically be inserted between
- * the box and the widget.
- *
- * Also see #GtkListBox.
- *
- * GtkFlowBox was added in GTK+ 3.12.
- */
-#ifdef HAVE_CONFIG_H
-#include <config.h>
-#endif
-
-#include <gtk/gtk.h>
-
-#include "gtkflowbox.h"
-
-/* Forward declarations and utilities {{{1 */
-
-static void gtk_flow_box_update_cursor (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-static void gtk_flow_box_select_and_activate (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-static void gtk_flow_box_update_selection (GtkFlowBox *box,
- GtkFlowBoxChild *child,
- gboolean modify,
- gboolean extend);
-static void gtk_flow_box_apply_filter (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-static void gtk_flow_box_apply_sort (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-static gint gtk_flow_box_sort (GtkFlowBoxChild *a,
- GtkFlowBoxChild *b,
- GtkFlowBox *box);
-
-static void
-get_current_selection_modifiers (GtkWidget *widget,
- gboolean *modify,
- gboolean *extend)
-{
- GdkModifierType state = 0;
- GdkModifierType mask;
-
- *modify = FALSE;
- *extend = FALSE;
-
- if (gtk_get_current_event_state (&state))
- {
- mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_MODIFY_SELECTION);
- if ((state & mask) == mask)
- *modify = TRUE;
- mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_EXTEND_SELECTION);
- if ((state & mask) == mask)
- *extend = TRUE;
- }
-}
-
-static void
-path_from_horizontal_line_rects (cairo_t *cr,
- GdkRectangle *lines,
- gint n_lines)
-{
- gint start_line, end_line;
- GdkRectangle *r;
- gint i;
-
- /* Join rows vertically by extending to the middle */
- for (i = 0; i < n_lines - 1; i++)
- {
- GdkRectangle *r1 = &lines[i];
- GdkRectangle *r2 = &lines[i+1];
- gint gap, old;
-
- gap = r2->y - (r1->y + r1->height);
- r1->height += gap / 2;
- old = r2->y;
- r2->y = r1->y + r1->height;
- r2->height += old - r2->y;
- }
-
- cairo_new_path (cr);
- start_line = 0;
-
- do
- {
- for (i = start_line; i < n_lines; i++)
- {
- r = &lines[i];
- if (i == start_line)
- cairo_move_to (cr, r->x + r->width, r->y);
- else
- cairo_line_to (cr, r->x + r->width, r->y);
- cairo_line_to (cr, r->x + r->width, r->y + r->height);
-
- if (i < n_lines - 1 &&
- (r->x + r->width < lines[i+1].x ||
- r->x > lines[i+1].x + lines[i+1].width))
- {
- i++;
- break;
- }
- }
- end_line = i;
- for (i = end_line - 1; i >= start_line; i--)
- {
- r = &lines[i];
- cairo_line_to (cr, r->x, r->y + r->height);
- cairo_line_to (cr, r->x, r->y);
- }
- cairo_close_path (cr);
- start_line = end_line;
- }
- while (end_line < n_lines);
-}
-
-static void
-path_from_vertical_line_rects (cairo_t *cr,
- GdkRectangle *lines,
- gint n_lines)
-{
- gint start_line, end_line;
- GdkRectangle *r;
- gint i;
-
- /* Join rows horizontally by extending to the middle */
- for (i = 0; i < n_lines - 1; i++)
- {
- GdkRectangle *r1 = &lines[i];
- GdkRectangle *r2 = &lines[i+1];
- gint gap, old;
-
- gap = r2->x - (r1->x + r1->width);
- r1->width += gap / 2;
- old = r2->x;
- r2->x = r1->x + r1->width;
- r2->width += old - r2->x;
- }
-
- cairo_new_path (cr);
- start_line = 0;
-
- do
- {
- for (i = start_line; i < n_lines; i++)
- {
- r = &lines[i];
- if (i == start_line)
- cairo_move_to (cr, r->x, r->y + r->height);
- else
- cairo_line_to (cr, r->x, r->y + r->height);
- cairo_line_to (cr, r->x + r->width, r->y + r->height);
-
- if (i < n_lines - 1 &&
- (r->y + r->height < lines[i+1].y ||
- r->y > lines[i+1].y + lines[i+1].height))
- {
- i++;
- break;
- }
- }
- end_line = i;
- for (i = end_line - 1; i >= start_line; i--)
- {
- r = &lines[i];
- cairo_line_to (cr, r->x + r->width, r->y);
- cairo_line_to (cr, r->x, r->y);
- }
- cairo_close_path (cr);
- start_line = end_line;
- }
- while (end_line < n_lines);
-}
-
-/* GtkFlowBoxChild {{{1 */
-
-/* GObject boilerplate {{{2 */
-
-enum {
- CHILD_ACTIVATE,
- CHILD_LAST_SIGNAL
-};
-
-static guint child_signals[CHILD_LAST_SIGNAL] = { 0 };
-
-typedef struct _GtkFlowBoxChildPrivate GtkFlowBoxChildPrivate;
-struct _GtkFlowBoxChildPrivate
-{
- GSequenceIter *iter;
- gboolean selected;
-};
-
-#define CHILD_PRIV(child) ((GtkFlowBoxChildPrivate*)gtk_flow_box_child_get_instance_private ((GtkFlowBoxChild*)(child)))
-
-G_DEFINE_TYPE_WITH_PRIVATE (GtkFlowBoxChild, gtk_flow_box_child, GTK_TYPE_BIN)
-
-/* Internal API {{{2 */
-
-static GtkFlowBox *
-gtk_flow_box_child_get_box (GtkFlowBoxChild *child)
-{
- GtkWidget *parent;
-
- parent = gtk_widget_get_parent (GTK_WIDGET (child));
- if (parent && GTK_IS_FLOW_BOX (parent))
- return GTK_FLOW_BOX (parent);
-
- return NULL;
-}
-
-static void
-gtk_flow_box_child_set_focus (GtkFlowBoxChild *child)
-{
- GtkFlowBox *box = gtk_flow_box_child_get_box (child);
- gboolean modify;
- gboolean extend;
-
- get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend);
-
- if (modify)
- gtk_flow_box_update_cursor (box, child);
- else
- gtk_flow_box_update_selection (box, child, FALSE, FALSE);
-}
-
-/* GtkWidget implementation {{{2 */
-
-static gboolean
-gtk_flow_box_child_focus (GtkWidget *widget,
- GtkDirectionType direction)
-{
- gboolean had_focus = FALSE;
- GtkWidget *child;
-
- child = gtk_bin_get_child (GTK_BIN (widget));
-
- g_object_get (widget, "has-focus", &had_focus, NULL);
- if (had_focus)
- {
- /* If on row, going right, enter into possible container */
- if (child &&
- (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD))
- {
- if (gtk_widget_child_focus (GTK_WIDGET (child), direction))
- return TRUE;
- }
-
- return FALSE;
- }
- else if (gtk_container_get_focus_child (GTK_CONTAINER (widget)) != NULL)
- {
- /* Child has focus, always navigate inside it first */
- if (gtk_widget_child_focus (child, direction))
- return TRUE;
-
- /* If exiting child container to the left, select child */
- if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD)
- {
- gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget));
- return TRUE;
- }
-
- return FALSE;
- }
- else
- {
- /* If coming from the left, enter into possible container */
- if (child &&
- (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD))
- {
- if (gtk_widget_child_focus (child, direction))
- return TRUE;
- }
-
- gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget));
- return TRUE;
- }
-}
-
-static void
-gtk_flow_box_child_activate (GtkFlowBoxChild *child)
-{
- GtkFlowBox *box;
-
- box = gtk_flow_box_child_get_box (child);
- if (box)
- gtk_flow_box_select_and_activate (box, child);
-}
-
-static gboolean
-gtk_flow_box_child_draw (GtkWidget *widget,
- cairo_t *cr)
-{
- GtkAllocation allocation = {0};
- GtkStyleContext* context;
- GtkStateFlags state;
- GtkBorder border;
- gint focus_pad;
-
- gtk_widget_get_allocation (widget, &allocation);
- context = gtk_widget_get_style_context (widget);
- state = gtk_widget_get_state_flags (widget);
-
- gtk_render_background (context, cr, 0, 0, allocation.width, allocation.height);
- gtk_render_frame (context, cr, 0, 0, allocation.width, allocation.height);
-
- if (gtk_widget_has_visible_focus (widget))
- {
- gtk_style_context_get_border (context, state, &border);
-
- gtk_style_context_get_style (context,
- "focus-padding", &focus_pad,
- NULL);
- gtk_render_focus (context, cr, border.left + focus_pad, border.top + focus_pad,
- allocation.width - 2 * focus_pad - border.left - border.right,
- allocation.height - 2 * focus_pad - border.top - border.bottom);
- }
-
- GTK_WIDGET_CLASS (gtk_flow_box_child_parent_class)->draw (widget, cr);
-
- return TRUE;
-}
-
-/* Size allocation {{{3 */
-
-static void
-gtk_flow_box_child_get_full_border (GtkFlowBoxChild *child,
- GtkBorder *full_border)
-{
- GtkWidget *widget = GTK_WIDGET (child);
- GtkStyleContext *context;
- GtkStateFlags state;
- GtkBorder padding, border;
- int focus_width, focus_pad;
-
- context = gtk_widget_get_style_context (widget);
- state = gtk_style_context_get_state (context);
-
- gtk_style_context_get_padding (context, state, &padding);
- gtk_style_context_get_border (context, state, &border);
- gtk_style_context_get_style (context,
- "focus-line-width", &focus_width,
- "focus-padding", &focus_pad,
- NULL);
-
- full_border->left = padding.left + border.left + focus_width + focus_pad;
- full_border->right = padding.right + border.right + focus_width + focus_pad;
- full_border->top = padding.top + border.top + focus_width + focus_pad;
- full_border->bottom = padding.bottom + border.bottom + focus_width + focus_pad;
-}
-
-static GtkSizeRequestMode
-gtk_flow_box_child_get_request_mode (GtkWidget *widget)
-{
- GtkFlowBox *box;
-
- box = gtk_flow_box_child_get_box (GTK_FLOW_BOX_CHILD (widget));
- if (box)
- return gtk_widget_get_request_mode (GTK_WIDGET (box));
- else
- return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
-}
-
-static void gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget,
- gint height,
- gint *minimum_width,
- gint *natural_width);
-static void gtk_flow_box_child_get_preferred_height (GtkWidget *widget,
- gint *minimum_height,
- gint *natural_height);
-
-static void
-gtk_flow_box_child_get_preferred_height_for_width (GtkWidget *widget,
- gint width,
- gint *minimum_height,
- gint *natural_height)
-{
- if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
- {
- GtkWidget *child;
- gint child_min = 0, child_natural = 0;
- GtkBorder full_border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border);
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- gtk_widget_get_preferred_height_for_width (child, width - full_border.left - full_border.right,
- &child_min, &child_natural);
-
- if (minimum_height)
- *minimum_height = full_border.top + child_min + full_border.bottom;
- if (natural_height)
- *natural_height = full_border.top + child_natural + full_border.bottom;
- }
- else
- {
- gtk_flow_box_child_get_preferred_height (widget, minimum_height, natural_height);
- }
-}
-
-static void
-gtk_flow_box_child_get_preferred_width (GtkWidget *widget,
- gint *minimum_width,
- gint *natural_width)
-{
- if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
- {
- GtkWidget *child;
- gint child_min = 0, child_natural = 0;
- GtkBorder full_border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border);
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- gtk_widget_get_preferred_width (child, &child_min, &child_natural);
-
- if (minimum_width)
- *minimum_width = full_border.left + child_min + full_border.right;
- if (natural_width)
- *natural_width = full_border.left + child_natural + full_border.right;
- }
- else
- {
- gint natural_height;
-
- gtk_flow_box_child_get_preferred_height (widget, NULL, &natural_height);
- gtk_flow_box_child_get_preferred_width_for_height (widget, natural_height,
- minimum_width, natural_width);
- }
-}
-
-static void
-gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget,
- gint height,
- gint *minimum_width,
- gint *natural_width)
-{
- if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
- {
- gtk_flow_box_child_get_preferred_width (widget, minimum_width, natural_width);
- }
- else
- {
- GtkWidget *child;
- gint child_min = 0, child_natural = 0;
- GtkBorder full_border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border);
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- gtk_widget_get_preferred_width_for_height (child, height - full_border.top - full_border.bottom,
- &child_min, &child_natural);
-
- if (minimum_width)
- *minimum_width = full_border.left + child_min + full_border.right;
- if (natural_width)
- *natural_width = full_border.left + child_natural + full_border.right;
- }
-}
-
-static void
-gtk_flow_box_child_get_preferred_height (GtkWidget *widget,
- gint *minimum_height,
- gint *natural_height)
-{
- if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
- {
- gint natural_width;
-
- gtk_flow_box_child_get_preferred_width (widget, NULL, &natural_width);
- gtk_flow_box_child_get_preferred_height_for_width (widget, natural_width,
- minimum_height, natural_height);
- }
- else
- {
- GtkWidget *child;
- gint child_min = 0, child_natural = 0;
- GtkBorder full_border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border);
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- gtk_widget_get_preferred_height (child, &child_min, &child_natural);
-
- if (minimum_height)
- *minimum_height = full_border.top + child_min + full_border.bottom;
- if (natural_height)
- *natural_height = full_border.top + child_natural + full_border.bottom;
- }
-}
-
-static void
-gtk_flow_box_child_size_allocate (GtkWidget *widget,
- GtkAllocation *allocation)
-{
- GtkWidget *child;
-
- gtk_widget_set_allocation (widget, allocation);
-
- child = gtk_bin_get_child (GTK_BIN (widget));
- if (child && gtk_widget_get_visible (child))
- {
- GtkAllocation child_allocation;
- GtkBorder border = { 0, };
-
- gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &border);
-
- child_allocation.x = allocation->x + border.left;
- child_allocation.y = allocation->y + border.top;
- child_allocation.width = allocation->width - border.left - border.right;
- child_allocation.height = allocation->height - border.top - border.bottom;
-
- child_allocation.width = MAX (1, child_allocation.width);
- child_allocation.height = MAX (1, child_allocation.height);
-
- gtk_widget_size_allocate (child, &child_allocation);
- }
-}
-
-/* GObject implementation {{{2 */
-
-static void
-gtk_flow_box_child_class_init (GtkFlowBoxChildClass *class)
-{
- GObjectClass *object_class = G_OBJECT_CLASS (class);
- GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
-
- widget_class->draw = gtk_flow_box_child_draw;
- widget_class->get_request_mode = gtk_flow_box_child_get_request_mode;
- widget_class->get_preferred_height = gtk_flow_box_child_get_preferred_height;
- widget_class->get_preferred_height_for_width = gtk_flow_box_child_get_preferred_height_for_width;
- widget_class->get_preferred_width = gtk_flow_box_child_get_preferred_width;
- widget_class->get_preferred_width_for_height = gtk_flow_box_child_get_preferred_width_for_height;
- widget_class->size_allocate = gtk_flow_box_child_size_allocate;
- widget_class->focus = gtk_flow_box_child_focus;
-
- class->activate = gtk_flow_box_child_activate;
-
- /**
- * GtkFlowBoxChild::activate:
- * @child: The child on which the signal is emitted
- *
- * The ::activate signal is emitted when the user activates
- * a child widget in a #GtkFlowBox, either by clicking or
- * double-clicking, or by using the Space or Enter key.
- *
- * While this signal is used as a
- * [keybinding signal][GtkBindingSignal],
- * it can be used by applications for their own purposes.
- */
- child_signals[CHILD_ACTIVATE] =
- g_signal_new ("activate",
- G_OBJECT_CLASS_TYPE (object_class),
- G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxChildClass, activate),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
- widget_class->activate_signal = child_signals[CHILD_ACTIVATE];
-
- gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_LIST_ITEM);
-}
-
-static void
-gtk_flow_box_child_init (GtkFlowBoxChild *child)
-{
- GtkStyleContext *context;
-
- gtk_widget_set_can_focus (GTK_WIDGET (child), TRUE);
- gtk_widget_set_redraw_on_allocate (GTK_WIDGET (child), TRUE);
-
- context = gtk_widget_get_style_context (GTK_WIDGET (child));
- gtk_style_context_add_class (context, "grid-child");
-}
-
-/* Public API {{{2 */
-
-/**
- * gtk_flow_box_child_new:
- *
- * Creates a new #GtkFlowBoxChild, to be used as a child
- * of a #GtkFlowBox.
- *
- * Returns: a new #GtkFlowBoxChild
- *
- * Since: 3.12
- */
-GtkWidget *
-gtk_flow_box_child_new (void)
-{
- return g_object_new (GTK_TYPE_FLOW_BOX_CHILD, NULL);
-}
-
-/**
- * gtk_flow_box_child_get_index:
- * @child: a #GtkFlowBoxChild
- *
- * Gets the current index of the @child in its #GtkFlowBox container.
- *
- * Returns: the index of the @child, or -1 if the @child is not
- * in a flow box.
- *
- * Since: 3.12
- */
-gint
-gtk_flow_box_child_get_index (GtkFlowBoxChild *child)
-{
- GtkFlowBoxChildPrivate *priv;
-
- g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), -1);
-
- priv = CHILD_PRIV (child);
-
- if (priv->iter != NULL)
- return g_sequence_iter_get_position (priv->iter);
-
- return -1;
-}
-
-/**
- * gtk_flow_box_child_is_selected:
- * @child: a #GtkFlowBoxChild
- *
- * Returns whether the @child is currently selected in its
- * #GtkFlowBox container.
- *
- * Returns: %TRUE if @child is selected
- *
- * Since: 3.12
- */
-gboolean
-gtk_flow_box_child_is_selected (GtkFlowBoxChild *child)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), FALSE);
-
- return CHILD_PRIV (child)->selected;
-}
-
-/**
- * gtk_flow_box_child_changed:
- * @child: a #GtkFlowBoxChild
- *
- * Marks @child as changed, causing any state that depends on this
- * to be updated. This affects sorting and filtering.
- *
- * Note that calls to this method must be in sync with the data
- * used for the sorting and filtering functions. For instance, if
- * the list is mirroring some external data set, and *two* children
- * changed in the external data set when you call
- * gtk_flow_box_child_changed() on the first child, the sort function
- * must only read the new data for the first of the two changed
- * children, otherwise the resorting of the children will be wrong.
- *
- * This generally means that if you don’t fully control the data
- * model, you have to duplicate the data that affects the sorting
- * and filtering functions into the widgets themselves. Another
- * alternative is to call gtk_flow_box_invalidate_sort() on any
- * model change, but that is more expensive.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_child_changed (GtkFlowBoxChild *child)
-{
- GtkFlowBox *box;
-
- g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child));
-
- box = gtk_flow_box_child_get_box (child);
-
- if (box == NULL)
- return;
-
- gtk_flow_box_apply_sort (box, child);
- gtk_flow_box_apply_filter (box, child);
-}
-
-/* GtkFlowBox {{{1 */
-
-/* Constants {{{2 */
-
-#define DEFAULT_MAX_CHILDREN_PER_LINE 7
-#define RUBBERBAND_START_DISTANCE 32
-#define AUTOSCROLL_FAST_DISTANCE 32
-#define AUTOSCROLL_FACTOR 20
-#define AUTOSCROLL_FACTOR_FAST 10
-
-/* GObject boilerplate {{{2 */
-
-enum {
- CHILD_ACTIVATED,
- SELECTED_CHILDREN_CHANGED,
- ACTIVATE_CURSOR_CHILD,
- TOGGLE_CURSOR_CHILD,
- MOVE_CURSOR,
- SELECT_ALL,
- UNSELECT_ALL,
- LAST_SIGNAL
-};
-
-static guint signals[LAST_SIGNAL] = { 0 };
-
-enum {
- PROP_0,
- PROP_ORIENTATION,
- PROP_HOMOGENEOUS,
- PROP_HALIGN_POLICY,
- PROP_VALIGN_POLICY,
- PROP_COLUMN_SPACING,
- PROP_ROW_SPACING,
- PROP_MIN_CHILDREN_PER_LINE,
- PROP_MAX_CHILDREN_PER_LINE,
- PROP_SELECTION_MODE,
- PROP_ACTIVATE_ON_SINGLE_CLICK
-};
-
-typedef struct _GtkFlowBoxPrivate GtkFlowBoxPrivate;
-struct _GtkFlowBoxPrivate {
- GtkOrientation orientation;
- gboolean homogeneous;
-
- guint row_spacing;
- guint column_spacing;
-
- GtkFlowBoxChild *prelight_child;
- GtkFlowBoxChild *cursor_child;
- GtkFlowBoxChild *selected_child;
-
- gboolean active_child_active;
- GtkFlowBoxChild *active_child;
-
- GtkSelectionMode selection_mode;
-
- GtkAdjustment *hadjustment;
- GtkAdjustment *vadjustment;
- gboolean activate_on_single_click;
-
- guint16 min_children_per_line;
- guint16 max_children_per_line;
- guint16 cur_children_per_line;
-
- GSequence *children;
-
- GtkFlowBoxFilterFunc filter_func;
- gpointer filter_data;
- GDestroyNotify filter_destroy;
-
- GtkFlowBoxSortFunc sort_func;
- gpointer sort_data;
- GDestroyNotify sort_destroy;
-
- gboolean track_motion;
- gboolean rubberband_select;
- GtkFlowBoxChild *rubberband_first;
- GtkFlowBoxChild *rubberband_last;
- gint button_down_x;
- gint button_down_y;
- gboolean rubberband_modify;
- gboolean rubberband_extend;
- GdkDevice *rubberband_device;
-
- GtkScrollType autoscroll_mode;
- guint autoscroll_id;
-};
-
-#define BOX_PRIV(box) ((GtkFlowBoxPrivate*)gtk_flow_box_get_instance_private ((GtkFlowBox*)(box)))
-
-G_DEFINE_TYPE_WITH_CODE (GtkFlowBox, gtk_flow_box, GTK_TYPE_CONTAINER,
- G_ADD_PRIVATE (GtkFlowBox)
- G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
-
-/* Internal API, utilities {{{2 */
-
-#define ORIENTATION_ALIGN(box) \
- (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \
- ? gtk_widget_get_halign (GTK_WIDGET (box)) \
- : gtk_widget_get_valign (GTK_WIDGET (box)))
-
-#define OPPOSING_ORIENTATION_ALIGN(box) \
- (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \
- ? gtk_widget_get_valign (GTK_WIDGET (box)) \
- : gtk_widget_get_halign (GTK_WIDGET (box)))
-
-/* Children are visible if they are shown by the app (visible)
- * and not filtered out (child_visible) by the box
- */
-static inline gboolean
-child_is_visible (GtkWidget *child)
-{
- return gtk_widget_get_visible (child) &&
- gtk_widget_get_child_visible (child);
-}
-
-static gint
-get_visible_children (GtkFlowBox *box)
-{
- GSequenceIter *iter;
- gint i = 0;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
-
- child = g_sequence_get (iter);
- if (child_is_visible (child))
- i++;
- }
-
- return i;
-}
-
-static GtkFlowBoxChild *
-gtk_flow_box_find_child_at_pos (GtkFlowBox *box,
- gint x,
- gint y)
-{
- GtkWidget *child;
- GSequenceIter *iter;
- GtkAllocation allocation;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- child = g_sequence_get (iter);
- if (!child_is_visible (child))
- continue;
- gtk_widget_get_allocation (child, &allocation);
- if (x >= allocation.x && x < (allocation.x + allocation.width) &&
- y >= allocation.y && y < (allocation.y + allocation.height))
- return GTK_FLOW_BOX_CHILD (child);
- }
-
- return NULL;
-}
-
-static void
-gtk_flow_box_update_prelight (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (child != priv->prelight_child)
- {
- priv->prelight_child = child;
- gtk_widget_queue_draw (GTK_WIDGET (box));
- }
-}
-
-static void
-gtk_flow_box_update_active (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gboolean val;
-
- val = priv->active_child == child;
- if (priv->active_child != NULL &&
- val != priv->active_child_active)
- {
- priv->active_child_active = val;
- gtk_widget_queue_draw (GTK_WIDGET (box));
- }
-}
-
-static void
-gtk_flow_box_apply_filter (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gboolean do_show;
-
- do_show = TRUE;
- if (priv->filter_func != NULL)
- do_show = priv->filter_func (child, priv->filter_data);
-
- gtk_widget_set_child_visible (GTK_WIDGET (child), do_show);
-}
-
-static void
-gtk_flow_box_apply_filter_all (GtkFlowBox *box)
-{
- GSequenceIter *iter;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkFlowBoxChild *child;
-
- child = g_sequence_get (iter);
- gtk_flow_box_apply_filter (box, child);
- }
- gtk_widget_queue_resize (GTK_WIDGET (box));
-}
-
-static void
-gtk_flow_box_apply_sort (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- if (BOX_PRIV (box)->sort_func != NULL)
- {
- g_sequence_sort_changed (CHILD_PRIV (child)->iter,
- (GCompareDataFunc)gtk_flow_box_sort, box);
- gtk_widget_queue_resize (GTK_WIDGET (box));
- }
-}
-
-/* Selection utilities {{{3 */
-
-static gboolean
-gtk_flow_box_child_set_selected (GtkFlowBoxChild *child,
- gboolean selected)
-{
- if (CHILD_PRIV (child)->selected != selected)
- {
- CHILD_PRIV (child)->selected = selected;
- if (selected)
- gtk_widget_set_state_flags (GTK_WIDGET (child),
- GTK_STATE_FLAG_SELECTED, FALSE);
- else
- gtk_widget_unset_state_flags (GTK_WIDGET (child),
- GTK_STATE_FLAG_SELECTED);
-
- gtk_widget_queue_draw (GTK_WIDGET (child));
-
- return TRUE;
- }
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_unselect_all_internal (GtkFlowBox *box)
-{
- GtkFlowBoxChild *child;
- GSequenceIter *iter;
- gboolean dirty = FALSE;
-
- if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE)
- return FALSE;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- child = g_sequence_get (iter);
- dirty |= gtk_flow_box_child_set_selected (child, FALSE);
- }
-
- return dirty;
-}
-
-static void
-gtk_flow_box_unselect_child_internal (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- if (!CHILD_PRIV (child)->selected)
- return;
-
- if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE)
- return;
- else if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE)
- gtk_flow_box_unselect_all_internal (box);
- else
- gtk_flow_box_child_set_selected (child, FALSE);
-
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-static void
-gtk_flow_box_update_cursor (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- BOX_PRIV (box)->cursor_child = child;
- gtk_widget_grab_focus (GTK_WIDGET (child));
- gtk_widget_queue_draw (GTK_WIDGET (child));
-}
-
-static void
-gtk_flow_box_select_child_internal (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- if (CHILD_PRIV (child)->selected)
- return;
-
- if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE)
- return;
- if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE)
- gtk_flow_box_unselect_all_internal (box);
-
- gtk_flow_box_child_set_selected (child, TRUE);
- BOX_PRIV (box)->selected_child = child;
-
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-static void
-gtk_flow_box_select_all_between (GtkFlowBox *box,
- GtkFlowBoxChild *child1,
- GtkFlowBoxChild *child2,
- gboolean modify)
-{
- GSequenceIter *iter, *iter1, *iter2;
-
- if (child1)
- iter1 = CHILD_PRIV (child1)->iter;
- else
- iter1 = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
-
- if (child2)
- iter2 = CHILD_PRIV (child2)->iter;
- else
- iter2 = g_sequence_get_end_iter (BOX_PRIV (box)->children);
-
- if (g_sequence_iter_compare (iter2, iter1) < 0)
- {
- iter = iter1;
- iter1 = iter2;
- iter2 = iter;
- }
-
- for (iter = iter1;
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
-
- child = g_sequence_get (iter);
- if (child_is_visible (child))
- {
- if (modify)
- gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), !CHILD_PRIV (child)->selected);
- else
- gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), TRUE);
- }
-
- if (g_sequence_iter_compare (iter, iter2) == 0)
- break;
- }
-}
-
-static void
-gtk_flow_box_update_selection (GtkFlowBox *box,
- GtkFlowBoxChild *child,
- gboolean modify,
- gboolean extend)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- gtk_flow_box_update_cursor (box, child);
-
- if (priv->selection_mode == GTK_SELECTION_NONE)
- return;
-
- if (priv->selection_mode == GTK_SELECTION_BROWSE)
- {
- gtk_flow_box_unselect_all_internal (box);
- gtk_flow_box_child_set_selected (child, TRUE);
- priv->selected_child = child;
- }
- else if (priv->selection_mode == GTK_SELECTION_SINGLE)
- {
- gboolean was_selected;
-
- was_selected = CHILD_PRIV (child)->selected;
- gtk_flow_box_unselect_all_internal (box);
- gtk_flow_box_child_set_selected (child, modify ? !was_selected : TRUE);
- priv->selected_child = CHILD_PRIV (child)->selected ? child : NULL;
- }
- else /* GTK_SELECTION_MULTIPLE */
- {
- if (extend)
- {
- gtk_flow_box_unselect_all_internal (box);
- if (priv->selected_child == NULL)
- {
- gtk_flow_box_child_set_selected (child, TRUE);
- priv->selected_child = child;
- }
- else
- gtk_flow_box_select_all_between (box, priv->selected_child, child, FALSE);
- }
- else
- {
- if (modify)
- {
- gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected);
- }
- else
- {
- gtk_flow_box_unselect_all_internal (box);
- gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected);
- priv->selected_child = child;
- }
- }
- }
-
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-static void
-gtk_flow_box_select_and_activate (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- if (child != NULL)
- {
- gtk_flow_box_select_child_internal (box, child);
- gtk_flow_box_update_cursor (box, child);
- g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child);
- }
-}
-
-/* Focus utilities {{{3 */
-
-static GSequenceIter *
-gtk_flow_box_get_previous_focusable (GtkFlowBox *box,
- GSequenceIter *iter)
-{
- GtkFlowBoxChild *child;
-
- while (!g_sequence_iter_is_begin (iter))
- {
- iter = g_sequence_iter_prev (iter);
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)) &&
- gtk_widget_is_sensitive (GTK_WIDGET (child)))
- return iter;
- }
-
- return NULL;
-}
-
-static GSequenceIter *
-gtk_flow_box_get_next_focusable (GtkFlowBox *box,
- GSequenceIter *iter)
-{
- GtkFlowBoxChild *child;
-
- while (TRUE)
- {
- iter = g_sequence_iter_next (iter);
- if (g_sequence_iter_is_end (iter))
- return NULL;
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)) &&
- gtk_widget_is_sensitive (GTK_WIDGET (child)))
- return iter;
- }
-
- return NULL;
-}
-
-static GSequenceIter *
-gtk_flow_box_get_first_focusable (GtkFlowBox *box)
-{
- GSequenceIter *iter;
- GtkFlowBoxChild *child;
-
- iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- if (g_sequence_iter_is_end (iter))
- return NULL;
-
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)) &&
- gtk_widget_is_sensitive (GTK_WIDGET (child)))
- return iter;
-
- return gtk_flow_box_get_next_focusable (box, iter);
-}
-
-static GSequenceIter *
-gtk_flow_box_get_last_focusable (GtkFlowBox *box)
-{
- GSequenceIter *iter;
-
- iter = g_sequence_get_end_iter (BOX_PRIV (box)->children);
- return gtk_flow_box_get_previous_focusable (box, iter);
-}
-
-
-static GSequenceIter *
-gtk_flow_box_get_above_focusable (GtkFlowBox *box,
- GSequenceIter *iter)
-{
- GtkFlowBoxChild *child = NULL;
- gint i;
-
- while (TRUE)
- {
- i = 0;
- while (i < BOX_PRIV (box)->cur_children_per_line)
- {
- if (g_sequence_iter_is_begin (iter))
- return NULL;
- iter = g_sequence_iter_prev (iter);
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)))
- i++;
- }
- if (child && gtk_widget_get_sensitive (GTK_WIDGET (child)))
- return iter;
- }
-
- return NULL;
-}
-
-static GSequenceIter *
-gtk_flow_box_get_below_focusable (GtkFlowBox *box,
- GSequenceIter *iter)
-{
- GtkFlowBoxChild *child = NULL;
- gint i;
-
- while (TRUE)
- {
- i = 0;
- while (i < BOX_PRIV (box)->cur_children_per_line)
- {
- iter = g_sequence_iter_next (iter);
- if (g_sequence_iter_is_end (iter))
- return NULL;
- child = g_sequence_get (iter);
- if (child_is_visible (GTK_WIDGET (child)))
- i++;
- }
- if (child && gtk_widget_get_sensitive (GTK_WIDGET (child)))
- return iter;
- }
-
- return NULL;
-}
-
-/* GtkWidget implementation {{{2 */
-
-/* Size allocation {{{3 */
-
-/* Used in columned modes where all items share at least their
- * equal widths or heights
- */
-static void
-get_max_item_size (GtkFlowBox *box,
- GtkOrientation orientation,
- gint *min_size,
- gint *nat_size)
-{
- GSequenceIter *iter;
- gint max_min_size = 0;
- gint max_nat_size = 0;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint child_min, child_nat;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_width (child, &child_min, &child_nat);
- else
- gtk_widget_get_preferred_height (child, &child_min, &child_nat);
-
- max_min_size = MAX (max_min_size, child_min);
- max_nat_size = MAX (max_nat_size, child_nat);
- }
-
- if (min_size)
- *min_size = max_min_size;
-
- if (nat_size)
- *nat_size = max_nat_size;
-}
-
-
-/* Gets the largest minimum/natural size for a given size (used to get
- * the largest item heights for a fixed item width and the opposite)
- */
-static void
-get_largest_size_for_opposing_orientation (GtkFlowBox *box,
- GtkOrientation orientation,
- gint item_size,
- gint *min_item_size,
- gint *nat_item_size)
-{
- GSequenceIter *iter;
- gint max_min_size = 0;
- gint max_nat_size = 0;
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint child_min, child_nat;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_height_for_width (child,
- item_size,
- &child_min, &child_nat);
- else
- gtk_widget_get_preferred_width_for_height (child,
- item_size,
- &child_min, &child_nat);
-
- max_min_size = MAX (max_min_size, child_min);
- max_nat_size = MAX (max_nat_size, child_nat);
- }
-
- if (min_item_size)
- *min_item_size = max_min_size;
-
- if (nat_item_size)
- *nat_item_size = max_nat_size;
-}
-
-/* Gets the largest minimum/natural size on a single line for a given size
- * (used to get the largest line heights for a fixed item width and the opposite
- * while iterating over a list of children, note the new index is returned)
- */
-static GSequenceIter *
-get_largest_size_for_line_in_opposing_orientation (GtkFlowBox *box,
- GtkOrientation orientation,
- GSequenceIter *cursor,
- gint line_length,
- GtkRequestedSize *item_sizes,
- gint extra_pixels,
- gint *min_item_size,
- gint *nat_item_size)
-{
- GSequenceIter *iter;
- gint max_min_size = 0;
- gint max_nat_size = 0;
- gint i;
-
- i = 0;
- for (iter = cursor;
- !g_sequence_iter_is_end (iter) && i < line_length;
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint child_min, child_nat, this_item_size;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- /* Distribute the extra pixels to the first children in the line
- * (could be fancier and spread them out more evenly) */
- this_item_size = item_sizes[i].minimum_size;
- if (extra_pixels > 0 && ORIENTATION_ALIGN (box) == GTK_ALIGN_FILL)
- {
- this_item_size++;
- extra_pixels--;
- }
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_height_for_width (child,
- this_item_size,
- &child_min, &child_nat);
- else
- gtk_widget_get_preferred_width_for_height (child,
- this_item_size,
- &child_min, &child_nat);
-
- max_min_size = MAX (max_min_size, child_min);
- max_nat_size = MAX (max_nat_size, child_nat);
-
- i++;
- }
-
- if (min_item_size)
- *min_item_size = max_min_size;
-
- if (nat_item_size)
- *nat_item_size = max_nat_size;
-
- /* Return next item in the list */
- return iter;
-}
-
-/* fit_aligned_item_requests() helper */
-static gint
-gather_aligned_item_requests (GtkFlowBox *box,
- GtkOrientation orientation,
- gint line_length,
- gint item_spacing,
- gint n_children,
- GtkRequestedSize *item_sizes)
-{
- GSequenceIter *iter;
- gint i;
- gint extra_items, natural_line_size = 0;
-
- extra_items = n_children % line_length;
-
- i = 0;
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- GtkAlign item_align;
- gint child_min, child_nat;
- gint position;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_width (child,
- &child_min, &child_nat);
- else
- gtk_widget_get_preferred_height (child,
- &child_min, &child_nat);
-
- /* Get the index and push it over for the last line when spreading to the end */
- position = i % line_length;
-
- item_align = ORIENTATION_ALIGN (box);
- if (item_align == GTK_ALIGN_END && i >= n_children - extra_items)
- position += line_length - extra_items;
-
- /* Round up the size of every column/row */
- item_sizes[position].minimum_size = MAX (item_sizes[position].minimum_size, child_min);
- item_sizes[position].natural_size = MAX (item_sizes[position].natural_size, child_nat);
-
- i++;
- }
-
- for (i = 0; i < line_length; i++)
- natural_line_size += item_sizes[i].natural_size;
-
- natural_line_size += (line_length - 1) * item_spacing;
-
- return natural_line_size;
-}
-
-static GtkRequestedSize *
-fit_aligned_item_requests (GtkFlowBox *box,
- GtkOrientation orientation,
- gint avail_size,
- gint item_spacing,
- gint *line_length, /* in-out */
- gint items_per_line,
- gint n_children)
-{
- GtkRequestedSize *sizes, *try_sizes;
- gint try_line_size, try_length;
-
- sizes = g_new0 (GtkRequestedSize, *line_length);
-
- /* get the sizes for the initial guess */
- try_line_size = gather_aligned_item_requests (box,
- orientation,
- *line_length,
- item_spacing,
- n_children,
- sizes);
-
- /* Try columnizing the whole thing and adding an item to the end of
- * the line; try to fit as many columns into the available size as
- * possible
- */
- for (try_length = *line_length + 1; try_line_size < avail_size; try_length++)
- {
- try_sizes = g_new0 (GtkRequestedSize, try_length);
- try_line_size = gather_aligned_item_requests (box,
- orientation,
- try_length,
- item_spacing,
- n_children,
- try_sizes);
-
- if (try_line_size <= avail_size &&
- items_per_line >= try_length)
- {
- *line_length = try_length;
-
- g_free (sizes);
- sizes = try_sizes;
- }
- else
- {
- /* oops, this one failed; stick to the last size that fit and then return */
- g_free (try_sizes);
- break;
- }
- }
-
- return sizes;
-}
-
-typedef struct {
- GArray *requested;
- gint extra_pixels;
-} AllocatedLine;
-
-static gint
-get_offset_pixels (GtkAlign align,
- gint pixels)
-{
- gint offset;
-
- switch (align) {
- case GTK_ALIGN_START:
- case GTK_ALIGN_FILL:
- offset = 0;
- break;
- case GTK_ALIGN_CENTER:
- offset = pixels / 2;
- break;
- case GTK_ALIGN_END:
- offset = pixels;
- break;
- default:
- g_assert_not_reached ();
- break;
- }
-
- return offset;
-}
-
-static void
-gtk_flow_box_size_allocate (GtkWidget *widget,
- GtkAllocation *allocation)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkAllocation child_allocation;
- gint avail_size, avail_other_size, min_items, item_spacing, line_spacing;
- GtkAlign item_align;
- GtkAlign line_align;
- GdkWindow *window;
- GtkRequestedSize *line_sizes = NULL;
- GtkRequestedSize *item_sizes = NULL;
- gint min_item_size, nat_item_size;
- gint line_length;
- gint item_size = 0;
- gint line_size = 0, min_fixed_line_size = 0, nat_fixed_line_size = 0;
- gint line_offset, item_offset, n_children, n_lines, line_count;
- gint extra_pixels = 0, extra_per_item = 0, extra_extra = 0;
- gint extra_line_pixels = 0, extra_per_line = 0, extra_line_extra = 0;
- gint i, this_line_size;
- GSequenceIter *iter;
-
- child_allocation.x = 0;
- child_allocation.y = 0;
- child_allocation.width = 0;
- child_allocation.height = 0;
-
- gtk_widget_set_allocation (widget, allocation);
- window = gtk_widget_get_window (widget);
- if (window != NULL)
- gdk_window_move_resize (window,
- allocation->x, allocation->y,
- allocation->width, allocation->height);
-
- child_allocation.x = 0;
- child_allocation.y = 0;
- child_allocation.width = allocation->width;
-
- min_items = MAX (1, priv->min_children_per_line);
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- avail_size = allocation->width;
- avail_other_size = allocation->height;
- item_spacing = priv->column_spacing; line_spacing = priv->row_spacing;
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- avail_size = allocation->height;
- avail_other_size = allocation->width;
- item_spacing = priv->row_spacing;
- line_spacing = priv->column_spacing;
- }
-
- item_align = ORIENTATION_ALIGN (box);
- line_align = OPPOSING_ORIENTATION_ALIGN (box);
-
- /* Get how many lines we'll be needing to flow */
- n_children = get_visible_children (box);
- if (n_children <= 0)
- return;
-
- /*
- * Deal with ALIGNED/HOMOGENEOUS modes first, start with
- * initial guesses at item/line sizes
- */
- get_max_item_size (box, priv->orientation, &min_item_size, &nat_item_size);
- if (nat_item_size <= 0)
- return;
-
- /* By default flow at the natural item width */
- line_length = avail_size / (nat_item_size + item_spacing);
-
- /* After the above aproximation, check if we cant fit one more on the line */
- if (line_length * item_spacing + (line_length + 1) * nat_item_size <= avail_size)
- line_length++;
-
- /* Its possible we were allocated just less than the natural width of the
- * minimum item flow length */
- line_length = MAX (min_items, line_length);
- line_length = MIN (line_length, priv->max_children_per_line);
-
- /* Here we just use the largest height-for-width and use that for the height
- * of all lines */
- if (priv->homogeneous)
- {
- n_lines = n_children / line_length;
- if ((n_children % line_length) > 0)
- n_lines++;
-
- n_lines = MAX (n_lines, 1);
-
- /* Now we need the real item allocation size */
- item_size = (avail_size - (line_length - 1) * item_spacing) / line_length;
-
- /* Cut out the expand space if we're not distributing any */
- if (item_align != GTK_ALIGN_FILL)
- item_size = MIN (item_size, nat_item_size);
-
- get_largest_size_for_opposing_orientation (box,
- priv->orientation,
- item_size,
- &min_fixed_line_size,
- &nat_fixed_line_size);
-
- /* resolve a fixed 'line_size' */
- line_size = (avail_other_size - (n_lines - 1) * line_spacing) / n_lines;
-
- if (line_align != GTK_ALIGN_FILL)
- line_size = MIN (line_size, nat_fixed_line_size);
-
- /* Get the real extra pixels incase of GTK_ALIGN_START lines */
- extra_pixels = avail_size - (line_length - 1) * item_spacing - item_size * line_length;
- extra_line_pixels = avail_other_size - (n_lines - 1) * line_spacing - line_size * n_lines;
- }
- else
- {
- gboolean first_line = TRUE;
-
- /* Find the amount of columns that can fit aligned into the available space
- * and collect their requests.
- */
- item_sizes = fit_aligned_item_requests (box,
- priv->orientation,
- avail_size,
- item_spacing,
- &line_length,
- priv->max_children_per_line,
- n_children);
-
- /* Calculate the number of lines after determining the final line_length */
- n_lines = n_children / line_length;
- if ((n_children % line_length) > 0)
- n_lines++;
-
- n_lines = MAX (n_lines, 1);
- line_sizes = g_new0 (GtkRequestedSize, n_lines);
-
- /* Get the available remaining size */
- avail_size -= (line_length - 1) * item_spacing;
- for (i = 0; i < line_length; i++)
- avail_size -= item_sizes[i].minimum_size;
-
- /* Perform a natural allocation on the columnized items and get the remaining pixels */
- if (avail_size > 0)
- extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes);
-
- /* Now that we have the size of each column of items find the size of each individual
- * line based on the aligned item sizes.
- */
-
- for (i = 0, iter = g_sequence_get_begin_iter (priv->children);
- !g_sequence_iter_is_end (iter) && i < n_lines;
- i++)
- {
- iter = get_largest_size_for_line_in_opposing_orientation (box,
- priv->orientation,
- iter,
- line_length,
- item_sizes,
- extra_pixels,
- &line_sizes[i].minimum_size,
- &line_sizes[i].natural_size);
-
-
- /* Its possible a line is made of completely invisible children */
- if (line_sizes[i].natural_size > 0)
- {
- if (first_line)
- first_line = FALSE;
- else
- avail_other_size -= line_spacing;
-
- avail_other_size -= line_sizes[i].minimum_size;
-
- line_sizes[i].data = GINT_TO_POINTER (i);
- }
- }
-
- /* Distribute space among lines naturally */
- if (avail_other_size > 0)
- extra_line_pixels = gtk_distribute_natural_allocation (avail_other_size, n_lines, line_sizes);
- }
-
- /*
- * Initial sizes of items/lines guessed at this point,
- * go on to distribute expand space if needed.
- */
-
- priv->cur_children_per_line = line_length;
-
- /* FIXME: This portion needs to consider which columns
- * and rows asked for expand space and distribute those
- * accordingly for the case of ALIGNED allocation.
- *
- * If at least one child in a column/row asked for expand;
- * we should make that row/column expand entirely.
- */
-
- /* Calculate expand space per item */
- if (item_align == GTK_ALIGN_FILL)
- {
- extra_per_item = extra_pixels / line_length;
- extra_extra = extra_pixels % line_length;
- }
-
- /* Calculate expand space per line */
- if (line_align == GTK_ALIGN_FILL)
- {
- extra_per_line = extra_line_pixels / n_lines;
- extra_line_extra = extra_line_pixels % n_lines;
- }
-
- /*
- * Prepare item/line initial offsets and jump into the
- * real allocation loop.
- */
- line_offset = item_offset = 0;
-
- /* prepend extra space to item_offset/line_offset for SPREAD_END */
- item_offset += get_offset_pixels (item_align, extra_pixels);
- line_offset += get_offset_pixels (line_align, extra_line_pixels);
-
- /* Get the allocation size for the first line */
- if (priv->homogeneous)
- this_line_size = line_size;
- else
- {
- this_line_size = line_sizes[0].minimum_size;
-
- if (line_align == GTK_ALIGN_FILL)
- {
- this_line_size += extra_per_line;
-
- if (extra_line_extra > 0)
- this_line_size++;
- }
- }
-
- i = 0;
- line_count = 0;
- for (iter = g_sequence_get_begin_iter (priv->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint position;
- gint this_item_size;
-
- child = g_sequence_get (iter);
-
- if (!child_is_visible (child))
- continue;
-
- /* Get item position */
- position = i % line_length;
-
- /* adjust the line_offset/count at the beginning of each new line */
- if (i > 0 && position == 0)
- {
- /* Push the line_offset */
- line_offset += this_line_size + line_spacing;
-
- line_count++;
-
- /* Get the new line size */
- if (priv->homogeneous)
- this_line_size = line_size;
- else
- {
- this_line_size = line_sizes[line_count].minimum_size;
-
- if (line_align == GTK_ALIGN_FILL)
- {
- this_line_size += extra_per_line;
-
- if (line_count < extra_line_extra)
- this_line_size++;
- }
- }
-
- item_offset = 0;
-
- if (item_align == GTK_ALIGN_CENTER)
- {
- item_offset += get_offset_pixels (item_align, extra_pixels);
- }
- else if (item_align == GTK_ALIGN_END)
- {
- item_offset += get_offset_pixels (item_align, extra_pixels);
-
- /* If we're on the last line, prepend the space for
- * any leading items */
- if (line_count == n_lines -1)
- {
- gint extra_items = n_children % line_length;
-
- if (priv->homogeneous)
- {
- item_offset += item_size * (line_length - extra_items);
- item_offset += item_spacing * (line_length - extra_items);
- }
- else
- {
- gint j;
-
- for (j = 0; j < (line_length - extra_items); j++)
- {
- item_offset += item_sizes[j].minimum_size;
- item_offset += item_spacing;
- }
- }
- }
- }
- }
-
- /* Push the index along for the last line when spreading to the end */
- if (item_align == GTK_ALIGN_END && line_count == n_lines -1)
- {
- gint extra_items = n_children % line_length;
-
- position += line_length - extra_items;
- }
-
- if (priv->homogeneous)
- this_item_size = item_size;
- else
- this_item_size = item_sizes[position].minimum_size;
-
- if (item_align == GTK_ALIGN_FILL)
- {
- this_item_size += extra_per_item;
-
- if (position < extra_extra)
- this_item_size++;
- }
-
- /* Do the actual allocation */
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- child_allocation.x = item_offset;
- child_allocation.y = line_offset;
- child_allocation.width = this_item_size;
- child_allocation.height = this_line_size;
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- child_allocation.x = line_offset;
- child_allocation.y = item_offset;
- child_allocation.width = this_line_size;
- child_allocation.height = this_item_size;
- }
-
- if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
- child_allocation.x = allocation->x + allocation->width - (child_allocation.x - allocation->x) - child_allocation.width;
- gtk_widget_size_allocate (child, &child_allocation);
-
- item_offset += this_item_size;
- item_offset += item_spacing;
-
- i++;
- }
-
- g_free (item_sizes);
- g_free (line_sizes);
-}
-
-static GtkSizeRequestMode
-gtk_flow_box_get_request_mode (GtkWidget *widget)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
-
- return (BOX_PRIV (box)->orientation == GTK_ORIENTATION_HORIZONTAL) ?
- GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH : GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
-}
-
-/* Gets the largest minimum and natural length of
- * 'line_length' consecutive items when aligned into rows/columns */
-static void
-get_largest_aligned_line_length (GtkFlowBox *box,
- GtkOrientation orientation,
- gint line_length,
- gint *min_size,
- gint *nat_size)
-{
- GSequenceIter *iter;
- gint max_min_size = 0;
- gint max_nat_size = 0;
- gint spacing, i;
- GtkRequestedSize *aligned_item_sizes;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- spacing = BOX_PRIV (box)->column_spacing;
- else
- spacing = BOX_PRIV (box)->row_spacing;
-
- aligned_item_sizes = g_new0 (GtkRequestedSize, line_length);
-
- /* Get the largest sizes of each index in the line.
- */
- i = 0;
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
- gint child_min, child_nat;
-
- child = g_sequence_get (iter);
- if (!child_is_visible (child))
- continue;
-
- if (orientation == GTK_ORIENTATION_HORIZONTAL)
- gtk_widget_get_preferred_width (child,
- &child_min, &child_nat);
- else /* GTK_ORIENTATION_VERTICAL */
- gtk_widget_get_preferred_height (child,
- &child_min, &child_nat);
-
- aligned_item_sizes[i % line_length].minimum_size =
- MAX (aligned_item_sizes[i % line_length].minimum_size, child_min);
-
- aligned_item_sizes[i % line_length].natural_size =
- MAX (aligned_item_sizes[i % line_length].natural_size, child_nat);
-
- i++;
- }
-
- /* Add up the largest indexes */
- for (i = 0; i < line_length; i++)
- {
- max_min_size += aligned_item_sizes[i].minimum_size;
- max_nat_size += aligned_item_sizes[i].natural_size;
- }
-
- g_free (aligned_item_sizes);
-
- max_min_size += (line_length - 1) * spacing;
- max_nat_size += (line_length - 1) * spacing;
-
- if (min_size)
- *min_size = max_min_size;
-
- if (nat_size)
- *nat_size = max_nat_size;
-}
-
-
-static void
-gtk_flow_box_get_preferred_width (GtkWidget *widget,
- gint *minimum_size,
- gint *natural_size)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gint min_item_width, nat_item_width;
- gint min_items, nat_items;
- gint min_width, nat_width;
-
- min_items = MAX (1, priv->min_children_per_line);
- nat_items = MAX (min_items, priv->max_children_per_line);
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- min_width = nat_width = 0;
-
- if (!priv->homogeneous)
- {
- /* When not homogeneous; horizontally oriented boxes
- * need enough width for the widest row */
- if (min_items == 1)
- {
- get_max_item_size (box,
- GTK_ORIENTATION_HORIZONTAL,
- &min_item_width,
- &nat_item_width);
-
- min_width += min_item_width;
- nat_width += nat_item_width;
- }
- else
- {
- gint min_line_length, nat_line_length;
-
- get_largest_aligned_line_length (box,
- GTK_ORIENTATION_HORIZONTAL,
- min_items,
- &min_line_length,
- &nat_line_length);
-
- if (nat_items > min_items)
- get_largest_aligned_line_length (box,
- GTK_ORIENTATION_HORIZONTAL,
- nat_items,
- NULL,
- &nat_line_length);
-
- min_width += min_line_length;
- nat_width += nat_line_length;
- }
- }
- else /* In homogeneous mode; horizontally oriented boxs
- * give the same width to all children */
- {
- get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL,
- &min_item_width, &nat_item_width);
-
- min_width += min_item_width * min_items;
- min_width += (min_items -1) * priv->column_spacing;
-
- nat_width += nat_item_width * nat_items;
- nat_width += (nat_items -1) * priv->column_spacing;
- }
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- /* Return the width for the minimum height */
- gint min_height;
-
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL);
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_width_for_height (widget,
- min_height,
- &min_width,
- &nat_width);
-
- }
-
- if (minimum_size)
- *minimum_size = min_width;
-
- if (natural_size)
- *natural_size = nat_width;
-}
-
-static void
-gtk_flow_box_get_preferred_height (GtkWidget *widget,
- gint *minimum_size,
- gint *natural_size)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gint min_item_height, nat_item_height;
- gint min_items, nat_items;
- gint min_height, nat_height;
-
- min_items = MAX (1, priv->min_children_per_line);
- nat_items = MAX (min_items, priv->max_children_per_line);
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- /* Return the height for the minimum width */
- gint min_width;
-
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL);
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_height_for_width (widget,
- min_width,
- &min_height,
- &nat_height);
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- min_height = nat_height = 0;
-
- if (! priv->homogeneous)
- {
- /* When not homogeneous; vertically oriented boxes
- * need enough height for the tallest column */
- if (min_items == 1)
- {
- get_max_item_size (box, GTK_ORIENTATION_VERTICAL,
- &min_item_height, &nat_item_height);
-
- min_height += min_item_height;
- nat_height += nat_item_height;
- }
- else
- {
- gint min_line_length, nat_line_length;
-
- get_largest_aligned_line_length (box,
- GTK_ORIENTATION_VERTICAL,
- min_items,
- &min_line_length,
- &nat_line_length);
-
- if (nat_items > min_items)
- get_largest_aligned_line_length (box,
- GTK_ORIENTATION_VERTICAL,
- nat_items,
- NULL,
- &nat_line_length);
-
- min_height += min_line_length;
- nat_height += nat_line_length;
- }
-
- }
- else
- {
- /* In homogeneous mode; vertically oriented boxes
- * give the same height to all children
- */
- get_max_item_size (box,
- GTK_ORIENTATION_VERTICAL,
- &min_item_height,
- &nat_item_height);
-
- min_height += min_item_height * min_items;
- min_height += (min_items -1) * priv->row_spacing;
-
- nat_height += nat_item_height * nat_items;
- nat_height += (nat_items -1) * priv->row_spacing;
- }
- }
-
- if (minimum_size)
- *minimum_size = min_height;
-
- if (natural_size)
- *natural_size = nat_height;
-}
-
-static void
-gtk_flow_box_get_preferred_height_for_width (GtkWidget *widget,
- gint width,
- gint *minimum_height,
- gint *natural_height)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gint min_item_width, nat_item_width;
- gint min_items;
- gint min_height, nat_height;
- gint avail_size, n_children;
-
- min_items = MAX (1, priv->min_children_per_line);
-
- min_height = 0;
- nat_height = 0;
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- gint min_width;
- gint line_length;
- gint item_size, extra_pixels;
-
- n_children = get_visible_children (box);
- if (n_children <= 0)
- goto out;
-
- /* Make sure its no smaller than the minimum */
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL);
-
- avail_size = MAX (width, min_width);
- if (avail_size <= 0)
- goto out;
-
- get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, &min_item_width, &nat_item_width);
- if (nat_item_width <= 0)
- goto out;
-
- /* By default flow at the natural item width */
- line_length = avail_size / (nat_item_width + priv->column_spacing);
-
- /* After the above aproximation, check if we cant fit one more on the line */
- if (line_length * priv->column_spacing + (line_length + 1) * nat_item_width <= avail_size)
- line_length++;
-
- /* Its possible we were allocated just less than the natural width of the
- * minimum item flow length
- */
- line_length = MAX (min_items, line_length);
- line_length = MIN (line_length, priv->max_children_per_line);
-
- /* Now we need the real item allocation size */
- item_size = (avail_size - (line_length - 1) * priv->column_spacing) / line_length;
-
- /* Cut out the expand space if we're not distributing any */
- if (gtk_widget_get_halign (widget) != GTK_ALIGN_FILL)
- {
- item_size = MIN (item_size, nat_item_width);
- extra_pixels = 0;
- }
- else
- /* Collect the extra pixels for expand children */
- extra_pixels = (avail_size - (line_length - 1) * priv->column_spacing) % line_length;
-
- if (priv->homogeneous)
- {
- gint min_item_height, nat_item_height;
- gint lines;
-
- /* Here we just use the largest height-for-width and
- * add up the size accordingly
- */
- get_largest_size_for_opposing_orientation (box,
- GTK_ORIENTATION_HORIZONTAL,
- item_size,
- &min_item_height,
- &nat_item_height);
-
- /* Round up how many lines we need to allocate for */
- lines = n_children / line_length;
- if ((n_children % line_length) > 0)
- lines++;
-
- min_height = min_item_height * lines;
- nat_height = nat_item_height * lines;
-
- min_height += (lines - 1) * priv->row_spacing;
- nat_height += (lines - 1) * priv->row_spacing;
- }
- else
- {
- gint min_line_height, nat_line_height, i;
- gboolean first_line = TRUE;
- GtkRequestedSize *item_sizes;
- GSequenceIter *iter;
-
- /* First get the size each set of items take to span the line
- * when aligning the items above and below after flowping.
- */
- item_sizes = fit_aligned_item_requests (box,
- priv->orientation,
- avail_size,
- priv->column_spacing,
- &line_length,
- priv->max_children_per_line,
- n_children);
-
- /* Get the available remaining size */
- avail_size -= (line_length - 1) * priv->column_spacing;
- for (i = 0; i < line_length; i++)
- avail_size -= item_sizes[i].minimum_size;
-
- if (avail_size > 0)
- extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes);
-
- for (iter = g_sequence_get_begin_iter (priv->children);
- !g_sequence_iter_is_end (iter);)
- {
- iter = get_largest_size_for_line_in_opposing_orientation (box,
- GTK_ORIENTATION_HORIZONTAL,
- iter,
- line_length,
- item_sizes,
- extra_pixels,
- &min_line_height,
- &nat_line_height);
- /* Its possible the line only had invisible widgets */
- if (nat_line_height > 0)
- {
- if (first_line)
- first_line = FALSE;
- else
- {
- min_height += priv->row_spacing;
- nat_height += priv->row_spacing;
- }
-
- min_height += min_line_height;
- nat_height += nat_line_height;
- }
- }
-
- g_free (item_sizes);
- }
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- /* Return the minimum height */
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, &nat_height);
- }
-
- out:
-
- if (minimum_height)
- *minimum_height = min_height;
-
- if (natural_height)
- *natural_height = nat_height;
-}
-
-static void
-gtk_flow_box_get_preferred_width_for_height (GtkWidget *widget,
- gint height,
- gint *minimum_width,
- gint *natural_width)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gint min_item_height, nat_item_height;
- gint min_items;
- gint min_width, nat_width;
- gint avail_size, n_children;
-
- min_items = MAX (1, priv->min_children_per_line);
-
- min_width = 0;
- nat_width = 0;
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- {
- /* Return the minimum width */
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, &nat_width);
- }
- else /* GTK_ORIENTATION_VERTICAL */
- {
- gint min_height;
- gint line_length;
- gint item_size, extra_pixels;
-
- n_children = get_visible_children (box);
- if (n_children <= 0)
- goto out;
-
- /* Make sure its no smaller than the minimum */
- GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL);
-
- avail_size = MAX (height, min_height);
- if (avail_size <= 0)
- goto out;
-
- get_max_item_size (box, GTK_ORIENTATION_VERTICAL, &min_item_height, &nat_item_height);
-
- /* By default flow at the natural item width */
- line_length = avail_size / (nat_item_height + priv->row_spacing);
-
- /* After the above aproximation, check if we cant fit one more on the line */
- if (line_length * priv->row_spacing + (line_length + 1) * nat_item_height <= avail_size)
- line_length++;
-
- /* Its possible we were allocated just less than the natural width of the
- * minimum item flow length
- */
- line_length = MAX (min_items, line_length);
- line_length = MIN (line_length, priv->max_children_per_line);
-
- /* Now we need the real item allocation size */
- item_size = (avail_size - (line_length - 1) * priv->row_spacing) / line_length;
-
- /* Cut out the expand space if we're not distributing any */
- if (gtk_widget_get_valign (widget) != GTK_ALIGN_FILL)
- {
- item_size = MIN (item_size, nat_item_height);
- extra_pixels = 0;
- }
- else
- /* Collect the extra pixels for expand children */
- extra_pixels = (avail_size - (line_length - 1) * priv->row_spacing) % line_length;
-
- if (priv->homogeneous)
- {
- gint min_item_width, nat_item_width;
- gint lines;
-
- /* Here we just use the largest height-for-width and
- * add up the size accordingly
- */
- get_largest_size_for_opposing_orientation (box,
- GTK_ORIENTATION_VERTICAL,
- item_size,
- &min_item_width,
- &nat_item_width);
-
- /* Round up how many lines we need to allocate for */
- n_children = get_visible_children (box);
- lines = n_children / line_length;
- if ((n_children % line_length) > 0)
- lines++;
-
- min_width = min_item_width * lines;
- nat_width = nat_item_width * lines;
-
- min_width += (lines - 1) * priv->column_spacing;
- nat_width += (lines - 1) * priv->column_spacing;
- }
- else
- {
- gint min_line_width, nat_line_width, i;
- gboolean first_line = TRUE;
- GtkRequestedSize *item_sizes;
- GSequenceIter *iter;
-
- /* First get the size each set of items take to span the line
- * when aligning the items above and below after flowping.
- */
- item_sizes = fit_aligned_item_requests (box,
- priv->orientation,
- avail_size,
- priv->row_spacing,
- &line_length,
- priv->max_children_per_line,
- n_children);
-
- /* Get the available remaining size */
- avail_size -= (line_length - 1) * priv->column_spacing;
- for (i = 0; i < line_length; i++)
- avail_size -= item_sizes[i].minimum_size;
-
- if (avail_size > 0)
- extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes);
-
- for (iter = g_sequence_get_begin_iter (priv->children);
- !g_sequence_iter_is_end (iter);)
- {
- iter = get_largest_size_for_line_in_opposing_orientation (box,
- GTK_ORIENTATION_VERTICAL,
- iter,
- line_length,
- item_sizes,
- extra_pixels,
- &min_line_width,
- &nat_line_width);
-
- /* Its possible the last line only had invisible widgets */
- if (nat_line_width > 0)
- {
- if (first_line)
- first_line = FALSE;
- else
- {
- min_width += priv->column_spacing;
- nat_width += priv->column_spacing;
- }
-
- min_width += min_line_width;
- nat_width += nat_line_width;
- }
- }
- g_free (item_sizes);
- }
- }
-
- out:
- if (minimum_width)
- *minimum_width = min_width;
-
- if (natural_width)
- *natural_width = nat_width;
-}
-
-/* Drawing {{{3 */
-
-static gboolean
-gtk_flow_box_draw (GtkWidget *widget,
- cairo_t *cr)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkAllocation allocation = { 0, };
- GtkStyleContext* context;
-
- gtk_widget_get_allocation (GTK_WIDGET (box), &allocation);
- context = gtk_widget_get_style_context (GTK_WIDGET (box));
- gtk_render_background (context, cr, 0, 0, allocation.width, allocation.height);
-
- GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->draw (widget, cr);
-
- if (priv->rubberband_first && priv->rubberband_last)
- {
- GSequenceIter *iter, *iter1, *iter2;
- GdkRectangle line_rect, rect;
- GArray *lines;
- gboolean vertical;
-
- vertical = priv->orientation == GTK_ORIENTATION_VERTICAL;
-
- cairo_save (cr);
-
- context = gtk_widget_get_style_context (widget);
- gtk_style_context_save (context);
- gtk_style_context_add_class (context, GTK_STYLE_CLASS_RUBBERBAND);
-
- iter1 = CHILD_PRIV (priv->rubberband_first)->iter;
- iter2 = CHILD_PRIV (priv->rubberband_last)->iter;
-
- if (g_sequence_iter_compare (iter2, iter1) < 0)
- {
- iter = iter1;
- iter1 = iter2;
- iter2 = iter;
- }
-
- line_rect.width = 0;
- lines = g_array_new (FALSE, FALSE, sizeof (GdkRectangle));
-
- for (iter = iter1;
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- GtkWidget *child;
-
- child = g_sequence_get (iter);
- gtk_widget_get_allocation (GTK_WIDGET (child), &rect);
- if (line_rect.width == 0)
- line_rect = rect;
- else
- {
- if ((vertical && rect.x == line_rect.x) ||
- (!vertical && rect.y == line_rect.y))
- gdk_rectangle_union (&rect, &line_rect, &line_rect);
- else
- {
- g_array_append_val (lines, line_rect);
- line_rect = rect;
- }
- }
-
- if (g_sequence_iter_compare (iter, iter2) == 0)
- break;
- }
-
- if (line_rect.width != 0)
- g_array_append_val (lines, line_rect);
-
- if (lines->len > 0)
- {
- GtkStateFlags state;
- cairo_path_t *path;
- GtkBorder border;
- GdkRGBA border_color;
-
- if (vertical)
- path_from_vertical_line_rects (cr, (GdkRectangle *)lines->data, lines->len);
- else
- path_from_horizontal_line_rects (cr, (GdkRectangle *)lines->data, lines->len);
-
- /* For some reason we need to copy and reapply the path,
- * or it gets eaten by gtk_render_background()
- */
- path = cairo_copy_path (cr);
-
- cairo_save (cr);
- cairo_clip (cr);
- gtk_widget_get_allocation (widget, &allocation);
- gtk_render_background (context, cr,
- 0, 0,
- allocation.width, allocation.height);
- cairo_restore (cr);
-
- cairo_append_path (cr, path);
- cairo_path_destroy (path);
-
- state = gtk_widget_get_state_flags (widget);
- gtk_style_context_get_border_color (context, state, &border_color);
- gtk_style_context_get_border (context, state, &border);
-
- cairo_set_line_width (cr, border.left);
- gdk_cairo_set_source_rgba (cr, &border_color);
- cairo_stroke (cr);
- }
- g_array_free (lines, TRUE);
-
- gtk_style_context_restore (context);
- cairo_restore (cr);
- }
-
- return TRUE;
-}
-
-/* Autoscrolling {{{3 */
-
-static void
-remove_autoscroll (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (priv->autoscroll_id)
- {
- gtk_widget_remove_tick_callback (GTK_WIDGET (box), priv->autoscroll_id);
- priv->autoscroll_id = 0;
- }
-
- priv->autoscroll_mode = GTK_SCROLL_NONE;
-}
-
-static gboolean
-autoscroll_cb (GtkWidget *widget,
- GdkFrameClock *frame_clock,
- gpointer data)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (data);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkAdjustment *adjustment;
- gdouble factor;
- gdouble increment;
- gdouble value;
-
- if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
- adjustment = priv->vadjustment;
- else
- adjustment = priv->hadjustment;
-
- switch (priv->autoscroll_mode)
- {
- case GTK_SCROLL_STEP_FORWARD:
- factor = AUTOSCROLL_FACTOR;
- break;
- case GTK_SCROLL_STEP_BACKWARD:
- factor = - AUTOSCROLL_FACTOR;
- break;
- case GTK_SCROLL_PAGE_FORWARD:
- factor = AUTOSCROLL_FACTOR_FAST;
- break;
- case GTK_SCROLL_PAGE_BACKWARD:
- factor = - AUTOSCROLL_FACTOR_FAST;
- break;
- default:
- g_assert_not_reached ();
- }
-
- increment = gtk_adjustment_get_step_increment (adjustment) / factor;
-
- value = gtk_adjustment_get_value (adjustment);
- value += increment;
- gtk_adjustment_set_value (adjustment, value);
-
- if (priv->rubberband_select)
- {
- gint x, y;
- GtkFlowBoxChild *child;
-
- gdk_window_get_device_position (gtk_widget_get_window (widget),
- priv->rubberband_device,
- &x, &y, NULL);
-
- child = gtk_flow_box_find_child_at_pos (box, x, y);
-
- gtk_flow_box_update_prelight (box, child);
- gtk_flow_box_update_active (box, child);
-
- if (child != NULL)
- priv->rubberband_last = child;
- }
-
- return G_SOURCE_CONTINUE;
-}
-
-static void
-add_autoscroll (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (priv->autoscroll_id != 0 ||
- priv->autoscroll_mode == GTK_SCROLL_NONE)
- return;
-
- priv->autoscroll_id = gtk_widget_add_tick_callback (GTK_WIDGET (box),
- (GtkTickCallback)autoscroll_cb,
- box,
- NULL);
-}
-
-static gboolean
-get_view_rect (GtkFlowBox *box,
- GdkRectangle *rect)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkWidget *parent;
- GdkWindow *view;
-
- parent = gtk_widget_get_parent (GTK_WIDGET (box));
- if (GTK_IS_VIEWPORT (parent))
- {
- view = gtk_viewport_get_view_window (GTK_VIEWPORT (parent));
- rect->x = gtk_adjustment_get_value (priv->hadjustment);
- rect->y = gtk_adjustment_get_value (priv->vadjustment);
- rect->width = gdk_window_get_width (view);
- rect->height = gdk_window_get_height (view);
- return TRUE;
- }
-
- return FALSE;
-}
-
-static void
-update_autoscroll_mode (GtkFlowBox *box,
- gint x,
- gint y)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkScrollType mode = GTK_SCROLL_NONE;
- GdkRectangle rect;
- gint size, pos;
-
- if (priv->rubberband_select && get_view_rect (box, &rect))
- {
- if (priv->orientation == GTK_ORIENTATION_VERTICAL)
- {
- size = rect.width;
- pos = x - rect.x;
- }
- else
- {
- size = rect.height;
- pos = y - rect.y;
- }
-
- if (pos < 0 - AUTOSCROLL_FAST_DISTANCE)
- mode = GTK_SCROLL_PAGE_BACKWARD;
- else if (pos > size + AUTOSCROLL_FAST_DISTANCE)
- mode = GTK_SCROLL_PAGE_FORWARD;
- else if (pos < 0)
- mode = GTK_SCROLL_STEP_BACKWARD;
- else if (pos > size)
- mode = GTK_SCROLL_STEP_FORWARD;
- }
-
- if (mode != priv->autoscroll_mode)
- {
- remove_autoscroll (box);
- priv->autoscroll_mode = mode;
- add_autoscroll (box);
- }
-}
-
-/* Event handling {{{3 */
-
-static gboolean
-gtk_flow_box_enter_notify_event (GtkWidget *widget,
- GdkEventCrossing *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxChild *child;
-
- if (event->window != gtk_widget_get_window (GTK_WIDGET (box)))
- return FALSE;
-
- child = gtk_flow_box_find_child_at_pos (box, event->x, event->y);
- gtk_flow_box_update_prelight (box, child);
- gtk_flow_box_update_active (box, child);
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_leave_notify_event (GtkWidget *widget,
- GdkEventCrossing *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxChild *child = NULL;
-
- if (event->window != gtk_widget_get_window (GTK_WIDGET (box)))
- return FALSE;
-
- if (event->detail != GDK_NOTIFY_INFERIOR)
- child = NULL;
- else
- child = gtk_flow_box_find_child_at_pos (box, event->x, event->y);
-
- gtk_flow_box_update_prelight (box, child);
- gtk_flow_box_update_active (box, child);
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_motion_notify_event (GtkWidget *widget,
- GdkEventMotion *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkFlowBoxChild *child;
- GdkWindow *window;
- GdkWindow *event_window;
- gint relative_x;
- gint relative_y;
- gdouble parent_x;
- gdouble parent_y;
-
- window = gtk_widget_get_window (GTK_WIDGET (box));
- event_window = event->window;
- relative_x = event->x;
- relative_y = event->y;
-
- while ((event_window != NULL) && (event_window != window))
- {
- gdk_window_coords_to_parent (event_window,
- relative_x, relative_y,
- &parent_x, &parent_y);
- relative_x = parent_x;
- relative_y = parent_y;
- event_window = gdk_window_get_effective_parent (event_window);
- }
-
- child = gtk_flow_box_find_child_at_pos (box, relative_x, relative_y);
- gtk_flow_box_update_prelight (box, child);
- gtk_flow_box_update_active (box, child);
-
- if (priv->track_motion)
- {
- if (!priv->rubberband_select &&
- (event->x - priv->button_down_x) * (event->x - priv->button_down_x) +
- (event->y - priv->button_down_y) * (event->y - priv->button_down_y) > RUBBERBAND_START_DISTANCE * RUBBERBAND_START_DISTANCE)
- {
- priv->rubberband_select = TRUE;
- priv->rubberband_first = gtk_flow_box_find_child_at_pos (box, priv->button_down_x, priv->button_down_y);
-
- /* Grab focus here, so Escape-to-stop-rubberband works */
- gtk_flow_box_update_cursor (box, priv->rubberband_first);
- }
-
- if (priv->rubberband_select)
- {
- if (priv->rubberband_first == NULL)
- priv->rubberband_first = child;
- if (child != NULL)
- priv->rubberband_last = child;
-
- update_autoscroll_mode (box, event->x, event->y);
- }
- }
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_button_press_event (GtkWidget *widget,
- GdkEventButton *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- GtkFlowBoxChild *child;
-
- if (event->button == GDK_BUTTON_PRIMARY)
- {
- child = gtk_flow_box_find_child_at_pos (box, event->x, event->y);
- if (child != NULL)
- {
- priv->active_child = child;
- priv->active_child_active = TRUE;
- gtk_widget_queue_draw (GTK_WIDGET (box));
- if (event->type == GDK_2BUTTON_PRESS &&
- !priv->activate_on_single_click)
- {
- g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child);
- return TRUE;
- }
- }
-
- if (priv->selection_mode == GTK_SELECTION_MULTIPLE)
- {
- priv->track_motion = TRUE;
- priv->rubberband_select = FALSE;
- priv->rubberband_first = NULL;
- priv->rubberband_last = NULL;
- priv->button_down_x = event->x;
- priv->button_down_y = event->y;
- priv->rubberband_device = gdk_event_get_device ((GdkEvent*)event);
- get_current_selection_modifiers (widget, &priv->rubberband_modify, &priv->rubberband_extend);
- }
- }
-
- return FALSE;
-}
-
-static void
-gtk_flow_box_stop_rubberband (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- priv->rubberband_select = FALSE;
- priv->rubberband_first = NULL;
- priv->rubberband_last = NULL;
- priv->rubberband_device = NULL;
-
- remove_autoscroll (box);
-
- gtk_widget_queue_draw (GTK_WIDGET (box));
-}
-
-static gboolean
-gtk_flow_box_button_release_event (GtkWidget *widget,
- GdkEventButton *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (event->button == GDK_BUTTON_PRIMARY)
- {
- if (priv->active_child != NULL && priv->active_child_active)
- {
- if (priv->activate_on_single_click)
- gtk_flow_box_select_and_activate (box, priv->active_child);
- else
- {
- gboolean modify;
- gboolean extend;
- GdkDevice *device;
-
- get_current_selection_modifiers (widget, &modify, &extend);
-
- /* With touch, we default to modifying the selection.
- * You can still clear the selection and start over
- * by holding Ctrl.
- */
- device = gdk_event_get_source_device ((GdkEvent *)event);
- if (gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN)
- modify = !modify;
-
- gtk_flow_box_update_selection (box, priv->active_child, modify, extend);
- }
- }
-
- priv->active_child = NULL;
- priv->active_child_active = FALSE;
- gtk_widget_queue_draw (GTK_WIDGET (box));
- }
-
- priv->track_motion = FALSE;
- if (priv->rubberband_select)
- {
- if (!priv->rubberband_extend && !priv->rubberband_modify)
- gtk_flow_box_unselect_all_internal (box);
- gtk_flow_box_select_all_between (box, priv->rubberband_first, priv->rubberband_last, priv->rubberband_modify);
-
- gtk_flow_box_stop_rubberband (box);
-
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-
- }
-
- return FALSE;
-}
-
-static gboolean
-gtk_flow_box_key_press_event (GtkWidget *widget,
- GdkEventKey *event)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (priv->rubberband_select)
- {
- if (event->keyval == GDK_KEY_Escape)
- {
- gtk_flow_box_stop_rubberband (box);
- return TRUE;
- }
- }
-
- return GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->key_press_event (widget, event);
-}
-
-static void
-gtk_flow_box_grab_notify (GtkWidget *widget,
- gboolean was_grabbed)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (!was_grabbed)
- {
- if (priv->rubberband_select)
- gtk_flow_box_stop_rubberband (box);
- }
-}
-
-/* Realize and map {{{3 */
-
-static void
-gtk_flow_box_realize (GtkWidget *widget)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkAllocation allocation;
- GdkWindowAttr attributes = {0};
- GdkWindow *window;
-
- gtk_widget_get_allocation (GTK_WIDGET (box), &allocation);
- gtk_widget_set_realized (GTK_WIDGET (box), TRUE);
-
- attributes.x = allocation.x;
- attributes.y = allocation.y;
- attributes.width = allocation.width;
- attributes.height = allocation.height;
- attributes.window_type = GDK_WINDOW_CHILD;
- attributes.event_mask = gtk_widget_get_events (GTK_WIDGET (box))
- | GDK_ENTER_NOTIFY_MASK
- | GDK_LEAVE_NOTIFY_MASK
- | GDK_POINTER_MOTION_MASK
- | GDK_EXPOSURE_MASK
- | GDK_KEY_PRESS_MASK
- | GDK_BUTTON_PRESS_MASK
- | GDK_BUTTON_RELEASE_MASK;
- attributes.wclass = GDK_INPUT_OUTPUT;
-
- window = gdk_window_new (gtk_widget_get_parent_window (GTK_WIDGET (box)),
- &attributes, GDK_WA_X | GDK_WA_Y);
- gtk_widget_register_window (GTK_WIDGET (box), window);
- gtk_widget_set_window (GTK_WIDGET (box), window);
- gtk_style_context_set_background (gtk_widget_get_style_context (GTK_WIDGET (box)), window);
-}
-
-static void
-gtk_flow_box_unmap (GtkWidget *widget)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
-
- remove_autoscroll (box);
-
- GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->unmap (widget);
-}
-
-/* GtkContainer implementation {{{2 */
-
-static void
-gtk_flow_box_add (GtkContainer *container,
- GtkWidget *child)
-{
- gtk_flow_box_insert (GTK_FLOW_BOX (container), child, -1);
-}
-
-static void
-gtk_flow_box_remove (GtkContainer *container,
- GtkWidget *widget)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (container);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gboolean was_visible;
- gboolean was_selected;
- GtkFlowBoxChild *child;
-
- if (GTK_IS_FLOW_BOX_CHILD (widget))
- child = GTK_FLOW_BOX_CHILD (widget);
- else
- {
- child = (GtkFlowBoxChild*)gtk_widget_get_parent (widget);
- if (!GTK_IS_FLOW_BOX_CHILD (child))
- {
- g_warning ("Tried to remove non-child %p\n", widget);
- return;
- }
- }
-
- was_visible = child_is_visible (GTK_WIDGET (child));
- was_selected = CHILD_PRIV (child)->selected;
-
- if (child == priv->prelight_child)
- priv->prelight_child = NULL;
- if (child == priv->active_child)
- priv->active_child = NULL;
- if (child == priv->selected_child)
- priv->selected_child = NULL;
-
- gtk_widget_unparent (GTK_WIDGET (child));
- g_sequence_remove (CHILD_PRIV (child)->iter);
-
- if (was_visible && gtk_widget_get_visible (GTK_WIDGET (box)))
- gtk_widget_queue_resize (GTK_WIDGET (box));
-
- if (was_selected)
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-static void
-gtk_flow_box_forall (GtkContainer *container,
- gboolean include_internals,
- GtkCallback callback,
- gpointer callback_target)
-{
- GSequenceIter *iter;
- GtkWidget *child;
-
- iter = g_sequence_get_begin_iter (BOX_PRIV (container)->children);
- while (!g_sequence_iter_is_end (iter))
- {
- child = g_sequence_get (iter);
- iter = g_sequence_iter_next (iter);
- callback (child, callback_target);
- }
-}
-
-static GType
-gtk_flow_box_child_type (GtkContainer *container)
-{
- return GTK_TYPE_FLOW_BOX_CHILD;
-}
-
-/* Keynav {{{2 */
-
-static gboolean
-gtk_flow_box_focus (GtkWidget *widget,
- GtkDirectionType direction)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (widget);
- GtkWidget *focus_child;
- GSequenceIter *iter;
- GtkFlowBoxChild *next_focus_child;
-
- focus_child = gtk_container_get_focus_child (GTK_CONTAINER (box));
- next_focus_child = NULL;
-
- if (focus_child != NULL)
- {
- if (gtk_widget_child_focus (focus_child, direction))
- return TRUE;
-
- iter = CHILD_PRIV (focus_child)->iter;
-
- if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD)
- iter = gtk_flow_box_get_previous_focusable (box, iter);
- else if (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD)
- iter = gtk_flow_box_get_next_focusable (box, iter);
- else if (direction == GTK_DIR_UP)
- iter = gtk_flow_box_get_above_focusable (box, iter);
- else if (direction == GTK_DIR_DOWN)
- iter = gtk_flow_box_get_below_focusable (box, iter);
-
- if (iter != NULL)
- next_focus_child = g_sequence_get (iter);
- }
- else
- {
- if (BOX_PRIV (box)->selected_child)
- next_focus_child = BOX_PRIV (box)->selected_child;
- else
- {
- if (direction == GTK_DIR_UP || direction == GTK_DIR_TAB_BACKWARD)
- iter = gtk_flow_box_get_last_focusable (box);
- else
- iter = gtk_flow_box_get_first_focusable (box);
-
- if (iter != NULL)
- next_focus_child = g_sequence_get (iter);
- }
- }
-
- if (next_focus_child == NULL)
- {
- if (direction == GTK_DIR_UP || direction == GTK_DIR_DOWN ||
- direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT)
- {
- if (gtk_widget_keynav_failed (GTK_WIDGET (box), direction))
- return TRUE;
- }
-
- return FALSE;
- }
-
- if (gtk_widget_child_focus (GTK_WIDGET (next_focus_child), direction))
- return TRUE;
-
- return TRUE;
-}
-
-static void
-gtk_flow_box_add_move_binding (GtkBindingSet *binding_set,
- guint keyval,
- GdkModifierType modmask,
- GtkMovementStep step,
- gint count)
-{
- GdkDisplay *display;
- GdkModifierType extend_mod_mask = GDK_SHIFT_MASK;
- GdkModifierType modify_mod_mask = GDK_CONTROL_MASK;
-
- display = gdk_display_get_default ();
- if (display)
- {
- extend_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display),
- GDK_MODIFIER_INTENT_EXTEND_SELECTION);
- modify_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display),
- GDK_MODIFIER_INTENT_MODIFY_SELECTION);
- }
-
- gtk_binding_entry_add_signal (binding_set, keyval, modmask,
- "move-cursor", 2,
- GTK_TYPE_MOVEMENT_STEP, step,
- G_TYPE_INT, count,
- NULL);
- gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask,
- "move-cursor", 2,
- GTK_TYPE_MOVEMENT_STEP, step,
- G_TYPE_INT, count,
- NULL);
- gtk_binding_entry_add_signal (binding_set, keyval, modmask | modify_mod_mask,
- "move-cursor", 2,
- GTK_TYPE_MOVEMENT_STEP, step,
- G_TYPE_INT, count,
- NULL);
- gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask | modify_mod_mask,
- "move-cursor", 2,
- GTK_TYPE_MOVEMENT_STEP, step,
- G_TYPE_INT, count,
- NULL);
-}
-
-static void
-gtk_flow_box_activate_cursor_child (GtkFlowBox *box)
-{
- gtk_flow_box_select_and_activate (box, BOX_PRIV (box)->cursor_child);
-}
-
-static void
-gtk_flow_box_toggle_cursor_child (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- if (priv->cursor_child == NULL)
- return;
-
- if ((priv->selection_mode == GTK_SELECTION_SINGLE ||
- priv->selection_mode == GTK_SELECTION_MULTIPLE) &&
- CHILD_PRIV (priv->cursor_child)->selected)
- gtk_flow_box_unselect_child_internal (box, priv->cursor_child);
- else
- gtk_flow_box_select_and_activate (box, priv->cursor_child);
-}
-
-static void
-gtk_flow_box_move_cursor (GtkFlowBox *box,
- GtkMovementStep step,
- gint count)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
- gboolean modify;
- gboolean extend;
- GtkFlowBoxChild *child;
- GtkFlowBoxChild *prev;
- GtkFlowBoxChild *next;
- GtkAllocation allocation;
- gint page_size;
- GSequenceIter *iter;
- gint start;
- GtkAdjustment *adjustment;
- gboolean vertical;
-
- vertical = priv->orientation == GTK_ORIENTATION_VERTICAL;
-
- if (vertical)
- {
- switch (step)
- {
- case GTK_MOVEMENT_VISUAL_POSITIONS:
- step = GTK_MOVEMENT_DISPLAY_LINES;
- break;
- case GTK_MOVEMENT_DISPLAY_LINES:
- step = GTK_MOVEMENT_VISUAL_POSITIONS;
- break;
- default: ;
- }
- }
-
- child = NULL;
- switch (step)
- {
- case GTK_MOVEMENT_VISUAL_POSITIONS:
- if (priv->cursor_child != NULL)
- {
- iter = CHILD_PRIV (priv->cursor_child)->iter;
- if (gtk_widget_get_direction (GTK_WIDGET (box)) == GTK_TEXT_DIR_RTL)
- count = - count;
-
- while (count < 0 && iter != NULL)
- {
- iter = gtk_flow_box_get_previous_focusable (box, iter);
- count = count + 1;
- }
- while (count > 0 && iter != NULL)
- {
- iter = gtk_flow_box_get_next_focusable (box, iter);
- count = count - 1;
- }
-
- if (iter != NULL && !g_sequence_iter_is_end (iter))
- child = g_sequence_get (iter);
- }
- break;
-
- case GTK_MOVEMENT_BUFFER_ENDS:
- if (count < 0)
- iter = gtk_flow_box_get_first_focusable (box);
- else
- iter = gtk_flow_box_get_last_focusable (box);
- if (iter != NULL)
- child = g_sequence_get (iter);
- break;
-
- case GTK_MOVEMENT_DISPLAY_LINES:
- if (priv->cursor_child != NULL)
- {
- iter = CHILD_PRIV (priv->cursor_child)->iter;
-
- while (count < 0 && iter != NULL)
- {
- iter = gtk_flow_box_get_above_focusable (box, iter);
- count = count + 1;
- }
- while (count > 0 && iter != NULL)
- {
- iter = gtk_flow_box_get_below_focusable (box, iter);
- count = count - 1;
- }
-
- if (iter != NULL)
- child = g_sequence_get (iter);
- }
- break;
-
- case GTK_MOVEMENT_PAGES:
- page_size = 100;
- adjustment = vertical ? priv->hadjustment : priv->vadjustment;
- if (adjustment)
- page_size = gtk_adjustment_get_page_increment (adjustment);
-
- if (priv->cursor_child != NULL)
- {
- child = priv->cursor_child;
- iter = CHILD_PRIV (child)->iter;
- gtk_widget_get_allocation (GTK_WIDGET (child), &allocation);
- start = vertical ? allocation.x : allocation.y;
-
- if (count < 0)
- {
- gint i = 0;
-
- /* Up */
- while (iter != NULL)
- {
- iter = gtk_flow_box_get_previous_focusable (box, iter);
- if (iter == NULL)
- break;
-
- prev = g_sequence_get (iter);
-
- /* go up an even number of rows */
- if (i % priv->cur_children_per_line == 0)
- {
- gtk_widget_get_allocation (GTK_WIDGET (prev), &allocation);
- if ((vertical ? allocation.x : allocation.y) < start - page_size)
- break;
- }
-
- child = prev;
- i++;
- }
- }
- else
- {
- gint i = 0;
-
- /* Down */
- while (!g_sequence_iter_is_end (iter))
- {
- iter = gtk_flow_box_get_next_focusable (box, iter);
- if (g_sequence_iter_is_end (iter))
- break;
-
- next = g_sequence_get (iter);
-
- if (i % priv->cur_children_per_line == 0)
- {
- gtk_widget_get_allocation (GTK_WIDGET (next), &allocation);
- if ((vertical ? allocation.x : allocation.y) > start + page_size)
- break;
- }
-
- child = next;
- i++;
- }
- }
- gtk_widget_get_allocation (GTK_WIDGET (child), &allocation);
- }
- break;
-
- default:
- g_assert_not_reached ();
- }
-
- if (child == NULL || child == priv->cursor_child)
- {
- GtkDirectionType direction = count < 0 ? GTK_DIR_UP : GTK_DIR_DOWN;
-
- if (!gtk_widget_keynav_failed (GTK_WIDGET (box), direction))
- {
- GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (box));
-
- if (toplevel)
- gtk_widget_child_focus (toplevel,
- direction == GTK_DIR_UP ?
- GTK_DIR_TAB_BACKWARD :
- GTK_DIR_TAB_FORWARD);
-
- }
-
- return;
- }
-
- get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend);
-
- gtk_flow_box_update_cursor (box, child);
- if (!modify)
- gtk_flow_box_update_selection (box, child, FALSE, extend);
-}
-
-/* Selection {{{2 */
-
-static void
-gtk_flow_box_selected_children_changed (GtkFlowBox *box)
-{
-}
-
-/* GObject implementation {{{2 */
-
-static void
-gtk_flow_box_get_property (GObject *object,
- guint prop_id,
- GValue *value,
- GParamSpec *pspec)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (object);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- switch (prop_id)
- {
- case PROP_ORIENTATION:
- g_value_set_enum (value, priv->orientation);
- break;
- case PROP_HOMOGENEOUS:
- g_value_set_boolean (value, priv->homogeneous);
- break;
- case PROP_COLUMN_SPACING:
- g_value_set_uint (value, priv->column_spacing);
- break;
- case PROP_ROW_SPACING:
- g_value_set_uint (value, priv->row_spacing);
- break;
- case PROP_MIN_CHILDREN_PER_LINE:
- g_value_set_uint (value, priv->min_children_per_line);
- break;
- case PROP_MAX_CHILDREN_PER_LINE:
- g_value_set_uint (value, priv->max_children_per_line);
- break;
- case PROP_SELECTION_MODE:
- g_value_set_enum (value, priv->selection_mode);
- break;
- case PROP_ACTIVATE_ON_SINGLE_CLICK:
- g_value_set_boolean (value, priv->activate_on_single_click);
- break;
- default:
- G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
- break;
- }
-}
-
-static void
-gtk_flow_box_set_property (GObject *object,
- guint prop_id,
- const GValue *value,
- GParamSpec *pspec)
-{
- GtkFlowBox *box = GTK_FLOW_BOX (object);
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- switch (prop_id)
- {
- case PROP_ORIENTATION:
- priv->orientation = g_value_get_enum (value);
- /* Re-box the children in the new orientation */
- gtk_widget_queue_resize (GTK_WIDGET (box));
- break;
- case PROP_HOMOGENEOUS:
- gtk_flow_box_set_homogeneous (box, g_value_get_boolean (value));
- break;
- case PROP_COLUMN_SPACING:
- gtk_flow_box_set_column_spacing (box, g_value_get_uint (value));
- break;
- case PROP_ROW_SPACING:
- gtk_flow_box_set_row_spacing (box, g_value_get_uint (value));
- break;
- case PROP_MIN_CHILDREN_PER_LINE:
- gtk_flow_box_set_min_children_per_line (box, g_value_get_uint (value));
- break;
- case PROP_MAX_CHILDREN_PER_LINE:
- gtk_flow_box_set_max_children_per_line (box, g_value_get_uint (value));
- break;
- case PROP_SELECTION_MODE:
- gtk_flow_box_set_selection_mode (box, g_value_get_enum (value));
- break;
- case PROP_ACTIVATE_ON_SINGLE_CLICK:
- gtk_flow_box_set_activate_on_single_click (box, g_value_get_boolean (value));
- break;
- default:
- G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
- break;
- }
-}
-
-static void
-gtk_flow_box_finalize (GObject *obj)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (obj);
-
- if (priv->filter_destroy != NULL)
- priv->filter_destroy (priv->filter_data);
- if (priv->sort_destroy != NULL)
- priv->sort_destroy (priv->sort_data);
-
- g_sequence_free (priv->children);
- g_clear_object (&priv->hadjustment);
- g_clear_object (&priv->vadjustment);
-
- G_OBJECT_CLASS (gtk_flow_box_parent_class)->finalize (obj);
-}
-
-static void
-gtk_flow_box_class_init (GtkFlowBoxClass *class)
-{
- GObjectClass *object_class = G_OBJECT_CLASS (class);
- GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
- GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class);
- GtkBindingSet *binding_set;
-
- object_class->finalize = gtk_flow_box_finalize;
- object_class->get_property = gtk_flow_box_get_property;
- object_class->set_property = gtk_flow_box_set_property;
-
- widget_class->enter_notify_event = gtk_flow_box_enter_notify_event;
- widget_class->leave_notify_event = gtk_flow_box_leave_notify_event;
- widget_class->motion_notify_event = gtk_flow_box_motion_notify_event;
- widget_class->size_allocate = gtk_flow_box_size_allocate;
- widget_class->realize = gtk_flow_box_realize;
- widget_class->unmap = gtk_flow_box_unmap;
- widget_class->focus = gtk_flow_box_focus;
- widget_class->draw = gtk_flow_box_draw;
- widget_class->button_press_event = gtk_flow_box_button_press_event;
- widget_class->button_release_event = gtk_flow_box_button_release_event;
- widget_class->key_press_event = gtk_flow_box_key_press_event;
- widget_class->grab_notify = gtk_flow_box_grab_notify;
- widget_class->get_request_mode = gtk_flow_box_get_request_mode;
- widget_class->get_preferred_width = gtk_flow_box_get_preferred_width;
- widget_class->get_preferred_height = gtk_flow_box_get_preferred_height;
- widget_class->get_preferred_height_for_width = gtk_flow_box_get_preferred_height_for_width;
- widget_class->get_preferred_width_for_height = gtk_flow_box_get_preferred_width_for_height;
-
- container_class->add = gtk_flow_box_add;
- container_class->remove = gtk_flow_box_remove;
- container_class->forall = gtk_flow_box_forall;
- container_class->child_type = gtk_flow_box_child_type;
- gtk_container_class_handle_border_width (container_class);
-
- class->activate_cursor_child = gtk_flow_box_activate_cursor_child;
- class->toggle_cursor_child = gtk_flow_box_toggle_cursor_child;
- class->move_cursor = gtk_flow_box_move_cursor;
- class->select_all = gtk_flow_box_select_all;
- class->unselect_all = gtk_flow_box_unselect_all;
- class->selected_children_changed = gtk_flow_box_selected_children_changed;
-
- g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation");
-
- /**
- * GtkFlowBox:selection-mode:
- *
- * The selection mode used by the flow box.
- */
- g_object_class_install_property (object_class,
- PROP_SELECTION_MODE,
- g_param_spec_enum ("selection-mode",
- "Selection mode",
- "The selection mode",
- GTK_TYPE_SELECTION_MODE,
- GTK_SELECTION_SINGLE,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:activate-on-single-click:
- *
- * Determines whether children can be activated with a single
- * click, or require a double-click.
- */
- g_object_class_install_property (object_class,
- PROP_ACTIVATE_ON_SINGLE_CLICK,
- g_param_spec_boolean ("activate-on-single-click",
- "Activate on Single Click",
- "Activate row on a single click",
- TRUE,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:homogeneous:
- *
- * Determines whether all children should be allocated the
- * same size.
- */
- g_object_class_install_property (object_class,
- PROP_HOMOGENEOUS,
- g_param_spec_boolean ("homogeneous",
- "Homogeneous",
- "Whether the children should all be the same size",
- FALSE,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:min-children-per-line:
- *
- * The minimum number of children to allocate consecutively
- * in the given orientation.
- *
- * Setting the minimum children per line ensures
- * that a reasonably small height will be requested
- * for the overall minimum width of the box.
- */
- g_object_class_install_property (object_class,
- PROP_MIN_CHILDREN_PER_LINE,
- g_param_spec_uint ("min-children-per-line",
- "Minimum Children Per Line",
- "The minimum number of children to allocate "
- "consecutively in the given orientation.",
- 0,
- G_MAXUINT,
- 0,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:max-children-per-line:
- *
- * The maximum amount of children to request space for consecutively
- * in the given orientation.
- */
- g_object_class_install_property (object_class,
- PROP_MAX_CHILDREN_PER_LINE,
- g_param_spec_uint ("max-children-per-line",
- "Maximum Children Per Line",
- "The maximum amount of children to request space for "
- "consecutively in the given orientation.",
- 0,
- G_MAXUINT,
- DEFAULT_MAX_CHILDREN_PER_LINE,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:row-spacing:
- *
- * The amount of vertical space between two children.
- */
- g_object_class_install_property (object_class,
- PROP_ROW_SPACING,
- g_param_spec_uint ("row-spacing",
- "Vertical spacing",
- "The amount of vertical space between two children",
- 0,
- G_MAXUINT,
- 0,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox:column-spacing:
- *
- * The amount of horizontal space between two children.
- */
- g_object_class_install_property (object_class,
- PROP_COLUMN_SPACING,
- g_param_spec_uint ("column-spacing",
- "Horizontal spacing",
- "The amount of horizontal space between two children",
- 0,
- G_MAXUINT,
- 0,
- G_PARAM_READWRITE));
-
- /**
- * GtkFlowBox::child-activated:
- * @box: the #GtkFlowBox on which the signal is emitted
- * @child: the child that is activated
- *
- * The ::child-activated signal is emitted when a child has been
- * activated by the user.
- */
- signals[CHILD_ACTIVATED] = g_signal_new ("child-activated",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST,
- G_STRUCT_OFFSET (GtkFlowBoxClass, child_activated),
- NULL, NULL,
- g_cclosure_marshal_VOID__OBJECT,
- G_TYPE_NONE, 1,
- GTK_TYPE_FLOW_BOX_CHILD);
-
- /**
- * GtkFlowBox::selected-children-changed:
- * @box: the #GtkFlowBox on wich the signal is emitted
- *
- * The ::selected-children-changed signal is emitted when the
- * set of selected children changes.
- *
- * Use gtk_flow_box_selected_foreach() or
- * gtk_flow_box_get_selected_children() to obtain the
- * selected children.
- */
- signals[SELECTED_CHILDREN_CHANGED] = g_signal_new ("selected-children-changed",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_FIRST,
- G_STRUCT_OFFSET (GtkFlowBoxClass, selected_children_changed),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- /**
- * GtkFlowBox::activate-cursor-child:
- * @box: the #GtkFlowBox on which the signal is emitted
- *
- * The ::activate-cursor-child signal is a
- * [keybinding signal][GtkBindingSignal]
- * which gets emitted when the user activates the @box.
- */
- signals[ACTIVATE_CURSOR_CHILD] = g_signal_new ("activate-cursor-child",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, activate_cursor_child),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- /**
- * GtkFlowBox::toggle-cursor-child:
- * @box: the #GtkFlowBox on which the signal is emitted
- *
- * The ::toggle-cursor-child signal is a
- * [keybinding signal][GtkBindingSignal]
- * which toggles the selection of the child that has the focus.
- *
- * The default binding for this signal is Ctrl-Space.
- */
- signals[TOGGLE_CURSOR_CHILD] = g_signal_new ("toggle-cursor-child",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, toggle_cursor_child),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- /**
- * GtkFlowBox::move-cursor:
- * @box: the #GtkFlowBox on which the signal is emitted
- * @step: the granularity fo the move, as a #GtkMovementStep
- * @count: the number of @step units to move
- *
- * The ::move-cursor signal is a
- * [keybinding signal][GtkBindingSignal]
- * which gets emitted when the user initiates a cursor movement.
- * If the cursor is not visible in @text_view, this signal causes
- * the viewport to be moved instead.
- *
- * Applications should not connect to it, but may emit it with
- * g_signal_emit_by_name() if they need to control the cursor
- * programmatically.
- *
- * The default bindings for this signal come in two variants,
- * the variant with the Shift modifier extends the selection,
- * the variant without the Shift modifer does not.
- * There are too many key combinations to list them all here.
- * - Arrow keys move by individual children
- * - Home/End keys move to the ends of the box
- * - PageUp/PageDown keys move vertically by pages
- */
- signals[MOVE_CURSOR] = g_signal_new ("move-cursor",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, move_cursor),
- NULL, NULL,
- NULL,
- G_TYPE_NONE, 2,
- GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT);
- /**
- * GtkFlowBox::select-all:
- * @box: the #GtkFlowBox on which the signal is emitted
- *
- * The ::select-all signal is a
- * [keybinding signal][GtkBindingSignal]
- * which gets emitted to select all children of the box, if
- * the selection mode permits it.
- *
- * The default bindings for this signal is Ctrl-a.
- */
- signals[SELECT_ALL] = g_signal_new ("select-all",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, select_all),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- /**
- * GtkFlowBox::unselect-all:
- * @box: the #GtkFlowBox on which the signal is emitted
- *
- * The ::unselect-all signal is a
- * [keybinding signal][GtkBindingSignal]
- * which gets emitted to unselect all children of the box, if
- * the selection mode permits it.
- *
- * The default bindings for this signal is Ctrl-Shift-a.
- */
- signals[UNSELECT_ALL] = g_signal_new ("unselect-all",
- GTK_TYPE_FLOW_BOX,
- G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
- G_STRUCT_OFFSET (GtkFlowBoxClass, unselect_all),
- NULL, NULL,
- g_cclosure_marshal_VOID__VOID,
- G_TYPE_NONE, 0);
-
- widget_class->activate_signal = signals[ACTIVATE_CURSOR_CHILD];
-
- binding_set = gtk_binding_set_by_class (class);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Home, 0,
- GTK_MOVEMENT_BUFFER_ENDS, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Home, 0,
- GTK_MOVEMENT_BUFFER_ENDS, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_End, 0,
- GTK_MOVEMENT_BUFFER_ENDS, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_End, 0,
- GTK_MOVEMENT_BUFFER_ENDS, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Up, 0,
- GTK_MOVEMENT_DISPLAY_LINES, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Up, 0,
- GTK_MOVEMENT_DISPLAY_LINES, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Down, 0,
- GTK_MOVEMENT_DISPLAY_LINES, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Down, 0,
- GTK_MOVEMENT_DISPLAY_LINES, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Up, 0,
- GTK_MOVEMENT_PAGES, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Up, 0,
- GTK_MOVEMENT_PAGES, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Down, 0,
- GTK_MOVEMENT_PAGES, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Down, 0,
- GTK_MOVEMENT_PAGES, 1);
-
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Right, 0,
- GTK_MOVEMENT_VISUAL_POSITIONS, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Right, 0,
- GTK_MOVEMENT_VISUAL_POSITIONS, 1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Left, 0,
- GTK_MOVEMENT_VISUAL_POSITIONS, -1);
- gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Left, 0,
- GTK_MOVEMENT_VISUAL_POSITIONS, -1);
-
- gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, GDK_CONTROL_MASK,
- "toggle-cursor-child", 0, NULL);
- gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Space, GDK_CONTROL_MASK,
- "toggle-cursor-child", 0, NULL);
-
- gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK,
- "select-all", 0);
- gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
- "unselect-all", 0);
-}
-
-static void
-gtk_flow_box_init (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- gtk_widget_set_has_window (GTK_WIDGET (box), TRUE);
- gtk_widget_set_redraw_on_allocate (GTK_WIDGET (box), TRUE);
-
- priv->orientation = GTK_ORIENTATION_HORIZONTAL;
- priv->selection_mode = GTK_SELECTION_SINGLE;
- priv->max_children_per_line = DEFAULT_MAX_CHILDREN_PER_LINE;
- priv->column_spacing = 0;
- priv->row_spacing = 0;
- priv->activate_on_single_click = TRUE;
-
- priv->children = g_sequence_new (NULL);
-}
-
- /* Public API {{{2 */
-
-/**
- * gtk_flow_box_new:
- *
- * Creates a GtkFlowBox.
- *
- * Returns: a new #GtkFlowBox container
- *
- * Since: 3.12
- */
-GtkWidget *
-gtk_flow_box_new (void)
-{
- return (GtkWidget *)g_object_new (GTK_TYPE_FLOW_BOX, NULL);
-}
-
-/**
- * gtk_flow_box_insert:
- * @box: a #GtkFlowBox
- * @widget: the #GtkWidget to add
- * @position: the position to insert @child in
- *
- * Inserts the @widget into @box at @position.
- *
- * If a sort function is set, the widget will actually be inserted
- * at the calculated position and this function has the same effect
- * as gtk_container_add().
- *
- * If @position is -1, or larger than the total number of children
- * in the @box, then the @widget will be appended to the end.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_insert (GtkFlowBox *box,
- GtkWidget *widget,
- gint position)
-{
- GtkFlowBoxPrivate *priv;
- GtkFlowBoxChild *child;
- GSequenceIter *iter;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_WIDGET (widget));
-
- priv = BOX_PRIV (box);
-
- if (GTK_IS_FLOW_BOX_CHILD (widget))
- child = GTK_FLOW_BOX_CHILD (widget);
- else
- {
- child = GTK_FLOW_BOX_CHILD (gtk_flow_box_child_new ());
- gtk_widget_show (GTK_WIDGET (child));
- gtk_container_add (GTK_CONTAINER (child), widget);
- }
-
- if (priv->sort_func != NULL)
- iter = g_sequence_insert_sorted (priv->children, child,
- (GCompareDataFunc)gtk_flow_box_sort, box);
- else if (position == 0)
- iter = g_sequence_prepend (priv->children, child);
- else if (position == -1)
- iter = g_sequence_append (priv->children, child);
- else
- {
- GSequenceIter *pos;
- pos = g_sequence_get_iter_at_pos (priv->children, position);
- iter = g_sequence_insert_before (pos, child);
- }
-
- CHILD_PRIV (child)->iter = iter;
- gtk_widget_set_parent (GTK_WIDGET (child), GTK_WIDGET (box));
- gtk_flow_box_apply_filter (box, child);
-}
-
-/**
- * gtk_flow_box_get_child_at_index:
- * @box: a #GtkFlowBox
- * @idx: the position of the child
- *
- * Gets the nth child in the @box.
- *
- * Returns: (transfer none): the child widget, which will
- * always be a #GtkFlowBoxChild
- *
- * Since: 3.12
- */
-GtkFlowBoxChild *
-gtk_flow_box_get_child_at_index (GtkFlowBox *box,
- gint idx)
-{
- GSequenceIter *iter;
-
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL);
-
- iter = g_sequence_get_iter_at_pos (BOX_PRIV (box)->children, idx);
- if (iter)
- return g_sequence_get (iter);
-
- return NULL;
-}
-
-/**
- * gtk_flow_box_set_hadjustment:
- * @box: a #GtkFlowBox
- * @adjustment: an adjustment which should be adjusted
- * when the focus is moved among the descendents of @container
- *
- * Hooks up an adjustment to focus handling in @box.
- * The adjustment is also used for autoscrolling during
- * rubberband selection. See gtk_scrolled_window_get_hadjustment()
- * for a typical way of obtaining the adjustment, and
- * gtk_flow_box_set_vadjustment()for setting the vertical
- * adjustment.
- *
- * The adjustments have to be in pixel units and in the same
- * coordinate system as the allocation for immediate children
- * of the box.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_hadjustment (GtkFlowBox *box,
- GtkAdjustment *adjustment)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment));
-
- priv = BOX_PRIV (box);
-
- g_object_ref (adjustment);
- if (priv->hadjustment)
- g_object_unref (priv->hadjustment);
- priv->hadjustment = adjustment;
- gtk_container_set_focus_hadjustment (GTK_CONTAINER (box), adjustment);
-}
-
-/**
- * gtk_flow_box_set_vadjustment:
- * @box: a #GtkFlowBox
- * @adjustment: an adjustment which should be adjusted
- * when the focus is moved among the descendents of @container
- *
- * Hooks up an adjustment to focus handling in @box.
- * The adjustment is also used for autoscrolling during
- * rubberband selection. See gtk_scrolled_window_get_vadjustment()
- * for a typical way of obtaining the adjustment, and
- * gtk_flow_box_set_hadjustment()for setting the horizontal
- * adjustment.
- *
- * The adjustments have to be in pixel units and in the same
- * coordinate system as the allocation for immediate children
- * of the box.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_vadjustment (GtkFlowBox *box,
- GtkAdjustment *adjustment)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment));
-
- priv = BOX_PRIV (box);
-
- g_object_ref (adjustment);
- if (priv->vadjustment)
- g_object_unref (priv->vadjustment);
- priv->vadjustment = adjustment;
- gtk_container_set_focus_vadjustment (GTK_CONTAINER (box), adjustment);
-}
-
-/* Setters and getters {{{2 */
-
-/**
- * gtk_flow_box_get_homogeneous:
- * @box: a #GtkFlowBox
- *
- * Returns whether the box is homogeneous (all children are the
- * same size). See gtk_box_set_homogeneous().
- *
- * Returns: %TRUE if the box is homogeneous.
- *
- * Since: 3.12
- */
-gboolean
-gtk_flow_box_get_homogeneous (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->homogeneous;
-}
-
-/**
- * gtk_flow_box_set_homogeneous:
- * @box: a #GtkFlowBox
- * @homogeneous: %TRUE to create equal allotments,
- * %FALSE for variable allotments
- *
- * Sets the #GtkFlowBox:homogeneous property of @box, controlling
- * whether or not all children of @box are given equal space
- * in the box.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_homogeneous (GtkFlowBox *box,
- gboolean homogeneous)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- homogeneous = homogeneous != FALSE;
-
- if (BOX_PRIV (box)->homogeneous != homogeneous)
- {
- BOX_PRIV (box)->homogeneous = homogeneous;
-
- g_object_notify (G_OBJECT (box), "homogeneous");
- gtk_widget_queue_resize (GTK_WIDGET (box));
- }
-}
-
-/**
- * gtk_flow_box_set_row_spacing:
- * @box: a #GtkFlowBox
- * @spacing: the spacing to use
- *
- * Sets the vertical space to add between children.
- * See the #GtkFlowBox:row-spacing property.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_row_spacing (GtkFlowBox *box,
- guint spacing)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->row_spacing != spacing)
- {
- BOX_PRIV (box)->row_spacing = spacing;
-
- gtk_widget_queue_resize (GTK_WIDGET (box));
- g_object_notify (G_OBJECT (box), "row-spacing");
- }
-}
-
-/**
- * gtk_flow_box_get_row_spacing:
- * @box: a #GtkFlowBox
- *
- * Gets the vertical spacing.
- *
- * Returns: the vertical spacing
- *
- * Since: 3.12
- */
-guint
-gtk_flow_box_get_row_spacing (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->row_spacing;
-}
-
-/**
- * gtk_flow_box_set_column_spacing:
- * @box: a #GtkFlowBox
- * @spacing: the spacing to use
- *
- * Sets the horizontal space to add between children.
- * See the #GtkFlowBox:column-spacing property.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_column_spacing (GtkFlowBox *box,
- guint spacing)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->column_spacing != spacing)
- {
- BOX_PRIV (box)->column_spacing = spacing;
-
- gtk_widget_queue_resize (GTK_WIDGET (box));
- g_object_notify (G_OBJECT (box), "column-spacing");
- }
-}
-
-/**
- * gtk_flow_box_get_column_spacing:
- * @box: a #GtkFlowBox
- *
- * Gets the horizontal spacing.
- *
- * Returns: the horizontal spacing
- *
- * Since: 3.12
- */
-guint
-gtk_flow_box_get_column_spacing (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->column_spacing;
-}
-
-/**
- * gtk_flow_box_set_min_children_per_line:
- * @box: a #GtkFlowBox
- * @n_children: the minimum number of children per line
- *
- * Sets the minimum number of children to line up
- * in @box’s orientation before flowing.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_min_children_per_line (GtkFlowBox *box,
- guint n_children)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->min_children_per_line != n_children)
- {
- BOX_PRIV (box)->min_children_per_line = n_children;
-
- gtk_widget_queue_resize (GTK_WIDGET (box));
- g_object_notify (G_OBJECT (box), "min-children-per-line");
- }
-}
-
-/**
- * gtk_flow_box_get_min_children_per_line:
- * @box: a #GtkFlowBox
- *
- * Gets the minimum number of children per line.
- *
- * Returns: the minimum number of children per line
- *
- * Since: 3.12
- */
-guint
-gtk_flow_box_get_min_children_per_line (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->min_children_per_line;
-}
-
-/**
- * gtk_flow_box_set_max_children_per_line:
- * @box: a #GtkFlowBox
- * @n_children: the maximum number of children per line
- *
- * Sets the maximum number of children to request and
- * allocate space for in @box’s orientation.
- *
- * Setting the maximum number of children per line
- * limits the overall natural size request to be no more
- * than @n_children children long in the given orientation.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_max_children_per_line (GtkFlowBox *box,
- guint n_children)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->max_children_per_line != n_children)
- {
- BOX_PRIV (box)->max_children_per_line = n_children;
-
- gtk_widget_queue_resize (GTK_WIDGET (box));
- g_object_notify (G_OBJECT (box), "max-children-per-line");
- }
-}
-
-/**
- * gtk_flow_box_get_max_children_per_line:
- * @box: a #GtkFlowBox
- *
- * Gets the maximum number of children per line.
- *
- * Returns: the maximum number of children per line
- *
- * Since: 3.12
- */
-guint
-gtk_flow_box_get_max_children_per_line (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->max_children_per_line;
-}
-
-/**
- * gtk_flow_box_set_activate_on_single_click:
- * @box: a #GtkFlowBox
- * @single: %TRUE to emit child-activated on a single click
- *
- * If @single is %TRUE, children will be activated when you click
- * on them, otherwise you need to double-click.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box,
- gboolean single)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- single = single != FALSE;
-
- if (BOX_PRIV (box)->activate_on_single_click != single)
- {
- BOX_PRIV (box)->activate_on_single_click = single;
- g_object_notify (G_OBJECT (box), "activate-on-single-click");
- }
-}
-
-/**
- * gtk_flow_box_get_activate_on_single_click:
- * @box: a #GtkFlowBox
- *
- * Returns whether children activate on single clicks.
- *
- * Returns: %TRUE if children are activated on single click,
- * %FALSE otherwise
- *
- * Since: 3.12
- */
-gboolean
-gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE);
-
- return BOX_PRIV (box)->activate_on_single_click;
-}
-
- /* Selection handling {{{2 */
-
-/**
- * gtk_flow_box_get_selected_children:
- * @box: a #GtkFlowBox
- *
- * Creates a list of all selected children.
- *
- * Returns: (element-type GtkFlowBoxChild) (transfer container):
- * A #GList containing the #GtkWidget for each selected child.
- * Free with g_list_free() when done.
- *
- * Since: 3.12
- */
-GList *
-gtk_flow_box_get_selected_children (GtkFlowBox *box)
-{
- GtkFlowBoxChild *child;
- GSequenceIter *iter;
- GList *selected = NULL;
-
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL);
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- child = g_sequence_get (iter);
- if (CHILD_PRIV (child)->selected)
- selected = g_list_prepend (selected, child);
- }
-
- return g_list_reverse (selected);
-}
-
-/**
- * gtk_flow_box_select_child:
- * @box: a #GtkFlowBox
- * @child: a child of @box
- *
- * Selects a single child of @box, if the selection
- * mode allows it.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_select_child (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child));
-
- gtk_flow_box_select_child_internal (box, child);
-}
-
-/**
- * gtk_flow_box_unselect_child:
- * @box: a #GtkFlowBox
- * @child: a child of @box
- *
- * Unselects a single child of @box, if the selection
- * mode allows it.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_unselect_child (GtkFlowBox *box,
- GtkFlowBoxChild *child)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
- g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child));
-
- gtk_flow_box_unselect_child_internal (box, child);
-}
-
-/**
- * gtk_flow_box_select_all:
- * @box: a #GtkFlowBox
- *
- * Select all children of @box, if the selection
- * mode allows it.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_select_all (GtkFlowBox *box)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE)
- return;
-
- if (g_sequence_get_length (BOX_PRIV (box)->children) > 0)
- {
- gtk_flow_box_select_all_between (box, NULL, NULL, FALSE);
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
- }
-}
-
-/**
- * gtk_flow_box_unselect_all:
- * @box: a #GtkFlowBox
- *
- * Unselect all children of @box, if the selection
- * mode allows it.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_unselect_all (GtkFlowBox *box)
-{
- gboolean dirty = FALSE;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_BROWSE)
- return;
-
- dirty = gtk_flow_box_unselect_all_internal (box);
-
- if (dirty)
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-/**
- * GtkFlowBoxForeachFunc:
- * @box: a #GtkFlowBox
- * @child: a #GtkFlowBoxChild
- * @user_data: (closure): user data
- *
- * A function used by gtk_flow_box_selected_foreach().
- * It will be called on every selected child of the @box.
- *
- * Since: 3.12
- */
-
-/**
- * gtk_flow_box_selected_foreach:
- * @box: a #GtkFlowBox
- * @func: (scope call): the function to call for each selected child
- * @data: user data to pass to the function
- *
- * Calls a function for each selected child.
- *
- * Note that the selection cannot be modified from within
- * this function.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_selected_foreach (GtkFlowBox *box,
- GtkFlowBoxForeachFunc func,
- gpointer data)
-{
- GtkFlowBoxChild *child;
- GSequenceIter *iter;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children);
- !g_sequence_iter_is_end (iter);
- iter = g_sequence_iter_next (iter))
- {
- child = g_sequence_get (iter);
- if (CHILD_PRIV (child)->selected)
- (*func) (box, child, data);
- }
-}
-
-/**
- * gtk_flow_box_set_selection_mode:
- * @box: a #GtkFlowBox
- * @mode: the new selection mode
- *
- * Sets how selection works in @box.
- * See #GtkSelectionMode for details.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_selection_mode (GtkFlowBox *box,
- GtkSelectionMode mode)
-{
- gboolean dirty = FALSE;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (mode == BOX_PRIV (box)->selection_mode)
- return;
-
- if (mode == GTK_SELECTION_NONE ||
- BOX_PRIV (box)->selection_mode == GTK_SELECTION_MULTIPLE)
- {
- dirty = gtk_flow_box_unselect_all_internal (box);
- BOX_PRIV (box)->selected_child = NULL;
- }
-
- BOX_PRIV (box)->selection_mode = mode;
-
- g_object_notify (G_OBJECT (box), "selection-mode");
-
- if (dirty)
- g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0);
-}
-
-/**
- * gtk_flow_box_get_selection_mode:
- * @box: a #GtkFlowBox
- *
- * Gets the selection mode of @box.
- *
- * Returns: the #GtkSelectionMode
- *
- * Since: 3.12
- */
-GtkSelectionMode
-gtk_flow_box_get_selection_mode (GtkFlowBox *box)
-{
- g_return_val_if_fail (GTK_IS_FLOW_BOX (box), GTK_SELECTION_SINGLE);
-
- return BOX_PRIV (box)->selection_mode;
-}
-
-/* Filtering {{{2 */
-
-/**
- * GtkFlowBoxFilterFunc:
- * @child: a #GtkFlowBoxChild that may be filtered
- * @user_data: (closure): user data
- *
- * A function that will be called whenrever a child changes
- * or is added. It lets you control if the child should be
- * visible or not.
- *
- * Returns: %TRUE if the row should be visible, %FALSE otherwise
- *
- * Since: 3.12
- */
-
-/**
- * gtk_flow_box_set_filter_func:
- * @box: a #GtkFlowBox
- * @filter_func: (closure user_data) (allow-none): callback that
- * lets you filter which children to show
- * @user_data: user data passed to @filter_func
- * @destroy: destroy notifier for @user_data
- *
- * By setting a filter function on the @box one can decide dynamically
- * which of the children to show. For instance, to implement a search
- * function that only shows the children matching the search terms.
- *
- * The @filter_func will be called for each child after the call, and
- * it will continue to be called each time a child changes (via
- * gtk_flow_box_child_changed()) or when gtk_flow_box_invalidate_filter()
- * is called.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_filter_func (GtkFlowBox *box,
- GtkFlowBoxFilterFunc filter_func,
- gpointer user_data,
- GDestroyNotify destroy)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- priv = BOX_PRIV (box);
-
- if (priv->filter_destroy != NULL)
- priv->filter_destroy (priv->filter_data);
-
- priv->filter_func = filter_func;
- priv->filter_data = user_data;
- priv->filter_destroy = destroy;
-
- gtk_flow_box_apply_filter_all (box);
-}
-
-/**
- * gtk_flow_box_invalidate_filter:
- * @box: a #GtkFlowBox
- *
- * Updates the filtering for all children.
- *
- * Call this function when the result of the filter
- * function on the @box is changed due ot an external
- * factor. For instance, this would be used if the
- * filter function just looked for a specific search
- * term, and the entry with the string has changed.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_invalidate_filter (GtkFlowBox *box)
-{
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- if (BOX_PRIV (box)->filter_func != NULL)
- gtk_flow_box_apply_filter_all (box);
-}
-
-/* Sorting {{{2 */
-
-/**
- * GtkFlowBoxSortFunc:
- * @child1: the first child
- * @child2: the second child
- * @user_data: (closure): user data
- *
- * A function to compare two children to determine which
- * should come first.
- *
- * Returns: < 0 if @child1 should be before @child2, 0 if
- * the are equal, and > 0 otherwise
- *
- * Since: 3.12
- */
-
-/**
- * gtk_flow_box_set_sort_func:
- * @box: a #GtkFlowBox
- * @sort_func: (closure user_data) (allow-none): the sort function
- * @user_data: user data passed to @sort_func
- * @destroy: destroy notifier for @user_data
- *
- * By setting a sort function on the @box, one can dynamically
- * reorder the children of the box, based on the contents of
- * the children.
- *
- * The @sort_func will be called for each child after the call,
- * and will continue to be called each time a child changes (via
- * gtk_flow_box_child_changed()) and when gtk_flow_box_invalidate_sort()
- * is called.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_set_sort_func (GtkFlowBox *box,
- GtkFlowBoxSortFunc sort_func,
- gpointer user_data,
- GDestroyNotify destroy)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- priv = BOX_PRIV (box);
-
- if (priv->sort_destroy != NULL)
- priv->sort_destroy (priv->sort_data);
-
- priv->sort_func = sort_func;
- priv->sort_data = user_data;
- priv->sort_destroy = destroy;
-
- gtk_flow_box_invalidate_sort (box);
-}
-
-static gint
-gtk_flow_box_sort (GtkFlowBoxChild *a,
- GtkFlowBoxChild *b,
- GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv = BOX_PRIV (box);
-
- return priv->sort_func (a, b, priv->sort_data);
-}
-
-/**
- * gtk_flow_box_invalidate_sort:
- * @box: a #GtkFlowBox
- *
- * Updates the sorting for all children.
- *
- * Call this when the result of the sort function on
- * @box is changed due to an external factor.
- *
- * Since: 3.12
- */
-void
-gtk_flow_box_invalidate_sort (GtkFlowBox *box)
-{
- GtkFlowBoxPrivate *priv;
-
- g_return_if_fail (GTK_IS_FLOW_BOX (box));
-
- priv = BOX_PRIV (box);
-
- if (priv->sort_func != NULL)
- {
- g_sequence_sort (priv->children,
- (GCompareDataFunc)gtk_flow_box_sort, box);
- gtk_widget_queue_resize (GTK_WIDGET (box));
- }
-}
-
-/* vim:set foldmethod=marker expandtab: */
diff --git a/src/interface-gtk/gtkflowbox.h b/src/interface-gtk/gtkflowbox.h
deleted file mode 100644
index 6f0549f..0000000
--- a/src/interface-gtk/gtkflowbox.h
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2010 Openismus GmbH
- * Copyright (C) 2013 Red Hat, Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Library General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This library 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
- * Library General Public License for more details.
- *
- * You should have received a copy of the GNU Library General Public
- * License along with this library; if not, see <http://www.gnu.org/licenses/>.
-
- *
- * Authors:
- * Tristan Van Berkom <tristanvb@openismus.com>
- * Matthias Clasen <mclasen@redhat.com>
- * William Jon McCann <jmccann@redhat.com>
- */
-
-#ifndef __GTK_FLOW_BOX_H__
-#define __GTK_FLOW_BOX_H__
-
-#include <gtk/gtk.h>
-
-G_BEGIN_DECLS
-
-
-#define GTK_TYPE_FLOW_BOX (gtk_flow_box_get_type ())
-#define GTK_FLOW_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_FLOW_BOX, GtkFlowBox))
-#define GTK_FLOW_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FLOW_BOX, GtkFlowBoxClass))
-#define GTK_IS_FLOW_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_FLOW_BOX))
-#define GTK_IS_FLOW_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FLOW_BOX))
-#define GTK_FLOW_BOX_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_FLOW_BOX, GtkFlowBoxClass))
-
-typedef struct _GtkFlowBox GtkFlowBox;
-typedef struct _GtkFlowBoxClass GtkFlowBoxClass;
-
-typedef struct _GtkFlowBoxChild GtkFlowBoxChild;
-typedef struct _GtkFlowBoxChildClass GtkFlowBoxChildClass;
-
-struct _GtkFlowBox
-{
- GtkContainer container;
-};
-
-struct _GtkFlowBoxClass
-{
- GtkContainerClass parent_class;
-
- void (*child_activated) (GtkFlowBox *box,
- GtkFlowBoxChild *child);
- void (*selected_children_changed) (GtkFlowBox *box);
- void (*activate_cursor_child) (GtkFlowBox *box);
- void (*toggle_cursor_child) (GtkFlowBox *box);
- void (*move_cursor) (GtkFlowBox *box,
- GtkMovementStep step,
- gint count);
- void (*select_all) (GtkFlowBox *box);
- void (*unselect_all) (GtkFlowBox *box);
-
- /* Padding for future expansion */
- void (*_gtk_reserved1) (void);
- void (*_gtk_reserved2) (void);
- void (*_gtk_reserved3) (void);
- void (*_gtk_reserved4) (void);
- void (*_gtk_reserved5) (void);
- void (*_gtk_reserved6) (void);
-};
-
-#define GTK_TYPE_FLOW_BOX_CHILD (gtk_flow_box_child_get_type ())
-#define GTK_FLOW_BOX_CHILD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChild))
-#define GTK_FLOW_BOX_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChildClass))
-#define GTK_IS_FLOW_BOX_CHILD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_FLOW_BOX_CHILD))
-#define GTK_IS_FLOW_BOX_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FLOW_BOX_CHILD))
-#define GTK_FLOW_BOX_CHILD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EG_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChildClass))
-
-struct _GtkFlowBoxChild
-{
- GtkBin parent_instance;
-};
-
-struct _GtkFlowBoxChildClass
-{
- GtkBinClass parent_class;
-
- void (* activate) (GtkFlowBoxChild *child);
-
- /* Padding for future expansion */
- void (*_gtk_reserved1) (void);
- void (*_gtk_reserved2) (void);
-};
-
-GType gtk_flow_box_child_get_type (void) G_GNUC_CONST;
-GtkWidget* gtk_flow_box_child_new (void);
-gint gtk_flow_box_child_get_index (GtkFlowBoxChild *child);
-gboolean gtk_flow_box_child_is_selected (GtkFlowBoxChild *child);
-void gtk_flow_box_child_changed (GtkFlowBoxChild *child);
-
-
-GType gtk_flow_box_get_type (void) G_GNUC_CONST;
-
-GtkWidget *gtk_flow_box_new (void);
-void gtk_flow_box_set_homogeneous (GtkFlowBox *box,
- gboolean homogeneous);
-gboolean gtk_flow_box_get_homogeneous (GtkFlowBox *box);
-void gtk_flow_box_set_row_spacing (GtkFlowBox *box,
- guint spacing);
-guint gtk_flow_box_get_row_spacing (GtkFlowBox *box);
-
-void gtk_flow_box_set_column_spacing (GtkFlowBox *box,
- guint spacing);
-guint gtk_flow_box_get_column_spacing (GtkFlowBox *box);
-
-void gtk_flow_box_set_min_children_per_line (GtkFlowBox *box,
- guint n_children);
-guint gtk_flow_box_get_min_children_per_line (GtkFlowBox *box);
-
-void gtk_flow_box_set_max_children_per_line (GtkFlowBox *box,
- guint n_children);
-guint gtk_flow_box_get_max_children_per_line (GtkFlowBox *box);
-void gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box,
- gboolean single);
-gboolean gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box);
-
-void gtk_flow_box_insert (GtkFlowBox *box,
- GtkWidget *widget,
- gint position);
-GtkFlowBoxChild *gtk_flow_box_get_child_at_index (GtkFlowBox *box,
- gint idx);
-
-typedef void (* GtkFlowBoxForeachFunc) (GtkFlowBox *box,
- GtkFlowBoxChild *child,
- gpointer user_data);
-
-void gtk_flow_box_selected_foreach (GtkFlowBox *box,
- GtkFlowBoxForeachFunc func,
- gpointer data);
-GList *gtk_flow_box_get_selected_children (GtkFlowBox *box);
-void gtk_flow_box_select_child (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-void gtk_flow_box_unselect_child (GtkFlowBox *box,
- GtkFlowBoxChild *child);
-void gtk_flow_box_select_all (GtkFlowBox *box);
-void gtk_flow_box_unselect_all (GtkFlowBox *box);
-void gtk_flow_box_set_selection_mode (GtkFlowBox *box,
- GtkSelectionMode mode);
-GtkSelectionMode gtk_flow_box_get_selection_mode (GtkFlowBox *box);
-void gtk_flow_box_set_hadjustment (GtkFlowBox *box,
- GtkAdjustment *adjustment);
-void gtk_flow_box_set_vadjustment (GtkFlowBox *box,
- GtkAdjustment *adjustment);
-
-typedef gboolean (*GtkFlowBoxFilterFunc) (GtkFlowBoxChild *child,
- gpointer user_data);
-
-void gtk_flow_box_set_filter_func (GtkFlowBox *box,
- GtkFlowBoxFilterFunc filter_func,
- gpointer user_data,
- GDestroyNotify destroy);
-void gtk_flow_box_invalidate_filter (GtkFlowBox *box);
-
-typedef gint (*GtkFlowBoxSortFunc) (GtkFlowBoxChild *child1,
- GtkFlowBoxChild *child2,
- gpointer user_data);
-
-void gtk_flow_box_set_sort_func (GtkFlowBox *box,
- GtkFlowBoxSortFunc sort_func,
- gpointer user_data,
- GDestroyNotify destroy);
-void gtk_flow_box_invalidate_sort (GtkFlowBox *box);
-
-G_END_DECLS
-
-
-#endif /* __GTK_FLOW_BOX_H__ */
diff --git a/src/interface-gtk/interface-gtk.cpp b/src/interface-gtk/interface-gtk.cpp
deleted file mode 100644
index 9486802..0000000
--- a/src/interface-gtk/interface-gtk.cpp
+++ /dev/null
@@ -1,1132 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <stdarg.h>
-#include <string.h>
-#include <signal.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-/*
- * FIXME: Because of gdk_threads_enter().
- * The only way to do it in Gtk3 style would be using
- * idle callbacks into the main thread and sync barriers (inefficient!)
- * or doing it single-threaded and ticking the Gtk main loop
- * (may be inefficient since gtk_events_pending() is doing
- * syscalls; however that may be ailed by doing it less frequently).
- */
-#define GDK_DISABLE_DEPRECATION_WARNINGS
-#include <gdk/gdk.h>
-#include <gdk-pixbuf/gdk-pixbuf.h>
-
-#include <gtk/gtk.h>
-
-#include <gio/gio.h>
-
-#include <Scintilla.h>
-#include <ScintillaWidget.h>
-
-#include "gtk-info-popup.h"
-#include "gtk-canonicalized-label.h"
-
-#include "sciteco.h"
-#include "string-utils.h"
-#include "cmdline.h"
-#include "qregisters.h"
-#include "ring.h"
-#include "interface.h"
-#include "interface-gtk.h"
-
-/*
- * Signal handlers (e.g. for handling SIGTERM) are only
- * available on Unix and beginning with v2.30, while
- * we still support v2.28.
- * Handlers using `signal()` cannot be used easily for
- * this purpose.
- */
-#if defined(G_OS_UNIX) && GLIB_CHECK_VERSION(2,30,0)
-#include <glib-unix.h>
-#define SCITECO_HANDLE_SIGNALS
-#endif
-
-namespace SciTECO {
-
-extern "C" {
-
-static void scintilla_notify(ScintillaObject *sci, uptr_t idFrom,
- SCNotification *notify, gpointer user_data);
-
-static gpointer exec_thread_cb(gpointer data);
-static gboolean cmdline_key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
- gpointer user_data);
-static gboolean window_delete_cb(GtkWidget *w, GdkEventAny *e,
- gpointer user_data);
-
-static gboolean sigterm_handler(gpointer user_data) G_GNUC_UNUSED;
-
-static gboolean
-g_object_unref_idle_cb(gpointer user_data)
-{
- g_object_unref(user_data);
- return G_SOURCE_REMOVE;
-}
-
-} /* extern "C" */
-
-#define 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"
-
-/**
- * Convert Scintilla-style BGR color triple to
- * RGB.
- */
-static inline guint32
-bgr2rgb(guint32 bgr)
-{
- return ((bgr & 0x0000FF) << 16) |
- ((bgr & 0x00FF00) << 0) |
- ((bgr & 0xFF0000) >> 16);
-}
-
-void
-ViewGtk::initialize_impl(void)
-{
- gint events;
-
- gdk_threads_enter();
-
- sci = SCINTILLA(scintilla_new());
- /*
- * We don't want the object to be destroyed
- * when it is removed from the vbox.
- */
- g_object_ref_sink(sci);
-
- scintilla_set_id(sci, 0);
-
- gtk_widget_set_size_request(get_widget(), 500, 300);
-
- /*
- * This disables mouse and key events on this view.
- * For some strange reason, masking events on
- * the event box does NOT work.
- * NOTE: Scroll events are still allowed - scrolling
- * is currently not under direct control of SciTECO
- * (i.e. it is OK the side effects of scrolling are not
- * tracked).
- */
- gtk_widget_set_can_focus(get_widget(), FALSE);
- events = gtk_widget_get_events(get_widget());
- events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
- events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
- gtk_widget_set_events(get_widget(), events);
-
- g_signal_connect(sci, SCINTILLA_NOTIFY,
- G_CALLBACK(scintilla_notify), NULL);
-
- /*
- * setup() calls Scintilla messages, so we must unlock
- * here already to avoid deadlocks.
- */
- gdk_threads_leave();
-
- setup();
-}
-
-ViewGtk::~ViewGtk()
-{
- /*
- * This does NOT destroy the Scintilla object
- * and GTK widget, if it is the current view
- * (and therefore added to the vbox).
- * FIXME: This only uses an idle watcher
- * because the destructor can be called with
- * the Gdk lock held and without.
- * Once the threading model is revised this
- * can be simplified and inlined again.
- */
- if (sci)
- gdk_threads_add_idle(g_object_unref_idle_cb, sci);
-}
-
-GOptionGroup *
-InterfaceGtk::get_options(void)
-{
- const GOptionEntry entries[] = {
- {"no-csd", 0, G_OPTION_FLAG_IN_MAIN | G_OPTION_FLAG_REVERSE,
- G_OPTION_ARG_NONE, &use_csd,
- "Disable client-side decorations.", NULL},
- {NULL}
- };
-
- /*
- * Parsing the option context with the Gtk option group
- * will automatically initialize Gtk, but we do not yet
- * open the default display.
- */
- GOptionGroup *group = gtk_get_option_group(FALSE);
-
- g_option_group_add_entries(group, entries);
-
- return group;
-}
-
-void
-InterfaceGtk::init(void)
-{
- static const Cmdline empty_cmdline;
-
- GtkWidget *vbox;
- GtkWidget *overlay_widget, *overlay_vbox;
- GtkWidget *message_bar_content;
-
- /*
- * g_thread_init() is required prior to v2.32
- * (we still support v2.28) but generates a warning
- * on newer versions.
- */
-#if !GLIB_CHECK_VERSION(2,32,0)
- g_thread_init(NULL);
-#endif
- gdk_threads_init();
-
- /*
- * gtk_init() is not necessary when using gtk_get_option_group(),
- * but this will open the default display.
- * FIXME: Perhaps it is possible to defer this until we initialize
- * interactive mode!?
- */
- gtk_init(NULL, NULL);
-
- /*
- * Register clipboard registers.
- * Unfortunately, we cannot find out which
- * clipboards/selections are supported on this system,
- * so we register only some default ones.
- */
- QRegisters::globals.insert(new QRegisterClipboard());
- QRegisters::globals.insert(new QRegisterClipboard("P"));
- QRegisters::globals.insert(new QRegisterClipboard("S"));
- QRegisters::globals.insert(new QRegisterClipboard("C"));
-
- /*
- * The event queue is initialized now, so we can
- * pass it as user data to C-linkage callbacks.
- */
- event_queue = g_async_queue_new();
-
- window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- g_signal_connect(G_OBJECT(window), "delete-event",
- G_CALLBACK(window_delete_cb), event_queue);
-
- vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-
- info_current = g_strdup("");
-
- /*
- * The info bar is tried to be made the title bar of the
- * window which also disables the default window decorations
- * (client-side decorations) unless --no-csd was specified.
- * NOTE: Client-side decoations could fail, leaving us with a
- * standard title bar and the info bar with close buttons.
- * Other window managers have undesirable side-effects.
- */
- info_bar_widget = gtk_header_bar_new();
- gtk_widget_set_name(info_bar_widget, "sciteco-info-bar");
- info_name_widget = gtk_canonicalized_label_new(NULL);
- gtk_widget_set_valign(info_name_widget, GTK_ALIGN_CENTER);
- gtk_style_context_add_class(gtk_widget_get_style_context(info_name_widget),
- "name-label");
- gtk_label_set_selectable(GTK_LABEL(info_name_widget), TRUE);
- /* NOTE: Header bar does not resize for multi-line labels */
- //gtk_label_set_line_wrap(GTK_LABEL(info_name_widget), TRUE);
- //gtk_label_set_lines(GTK_LABEL(info_name_widget), 2);
- gtk_header_bar_set_custom_title(GTK_HEADER_BAR(info_bar_widget), info_name_widget);
- info_image = gtk_image_new();
- gtk_header_bar_pack_start(GTK_HEADER_BAR(info_bar_widget), info_image);
- info_type_widget = gtk_label_new(NULL);
- gtk_widget_set_valign(info_type_widget, GTK_ALIGN_CENTER);
- gtk_style_context_add_class(gtk_widget_get_style_context(info_type_widget),
- "type-label");
- gtk_header_bar_pack_start(GTK_HEADER_BAR(info_bar_widget), info_type_widget);
- if (use_csd) {
- /* use client-side decorations */
- gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(info_bar_widget), TRUE);
- gtk_window_set_titlebar(GTK_WINDOW(window), info_bar_widget);
- } else {
- /* fall back to adding the info bar as an ordinary widget */
- gtk_box_pack_start(GTK_BOX(vbox), info_bar_widget, FALSE, FALSE, 0);
- }
-
- /*
- * Overlay widget will allow overlaying the Scintilla view
- * and message widgets with the info popup.
- * Therefore overlay_vbox (containing the view and popup)
- * will be the main child of the overlay.
- */
- overlay_widget = gtk_overlay_new();
- overlay_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-
- /*
- * The event box is the parent of all Scintilla views
- * that should be displayed.
- * This is handy when adding or removing current views,
- * enabling and disabling GDK updates and in order to filter
- * mouse and keyboard events going to Scintilla.
- */
- event_box_widget = gtk_event_box_new();
- gtk_event_box_set_above_child(GTK_EVENT_BOX(event_box_widget), TRUE);
- gtk_box_pack_start(GTK_BOX(overlay_vbox), event_box_widget,
- TRUE, TRUE, 0);
-
- message_bar_widget = gtk_info_bar_new();
- gtk_widget_set_name(message_bar_widget, "sciteco-message-bar");
- message_bar_content = gtk_info_bar_get_content_area(GTK_INFO_BAR(message_bar_widget));
- /* NOTE: Messages are always pre-canonicalized */
- message_widget = gtk_label_new(NULL);
- gtk_label_set_selectable(GTK_LABEL(message_widget), TRUE);
- gtk_label_set_line_wrap(GTK_LABEL(message_widget), TRUE);
- gtk_container_add(GTK_CONTAINER(message_bar_content), message_widget);
- gtk_box_pack_start(GTK_BOX(overlay_vbox), message_bar_widget,
- FALSE, FALSE, 0);
-
- gtk_container_add(GTK_CONTAINER(overlay_widget), overlay_vbox);
- gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0);
-
- cmdline_widget = gtk_entry_new();
- gtk_widget_set_name(cmdline_widget, "sciteco-cmdline");
- gtk_entry_set_has_frame(GTK_ENTRY(cmdline_widget), FALSE);
- gtk_editable_set_editable(GTK_EDITABLE(cmdline_widget), FALSE);
- g_signal_connect(G_OBJECT(cmdline_widget), "key-press-event",
- G_CALLBACK(cmdline_key_pressed_cb), event_queue);
- gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0);
-
- gtk_container_add(GTK_CONTAINER(window), vbox);
-
- /*
- * Popup widget will be shown in the bottom
- * of the overlay widget (i.e. the Scintilla views),
- * filling the entire width.
- */
- popup_widget = gtk_info_popup_new();
- gtk_widget_set_name(popup_widget, "sciteco-info-popup");
- gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), popup_widget);
- g_signal_connect(overlay_widget, "get-child-position",
- G_CALLBACK(gtk_info_popup_get_position_in_overlay), NULL);
-
- gtk_widget_grab_focus(cmdline_widget);
-
- cmdline_update(&empty_cmdline);
-}
-
-void
-InterfaceGtk::vmsg_impl(MessageType type, const gchar *fmt, va_list ap)
-{
- /*
- * The message types are chosen such that there is a CSS class
- * for every one of them. GTK_MESSAGE_OTHER does not have
- * a CSS class.
- */
- static const GtkMessageType type2gtk[] = {
- /* [MSG_USER] = */ GTK_MESSAGE_QUESTION,
- /* [MSG_INFO] = */ GTK_MESSAGE_INFO,
- /* [MSG_WARNING] = */ GTK_MESSAGE_WARNING,
- /* [MSG_ERROR] = */ GTK_MESSAGE_ERROR
- };
-
- va_list aq;
- gchar buf[255];
-
- /*
- * stdio_vmsg() leaves `ap` undefined and we are expected
- * to do the same and behave like vprintf().
- */
- va_copy(aq, ap);
- stdio_vmsg(type, fmt, ap);
- g_vsnprintf(buf, sizeof(buf), fmt, aq);
- va_end(aq);
-
- gdk_threads_enter();
-
- gtk_info_bar_set_message_type(GTK_INFO_BAR(message_bar_widget),
- type2gtk[type]);
- gtk_label_set_text(GTK_LABEL(message_widget), buf);
-
- if (type == MSG_ERROR)
- gtk_widget_error_bell(window);
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::msg_clear(void)
-{
- gdk_threads_enter();
-
- gtk_info_bar_set_message_type(GTK_INFO_BAR(message_bar_widget),
- GTK_MESSAGE_QUESTION);
- gtk_label_set_text(GTK_LABEL(message_widget), "");
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::show_view_impl(ViewGtk *view)
-{
- current_view = view;
-}
-
-void
-InterfaceGtk::refresh_info(void)
-{
- GtkStyleContext *style = gtk_widget_get_style_context(info_bar_widget);
- const gchar *info_type_str = PACKAGE;
- gchar *info_current_temp = g_strdup(info_current);
- gchar *info_current_canon;
- GIcon *icon;
- gchar *title;
-
- gtk_style_context_remove_class(style, "info-qregister");
- gtk_style_context_remove_class(style, "info-buffer");
- gtk_style_context_remove_class(style, "dirty");
-
- if (info_type == INFO_TYPE_BUFFER_DIRTY)
- String::append(info_current_temp, "*");
- gtk_canonicalized_label_set_text(GTK_CANONICALIZED_LABEL(info_name_widget),
- info_current_temp);
- info_current_canon = String::canonicalize_ctl(info_current_temp);
- g_free(info_current_temp);
-
- switch (info_type) {
- case INFO_TYPE_QREGISTER:
- gtk_style_context_add_class(style, "info-qregister");
-
- info_type_str = PACKAGE_NAME " - <QRegister> ";
- gtk_label_set_text(GTK_LABEL(info_type_widget), "QRegister");
- gtk_label_set_ellipsize(GTK_LABEL(info_name_widget),
- PANGO_ELLIPSIZE_START);
-
- /* FIXME: Use a Q-Register icon */
- gtk_image_clear(GTK_IMAGE(info_image));
- break;
-
- case INFO_TYPE_BUFFER_DIRTY:
- gtk_style_context_add_class(style, "dirty");
- /* fall through */
- case INFO_TYPE_BUFFER:
- gtk_style_context_add_class(style, "info-buffer");
-
- info_type_str = PACKAGE_NAME " - <Buffer> ";
- gtk_label_set_text(GTK_LABEL(info_type_widget), "Buffer");
- gtk_label_set_ellipsize(GTK_LABEL(info_name_widget),
- PANGO_ELLIPSIZE_MIDDLE);
-
- icon = gtk_info_popup_get_icon_for_path(info_current,
- "text-x-generic");
- if (!icon)
- break;
- gtk_image_set_from_gicon(GTK_IMAGE(info_image),
- icon, GTK_ICON_SIZE_LARGE_TOOLBAR);
- g_object_unref(icon);
- break;
- }
-
- title = g_strconcat(info_type_str, info_current_canon, NIL);
- gtk_window_set_title(GTK_WINDOW(window), title);
- g_free(title);
- g_free(info_current_canon);
-}
-
-void
-InterfaceGtk::info_update_impl(const QRegister *reg)
-{
- g_free(info_current);
- info_type = INFO_TYPE_QREGISTER;
- /* NOTE: will contain control characters */
- info_current = g_strdup(reg->name);
-}
-
-void
-InterfaceGtk::info_update_impl(const Buffer *buffer)
-{
- g_free(info_current);
- info_type = buffer->dirty ? INFO_TYPE_BUFFER_DIRTY
- : INFO_TYPE_BUFFER;
- info_current = g_strdup(buffer->filename ? : UNNAMED_FILE);
-}
-
-void
-InterfaceGtk::cmdline_insert_chr(gint &pos, gchar chr)
-{
- gchar buffer[5+1];
-
- /*
- * NOTE: This mapping is similar to
- * View::set_representations()
- */
- switch (chr) {
- case CTL_KEY_ESC:
- strcpy(buffer, "$");
- break;
- case '\r':
- strcpy(buffer, "<CR>");
- break;
- case '\n':
- strcpy(buffer, "<LF>");
- break;
- case '\t':
- strcpy(buffer, "<TAB>");
- break;
- default:
- if (IS_CTL(chr)) {
- buffer[0] = '^';
- buffer[1] = CTL_ECHO(chr);
- buffer[2] = '\0';
- } else {
- buffer[0] = chr;
- buffer[1] = '\0';
- }
- }
-
- gtk_editable_insert_text(GTK_EDITABLE(cmdline_widget),
- buffer, -1, &pos);
-}
-
-void
-InterfaceGtk::cmdline_update_impl(const Cmdline *cmdline)
-{
- gint pos = 1;
- gint cmdline_len;
-
- gdk_threads_enter();
-
- /*
- * We don't know if the new command line is similar to
- * the old one, so we can just as well rebuild it.
- */
- gtk_entry_set_text(GTK_ENTRY(cmdline_widget), "*");
-
- /* format effective command line */
- for (guint i = 0; i < cmdline->len; i++)
- cmdline_insert_chr(pos, (*cmdline)[i]);
- /* save end of effective command line */
- cmdline_len = pos;
-
- /* format rubbed out command line */
- for (guint i = cmdline->len; i < cmdline->len+cmdline->rubout_len; i++)
- cmdline_insert_chr(pos, (*cmdline)[i]);
-
- /* set cursor after effective command line */
- gtk_editable_set_position(GTK_EDITABLE(cmdline_widget), cmdline_len);
-
- gdk_threads_leave();
-}
-
-static GdkAtom
-get_selection_by_name(const gchar *name)
-{
- /*
- * We can use gdk_atom_intern() to support arbitrary X11 selection
- * names. However, since we cannot find out which selections are
- * registered, we are only providing QRegisters for the three default
- * selections.
- * 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);
-}
-
-void
-InterfaceGtk::set_clipboard(const gchar *name, const gchar *str, gssize str_len)
-{
- GtkClipboard *clipboard;
-
- gdk_threads_enter();
-
- clipboard = gtk_clipboard_get(get_selection_by_name(name));
-
- /*
- * NOTE: function has compatible semantics for str_len < 0.
- */
- gtk_clipboard_set_text(clipboard, str, str_len);
-
- gdk_threads_leave();
-}
-
-gchar *
-InterfaceGtk::get_clipboard(const gchar *name, gsize *str_len)
-{
- GtkClipboard *clipboard;
- gchar *str;
-
- gdk_threads_enter();
-
- clipboard = gtk_clipboard_get(get_selection_by_name(name));
- /*
- * Could return NULL for an empty clipboard.
- * NOTE: This converts to UTF8 and we loose the ability
- * to get clipboard with embedded nulls.
- */
- str = gtk_clipboard_wait_for_text(clipboard);
-
- gdk_threads_leave();
-
- if (str_len)
- *str_len = str ? strlen(str) : 0;
- return str;
-}
-
-void
-InterfaceGtk::popup_add_impl(PopupEntryType type,
- const gchar *name, bool highlight)
-{
- static const GtkInfoPopupEntryType type2gtk[] = {
- /* [POPUP_PLAIN] = */ GTK_INFO_POPUP_PLAIN,
- /* [POPUP_FILE] = */ GTK_INFO_POPUP_FILE,
- /* [POPUP_DIRECTORY] = */ GTK_INFO_POPUP_DIRECTORY
- };
-
- gdk_threads_enter();
-
- gtk_info_popup_add(GTK_INFO_POPUP(popup_widget),
- type2gtk[type], name, highlight);
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::popup_show_impl(void)
-{
- gdk_threads_enter();
-
- if (gtk_widget_get_visible(popup_widget))
- gtk_info_popup_scroll_page(GTK_INFO_POPUP(popup_widget));
- else
- gtk_widget_show(popup_widget);
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::popup_clear_impl(void)
-{
- gdk_threads_enter();
-
- if (gtk_widget_get_visible(popup_widget)) {
- gtk_widget_hide(popup_widget);
- gtk_info_popup_clear(GTK_INFO_POPUP(popup_widget));
- }
-
- gdk_threads_leave();
-}
-
-void
-InterfaceGtk::set_css_variables_from_view(ViewGtk *view)
-{
- guint font_size;
- gchar buffer[256];
-
- /*
- * Unfortunately, we cannot use CSS variables to pass around
- * font names and sizes, necessary for styling the command line
- * widget.
- * Therefore we just style it using generated CSS here.
- * This one of the few non-deprecated ways that Gtk leaves us
- * to set a custom font name.
- * CSS customizations have to take that into account.
- * NOTE: We don't actually know apriori how
- * large our buffer should be, but luckily STYLEGETFONT with
- * a sptr==0 will return only the size.
- * This is undocumented in the Scintilla docs.
- */
- gchar font_name[view->ssm(SCI_STYLEGETFONT, STYLE_DEFAULT) + 1];
- view->ssm(SCI_STYLEGETFONT, STYLE_DEFAULT, (sptr_t)font_name);
- font_size = view->ssm(SCI_STYLEGETSIZEFRACTIONAL, STYLE_DEFAULT);
-
- /*
- * Generates a CSS that sets some predefined color variables.
- * This effectively "exports" Scintilla styles into the CSS
- * world.
- * Those colors are used by the fallback.css shipping with SciTECO
- * in order to apply the SciTECO-controlled color scheme to all the
- * predefined UI elements.
- * They can also be used in user-customizations.
- */
- g_snprintf(buffer, sizeof(buffer),
- "@define-color sciteco_default_fg_color " CSS_COLOR_FORMAT ";"
- "@define-color sciteco_default_bg_color " CSS_COLOR_FORMAT ";"
- "@define-color sciteco_calltip_fg_color " CSS_COLOR_FORMAT ";"
- "@define-color sciteco_calltip_bg_color " CSS_COLOR_FORMAT ";"
- "#%s{"
- "font: %s %u.%u"
- "}",
- bgr2rgb(view->ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)),
- bgr2rgb(view->ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)),
- bgr2rgb(view->ssm(SCI_STYLEGETFORE, STYLE_CALLTIP)),
- bgr2rgb(view->ssm(SCI_STYLEGETBACK, STYLE_CALLTIP)),
- gtk_widget_get_name(cmdline_widget),
- font_name,
- font_size / SC_FONT_SIZE_MULTIPLIER,
- font_size % SC_FONT_SIZE_MULTIPLIER);
-
- /*
- * The GError and return value has been deprecated.
- * A CSS parsing error would point to a programming
- * error anyway.
- */
- gtk_css_provider_load_from_data(css_var_provider, buffer, -1, NULL);
-}
-
-void
-InterfaceGtk::event_loop_impl(void)
-{
- static const gchar *icon_files[] = {
- SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-16.png",
- SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-32.png",
- SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-48.png",
- NULL
- };
-
- GdkScreen *default_screen = gdk_screen_get_default();
- GtkCssProvider *user_css_provider;
- gchar *config_path, *user_css_file;
-
- GList *icon_list = NULL;
- GThread *thread;
-
- /*
- * Assign an icon to the window.
- * If the file could not be found, we fail silently.
- * FIXME: On Windows, it may be better to load the icon compiled
- * as a resource into the binary.
- */
- for (const gchar **file = icon_files; *file; file++) {
- GdkPixbuf *icon_pixbuf = gdk_pixbuf_new_from_file(*file, NULL);
-
- /* fail silently if there's a problem with one of the icons */
- if (icon_pixbuf)
- icon_list = g_list_append(icon_list, icon_pixbuf);
- }
-
- gtk_window_set_default_icon_list(icon_list);
-
- if (icon_list)
- g_list_free_full(icon_list, g_object_unref);
-
- refresh_info();
-
- /*
- * Initialize the CSS variable provider and the CSS provider
- * for the included fallback.css.
- * NOTE: The return value of gtk_css_provider_load() is deprecated.
- * Instead we could register for the "parsing-error" signal.
- * For the time being we just silently ignore parsing errors.
- * They will be printed to stderr by Gtk anyway.
- */
- css_var_provider = gtk_css_provider_new();
- if (current_view)
- /* set CSS variables initially */
- set_css_variables_from_view(current_view);
- gtk_style_context_add_provider_for_screen(default_screen,
- GTK_STYLE_PROVIDER(css_var_provider),
- GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
- user_css_provider = gtk_css_provider_new();
- /* get path of $SCITECOCONFIG/.teco_css */
- config_path = QRegisters::globals["$SCITECOCONFIG"]->get_string();
- user_css_file = g_build_filename(config_path, USER_CSS_FILE, NIL);
- if (g_file_test(user_css_file, G_FILE_TEST_IS_REGULAR))
- /* open user CSS */
- gtk_css_provider_load_from_path(user_css_provider,
- user_css_file, NULL);
- else
- /* use fallback CSS */
- gtk_css_provider_load_from_path(user_css_provider,
- SCITECODATADIR G_DIR_SEPARATOR_S
- "fallback.css",
- NULL);
- g_free(user_css_file);
- g_free(config_path);
- gtk_style_context_add_provider_for_screen(default_screen,
- GTK_STYLE_PROVIDER(user_css_provider),
- GTK_STYLE_PROVIDER_PRIORITY_USER);
-
- /*
- * When changing views, the new widget is not
- * added immediately to avoid flickering in the GUI.
- * It is only updated once per key press and only
- * if it really changed.
- * Therefore we must add the current view to the
- * window initially.
- * For the same reason, window title updates are
- * deferred to once after every key press, so we must
- * set the window title initially.
- */
- if (current_view) {
- current_view_widget = current_view->get_widget();
- gtk_container_add(GTK_CONTAINER(event_box_widget),
- current_view_widget);
- }
-
- gtk_widget_show_all(window);
- /* don't show popup by default */
- gtk_widget_hide(popup_widget);
-
- /*
- * SIGTERM emulates the "Close" key just like when
- * closing the window if supported by this version of glib.
- * Note that this replaces SciTECO's default SIGTERM handler
- * so it will additionally raise(SIGINT).
- */
-#ifdef SCITECO_HANDLE_SIGNALS
- g_unix_signal_add(SIGTERM, sigterm_handler, event_queue);
-#endif
-
- /*
- * Start up SciTECO execution thread.
- * Whenever it needs to send a Scintilla message
- * it locks the GDK mutex.
- */
- thread = g_thread_new("sciteco-exec",
- exec_thread_cb, event_queue);
-
- /*
- * NOTE: The watchers do not modify any GTK objects
- * using one of the methods that lock the GDK mutex.
- * This is from now on reserved to the execution
- * thread. Therefore there can be no dead-locks.
- */
- gdk_threads_enter();
- gtk_main();
- gdk_threads_leave();
-
- /*
- * This usually means that the user requested program
- * termination and the execution thread called
- * gtk_main_quit().
- * We still wait for the execution thread to shut down
- * properly. This also frees `thread`.
- */
- g_thread_join(thread);
-
- /*
- * Make sure the window is hidden
- * now already, as there may be code that has to be
- * executed in batch mode.
- */
- gtk_widget_hide(window);
-}
-
-static gpointer
-exec_thread_cb(gpointer data)
-{
- GAsyncQueue *event_queue = (GAsyncQueue *)data;
-
- for (;;) {
- GdkEventKey *event = (GdkEventKey *)g_async_queue_pop(event_queue);
-
- bool is_shift = event->state & GDK_SHIFT_MASK;
- bool is_ctl = event->state & GDK_CONTROL_MASK;
-
- try {
- sigint_occurred = FALSE;
- interface.handle_key_press(is_shift, is_ctl, event->keyval);
- sigint_occurred = FALSE;
- } catch (Quit) {
- /*
- * SciTECO should terminate, so we exit
- * this thread.
- * The main loop will terminate and
- * event_loop() will return.
- */
- gdk_event_free((GdkEvent *)event);
-
- gdk_threads_enter();
- gtk_main_quit();
- gdk_threads_leave();
- break;
- }
-
- gdk_event_free((GdkEvent *)event);
- }
-
- return NULL;
-}
-
-void
-InterfaceGtk::handle_key_press(bool is_shift, bool is_ctl, guint keyval)
-{
- GdkWindow *view_window;
- ViewGtk *last_view = current_view;
-
- /*
- * Avoid redraws of the current view by freezing updates
- * on the view's GDK window (we're running in parallel
- * to the main loop so there could be frequent redraws).
- * By freezing updates, the behaviour is similar to
- * the Curses UI.
- */
- gdk_threads_enter();
- view_window = gtk_widget_get_parent_window(event_box_widget);
- gdk_window_freeze_updates(view_window);
- gdk_threads_leave();
-
- switch (keyval) {
- case GDK_KEY_Escape:
- cmdline.keypress(CTL_KEY_ESC);
- break;
- case GDK_KEY_BackSpace:
- cmdline.keypress(CTL_KEY('H'));
- break;
- case GDK_KEY_Tab:
- cmdline.keypress('\t');
- break;
- case GDK_KEY_Return:
- cmdline.keypress('\n');
- break;
-
- /*
- * Function key macros
- */
-#define FN(KEY, MACRO) \
- case GDK_KEY_##KEY: cmdline.fnmacro(#MACRO); break
-#define FNS(KEY, MACRO) \
- case GDK_KEY_##KEY: cmdline.fnmacro(is_shift ? "S" #MACRO : #MACRO); break
- FN(Down, DOWN); FN(Up, UP);
- FNS(Left, LEFT); FNS(Right, RIGHT);
- FN(KP_Down, DOWN); FN(KP_Up, UP);
- FNS(KP_Left, LEFT); FNS(KP_Right, RIGHT);
- FNS(Home, HOME);
- case GDK_KEY_F1...GDK_KEY_F35: {
- gchar macro_name[3+1];
-
- g_snprintf(macro_name, sizeof(macro_name),
- "F%d", keyval - GDK_KEY_F1 + 1);
- cmdline.fnmacro(macro_name);
- break;
- }
- FNS(Delete, DC);
- FNS(Insert, IC);
- FN(Page_Down, NPAGE); FN(Page_Up, PPAGE);
- FNS(Print, PRINT);
- FN(KP_Home, A1); FN(KP_Prior, A3);
- FN(KP_Begin, B2);
- FN(KP_End, C1); FN(KP_Next, C3);
- FNS(End, END);
- FNS(Help, HELP);
- FN(Close, CLOSE);
-#undef FNS
-#undef FN
-
- /*
- * Control keys and keys with printable representation
- */
- default:
- gunichar u = gdk_keyval_to_unicode(keyval);
-
- if (u && g_unichar_to_utf8(u, NULL) == 1) {
- gchar key;
-
- g_unichar_to_utf8(u, &key);
- if (key > 0x7F)
- break;
- if (is_ctl)
- key = CTL_KEY(g_ascii_toupper(key));
-
- cmdline.keypress(key);
- }
- }
-
- /*
- * The styles configured via Scintilla might change
- * with every keypress.
- */
- set_css_variables_from_view(current_view);
-
- /*
- * The info area is updated very often and setting the
- * window title each time it is updated is VERY costly.
- * So we set it here once after every keypress even if the
- * info line did not change.
- * View changes are also only applied here to the GTK
- * window even though GDK updates have been frozen since
- * the size reallocations are very costly.
- */
- gdk_threads_enter();
-
- refresh_info();
-
- if (current_view != last_view) {
- /*
- * The last view's object is not guaranteed to
- * still exist.
- * However its widget is, due to reference counting.
- */
- if (current_view_widget)
- gtk_container_remove(GTK_CONTAINER(event_box_widget),
- current_view_widget);
-
- current_view_widget = current_view->get_widget();
-
- gtk_container_add(GTK_CONTAINER(event_box_widget),
- current_view_widget);
- gtk_widget_show(current_view_widget);
- }
-
- gdk_window_thaw_updates(view_window);
-
- gdk_threads_leave();
-}
-
-InterfaceGtk::~InterfaceGtk()
-{
- g_free(info_current);
-
- if (window)
- gtk_widget_destroy(window);
-
- scintilla_release_resources();
-
- if (event_queue) {
- GdkEvent *e;
-
- while ((e = (GdkEvent *)g_async_queue_try_pop(event_queue)))
- gdk_event_free(e);
-
- g_async_queue_unref(event_queue);
- }
-
- if (css_var_provider)
- g_object_unref(css_var_provider);
-}
-
-/*
- * GTK+ callbacks
- */
-
-static void
-scintilla_notify(ScintillaObject *sci, uptr_t idFrom,
- SCNotification *notify, gpointer user_data)
-{
- interface.process_notify(notify);
-}
-
-static gboolean
-cmdline_key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
- gpointer user_data)
-{
- GAsyncQueue *event_queue = (GAsyncQueue *)user_data;
-
- bool is_ctl = event->state & GDK_CONTROL_MASK;
-
-#ifdef DEBUG
- g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n",
- event->string, *event->string,
- event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK);
-#endif
-
- g_async_queue_lock(event_queue);
-
- if (g_async_queue_length_unlocked(event_queue) >= 0 &&
- is_ctl && gdk_keyval_to_upper(event->keyval) == GDK_KEY_C) {
- /*
- * Handle asynchronous interruptions if CTRL+C is pressed.
- * This will usually send SIGINT to the entire process
- * group and set `sigint_occurred`.
- * If the execution thread is currently blocking,
- * the key is delivered like an ordinary key press.
- */
- interrupt();
- } else {
- /*
- * Copies the key-press event, since it must be evaluated
- * by the exec_thread_cb. This is costly, but since we're
- * using the event queue as a kind of keyboard buffer,
- * who cares?
- */
- g_async_queue_push_unlocked(event_queue,
- gdk_event_copy((GdkEvent *)event));
- }
-
- g_async_queue_unlock(event_queue);
-
- return TRUE;
-}
-
-static gboolean
-window_delete_cb(GtkWidget *w, GdkEventAny *e, gpointer user_data)
-{
- GAsyncQueue *event_queue = (GAsyncQueue *)user_data;
- GdkEventKey *close_event;
-
- /*
- * 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
- * will eventually terminate.
- */
- close_event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS);
- close_event->window = gtk_widget_get_parent_window(w);
- close_event->keyval = GDK_KEY_Close;
-
- g_async_queue_push(event_queue, close_event);
-
- return TRUE;
-}
-
-static gboolean
-sigterm_handler(gpointer user_data)
-{
- GAsyncQueue *event_queue = (GAsyncQueue *)user_data;
- GdkEventKey *close_event;
-
- /*
- * Since this handler replaces the default one, we
- * also have to make sure it interrupts.
- */
- interrupt();
-
- /*
- * Similar to window deletion - emulate "close" key press.
- */
- close_event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS);
- close_event->keyval = GDK_KEY_Close;
-
- g_async_queue_push(event_queue, close_event);
-
- return G_SOURCE_CONTINUE;
-}
-
-} /* namespace SciTECO */
diff --git a/src/interface-gtk/interface-gtk.h b/src/interface-gtk/interface-gtk.h
deleted file mode 100644
index 82ed96b..0000000
--- a/src/interface-gtk/interface-gtk.h
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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/>.
- */
-
-#ifndef __INTERFACE_GTK_H
-#define __INTERFACE_GTK_H
-
-#include <stdarg.h>
-
-#include <glib.h>
-
-/* FIXME: see interface-gtk.cpp */
-#define GDK_DISABLE_DEPRECATION_WARNINGS
-#include <gdk/gdk.h>
-#include <gtk/gtk.h>
-
-#include <Scintilla.h>
-#include <ScintillaWidget.h>
-
-#include "interface.h"
-
-namespace SciTECO {
-
-typedef class ViewGtk : public View<ViewGtk> {
- ScintillaObject *sci;
-
-public:
- ViewGtk() : sci(NULL) {}
-
- /* implementation of View::initialize() */
- void initialize_impl(void);
-
- ~ViewGtk();
-
- inline GtkWidget *
- get_widget(void)
- {
- return GTK_WIDGET(sci);
- }
-
- /* implementation of View::ssm() */
- inline sptr_t
- ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0)
- {
- sptr_t ret;
-
- gdk_threads_enter();
- ret = scintilla_send_message(sci, iMessage, wParam, lParam);
- gdk_threads_leave();
-
- return ret;
- }
-} ViewCurrent;
-
-typedef class InterfaceGtk : public Interface<InterfaceGtk, ViewGtk> {
- GtkCssProvider *css_var_provider;
-
- GtkWidget *window;
-
- enum {
- INFO_TYPE_BUFFER = 0,
- INFO_TYPE_BUFFER_DIRTY,
- INFO_TYPE_QREGISTER
- } info_type;
- gchar *info_current;
- gboolean use_csd;
- GtkWidget *info_bar_widget;
- GtkWidget *info_image;
- GtkWidget *info_type_widget;
- GtkWidget *info_name_widget;
-
- GtkWidget *event_box_widget;
-
- GtkWidget *message_bar_widget;
- GtkWidget *message_widget;
-
- GtkWidget *cmdline_widget;
-
- GtkWidget *popup_widget;
-
- GtkWidget *current_view_widget;
-
- GAsyncQueue *event_queue;
-
-public:
- InterfaceGtk() : css_var_provider(NULL),
- window(NULL),
- info_type(INFO_TYPE_BUFFER), info_current(NULL),
- use_csd(TRUE),
- info_bar_widget(NULL),
- info_image(NULL), info_type_widget(NULL), info_name_widget(NULL),
- event_box_widget(NULL),
- message_bar_widget(NULL), message_widget(NULL),
- cmdline_widget(NULL),
- popup_widget(NULL),
- current_view_widget(NULL),
- event_queue(NULL) {}
- ~InterfaceGtk();
-
- /* overrides Interface::get_options() */
- GOptionGroup *get_options(void);
-
- /* override of Interface::init() */
- void init(void);
-
- /* implementation of Interface::vmsg() */
- void vmsg_impl(MessageType type, const gchar *fmt, va_list ap);
- /* overrides Interface::msg_clear() */
- void msg_clear(void);
-
- /* implementation of Interface::show_view() */
- void show_view_impl(ViewGtk *view);
-
- /* implementation of Interface::info_update() */
- void info_update_impl(const QRegister *reg);
- void info_update_impl(const Buffer *buffer);
-
- /* implementation of Interface::cmdline_update() */
- void cmdline_update_impl(const Cmdline *cmdline);
-
- /* override of Interface::set_clipboard() */
- void set_clipboard(const gchar *name,
- const gchar *str = NULL, gssize str_len = -1);
- /* override of Interface::get_clipboard() */
- gchar *get_clipboard(const gchar *name, gsize *str_len = NULL);
-
- /* implementation of Interface::popup_add() */
- void popup_add_impl(PopupEntryType type,
- const gchar *name, bool highlight = false);
- /* implementation of Interface::popup_show() */
- void popup_show_impl(void);
-
- /* implementation of Interface::popup_is_shown() */
- inline bool
- popup_is_shown_impl(void)
- {
- bool ret;
-
- gdk_threads_enter();
- ret = gtk_widget_get_visible(popup_widget);
- gdk_threads_leave();
-
- return ret;
- }
- /* implementation of Interface::popup_clear() */
- void popup_clear_impl(void);
-
- /* main entry point (implementation) */
- void event_loop_impl(void);
-
- /*
- * FIXME: This is for internal use only and could be
- * hidden in a nested forward-declared friend struct.
- */
- void handle_key_press(bool is_shift, bool is_ctl, guint keyval);
-
-private:
- void set_css_variables_from_view(ViewGtk *view);
-
- void refresh_info(void);
- void cmdline_insert_chr(gint &pos, gchar chr);
-} InterfaceCurrent;
-
-} /* namespace SciTECO */
-
-#endif
diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c
new file mode 100644
index 0000000..afc8fe3
--- /dev/null
+++ b/src/interface-gtk/interface.c
@@ -0,0 +1,1203 @@
+/*
+ * 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 <stdarg.h>
+#include <string.h>
+#include <signal.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#ifdef G_OS_UNIX
+#include <glib-unix.h>
+#endif
+
+#include <gdk/gdk.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include <gtk/gtk.h>
+
+#include <gio/gio.h>
+
+#include <Scintilla.h>
+#include <ScintillaWidget.h>
+
+#include "teco-gtk-info-popup.h"
+#include "teco-gtk-label.h"
+
+#include "sciteco.h"
+#include "error.h"
+#include "string-utils.h"
+#include "cmdline.h"
+#include "qreg.h"
+#include "ring.h"
+#include "interface.h"
+
+//#define DEBUG
+
+static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
+ GdkRectangle *allocation,
+ gpointer user_data);
+static gboolean teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
+ 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;
+
+#define 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)
+{
+ return GUINT32_SWAP_LE_BE(bgr) >> 8;
+}
+
+/*
+ * NOTE: The teco_view_t pointer is reused to directly
+ * point to the ScintillaObject.
+ * This saves one heap object per view.
+ */
+
+static void
+teco_view_scintilla_notify(ScintillaObject *sci, gint id,
+ struct SCNotification *notify, gpointer user_data)
+{
+ teco_interface_process_notify(notify);
+}
+
+teco_view_t *
+teco_view_new(void)
+{
+ ScintillaObject *sci = SCINTILLA(scintilla_new());
+ /*
+ * We don't want the object to be destroyed
+ * when it is removed from the vbox.
+ */
+ g_object_ref_sink(sci);
+
+ scintilla_set_id(sci, 0);
+
+ gtk_widget_set_size_request(GTK_WIDGET(sci), 500, 300);
+
+ /*
+ * This disables mouse and key events on this view.
+ * For some strange reason, masking events on
+ * the event box does NOT work.
+ *
+ * NOTE: Scroll events are still allowed - scrolling
+ * is currently not under direct control of SciTECO
+ * (i.e. it is OK the side effects of scrolling are not
+ * tracked).
+ */
+ gtk_widget_set_can_focus(GTK_WIDGET(sci), FALSE);
+ gint events = gtk_widget_get_events(GTK_WIDGET(sci));
+ events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
+ events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
+ gtk_widget_set_events(GTK_WIDGET(sci), events);
+
+ g_signal_connect(sci, SCINTILLA_NOTIFY,
+ G_CALLBACK(teco_view_scintilla_notify), NULL);
+
+ return (teco_view_t *)sci;
+}
+
+static inline GtkWidget *
+teco_view_get_widget(teco_view_t *ctx)
+{
+ return GTK_WIDGET(ctx);
+}
+
+sptr_t
+teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam)
+{
+ return scintilla_send_message(SCINTILLA(ctx), iMessage, wParam, lParam);
+}
+
+static gboolean
+teco_view_free_idle_cb(gpointer user_data)
+{
+ /*
+ * This does NOT destroy the Scintilla object
+ * and GTK widget, if it is the current view
+ * (and therefore added to the vbox).
+ */
+ g_object_unref(user_data);
+ return G_SOURCE_REMOVE;
+}
+
+void
+teco_view_free(teco_view_t *ctx)
+{
+ /*
+ * FIXME: The widget is unreffed only in an idle watcher because
+ * Scintilla may have idle callbacks activated (see ScintillaGTK.cxx)
+ * and we must prevent use-after-frees.
+ * A simple g_idle_remove_by_data() does not suffice for some strange reason
+ * (perhaps it does not prevent the invocation of already activated watchers).
+ * This is a bug should better be fixed by reference counting in
+ * ScintillaGTK.cxx itself.
+ */
+ g_idle_add_full(G_PRIORITY_LOW, teco_view_free_idle_cb, SCINTILLA(ctx), NULL);
+}
+
+static struct {
+ GtkCssProvider *css_var_provider;
+
+ GtkWidget *window;
+
+ enum {
+ TECO_INFO_TYPE_BUFFER = 0,
+ TECO_INFO_TYPE_BUFFER_DIRTY,
+ TECO_INFO_TYPE_QREG
+ } info_type;
+ teco_string_t info_current;
+
+ gboolean no_csd;
+ GtkWidget *info_bar_widget;
+ GtkWidget *info_image;
+ GtkWidget *info_type_widget;
+ GtkWidget *info_name_widget;
+
+ GtkWidget *event_box_widget;
+
+ GtkWidget *message_bar_widget;
+ GtkWidget *message_widget;
+
+ teco_view_t *cmdline_view;
+
+ GtkWidget *popup_widget;
+
+ GtkWidget *current_view_widget;
+
+ GQueue *event_queue;
+} teco_interface;
+
+void
+teco_interface_init(void)
+{
+ /*
+ * gtk_init() is not necessary when using gtk_get_option_group(),
+ * but this will open the default display.
+ *
+ * FIXME: Perhaps it is possible to defer this until we initialize
+ * interactive mode!?
+ */
+ gtk_init(NULL, NULL);
+
+ /*
+ * Register clipboard registers.
+ * Unfortunately, we cannot find out which
+ * 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_interface.event_queue = g_queue_new();
+
+ teco_interface.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ g_signal_connect(teco_interface.window, "delete-event",
+ G_CALLBACK(teco_interface_window_delete_cb), NULL);
+
+ g_signal_connect(teco_interface.window, "key-press-event",
+ G_CALLBACK(teco_interface_key_pressed_cb), NULL);
+
+ GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
+ /*
+ * The info bar is tried to be made the title bar of the
+ * window which also disables the default window decorations
+ * (client-side decorations) unless --no-csd was specified.
+ *
+ * NOTE: Client-side decoations could fail, leaving us with a
+ * standard title bar and the info bar with close buttons.
+ * Other window managers have undesirable side-effects.
+ */
+ 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);
+ 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),
+ "label");
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget),
+ "name-label");
+ gtk_label_set_selectable(GTK_LABEL(teco_interface.info_name_widget), TRUE);
+ /* NOTE: Header bar does not resize for multi-line labels */
+ //gtk_label_set_line_wrap(GTK_LABEL(teco_interface.info_name_widget), TRUE);
+ //gtk_label_set_lines(GTK_LABEL(teco_interface.info_name_widget), 2);
+ gtk_header_bar_set_custom_title(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_name_widget);
+ teco_interface.info_image = gtk_image_new();
+ gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_image);
+ teco_interface.info_type_widget = gtk_label_new(NULL);
+ gtk_widget_set_valign(teco_interface.info_type_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_type_widget),
+ "label");
+ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_type_widget),
+ "type-label");
+ gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget),
+ teco_interface.info_type_widget);
+ if (teco_interface.no_csd) {
+ /* fall back to adding the info bar as an ordinary widget */
+ gtk_box_pack_start(GTK_BOX(vbox), teco_interface.info_bar_widget,
+ FALSE, FALSE, 0);
+ } else {
+ /* use client-side decorations */
+ gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(teco_interface.info_bar_widget), TRUE);
+ gtk_window_set_titlebar(GTK_WINDOW(teco_interface.window),
+ teco_interface.info_bar_widget);
+ }
+
+ /*
+ * Overlay widget will allow overlaying the Scintilla view
+ * and message widgets with the info popup.
+ * Therefore overlay_vbox (containing the view and popup)
+ * will be the main child of the overlay.
+ */
+ GtkWidget *overlay_widget = gtk_overlay_new();
+ GtkWidget *overlay_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
+ /*
+ * The event box is the parent of all Scintilla views
+ * that should be displayed.
+ * This is handy when adding or removing current views,
+ * enabling and disabling GDK updates and in order to filter
+ * mouse and keyboard events going to Scintilla.
+ */
+ teco_interface.event_box_widget = gtk_event_box_new();
+ gtk_event_box_set_above_child(GTK_EVENT_BOX(teco_interface.event_box_widget), TRUE);
+ gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.event_box_widget,
+ TRUE, TRUE, 0);
+
+ 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);
+ /* 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");
+ gtk_label_set_selectable(GTK_LABEL(teco_interface.message_widget), TRUE);
+ gtk_label_set_line_wrap(GTK_LABEL(teco_interface.message_widget), TRUE);
+ gtk_container_add(GTK_CONTAINER(message_bar_content), teco_interface.message_widget);
+ gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.message_bar_widget,
+ FALSE, FALSE, 0);
+
+ 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)"*");
+ 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);
+
+ GtkWidget *cmdline_widget = teco_view_get_widget(teco_interface.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);
+ gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0);
+
+ gtk_container_add(GTK_CONTAINER(teco_interface.window), vbox);
+
+ /*
+ * Popup widget will be shown in the bottom
+ * of the overlay widget (i.e. the Scintilla views),
+ * filling the entire width.
+ */
+ teco_interface.popup_widget = teco_gtk_info_popup_new();
+ gtk_widget_set_name(teco_interface.popup_widget, "sciteco-info-popup");
+ gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), teco_interface.popup_widget);
+ g_signal_connect(overlay_widget, "get-child-position",
+ G_CALLBACK(teco_gtk_info_popup_get_position_in_overlay), NULL);
+
+ /*
+ * FIXME: Nothing can really take the focus, so it will end up in the
+ * selectable labels unless we explicitly prevent it.
+ */
+ 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);
+}
+
+GOptionGroup *
+teco_interface_get_options(void)
+{
+ /*
+ * FIXME: On platforms where you want to disable CSD, you usually
+ * want to disable it always, so it should be configurable in the SciTECO
+ * profile.
+ * On the other hand, you could just install gtk3-nocsd.
+ */
+ static const GOptionEntry entries[] = {
+ {"no-csd", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, &teco_interface.no_csd,
+ "Disable client-side decorations.", NULL},
+ {NULL}
+ };
+
+ /*
+ * Parsing the option context with the Gtk option group
+ * will automatically initialize Gtk, but we do not yet
+ * open the default display.
+ */
+ GOptionGroup *group = gtk_get_option_group(FALSE);
+
+ g_option_group_add_entries(group, entries);
+
+ return group;
+}
+
+void teco_interface_init_color(guint color, guint32 rgb) {}
+
+void
+teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+{
+ /*
+ * The message types are chosen such that there is a CSS class
+ * for every one of them. GTK_MESSAGE_OTHER does not have
+ * a CSS class.
+ */
+ static const GtkMessageType type2gtk[] = {
+ [TECO_MSG_USER] = GTK_MESSAGE_QUESTION,
+ [TECO_MSG_INFO] = GTK_MESSAGE_INFO,
+ [TECO_MSG_WARNING] = GTK_MESSAGE_WARNING,
+ [TECO_MSG_ERROR] = GTK_MESSAGE_ERROR
+ };
+
+ 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);
+
+ 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);
+
+ if (type == TECO_MSG_ERROR)
+ gtk_widget_error_bell(teco_interface.window);
+}
+
+void
+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), "");
+}
+
+void
+teco_interface_show_view(teco_view_t *view)
+{
+ teco_interface_current_view = view;
+}
+
+static void
+teco_interface_refresh_info(void)
+{
+ GtkStyleContext *style = gtk_widget_get_style_context(teco_interface.info_bar_widget);
+
+ gtk_style_context_remove_class(style, "info-qregister");
+ gtk_style_context_remove_class(style, "info-buffer");
+ 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_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),
+ info_current_temp.data, info_current_temp.len);
+ g_autofree gchar *info_current_canon =
+ teco_string_echo(info_current_temp.data, info_current_temp.len);
+
+ const gchar *info_type_str = PACKAGE;
+ g_autoptr(GIcon) icon = NULL;
+
+ switch (teco_interface.info_type) {
+ case TECO_INFO_TYPE_QREG:
+ gtk_style_context_add_class(style, "info-qregister");
+
+ info_type_str = PACKAGE_NAME " - <QRegister> ";
+ gtk_label_set_text(GTK_LABEL(teco_interface.info_type_widget), "QRegister");
+ gtk_label_set_ellipsize(GTK_LABEL(teco_interface.info_name_widget),
+ PANGO_ELLIPSIZE_START);
+
+ /*
+ * FIXME: Perhaps we should use the SciTECO icon for Q-Registers.
+ */
+ icon = g_icon_new_for_string("emblem-generic", NULL);
+ break;
+
+ case TECO_INFO_TYPE_BUFFER_DIRTY:
+ gtk_style_context_add_class(style, "dirty");
+ /* fall through */
+ case TECO_INFO_TYPE_BUFFER:
+ gtk_style_context_add_class(style, "info-buffer");
+
+ info_type_str = PACKAGE_NAME " - <Buffer> ";
+ gtk_label_set_text(GTK_LABEL(teco_interface.info_type_widget), "Buffer");
+ gtk_label_set_ellipsize(GTK_LABEL(teco_interface.info_name_widget),
+ PANGO_ELLIPSIZE_MIDDLE);
+
+ icon = teco_gtk_info_popup_get_icon_for_path(teco_interface.info_current.data,
+ "text-x-generic");
+ break;
+ }
+
+ g_autofree gchar *title = g_strconcat(info_type_str, info_current_canon, NULL);
+ gtk_window_set_title(GTK_WINDOW(teco_interface.window), title);
+
+ if (icon) {
+ gint width, height;
+ gtk_icon_size_lookup(GTK_ICON_SIZE_LARGE_TOOLBAR, &width, &height);
+
+ gtk_image_set_from_gicon(GTK_IMAGE(teco_interface.info_image),
+ icon, GTK_ICON_SIZE_LARGE_TOOLBAR);
+ /* This is necessary so that oversized icons get scaled down. */
+ gtk_image_set_pixel_size(GTK_IMAGE(teco_interface.info_image), height);
+ }
+}
+
+void
+teco_interface_info_update_qreg(const teco_qreg_t *reg)
+{
+ teco_string_clear(&teco_interface.info_current);
+ teco_string_init(&teco_interface.info_current,
+ reg->head.name.data, reg->head.name.len);
+ teco_interface.info_type = TECO_INFO_TYPE_QREG;
+}
+
+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);
+ teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 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);
+}
+
+static GdkAtom
+teco_interface_get_selection_by_name(const gchar *name)
+{
+ /*
+ * We can use gdk_atom_intern() to support arbitrary X11 selection
+ * names. However, since we cannot find out which selections are
+ * registered, we are only providing QRegisters for the three default
+ * selections.
+ * 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);
+}
+
+gboolean
+teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, GError **error)
+{
+ GtkClipboard *clipboard = gtk_clipboard_get(teco_interface_get_selection_by_name(name));
+
+ /*
+ * NOTE: function has compatible semantics for str_len < 0.
+ */
+ gtk_clipboard_set_text(clipboard, str, str_len);
+
+ return TRUE;
+}
+
+gboolean
+teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error)
+{
+ GtkClipboard *clipboard = gtk_clipboard_get(teco_interface_get_selection_by_name(name));
+ /*
+ * Could return NULL for an empty clipboard.
+ *
+ * FIXME: This converts to UTF8 and we loose the ability
+ * to get clipboard with embedded nulls.
+ */
+ g_autofree gchar *contents = gtk_clipboard_wait_for_text(clipboard);
+
+ *len = contents ? strlen(contents) : 0;
+ if (str)
+ *str = g_steal_pointer(&contents);
+
+ return TRUE;
+}
+
+void
+teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len,
+ gboolean highlight)
+{
+ teco_gtk_info_popup_add(TECO_GTK_INFO_POPUP(teco_interface.popup_widget),
+ type, name, name_len, highlight);
+}
+
+void
+teco_interface_popup_show(void)
+{
+ if (gtk_widget_get_visible(teco_interface.popup_widget))
+ teco_gtk_info_popup_scroll_page(TECO_GTK_INFO_POPUP(teco_interface.popup_widget));
+ else
+ gtk_widget_show(teco_interface.popup_widget);
+}
+
+gboolean
+teco_interface_popup_is_shown(void)
+{
+ return gtk_widget_get_visible(teco_interface.popup_widget);
+}
+
+void
+teco_interface_popup_clear(void)
+{
+ if (gtk_widget_get_visible(teco_interface.popup_widget)) {
+ gtk_widget_hide(teco_interface.popup_widget);
+ teco_gtk_info_popup_clear(TECO_GTK_INFO_POPUP(teco_interface.popup_widget));
+ }
+}
+
+/**
+ * Whether the execution has been interrupted (CTRL+C).
+ *
+ * This is called regularily, so it is used to drive the
+ * main loop so that we can still process key presses.
+ *
+ * This approach is significantly slower in interactive mode
+ * than executing in a separate thread probably due to the
+ * system call overhead.
+ * But the GDK lock that would be necessary for synchronization
+ * has been deprecated.
+ */
+gboolean
+teco_interface_is_interrupted(void)
+{
+ if (gtk_main_level() > 0)
+ gtk_main_iteration_do(FALSE);
+
+ return teco_sigint_occurred != FALSE;
+}
+
+static void
+teco_interface_set_css_variables(teco_view_t *view)
+{
+ guint32 default_fg_color = teco_view_ssm(view, SCI_STYLEGETFORE, STYLE_DEFAULT, 0);
+ guint32 default_bg_color = teco_view_ssm(view, SCI_STYLEGETBACK, STYLE_DEFAULT, 0);
+ guint32 calltip_fg_color = teco_view_ssm(view, SCI_STYLEGETFORE, STYLE_CALLTIP, 0);
+ 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, default_fg_color, 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.
+ * Those colors are used by the fallback.css shipping with SciTECO
+ * in order to apply the SciTECO-controlled color scheme to all the
+ * predefined UI elements.
+ * They can also be used in user-customizations.
+ */
+ gchar css[256];
+ g_snprintf(css, sizeof(css),
+ "@define-color sciteco_default_fg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_default_bg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_calltip_fg_color " CSS_COLOR_FORMAT ";"
+ "@define-color sciteco_calltip_bg_color " CSS_COLOR_FORMAT ";",
+ teco_bgr2rgb(default_fg_color), teco_bgr2rgb(default_bg_color),
+ teco_bgr2rgb(calltip_fg_color), teco_bgr2rgb(calltip_bg_color));
+
+ /*
+ * The GError and return value has been deprecated.
+ * A CSS parsing error would point to a programming
+ * error anyway.
+ */
+ 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,
+ * 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(teco_view_get_widget(teco_interface.cmdline_view), -1, text_height);
+}
+
+static gboolean
+teco_interface_handle_key_press(guint keyval, guint state, GError **error)
+{
+ teco_view_t *last_view = teco_interface_current_view;
+
+ switch (keyval) {
+ case GDK_KEY_Escape:
+ if (!teco_cmdline_keypress_c('\e', error))
+ return FALSE;
+ break;
+ case GDK_KEY_BackSpace:
+ if (!teco_cmdline_keypress_c(TECO_CTL_KEY('H'), error))
+ return FALSE;
+ break;
+ case GDK_KEY_Tab:
+ if (!teco_cmdline_keypress_c('\t', error))
+ return FALSE;
+ break;
+ case GDK_KEY_Return:
+ if (!teco_cmdline_keypress_c('\n', error))
+ return FALSE;
+ break;
+
+ /*
+ * Function key macros
+ */
+#define FN(KEY, MACRO) \
+ case GDK_KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(#MACRO, error)) \
+ return FALSE; \
+ break
+#define FNS(KEY, MACRO) \
+ case GDK_KEY_##KEY: \
+ if (!teco_cmdline_fnmacro(state & GDK_SHIFT_MASK ? "S" #MACRO : #MACRO, error)) \
+ return FALSE; \
+ break
+ FN(Down, DOWN); FN(Up, UP);
+ FNS(Left, LEFT); FNS(Right, RIGHT);
+ FN(KP_Down, DOWN); FN(KP_Up, UP);
+ FNS(KP_Left, LEFT); FNS(KP_Right, RIGHT);
+ FNS(Home, HOME);
+ case GDK_KEY_F1...GDK_KEY_F35: {
+ gchar macro_name[3+1];
+
+ g_snprintf(macro_name, sizeof(macro_name),
+ "F%d", keyval - GDK_KEY_F1 + 1);
+ if (!teco_cmdline_fnmacro(macro_name, error))
+ return FALSE;
+ break;
+ }
+ FNS(Delete, DC);
+ FNS(Insert, IC);
+ FN(Page_Down, NPAGE); FN(Page_Up, PPAGE);
+ FNS(Print, PRINT);
+ FN(KP_Home, A1); FN(KP_Prior, A3);
+ FN(KP_Begin, B2);
+ FN(KP_End, C1); FN(KP_Next, C3);
+ FNS(End, END);
+ FNS(Help, HELP);
+ FN(Close, CLOSE);
+#undef FNS
+#undef FN
+
+ /*
+ * Control keys and keys with printable representation
+ */
+ default: {
+ gunichar u = gdk_keyval_to_unicode(keyval);
+
+ if (!u || g_unichar_to_utf8(u, NULL) != 1)
+ break;
+
+ gchar key;
+
+ g_unichar_to_utf8(u, &key);
+ if (key > 0x7F)
+ break;
+ if (state & GDK_CONTROL_MASK)
+ key = TECO_CTL_KEY(g_ascii_toupper(key));
+
+ if (!teco_cmdline_keypress_c(key, error))
+ return FALSE;
+ }
+ }
+
+ /*
+ * The styles configured via Scintilla might change
+ * with every keypress.
+ */
+ teco_interface_set_css_variables(teco_interface_current_view);
+
+ /*
+ * The info area is updated very often and setting the
+ * window title each time it is updated is VERY costly.
+ * So we set it here once after every keypress even if the
+ * info line did not change.
+ * View changes are also only applied here to the GTK
+ * window even though GDK updates have been frozen since
+ * the size reallocations are very costly.
+ */
+ teco_interface_refresh_info();
+
+ if (teco_interface_current_view != last_view) {
+ /*
+ * The last view's object is not guaranteed to
+ * still exist.
+ * However its widget is, due to reference counting.
+ */
+ if (teco_interface.current_view_widget)
+ gtk_container_remove(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+
+ teco_interface.current_view_widget = teco_view_get_widget(teco_interface_current_view);
+
+ gtk_container_add(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+ gtk_widget_show(teco_interface.current_view_widget);
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_interface_event_loop(GError **error)
+{
+ static const gchar *icon_files[] = {
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-48.png",
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-32.png",
+ SCITECODATADIR G_DIR_SEPARATOR_S "sciteco-16.png",
+ NULL
+ };
+
+ /*
+ * Assign an icon to the window.
+ *
+ * FIXME: On Windows, it may be better to load the icon compiled
+ * as a resource into the binary.
+ */
+ GList *icon_list = NULL;
+
+ for (const gchar **file = icon_files; *file; file++) {
+ GdkPixbuf *icon_pixbuf = gdk_pixbuf_new_from_file(*file, NULL);
+
+ /* fail silently if there's a problem with one of the icons */
+ if (icon_pixbuf)
+ icon_list = g_list_append(icon_list, icon_pixbuf);
+ }
+
+ gtk_window_set_default_icon_list(icon_list);
+
+ g_list_free_full(icon_list, g_object_unref);
+
+ teco_interface_refresh_info();
+
+ /*
+ * Initialize the CSS variable provider and the CSS provider
+ * for the included fallback.css.
+ */
+ teco_interface.css_var_provider = gtk_css_provider_new();
+ if (teco_interface_current_view)
+ /* set CSS variables initially */
+ teco_interface_set_css_variables(teco_interface_current_view);
+ GdkScreen *default_screen = gdk_screen_get_default();
+ gtk_style_context_add_provider_for_screen(default_screen,
+ GTK_STYLE_PROVIDER(teco_interface.css_var_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ /* get path of $SCITECOCONFIG/.teco_css */
+ teco_qreg_t *config_path_reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECOCONFIG", 14);
+ g_assert(config_path_reg != NULL);
+ g_auto(teco_string_t) config_path = {NULL, 0};
+ if (!config_path_reg->vtable->get_string(config_path_reg, &config_path.data, &config_path.len, error))
+ return FALSE;
+ if (teco_string_contains(&config_path, '\0')) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Null-character not allowed in filenames");
+ return FALSE;
+ }
+ g_autofree gchar *user_css_file = g_build_filename(config_path.data, USER_CSS_FILE, NULL);
+
+ GtkCssProvider *user_css_provider = gtk_css_provider_new();
+ /*
+ * NOTE: The return value of gtk_css_provider_load() is deprecated.
+ * Instead we could register for the "parsing-error" signal.
+ * For the time being we just silently ignore parsing errors.
+ * They will be printed to stderr by Gtk anyway.
+ */
+ if (g_file_test(user_css_file, G_FILE_TEST_IS_REGULAR))
+ /* open user CSS */
+ gtk_css_provider_load_from_path(user_css_provider, user_css_file, NULL);
+ else
+ /* use fallback CSS */
+ gtk_css_provider_load_from_path(user_css_provider,
+ SCITECODATADIR G_DIR_SEPARATOR_S "fallback.css",
+ NULL);
+ gtk_style_context_add_provider_for_screen(default_screen,
+ GTK_STYLE_PROVIDER(user_css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+
+ /*
+ * When changing views, the new widget is not
+ * added immediately to avoid flickering in the GUI.
+ * It is only updated once per key press and only
+ * if it really changed.
+ * Therefore we must add the current view to the
+ * window initially.
+ * For the same reason, window title updates are
+ * deferred to once after every key press, so we must
+ * set the window title initially.
+ */
+ if (teco_interface_current_view) {
+ teco_interface.current_view_widget = teco_view_get_widget(teco_interface_current_view);
+ gtk_container_add(GTK_CONTAINER(teco_interface.event_box_widget),
+ teco_interface.current_view_widget);
+ }
+
+ gtk_widget_show_all(teco_interface.window);
+ /* don't show popup by default */
+ gtk_widget_hide(teco_interface.popup_widget);
+
+ /*
+ * SIGTERM emulates the "Close" key just like when
+ * closing the window if supported by this version of glib.
+ * Note that this replaces SciTECO's default SIGTERM handler
+ * so it will additionally raise(SIGINT).
+ *
+ * FIXME: On ^Z, we do not suspend properly. The window is still shown.
+ * Perhaps we should try to catch SIGTSTP?
+ * This does not work with g_unix_signal_add(), though, so any
+ * workaround would be tricky.
+ * We could create a pipe via g_unix_open_pipe() which we
+ * write to using write() in a normal signal handler.
+ * We can then add a watcher using g_unix_fd_add() which will
+ * hide the main window.
+ */
+#ifdef G_OS_UNIX
+ g_unix_signal_add(SIGTERM, teco_interface_sigterm_handler, NULL);
+#endif
+
+ gtk_main();
+
+ /*
+ * Make sure the window is hidden
+ * now already, as there may be code that has to be
+ * executed in batch mode.
+ */
+ gtk_widget_hide(teco_interface.window);
+
+ return TRUE;
+}
+
+void
+teco_interface_cleanup(void)
+{
+ teco_string_clear(&teco_interface.info_current);
+
+ if (teco_interface.window)
+ gtk_widget_destroy(teco_interface.window);
+
+ scintilla_release_resources();
+
+ if (teco_interface.event_queue)
+ g_queue_free_full(teco_interface.event_queue,
+ (GDestroyNotify)gdk_event_free);
+
+ if (teco_interface.css_var_provider)
+ g_object_unref(teco_interface.css_var_provider);
+}
+
+/*
+ * GTK+ callbacks
+ */
+
+/**
+ * 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.
+ */
+static void
+teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
+ GdkRectangle *allocation, gpointer user_data)
+{
+ /*
+ * The GDK lock is already held, so we avoid using teco_view_ssm().
+ */
+ scintilla_send_message(SCINTILLA(widget), SCI_SETXCARETPOLICY,
+ CARET_SLOP, allocation->width/2);
+}
+
+static gboolean
+teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+
+#ifdef DEBUG
+ g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n",
+ event->string, *event->string,
+ event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK);
+#endif
+
+ if (teco_cmdline.pc < teco_cmdline.effective_len) {
+ /*
+ * We're already executing, so this event is processed
+ * from gtk_main_iteration_do().
+ * Unfortunately, gtk_main_level() is still 1 in this case.
+ *
+ * We might also completely replace the watchers
+ * during execution, but the current implementation is
+ * probably easier.
+ */
+ if (event->state & GDK_CONTROL_MASK &&
+ gdk_keyval_to_upper(event->keyval) == GDK_KEY_C)
+ /*
+ * Handle asynchronous interruptions if CTRL+C is pressed.
+ * This will usually send SIGINT to the entire process
+ * group and set `teco_sigint_occurred`.
+ * If the execution thread is currently blocking,
+ * the key is delivered like an ordinary key press.
+ */
+ teco_interrupt();
+ else
+ g_queue_push_tail(teco_interface.event_queue,
+ gdk_event_copy((GdkEvent *)event));
+
+ return TRUE;
+ }
+
+ g_queue_push_tail(teco_interface.event_queue, gdk_event_copy((GdkEvent *)event));
+
+ /*
+ * Avoid redraws of the current view by freezing updates
+ * on the view's GDK window (we're running in parallel
+ * to the main loop so there could be frequent redraws).
+ * By freezing updates, the behaviour is similar to
+ * the Curses UI.
+ */
+ GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window));
+ /*
+ * FIXME: A simple freeze will not suffice to prevent updates in code like <Sx$;>.
+ * gdk_window_freeze_toplevel_updates_libgtk_only() is deprecated, though.
+ * Perhaps this hack is no longer required after upgrading Scintilla.
+ *
+ * For the time being, we just live with the expected deprecation warnings,
+ * although they could theoretically be suppressed using
+ * `#pragma GCC diagnostic ignored`.
+ */
+ //gdk_window_freeze_updates(top_window);
+ gdk_window_freeze_toplevel_updates_libgtk_only(top_window);
+
+ /*
+ * The event queue might be filled when pressing keys when SciTECO
+ * is busy executing code.
+ */
+ do {
+ g_autoptr(GdkEvent) event = g_queue_pop_head(teco_interface.event_queue);
+
+ teco_sigint_occurred = FALSE;
+ teco_interface_handle_key_press(event->key.keyval, event->key.state, &error);
+ teco_sigint_occurred = FALSE;
+
+ if (g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT)) {
+ gtk_main_quit();
+ break;
+ }
+ } while (!g_queue_is_empty(teco_interface.event_queue));
+
+ gdk_window_thaw_toplevel_updates_libgtk_only(top_window);
+ //gdk_window_thaw_updates(top_window);
+
+ return TRUE;
+}
+
+static gboolean
+teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer user_data)
+{
+ /*
+ * 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
+ * will eventually terminate.
+ */
+ 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_key_pressed_cb(widget, &close_event->key, NULL);
+}
+
+static gboolean
+teco_interface_sigterm_handler(gpointer user_data)
+{
+ /*
+ * Since this handler replaces the default signal handler,
+ * we also have to make sure it interrupts.
+ */
+ teco_interrupt();
+
+ /*
+ * Similar to window deletion - emulate "close" key press.
+ */
+ g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS);
+ close_event->key.keyval = GDK_KEY_Close;
+
+ return teco_interface_key_pressed_cb(teco_interface.window, &close_event->key, NULL);
+}
diff --git a/src/interface-gtk/teco-gtk-info-popup.gob b/src/interface-gtk/teco-gtk-info-popup.gob
new file mode 100644
index 0000000..f08b0d7
--- /dev/null
+++ b/src/interface-gtk/teco-gtk-info-popup.gob
@@ -0,0 +1,446 @@
+/*
+ * 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/>.
+ */
+
+requires 2.0.20
+
+%ctop{
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+#include <math.h>
+
+#include <glib/gprintf.h>
+
+#include "list.h"
+#include "string-utils.h"
+#include "teco-gtk-label.h"
+%}
+
+%h{
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+
+#include <gio/gio.h>
+
+#include "interface.h"
+%}
+
+%{
+/*
+ * FIXME: This is redundant with curses-info-popup.c.
+ */
+typedef struct {
+ teco_stailq_entry_t entry;
+
+ teco_popup_entry_type_t type;
+ teco_string_t name;
+ gboolean highlight;
+} teco_popup_entry_t;
+%}
+
+/*
+ * NOTE: Deriving from GtkEventBox ensures that we can
+ * set a background on the entire popup widget.
+ */
+class Teco:Gtk:Info:Popup from Gtk:Event:Box {
+ /* These are added to other widgets, so they don't have to be destroyed */
+ public GtkAdjustment *hadjustment = {gtk_adjustment_new(0, 0, 0, 0, 0, 0)};
+ public GtkAdjustment *vadjustment = {gtk_adjustment_new(0, 0, 0, 0, 0, 0)};
+
+ private GtkWidget *flow_box = {gtk_flow_box_new()};
+
+ private GStringChunk *chunk = {g_string_chunk_new(32)}
+ destroywith g_string_chunk_free;
+ private teco_stailq_head_t list
+ destroy {
+ teco_stailq_entry_t *entry;
+ while ((entry = teco_stailq_remove_head(&VAR)))
+ g_free(entry);
+ };
+ private guint idle_id = 0;
+ private gboolean frozen = FALSE;
+
+ init(self)
+ {
+ /*
+ * A box containing a viewport and scrollbar will
+ * "emulate" a scrolled window.
+ * We cannot use a scrolled window since it ignores
+ * the preferred height of its viewport which breaks
+ * height-for-width management.
+ */
+ GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+ GtkWidget *scrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL,
+ self->vadjustment);
+ /* show/hide the scrollbar dynamically */
+ g_signal_connect(self->vadjustment, "changed",
+ G_CALLBACK(self_vadjustment_changed), scrollbar);
+
+ /* take as little height as necessary */
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(selfp->flow_box),
+ GTK_ORIENTATION_HORIZONTAL);
+ //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(selfp->flow_box), TRUE);
+ /* this for focus handling only, not for scrolling */
+ gtk_flow_box_set_hadjustment(GTK_FLOW_BOX(selfp->flow_box),
+ self->hadjustment);
+ gtk_flow_box_set_vadjustment(GTK_FLOW_BOX(selfp->flow_box),
+ self->vadjustment);
+
+ GtkWidget *viewport = gtk_viewport_new(self->hadjustment, self->vadjustment);
+ gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
+ gtk_container_add(GTK_CONTAINER(viewport), selfp->flow_box);
+
+ gtk_box_pack_start(GTK_BOX(box), viewport, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(box), scrollbar, FALSE, FALSE, 0);
+ gtk_widget_show_all(box);
+
+ /*
+ * NOTE: Everything shown except the top-level container.
+ * Therefore a gtk_widget_show() is enough to show our popup.
+ */
+ gtk_container_add(GTK_CONTAINER(self), box);
+
+ selfp->list = TECO_STAILQ_HEAD_INITIALIZER(&selfp->list);
+ }
+
+ /**
+ * Allocate position in an overlay.
+ *
+ * This function can be used as the "get-child-position" signal
+ * handler of a GtkOverlay in order to position the popup at the
+ * bottom of the overlay's main child, spanning the entire width.
+ * In contrast to the GtkOverlay's default allocation schemes,
+ * this makes sure that the widget will not be larger than the
+ * main child, so the popup properly scrolls when becoming too large
+ * in height.
+ *
+ * @param user_data unused by this callback
+ */
+ public gboolean
+ get_position_in_overlay(Gtk:Overlay *overlay, Gtk:Widget *widget,
+ Gdk:Rectangle *allocation, gpointer user_data)
+ {
+ GtkWidget *main_child = gtk_bin_get_child(GTK_BIN(overlay));
+ GtkAllocation main_child_alloc;
+ gint natural_height;
+
+ gtk_widget_get_allocation(main_child, &main_child_alloc);
+ gtk_widget_get_preferred_height_for_width(widget,
+ main_child_alloc.width,
+ NULL, &natural_height);
+
+ /*
+ * FIXME: Probably due to some bug in the height-for-width
+ * calculation of Gtk (at least in 3.10 or in the GtkFlowBox
+ * fallback included with SciTECO), the natural height
+ * is a bit too small to accommodate the entire GtkFlowBox,
+ * resulting in the GtkViewport always scrolling.
+ * This hack fixes it up in a NONPORTABLE manner.
+ */
+ natural_height += 5;
+
+ allocation->width = main_child_alloc.width;
+ allocation->height = MIN(natural_height, main_child_alloc.height);
+ allocation->x = 0;
+ allocation->y = main_child_alloc.height - allocation->height;
+
+ return TRUE;
+ }
+
+ /*
+ * Adapted from GtkScrolledWindow's gtk_scrolled_window_scroll_event()
+ * since the viewport does not react to scroll events.
+ * This is registered for our container widget instead of only for
+ * GtkViewport since this is what GtkScrolledWindow does.
+ *
+ * FIXME: May need to handle non-delta scrolling, i.e. GDK_SCROLL_UP
+ * and GDK_SCROLL_DOWN.
+ */
+ override (Gtk:Widget) gboolean
+ scroll_event(Gtk:Widget *widget, Gdk:Event:Scroll *event)
+ {
+ Self *self = SELF(widget);
+ gdouble delta_x, delta_y;
+
+ if (!gdk_event_get_scroll_deltas((GdkEvent *)event,
+ &delta_x, &delta_y))
+ return FALSE;
+
+ GtkAdjustment *adj = self->vadjustment;
+ gdouble page_size = gtk_adjustment_get_page_size(adj);
+ gdouble scroll_unit = pow(page_size, 2.0 / 3.0);
+ gdouble new_value;
+
+ new_value = CLAMP(gtk_adjustment_get_value(adj) + delta_y * scroll_unit,
+ gtk_adjustment_get_lower(adj),
+ gtk_adjustment_get_upper(adj) -
+ gtk_adjustment_get_page_size(adj));
+
+ gtk_adjustment_set_value(adj, new_value);
+
+ return TRUE;
+ }
+
+ private void
+ vadjustment_changed(Gtk:Adjustment *vadjustment, Gtk:Widget *scrollbar)
+ {
+ /*
+ * This shows/hides the widget using opacity instead of using
+ * gtk_widget_set_visibility() since the latter would influence
+ * size allocations. A widget with opacity 0 keeps its size.
+ */
+ gtk_widget_set_opacity(scrollbar,
+ gtk_adjustment_get_upper(vadjustment) -
+ gtk_adjustment_get_lower(vadjustment) >
+ gtk_adjustment_get_page_size(vadjustment) ? 1 : 0);
+ }
+
+ public GtkWidget *
+ new(void)
+ {
+ return GTK_WIDGET(GET_NEW);
+ }
+
+ public GIcon *
+ get_icon_for_path(const gchar *path, const gchar *fallback_name)
+ {
+ GIcon *icon = NULL;
+
+ g_autoptr(GFile) file = g_file_new_for_path(path);
+ g_autoptr(GFileInfo) info = g_file_query_info(file, "standard::icon", 0, NULL, NULL);
+ if (info) {
+ icon = g_file_info_get_icon(info);
+ g_object_ref(icon);
+ } else {
+ /* fall back to standard icon, but this can still return NULL! */
+ icon = g_icon_new_for_string(fallback_name, NULL);
+ }
+
+ return icon;
+ }
+
+ public void
+ add(self, teco_popup_entry_type_t type,
+ const gchar *name, gssize len, gboolean highlight)
+ {
+ teco_popup_entry_t *entry = g_new(teco_popup_entry_t, 1);
+ entry->type = type;
+ /*
+ * Popup entries aren't removed individually, so we can
+ * more efficiently store them via GStringChunk.
+ */
+ teco_string_init_chunk(&entry->name, name, len < 0 ? strlen(name) : len,
+ selfp->chunk);
+ entry->highlight = highlight;
+
+ /*
+ * NOTE: We don't immediately create the Gtk+ widget and add it
+ * to the GtkFlowBox since it would be too slow for very large
+ * numbers of popup entries.
+ * Instead, we queue and process them in idle time only once the widget
+ * is shown. This ensures a good reactivity, even though the popup may
+ * not yet be complete when first shown.
+ *
+ * While it would be possible to show the widget before the first
+ * add() call to achieve the same effect, this would prevent keyboard
+ * interaction unless we add support for interruptions or drive
+ * the event loop manually.
+ */
+ teco_stailq_insert_tail(&selfp->list, &entry->entry);
+ }
+
+ override (Gtk:Widget) void
+ show(Gtk:Widget *widget)
+ {
+ Self *self = SELF(widget);
+
+ if (!selfp->idle_id) {
+ selfp->idle_id = gdk_threads_add_idle((GSourceFunc)self_idle_cb, self);
+
+ /*
+ * To prevent a visible popup build-up for small popups,
+ * the display is frozen until the popup is large enough for
+ * scrolling or until all entries have been added.
+ */
+ GdkWindow *window = gtk_widget_get_window(widget);
+ if (window) {
+ gdk_window_freeze_updates(window);
+ selfp->frozen = TRUE;
+ }
+ }
+
+ PARENT_HANDLER(widget);
+ }
+
+ private gboolean
+ idle_cb(self)
+ {
+ /*
+ * The more often this is repeated, the faster we will add all popup entries,
+ * but at the same time, the UI will be less responsive.
+ */
+ for (gint i = 0; i < 5; i++) {
+ teco_popup_entry_t *head = (teco_popup_entry_t *)teco_stailq_remove_head(&selfp->list);
+ if (G_UNLIKELY(!head)) {
+ if (selfp->frozen)
+ gdk_window_thaw_updates(gtk_widget_get_window(GTK_WIDGET(self)));
+ selfp->frozen = FALSE;
+ selfp->idle_id = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ self_idle_add(self, head->type, head->name.data, head->name.len, head->highlight);
+
+ /* All teco_popup_entry_t::names are freed via GStringChunk */
+ g_free(head);
+ }
+
+ if (selfp->frozen &&
+ gtk_adjustment_get_upper(self->vadjustment) -
+ gtk_adjustment_get_lower(self->vadjustment) > gtk_adjustment_get_page_size(self->vadjustment)) {
+ /* the GtkFlowBox needs scrolling - time to thaw */
+ gdk_window_thaw_updates(gtk_widget_get_window(GTK_WIDGET(self)));
+ selfp->frozen = FALSE;
+ }
+
+ return G_SOURCE_CONTINUE;
+ }
+
+ private void
+ idle_add(self, teco_popup_entry_type_t type,
+ const gchar *name, gssize len, gboolean highlight)
+ {
+ GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
+ if (highlight)
+ gtk_style_context_add_class(gtk_widget_get_style_context(hbox),
+ "highlight");
+
+ /*
+ * FIXME: The icon fetching takes about 1/3 of the time required to
+ * add all widgets.
+ * Perhaps it's possible to optimize this.
+ */
+ if (type == TECO_POPUP_FILE || type == TECO_POPUP_DIRECTORY) {
+ 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 = self_get_icon_for_path(path, fallback);
+ if (icon) {
+ gint width, height;
+ gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
+
+ GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU);
+ /* This is necessary so that oversized icons get scaled down. */
+ gtk_image_set_pixel_size(GTK_IMAGE(image), height);
+ gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
+ }
+ }
+
+ GtkWidget *label = teco_gtk_label_new(name, len);
+ /*
+ * Gtk v3.20 changed the CSS element names.
+ * Adding a style class eases writing a portable fallback.css.
+ */
+ gtk_style_context_add_class(gtk_widget_get_style_context(label), "label");
+ gtk_widget_set_halign(label, GTK_ALIGN_START);
+ gtk_widget_set_valign(label, GTK_ALIGN_CENTER);
+
+ /*
+ * FIXME: This makes little sense once we've got mouse support.
+ * But for the time being, it's a useful setting.
+ */
+ gtk_label_set_selectable(GTK_LABEL(label), TRUE);
+
+ switch (type) {
+ case TECO_POPUP_PLAIN:
+ gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_START);
+ break;
+ case TECO_POPUP_FILE:
+ case TECO_POPUP_DIRECTORY:
+ gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
+ break;
+ }
+
+ gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
+
+ gtk_widget_show_all(hbox);
+ gtk_container_add(GTK_CONTAINER(selfp->flow_box), hbox);
+ }
+
+ public void
+ scroll_page(self)
+ {
+ GtkAdjustment *adj = self->vadjustment;
+ gdouble new_value;
+
+ if (gtk_adjustment_get_value(adj) + gtk_adjustment_get_page_size(adj) ==
+ gtk_adjustment_get_upper(adj)) {
+ /* wrap and scroll back to the top */
+ new_value = gtk_adjustment_get_lower(adj);
+ } else {
+ /* scroll one page */
+ new_value = gtk_adjustment_get_value(adj) +
+ gtk_adjustment_get_page_size(adj);
+
+ /*
+ * Adjust this so only complete entries are shown.
+ * Effectively, this rounds down to the line height.
+ */
+ GList *child_list = gtk_container_get_children(GTK_CONTAINER(selfp->flow_box));
+ if (child_list) {
+ new_value -= (gint)new_value %
+ gtk_widget_get_allocated_height(GTK_WIDGET(child_list->data));
+ g_list_free(child_list);
+ }
+
+ /* clip to the maximum possible value */
+ new_value = MIN(new_value, gtk_adjustment_get_upper(adj));
+ }
+
+ gtk_adjustment_set_value(adj, new_value);
+ }
+
+ private void
+ destroy_cb(Gtk:Widget *widget, gpointer user_data)
+ {
+ gtk_widget_destroy(widget);
+ }
+
+ public void
+ clear(self)
+ {
+ gtk_container_foreach(GTK_CONTAINER(selfp->flow_box), self_destroy_cb, NULL);
+
+ /*
+ * If there are still queued popoup entries, the next self_idle_cb()
+ * invocation will also stop the GSource.
+ */
+ teco_stailq_entry_t *entry;
+ while ((entry = teco_stailq_remove_head(&selfp->list)))
+ g_free(entry);
+
+ g_string_chunk_clear(selfp->chunk);
+ }
+}
diff --git a/src/interface-gtk/gtk-canonicalized-label.gob b/src/interface-gtk/teco-gtk-label.gob
index c6adeeb..2167dbc 100644
--- a/src/interface-gtk/gtk-canonicalized-label.gob
+++ b/src/interface-gtk/teco-gtk-label.gob
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -28,68 +28,86 @@ requires 2.0.20
#include <gdk/gdk.h>
-/*
- * NOTE: These definitions are also in sciteco.h,
- * but we cannot include them from a plain C file.
- */
-#define IS_CTL(C) ((C) < ' ')
-#define CTL_ECHO(C) ((C) | 0x40)
-#define CTL_KEY_ESC 27
+#include "sciteco.h"
+#include "string-utils.h"
-#define GDK_TO_PANGO_COLOR(X) \
- ((guint16)((X) * G_MAXUINT16))
+#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16))
%}
%h{
#include <gtk/gtk.h>
%}
-class Gtk:Canonicalized:Label from Gtk:Label {
+class Teco:Gtk:Label from Gtk:Label {
private PangoColor fg;
private guint16 fg_alpha;
private PangoColor bg;
private guint16 bg_alpha;
+ private teco_string_t string
+ destroy {
+ teco_string_clear(&VAR);
+ };
+
override (Gtk:Widget) void
style_updated(Gtk:Widget *widget)
{
Self *self = SELF(widget);
- GtkStyleContext *style;
- GdkRGBA normal_color;
-
PARENT_HANDLER(widget);
- style = gtk_widget_get_style_context(widget);
+ GtkStyleContext *style = gtk_widget_get_style_context(widget);
+ GdkRGBA normal_color;
gtk_style_context_get_color(style, GTK_STATE_NORMAL, &normal_color);
- self->_priv->bg.red = GDK_TO_PANGO_COLOR(normal_color.red);
- self->_priv->bg.green = GDK_TO_PANGO_COLOR(normal_color.green);
- self->_priv->bg.blue = GDK_TO_PANGO_COLOR(normal_color.blue);
- self->_priv->bg_alpha = GDK_TO_PANGO_COLOR(normal_color.alpha);
+ selfp->bg.red = GDK_TO_PANGO_COLOR(normal_color.red);
+ selfp->bg.green = GDK_TO_PANGO_COLOR(normal_color.green);
+ selfp->bg.blue = GDK_TO_PANGO_COLOR(normal_color.blue);
+ selfp->bg_alpha = GDK_TO_PANGO_COLOR(normal_color.alpha);
/*
* If Pango does not support transparent foregrounds,
* it will at least use a high-contrast foreground.
+ *
* NOTE: It would be very hard to get an appropriate background
* color even if Gtk supports it since the label itself may
* not have one but one of its parents.
+ *
* FIXME: We may want to honour the background color,
* so we can at least get decent reverse text when setting
* the background color in the CSS.
*/
- self->_priv->fg.red = G_MAXUINT16 - self->_priv->bg.red;
- self->_priv->fg.green = G_MAXUINT16 - self->_priv->bg.green;
- self->_priv->fg.blue = G_MAXUINT16 - self->_priv->bg.blue;
+ selfp->fg.red = normal_color.red > 0.5 ? 0 : G_MAXUINT16;
+ selfp->fg.green = normal_color.green > 0.5 ? 0 : G_MAXUINT16;
+ selfp->fg.blue = normal_color.blue > 0.5 ? 0 : G_MAXUINT16;
/* try hard to get a transparent foreground anyway */
- self->_priv->fg_alpha = G_MAXUINT16;
+ selfp->fg_alpha = 0;
+
+ /*
+ * The widget might be styled after the text has been set on it,
+ * we must recreate the Pango attributes.
+ */
+ if (selfp->string.len > 0) {
+ PangoAttrList *attribs = NULL;
+ g_autofree gchar *plaintext = NULL;
+
+ self_parse_string(selfp->string.data, selfp->string.len,
+ &selfp->fg, selfp->fg_alpha,
+ &selfp->bg, selfp->bg_alpha,
+ &attribs, &plaintext);
+
+ gtk_label_set_attributes(GTK_LABEL(self), attribs);
+ pango_attr_list_unref(attribs);
+
+ g_assert(!g_strcmp0(plaintext, gtk_label_get_text(GTK_LABEL(self))));
+ }
}
public GtkWidget *
- new(const gchar *str)
+ new(const gchar *str, gssize len)
{
Self *widget = GET_NEW;
- self_set_text(widget, str);
+ self_set_text(widget, str, len);
return GTK_WIDGET(widget);
}
@@ -102,6 +120,11 @@ class Gtk:Canonicalized:Label from Gtk:Label {
{
PangoAttribute *attr;
+ /*
+ * NOTE: Transparent foreground do not seem to work,
+ * 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;
@@ -131,28 +154,27 @@ class Gtk:Canonicalized:Label from Gtk:Label {
Pango:Color *bg, guint16 bg_alpha,
Pango:Attr:List **attribs, gchar **text)
{
- gsize text_len = 1; /* for trailing 0 */
- gint index = 0;
-
if (len < 0)
len = strlen(str);
/*
* Approximate size of unformatted text.
*/
+ gsize text_len = 1; /* for trailing 0 */
for (gint i = 0; i < len; i++)
- text_len += IS_CTL(str[i]) ? 3 : 1;
+ text_len += TECO_IS_CTL(str[i]) ? 3 : 1;
*attribs = pango_attr_list_new();
*text = g_malloc(text_len);
+ gint index = 0;
while (len > 0) {
/*
* NOTE: This mapping is similar to
- * View::set_presentations()
+ * teco_view_set_presentations()
*/
switch (*str) {
- case CTL_KEY_ESC:
+ case '\e':
self_add_highlight_attribs(*attribs,
fg, fg_alpha,
bg, bg_alpha,
@@ -185,13 +207,13 @@ class Gtk:Canonicalized:Label from Gtk:Label {
(*text)[index++] = 'B';
break;
default:
- if (IS_CTL(*str)) {
+ if (TECO_IS_CTL(*str)) {
self_add_highlight_attribs(*attribs,
fg, fg_alpha,
bg, bg_alpha,
index, 2);
(*text)[index++] = '^';
- (*text)[index++] = CTL_ECHO(*str);
+ (*text)[index++] = TECO_CTL_ECHO(*str);
} else {
(*text)[index++] = *str;
}
@@ -207,20 +229,25 @@ class Gtk:Canonicalized:Label from Gtk:Label {
}
public void
- set_text(self, const gchar *str)
+ set_text(self, const gchar *str, gssize len)
{
- PangoAttrList *attribs = NULL;
- gchar *plaintext = NULL;
+ teco_string_clear(&selfp->string);
+ teco_string_init(&selfp->string, str, len < 0 ? strlen(str) : len);
+
+ g_autofree gchar *plaintext = NULL;
- if (str)
- self_parse_string(str, -1,
- &self->_priv->fg, self->_priv->fg_alpha,
- &self->_priv->bg, self->_priv->bg_alpha,
+ if (selfp->string.len > 0) {
+ PangoAttrList *attribs = NULL;
+
+ self_parse_string(selfp->string.data, selfp->string.len,
+ &selfp->fg, selfp->fg_alpha,
+ &selfp->bg, selfp->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);
- g_free(plaintext);
}
}
diff --git a/src/interface.c b/src/interface.c
new file mode 100644
index 0000000..21a83ff
--- /dev/null
+++ b/src/interface.c
@@ -0,0 +1,120 @@
+/*
+ * 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 <stdarg.h>
+#include <stdio.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+#include <SciLexer.h>
+
+#include "sciteco.h"
+#include "undo.h"
+#include "view.h"
+#include "interface.h"
+
+//#define DEBUG
+
+teco_view_t *teco_interface_current_view = NULL;
+
+TECO_DEFINE_UNDO_CALL(teco_interface_show_view, teco_view_t *);
+TECO_DEFINE_UNDO_CALL(teco_interface_ssm, unsigned int, uptr_t, sptr_t);
+TECO_DEFINE_UNDO_CALL(teco_interface_info_update_qreg, const teco_qreg_t *);
+TECO_DEFINE_UNDO_CALL(teco_interface_info_update_buffer, const teco_buffer_t *);
+
+typedef struct {
+ teco_string_t str;
+ gchar name[];
+} teco_undo_set_clipboard_t;
+
+static void
+teco_undo_set_clipboard_action(teco_undo_set_clipboard_t *ctx, gboolean run)
+{
+ if (run)
+ teco_interface_set_clipboard(ctx->name, ctx->str.data, ctx->str.len, NULL);
+ teco_string_clear(&ctx->str);
+}
+
+/**
+ * Set the clipboard upon rubout.
+ *
+ * This passes ownership of the clipboard content string
+ * to the undo token object.
+ */
+void
+teco_interface_undo_set_clipboard(const gchar *name, gchar *str, gsize len)
+{
+ teco_undo_set_clipboard_t *ctx;
+
+ ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_set_clipboard_action,
+ sizeof(*ctx) + strlen(name) + 1);
+ if (ctx) {
+ ctx->str.data = str;
+ ctx->str.len = len;
+ strcpy(ctx->name, name);
+ } else {
+ g_free(str);
+ }
+}
+
+/**
+ * Print a 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`.
+ */
+void
+teco_interface_stdio_vmsg(teco_msg_t type, const gchar *fmt, va_list ap)
+{
+ FILE *stream = stdout;
+
+ switch (type) {
+ case TECO_MSG_USER:
+ break;
+ case TECO_MSG_INFO:
+ fputs("Info: ", stream);
+ break;
+ case TECO_MSG_WARNING:
+ stream = stderr;
+ fputs("Warning: ", stream);
+ break;
+ case TECO_MSG_ERROR:
+ stream = stderr;
+ fputs("Error: ", stream);
+ break;
+ }
+
+ g_vfprintf(stream, fmt, ap);
+ fputc('\n', stream);
+}
+
+void
+teco_interface_process_notify(struct SCNotification *notify)
+{
+#ifdef DEBUG
+ g_printf("SCINTILLA NOTIFY: code=%d\n", notify->nmhdr.code);
+#endif
+}
diff --git a/src/interface.cpp b/src/interface.cpp
deleted file mode 100644
index cc8b069..0000000
--- a/src/interface.cpp
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <stdarg.h>
-#include <stdio.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include <Scintilla.h>
-#include <SciLexer.h>
-
-#include "sciteco.h"
-#include "interface.h"
-
-namespace SciTECO {
-
-template <class ViewImpl>
-void
-View<ViewImpl>::set_representations(void)
-{
- 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'};
- ssm(SCI_SETREPRESENTATION, (uptr_t)buf, (sptr_t)reps[cc]);
- }
-}
-
-template <class ViewImpl>
-void
-View<ViewImpl>::setup(void)
-{
- /*
- * Start with or without undo collection,
- * depending on undo.enabled.
- */
- ssm(SCI_SETUNDOCOLLECTION, undo.enabled);
-
- ssm(SCI_SETFOCUS, TRUE);
-
- /*
- * Some Scintilla implementations show the horizontal
- * scroll bar by default.
- * Ensure it is never displayed by default.
- */
- ssm(SCI_SETHSCROLLBAR, FALSE);
-
- /*
- * Only margin 1 is given a width by default.
- * To provide a minimalist default view, it is disabled.
- */
- ssm(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).
- */
- ssm(SCI_SETCARETSTYLE, CARETSTYLE_BLOCK);
- ssm(SCI_SETCARETPERIOD, 0);
- ssm(SCI_SETCARETFORE, 0xFFFFFF);
-
- ssm(SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF);
- ssm(SCI_STYLESETBACK, STYLE_DEFAULT, 0x000000);
- ssm(SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)"Courier");
- ssm(SCI_STYLECLEARALL);
-
- /*
- * FIXME: The line number background is apparently not
- * affected by SCI_STYLECLEARALL
- */
- ssm(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).
- */
- ssm(SCI_STYLESETFORE, STYLE_CALLTIP, 0x000000);
- ssm(SCI_STYLESETBACK, STYLE_CALLTIP, 0xFFFFFF);
-}
-
-template class View<ViewCurrent>;
-
-template <class InterfaceImpl, class ViewImpl>
-void
-Interface<InterfaceImpl, ViewImpl>::UndoTokenShowView::run(void)
-{
- /*
- * Implementing this here allows us to reference
- * `interface`
- */
- interface.show_view(view);
-}
-
-template <class InterfaceImpl, class ViewImpl>
-template <class Type>
-void
-Interface<InterfaceImpl, ViewImpl>::UndoTokenInfoUpdate<Type>::run(void)
-{
- interface.info_update(obj);
-}
-
-/**
- * Print a 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`.
- */
-template <class InterfaceImpl, class ViewImpl>
-void
-Interface<InterfaceImpl, ViewImpl>::stdio_vmsg(MessageType type, const gchar *fmt, va_list ap)
-{
- FILE *stream = stdout;
-
- switch (type) {
- case MSG_USER:
- break;
- case MSG_INFO:
- fputs("Info: ", stream);
- break;
- case MSG_WARNING:
- stream = stderr;
- fputs("Warning: ", stream);
- break;
- case MSG_ERROR:
- stream = stderr;
- fputs("Error: ", stream);
- break;
- }
-
- g_vfprintf(stream, fmt, ap);
- fputc('\n', stream);
-}
-
-template <class InterfaceImpl, class ViewImpl>
-void
-Interface<InterfaceImpl, ViewImpl>::process_notify(SCNotification *notify)
-{
-#ifdef DEBUG
- g_printf("SCINTILLA NOTIFY: code=%d\n", notify->nmhdr.code);
-#endif
-}
-
-template class Interface<InterfaceCurrent, ViewCurrent>;
-
-} /* namespace SciTECO */
diff --git a/src/interface.h b/src/interface.h
index 607a42c..c396225 100644
--- a/src/interface.h
+++ b/src/interface.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,9 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __INTERFACE_H
-#define __INTERFACE_H
+#pragma once
#include <stdarg.h>
#include <signal.h>
@@ -25,349 +23,151 @@
#include <Scintilla.h>
-#include "memory.h"
-#include "undo.h"
-#include "error.h"
-
-namespace SciTECO {
-
-/* avoid include dependency conflict */
-class QRegister;
-class Buffer;
-class Cmdline;
-extern sig_atomic_t sigint_occurred;
+#include "sciteco.h"
+#include "qreg.h"
+#include "ring.h"
+#include "cmdline.h"
+#include "view.h"
/**
- * Base class for all SciTECO views. This is a minimal
- * abstraction where the implementor only has to provide
- * a method for dispatching Scintilla messages.
- * Everything else is handled by other SciTECO classes.
+ * @file
+ * Abstract user interface.
*
- * This interface employs the Curiously Recurring Template
- * Pattern (CRTP). To implement it, one must derive from
- * View<DerivedClass>. The methods to implement actually
- * have an "_impl" suffix so as to avoid infinite recursion
- * if an implementation is missing.
- * Externally however, the methods as given in this interface
- * may be called.
+ * Interface of all SciTECO user interfaces (e.g. Curses or GTK+).
+ * All functions that must be provided are marked with the \@pure tag.
*
- * The CRTP has a runtime overhead at low optimization levels
- * (additional non-inlined calls), but should provide a
- * significant performance boost when inlining is enabled.
- *
- * Note that not all methods have to be defined in the
- * class. Explicit template instantiation is used to outsource
- * base-class implementations to interface.cpp.
+ * @note
+ * We do not provide default implementations for any of the interface
+ * functions by declaring them "weak" since this is a non-portable linker
+ * feature.
*/
-template <class ViewImpl>
-class View : public Object {
- inline ViewImpl &
- impl(void)
- {
- return *(ViewImpl *)this;
- }
-
- class UndoTokenMessage : public UndoToken {
- ViewImpl &view;
- unsigned int iMessage;
- uptr_t wParam;
- sptr_t lParam;
+/** @protected */
+extern teco_view_t *teco_interface_current_view;
- public:
- UndoTokenMessage(ViewImpl &_view, unsigned int _iMessage,
- uptr_t _wParam = 0, sptr_t _lParam = 0)
- : view(_view), iMessage(_iMessage),
- wParam(_wParam), lParam(_lParam) {}
+/** @pure */
+void teco_interface_init(void);
- void
- run(void)
- {
- view.ssm(iMessage, wParam, lParam);
- }
- };
+/** @pure */
+GOptionGroup *teco_interface_get_options(void);
- class UndoTokenSetRepresentations : public UndoToken {
- ViewImpl &view;
+/** @pure makes sense only on Curses */
+void teco_interface_init_color(guint color, guint32 rgb);
- public:
- UndoTokenSetRepresentations(ViewImpl &_view)
- : view(_view) {}
+typedef enum {
+ TECO_MSG_USER,
+ TECO_MSG_INFO,
+ TECO_MSG_WARNING,
+ TECO_MSG_ERROR
+} teco_msg_t;
- void
- run(void)
- {
- view.set_representations();
- }
- };
+/** @pure */
+void teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap);
-public:
- /*
- * called after Interface initialization.
- * Should call setup()
- */
- inline void
- initialize(void)
- {
- impl().initialize_impl();
- }
+static inline void G_GNUC_PRINTF(2, 3)
+teco_interface_msg(teco_msg_t type, const gchar *fmt, ...)
+{
+ va_list ap;
- inline sptr_t
- ssm(unsigned int iMessage,
- uptr_t wParam = 0, sptr_t lParam = 0)
- {
- return impl().ssm_impl(iMessage, wParam, lParam);
- }
+ va_start(ap, fmt);
+ teco_interface_vmsg(type, fmt, ap);
+ va_end(ap);
+}
- inline void
- undo_ssm(unsigned int iMessage,
- uptr_t wParam = 0, sptr_t lParam = 0)
- {
- undo.push<UndoTokenMessage>(impl(), iMessage, wParam, lParam);
- }
+/** @pure */
+void teco_interface_msg_clear(void);
- void set_representations(void);
- inline void
- undo_set_representations(void)
- {
- undo.push<UndoTokenSetRepresentations>(impl());
- }
+/** @pure */
+void teco_interface_show_view(teco_view_t *view);
+void undo__teco_interface_show_view(teco_view_t *);
- inline void
- set_scintilla_undo(bool state)
- {
- ssm(SCI_EMPTYUNDOBUFFER);
- ssm(SCI_SETUNDOCOLLECTION, state);
- }
+static inline sptr_t
+teco_interface_ssm(unsigned int iMessage, uptr_t wParam, sptr_t lParam)
+{
+ return teco_view_ssm(teco_interface_current_view, iMessage, wParam, lParam);
+}
-protected:
- void setup(void);
-};
-
-/**
- * Base class and interface of all SciTECO user interfaces
- * (e.g. Curses or GTK+).
- *
- * This uses the same Curiously Recurring Template Pattern (CRTP)
- * as the View interface above, as there is only one type of
- * user interface at runtime.
+/*
+ * NOTE: You could simply call undo__teco_view_ssm(teco_interface_current_view, ...).
+ * undo__teco_interface_ssm(...) exists for brevity and aestethics.
*/
-template <class InterfaceImpl, class ViewImpl>
-class Interface : public Object {
- inline InterfaceImpl &
- impl(void)
- {
- return *(InterfaceImpl *)this;
- }
-
- class UndoTokenShowView : public UndoToken {
- ViewImpl *view;
-
- public:
- UndoTokenShowView(ViewImpl *_view)
- : view(_view) {}
-
- void run(void);
- };
-
- template <class Type>
- class UndoTokenInfoUpdate : public UndoToken {
- const Type *obj;
-
- public:
- UndoTokenInfoUpdate(const Type *_obj)
- : obj(_obj) {}
-
- void run(void);
- };
-
-protected:
- ViewImpl *current_view;
-
-public:
- Interface() : current_view(NULL) {}
-
- /* default implementation */
- inline GOptionGroup *
- get_options(void)
- {
- return NULL;
- }
-
- /* default implementation */
- inline void init(void) {}
-
- /* makes sense only on Curses */
- inline void init_color(guint color, guint32 rgb) {}
+void undo__teco_interface_ssm(unsigned int, uptr_t, sptr_t);
- enum MessageType {
- MSG_USER,
- MSG_INFO,
- MSG_WARNING,
- MSG_ERROR
- };
- inline void
- vmsg(MessageType type, const gchar *fmt, va_list ap)
- {
- impl().vmsg_impl(type, fmt, ap);
- }
- inline void
- msg(MessageType type, const gchar *fmt, ...) G_GNUC_PRINTF(3, 4)
- {
- va_list ap;
+/** @pure */
+void teco_interface_info_update_qreg(const teco_qreg_t *reg);
+/** @pure */
+void teco_interface_info_update_buffer(const teco_buffer_t *buffer);
- va_start(ap, fmt);
- vmsg(type, fmt, ap);
- va_end(ap);
- }
- /* default implementation */
- inline void msg_clear(void) {}
+#define teco_interface_info_update(X) \
+ (_Generic((X), teco_qreg_t * : teco_interface_info_update_qreg, \
+ const teco_qreg_t * : teco_interface_info_update_qreg, \
+ teco_buffer_t * : teco_interface_info_update_buffer, \
+ const teco_buffer_t * : teco_interface_info_update_buffer)(X))
- inline void
- show_view(ViewImpl *view)
- {
- impl().show_view_impl(view);
- }
- inline void
- undo_show_view(ViewImpl *view)
- {
- undo.push<UndoTokenShowView>(view);
- }
+void undo__teco_interface_info_update_qreg(const teco_qreg_t *);
+void undo__teco_interface_info_update_buffer(const teco_buffer_t *);
- inline ViewImpl *
- get_current_view(void)
- {
- return current_view;
- }
+/** @pure */
+void teco_interface_cmdline_update(const teco_cmdline_t *cmdline);
- inline sptr_t
- ssm(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0)
- {
- return current_view->ssm(iMessage, wParam, lParam);
- }
- inline void
- undo_ssm(unsigned int iMessage,
- uptr_t wParam = 0, sptr_t lParam = 0)
- {
- current_view->undo_ssm(iMessage, wParam, lParam);
- }
-
- /*
- * NOTE: could be rolled into a template, but
- * this way it is explicit what must be implemented
- * by the deriving class.
- */
- inline void
- info_update(const QRegister *reg)
- {
- impl().info_update_impl(reg);
- }
- inline void
- info_update(const Buffer *buffer)
- {
- impl().info_update_impl(buffer);
- }
-
- inline void
- undo_info_update(const QRegister *reg)
- {
- undo.push<UndoTokenInfoUpdate<QRegister>>(reg);
- }
- inline void
- undo_info_update(const Buffer *buffer)
- {
- undo.push<UndoTokenInfoUpdate<Buffer>>(buffer);
- }
-
- inline void
- cmdline_update(const Cmdline *cmdline)
- {
- impl().cmdline_update_impl(cmdline);
- }
-
- /* default implementation */
- inline void
- set_clipboard(const gchar *name,
- const gchar *str = NULL, gssize str_len = -1)
- {
- throw Error("Setting clipboard unsupported");
- }
-
- /* default implementation */
- inline gchar *
- get_clipboard(const gchar *name, gsize *str_len = NULL)
- {
- throw Error("Getting clipboard unsupported");
- }
-
- enum PopupEntryType {
- POPUP_PLAIN,
- POPUP_FILE,
- POPUP_DIRECTORY
- };
- inline void
- popup_add(PopupEntryType type,
- const gchar *name, bool highlight = false)
- {
- impl().popup_add_impl(type, name, highlight);
- }
- inline void
- popup_show(void)
- {
- impl().popup_show_impl();
- }
- inline bool
- popup_is_shown(void)
- {
- return impl().popup_is_shown_impl();
- }
- inline void
- popup_clear(void)
- {
- impl().popup_clear_impl();
- }
-
- /* default implementation */
- inline bool
- is_interrupted(void)
- {
- return sigint_occurred != FALSE;
- }
-
- /* main entry point */
- inline void
- event_loop(void)
- {
- impl().event_loop_impl();
- }
-
- /*
- * Interfacing to the external SciTECO world
- */
-protected:
- void stdio_vmsg(MessageType type, const gchar *fmt, va_list ap);
-public:
- void process_notify(SCNotification *notify);
-};
-
-} /* namespace SciTECO */
-
-#ifdef INTERFACE_GTK
-#include "interface-gtk/interface-gtk.h"
-#elif defined(INTERFACE_CURSES)
-#include "interface-curses/interface-curses.h"
-#else
-#error No interface selected!
-#endif
-
-namespace SciTECO {
+/** @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);
+/**
+ * Semantics are compatible with teco_qreg_vtable_t::get_string() since that is the
+ * main user of this function.
+ *
+ * @pure
+ */
+gboolean teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error);
+
+typedef enum {
+ TECO_POPUP_PLAIN,
+ TECO_POPUP_FILE,
+ TECO_POPUP_DIRECTORY
+} teco_popup_entry_type_t;
+
+/** @pure */
+void teco_interface_popup_add(teco_popup_entry_type_t type,
+ const gchar *name, gsize name_len, gboolean highlight);
+/** @pure */
+void teco_interface_popup_show(void);
+/** @pure */
+gboolean teco_interface_popup_is_shown(void);
+/** @pure */
+void teco_interface_popup_clear(void);
+
+/** @pure */
+gboolean teco_interface_is_interrupted(void);
+
+/** @pure main entry point */
+gboolean teco_interface_event_loop(GError **error);
-/* object defined in main.cpp */
-extern InterfaceCurrent interface;
+/*
+ * 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_process_notify(struct SCNotification *notify);
-extern template class View<ViewCurrent>;
-extern template class Interface<InterfaceCurrent, ViewCurrent>;
+/** @pure */
+void teco_interface_cleanup(void);
-} /* namespace SciTECO */
+/*
+ * The following functions are here for lack of a better place.
+ * They could also be in sciteco.h, but only if declared as non-inline
+ * since sciteco.h should not depend on interface.h.
+ */
-#endif
+static inline gboolean
+teco_validate_pos(teco_int_t n)
+{
+ return 0 <= n && n <= teco_interface_ssm(SCI_GETLENGTH, 0, 0);
+}
+
+static inline gboolean
+teco_validate_line(teco_int_t n)
+{
+ return 0 <= n && n < teco_interface_ssm(SCI_GETLINECOUNT, 0, 0);
+}
diff --git a/src/ioview.cpp b/src/ioview.cpp
deleted file mode 100644
index 383b9bb..0000000
--- a/src/ioview.cpp
+++ /dev/null
@@ -1,512 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include <Scintilla.h>
-
-#include "sciteco.h"
-#include "interface.h"
-#include "undo.h"
-#include "error.h"
-#include "qregisters.h"
-#include "eol.h"
-#include "ioview.h"
-
-#ifdef HAVE_WINDOWS_H
-/* here it shouldn't cause conflicts with other headers */
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#endif
-
-namespace SciTECO {
-
-#ifdef G_OS_WIN32
-
-typedef DWORD FileAttributes;
-/* INVALID_FILE_ATTRIBUTES already defined */
-
-static inline FileAttributes
-get_file_attributes(const gchar *filename)
-{
- return GetFileAttributes((LPCTSTR)filename);
-}
-
-static inline void
-set_file_attributes(const gchar *filename, FileAttributes attrs)
-{
- SetFileAttributes((LPCTSTR)filename, attrs);
-}
-
-#else
-
-typedef int FileAttributes;
-#define INVALID_FILE_ATTRIBUTES (-1)
-
-static inline FileAttributes
-get_file_attributes(const gchar *filename)
-{
- struct stat buf;
-
- return g_stat(filename, &buf) ? INVALID_FILE_ATTRIBUTES : buf.st_mode;
-}
-
-static inline void
-set_file_attributes(const gchar *filename, FileAttributes attrs)
-{
- g_chmod(filename, attrs);
-}
-
-#endif /* !G_OS_WIN32 */
-
-/**
- * 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 channel Channel to read from.
- */
-void
-IOView::load(GIOChannel *channel)
-{
- GStatBuf stat_buf;
-
- EOLReaderGIO reader(channel);
-
- ssm(SCI_BEGINUNDOACTION);
- ssm(SCI_CLEARALL);
-
- /*
- * 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.
- */
- stat_buf.st_size = 0;
- if (!fstat(g_io_channel_unix_get_fd(channel), &stat_buf) &&
- stat_buf.st_size > 0)
- ssm(SCI_ALLOCATE, stat_buf.st_size);
-
- try {
- const gchar *data;
- gsize data_len;
-
- while ((data = reader.convert(data_len)))
- ssm(SCI_APPENDTEXT, data_len, (sptr_t)data);
- } catch (...) {
- ssm(SCI_ENDUNDOACTION);
- throw; /* forward */
- }
-
- /*
- * 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)
- ssm(SCI_SETEOLMODE, reader.eol_style);
-
- if (reader.eol_style_inconsistent)
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Inconsistent EOL styles normalized");
-
- ssm(SCI_ENDUNDOACTION);
-}
-
-/**
- * Load view's document from file.
- */
-void
-IOView::load(const gchar *filename)
-{
- GError *error = NULL;
- GIOChannel *channel;
-
- channel = g_io_channel_new_file(filename, "r", &error);
- if (!channel) {
- Error err("Error opening file \"%s\" for reading: %s",
- filename, error->message);
- g_error_free(error);
- throw err;
- }
-
- /*
- * 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);
-
- try {
- load(channel);
- } catch (Error &e) {
- Error err("Error reading file \"%s\": %s",
- filename, e.description);
- g_io_channel_unref(channel);
- throw err;
- }
-
- /* also closes file: */
- g_io_channel_unref(channel);
-}
-
-#if 0
-
-/*
- * TODO: on UNIX it may be better to open() the current file, unlink() it
- * and keep the file descriptor in the UndoToken.
- * 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;
-
-class UndoTokenRestoreSavePoint : public UndoToken {
- gchar *savepoint;
- gchar *filename;
-
-#ifdef G_OS_WIN32
- FileAttributes orig_attrs;
-#endif
-
-public:
- UndoTokenRestoreSavePoint(gchar *_savepoint, const gchar *_filename)
- : savepoint(_savepoint), filename(g_strdup(_filename))
- {
-#ifdef G_OS_WIN32
- orig_attrs = get_file_attributes(filename);
- if (orig_attrs != INVALID_FILE_ATTRIBUTES)
- set_file_attributes(savepoint,
- orig_attrs | FILE_ATTRIBUTE_HIDDEN);
-#endif
- }
-
- ~UndoTokenRestoreSavePoint()
- {
- if (savepoint) {
- g_unlink(savepoint);
- g_free(savepoint);
- }
- g_free(filename);
-
- savepoint_id--;
- }
-
- void
- run(void)
- {
- if (!g_rename(savepoint, filename)) {
- g_free(savepoint);
- savepoint = NULL;
-#ifdef G_OS_WIN32
- if (orig_attrs != INVALID_FILE_ATTRIBUTES)
- set_file_attributes(filename,
- orig_attrs);
-#endif
- } else {
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Unable to restore save point file \"%s\"",
- savepoint);
- }
- }
-};
-
-static void
-make_savepoint(const gchar *filename)
-{
- gchar *dirname, *basename, *savepoint;
- gchar savepoint_basename[FILENAME_MAX];
-
- basename = g_path_get_basename(filename);
- g_snprintf(savepoint_basename, sizeof(savepoint_basename),
- ".teco-%d-%s~", savepoint_id, basename);
- g_free(basename);
- dirname = g_path_get_dirname(filename);
- savepoint = g_build_filename(dirname, savepoint_basename, NIL);
- g_free(dirname);
-
- if (g_rename(filename, savepoint)) {
- interface.msg(InterfaceCurrent::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.
- */
- undo.push_own<UndoTokenRestoreSavePoint>(savepoint, filename);
-}
-
-#endif
-
-void
-IOView::save(GIOChannel *channel)
-{
- EOLWriterGIO writer(channel, ssm(SCI_GETEOLMODE));
- sptr_t gap;
- gsize size;
- const gchar *buffer;
- gsize bytes_written;
-
- /* write part of buffer before gap */
- gap = ssm(SCI_GETGAPPOSITION);
- if (gap > 0) {
- buffer = (const gchar *)ssm(SCI_GETRANGEPOINTER, 0, gap);
- bytes_written = writer.convert(buffer, gap);
- g_assert(bytes_written == (gsize)gap);
- }
-
- /* write part of buffer after gap */
- size = ssm(SCI_GETLENGTH) - gap;
- if (size > 0) {
- buffer = (const gchar *)ssm(SCI_GETRANGEPOINTER, gap, (sptr_t)size);
- bytes_written = writer.convert(buffer, size);
- g_assert(bytes_written == size);
- }
-}
-
-void
-IOView::save(const gchar *filename)
-{
- GError *error = NULL;
- GIOChannel *channel;
-
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
- GStatBuf file_stat;
- file_stat.st_uid = -1;
- file_stat.st_gid = -1;
-#endif
- FileAttributes attributes = INVALID_FILE_ATTRIBUTES;
-
- if (undo.enabled) {
- if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
- g_stat(filename, &file_stat);
-#endif
- attributes = get_file_attributes(filename);
- make_savepoint(filename);
- } else {
- undo.push<UndoTokenRemoveFile>(filename);
- }
- }
-
- /* leaves access mode intact if file still exists */
- channel = g_io_channel_new_file(filename, "w", &error);
- if (!channel)
- throw GlibError(error);
-
- /*
- * save(GIOChannel *, const gchar *) expects a buffered
- * and blocking channel
- */
- g_io_channel_set_encoding(channel, NULL, NULL);
- g_io_channel_set_buffered(channel, TRUE);
-
- try {
- save(channel);
- } catch (Error &e) {
- Error err("Error writing file \"%s\": %s", filename, e.description);
- g_io_channel_unref(channel);
- throw err;
- }
-
- /* if file existed but has been renamed, restore attributes */
- if (attributes != INVALID_FILE_ATTRIBUTES)
- set_file_attributes(filename, attributes);
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
- /*
- * 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))
- interface.msg(InterfaceCurrent::MSG_WARNING,
- "Unable to preserve owner of \"%s\": %s",
- filename, g_strerror(errno));
-#endif
-
- /* also closes file */
- g_io_channel_unref(channel);
-}
-
-/*
- * Auxiliary functions
- */
-
-/**
- * Perform tilde expansion on a file name or path.
- *
- * This supports only strings with a "~" prefix.
- * A user name after "~" is not supported.
- * The $HOME environment variable/register is used to retrieve
- * the current user's home directory.
- */
-gchar *
-expand_path(const gchar *path)
-{
- gchar *home, *ret;
-
- if (!path)
- path = "";
-
- if (path[0] != '~' || (path[1] && !G_IS_DIR_SEPARATOR(path[1])))
- return g_strdup(path);
-
- /*
- * $HOME should not have a trailing directory separator since
- * it is canonicalized to an absolute path at startup,
- * but this ensures that a proper path is constructed even if
- * it does (e.g. $HOME is changed later on).
- */
- home = QRegisters::globals["$HOME"]->get_string();
- ret = g_build_filename(home, path+1, NIL);
- g_free(home);
-
- return ret;
-}
-
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
-
-gchar *
-get_absolute_path(const gchar *path)
-{
- gchar buf[PATH_MAX];
- gchar *resolved;
-
- if (!path)
- return NULL;
-
- if (realpath(path, buf)) {
- resolved = g_strdup(buf);
- } else if (g_path_is_absolute(path)) {
- resolved = g_strdup(path);
- } else {
- gchar *cwd = g_get_current_dir();
- resolved = g_build_filename(cwd, path, NIL);
- g_free(cwd);
- }
-
- return resolved;
-}
-
-bool
-file_is_visible(const gchar *path)
-{
- gchar *basename = g_path_get_basename(path);
- bool ret = *basename != '.';
-
- g_free(basename);
- return ret;
-}
-
-#elif defined(G_OS_WIN32)
-
-gchar *
-get_absolute_path(const gchar *path)
-{
- TCHAR buf[MAX_PATH];
- gchar *resolved = NULL;
-
- if (path && GetFullPathName(path, sizeof(buf), buf, NULL))
- resolved = g_strdup(buf);
-
- return resolved;
-}
-
-bool
-file_is_visible(const gchar *path)
-{
- return !(get_file_attributes(path) & FILE_ATTRIBUTE_HIDDEN);
-}
-
-#else /* !G_OS_UNIX && !G_OS_HAIKU && !G_OS_WIN32 */
-
-/*
- * This will never canonicalize relative paths.
- * I.e. the absolute path will often contain
- * relative components, even if `path` exists.
- * The only exception would be a simple filename
- * not containing any "..".
- */
-gchar *
-get_absolute_path(const gchar *path)
-{
- gchar *resolved;
-
- if (!path)
- return NULL;
-
- if (g_path_is_absolute(path)) {
- resolved = g_strdup(path);
- } else {
- gchar *cwd = g_get_current_dir();
- resolved = g_build_filename(cwd, path, NIL);
- g_free(cwd);
- }
-
- return resolved;
-}
-
-/*
- * There's no platform-independent way to determine if a file
- * is visible/hidden, so we just assume that all files are
- * visible.
- */
-bool
-file_is_visible(const gchar *path)
-{
- return true;
-}
-
-#endif /* !G_OS_UNIX && !G_OS_HAIKU && !G_OS_WIN32 */
-
-} /* namespace SciTECO */
diff --git a/src/list.h b/src/list.h
new file mode 100644
index 0000000..7249f9f
--- /dev/null
+++ b/src/list.h
@@ -0,0 +1,94 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+typedef struct teco_stailq_entry_t {
+ struct teco_stailq_entry_t *next;
+} teco_stailq_entry_t;
+
+typedef struct {
+ /** Pointer to the first element or NULL */
+ teco_stailq_entry_t *first;
+ /** Pointer to the last element's `next` field or the head's `first` field */
+ teco_stailq_entry_t **last;
+} teco_stailq_head_t;
+
+#define TECO_STAILQ_HEAD_INITIALIZER(HEAD) ((teco_stailq_head_t){NULL, &(HEAD)->first})
+
+static inline void
+teco_stailq_insert_tail(teco_stailq_head_t *head, teco_stailq_entry_t *entry)
+{
+ entry->next = NULL;
+ *head->last = entry;
+ head->last = &entry->next;
+}
+
+static inline teco_stailq_entry_t *
+teco_stailq_remove_head(teco_stailq_head_t *head)
+{
+ teco_stailq_entry_t *first = head->first;
+ if (first && !(head->first = first->next))
+ head->last = &head->first;
+ return first;
+}
+
+/** Can be both a tail queue head or an entry (tail queue element). */
+typedef union teco_tailq_entry_t {
+ struct {
+ /** Pointer to the next entry or NULL */
+ union teco_tailq_entry_t *next;
+ /** Pointer to the previous entry or to the queue head */
+ union teco_tailq_entry_t *prev;
+ };
+
+ struct {
+ /** Pointer to the first entry or NULL */
+ union teco_tailq_entry_t *first;
+ /** Pointer to the last entry or to the queue head */
+ union teco_tailq_entry_t *last;
+ };
+} teco_tailq_entry_t;
+
+#define TECO_TAILQ_HEAD_INITIALIZER(HEAD) ((teco_tailq_entry_t){.first = NULL, .last = (HEAD)})
+
+static inline void
+teco_tailq_insert_before(teco_tailq_entry_t *entry_a, teco_tailq_entry_t *entry_b)
+{
+ entry_b->prev = entry_a->prev;
+ entry_b->next = entry_a;
+ entry_a->prev->next = entry_b;
+ entry_a->prev = entry_b;
+}
+
+static inline void
+teco_tailq_insert_tail(teco_tailq_entry_t *head, teco_tailq_entry_t *entry)
+{
+ entry->next = NULL;
+ entry->prev = head->last;
+ head->last->next = entry;
+ head->last = entry;
+}
+
+static inline void
+teco_tailq_remove(teco_tailq_entry_t *head, teco_tailq_entry_t *entry)
+{
+ if (entry->next)
+ entry->next->prev = entry->prev;
+ else
+ head->last = entry->prev;
+ entry->prev->next = entry->next;
+}
diff --git a/src/main.cpp b/src/main.c
index e7c87d4..3d149d4 100644
--- a/src/main.cpp
+++ b/src/main.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -29,12 +29,12 @@
#include <glib/gstdio.h>
#include "sciteco.h"
+#include "file-utils.h"
#include "cmdline.h"
#include "interface.h"
-#include "ioview.h"
#include "parser.h"
#include "goto.h"
-#include "qregisters.h"
+#include "qreg.h"
#include "ring.h"
#include "undo.h"
#include "error.h"
@@ -48,47 +48,16 @@
*/
//#define DEBUG_PAUSE
-namespace SciTECO {
-
#define INI_FILE ".teco_ini"
-/*
- * defining the global objects here ensures
- * a ctor/dtor order without depending on the
- * GCC init_priority() attribute
- */
-InterfaceCurrent interface;
-IOView QRegisters::view;
-
-/*
- * Scintilla will be initialized after these
- * ctors (in main()), but dtors are guaranteed
- * to be executed before Scintilla's
- * destruction
- */
-QRegisterTable QRegisters::globals;
-Ring ring;
-
-namespace Flags {
- tecoInt ed = ED_AUTOEOL;
-}
-
-static gchar *eval_macro = NULL;
-static gboolean mung_file = FALSE;
-static gboolean mung_profile = TRUE;
+teco_int_t teco_ed = TECO_ED_AUTOEOL;
-sig_atomic_t sigint_occurred = FALSE;
+volatile sig_atomic_t teco_sigint_occurred = FALSE;
-extern "C" {
-
-static void sigint_handler(int signal);
-
-} /* extern "C" */
-
-#if defined(G_OS_UNIX) || defined(G_OS_HAIKU)
+#ifdef G_OS_UNIX
void
-interrupt(void)
+teco_interrupt(void)
{
/*
* This sends SIGINT to the entire process group,
@@ -96,34 +65,23 @@ interrupt(void)
* even when called from the wrong thread.
*/
if (kill(0, SIGINT))
- sigint_occurred = TRUE;
+ teco_sigint_occurred = TRUE;
}
-#else /* !G_OS_UNIX && !G_OS_HAIKU */
+#else /* !G_OS_UNIX */
void
-interrupt(void)
+teco_interrupt(void)
{
if (raise(SIGINT))
- sigint_occurred = TRUE;
+ teco_sigint_occurred = TRUE;
}
#endif
-const gchar *
-get_eol_seq(gint eol_mode)
-{
- switch (eol_mode) {
- case SC_EOL_CRLF:
- return "\r\n";
- case SC_EOL_CR:
- return "\r";
- case SC_EOL_LF:
- default:
- return "\n";
- }
-}
-
+/*
+ * FIXME: Move this into file-utils.c?
+ */
#ifdef G_OS_WIN32
/*
@@ -133,60 +91,59 @@ get_eol_seq(gint eol_mode)
* program's directory.
*/
static inline gchar *
-get_default_config_path(const gchar *program)
+teco_get_default_config_path(const gchar *program)
{
return g_path_get_dirname(program);
}
-#elif defined(G_OS_UNIX)
+#elif defined(G_OS_UNIX) && !defined(__HAIKU__)
-/*
- * NOTE: We explicitly do not handle
- * Haiku like UNIX here, since it appears to
- * be uncommon on Haiku to clutter the HOME directory
- * with config files.
- */
static inline gchar *
-get_default_config_path(const gchar *program)
+teco_get_default_config_path(const gchar *program)
{
- return g_strdup(g_getenv("HOME"));
+ return g_strdup(g_get_home_dir());
}
-#else
+#else /* !G_OS_WIN32 && (!G_OS_UNIX || __HAIKU__) */
+/*
+ * NOTE: We explicitly do not handle
+ * Haiku like UNIX, since it appears to
+ * be uncommon on Haiku to clutter the $HOME directory
+ * with config files.
+ */
static inline gchar *
-get_default_config_path(const gchar *program)
+teco_get_default_config_path(const gchar *program)
{
return g_strdup(g_get_user_config_dir());
}
#endif
-static inline gchar *
-process_options(int &argc, char **&argv)
+static gchar *teco_eval_macro = NULL;
+static gboolean teco_mung_file = FALSE;
+static gboolean teco_mung_profile = TRUE;
+
+static gchar *
+teco_process_options(gint *argc, gchar ***argv)
{
static const GOptionEntry option_entries[] = {
- {"eval", 'e', 0, G_OPTION_ARG_STRING, &eval_macro,
+ {"eval", 'e', 0, G_OPTION_ARG_STRING, &teco_eval_macro,
"Evaluate macro", "macro"},
- {"mung", 'm', 0, G_OPTION_ARG_NONE, &mung_file,
+ {"mung", 'm', 0, G_OPTION_ARG_NONE, &teco_mung_file,
"Mung script file (first non-option argument) instead of "
"$SCITECOCONFIG" G_DIR_SEPARATOR_S INI_FILE},
{"no-profile", 0, G_OPTION_FLAG_REVERSE,
- G_OPTION_ARG_NONE, &mung_profile,
+ G_OPTION_ARG_NONE, &teco_mung_profile,
"Do not mung "
"$SCITECOCONFIG" G_DIR_SEPARATOR_S INI_FILE " "
"even if it exists"},
{NULL}
};
- gchar *mung_filename = NULL;
-
- GError *gerror = NULL;
+ g_autoptr(GError) error = NULL;
- GOptionContext *options;
- GOptionGroup *interface_group = interface.get_options();
-
- options = g_option_context_new("[--] [SCRIPT] [ARGUMENT...]");
+ g_autoptr(GOptionContext) options = g_option_context_new("[--] [SCRIPT] [ARGUMENT...]");
g_option_context_set_summary(
options,
@@ -199,12 +156,13 @@ process_options(int &argc, char **&argv)
);
g_option_context_add_main_entries(options, option_entries, NULL);
+
+ GOptionGroup *interface_group = teco_interface_get_options();
if (interface_group)
g_option_context_add_group(options, interface_group);
-#if GLIB_CHECK_VERSION(2,44,0)
/*
- * If possible we parse in POSIX mode, which means that
+ * We parse in POSIX mode, which means that
* the first non-option argument terminates option parsing.
* SciTECO considers all non-option arguments to be script
* arguments and it makes little sense to mix script arguments
@@ -212,20 +170,16 @@ process_options(int &argc, char **&argv)
* in many situations.
* It is also strictly required to make hash-bang lines like
* #!/usr/bin/sciteco -m
- * work (see sciteco(1)).
+ * work.
*/
g_option_context_set_strict_posix(options, TRUE);
-#endif
- if (!g_option_context_parse(options, &argc, &argv, &gerror)) {
+ if (!g_option_context_parse(options, argc, argv, &error)) {
g_fprintf(stderr, "Option parsing failed: %s\n",
- gerror->message);
- g_error_free(gerror);
+ error->message);
exit(EXIT_FAILURE);
}
- g_option_context_free(options);
-
/*
* GOption will NOT remove "--" if followed by an
* option-argument, which may interfer with scripts
@@ -235,38 +189,41 @@ process_options(int &argc, char **&argv)
* and "--" is not the first non-option argument as in
* sciteco foo -- -C bar.
*/
- if (argc >= 2 && !strcmp(argv[1], "--")) {
- argv[1] = argv[0];
- argv++;
- argc--;
+ if (*argc >= 2 && !strcmp((*argv)[1], "--")) {
+ (*argv)[1] = (*argv)[0];
+ (*argv)++;
+ (*argc)--;
}
- if (mung_file) {
- if (argc < 2) {
+ gchar *mung_filename = NULL;
+
+ if (teco_mung_file) {
+ if (*argc < 2) {
g_fprintf(stderr, "Script to mung expected!\n");
exit(EXIT_FAILURE);
}
- if (!g_file_test(argv[1], G_FILE_TEST_IS_REGULAR)) {
+ if (!g_file_test((*argv)[1], G_FILE_TEST_IS_REGULAR)) {
g_fprintf(stderr, "Cannot mung \"%s\". File does not exist!\n",
- argv[1]);
+ (*argv)[1]);
exit(EXIT_FAILURE);
}
- mung_filename = g_strdup(argv[1]);
+ mung_filename = g_strdup((*argv)[1]);
- argv[1] = argv[0];
- argv++;
- argc--;
+ (*argv)[1] = (*argv)[0];
+ (*argv)++;
+ (*argc)--;
}
return mung_filename;
}
-static inline void
-initialize_environment(const gchar *program)
+static void
+teco_initialize_environment(const gchar *program)
{
- gchar *default_configpath, *abs_path;
+ g_autoptr(GError) error = NULL;
+ gchar *abs_path;
/*
* Initialize some "special" environment variables.
@@ -279,44 +236,41 @@ initialize_environment(const gchar *program)
* Initialize and canonicalize $HOME.
* Therefore we can refer to $HOME as the
* current user's home directory on any platform
- * and it can be re-configured even though g_get_home_dir()
- * evaluates $HOME only beginning with glib v2.36.
+ * and you can even start SciTECO with $HOME set to a relative
+ * path (sometimes useful for testing).
*/
g_setenv("HOME", g_get_home_dir(), FALSE);
- abs_path = get_absolute_path(g_getenv("HOME"));
+ abs_path = teco_file_get_absolute_path(g_getenv("HOME"));
g_setenv("HOME", abs_path, TRUE);
g_free(abs_path);
#ifdef G_OS_WIN32
- g_setenv("COMSPEC", "cmd.exe", FALSE);
-#elif defined(G_OS_UNIX) || defined(G_OS_HAIKU)
+ g_setenv("ComSpec", "cmd.exe", FALSE);
+#elif defined(G_OS_UNIX)
g_setenv("SHELL", "/bin/sh", FALSE);
#endif
/*
* Initialize $SCITECOCONFIG and $SCITECOPATH
*/
- default_configpath = get_default_config_path(program);
+ g_autofree gchar *default_configpath = teco_get_default_config_path(program);
g_setenv("SCITECOCONFIG", default_configpath, FALSE);
#ifdef G_OS_WIN32
- gchar *default_scitecopath;
- default_scitecopath = g_build_filename(default_configpath, "lib", NIL);
+ g_autofree gchar *default_scitecopath = g_build_filename(default_configpath, "lib", NULL);
g_setenv("SCITECOPATH", default_scitecopath, FALSE);
- g_free(default_scitecopath);
#else
g_setenv("SCITECOPATH", SCITECOLIBDIR, FALSE);
#endif
- g_free(default_configpath);
/*
* $SCITECOCONFIG and $SCITECOPATH may still be relative.
* They are canonicalized, so macros can use them even
* if the current working directory changes.
*/
- abs_path = get_absolute_path(g_getenv("SCITECOCONFIG"));
+ abs_path = teco_file_get_absolute_path(g_getenv("SCITECOCONFIG"));
g_setenv("SCITECOCONFIG", abs_path, TRUE);
g_free(abs_path);
- abs_path = get_absolute_path(g_getenv("SCITECOPATH"));
+ abs_path = teco_file_get_absolute_path(g_getenv("SCITECOPATH"));
g_setenv("SCITECOPATH", abs_path, TRUE);
g_free(abs_path);
@@ -332,7 +286,11 @@ initialize_environment(const gchar *program)
* the environment variables, the environment should
* be exported via QRegisters::globals.get_environ().
*/
- QRegisters::globals.set_environ();
+ if (!teco_qreg_table_set_environ(&teco_qreg_table_globals, &error)) {
+ g_fprintf(stderr, "Error intializing environment: %s\n",
+ error->message);
+ exit(EXIT_FAILURE);
+ }
}
/*
@@ -340,159 +298,162 @@ initialize_environment(const gchar *program)
*/
static void
-sigint_handler(int signal)
+teco_sigint_handler(int signal)
{
- sigint_occurred = TRUE;
+ teco_sigint_occurred = TRUE;
}
-} /* namespace SciTECO */
-
-/*
- * main() must be defined in the root
- * namespace, so we import the "SciTECO"
- * namespace. We have no more declarations
- * to make in the "SciTECO" namespace.
- */
-using namespace SciTECO;
-
int
main(int argc, char **argv)
{
- static GotoTable cmdline_goto_table;
- static QRegisterTable local_qregs;
-
- gchar *mung_filename;
+ g_autoptr(GError) error = NULL;
#ifdef DEBUG_PAUSE
/* Windows debugging hack (see above) */
system("pause");
#endif
- signal(SIGINT, sigint_handler);
- signal(SIGTERM, sigint_handler);
+ signal(SIGINT, teco_sigint_handler);
+ signal(SIGTERM, teco_sigint_handler);
- mung_filename = process_options(argc, argv);
+ g_autofree gchar *mung_filename = teco_process_options(&argc, &argv);
/*
* All remaining arguments in argv are arguments
* to the macro or munged file.
*/
- interface.init();
+
+ /*
+ * Theoretically, QReg tables should only be initialized
+ * after the interface, since they contain Scintilla documents.
+ * However, this would prevent the inialization of clipboard QRegs
+ * in teco_interface_init() and those should be available in batch mode
+ * as well.
+ * As long as the string parts are not accessed, that should be OK.
+ *
+ * FIXME: Perhaps it would be better to introduce something like
+ * teco_interface_init_clipboard()?
+ */
+ teco_qreg_table_init(&teco_qreg_table_globals, TRUE);
+
+ teco_interface_init();
/*
* QRegister view must be initialized only now
* (e.g. after Curses/GTK initialization).
*/
- QRegisters::view.initialize();
+ teco_qreg_view = teco_view_new();
+ teco_view_setup(teco_qreg_view);
- /* the default registers (A-Z and 0-9) */
- QRegisters::globals.insert_defaults();
/* search string and status register */
- QRegisters::globals.insert("_");
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("_", 1));
/* replacement string register */
- QRegisters::globals.insert("-");
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("-", 1));
/* current buffer name and number ("*") */
- QRegisters::globals.insert(new QRegisterBufferInfo());
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_bufferinfo_new());
/* current working directory ("$") */
- QRegisters::globals.insert(new QRegisterWorkingDir());
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_workingdir_new());
/* environment defaults and registers */
- initialize_environment(argv[0]);
+ teco_initialize_environment(argv[0]);
- /* the default registers (A-Z and 0-9) */
- local_qregs.insert_defaults();
- QRegisters::locals = &local_qregs;
+ teco_qreg_table_t local_qregs;
+ teco_qreg_table_init(&local_qregs, TRUE);
- ring.edit((const gchar *)NULL);
+ if (!teco_ring_edit_by_name(NULL, &error)) {
+ g_fprintf(stderr, "Error editing unnamed file: %s\n",
+ error->message);
+ exit(EXIT_FAILURE);
+ }
- /* add remaining arguments to unnamed buffer */
+ /*
+ * Add remaining arguments to unnamed buffer.
+ *
+ * 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...
+ */
for (gint i = 1; i < argc; i++) {
- /*
- * FIXME: arguments may contain line-feeds.
- * Once SciTECO is 8-byte clear, we can add the
- * command-line params null-terminated.
- */
- interface.ssm(SCI_APPENDTEXT, strlen(argv[i]), (sptr_t)argv[i]);
- interface.ssm(SCI_APPENDTEXT, 1, (sptr_t)"\n");
+ teco_interface_ssm(SCI_APPENDTEXT, strlen(argv[i]), (sptr_t)argv[i]);
+ teco_interface_ssm(SCI_APPENDTEXT, 1, (sptr_t)"\n");
}
/*
* Execute macro or mung file
*/
- try {
- if (eval_macro) {
- try {
- Execute::macro(eval_macro, false);
- } catch (Error &error) {
- error.add_frame(new Error::ToplevelFrame());
- throw; /* forward */
- } catch (Quit) {
- /*
- * ^C invoked, quit hook should still
- * be executed.
- */
- }
- QRegisters::hook(QRegisters::HOOK_QUIT);
- exit(EXIT_SUCCESS);
+ if (teco_eval_macro) {
+ if (!teco_execute_macro(teco_eval_macro, strlen(teco_eval_macro),
+ &local_qregs, &error) &&
+ !g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT)) {
+ teco_error_add_frame_toplevel();
+ goto error;
}
+ if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
+ goto error;
+ goto cleanup;
+ }
+
+ if (!mung_filename && teco_mung_profile)
+ /* NOTE: Still safe to use g_getenv() */
+ mung_filename = g_build_filename(g_getenv("SCITECOCONFIG"), INI_FILE, NULL);
- if (!mung_filename && mung_profile)
- /* NOTE: Still safe to use g_getenv() */
- mung_filename = g_build_filename(g_getenv("SCITECOCONFIG"),
- INI_FILE, NIL);
-
- if (mung_filename &&
- g_file_test(mung_filename, G_FILE_TEST_IS_REGULAR)) {
- try {
- Execute::file(mung_filename, false);
- } catch (Quit) {
- /*
- * ^C invoked, quit hook should still
- * be executed.
- */
- }
-
- if (quit_requested) {
- QRegisters::hook(QRegisters::HOOK_QUIT);
- exit(EXIT_SUCCESS);
- }
+ if (mung_filename && g_file_test(mung_filename, G_FILE_TEST_IS_REGULAR)) {
+ if (!teco_execute_file(mung_filename, &local_qregs, &error) &&
+ !g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT))
+ goto error;
+
+ if (teco_quit_requested) {
+ if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
+ goto error;
+ goto cleanup;
}
- } catch (Error &error) {
- error.display_full();
- exit(EXIT_FAILURE);
- } catch (...) {
- exit(EXIT_FAILURE);
}
/*
* If munged file didn't quit, switch into interactive mode
*/
/* commandline replacement string register */
- QRegisters::globals.insert(CTL_KEY_ESC_STR);
+ teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("\e", 1));
+
+ teco_undo_enabled = TRUE;
+ teco_ring_set_scintilla_undo(TRUE);
+ teco_view_set_scintilla_undo(teco_qreg_view, TRUE);
- Goto::table = &cmdline_goto_table;
- undo.enabled = true;
- ring.set_scintilla_undo(true);
- QRegisters::view.set_scintilla_undo(true);
+ /*
+ * FIXME: Perhaps we should simply call teco_cmdline_init() and
+ * teco_cmdline_cleanup() here.
+ */
+ teco_machine_main_init(&teco_cmdline.machine, &local_qregs, TRUE);
+
+ if (!teco_interface_event_loop(&error))
+ goto error;
- interface.event_loop();
+ teco_machine_main_clear(&teco_cmdline.machine);
+ memset(&teco_cmdline.machine, 0, sizeof(teco_cmdline.machine));
/*
* Ordinary application termination:
* Interface is shut down, so we are
* in non-interactive mode again.
*/
- undo.enabled = false;
- undo.clear();
+ teco_undo_enabled = FALSE;
+ teco_undo_clear();
/* also empties all Scintilla undo buffers */
- ring.set_scintilla_undo(false);
- QRegisters::view.set_scintilla_undo(false);
-
- try {
- QRegisters::hook(QRegisters::HOOK_QUIT);
- } catch (Error &error) {
- error.display_full();
- exit(EXIT_FAILURE);
- }
-
- g_free(mung_filename);
+ teco_ring_set_scintilla_undo(FALSE);
+ teco_view_set_scintilla_undo(teco_qreg_view, FALSE);
+
+ if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error))
+ goto error;
+
+cleanup:
+#ifndef NDEBUG
+ teco_ring_cleanup();
+ teco_qreg_table_clear(&local_qregs);
+ teco_qreg_table_clear(&teco_qreg_table_globals);
+ teco_view_free(teco_qreg_view);
+#endif
+ teco_interface_cleanup();
return 0;
+
+error:
+ teco_error_display_full(error);
+ return EXIT_FAILURE;
}
diff --git a/src/memory.c b/src/memory.c
new file mode 100644
index 0000000..f8942fd
--- /dev/null
+++ b/src/memory.c
@@ -0,0 +1,672 @@
+/*
+ * 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
+
+#define USE_DL_PREFIX /* for dlmalloc */
+
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#ifdef HAVE_MALLOC_H
+#include <malloc.h>
+#endif
+#ifdef HAVE_MALLOC_NP_H
+#include <malloc_np.h>
+#endif
+
+#ifdef HAVE_WINDOWS_H
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <psapi.h>
+#endif
+
+/*
+ * For task_info() on OS X.
+ */
+#ifdef HAVE_MACH_MACH_H
+#include <mach/mach.h>
+#endif
+#ifdef HAVE_MACH_MESSAGE_H
+#include <mach/message.h>
+#endif
+#ifdef HAVE_MACH_KERN_RETURN_H
+#include <mach/kern_return.h>
+#endif
+#ifdef HAVE_MACH_TASK_INFO_H
+#include <mach/task_info.h>
+#endif
+
+/*
+ * For sysctl() on FreeBSD.
+ */
+#ifdef HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+#ifdef HAVE_SYS_SYSCTL_H
+#include <sys/sysctl.h>
+#endif
+
+/*
+ * For sysconf() on Linux.
+ */
+#ifdef HAVE_SYS_TIME_H
+#include <sys/time.h>
+#endif
+#ifdef HAVE_SYS_RESOURCE_H
+#include <sys/resource.h>
+#endif
+
+#include <glib.h>
+
+/*
+ * For open() (currently only on Linux).
+ */
+#ifdef G_OS_UNIX
+#include <sys/stat.h>
+#include <fcntl.h>
+#endif
+
+#include "sciteco.h"
+#include "error.h"
+#include "undo.h"
+#include "memory.h"
+
+/**
+ * @file
+ * Memory measurement and limiting.
+ *
+ * A discussion of memory measurement techniques on Linux
+ * and UNIXoid operating systems is in order, since this
+ * problem turned out to be rather tricky.
+ *
+ * @par Size of the program break
+ * There is also the old-school technique of calculating the size
+ * of the program break, ie. the effective size of the DATA segment.
+ * This works under the assumption that all allocations are
+ * performed by extending the program break, as is __traditionally__
+ * done by malloc() and friends.
+ *
+ * - Unfortunately, modern malloc() implementations sometimes
+ * mmap() memory, especially for large allocations.
+ * SciTECO mostly allocates small chunks.
+ * Unfortunately, some malloc implementations like jemalloc
+ * only claim memory using mmap(), thus rendering sbrk(0)
+ * useless.
+ * - Furthermore, some malloc-implementations like glibc will
+ * only shrink the program break when told so explicitly
+ * using malloc_trim(0).
+ * - The sbrk(0) method thus depends on implementation details
+ * of the libc.
+ * - However, this might be a suitable backend on old UNIX-platforms
+ * or a as a fallback for teco_memory_get_usage().
+ *
+ * @par Resource limits
+ * UNIX has resource limits, which could be used to enforce
+ * the memory limit, but in case they are hit, malloc()
+ * will return NULL, so g_malloc() would abort().
+ * Wrapping malloc() to work around that has the same
+ * problems described below.
+ *
+ * @par Hooking malloc()
+ * malloc_usable_size() could be used to count the memory
+ * consumption by updating a counter after every malloc(),
+ * realloc() and free().
+ * malloc_usable_size() is libc-specific, but available at least in
+ * glibc and jemalloc (FreeBSD). Windows (MSVCRT) has `_msize()`.
+ * This would require overwriting or hooking all calls to
+ * malloc() and friends, though.
+ * For all other platforms, we'd have to rely on writing the
+ * heap object size into every heap object, thus wasting
+ * one word per heap object.
+ *
+ * - glibc has malloc hooks, but they are non-portable and
+ * deprecated.
+ * - It is possible to effectively wrap malloc() by overriding
+ * the libc's implementation, which will even work when
+ * statically linking in libc since malloc() is usually
+ * declared `weak`.
+ * This however does probably not work on all platforms and
+ * means you need to know the original function (pointers).
+ * It should work sufficiently when linking everything statically.
+ * - glibc exports symbols for the original malloc() implementation
+ * like `__libc_malloc()` that could be used for wrapping.
+ * This is undocumented and libc-specific, though.
+ * - The GNU ld --wrap option allows us to intercept calls,
+ * but obviously won't work for shared libraries.
+ * - The portable dlsym() could be used to look up the original
+ * library symbol, but it may and does call malloc functions,
+ * eg. calloc() on glibc.
+ * Some people work around this using bootstrap makeshift allocators
+ * used only during dlsym().
+ * __In other words, there is no way to portably and reliably
+ * wrap malloc() and friends when linking dynamically.__
+ * - Another difficulty is that, when free() is overridden, every
+ * function that can __independently__ allocate memory that
+ * can be passed to free() must also be overridden.
+ * This is impossible to know without making assumptions about the
+ * malloc implementation used.
+ * Otherwise the measurement is not precise and there can even
+ * be underruns. Thus we'd have to guard against underruns.
+ * - Unfortunately, it is undefined whether the "usable" size of
+ * a heap object can change unwittingly, ie. not by malloc() or
+ * realloc() on that same heap object, but for instance after a
+ * neighbouring heap object is freed.
+ * If this can happen, free() on that heap object might subtract
+ * more than was initially added for this heap object, resulting
+ * in measurement underruns.
+ * - malloc() and friends are MT-safe, so any replacement function
+ * would have to be MT-safe as well to avoid memory corruption.
+ *
+ * Memory counting using malloc_usable_size() in overwritten/wrapped
+ * malloc()/realloc()/free() calls has thus been deemed impractical.
+ *
+ * Overriding could only work if we store the allocated size
+ * at the beginning of each heap object and would link in an external
+ * malloc() implementation, so that the symbol names are known.
+ *
+ * Unfortunately, overwriting libc functions is also non-portable,
+ * so replacing the libc malloc with an external allocator is tricky.
+ * On Linux (and hopefully other UNIXes), you can simply link
+ * in the malloc replacement statically which will even let the
+ * dynamic linker pick the new implementation.
+ * On Windows however, we would apparently need incredibly hacky code
+ * to patch the symbol tables
+ * (see https://github.com/ned14/nedmalloc/blob/master/winpatcher.c).
+ * Alternatively, everything __including__ MSVCRT needs to be linked
+ * in statically. This is not supported by MinGW and would have certain
+ * disadvantages even if it worked.
+ *
+ * @par malloc() introspection
+ * glibc and some other platforms have mallinfo().
+ * But at least on glibc it can get unbearably slow on programs
+ * with a lot of (virtual/resident) memory.
+ * Besides, mallinfo's API is broken on 64-bit systems, effectively
+ * limiting the enforcable memory limit to 4GB.
+ * Other glibc-specific introspection functions like malloc_info()
+ * can be even slower because of the syscalls required.
+ *
+ * - FreeBSD/jemalloc has mallctl("stats.allocated") which even when
+ * optimized is significantly slower than the current implementation
+ * but generally acceptable.
+ * - dlmalloc has malloc_footprint() which is very fast.
+ * It was therefore considered to simply import dlmalloc as the default
+ * allocator on (almost) all platforms.
+ * Despite problems overwriting malloc() globally on some platforms,
+ * this turned out to be impractical since malloc_footprint() includes
+ * only the mmapped memory and memory is not always unmapped even when
+ * calling malloc_trim(), so we couldn't recover after hitting
+ * the memory limit.
+ * - rpmalloc has a cheap rpmalloc_global_statistics() but enabling it
+ * comes with a memory overhead.
+ * - There seems to be no other malloc() replacement with a constant-time
+ * function returning the footprint.
+ *
+ * @par Instrumenting all of SciTECO's and C++ allocations.
+ * If we don't want to count each and every allocation in the system,
+ * we could also use custom allocators/deallocators together with
+ * malloc_usable_size().
+ * For many objects, the size will also be known at free() time, so
+ * malloc_usable_size() can be avoided.
+ *
+ * - To track Scintilla's memory usage, custom C++ allocators/deallocators
+ * can be defined.
+ * - Beginning with C++14 (or earlier with -fsized-deallocation),
+ * it is possible to globally replace sized allocation/deallocation
+ * functions, which could be used to avoid the malloc_usable_size()
+ * workaround. Unfortunately, this may not be used for arrays,
+ * since the compiler may have to call non-sized variants if the
+ * original allocation size is unknown - and there is no way to detect
+ * that when the new[] call is made.
+ * What's worse is that at least G++ STL is broken seriously and
+ * some versions will call the non-sized delete() even when sized-deallocation
+ * is available. Again, this cannot be detected at new() time.
+ * Therefore, I had to remove the sized-deallocation based
+ * optimization.
+ * - This approach has the same disadvantages as wrapping malloc() because
+ * of the unreliability of malloc_usable_size().
+ * Furthermore, all allocations by glib (eg. g_strdup()) will be missed.
+ *
+ * @par Directly measuring the resident memory size
+ * 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.
+ *
+ * - Windows has GetProcessMemoryInfo() which is quite slow.
+ * When polled on a separate thread, the slow down is very acceptable.
+ * - 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.
+ * Also, use of malloc_trim() after hitting the memory limit is crucial
+ * since the RSS will otherwise not decrease.
+ * - Haiku has no usable constant-time API.
+ *
+ * @par Conclusion
+ * Every approach sucks and no platform supports everything.
+ * We therefore now opted for a combined strategy:
+ * Most platforms will by default try to replace malloc() with dlmalloc.
+ * The dlmalloc functions are wrapped and the memory usage is counted via
+ * malloc_usable_size() which in the case of dlmalloc should never change
+ * for one heap object unless we realloc() it.
+ * This should be fastest, the most precise and there is a guaranteed
+ * malloc_trim().
+ * Malloc overriding can be disabled at compile time to aid in memory
+ * debugging.
+ * On Windows, we never even try to link in dlmalloc.
+ * If disabled, we try to directly measure memory consumption using
+ * OS APIs.
+ * Polling of the RSS takes place in a dedicated thread that is started
+ * on demand and paused whenever the main thread is idle (eg. waits for
+ * user input), so we don't waste cycles.
+ */
+
+/**
+ * Current memory usage.
+ * Access must be synchronized using atomic operations.
+ */
+static gint teco_memory_usage = 0;
+
+/*
+ * NOTE: This implementation based on malloc_usable_size() might
+ * also work with other malloc libraries, given that they provide
+ * a malloc_usable_size() which does not change for a heap object
+ * (unless it is reallocated of course).
+ */
+#ifdef REPLACE_MALLOC
+
+void * __attribute__((used))
+malloc(size_t size)
+{
+ void *ptr = dlmalloc(size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+void __attribute__((used))
+free(void *ptr)
+{
+ if (!ptr)
+ return;
+ g_atomic_int_add(&teco_memory_usage, -dlmalloc_usable_size(ptr));
+ dlfree(ptr);
+}
+
+void * __attribute__((used))
+calloc(size_t nmemb, size_t size)
+{
+ void *ptr = dlcalloc(nmemb, size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+void * __attribute__((used))
+realloc(void *ptr, size_t size)
+{
+ if (ptr)
+ g_atomic_int_add(&teco_memory_usage, -dlmalloc_usable_size(ptr));
+ ptr = dlrealloc(ptr, size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+void * __attribute__((used))
+memalign(size_t alignment, size_t size)
+{
+ void *ptr = dlmemalign(alignment, size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+void *aligned_alloc(size_t, size_t) __attribute__((used, alias("memalign")));
+
+int __attribute__((used))
+posix_memalign(void **memptr, size_t alignment, size_t size)
+{
+ int ret = dlposix_memalign(memptr, alignment, size);
+ if (G_LIKELY(!ret))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(*memptr));
+ return ret;
+}
+
+void * __attribute__((used))
+valloc(size_t size)
+{
+ void *ptr = dlvalloc(size);
+ if (G_LIKELY(ptr != NULL))
+ g_atomic_int_add(&teco_memory_usage, dlmalloc_usable_size(ptr));
+ return ptr;
+}
+
+/*
+ * The glibc manual claims we have to replace this function
+ * but we'd need sysconf(_SC_PAGESIZE) to implement it.
+ */
+void * __attribute__((used))
+pvalloc(size_t size)
+{
+ g_assert_not_reached();
+ return NULL;
+}
+
+size_t __attribute__((used))
+malloc_usable_size(void *ptr)
+{
+ return dlmalloc_usable_size(ptr);
+}
+
+int __attribute__((used))
+malloc_trim(size_t pad)
+{
+ return dlmalloc_trim(pad);
+}
+
+/*
+ * FIXME: Which platforms might need malloc_trim() to
+ * recover from hitting the memory limit?
+ * In other words, which platform's teco_memory_get_usage()
+ * might return a large value even if most memory has already
+ * been deallocated?
+ */
+#elif defined(G_OS_WIN32)
+
+/*
+ * On Windows, we never link in dlmalloc.
+ *
+ * NOTE: At least on Windows 2000, we run twice as fast than
+ * when polling from a dedicated thread.
+ */
+static gsize
+teco_memory_get_usage(void)
+{
+ PROCESS_MEMORY_COUNTERS info;
+
+ /*
+ * This __should__ not fail since the current process has
+ * PROCESS_ALL_ACCESS, but who knows...
+ * Since memory limiting cannot be turned off when this
+ * happens, we can just as well terminate abnormally.
+ */
+ if (G_UNLIKELY(!GetProcessMemoryInfo(GetCurrentProcess(),
+ &info, sizeof(info)))) {
+ g_autofree gchar *msg = g_win32_error_message(GetLastError());
+ g_error("Cannot get memory usage: %s", msg);
+ return 0;
+ }
+
+ return info.WorkingSetSize;
+}
+
+#define NEED_POLL_THREAD
+
+#elif defined(HAVE_TASK_INFO)
+
+/*
+ * Practically only for Mac OS X.
+ *
+ * FIXME: Benchmark whether polling in a thread really
+ * improves performances as much as on Linux.
+ * Is this even critical or can we link in dlmalloc?
+ */
+static gsize
+teco_memory_get_usage(void)
+{
+ struct mach_task_basic_info info;
+ mach_msg_type_number_t info_count = MACH_TASK_BASIC_INFO_COUNT;
+
+ if (G_UNLIKELY(task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
+ (task_info_t)&info, &info_count) != KERN_SUCCESS))
+ return 0; // FIXME
+
+ return info.resident_size;
+}
+
+#define NEED_POLL_THREAD
+
+#elif defined(G_OS_UNIX) && defined(HAVE_SYSCTL)
+
+/*
+ * Practically only for FreeBSD.
+ *
+ * FIXME: Is this even critical or can we link in dlmalloc?
+ */
+static gsize
+teco_memory_get_usage(void)
+{
+ struct kinfo_proc procstk;
+ size_t len = sizeof(procstk);
+ int pidinfo[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
+
+ sysctl(pidinfo, G_N_ELEMENTS(pidinfo), &procstk, &len, NULL, 0);
+
+ return procstk.ki_rssize; // FIXME: Which unit?
+}
+
+#define NEED_POLL_THREAD
+
+#elif defined(G_OS_UNIX) && defined(HAVE_SYSCONF) && defined(HAVE_PROCFS)
+
+#ifndef HAVE_MALLOC_TRIM
+#warning malloc_trim() missing - Might not recover from hitting the memory limit!
+#endif
+
+/*
+ * Mainly for Linux, but there might be other UNIXoids supporting procfs.
+ * This would be ridiculously slow if polled from the main thread.
+ *
+ * Since Linux supports dlmalloc(), this will usually not be required
+ * unless you disable it explicitly.
+ *
+ * NOTE: This is conciously avoiding glib and stdio APIs since we run in
+ * a very tight loop and should avoid any unnecessary allocations which could
+ * significantly slow down the main thread.
+ */
+static gsize
+teco_memory_get_usage(void)
+{
+ static long page_size = 0;
+
+ if (G_UNLIKELY(!page_size))
+ page_size = sysconf(_SC_PAGESIZE);
+
+ int fd = open("/proc/self/statm", O_RDONLY);
+ if (fd < 0)
+ /* procfs might not be mounted */
+ return 0;
+
+ gchar buf[256];
+ ssize_t len = read(fd, buf, sizeof(buf)-1);
+ close(fd);
+ if (G_UNLIKELY(len < 0))
+ return 0;
+ buf[len] = '\0';
+
+ gsize memory_usage = 0;
+ sscanf(buf, "%*u %" G_GSIZE_FORMAT, &memory_usage);
+
+ return memory_usage * page_size;
+}
+
+#define NEED_POLL_THREAD
+
+#else
+
+/*
+ * We've got neither dlmalloc, nor any particular OS backend.
+ */
+#warning dlmalloc is disabled and there is no memory counting backend - memory limiting will be unavailable!
+
+#endif
+
+#ifdef NEED_POLL_THREAD
+
+static GThread *teco_memory_thread = NULL;
+
+static enum {
+ TECO_MEMORY_STATE_ON,
+ TECO_MEMORY_STATE_OFF,
+ TECO_MEMORY_STATE_SHUTDOWN
+} teco_memory_state = TECO_MEMORY_STATE_ON;
+
+static GMutex teco_memory_mutex;
+static GCond teco_memory_cond;
+
+/*
+ * FIXME: What if we activated the thread only whenever the
+ * usage is queried in the main thread?
+ * This would automatically "clock" the threaded polling at the same rate
+ * as the main thread is polling.
+ * On the downside, the value of teco_memory_usage would be more outdated,
+ * so a memory overrun would be detected with even more delay.
+ */
+static gpointer
+teco_memory_poll_thread_cb(gpointer data)
+{
+ g_mutex_lock(&teco_memory_mutex);
+
+ for (;;) {
+ while (teco_memory_state == TECO_MEMORY_STATE_ON) {
+ g_mutex_unlock(&teco_memory_mutex);
+ /*
+ * NOTE: teco_memory_mutex is not used for teco_memory_usage
+ * since it is locked most of the time which would extremely slow
+ * down the main thread.
+ */
+ g_atomic_int_set(&teco_memory_usage, teco_memory_get_usage());
+ g_thread_yield();
+ g_mutex_lock(&teco_memory_mutex);
+ }
+ if (G_UNLIKELY(teco_memory_state == TECO_MEMORY_STATE_SHUTDOWN))
+ break;
+
+ g_cond_wait(&teco_memory_cond, &teco_memory_mutex);
+ /* teco_memory_mutex is locked */
+ }
+
+ g_mutex_unlock(&teco_memory_mutex);
+ return NULL;
+}
+
+void __attribute__((constructor))
+teco_memory_start_limiting(void)
+{
+ if (!teco_memory_limit)
+ return;
+
+ /*
+ * FIXME: Setting a low thread priority would certainly help.
+ * This would be less important for platforms like Linux where
+ * we usually don't need a polling thread at all.
+ */
+ if (G_UNLIKELY(!teco_memory_thread))
+ teco_memory_thread = g_thread_new(NULL, teco_memory_poll_thread_cb, NULL);
+
+ g_mutex_lock(&teco_memory_mutex);
+ teco_memory_state = TECO_MEMORY_STATE_ON;
+ g_cond_signal(&teco_memory_cond);
+ g_mutex_unlock(&teco_memory_mutex);
+}
+
+void
+teco_memory_stop_limiting(void)
+{
+ g_mutex_lock(&teco_memory_mutex);
+ teco_memory_state = TECO_MEMORY_STATE_OFF;
+ g_mutex_unlock(&teco_memory_mutex);
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_memory_cleanup(void)
+{
+ if (!teco_memory_thread)
+ return;
+
+ g_mutex_lock(&teco_memory_mutex);
+ teco_memory_state = TECO_MEMORY_STATE_SHUTDOWN;
+ g_cond_signal(&teco_memory_cond);
+ g_mutex_unlock(&teco_memory_mutex);
+
+ g_thread_join(teco_memory_thread);
+}
+#endif
+
+#else /* !NEED_POLL_THREAD */
+
+void teco_memory_start_limiting(void) {}
+void teco_memory_stop_limiting(void) {}
+
+#endif
+
+/**
+ * Memory limit in bytes (500mb by default, assuming SI units).
+ * 0 means no limiting.
+ */
+gsize teco_memory_limit = 500*1000*1000;
+
+gboolean
+teco_memory_set_limit(gsize new_limit, GError **error)
+{
+ gsize memory_usage = g_atomic_int_get(&teco_memory_usage);
+
+ if (G_UNLIKELY(new_limit && memory_usage > new_limit)) {
+ g_autofree gchar *usage_str = g_format_size(memory_usage);
+ g_autofree gchar *limit_str = g_format_size(new_limit);
+
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Cannot set undo memory limit (%s): "
+ "Current usage too large (%s).",
+ limit_str, usage_str);
+ return FALSE;
+ }
+
+ teco_undo_gsize(teco_memory_limit) = new_limit;
+
+ if (teco_memory_limit)
+ teco_memory_start_limiting();
+ else
+ teco_memory_stop_limiting();
+
+ return TRUE;
+}
+
+gboolean
+teco_memory_check(GError **error)
+{
+ gsize memory_usage = g_atomic_int_get(&teco_memory_usage);
+
+ if (G_UNLIKELY(teco_memory_limit && memory_usage > teco_memory_limit)) {
+ g_autofree gchar *limit_str = g_format_size(memory_usage);
+
+ g_set_error(error, TECO_ERROR, TECO_ERROR_MEMLIMIT,
+ "Memory limit (%s) exceeded. See <EJ> command.",
+ limit_str);
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/memory.cpp b/src/memory.cpp
deleted file mode 100644
index fd7adf7..0000000
--- a/src/memory.cpp
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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
-
-/* for malloc_usable_size() */
-#ifdef HAVE_MALLOC_H
-#include <malloc.h>
-#endif
-#ifdef HAVE_MALLOC_NP_H
-#include <malloc_np.h>
-#endif
-
-#include <new>
-
-#include <glib.h>
-
-#include "sciteco.h"
-#include "memory.h"
-#include "error.h"
-#include "undo.h"
-
-#ifdef HAVE_WINDOWS_H
-/* here it shouldn't cause conflicts with other headers */
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#include <psapi.h>
-#endif
-
-namespace SciTECO {
-
-/*
- * Define this to prefix each heap object allocated
- * by the custom allocators with a magic value.
- * This helps to detect non-matching calls to the
- * overridden new/delete operators which can cause
- * underruns of the memory counter.
- */
-//#define DEBUG_MAGIC ((guintptr)0xDEAD15DE5E1BEAF0)
-
-MemoryLimit memlimit;
-
-/*
- * A discussion of memory measurement techniques on Linux
- * and UNIXoid operating systems is in order, since this
- * problem turned out to be rather tricky.
- *
- * - UNIX has resource limits, which could be used to enforce
- * the memory limit, but in case they are hit, malloc()
- * will return NULL, so g_malloc() would abort().
- * Wrapping malloc() to work around that has the same
- * problems described below.
- * - glibc has malloc hooks, but they are non-portable and
- * deprecated.
- * - It is possible to effectively wrap malloc() by overriding
- * the libc's implementation, which will even work when
- * statically linking in libc since malloc() is usually
- * delcared `weak`.
- * - When wrapping malloc(), malloc_usable_size() could be
- * used to count the memory consumption.
- * This is libc-specific, but available at least in
- * glibc and jemalloc (FreeBSD).
- * - glibc exports symbols for the original malloc() implementation
- * like __libc_malloc() that could be used for wrapping.
- * This is undocumented and libc-specific, though.
- * - The GNU ld --wrap option allows us to intercept calls,
- * but obviously won't work for shared libraries.
- * - The portable dlsym() could be used to look up the original
- * library symbol, but it may and does call malloc functions,
- * eg. calloc() on glibc.
- * In other words, there is no way to portably and reliably
- * wrap malloc() and friends when linking dynamically.
- * - Another difficulty is that, when free() is overridden, every
- * function that can __independently__ allocate memory that
- * can be passed to free() must also be overridden.
- * Otherwise the measurement is not precise and there can even
- * be underruns. Thus we'd have to guard against underruns.
- * - malloc() and friends are MT-safe, so any replacement function
- * would have to be MT-safe as well to avoid memory corruption.
- * E.g. even in single-threaded builds, glib might use
- * threads internally.
- * - There is also the old-school technique of calculating the size
- * of the program break, ie. the effective size of the DATA segment.
- * This works under the assumption that all allocations are
- * performed by extending the program break, as is __traditionally__
- * done by malloc() and friends.
- * - Unfortunately, modern malloc() implementations sometimes
- * mmap() memory, especially for large allocations.
- * SciTECO mostly allocates small chunks.
- * Unfortunately, some malloc implementations like jemalloc
- * only claim memory using mmap(), thus rendering sbrk(0)
- * useless.
- * - Furthermore, some malloc-implementations like glibc will
- * only shrink the program break when told so explicitly
- * using malloc_trim(0).
- * - The sbrk(0) method thus depends on implementation details
- * of the libc.
- * - glibc and some other platforms have mallinfo().
- * But at least on glibc it can get unbearably slow on programs
- * with a lot of (virtual/resident) memory.
- * Besides, mallinfo's API is broken on 64-bit systems, effectively
- * limiting the enforcable memory limit to 4GB.
- * Other glibc-specific introspection functions like malloc_info()
- * can be even slower because of the syscalls required.
- * - Linux has /proc/self/stat and /proc/self/statm but polling them
- * is very inefficient.
- * - FreeBSD/jemalloc has mallctl("stats.allocated") which even when
- * optimized is significantly slower than the fallback but generally
- * acceptable.
- * - On all other platforms we (have to) rely on the fallback
- * implementation based on C++ allocators/deallocators.
- * They have been improved significantly to count as much memory
- * as possible, even using libc-specific APIs like malloc_usable_size().
- * Since this has been proven to work sufficiently well even on FreeBSD,
- * there is no longer any UNIX-specific implementation.
- * Even the malloc_usable_size() workaround for old or non-GNU
- * compilers is still faster than mallctl() on FreeBSD.
- * This might need to change in the future.
- * - Beginning with C++14 (or earlier with -fsized-deallocation),
- * it is possible to globally replace sized allocation/deallocation
- * functions, which could be used to avoid the malloc_usable_size()
- * workaround. Unfortunately, this may not be used for arrays,
- * since the compiler may have to call non-sized variants if the
- * original allocation size is unknown - and there is no way to detect
- * that when the new[] call is made.
- * What's worse is that at least G++ STL is broken seriously and
- * some versions will call the non-sized delete() even when sized-deallocation
- * is available. Again, this cannot be detected at new() time.
- * Therefore, I had to remove the sized-deallocation based
- * optimization.
- */
-
-#ifdef G_OS_WIN32
-/*
- * Uses the Windows-specific GetProcessMemoryInfo(),
- * so the entire process heap is measured.
- *
- * FIXME: Unfortunately, this is much slower than the portable
- * fallback implementation.
- * It may be possible to overwrite malloc() and friends,
- * counting the chunks with the MSVCRT-specific _minfo().
- * Since we will always run against MSVCRT, the disadvantages
- * discussed above for the UNIX-case may not be important.
- * We might also just use the fallback implementation with some
- * additional support for _msize().
- */
-
-gsize
-MemoryLimit::get_usage(void)
-{
- PROCESS_MEMORY_COUNTERS info;
-
- /*
- * This __should__ not fail since the current process has
- * PROCESS_ALL_ACCESS, but who knows...
- * Since memory limiting cannot be turned off when this
- * happens, we can just as well terminate abnormally.
- */
- if (G_UNLIKELY(!GetProcessMemoryInfo(GetCurrentProcess(),
- &info, sizeof(info)))) {
- gchar *msg = g_win32_error_message(GetLastError());
- g_error("Cannot get memory usage: %s", msg);
- /* shouldn't be reached */
- g_free(msg);
- return 0;
- }
-
- return info.WorkingSetSize;
-}
-
-#else
-/*
- * Portable fallback-implementation relying on C++11 sized allocators.
- *
- * Unfortunately, in the worst case, this will only measure the heap used
- * by C++ objects in SciTECO's sources; not even Scintilla, nor all
- * g_malloc() calls.
- * Usually, we will be able to use global non-sized deallocators with
- * libc-specific support to get more accurate results, though.
- */
-
-#define MEMORY_USAGE_FALLBACK
-
-/**
- * Current memory usage in bytes.
- *
- * @bug This only works in single-threaded applications.
- * Should SciTECO or Scintilla ever use multiple threads,
- * it will be necessary to use atomic operations.
- */
-static gsize memory_usage = 0;
-
-gsize
-MemoryLimit::get_usage(void)
-{
- return memory_usage;
-}
-
-#endif /* MEMORY_USAGE_FALLBACK */
-
-void
-MemoryLimit::set_limit(gsize new_limit)
-{
- gsize memory_usage = get_usage();
-
- if (G_UNLIKELY(new_limit && memory_usage > new_limit)) {
- gchar *usage_str = g_format_size(memory_usage);
- gchar *limit_str = g_format_size(new_limit);
-
- Error err("Cannot set undo memory limit (%s): "
- "Current usage too large (%s).",
- limit_str, usage_str);
-
- g_free(limit_str);
- g_free(usage_str);
- throw err;
- }
-
- undo.push_var(limit) = new_limit;
-}
-
-void
-MemoryLimit::check(void)
-{
- if (G_UNLIKELY(limit && get_usage() > limit)) {
- gchar *limit_str = g_format_size(limit);
-
- Error err("Memory limit (%s) exceeded. See <EJ> command.",
- limit_str);
-
- g_free(limit_str);
- throw err;
- }
-}
-
-/*
- * The object-specific sized deallocators allow memory
- * counting portably, even in strict C++11 mode.
- * Once we depend on C++14, they and the entire `Object`
- * class hack may be avoided.
- * But see above - due to broken STLs, this may not actually
- * be safe!
- */
-
-void *
-Object::operator new(size_t size) noexcept
-{
-#ifdef MEMORY_USAGE_FALLBACK
- memory_usage += size;
-#endif
-
-#ifdef DEBUG_MAGIC
- guintptr *ptr = (guintptr *)g_malloc(sizeof(guintptr) + size);
- *ptr = DEBUG_MAGIC;
- return ptr + 1;
-#else
- /*
- * Since we've got the sized-delete operator
- * below, we could allocate via g_slice.
- *
- * Using g_slice however would render malloc_trim()
- * ineffective. Also, it has been shown to be
- * unnecessary on Linux/glibc.
- * Glib is guaranteed to use the system malloc(),
- * so g_malloc() cooperates with malloc_trim().
- *
- * On Windows (even Windows 2000), the slice allocator
- * did not show any significant performance boost
- * either. Also, since g_slice never seems to return
- * memory to the OS and we cannot force it to do so,
- * it will not cooperate with the Windows-specific
- * memory measurement and it is hard to recover
- * from memory limit exhaustions.
- */
- return g_malloc(size);
-#endif
-}
-
-void
-Object::operator delete(void *ptr, size_t size) noexcept
-{
-#ifdef DEBUG_MAGIC
- if (ptr) {
- ptr = (guintptr *)ptr - 1;
- g_assert(*(guintptr *)ptr == DEBUG_MAGIC);
- }
-#endif
-
- g_free(ptr);
-
-#ifdef MEMORY_USAGE_FALLBACK
- memory_usage -= size;
-#endif
-}
-
-} /* namespace SciTECO */
-
-/*
- * In strict C++11, we can still use global non-sized
- * deallocators.
- *
- * On their own, they bring little benefit, but with
- * some libc-specific functionality, they can be used
- * to improve the fallback memory measurements to include
- * all allocations (including Scintilla).
- * This comes with a moderate runtime penalty.
- *
- * Unfortunately, even in C++14, defining replacement
- * sized deallocators may be very dangerous, so this
- * seems to be as best as we can get (see above).
- */
-
-void *
-operator new(size_t size)
-{
- void *ptr = g_malloc(size);
-
-#if defined(MEMORY_USAGE_FALLBACK) && defined(HAVE_MALLOC_USABLE_SIZE)
- /* NOTE: g_malloc() should always use the system malloc(). */
- SciTECO::memory_usage += malloc_usable_size(ptr);
-#endif
-
- return ptr;
-}
-
-void
-operator delete(void *ptr) noexcept
-{
-#if defined(MEMORY_USAGE_FALLBACK) && defined(HAVE_MALLOC_USABLE_SIZE)
- if (ptr)
- SciTECO::memory_usage -= malloc_usable_size(ptr);
-#endif
- g_free(ptr);
-}
diff --git a/src/memory.h b/src/memory.h
index 693a208..58705a7 100644
--- a/src/memory.h
+++ b/src/memory.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,77 +14,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __MEMORY_H
-#define __MEMORY_H
+#pragma once
#include <glib.h>
-/**
- * Default memory limit (500mb, assuming SI units).
- */
-#define MEMORY_LIMIT_DEFAULT (500*1000*1000)
-
-namespace SciTECO {
-
-/**
- * Common base class for all objects in SciTECO.
- * This is currently only used to provide custom new/delete
- * replacements in order to support unified allocation via
- * Glib (g_malloc and g_slice) and as a memory usage
- * counting fallback.
- *
- * This approach has certain drawbacks, e.g. you cannot
- * derive from Object privately; nor is it possible to
- * influence allocations in other libraries or even of
- * scalars (e.g. new char[5]).
- *
- * C++14 (supported by GCC >= 5) has global sized delete
- * replacements which would be effective in the entire application.
- * We're using them too if support is detected and there is
- * also a fallback using malloc_usable_size().
- * Another fallback with a size field would be possible
- * but is probably not worth the trouble.
- */
-class Object {
-public:
- static void *operator new(size_t size) noexcept;
- static inline void *
- operator new[](size_t size) noexcept
- {
- return operator new(size);
- }
- static inline void *
- operator new(size_t size, void *ptr) noexcept
- {
- return ptr;
- }
-
- static void operator delete(void *ptr, size_t size) noexcept;
- static inline void
- operator delete[](void *ptr, size_t size) noexcept
- {
- operator delete(ptr, size);
- }
-};
-
-extern class MemoryLimit : public Object {
-public:
- /**
- * Undo stack memory limit in bytes.
- * 0 means no limiting.
- */
- gsize limit;
-
- MemoryLimit() : limit(MEMORY_LIMIT_DEFAULT) {}
-
- static gsize get_usage(void);
-
- void set_limit(gsize new_limit = 0);
+extern gsize teco_memory_limit;
- void check(void);
-} memlimit;
+void teco_memory_start_limiting(void);
+void teco_memory_stop_limiting(void);
-} /* namespace SciTECO */
+gboolean teco_memory_set_limit(gsize new_limit, GError **error);
-#endif
+gboolean teco_memory_check(GError **error);
diff --git a/src/parser.c b/src/parser.c
new file mode 100644
index 0000000..ff1fd18
--- /dev/null
+++ b/src/parser.c
@@ -0,0 +1,902 @@
+/*
+ * 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 <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include "sciteco.h"
+#include "memory.h"
+#include "string-utils.h"
+#include "interface.h"
+#include "undo.h"
+#include "expressions.h"
+#include "qreg.h"
+#include "ring.h"
+#include "glob.h"
+#include "error.h"
+#include "core-commands.h"
+#include "goto-commands.h"
+#include "parser.h"
+
+//#define DEBUG
+
+GArray *teco_loop_stack;
+
+static void __attribute__((constructor))
+teco_loop_stack_init(void)
+{
+ teco_loop_stack = g_array_sized_new(FALSE, FALSE, sizeof(teco_loop_context_t), 1024);
+}
+
+TECO_DEFINE_ARRAY_UNDO_INSERT_VAL(teco_loop_stack, teco_loop_context_t);
+TECO_DEFINE_ARRAY_UNDO_REMOVE_INDEX(teco_loop_stack);
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_loop_stack_cleanup(void)
+{
+ g_array_free(teco_loop_stack, TRUE);
+}
+#endif
+
+gboolean
+teco_machine_input(teco_machine_t *ctx, gchar chr, GError **error)
+{
+ teco_state_t *next = ctx->current->input_cb(ctx, chr, error);
+ if (!next)
+ return FALSE;
+
+ if (next != ctx->current) {
+ if (ctx->must_undo)
+ teco_undo_ptr(ctx->current);
+ ctx->current = next;
+
+ if (ctx->current->initial_cb && !ctx->current->initial_cb(ctx, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+teco_state_end_of_macro(teco_machine_t *ctx, GError **error)
+{
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Unterminated command");
+ return FALSE;
+}
+
+/**
+ * Handles all expected exceptions and preparing them for stack frame insertion.
+ */
+gboolean
+teco_machine_main_step(teco_machine_main_t *ctx, const gchar *macro, gint stop_pos, GError **error)
+{
+ while (ctx->macro_pc < stop_pos) {
+#ifdef DEBUG
+ g_printf("EXEC(%d): input='%c'/%x, state=%p, mode=%d\n",
+ ctx->macro_pc, macro[ctx->macro_pc], macro[ctx->macro_pc],
+ ctx->parent.current, ctx->mode);
+#endif
+
+ if (teco_interface_is_interrupted()) {
+ teco_error_interrupted_set(error);
+ goto error_attach;
+ }
+
+ if (!teco_memory_check(error))
+ goto error_attach;
+
+ if (!teco_machine_input(&ctx->parent, macro[ctx->macro_pc], error))
+ goto error_attach;
+ ctx->macro_pc++;
+ }
+
+ /*
+ * Provide interactive feedback when the
+ * PC is at the end of the command line.
+ * This will actually be called in other situations,
+ * like at the end of macros but that does not hurt.
+ * It should perhaps be in teco_cmdline_insert(),
+ * but doing it here ensures that exceptions get
+ * normalized.
+ */
+ if (ctx->parent.current->refresh_cb &&
+ !ctx->parent.current->refresh_cb(&ctx->parent, error))
+ goto error_attach;
+
+ return TRUE;
+
+error_attach:
+ g_assert(!error || *error != NULL);
+ /*
+ * FIXME: Maybe this can be avoided altogether by passing in ctx->macro_pc
+ * from the callees?
+ */
+ teco_error_set_coord(macro, ctx->macro_pc);
+ return FALSE;
+}
+
+gboolean
+teco_execute_macro(const gchar *macro, gsize macro_len,
+ teco_qreg_table_t *qreg_table_locals, GError **error)
+{
+ /*
+ * This is not auto-cleaned up, so it can be initialized
+ * on demand.
+ */
+ teco_qreg_table_t macro_locals;
+
+ if (!qreg_table_locals)
+ teco_qreg_table_init(&macro_locals, FALSE);
+
+ guint parent_brace_level = teco_brace_level;
+
+ g_auto(teco_machine_main_t) macro_machine;
+ teco_machine_main_init(&macro_machine, qreg_table_locals ? : &macro_locals, FALSE);
+
+ GError *tmp_error = NULL;
+
+ if (!teco_machine_main_step(&macro_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);
+ }
+
+ if (G_UNLIKELY(teco_loop_stack->len > macro_machine.loop_stack_fp)) {
+ const teco_loop_context_t *ctx = &g_array_index(teco_loop_stack, teco_loop_context_t, teco_loop_stack->len-1);
+ teco_error_set_coord(macro, ctx->pc);
+
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Unterminated loop");
+ goto error_cleanup;
+ }
+
+ 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);
+ goto error_attach;
+ }
+
+ /*
+ * Some states (esp. commands involving a
+ * "lookahead") are valid at the end of a macro.
+ */
+ if (macro_machine.parent.current->end_of_macro_cb &&
+ !macro_machine.parent.current->end_of_macro_cb(&macro_machine.parent, error))
+ goto error_attach;
+
+ /*
+ * This handles the problem of Q-Registers
+ * local to the macro invocation being edited
+ * when the macro terminates without additional
+ * complexity.
+ * teco_qreg_table_empty() might leave the table
+ * half-empty, but it will eventually be completely
+ * cleared by teco_qreg_table_clear().
+ * This does not hurt since an error will rub out the
+ * macro invocation itself and macro_locals don't have
+ * to be preserved.
+ */
+ if (!qreg_table_locals && !teco_qreg_table_empty(&macro_locals, error))
+ goto error_attach;
+
+ return TRUE;
+
+error_attach:
+ teco_error_set_coord(macro, macro_machine.macro_pc);
+ /* fall through */
+error_cleanup:
+ if (!qreg_table_locals)
+ teco_qreg_table_clear(&macro_locals);
+ /* make sure teco_goto_skip_label will be NULL even in batch mode */
+ teco_string_truncate(&teco_goto_skip_label, 0);
+ return FALSE;
+}
+
+gboolean
+teco_execute_file(const gchar *filename, teco_qreg_table_t *qreg_table_locals, GError **error)
+{
+ g_auto(teco_string_t) macro = {NULL, 0};
+ if (!g_file_get_contents(filename, &macro.data, &macro.len, error))
+ return FALSE;
+
+ gchar *p;
+
+ /* only when executing files, ignore Hash-Bang line */
+ if (*macro.data == '#') {
+ /*
+ * NOTE: We assume that a file starting with Hash does not contain
+ * a null-byte in its first line.
+ */
+ p = strpbrk(macro.data, "\r\n");
+ if (G_UNLIKELY(!p))
+ /* empty script */
+ return TRUE;
+ p++;
+ } else {
+ p = macro.data;
+ }
+
+ if (!teco_execute_macro(p, macro.len - (p - macro.data),
+ qreg_table_locals, error)) {
+ /* correct error position for Hash-Bang line */
+ teco_error_pos += p - macro.data;
+ if (*macro.data == '#')
+ teco_error_line++;
+ teco_error_add_frame_file(filename);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+void
+teco_machine_main_init(teco_machine_main_t *ctx, teco_qreg_table_t *qreg_table_locals,
+ gboolean must_undo)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ teco_machine_init(&ctx->parent, &teco_state_start, must_undo);
+ ctx->loop_stack_fp = teco_loop_stack->len;
+ teco_goto_table_init(&ctx->goto_table, must_undo);
+ ctx->qreg_table_locals = qreg_table_locals;
+
+ ctx->expectstring.nesting = 1;
+ teco_machine_stringbuilding_init(&ctx->expectstring.machine, '\e', qreg_table_locals, must_undo);
+}
+
+gboolean
+teco_machine_main_eval_colon(teco_machine_main_t *ctx)
+{
+ if (!ctx->modifier_colon)
+ return FALSE;
+
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->modifier_colon = FALSE;
+ return TRUE;
+}
+
+teco_state_t *
+teco_machine_main_transition_input(teco_machine_main_t *ctx,
+ teco_machine_main_transition_t *transitions,
+ guint len, gchar chr, GError **error)
+{
+ if (chr < 0 || chr >= len || !transitions[(guint)chr].next) {
+ teco_error_syntax_set(error, chr);
+ return NULL;
+ }
+
+ if (ctx->mode == TECO_MODE_NORMAL && transitions[(guint)chr].transition_cb) {
+ /*
+ * NOTE: We could also just let transition_cb return a boolean...
+ */
+ GError *tmp_error = NULL;
+ transitions[(guint)chr].transition_cb(ctx, &tmp_error);
+ if (tmp_error) {
+ g_propagate_error(error, tmp_error);
+ return NULL;
+ }
+ }
+
+ return transitions[(guint)chr].next;
+}
+
+void
+teco_machine_main_clear(teco_machine_main_t *ctx)
+{
+ teco_goto_table_clear(&ctx->goto_table);
+ teco_machine_stringbuilding_clear(&ctx->expectstring.machine);
+}
+
+/*
+ * FIXME: All teco_state_stringbuilding_* states could be static?
+ */
+static teco_state_t *teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx,
+ gchar chr, GError **error);
+TECO_DECLARE_STATE(teco_state_stringbuilding_ctl);
+
+static teco_state_t *teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx,
+ gchar chr, GError **error);
+TECO_DECLARE_STATE(teco_state_stringbuilding_escaped);
+
+TECO_DECLARE_STATE(teco_state_stringbuilding_lower);
+TECO_DECLARE_STATE(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_start_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ if (chr == '^')
+ return &teco_state_stringbuilding_ctl;
+ 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);
+}
+
+/* in cmdline.c */
+gboolean teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
+ gchar key, 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
+);
+
+static teco_state_t *
+teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ chr = teco_ascii_toupper(chr);
+
+ switch (chr) {
+ case '^': break;
+ case 'Q':
+ case 'R': return &teco_state_stringbuilding_escaped;
+ case 'V': return &teco_state_stringbuilding_lower;
+ case 'W': return &teco_state_stringbuilding_upper;
+ case 'E': return &teco_state_stringbuilding_ctle;
+ default:
+ chr = TECO_CTL_KEY(chr);
+ }
+
+ if (ctx->result)
+ teco_string_append_c(ctx->result, chr);
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctl);
+
+static teco_state_t *
+teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ switch (ctx->mode) {
+ case TECO_STRINGBUILDING_MODE_UPPER:
+ chr = g_ascii_toupper(chr);
+ break;
+ case TECO_STRINGBUILDING_MODE_LOWER:
+ chr = g_ascii_tolower(chr);
+ break;
+ default:
+ break;
+ }
+
+ teco_string_append_c(ctx->result, chr);
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE(teco_state_stringbuilding_escaped);
+
+static teco_state_t *
+teco_state_stringbuilding_lower_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ /*
+ * FIXME: This does not handle ^V^V typed with up-carets.
+ */
+ if (chr == TECO_CTL_KEY('V')) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->mode);
+ ctx->mode = TECO_STRINGBUILDING_MODE_LOWER;
+ } else {
+ teco_string_append_c(ctx->result, g_ascii_tolower(chr));
+ }
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE(teco_state_stringbuilding_lower);
+
+static teco_state_t *
+teco_state_stringbuilding_upper_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ /*
+ * FIXME: This does not handle ^W^W typed with up-carets.
+ */
+ if (chr == TECO_CTL_KEY('W')) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->mode);
+ ctx->mode = TECO_STRINGBUILDING_MODE_UPPER;
+ } else {
+ teco_string_append_c(ctx->result, g_ascii_toupper(chr));
+ }
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE(teco_state_stringbuilding_upper);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_state_t *next;
+
+ switch (teco_ascii_toupper(chr)) {
+ case '\\': next = &teco_state_stringbuilding_ctle_num; break;
+ case 'U': next = &teco_state_stringbuilding_ctle_u; 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;
+ default:
+ if (ctx->result) {
+ gchar buf[] = {TECO_CTL_KEY('E'), chr};
+ teco_string_append(ctx->result, buf, sizeof(buf));
+ }
+ return &teco_state_stringbuilding_start;
+ }
+
+ if (ctx->machine_qregspec)
+ teco_machine_qregspec_reset(ctx->machine_qregspec);
+ else
+ ctx->machine_qregspec = teco_machine_qregspec_new(TECO_QREG_REQUIRED,
+ ctx->qreg_table_locals,
+ ctx->parent.must_undo);
+ return next;
+}
+
+TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctle);
+
+/* in cmdline.c */
+gboolean teco_state_stringbuilding_qreg_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx,
+ gchar chr, GError **error);
+
+/**
+ * @interface TECO_DEFINE_STATE_STRINGBUILDING_QREG
+ * @implements TECO_DEFINE_STATE
+ * @ingroup states
+ */
+#define TECO_DEFINE_STATE_STRINGBUILDING_QREG(NAME, ...) \
+ TECO_DEFINE_STATE(NAME, \
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
+ teco_state_stringbuilding_qreg_process_edit_cmd, \
+ ##__VA_ARGS__ \
+ )
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_num_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, NULL, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_num;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ teco_int_t value;
+ if (!qreg->vtable->get_integer(qreg, &value, error))
+ return NULL;
+
+ /*
+ * NOTE: Numbers can always be safely formatted as null-terminated strings.
+ */
+ gchar buffer[TECO_EXPRESSIONS_FORMAT_LEN];
+ const gchar *num = teco_expressions_format(buffer, value);
+ teco_string_append(ctx->result, num, strlen(num));
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_num);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_u_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, NULL, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_u;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ teco_int_t value;
+ if (!qreg->vtable->get_integer(qreg, &value, error))
+ return NULL;
+ if (value < 0 || value > 0xFF) {
+ g_autofree gchar *name_printable = teco_string_echo(qreg->head.name.data, qreg->head.name.len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Q-Register \"%s\" does not contain a valid character", name_printable);
+ return NULL;
+ }
+
+ teco_string_append_c(ctx->result, (gchar)value);
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_u);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_q_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, NULL, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_q;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ /*
+ * FIXME: Should we have a special teco_qreg_get_string_append() function?
+ */
+ g_auto(teco_string_t) str = {NULL, 0};
+ if (!qreg->vtable->get_string(qreg, &str.data, &str.len, error))
+ return NULL;
+ teco_string_append(ctx->result, str.data, str.len);
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_q);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_quote_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+ teco_qreg_table_t *table;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, &table, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_quote;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+ if (!qreg->vtable->get_string(qreg, &str.data, &str.len, error))
+ return NULL;
+ /*
+ * NOTE: g_shell_quote() expects a null-terminated string, so it is
+ * important to check that there are no embedded nulls.
+ * The restriction itself is probably valid since null-bytes are not allowed
+ * in command line arguments anyway.
+ * Otherwise, we'd have to implement our own POSIX shell escape function.
+ */
+ 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;
+ }
+ g_autofree gchar *str_quoted = g_shell_quote(str.data);
+ teco_string_append(ctx->result, str_quoted, strlen(str_quoted));
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_quote);
+
+static teco_state_t *
+teco_state_stringbuilding_ctle_n_input(teco_machine_stringbuilding_t *ctx, gchar chr, GError **error)
+{
+ teco_qreg_t *qreg;
+ teco_qreg_table_t *table;
+
+ switch (teco_machine_qregspec_input(ctx->machine_qregspec, chr,
+ ctx->result ? &qreg : NULL, &table, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return &teco_state_stringbuilding_ctle_n;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ if (!ctx->result)
+ /* parse-only mode */
+ return &teco_state_stringbuilding_start;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+ if (!qreg->vtable->get_string(qreg, &str.data, &str.len, error))
+ return NULL;
+ 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;
+ }
+
+ g_autofree gchar *str_escaped = teco_globber_escape_pattern(str.data);
+ teco_string_append(ctx->result, str_escaped, strlen(str_escaped));
+
+ return &teco_state_stringbuilding_start;
+}
+
+TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_n);
+
+void
+teco_machine_stringbuilding_init(teco_machine_stringbuilding_t *ctx, gchar escape_char,
+ teco_qreg_table_t *locals, gboolean must_undo)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ teco_machine_init(&ctx->parent, &teco_state_stringbuilding_start, must_undo);
+ ctx->escape_char = escape_char;
+ ctx->qreg_table_locals = locals;
+}
+
+void
+teco_machine_stringbuilding_reset(teco_machine_stringbuilding_t *ctx)
+{
+ teco_machine_reset(&ctx->parent, &teco_state_stringbuilding_start);
+ if (ctx->machine_qregspec)
+ teco_machine_qregspec_reset(ctx->machine_qregspec);
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->mode);
+ ctx->mode = TECO_STRINGBUILDING_MODE_NORMAL;
+}
+
+void
+teco_machine_stringbuilding_escape(teco_machine_stringbuilding_t *ctx, const gchar *str, gsize len,
+ teco_string_t *target)
+{
+ target->data = g_malloc(len*2+1);
+ target->len = 0;
+
+ for (guint i = 0; i < len; i++) {
+ if (teco_ascii_toupper(str[i]) == ctx->escape_char ||
+ (ctx->escape_char == '[' && str[i] == ']') ||
+ (ctx->escape_char == '{' && str[i] == '}'))
+ target->data[target->len++] = TECO_CTL_KEY('Q');
+ target->data[target->len++] = str[i];
+ }
+
+ target->data[target->len] = '\0';
+}
+
+void
+teco_machine_stringbuilding_clear(teco_machine_stringbuilding_t *ctx)
+{
+ if (ctx->machine_qregspec)
+ teco_machine_qregspec_free(ctx->machine_qregspec);
+}
+
+teco_state_t *
+teco_state_expectstring_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ teco_state_t *current = ctx->parent.current;
+
+ /*
+ * String termination handling
+ */
+ if (ctx->modifier_at) {
+ if (current->expectstring.last) {
+ if (ctx->parent.must_undo)
+ teco_undo_guint(ctx->__flags);
+ ctx->modifier_at = FALSE;
+ }
+
+ /*
+ * FIXME: Exclude setting at least whitespace characters as the
+ * new string escape character to avoid accidental errors?
+ */
+ switch (ctx->expectstring.machine.escape_char) {
+ case '\e':
+ case '{':
+ if (ctx->parent.must_undo)
+ teco_undo_gchar(ctx->expectstring.machine.escape_char);
+ ctx->expectstring.machine.escape_char = teco_ascii_toupper(chr);
+ return current;
+ }
+ }
+
+ /*
+ * This makes sure that escape characters (or braces) within string-building
+ * constructs and Q-Register specifications do not have to be escaped.
+ * This makes also sure that string terminators can be escaped via ^Q/^R.
+ */
+ if (ctx->expectstring.machine.parent.current->is_start) {
+ if (ctx->expectstring.machine.escape_char == '{') {
+ switch (chr) {
+ case '{':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->expectstring.nesting);
+ ctx->expectstring.nesting++;
+ break;
+ case '}':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->expectstring.nesting);
+ ctx->expectstring.nesting--;
+ break;
+ }
+ } else if (teco_ascii_toupper(chr) == ctx->expectstring.machine.escape_char) {
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->expectstring.nesting);
+ ctx->expectstring.nesting--;
+ }
+ }
+
+ if (!ctx->expectstring.nesting) {
+ /*
+ * Call process_cb() even if interactive feedback
+ * has not been requested using refresh_cb().
+ * This is necessary since commands are either
+ * written for interactive execution or not,
+ * 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,
+ ctx->expectstring.insert_len, error))
+ return NULL;
+
+ 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);
+ else
+ teco_string_clear(&ctx->expectstring.string);
+ memset(&ctx->expectstring.string, 0, sizeof(ctx->expectstring.string));
+
+ if (current->expectstring.last) {
+ if (ctx->parent.must_undo)
+ teco_undo_gchar(ctx->expectstring.machine.escape_char);
+ ctx->expectstring.machine.escape_char = '\e';
+ }
+ ctx->expectstring.nesting = 1;
+
+ if (current->expectstring.string_building)
+ teco_machine_stringbuilding_reset(&ctx->expectstring.machine);
+
+ ctx->expectstring.insert_len = 0;
+ return next;
+ }
+
+ /*
+ * NOTE: Since we only ever append to `string`, this is more efficient
+ * than teco_undo_string(ctx->expectstring.string).
+ */
+ if (ctx->mode == TECO_MODE_NORMAL && ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->expectstring.string, ctx->expectstring.string.len);
+
+ /*
+ * String building characters and string argument accumulation.
+ */
+ gsize old_len = ctx->expectstring.string.len;
+ if (current->expectstring.string_building) {
+ teco_string_t *str = ctx->mode == TECO_MODE_NORMAL
+ ? &ctx->expectstring.string : NULL;
+ if (!teco_machine_stringbuilding_input(&ctx->expectstring.machine, chr, str, error))
+ return NULL;
+ } else if (ctx->mode == TECO_MODE_NORMAL) {
+ teco_string_append_c(&ctx->expectstring.string, chr);
+ }
+ /*
+ * NOTE: As an optimization insert_len is not
+ * restored on undo since that is only
+ * necessary in interactive mode and we get
+ * called once per character when this is necessary.
+ */
+ ctx->expectstring.insert_len += ctx->expectstring.string.len - old_len;
+
+ return current;
+}
+
+gboolean
+teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error)
+{
+ teco_state_t *current = ctx->parent.current;
+
+ /* 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,
+ ctx->expectstring.insert_len, error))
+ return FALSE;
+
+ ctx->expectstring.insert_len = 0;
+ return TRUE;
+}
+
+gboolean
+teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str,
+ gsize new_chars, GError **error)
+{
+ g_assert(str->data != NULL);
+
+ /*
+ * Null-chars must not ocur in filename/path strings and at some point
+ * teco_string_t has to be converted to a null-terminated C string
+ * as all the glib filename functions rely on null-terminated strings.
+ * 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)) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Null-character not allowed in filenames");
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/parser.cpp b/src/parser.cpp
deleted file mode 100644
index fe22560..0000000
--- a/src/parser.cpp
+++ /dev/null
@@ -1,2883 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <string.h>
-#include <exception>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include "sciteco.h"
-#include "memory.h"
-#include "string-utils.h"
-#include "interface.h"
-#include "undo.h"
-#include "expressions.h"
-#include "goto.h"
-#include "qregisters.h"
-#include "ring.h"
-#include "parser.h"
-#include "symbols.h"
-#include "search.h"
-#include "spawn.h"
-#include "glob.h"
-#include "help.h"
-#include "cmdline.h"
-#include "ioview.h"
-#include "error.h"
-
-namespace SciTECO {
-
-//#define DEBUG
-
-gint macro_pc = 0;
-
-namespace States {
- StateStart start;
- StateControl control;
- StateASCII ascii;
- StateEscape escape;
- StateFCommand fcommand;
- StateChangeDir changedir;
- StateCondCommand condcommand;
- StateECommand ecommand;
- StateScintilla_symbols scintilla_symbols;
- StateScintilla_lParam scintilla_lparam;
- StateInsert insert_building(true);
- StateInsert insert_nobuilding(false);
- StateInsertIndent insert_indent;
-
- State *current = &start;
-}
-
-namespace Modifiers {
- static bool colon = false;
- static bool at = false;
-}
-
-enum Mode mode = MODE_NORMAL;
-
-/* FIXME: perhaps integrate into Mode */
-static bool skip_else = false;
-
-static gint nest_level = 0;
-
-gchar *strings[2] = {NULL, NULL};
-gchar escape_char = CTL_KEY_ESC;
-
-LoopStack loop_stack;
-
-/**
- * Loop frame pointer: The number of elements on
- * the loop stack when a macro invocation frame is
- * created.
- * This is used to perform checks for flow control
- * commands to avoid jumping with invalid PCs while
- * not creating a new stack per macro frame.
- */
-static guint loop_stack_fp = 0;
-
-/**
- * Handles all expected exceptions, converting them to
- * SciTECO::Error and preparing them for stack frame insertion.
- * This method will only throw SciTECO::Error and
- * SciTECO::Cmdline *.
- */
-void
-Execute::step(const gchar *macro, gint stop_pos)
-{
- try {
- /*
- * Convert bad_alloc and other C++ standard
- * library exceptions.
- * bad_alloc should no longer be thrown, though
- * since new/delete uses Glib allocations and we
- * uniformly terminate abnormally in case of OOM.
- */
- try {
- while (macro_pc < stop_pos) {
-#ifdef DEBUG
- g_printf("EXEC(%d): input='%c'/%x, state=%p, mode=%d\n",
- macro_pc, macro[macro_pc], macro[macro_pc],
- States::current, mode);
-#endif
-
- if (interface.is_interrupted())
- throw Error("Interrupted");
-
- memlimit.check();
-
- State::input(macro[macro_pc]);
- macro_pc++;
- }
-
- /*
- * Provide interactive feedback when the
- * PC is at the end of the command line.
- * This will actually be called in other situations,
- * like at the end of macros but that does not hurt.
- * It should perhaps be in Cmdline::insert(),
- * but doing it here ensures that exceptions get
- * normalized.
- */
- States::current->refresh();
- } catch (std::exception &error) {
- throw StdError(error);
- }
- } catch (Error &error) {
- error.set_coord(macro, macro_pc);
- throw; /* forward */
- }
-}
-
-/*
- * may throw non SciTECO::Error exceptions which are not to be
- * associated with the macro invocation stack frame
- */
-void
-Execute::macro(const gchar *macro, bool locals)
-{
- GotoTable *parent_goto_table = Goto::table;
- GotoTable macro_goto_table(false);
-
- QRegisterTable *parent_locals = QRegisters::locals;
- /*
- * NOTE: A local QReg table is not required
- * for local macro calls (:M).
- * However allocating it on the stack on-demand is
- * tricky (VLAs are not in standard C++ and alloca()
- * is buggy on MSCVRT), so we always reserve a
- * local Q-Reg table.
- * This is OK since the table object itself is very
- * small and it's empty by default.
- * Best would be to let Execute::macro() be a wrapper
- * around something like Execute::local_macro() which
- * cares about local Q-Reg allocation, but the special
- * handling of currently-edited local Q-Regs below
- * prevents this.
- */
- QRegisterTable macro_locals(false);
-
- State *parent_state = States::current;
- gint parent_pc = macro_pc;
- guint parent_loop_fp = loop_stack_fp;
-
- guint parent_brace_level = expressions.brace_level;
-
- /*
- * need this to fixup state on rubout: state machine emits undo token
- * resetting state to parent's one, but the macro executed also emitted
- * undo tokens resetting the state to StateStart
- */
- undo.push_var(States::current) = &States::start;
- macro_pc = 0;
- loop_stack_fp = loop_stack.items();
-
- Goto::table = &macro_goto_table;
-
- /*
- * Locals are only initialized when needed to
- * improve the speed of local macro calls.
- */
- if (locals) {
- macro_locals.insert_defaults();
- QRegisters::locals = &macro_locals;
- }
-
- try {
- try {
- step(macro, strlen(macro));
- } catch (Return &info) {
- /*
- * 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(States::current == &States::start);
-
- /*
- * Discard all braces, except the current one.
- */
- expressions.brace_return(parent_brace_level, info.args);
-
- /*
- * Clean up the loop stack.
- * We are allowed to return in loops.
- * NOTE: This does not have to be undone.
- */
- loop_stack.clear(loop_stack_fp);
- }
-
- if (G_UNLIKELY(loop_stack.items() > loop_stack_fp)) {
- Error error("Unterminated loop");
- error.set_coord(macro, loop_stack.peek().pc);
- throw error;
- }
-
- /*
- * Subsequent errors must still be
- * attached to this macro invocation
- * via Error::set_coord()
- */
- try {
- if (G_UNLIKELY(Goto::skip_label))
- throw Error("Label \"%s\" not found",
- Goto::skip_label);
-
- /*
- * Some states (esp. commands involving a
- * "lookahead") are valid at the end of a macro.
- */
- States::current->end_of_macro();
-
- /*
- * This handles the problem of Q-Registers
- * local to the macro invocation being edited
- * when the macro terminates.
- * QRegisterTable::clear() throws an error
- * if this happens and the Q-Reg editing
- * is undone.
- */
- if (locals)
- QRegisters::locals->clear();
- } catch (Error &error) {
- error.set_coord(macro, macro_pc);
- throw; /* forward */
- }
- } catch (...) {
- g_free(Goto::skip_label);
- Goto::skip_label = NULL;
-
- QRegisters::locals = parent_locals;
- Goto::table = parent_goto_table;
-
- loop_stack_fp = parent_loop_fp;
- macro_pc = parent_pc;
- States::current = parent_state;
-
- throw; /* forward */
- }
-
- QRegisters::locals = parent_locals;
- Goto::table = parent_goto_table;
-
- loop_stack_fp = parent_loop_fp;
- macro_pc = parent_pc;
- States::current = parent_state;
-}
-
-void
-Execute::file(const gchar *filename, bool locals)
-{
- GError *gerror = NULL;
- gchar *macro_str, *p;
-
- if (!g_file_get_contents(filename, &macro_str, NULL, &gerror))
- throw GlibError(gerror);
-
- /* only when executing files, ignore Hash-Bang line */
- if (*macro_str == '#') {
- p = strpbrk(macro_str, "\r\n");
- if (G_UNLIKELY(!p))
- /* empty script */
- goto cleanup;
- p++;
- } else {
- p = macro_str;
- }
-
- try {
- macro(p, locals);
- } catch (Error &error) {
- error.pos += p - macro_str;
- if (*macro_str == '#')
- error.line++;
- error.add_frame(new Error::FileFrame(filename));
-
- g_free(macro_str);
- throw; /* forward */
- } catch (...) {
- g_free(macro_str);
- throw; /* forward */
- }
-
-cleanup:
- g_free(macro_str);
-}
-
-State::State()
-{
- for (guint i = 0; i < G_N_ELEMENTS(transitions); i++)
- transitions[i] = NULL;
-}
-
-bool
-State::eval_colon(void)
-{
- if (!Modifiers::colon)
- return false;
-
- undo.push_var<bool>(Modifiers::colon);
- Modifiers::colon = false;
- return true;
-}
-
-void
-State::input(gchar chr)
-{
- State *state = States::current;
-
- for (;;) {
- State *next = state->get_next_state(chr);
-
- if (next == state)
- break;
-
- state = next;
- chr = '\0';
- }
-
- if (state != States::current) {
- undo.push_var<State *>(States::current);
- States::current = state;
- }
-}
-
-State *
-State::get_next_state(gchar chr)
-{
- State *next = NULL;
- guint upper = String::toupper(chr);
-
- if (upper < G_N_ELEMENTS(transitions))
- next = transitions[upper];
- if (!next)
- next = custom(chr);
- if (!next)
- throw SyntaxError(chr);
-
- return next;
-}
-
-void
-StringBuildingMachine::reset(void)
-{
- MicroStateMachine<gchar *>::reset();
- undo.push_obj(qregspec_machine) = NULL;
- undo.push_var(mode) = MODE_NORMAL;
- undo.push_var(toctl) = false;
-}
-
-bool
-StringBuildingMachine::input(gchar chr, gchar *&result)
-{
- QRegister *reg;
- gchar *str;
-
- switch (mode) {
- case MODE_UPPER:
- chr = g_ascii_toupper(chr);
- break;
- case MODE_LOWER:
- chr = g_ascii_tolower(chr);
- break;
- default:
- break;
- }
-
- if (toctl) {
- if (chr != '^')
- chr = CTL_KEY(String::toupper(chr));
- undo.push_var(toctl) = false;
- } else if (chr == '^') {
- undo.push_var(toctl) = true;
- return false;
- }
-
-MICROSTATE_START;
- switch (chr) {
- case CTL_KEY('Q'):
- case CTL_KEY('R'): set(&&StateEscaped); break;
- case CTL_KEY('V'): set(&&StateLower); break;
- case CTL_KEY('W'): set(&&StateUpper); break;
- case CTL_KEY('E'): set(&&StateCtlE); break;
- default:
- goto StateEscaped;
- }
-
- return false;
-
-StateLower:
- set(StateStart);
-
- if (chr != CTL_KEY('V')) {
- result = String::chrdup(g_ascii_tolower(chr));
- return true;
- }
-
- undo.push_var(mode) = MODE_LOWER;
- return false;
-
-StateUpper:
- set(StateStart);
-
- if (chr != CTL_KEY('W')) {
- result = String::chrdup(g_ascii_toupper(chr));
- return true;
- }
-
- undo.push_var(mode) = MODE_UPPER;
- return false;
-
-StateCtlE:
- switch (String::toupper(chr)) {
- case '\\':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlENum);
- break;
- case 'U':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlEU);
- break;
- case 'Q':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlEQ);
- break;
- case '@':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlEQuote);
- break;
- case 'N':
- undo.push_obj(qregspec_machine) = new QRegSpecMachine;
- set(&&StateCtlEN);
- break;
- default:
- result = (gchar *)g_malloc(3);
-
- set(StateStart);
- result[0] = CTL_KEY('E');
- result[1] = chr;
- result[2] = '\0';
- return true;
- }
-
- return false;
-
-StateCtlENum:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- result = g_strdup(expressions.format(reg->get_integer()));
- return true;
-
-StateCtlEU:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- result = String::chrdup((gchar)reg->get_integer());
- return true;
-
-StateCtlEQ:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- result = reg->get_string();
- return true;
-
-StateCtlEQuote:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- str = reg->get_string();
- result = g_shell_quote(str);
- g_free(str);
- return true;
-
-StateCtlEN:
- if (!qregspec_machine->input(chr, reg))
- return false;
-
- undo.push_obj(qregspec_machine) = NULL;
- set(StateStart);
- str = reg->get_string();
- result = Globber::escape_pattern(str);
- g_free(str);
- return true;
-
-StateEscaped:
- set(StateStart);
- result = String::chrdup(chr);
- return true;
-}
-
-StringBuildingMachine::~StringBuildingMachine()
-{
- delete qregspec_machine;
-}
-
-State *
-StateExpectString::custom(gchar chr)
-{
- if (chr == '\0') {
- BEGIN_EXEC(this);
- initial();
- return this;
- }
-
- /*
- * String termination handling
- */
- if (Modifiers::at) {
- if (last)
- undo.push_var(Modifiers::at) = false;
-
- switch (escape_char) {
- case CTL_KEY_ESC:
- case '{':
- undo.push_var(escape_char) = String::toupper(chr);
- return this;
- }
- }
-
- if (escape_char == '{') {
- switch (chr) {
- case '{':
- undo.push_var(nesting)++;
- break;
- case '}':
- undo.push_var(nesting)--;
- break;
- }
- } else if (String::toupper(chr) == escape_char) {
- undo.push_var(nesting)--;
- }
-
- if (!nesting) {
- State *next;
- gchar *string = strings[0];
-
- undo.push_str(strings[0]) = NULL;
- if (last)
- undo.push_var(escape_char) = CTL_KEY_ESC;
- nesting = 1;
-
- if (string_building)
- machine.reset();
-
- try {
- /*
- * Call process() even if interactive feedback
- * has not been requested using refresh().
- * This is necessary since commands are either
- * written for interactive execution or not,
- * so they may do their main activity in process().
- */
- if (insert_len)
- process(string ? : "", insert_len);
- next = done(string ? : "");
- } catch (...) {
- g_free(string);
- throw;
- }
-
- g_free(string);
- insert_len = 0;
- return next;
- }
-
- BEGIN_EXEC(this);
-
- /*
- * String building characters and
- * string argument accumulation.
- *
- * NOTE: As an optimization insert_len is not
- * restored on undo since that is only
- * necessary in interactive mode and we get
- * called once per character when this is necessary.
- * If this gets too confusing, just undo changes
- * to insert_len.
- */
- if (string_building) {
- gchar *insert;
-
- if (!machine.input(chr, insert))
- return this;
-
- undo.push_str(strings[0]);
- String::append(strings[0], insert);
- insert_len += strlen(insert);
-
- g_free(insert);
- } else {
- undo.push_str(strings[0]);
- String::append(strings[0], chr);
- insert_len++;
- }
-
- return this;
-}
-
-void
-StateExpectString::refresh(void)
-{
- /* never calls process() in parse-only mode */
- if (insert_len)
- process(strings[0], insert_len);
- insert_len = 0;
-}
-
-State *
-StateExpectFile::done(const gchar *str)
-{
- gchar *filename = expand_path(str);
- State *next;
-
- try {
- next = got_file(filename);
- } catch (...) {
- g_free(filename);
- throw;
- }
-
- g_free(filename);
- return next;
-}
-
-StateStart::StateStart()
-{
- transitions['\0'] = this;
- init(" \f\r\n\v");
-
- transitions['$'] = &States::escape;
- transitions['!'] = &States::label;
- transitions['O'] = &States::gotocmd;
- transitions['^'] = &States::control;
- transitions['F'] = &States::fcommand;
- transitions['"'] = &States::condcommand;
- transitions['E'] = &States::ecommand;
- transitions['I'] = &States::insert_building;
- transitions['?'] = &States::gethelp;
- transitions['S'] = &States::search;
- transitions['N'] = &States::searchall;
-
- transitions['['] = &States::pushqreg;
- transitions[']'] = &States::popqreg;
- transitions['G'] = &States::getqregstring;
- transitions['Q'] = &States::queryqreg;
- transitions['U'] = &States::setqreginteger;
- transitions['%'] = &States::increaseqreg;
- transitions['M'] = &States::macro;
- transitions['X'] = &States::copytoqreg;
-}
-
-void
-StateStart::insert_integer(tecoInt v)
-{
- const gchar *str = expressions.format(v);
-
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_ADDTEXT, strlen(str), (sptr_t)str);
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
-}
-
-tecoInt
-StateStart::read_integer(void)
-{
- uptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
- gchar c = (gchar)interface.ssm(SCI_GETCHARAT, pos);
- tecoInt v = 0;
- gint sign = 1;
-
- if (c == '-') {
- pos++;
- sign = -1;
- }
-
- for (;;) {
- c = String::toupper((gchar)interface.ssm(SCI_GETCHARAT, pos));
- if (c >= '0' && c <= '0' + MIN(expressions.radix, 10) - 1)
- v = (v*expressions.radix) + (c - '0');
- else if (c >= 'A' &&
- c <= 'A' + MIN(expressions.radix - 10, 26) - 1)
- v = (v*expressions.radix) + 10 + (c - 'A');
- else
- break;
-
- pos++;
- }
-
- return sign * v;
-}
-
-tecoBool
-StateStart::move_chars(tecoInt n)
-{
- sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
-
- if (!Validate::pos(pos + n))
- return FAILURE;
-
- interface.ssm(SCI_GOTOPOS, pos + n);
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS, pos);
-
- return SUCCESS;
-}
-
-tecoBool
-StateStart::move_lines(tecoInt n)
-{
- sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
- sptr_t line = interface.ssm(SCI_LINEFROMPOSITION, pos) + n;
-
- if (!Validate::line(line))
- return FAILURE;
-
- interface.ssm(SCI_GOTOLINE, line);
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS, pos);
-
- return SUCCESS;
-}
-
-tecoBool
-StateStart::delete_words(tecoInt n)
-{
- sptr_t pos, size;
-
- if (!n)
- return SUCCESS;
-
- pos = interface.ssm(SCI_GETCURRENTPOS);
- size = interface.ssm(SCI_GETLENGTH);
- interface.ssm(SCI_BEGINUNDOACTION);
- /*
- * FIXME: would be nice to do this with constant amount of
- * editor messages. E.g. by using custom algorithm accessing
- * the internal document buffer.
- */
- if (n > 0) {
- while (n--) {
- sptr_t size = interface.ssm(SCI_GETLENGTH);
- interface.ssm(SCI_DELWORDRIGHTEND);
- if (size == interface.ssm(SCI_GETLENGTH))
- break;
- }
- } else {
- n *= -1;
- while (n--) {
- sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
- //interface.ssm(SCI_DELWORDLEFTEND);
- interface.ssm(SCI_WORDLEFTEND);
- if (pos == interface.ssm(SCI_GETCURRENTPOS))
- break;
- interface.ssm(SCI_DELWORDRIGHTEND);
- }
- }
- interface.ssm(SCI_ENDUNDOACTION);
-
- if (n >= 0) {
- if (size != interface.ssm(SCI_GETLENGTH)) {
- interface.ssm(SCI_UNDO);
- interface.ssm(SCI_GOTOPOS, pos);
- }
- return FAILURE;
- }
-
- interface.undo_ssm(SCI_GOTOPOS, pos);
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
- ring.dirtify();
-
- return SUCCESS;
-}
-
-State *
-StateStart::custom(gchar chr)
-{
- tecoInt v;
- tecoBool rc;
-
- /*
- * <CTRL/x> commands implemented in StateControl
- */
- if (IS_CTL(chr))
- return States::control.get_next_state(CTL_ECHO(chr));
-
- /*
- * arithmetics
- */
- /*$ 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
- *
- * Integer constants in \*(ST may be thought of and are
- * technically sequences of single-digit commands.
- * These commands take one argument from the stack
- * (0 is implied), multiply it with the current radix
- * (2, 8, 10, 16, ...), add the digit's value and
- * return the resultant integer.
- *
- * The command-like semantics of digits may be abused
- * in macros, for instance to append digits to computed
- * integers.
- * It is not an error to append a digit greater than the
- * current radix - this may be changed in the future.
- */
- if (g_ascii_isdigit(chr)) {
- BEGIN_EXEC(this);
- expressions.add_digit(chr);
- return this;
- }
-
- chr = String::toupper(chr);
- switch (chr) {
- case '/':
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_DIV);
- break;
-
- case '*':
- if (cmdline.len == 1 && cmdline[0] == '*')
- /* special save last commandline command */
- return &States::save_cmdline;
-
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_MUL);
- break;
-
- case '+':
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_ADD);
- break;
-
- case '-':
- BEGIN_EXEC(this);
- if (!expressions.args())
- expressions.set_num_sign(-expressions.num_sign);
- else
- expressions.push_calc(Expressions::OP_SUB);
- break;
-
- case '&':
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_AND);
- break;
-
- case '#':
- BEGIN_EXEC(this);
- expressions.push_calc(Expressions::OP_OR);
- break;
-
- case '(':
- BEGIN_EXEC(this);
- if (expressions.num_sign < 0) {
- expressions.set_num_sign(1);
- expressions.eval();
- expressions.push(-1);
- expressions.push_calc(Expressions::OP_MUL);
- }
- expressions.brace_open();
- break;
-
- case ')':
- BEGIN_EXEC(this);
- expressions.brace_close();
- break;
-
- case ',':
- BEGIN_EXEC(this);
- expressions.eval();
- expressions.push(Expressions::OP_NEW);
- break;
-
- /*$ "." dot
- * \&. -> dot -- Return buffer position
- *
- * \(lq.\(rq pushes onto the stack, the current
- * position (also called <dot>) of the currently
- * selected buffer or Q-Register.
- */
- case '.':
- BEGIN_EXEC(this);
- expressions.eval();
- expressions.push(interface.ssm(SCI_GETCURRENTPOS));
- break;
-
- /*$ Z size
- * Z -> size -- Return buffer size
- *
- * Pushes onto the stack, the size of the currently selected
- * buffer or Q-Register.
- * This is value is also the buffer position of the document's
- * end.
- */
- case 'Z':
- BEGIN_EXEC(this);
- expressions.eval();
- expressions.push(interface.ssm(SCI_GETLENGTH));
- break;
-
- /*$ H
- * H -> 0,Z -- Return range for entire buffer
- *
- * Pushes onto the stack the integer 0 (position of buffer
- * beginning) and the current buffer's size.
- * It is thus often equivalent to the expression
- * \(lq0,Z\(rq, or more generally \(lq(0,Z)\(rq.
- */
- case 'H':
- BEGIN_EXEC(this);
- expressions.eval();
- expressions.push(0);
- expressions.push(interface.ssm(SCI_GETLENGTH));
- break;
-
- /*$ "\\"
- * n\\ -- Insert or read ASCII numbers
- * \\ -> n
- *
- * Backslash pops a value from the stack, formats it
- * according to the current radix and inserts it in the
- * current buffer or Q-Register at dot.
- * If <n> is omitted (empty stack), it does the reverse -
- * it reads from the current buffer position an integer
- * in the current radix and pushes it onto the stack.
- * Dot is not changed when reading integers.
- *
- * In other words, the command serializes or deserializes
- * integers as ASCII characters.
- */
- case '\\':
- BEGIN_EXEC(this);
- expressions.eval();
- if (expressions.args())
- insert_integer(expressions.pop_num_calc());
- else
- expressions.push(read_integer());
- break;
-
- /*
- * control structures (loops)
- */
- case '<':
- if (mode == MODE_PARSE_ONLY_LOOP) {
- undo.push_var(nest_level)++;
- } else {
- LoopContext ctx;
-
- BEGIN_EXEC(this);
-
- expressions.eval();
- ctx.pass_through = eval_colon();
- ctx.counter = expressions.pop_num_calc(0, -1);
- if (ctx.counter) {
- /*
- * Non-colon modified, we add implicit
- * braces, so loop body won't see parameters.
- * Colon modified, loop starts can be used
- * to process stack elements which is symmetric
- * to ":>".
- */
- if (!ctx.pass_through)
- expressions.brace_open();
-
- ctx.pc = macro_pc;
- loop_stack.push(ctx);
- LoopStack::undo_pop<loop_stack>();
- } else {
- /* skip to end of loop */
- undo.push_var(mode) = MODE_PARSE_ONLY_LOOP;
- }
- }
- break;
-
- case '>':
- if (mode == MODE_PARSE_ONLY_LOOP) {
- if (!nest_level)
- undo.push_var(mode) = MODE_NORMAL;
- else
- undo.push_var(nest_level)--;
- } else {
- BEGIN_EXEC(this);
-
- if (loop_stack.items() <= loop_stack_fp)
- throw Error("Loop end without corresponding "
- "loop start command");
- LoopContext &ctx = loop_stack.peek();
- bool colon_modified = eval_colon();
-
- /*
- * Colon-modified loop ends can be used to
- * aggregate values on the stack.
- * A non-colon modified ">" behaves like ":>"
- * for pass-through loop starts, though.
- */
- if (!ctx.pass_through) {
- if (colon_modified) {
- expressions.eval();
- expressions.push(Expressions::OP_NEW);
- } else {
- expressions.discard_args();
- }
- }
-
- if (ctx.counter == 1) {
- /* this was the last loop iteration */
- if (!ctx.pass_through)
- expressions.brace_close();
- LoopStack::undo_push<loop_stack>(loop_stack.pop());
- } else {
- /*
- * Repeat loop:
- * NOTE: One undo token per iteration could
- * be avoided by saving the original counter
- * in the LoopContext.
- * We do however optimize the case of infinite loops
- * because the loop counter does not have to be
- * updated.
- */
- macro_pc = ctx.pc;
- if (ctx.counter >= 0)
- undo.push_var(ctx.counter) = ctx.counter - 1;
- }
- }
- break;
-
- /*$ ";" break
- * [bool]; -- Conditionally break from loop
- * [bool]:;
- *
- * Breaks from the current inner-most loop if <bool>
- * signifies failure (non-negative value).
- * If colon-modified, breaks from the loop if <bool>
- * signifies success (negative value).
- *
- * If the condition code cannot be popped from the stack,
- * the global search register's condition integer
- * is implied instead.
- * This way, you may break on search success/failures
- * without colon-modifying the search command (or at a
- * later point).
- *
- * Executing \(lq;\(rq outside of iterations in the current
- * macro invocation level yields an error. It is thus not
- * possible to let a macro break a caller's loop.
- */
- case ';':
- BEGIN_EXEC(this);
-
- if (loop_stack.items() <= loop_stack_fp)
- throw Error("<;> only allowed in iterations");
-
- v = QRegisters::globals["_"]->get_integer();
- rc = expressions.pop_num_calc(0, v);
- if (eval_colon())
- rc = ~rc;
-
- if (IS_FAILURE(rc)) {
- LoopContext ctx = loop_stack.pop();
-
- expressions.discard_args();
- if (!ctx.pass_through)
- expressions.brace_close();
-
- LoopStack::undo_push<loop_stack>(ctx);
-
- /* skip to end of loop */
- undo.push_var(mode) = MODE_PARSE_ONLY_LOOP;
- }
- break;
-
- /*
- * control structures (conditionals)
- */
- case '|':
- if (mode == MODE_PARSE_ONLY_COND) {
- if (!skip_else && !nest_level) {
- undo.push_var<Mode>(mode);
- mode = MODE_NORMAL;
- }
- return this;
- }
- BEGIN_EXEC(this);
-
- /* skip to end of conditional; skip ELSE-part */
- undo.push_var<Mode>(mode);
- mode = MODE_PARSE_ONLY_COND;
- break;
-
- case '\'':
- if (mode != MODE_PARSE_ONLY_COND)
- break;
-
- if (!nest_level) {
- undo.push_var<Mode>(mode);
- mode = MODE_NORMAL;
- undo.push_var<bool>(skip_else);
- skip_else = false;
- } else {
- undo.push_var<gint>(nest_level);
- nest_level--;
- }
- break;
-
- /*
- * Command-line editing
- */
- /*$ "{" "}"
- * { -- Edit command line
- * }
- *
- * The opening curly bracket is a powerful command
- * to edit command lines but has very simple semantics.
- * It copies the current commandline into the global
- * command line editing register (called Escape, i.e.
- * ASCII 27) and edits this register.
- * The curly bracket itself is not copied.
- *
- * The command line may then be edited using any
- * \*(ST command or construct.
- * You may switch between the command line editing
- * register and other registers or buffers.
- * The user will then usually reapply (called update)
- * the current command-line.
- *
- * The closing curly bracket will update the current
- * command-line with the contents of the global command
- * line editing register.
- * To do so it merely rubs-out the current command-line
- * up to the first changed character and inserts
- * all characters following from the updated command
- * line into the command stream.
- * To prevent the undesired rubout of the entire
- * command-line, the replacement command ("}") is only
- * allowed when the replacement register currently edited
- * since it will otherwise be usually empty.
- *
- * .B Note:
- * - Command line editing only works on command lines,
- * but not arbitrary macros.
- * It is therefore not available in batch mode and
- * will yield an error if used.
- * - Command line editing commands may be safely used
- * from macro invocations.
- * Such macros are called command line editing macros.
- * - A command line update from a macro invocation will
- * always yield to the outer-most macro level (i.e.
- * the command line macro).
- * Code following the update command in the macro
- * will thus never be executed.
- * - As a safe-guard against command line trashing due
- * to erroneous changes at the beginning of command
- * lines, a backup mechanism is implemented:
- * If the updated command line yields an error at
- * any command during the update, the original
- * command line will be restored with an algorithm
- * similar to command line updating and the update
- * command will fail instead.
- * That way it behaves like any other command that
- * yields an error:
- * The character resulting in the update is rejected
- * by the command line input subsystem.
- * - In the rare case that an aforementioned command line
- * backup fails, the commands following the erroneous
- * character will not be inserted again (will be lost).
- */
- case '{':
- BEGIN_EXEC(this);
- if (!undo.enabled)
- throw Error("Command-line editing only possible in "
- "interactive mode");
-
- current_doc_undo_edit();
- QRegisters::globals.edit(CTL_KEY_ESC_STR);
-
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_CLEARALL);
- interface.ssm(SCI_ADDTEXT, cmdline.pc, (sptr_t)cmdline.str);
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
-
- /* must always support undo on global register */
- interface.undo_ssm(SCI_UNDO);
- break;
-
- case '}':
- BEGIN_EXEC(this);
- if (!undo.enabled)
- throw Error("Command-line editing only possible in "
- "interactive mode");
- if (QRegisters::current != QRegisters::globals[CTL_KEY_ESC_STR])
- throw Error("Command-line replacement only allowed when "
- "editing the replacement register");
-
- /* replace cmdline in the outer macro environment */
- cmdline.replace();
- /* never reached */
-
- /*
- * modifiers
- */
- case '@':
- /*
- * @ modifier has syntactic significance, so set it even
- * in PARSE_ONLY* modes
- */
- undo.push_var<bool>(Modifiers::at);
- Modifiers::at = true;
- break;
-
- case ':':
- BEGIN_EXEC(this);
- undo.push_var<bool>(Modifiers::colon);
- Modifiers::colon = true;
- break;
-
- /*
- * commands
- */
- /*$ J jump
- * [position]J -- Go to position in buffer
- * [position]:J -> Success|Failure
- *
- * Sets dot to <position>.
- * If <position> is omitted, 0 is implied and \(lqJ\(rq will
- * go to the beginning of the buffer.
- *
- * If <position> is outside the range of the buffer, the
- * command yields an error.
- * If colon-modified, the command will instead return a
- * condition boolean signalling whether the position could
- * be changed or not.
- */
- case 'J':
- BEGIN_EXEC(this);
- v = expressions.pop_num_calc(0, 0);
- if (Validate::pos(v)) {
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS,
- interface.ssm(SCI_GETCURRENTPOS));
- interface.ssm(SCI_GOTOPOS, v);
-
- if (eval_colon())
- expressions.push(SUCCESS);
- } else if (eval_colon()) {
- expressions.push(FAILURE);
- } else {
- throw MoveError("J");
- }
- break;
-
- /*$ C move
- * [n]C -- Move dot <n> characters
- * -C
- * [n]:C -> Success|Failure
- *
- * Adds <n> to dot. 1 or -1 is implied if <n> is omitted.
- * Fails if <n> would move dot off-page.
- * The colon modifier results in a success-boolean being
- * returned instead.
- */
- case 'C':
- BEGIN_EXEC(this);
- rc = move_chars(expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw MoveError("C");
- break;
-
- /*$ R reverse
- * [n]R -- Move dot <n> characters backwards
- * -R
- * [n]:R -> Success|Failure
- *
- * Subtracts <n> from dot.
- * It is equivalent to \(lq-nC\(rq.
- */
- case 'R':
- BEGIN_EXEC(this);
- rc = move_chars(-expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw MoveError("R");
- break;
-
- /*$ L line
- * [n]L -- Move dot <n> lines forwards
- * -L
- * [n]:L -> Success|Failure
- *
- * Move dot to the beginning of the line specified
- * relatively to the current line.
- * Therefore a value of 0 for <n> goes to the
- * beginning of the current line, 1 will go to the
- * next line, -1 to the previous line etc.
- * If <n> is omitted, 1 or -1 is implied depending on
- * the sign prefix.
- *
- * If <n> would move dot off-page, the command yields
- * an error.
- * The colon-modifer results in a condition boolean
- * being returned instead.
- */
- case 'L':
- BEGIN_EXEC(this);
- rc = move_lines(expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw MoveError("L");
- break;
-
- /*$ B backwards
- * [n]B -- Move dot <n> lines backwards
- * -B
- * [n]:B -> Success|Failure
- *
- * Move dot to the beginning of the line <n>
- * lines before the current one.
- * It is equivalent to \(lq-nL\(rq.
- */
- case 'B':
- BEGIN_EXEC(this);
- rc = move_lines(-expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw MoveError("B");
- break;
-
- /*$ W word
- * [n]W -- Move dot by words
- * -W
- * [n]:W -> Success|Failure
- *
- * Move dot <n> words forward.
- * - If <n> is positive, dot is positioned at the beginning
- * of the word <n> words after the current one.
- * - If <n> is negative, dot is positioned at the end
- * of the word <n> words before the current one.
- * - If <n> is zero, dot is not moved.
- *
- * \(lqW\(rq uses Scintilla's definition of a word as
- * configurable using the
- * .B SCI_SETWORDCHARS
- * message.
- *
- * Otherwise, the command's behaviour is analogous to
- * the \(lqC\(rq command.
- */
- case 'W': {
- sptr_t pos;
- unsigned int msg = SCI_WORDRIGHTEND;
-
- BEGIN_EXEC(this);
- v = expressions.pop_num_calc();
-
- pos = interface.ssm(SCI_GETCURRENTPOS);
- /*
- * FIXME: would be nice to do this with constant amount of
- * editor messages. E.g. by using custom algorithm accessing
- * the internal document buffer.
- */
- if (v < 0) {
- v *= -1;
- msg = SCI_WORDLEFTEND;
- }
- while (v--) {
- sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
- interface.ssm(msg);
- if (pos == interface.ssm(SCI_GETCURRENTPOS))
- break;
- }
- if (v < 0) {
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS, pos);
- if (eval_colon())
- expressions.push(SUCCESS);
- } else {
- interface.ssm(SCI_GOTOPOS, pos);
- if (eval_colon())
- expressions.push(FAILURE);
- else
- throw MoveError("W");
- }
- break;
- }
-
- /*$ V
- * [n]V -- Delete words forward
- * -V
- * [n]:V -> Success|Failure
- *
- * Deletes the next <n> words until the end of the
- * n'th word after the current one.
- * If <n> is negative, deletes up to end of the
- * n'th word before the current one.
- * If <n> is omitted, 1 or -1 is implied depending on the
- * sign prefix.
- *
- * It uses Scintilla's definition of a word as configurable
- * using the
- * .B SCI_SETWORDCHARS
- * message.
- *
- * If the words to delete extend beyond the range of the
- * buffer, the command yields an error.
- * If colon-modified it instead returns a condition code.
- */
- case 'V':
- BEGIN_EXEC(this);
- rc = delete_words(expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw Error("Not enough words to delete with <V>");
- break;
-
- /*$ Y
- * [n]Y -- Delete word backwards
- * -Y
- * [n]:Y -> Success|Failure
- *
- * Delete <n> words backward.
- * <n>Y is equivalent to \(lq-nV\(rq.
- */
- case 'Y':
- BEGIN_EXEC(this);
- rc = delete_words(-expressions.pop_num_calc());
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw Error("Not enough words to delete with <Y>");
- break;
-
- /*$ "=" 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?
- */
- case '=':
- BEGIN_EXEC(this);
- expressions.eval();
- if (!expressions.args())
- throw ArgExpectedError('=');
- interface.msg(InterfaceCurrent::MSG_USER,
- "%" TECO_INTEGER_FORMAT,
- expressions.pop_num_calc());
- break;
-
- /*$ K kill
- * [n]K -- Kill lines
- * -K
- * from,to K
- * [n]:K -> Success|Failure
- * from,to:K -> Success|Failure
- *
- * Deletes characters up to the beginning of the
- * line <n> lines after or before the current one.
- * If <n> is 0, \(lqK\(rq will delete up to the beginning
- * of the current line.
- * If <n> is omitted, the sign prefix will be implied.
- * So to delete the entire line regardless of the position
- * in it, one can use \(lq0KK\(rq.
- *
- * If the deletion is beyond the buffer's range, the command
- * will yield an error unless it has been colon-modified
- * so it returns a condition code.
- *
- * If two arguments <from> and <to> are available, the
- * command is synonymous to <from>,<to>D.
- */
- case 'K':
- /*$ D delete
- * [n]D -- Delete characters
- * -D
- * from,to D
- * [n]:D -> Success|Failure
- * from,to:D -> Success|Failure
- *
- * If <n> is positive, the next <n> characters (up to and
- * character .+<n>) are deleted.
- * If <n> is negative, the previous <n> characters are
- * deleted.
- * If <n> is omitted, the sign prefix will be implied.
- *
- * If two arguments can be popped from the stack, the
- * command will delete the characters with absolute
- * position <from> up to <to> from the current buffer.
- *
- * If the character range to delete is beyond the buffer's
- * range, the command will yield an error unless it has
- * been colon-modified so it returns a condition code
- * instead.
- */
- case 'D': {
- tecoInt from, len;
-
- BEGIN_EXEC(this);
- expressions.eval();
-
- if (expressions.args() <= 1) {
- from = interface.ssm(SCI_GETCURRENTPOS);
- if (chr == 'D') {
- len = expressions.pop_num_calc();
- rc = TECO_BOOL(Validate::pos(from + len));
- } else /* chr == 'K' */ {
- sptr_t line;
- line = interface.ssm(SCI_LINEFROMPOSITION, from) +
- expressions.pop_num_calc();
- len = interface.ssm(SCI_POSITIONFROMLINE, line)
- - from;
- rc = TECO_BOOL(Validate::line(line));
- }
- if (len < 0) {
- len *= -1;
- from -= len;
- }
- } else {
- tecoInt to = expressions.pop_num();
- from = expressions.pop_num();
- len = to - from;
- rc = TECO_BOOL(len >= 0 && Validate::pos(from) &&
- Validate::pos(to));
- }
-
- if (eval_colon())
- expressions.push(rc);
- else if (IS_FAILURE(rc))
- throw RangeError(chr);
-
- if (len == 0 || IS_FAILURE(rc))
- break;
-
- if (current_doc_must_undo()) {
- interface.undo_ssm(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS));
- interface.undo_ssm(SCI_UNDO);
- }
-
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_DELETERANGE, from, len);
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
- break;
- }
-
- /*$ A
- * [n]A -> code -- Get character code from buffer
- * -A -> code
- *
- * Returns the character <code> of the character
- * <n> relative to dot from the buffer.
- * This can be an ASCII <code> or Unicode codepoint
- * depending on Scintilla's encoding of the current
- * buffer.
- * - If <n> is 0, return the <code> of the character
- * pointed to by dot.
- * - If <n> is 1, return the <code> of the character
- * immediately after dot.
- * - If <n> is -1, return the <code> of the character
- * immediately preceding dot, ecetera.
- * - 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.
- */
- /** @todo does Scintilla really return code points??? */
- case 'A':
- BEGIN_EXEC(this);
- v = interface.ssm(SCI_GETCURRENTPOS) +
- expressions.pop_num_calc();
- /*
- * NOTE: We cannot use Validate::pos() here since
- * the end of the buffer is not a valid position for <A>.
- */
- if (v < 0 || v >= interface.ssm(SCI_GETLENGTH))
- throw RangeError("A");
- expressions.push(interface.ssm(SCI_GETCHARAT, v));
- break;
-
- default:
- throw SyntaxError(chr);
- }
-
- return this;
-}
-
-StateFCommand::StateFCommand()
-{
- transitions['\0'] = this;
- transitions['K'] = &States::searchkill;
- transitions['D'] = &States::searchdelete;
- transitions['S'] = &States::replace;
- transitions['R'] = &States::replacedefault;
- transitions['G'] = &States::changedir;
-}
-
-State *
-StateFCommand::custom(gchar chr)
-{
- switch (chr) {
- /*
- * loop flow control
- */
- /*$ F<
- * F< -- Go to loop start or jump to beginning of macro
- *
- * Immediately jumps to the current loop's start.
- * Also works from inside conditionals.
- *
- * Outside of loops \(em or in a macro without
- * a loop \(em this jumps to the beginning of the macro.
- */
- case '<':
- BEGIN_EXEC(&States::start);
- /* FIXME: what if in brackets? */
- expressions.discard_args();
-
- macro_pc = loop_stack.items() > loop_stack_fp
- ? loop_stack.peek().pc : -1;
- break;
-
- /*$ F> continue
- * F> -- Go to loop end
- * :F>
- *
- * Jumps to the current loop's end.
- * If the loop has remaining iterations or runs indefinitely,
- * the jump is performed immediately just as if \(lq>\(rq
- * had been executed.
- * If the loop has reached its last iteration, \*(ST will
- * parse until the loop end command has been found and control
- * resumes after the end of the loop.
- *
- * In interactive mode, if the loop is incomplete and must
- * be exited, you can type in the loop's remaining commands
- * without them being executed (but they are parsed).
- *
- * When colon-modified, \fB:F>\fP behaves like \fB:>\fP
- * and allows numbers to be aggregated on the stack.
- *
- * Calling \fBF>\fP outside of a loop at the current
- * macro invocation level will throw an error.
- */
- /*
- * NOTE: This is almost identical to the normal
- * loop end since we don't really want to or need to
- * parse till the end of the loop.
- */
- case '>': {
- BEGIN_EXEC(&States::start);
-
- if (loop_stack.items() <= loop_stack_fp)
- throw Error("Jump to loop end without corresponding "
- "loop start command");
- LoopContext &ctx = loop_stack.peek();
- bool colon_modified = eval_colon();
-
- if (!ctx.pass_through) {
- if (colon_modified) {
- expressions.eval();
- expressions.push(Expressions::OP_NEW);
- } else {
- expressions.discard_args();
- }
- }
-
- if (ctx.counter == 1) {
- /* this was the last loop iteration */
- if (!ctx.pass_through)
- expressions.brace_close();
- LoopStack::undo_push<loop_stack>(loop_stack.pop());
- /* skip to end of loop */
- undo.push_var(mode) = MODE_PARSE_ONLY_LOOP;
- } else {
- /* repeat loop */
- macro_pc = ctx.pc;
- ctx.counter = MAX(ctx.counter - 1, -1);
- }
- break;
- }
-
- /*
- * conditional flow control
- */
- /*$ "F'"
- * F\' -- Jump to end of conditional
- */
- case '\'':
- BEGIN_EXEC(&States::start);
- /* skip to end of conditional */
- undo.push_var<Mode>(mode);
- mode = MODE_PARSE_ONLY_COND;
- undo.push_var<bool>(skip_else);
- skip_else = true;
- break;
-
- /*$ F|
- * F| -- Jump to else-part of conditional
- *
- * Jump to else-part of conditional or end of
- * conditional (only if invoked from inside the
- * condition's else-part).
- */
- case '|':
- BEGIN_EXEC(&States::start);
- /* skip to ELSE-part or end of conditional */
- undo.push_var<Mode>(mode);
- mode = MODE_PARSE_ONLY_COND;
- break;
-
- default:
- throw SyntaxError(chr);
- }
-
- return &States::start;
-}
-
-void
-UndoTokenChangeDir::run(void)
-{
- /*
- * Changing the directory on rub-out may fail.
- * This is handled silently.
- */
- g_chdir(dir);
-}
-
-/*$ FG cd change-dir folder-go
- * FG[directory]$ -- Change working directory
- *
- * Changes the process' current working directory
- * to <directory> which affects all subsequent
- * operations on relative file names like
- * tab-completions.
- * It is also inherited by external processes spawned
- * via \fBEC\fP and \fBEG\fP.
- *
- * If <directory> is omitted, the working directory
- * is changed to the current user's home directory
- * as set by the \fBHOME\fP environment variable
- * (i.e. its corresponding \(lq$HOME\(rq environment
- * register).
- * This variable is always initialized by \*(ST
- * (see \fBsciteco\fP(1)).
- * Therefore the expression \(lqFG\fB$\fP\(rq is
- * exactly equivalent to both \(lqFG~\fB$\fP\(rq and
- * \(lqFG^EQ[$HOME]\fB$\fP\(rq.
- *
- * The current working directory is also mapped to
- * the special global Q-Register \(lq$\(rq (dollar sign)
- * which may be used retrieve the current working directory.
- *
- * String-building characters are enabled on this
- * command and directories can be tab-completed.
- */
-State *
-StateChangeDir::got_file(const gchar *filename)
-{
- gchar *dir;
-
- BEGIN_EXEC(&States::start);
-
- /* passes ownership of string to undo token object */
- undo.push_own<UndoTokenChangeDir>(g_get_current_dir());
-
- dir = *filename ? g_strdup(filename)
- : QRegisters::globals["$HOME"]->get_string();
-
- if (g_chdir(dir)) {
- /* FIXME: Is errno usable on Windows here? */
- Error err("Cannot change working directory "
- "to \"%s\"", dir);
- g_free(dir);
- throw err;
- }
-
- g_free(dir);
- return &States::start;
-}
-
-StateCondCommand::StateCondCommand()
-{
- transitions['\0'] = this;
-}
-
-State *
-StateCondCommand::custom(gchar chr)
-{
- tecoInt value = 0;
- bool result;
-
- switch (mode) {
- case MODE_PARSE_ONLY_COND:
- undo.push_var(nest_level)++;
- break;
-
- case MODE_NORMAL:
- expressions.eval();
-
- if (chr == '~')
- /* don't pop value for ~ conditionals */
- break;
-
- if (!expressions.args())
- throw ArgExpectedError('"');
- value = expressions.pop_num_calc();
- break;
-
- default:
- break;
- }
-
- switch (String::toupper(chr)) {
- case '~':
- BEGIN_EXEC(&States::start);
- result = !expressions.args();
- break;
- case 'A':
- BEGIN_EXEC(&States::start);
- result = g_ascii_isalpha((gchar)value);
- break;
- case 'C':
- BEGIN_EXEC(&States::start);
- result = g_ascii_isalnum((gchar)value) ||
- value == '.' || value == '$' || value == '_';
- break;
- case 'D':
- BEGIN_EXEC(&States::start);
- result = g_ascii_isdigit((gchar)value);
- break;
- case 'I':
- BEGIN_EXEC(&States::start);
- result = G_IS_DIR_SEPARATOR((gchar)value);
- break;
- case 'S':
- case 'T':
- BEGIN_EXEC(&States::start);
- result = IS_SUCCESS(value);
- break;
- case 'F':
- case 'U':
- BEGIN_EXEC(&States::start);
- result = IS_FAILURE(value);
- break;
- case 'E':
- case '=':
- BEGIN_EXEC(&States::start);
- result = value == 0;
- break;
- case 'G':
- case '>':
- BEGIN_EXEC(&States::start);
- result = value > 0;
- break;
- case 'L':
- case '<':
- BEGIN_EXEC(&States::start);
- result = value < 0;
- break;
- case 'N':
- BEGIN_EXEC(&States::start);
- result = value != 0;
- break;
- case 'R':
- BEGIN_EXEC(&States::start);
- result = g_ascii_isalnum((gchar)value);
- break;
- case 'V':
- BEGIN_EXEC(&States::start);
- result = g_ascii_islower((gchar)value);
- break;
- case 'W':
- BEGIN_EXEC(&States::start);
- result = g_ascii_isupper((gchar)value);
- break;
- default:
- throw Error("Invalid conditional type \"%c\"", chr);
- }
-
- if (!result)
- /* skip to ELSE-part or end of conditional */
- undo.push_var(mode) = MODE_PARSE_ONLY_COND;
-
- return &States::start;
-}
-
-StateControl::StateControl()
-{
- transitions['\0'] = this;
- transitions['I'] = &States::insert_indent;
- transitions['U'] = &States::ctlucommand;
- transitions['^'] = &States::ascii;
- transitions['['] = &States::escape;
-}
-
-State *
-StateControl::custom(gchar chr)
-{
- switch (String::toupper(chr)) {
- /*$ ^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.
- */
- case 'C':
- BEGIN_EXEC(&States::start);
- if (undo.enabled)
- throw Error("<^C> not allowed in interactive mode");
- quit_requested = true;
- throw Quit();
-
- /*$ ^O octal
- * ^O -- Set radix to 8 (octal)
- */
- case 'O':
- BEGIN_EXEC(&States::start);
- expressions.set_radix(8);
- break;
-
- /*$ ^D decimal
- * ^D -- Set radix to 10 (decimal)
- */
- case 'D':
- BEGIN_EXEC(&States::start);
- expressions.set_radix(10);
- break;
-
- /*$ ^R radix
- * radix^R -- Set and get radix
- * ^R -> radix
- *
- * Set current radix to arbitrary value <radix>.
- * If <radix> is omitted, the command instead
- * returns the current radix.
- */
- case 'R':
- BEGIN_EXEC(&States::start);
- expressions.eval();
- if (!expressions.args())
- expressions.push(expressions.radix);
- else
- expressions.set_radix(expressions.pop_num_calc());
- break;
-
- /*
- * Additional numeric operations
- */
- /*$ ^_ negate
- * n^_ -> ~n -- Binary negation
- *
- * Binary negates (complements) <n> and returns
- * the result.
- * Binary complements are often used to negate
- * \*(ST booleans.
- */
- case '_':
- BEGIN_EXEC(&States::start);
- expressions.push(~expressions.pop_num_calc());
- break;
-
- case '*':
- BEGIN_EXEC(&States::start);
- expressions.push_calc(Expressions::OP_POW);
- break;
-
- case '/':
- BEGIN_EXEC(&States::start);
- expressions.push_calc(Expressions::OP_MOD);
- break;
-
- case '#':
- BEGIN_EXEC(&States::start);
- expressions.push_calc(Expressions::OP_XOR);
- break;
-
- default:
- throw Error("Unsupported command <^%c>", chr);
- }
-
- return &States::start;
-}
-
-/*$ ^^ ^^c
- * ^^c -> n -- Get ASCII code of character
- *
- * Returns the ASCII code of the character <c>
- * that is part of the command.
- * Can be used in place of integer constants for improved
- * readability.
- * For instance ^^A will return 65.
- *
- * Note that this command can be typed CTRL+Caret or
- * Caret-Caret.
- */
-StateASCII::StateASCII() : State()
-{
- transitions['\0'] = this;
-}
-
-State *
-StateASCII::custom(gchar chr)
-{
- BEGIN_EXEC(&States::start);
-
- expressions.push(chr);
-
- return &States::start;
-}
-
-/*
- * The Escape state is special, as it implements
- * a kind of "lookahead" for the ^[ command (discard all
- * arguments).
- * It is not executed immediately as usual in SciTECO
- * but only if not followed by an escape character.
- * This is necessary since $$ is the macro return
- * and command-line termination command and it must not
- * discard arguments.
- * Deferred execution of ^[ is possible since it does
- * not have any visible side-effects - its effects can
- * only be seen when executing the following command.
- */
-StateEscape::StateEscape()
-{
- transitions['\0'] = this;
-}
-
-State *
-StateEscape::custom(gchar chr)
-{
- /*$ ^[^[ ^[$ $$ 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 == CTL_KEY_ESC || chr == '$') {
- BEGIN_EXEC(&States::start);
- States::current = &States::start;
- expressions.eval();
- throw Return(expressions.args());
- }
-
- /*
- * Alternatives: ^[, <CTRL/[>, <ESC>, $ (dollar)
- */
- /*$ ^[ $ escape discard
- * $ -- Discard all arguments
- * ^[
- *
- * Pops and discards all values from the stack that
- * might otherwise be used as arguments to following
- * commands.
- * Therefore it stops popping on stack boundaries like
- * they are introduced by arithmetic brackets or loops.
- *
- * Note that ^[ is usually typed using the Escape key.
- * CTRL+[ however is possible as well and equivalent to
- * Escape in every manner.
- * The up-arrow notation however is processed like any
- * ordinary command and only works at the begining of
- * a command.
- * Additionally, this command may be written as a single
- * dollar character.
- */
- if (mode == MODE_NORMAL)
- expressions.discard_args();
- return States::start.get_next_state(chr);
-}
-
-void
-StateEscape::end_of_macro(void)
-{
- /*
- * Due to the deferred nature of ^[,
- * it is valid to end in the "escape" state.
- */
- expressions.discard_args();
-}
-
-StateECommand::StateECommand()
-{
- transitions['\0'] = this;
- transitions['%'] = &States::epctcommand;
- transitions['B'] = &States::editfile;
- transitions['C'] = &States::executecommand;
- transitions['G'] = &States::egcommand;
- transitions['I'] = &States::insert_nobuilding;
- transitions['M'] = &States::macro_file;
- transitions['N'] = &States::glob_pattern;
- transitions['S'] = &States::scintilla_symbols;
- transitions['Q'] = &States::eqcommand;
- transitions['U'] = &States::eucommand;
- transitions['W'] = &States::savefile;
-}
-
-State *
-StateECommand::custom(gchar chr)
-{
- switch (String::toupper(chr)) {
- /*$ EF close
- * [bool]EF -- Remove buffer from ring
- * -EF
- *
- * Removes buffer from buffer ring, effectively
- * closing it.
- * If the buffer is dirty (modified), EF 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.
- *
- * It is noteworthy that EF 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.
- */
- case 'F':
- BEGIN_EXEC(&States::start);
- if (QRegisters::current)
- throw Error("Q-Register currently edited");
-
- if (IS_FAILURE(expressions.pop_num_calc()) &&
- ring.current->dirty)
- throw Error("Buffer \"%s\" is dirty",
- ring.current->filename ? : "(Unnamed)");
-
- ring.close();
- break;
-
- /*$ ED flags
- * flags ED -- Set and get ED-flags
- * [off,]on ED
- * ED -> flags
- *
- * With arguments, the command will set the \fBED\fP flags.
- * <flags> is a bitmap of flags to set.
- * Specifying one argument to set the flags is a special
- * case of specifying two arguments that allow to control
- * which flags to enable/disable.
- * <off> is a bitmap of flags to disable (set to 0 in ED
- * flags) and <on> is a bitmap of flags that is ORed into
- * the flags variable.
- * If <off> is omitted, the value 0^_ is implied.
- * In otherwords, all flags are turned off before turning
- * on the <on> flags.
- * Without any argument ED returns the current flags.
- *
- * Currently, the following flags are used by \*(ST:
- * - 8: Enable/disable automatic folding of case-insensitive
- * command characters during interactive key translation.
- * The case of letter keys is inverted, so one or two
- * character commands will typically be inserted upper-case,
- * but you can still press Shift to insert lower-case letters.
- * Case-insensitive Q-Register specifications are not
- * case folded.
- * This is thought to improve the readability of the command
- * line macro.
- * - 16: Enable/disable automatic translation of end of
- * line sequences to and from line feed.
- * - 32: Enable/Disable buffer editing hooks
- * (via execution of macro in global Q-Register \(lqED\(rq)
- * - 64: Enable/Disable function key macros
- * - 128: Enable/Disable enforcement of UNIX98
- * \(lq/bin/sh\(rq emulation for operating system command
- * executions
- * - 256: Enable/Disable \fBxterm\fP(1) clipboard support.
- * Should only be enabled if XTerm allows the
- * \fIGetSelection\fP and \fISetSelection\fP window
- * operations.
- *
- * The features controlled thus are discribed in other sections
- * of this manual.
- *
- * The default value of the \fBED\fP flags is 16
- * (only automatic EOL translation enabled).
- */
- case 'D':
- BEGIN_EXEC(&States::start);
- expressions.eval();
- if (!expressions.args()) {
- expressions.push(Flags::ed);
- } else {
- tecoInt on = expressions.pop_num_calc();
- tecoInt off = expressions.pop_num_calc(0, ~(tecoInt)0);
-
- undo.push_var(Flags::ed);
- Flags::ed = (Flags::ed & ~off) | on;
- }
- break;
-
- /*$ EJ properties
- * [key]EJ -> value -- Get and set system properties
- * -EJ -> value
- * value,keyEJ
- * rgb,color,3EJ
- *
- * This command may be used to get and set system
- * properties.
- * With one argument, it retrieves a numeric property
- * identified by \fIkey\fP.
- * If \fIkey\fP is omitted, the prefix sign is implied
- * (1 or -1).
- * With two arguments, it sets property \fIkey\fP to
- * \fIvalue\fP and returns nothing. Some property \fIkeys\fP
- * may require more than one value. Properties may be
- * write-only or read-only.
- *
- * The following property keys are defined:
- * .IP 0 4
- * The current user interface: 1 for Curses, 2 for GTK
- * (\fBread-only\fP)
- * .IP 1
- * The current numbfer 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.
- * (\fBread-only\fP)
- * .IP 2
- * The current memory limit in bytes.
- * This limit helps to prevent dangerous out-of-memory
- * conditions (e.g. resulting from infinite loops) by
- * constantly sampling the memory requirements of \*(ST.
- * Note that not all platforms support precise measurements
- * of the current memory usage \(em \*(ST will fall back
- * to an approximation which might be less than the actual
- * usage on those platforms.
- * Memory limiting is effective in batch and interactive mode.
- * Commands which would exceed that limit will fail instead
- * allowing users to recover in interactive mode, e.g. by
- * terminating the command line.
- * When getting, a zero value indicates that memory limiting is
- * disabled.
- * Setting a value less than or equal to 0 as in
- * \(lq0,2EJ\(rq disables the limit.
- * \fBWarning:\fP Disabling memory limiting may provoke
- * out-of-memory errors in long running or infinite loops
- * (interactive mode) that result in abnormal program
- * termination.
- * Setting a new limit may fail if the current memory
- * requirements are too large for the new limit \(em if
- * this happens you may have to clear your command-line
- * first.
- * Memory limiting is enabled by default.
- * .IP 3
- * This \fBwrite-only\fP property allows redefining the
- * first 16 entries of the terminal color palette \(em a
- * feature required by some
- * color schemes when using the Curses user interface.
- * When setting this property, you are making a request
- * to define the terminal \fIcolor\fP as the Scintilla-compatible
- * RGB color value given in the \fIrgb\fP parameter.
- * \fIcolor\fP must be a value between 0 and 15
- * corresponding to black, red, green, yellow, blue, magenta,
- * cyan, white, bright black, bright red, etc. in that order.
- * The \fIrgb\fP value has the format 0xBBGGRR, i.e. the red
- * component is the least-significant byte and all other bytes
- * are ignored.
- * Note that on curses, RGB color values sent to Scintilla
- * are actually mapped to these 16 colors by the Scinterm port
- * and may represent colors with no resemblance to the \(lqRGB\(rq
- * value used (depending on the current palette) \(em they should
- * instead be viewed as placeholders for 16 standard terminal
- * color codes.
- * Please refer to the Scinterm manual for details on the allowed
- * \(lqRGB\(rq values and how they map to terminal colors.
- * This command provides a crude way to request exact RGB colors
- * for the first 16 terminal colors.
- * The color definition may be queued or be completely ignored
- * on other user interfaces and no feedback is given
- * if it fails. In fact feedback cannot be given reliably anyway.
- * Note that on 8 color terminals, only the first 8 colors
- * can be redefined (if you are lucky).
- * Note that due to restrictions of most terminal emulators
- * and some curses implementations, this command simply will not
- * restore the original palette entry or request
- * when rubbed out and should generally only be used in
- * \fIbatch-mode\fP \(em typically when loading a color scheme.
- * For the same reasons \(em even though \*(ST tries hard to
- * restore the original palette on exit \(em palette changes may
- * persist after \*(ST terminates on most terminal emulators on Unix.
- * The only emulator which will restore their default palette
- * on exit the author is aware of is \fBxterm\fP(1) and
- * the Linux console driver.
- * You have been warned. Good luck.
- */
- case 'J': {
- BEGIN_EXEC(&States::start);
-
- enum {
- EJ_USER_INTERFACE = 0,
- EJ_BUFFERS,
- EJ_MEMORY_LIMIT,
- EJ_INIT_COLOR
- };
- tecoInt property;
-
- expressions.eval();
- property = expressions.pop_num_calc();
- if (expressions.args() > 0) {
- /* set property */
- tecoInt value = expressions.pop_num_calc();
-
- switch (property) {
- case EJ_MEMORY_LIMIT:
- memlimit.set_limit(MAX(0, value));
- break;
-
- case EJ_INIT_COLOR:
- if (value < 0 || value >= 16)
- throw Error("Invalid color code %" TECO_INTEGER_FORMAT
- " specified for <EJ>", value);
- if (!expressions.args())
- throw ArgExpectedError("EJ");
- interface.init_color((guint)value,
- (guint32)expressions.pop_num_calc());
- break;
-
- default:
- throw Error("Cannot set property %" TECO_INTEGER_FORMAT
- " for <EJ>", property);
- }
-
- break;
- }
-
- switch (property) {
- case EJ_USER_INTERFACE:
-#ifdef INTERFACE_CURSES
- expressions.push(1);
-#elif defined(INTERFACE_GTK)
- expressions.push(2);
-#else
-#error Missing value for current interface!
-#endif
- break;
-
- case EJ_BUFFERS:
- expressions.push(ring.get_id(ring.last()));
- break;
-
- case EJ_MEMORY_LIMIT:
- expressions.push(memlimit.limit);
- break;
-
- default:
- throw Error("Invalid property %" TECO_INTEGER_FORMAT
- " for <EJ>", property);
- }
- break;
- }
-
- /*$ EL eol
- * 0EL -- Set or get End of Line mode
- * 13,10:EL
- * 1EL
- * 13:EL
- * 2EL
- * 10:EL
- * EL -> 0 | 1 | 2
- * :EL -> 13,10 | 13 | 10
- *
- * Sets or gets the current document's End Of Line (EOL) mode.
- * This is a thin wrapper around Scintilla's
- * \fBSCI_SETEOLMODE\fP and \fBSCI_GETEOLMODE\fP messages but is
- * shorter to type and supports restoring the EOL mode upon rubout.
- * Like the Scintilla message, <EL> does \fBnot\fP change the
- * characters in the current document.
- * If automatic EOL translation is activated (which is the default),
- * \*(ST will however use this information when saving files or
- * writing to external processes.
- *
- * With one argument, the EOL mode is set according to these
- * constants:
- * .IP 0 4
- * Carriage return (ASCII 13), followed by line feed (ASCII 10).
- * This is the default EOL mode on DOS/Windows.
- * .IP 1
- * Carriage return (ASCII 13).
- * The default EOL mode on old Mac OS systems.
- * .IP 2
- * Line feed (ASCII 10).
- * The default EOL mode on POSIX/UNIX systems.
- *
- * In its colon-modified form, the EOL mode is set according
- * to the EOL characters on the expression stack.
- * \*(ST will only pop as many values as are necessary to
- * determine the EOL mode.
- *
- * Without arguments, the current EOL mode is returned.
- * When colon-modified, the current EOL mode's character sequence
- * is pushed onto the expression stack.
- */
- case 'L':
- BEGIN_EXEC(&States::start);
-
- expressions.eval();
- if (expressions.args() > 0) {
- gint eol_mode;
-
- if (eval_colon()) {
- switch (expressions.pop_num_calc()) {
- case '\r':
- eol_mode = SC_EOL_CR;
- break;
- case '\n':
- if (!expressions.args()) {
- eol_mode = SC_EOL_LF;
- break;
- }
- if (expressions.pop_num_calc() == '\r') {
- eol_mode = SC_EOL_CRLF;
- break;
- }
- /* fall through */
- default:
- throw Error("Invalid EOL sequence for <EL>");
- }
- } else {
- eol_mode = expressions.pop_num_calc();
- switch (eol_mode) {
- case SC_EOL_CRLF:
- case SC_EOL_CR:
- case SC_EOL_LF:
- break;
- default:
- throw Error("Invalid EOL mode %d for <EL>",
- eol_mode);
- }
- }
-
- interface.undo_ssm(SCI_SETEOLMODE,
- interface.ssm(SCI_GETEOLMODE));
- interface.ssm(SCI_SETEOLMODE, eol_mode);
- } else if (eval_colon()) {
- expressions.push_str(get_eol_seq(interface.ssm(SCI_GETEOLMODE)));
- } else {
- expressions.push(interface.ssm(SCI_GETEOLMODE));
- }
- break;
-
- /*$ EX exit
- * [bool]EX -- Exit program
- * -EX
- * :EX
- *
- * Exits \*(ST, or rather requests program termination
- * at the end of the top-level macro.
- * Therefore instead of exiting immediately which
- * could be annoying in interactive mode, EX will
- * result in program termination only when the command line
- * is terminated.
- * This allows EX to be rubbed out and used in macros.
- * The usual command to exit \*(ST in interactive mode
- * is thus \(lqEX\fB$$\fP\(rq.
- * In batch mode EX will exit the program if control
- * reaches the end of the munged file \(em instead of
- * starting up interactive mode.
- *
- * If any buffer is dirty (modified), EX will yield
- * an error.
- * When specifying <bool> as a success/truth condition
- * boolean, EX will not check whether there are modified
- * buffers and will always succeed.
- * If <bool> is omitted, the sign prefix is implied
- * (1 or -1).
- * In other words \(lq-EX\fB$$\fP\(rq is the usual
- * interactive command sequence to discard all unsaved
- * changes and exit.
- *
- * When colon-modified, <bool> is ignored and EX
- * will instead immediately try to save all modified buffers \(em
- * this can of course be reversed using rubout.
- * Saving all buffers can fail, e.g. if the unnamed file
- * is modified or if there is an IO error.
- * \(lq:EX\fB$$\fP\(rq is nevertheless the usual interactive
- * command sequence to exit while saving all modified
- * buffers.
- */
- /** @bug what if changing file after EX? will currently still exit */
- case 'X':
- BEGIN_EXEC(&States::start);
-
- if (eval_colon())
- ring.save_all_dirty_buffers();
- else if (IS_FAILURE(expressions.pop_num_calc()) &&
- ring.is_any_dirty())
- throw Error("Modified buffers exist");
-
- undo.push_var(quit_requested) = true;
- break;
-
- default:
- throw SyntaxError(chr);
- }
-
- return &States::start;
-}
-
-static struct ScintillaMessage {
- unsigned int iMessage;
- uptr_t wParam;
- sptr_t lParam;
-} scintilla_message = {0, 0, 0};
-
-/*$ ES scintilla message
- * -- Send Scintilla message
- * [lParam[,wParam]]ESmessage[,wParam]$[lParam]$ -> result
- *
- * Send Scintilla message with code specified by symbolic
- * name <message>, <wParam> and <lParam>.
- * <wParam> may be symbolic when specified as part of the
- * first string argument.
- * If not it is popped from the stack.
- * <lParam> may be specified as a constant string whose
- * pointer is passed to Scintilla if specified as the second
- * string argument.
- * If the second string argument is empty, <lParam> is popped
- * from the stack instead.
- * Parameters popped from the stack may be omitted, in which
- * case 0 is implied.
- * The message's return value is pushed onto the stack.
- *
- * All messages defined by Scintilla (as C macros) can be
- * used by passing their name as a string to ES
- * (e.g. ESSCI_LINESONSCREEN...).
- * The \(lqSCI_\(rq prefix may be omitted and message symbols
- * are case-insensitive.
- * Only the Scintilla lexer symbols (SCLEX_..., SCE_...)
- * may be used symbolically with the ES command as <wParam>,
- * other values must be passed as integers on the stack.
- * In interactive mode, symbols may be auto-completed by
- * pressing Tab.
- * String-building characters are by default interpreted
- * in the string arguments.
- *
- * .BR Warning :
- * Almost all Scintilla messages may be dispatched using
- * this command.
- * \*(ST does not keep track of the editor state changes
- * performed by these commands and cannot undo them.
- * You should never use it to change the editor state
- * (position changes, deletions, etc.) or otherwise
- * rub out will result in an inconsistent editor state.
- * There are however exceptions:
- * - In the editor profile and batch mode in general,
- * the ES command may be used freely.
- * - In the ED hook macro (register \(lqED\(rq),
- * when a file is added to the ring, most destructive
- * operations can be performed since rubbing out the
- * EB command responsible for the hook execution also
- * removes the buffer from the ring again.
- */
-State *
-StateScintilla_symbols::done(const gchar *str)
-{
- BEGIN_EXEC(&States::scintilla_lparam);
-
- undo.push_var(scintilla_message);
- if (*str) {
- gchar **symbols = g_strsplit(str, ",", -1);
- tecoInt v;
-
- if (!symbols[0])
- goto cleanup;
- if (*symbols[0]) {
- v = Symbols::scintilla.lookup(symbols[0], "SCI_");
- if (v < 0)
- throw Error("Unknown Scintilla message symbol \"%s\"",
- symbols[0]);
- scintilla_message.iMessage = v;
- }
-
- if (!symbols[1])
- goto cleanup;
- if (*symbols[1]) {
- v = Symbols::scilexer.lookup(symbols[1]);
- if (v < 0)
- throw Error("Unknown Scintilla Lexer symbol \"%s\"",
- symbols[1]);
- scintilla_message.wParam = v;
- }
-
- if (!symbols[2])
- goto cleanup;
- if (*symbols[2]) {
- v = Symbols::scilexer.lookup(symbols[2]);
- if (v < 0)
- throw Error("Unknown Scintilla Lexer symbol \"%s\"",
- symbols[2]);
- scintilla_message.lParam = v;
- }
-
-cleanup:
- g_strfreev(symbols);
- }
-
- expressions.eval();
- if (!scintilla_message.iMessage) {
- if (!expressions.args())
- throw Error("<ES> command requires at least a message code");
-
- scintilla_message.iMessage = expressions.pop_num_calc(0, 0);
- }
- if (!scintilla_message.wParam)
- scintilla_message.wParam = expressions.pop_num_calc(0, 0);
-
- return &States::scintilla_lparam;
-}
-
-State *
-StateScintilla_lParam::done(const gchar *str)
-{
- BEGIN_EXEC(&States::start);
-
- if (!scintilla_message.lParam)
- scintilla_message.lParam = *str ? (sptr_t)str
- : expressions.pop_num_calc(0, 0);
-
- expressions.push(interface.ssm(scintilla_message.iMessage,
- scintilla_message.wParam,
- scintilla_message.lParam));
-
- undo.push_var(scintilla_message);
- memset(&scintilla_message, 0, sizeof(scintilla_message));
-
- return &States::start;
-}
-
-/*
- * NOTE: cannot support VideoTECO's <n>I because
- * beginning and end of strings must be determined
- * syntactically
- */
-/*$ I insert
- * [c1,c2,...]I[text]$ -- Insert text with string building characters
- *
- * First inserts characters for all the values
- * on the argument stack (interpreted as codepoints).
- * It does so in the order of the arguments, i.e.
- * <c1> is inserted before <c2>, ecetera.
- * Secondly, the command inserts <text>.
- * In interactive mode, <text> is inserted interactively.
- *
- * String building characters are \fBenabled\fP for the
- * I command.
- * When editing \*(ST macros, using the \fBEI\fP command
- * may be better, since it has string building characters
- * disabled.
- */
-/*$ 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.
- */
-void
-StateInsert::initial(void)
-{
- guint args;
-
- expressions.eval();
- args = expressions.args();
- if (!args)
- return;
-
- interface.ssm(SCI_BEGINUNDOACTION);
- for (int i = args; i > 0; i--) {
- gchar chr = (gchar)expressions.peek_num(i-1);
- interface.ssm(SCI_ADDTEXT, 1, (sptr_t)&chr);
- }
- for (int i = args; i > 0; i--)
- expressions.pop_num_calc();
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
-}
-
-void
-StateInsert::process(const gchar *str, gint new_chars)
-{
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_ADDTEXT, new_chars,
- (sptr_t)(str + strlen(str) - new_chars));
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
-}
-
-State *
-StateInsert::done(const gchar *str)
-{
- /* nothing to be done when done */
- return &States::start;
-}
-
-/*
- * Alternatives: ^i, ^I, <CTRL/I>, <TAB>
- */
-/*$ ^I indent
- * [char,...]^I[text]$ -- Insert with leading indention
- *
- * ^I (usually typed using the Tab key), first inserts
- * all the chars on the stack into the buffer, then indention
- * characters (one tab or multiple spaces) and eventually
- * the optional <text> is inserted interactively.
- * It is thus a derivate of the \fBI\fP (insertion) command.
- *
- * \*(ST uses Scintilla settings to determine the indention
- * characters.
- * If tab use is enabled with the \fBSCI_SETUSETABS\fP message,
- * a single tab character is inserted.
- * Tab use is enabled by default.
- * Otherwise, a number of spaces is inserted up to the
- * next tab stop so that the command's <text> argument
- * is inserted at the beginning of the next tab stop.
- * The size of the tab stops is configured by the
- * \fBSCI_SETTABWIDTH\fP Scintilla message (8 by default).
- * In combination with \*(ST's use of the tab key as an
- * immediate editing command for all insertions, this
- * implements support for different insertion styles.
- * The Scintilla settings apply to the current Scintilla
- * document and are thus local to the currently edited
- * buffer or Q-Register.
- *
- * However for the same reason, the ^I command is not
- * fully compatible with classic TECO which \fIalways\fP
- * inserts a single tab character and should not be used
- * for the purpose of inserting single tabs in generic
- * macros.
- * To insert a single tab character reliably, the idioms
- * \(lq9I$\(rq or \(lqI^I$\(rq may be used.
- *
- * Like the I command, ^I has string building characters
- * \fBenabled\fP.
- */
-void
-StateInsertIndent::initial(void)
-{
- StateInsert::initial();
-
- interface.ssm(SCI_BEGINUNDOACTION);
- if (interface.ssm(SCI_GETUSETABS)) {
- interface.ssm(SCI_ADDTEXT, 1, (sptr_t)"\t");
- } else {
- gint len = interface.ssm(SCI_GETTABWIDTH);
-
- len -= interface.ssm(SCI_GETCOLUMN,
- interface.ssm(SCI_GETCURRENTPOS)) % len;
-
- gchar spaces[len];
-
- memset(spaces, ' ', sizeof(spaces));
- interface.ssm(SCI_ADDTEXT, sizeof(spaces), (sptr_t)spaces);
- }
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
-}
-
-} /* namespace SciTECO */
diff --git a/src/parser.h b/src/parser.h
index 9255268..b594edc 100644
--- a/src/parser.h
+++ b/src/parser.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,47 +14,130 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __PARSER_H
-#define __PARSER_H
-
-#include <string.h>
+#pragma once
#include <glib.h>
+#include <Scintilla.h>
+
#include "sciteco.h"
-#include "memory.h"
-#include "undo.h"
-#include "error.h"
-#include "expressions.h"
-
-namespace SciTECO {
-
-/* TECO uses only lower 7 bits for commands */
-#define MAX_TRANSITIONS 127
-
-class State : public Object {
-protected:
- /* static transitions */
- State *transitions[MAX_TRANSITIONS];
-
- inline void
- init(const gchar *chars, State &state)
- {
- while (*chars)
- transitions[(int)*chars++] = &state;
- }
- inline void
- init(const gchar *chars)
- {
- init(chars, *this);
- }
+#include "string-utils.h"
+#include "goto.h"
+#include "qreg.h"
+
+/*
+ * Forward Declarations
+ */
+typedef const struct teco_state_t teco_state_t;
+typedef struct teco_machine_t teco_machine_t;
+typedef struct teco_machine_main_t teco_machine_main_t;
+
+typedef struct {
+ /** how many iterations are left */
+ teco_int_t counter;
+ /** Program counter of loop start command */
+ guint pc : sizeof(guint)*8 - 1;
+ /**
+ * Whether the loop represents an argument
+ * barrier or not (it "passes through"
+ * stack arguments).
+ *
+ * Since the program counter is usually
+ * a signed integer, it's ok steal one
+ * bit for the pass_through flag.
+ */
+ gboolean pass_through : 1;
+} teco_loop_context_t;
+
+extern GArray *teco_loop_stack;
+
+void undo__insert_val__teco_loop_stack(guint, teco_loop_context_t);
+void undo__remove_index__teco_loop_stack(guint);
+
+/**
+ * @defgroup states Parser states
+ *
+ * Parser states are defined as global constants using the TECO_DEFINE_STATE()
+ * macro, allowing individual fields and callbacks to be overwritten.
+ * Derived macros are defined to factor out common fields and settings.
+ * States therefore form a hierarchy, which is documented using
+ * \@interface and \@implements tags.
+ *
+ * @{
+ */
-public:
- State();
+/*
+ * FIXME: Remove _cb from all callback names. See qreg.h.
+ * FIXME: Maybe use TECO_DECLARE_VTABLE_METHOD()?
+ */
+typedef const struct {
+ gboolean string_building : 1;
+ gboolean last : 1;
- static void input(gchar chr);
- State *get_next_state(gchar chr);
+ /**
+ * Called repeatedly to process chunks of input and give interactive feedback.
+ *
+ * Can be NULL if no interactive feedback is required.
+ */
+ gboolean (*process_cb)(teco_machine_main_t *ctx, const teco_string_t *str,
+ gsize new_chars, GError **error);
+
+ /**
+ * Called at the end of the string argument to determine the next state.
+ * 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_expectstring_t;
+
+typedef const struct {
+ teco_qreg_type_t type;
+
+ /** Called when a register specification has been successfully parsed. */
+ teco_state_t *(*got_register_cb)(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error);
+} teco_state_expectqreg_t;
+
+typedef gboolean (*teco_state_initial_cb_t)(teco_machine_t *ctx, GError **error);
+typedef teco_state_t *(*teco_state_input_cb_t)(teco_machine_t *ctx, gchar chr, GError **error);
+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,
+ gchar key, GError **error);
+
+typedef enum {
+ TECO_FNMACRO_MASK_START = (1 << 0),
+ TECO_FNMACRO_MASK_STRING = (1 << 1),
+ TECO_FNMACRO_MASK_DEFAULT = ~((1 << 2)-1)
+} teco_fnmacro_mask_t;
+
+/**
+ * A teco_machine_t state.
+ * These are declared as constants using TECO_DEFINE_STATE() and friends.
+ *
+ * @note Unless you don't want to manually "upcast" the teco_machine_t* in
+ * callback implementations, you will have to cast your callback types when initializing
+ * the teco_state_t vtables.
+ * Casting to functions of different signature is theoretically undefined behavior,
+ * but works on all major platforms including Emscripten, as long as they differ only
+ * in pointer types.
+ */
+struct teco_state_t {
+ /**
+ * Called the first time this state is entered.
+ * Theoretically, you can use teco_machine_main_transition_t instead,
+ * but this callback improves reusability.
+ *
+ * It can be NULL if not required.
+ */
+ teco_state_initial_cb_t initial_cb;
+
+ /**
+ * Get next state given an input character.
+ *
+ * This is a mandatory field.
+ */
+ teco_state_input_cb_t input_cb;
/**
* Provide interactive feedback.
@@ -63,32 +146,20 @@ public:
* immediate interactive feedback should provide that
* feedback; allowing them to optimize batch mode,
* macro and many other cases.
+ *
+ * It can be NULL if not required.
*/
- virtual void refresh(void) {}
+ teco_state_refresh_cb_t refresh_cb;
/**
* Called at the end of a macro.
* Most states/commands are not allowed to end unterminated
* at the end of a macro.
+ *
+ * It can be NULL if not required.
*/
- virtual void
- end_of_macro(void)
- {
- throw Error("Unterminated command");
- }
+ teco_state_end_of_macro_cb_t end_of_macro_cb;
-protected:
- static bool eval_colon(void);
-
- /** Get next state given an input character */
- virtual State *
- custom(gchar chr)
- {
- throw SyntaxError(chr);
- return NULL;
- }
-
-public:
/**
* Process editing command (or key press).
*
@@ -97,383 +168,411 @@ public:
* editing commands (behaviour on key press).
*
* By implementing this method, sub-states can either
- * handle a key and return or chain to the
- * parent's process_edit_cmd() implementation.
+ * handle a key and return, chain to the
+ * parent's process_edit_cmd() implementation or even
+ * to the parent state machine's handler.
*
* All implementations of this method are defined in
- * cmdline.cpp.
+ * cmdline.c.
+ *
+ * This is a mandatory field.
*/
- virtual void process_edit_cmd(gchar key);
-
- enum fnmacroMask {
- FNMACRO_MASK_START = (1 << 0),
- FNMACRO_MASK_STRING = (1 << 1),
- FNMACRO_MASK_DEFAULT = ~((1 << 2)-1)
- };
+ teco_state_process_edit_cmd_cb_t process_edit_cmd_cb;
/**
- * Get the function key macro mask this
- * state refers to.
+ * Whether this state is a start state (ie. not within any
+ * escape sequence etc.).
+ * This is separate of TECO_FNMACRO_MASK_START which is set
+ * only in the main machine's start states.
+ */
+ gboolean is_start : 1;
+ /**
+ * Function key macro mask.
+ * This is not a bitmask since it is compared with values set
+ * from TECO, so the bitorder needs to be defined.
*
- * Could also be modelled as a State member.
+ * @fixme If we intend to "forward" masks from other state machines like
+ * teco_machine_stringbuilding_t, this should probably be a callback.
*/
- virtual fnmacroMask
- get_fnmacro_mask(void) const
- {
- return FNMACRO_MASK_DEFAULT;
- }
-};
+ teco_fnmacro_mask_t fnmacro_mask : 8;
-/**
- * Base class of states with case-insenstive input.
- *
- * This is meant for states accepting command characters
- * that can possibly be case-folded.
- */
-class StateCaseInsensitive : public State {
-protected:
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-template <typename Type>
-class MicroStateMachine : public Object {
-protected:
- /* label pointers */
- typedef const void *MicroState;
- const MicroState StateStart;
-#define MICROSTATE_START G_STMT_START { \
- if (this->state != StateStart) \
- goto *this->state; \
-} G_STMT_END
-
- MicroState state;
-
- inline void
- set(MicroState next)
- {
- if (next != state)
- undo.push_var(state) = next;
- }
-
-public:
- MicroStateMachine() : StateStart(NULL), state(StateStart) {}
- virtual ~MicroStateMachine() {}
-
- virtual inline void
- reset(void)
- {
- set(StateStart);
- }
-
- virtual bool input(gchar chr, Type &result) = 0;
+ /**
+ * Additional state-dependent callbacks and settings.
+ * This wastes some bytes compared to other techniques for extending teco_state_t
+ * but this is acceptable since there is only a limited number of constant instances.
+ * The main advantage of this approach is that we can use a single
+ * TECO_DEFINE_STATE() for defining and deriving all defaults.
+ */
+ union {
+ teco_state_expectstring_t expectstring;
+ teco_state_expectqreg_t expectqreg;
+ };
};
-/* avoid circular dependency on qregisters.h */
-class QRegSpecMachine;
-
-class StringBuildingMachine : public MicroStateMachine<gchar *> {
- enum Mode {
- MODE_NORMAL,
- MODE_UPPER,
- MODE_LOWER
- } mode;
-
- bool toctl;
-
-public:
- QRegSpecMachine *qregspec_machine;
+/** @} */
- StringBuildingMachine() : MicroStateMachine<gchar *>(),
- mode(MODE_NORMAL), toctl(false),
- qregspec_machine(NULL) {}
- ~StringBuildingMachine();
+gboolean teco_state_end_of_macro(teco_machine_t *ctx, GError **error);
- void reset(void);
+/* in cmdline.c */
+gboolean teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error);
- bool input(gchar chr, gchar *&result);
-};
-
-/*
- * Super-class for states accepting string arguments
- * Opaquely cares about alternative-escape characters,
- * string building commands and accumulation into a string
+/**
+ * @interface TECO_DEFINE_STATE
+ * @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.
*/
-class StateExpectString : public State {
- gsize insert_len;
-
- gint nesting;
-
- bool string_building;
- bool last;
-
- StringBuildingMachine machine;
-
-public:
- StateExpectString(bool _building = true, bool _last = true)
- : insert_len(0), nesting(1),
- string_building(_building), last(_last) {}
-
-private:
- State *custom(gchar chr);
- void refresh(void);
-
- virtual fnmacroMask
- get_fnmacro_mask(void) const
- {
- return FNMACRO_MASK_STRING;
+#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, \
+ .is_start = FALSE, \
+ .fnmacro_mask = TECO_FNMACRO_MASK_DEFAULT, \
+ ##__VA_ARGS__ \
}
-protected:
- virtual void initial(void) {}
- virtual void process(const gchar *str, gint new_chars) {}
- virtual State *done(const gchar *str) = 0;
-
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-class StateExpectFile : public StateExpectString {
-public:
- StateExpectFile(bool _building = true, bool _last = true)
- : StateExpectString(_building, _last) {}
-
-private:
- State *done(const gchar *str);
-
-protected:
- virtual State *got_file(const gchar *filename) = 0;
+/** @ingroup states */
+#define TECO_DECLARE_STATE(NAME) \
+ extern teco_state_t NAME
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
+/* in cmdline.c */
+gboolean teco_state_caseinsensitive_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error);
-class StateExpectDir : public StateExpectFile {
-public:
- StateExpectDir(bool _building = true, bool _last = true)
- : StateExpectFile(_building, _last) {}
+/**
+ * @interface TECO_DEFINE_STATE_CASEINSENSITIVE
+ * @implements TECO_DEFINE_STATE
+ * @ingroup states
+ *
+ * Base class of states with case-insenstive input.
+ *
+ * This is meant for states accepting command characters
+ * that can possibly be case-folded.
+ */
+#define TECO_DEFINE_STATE_CASEINSENSITIVE(NAME, ...) \
+ TECO_DEFINE_STATE(NAME, \
+ .process_edit_cmd_cb = teco_state_caseinsensitive_process_edit_cmd, \
+ ##__VA_ARGS__ \
+ )
-protected:
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
+/**
+ * Base class of state machine.
+ *
+ * @note On extending teco_machine_t:
+ * There is `-fplan9-extensions`, but Clang doesn't support it.
+ * There is `-fms-extensions`, but that would require type-unsafe
+ * casting to teco_machine_t*.
+ * It's possible to portably implement typesafe inheritance by using
+ * an anonymous union of an anonymous struct and a named struct, but it's
+ * not really worth the trouble in our flat "class" hierachy.
+ */
+struct teco_machine_t {
+ teco_state_t *current;
+ /**
+ * Whether side effects must be reverted on rubout.
+ * State machines created within macro calls don't have to
+ * even in interactive mode.
+ */
+ gboolean must_undo;
};
-class StateStart : public StateCaseInsensitive {
-public:
- StateStart();
+static inline void
+teco_machine_init(teco_machine_t *ctx, teco_state_t *initial, gboolean must_undo)
+{
+ ctx->current = initial;
+ ctx->must_undo = must_undo;
+}
-private:
- void insert_integer(tecoInt v);
- tecoInt read_integer(void);
+static inline void
+teco_machine_reset(teco_machine_t *ctx, teco_state_t *initial)
+{
+ if (ctx->current != initial)
+ teco_undo_ptr(ctx->current) = initial;
+}
- tecoBool move_chars(tecoInt n);
- tecoBool move_lines(tecoInt n);
+gboolean teco_machine_input(teco_machine_t *ctx, gchar chr, GError **error);
- tecoBool delete_words(tecoInt n);
+typedef enum {
+ TECO_STRINGBUILDING_MODE_NORMAL = 0,
+ TECO_STRINGBUILDING_MODE_UPPER,
+ TECO_STRINGBUILDING_MODE_LOWER
+} teco_stringbuilding_mode_t;
- State *custom(gchar chr);
+/**
+ * A stringbuilding state machine.
+ *
+ * @fixme Should contain the escape char (currently in teco_machine_expectstring_t),
+ * so that we can escape it via ^Q.
+ *
+ * @extends teco_machine_t
+ */
+typedef struct teco_machine_stringbuilding_t {
+ teco_machine_t parent;
- void end_of_macro(void) {}
+ /**
+ * A teco_stringbuilding_mode_t.
+ * This is still a guint, so you can call teco_undo_guint().
+ */
+ guint mode;
- fnmacroMask
- get_fnmacro_mask(void) const
- {
- return FNMACRO_MASK_START;
- }
-};
+ /**
+ * The escape/termination character.
+ *
+ * If this is `[` or `{`, it is assumed that `]` and `}` must
+ * be escaped as well by teco_machine_stringbuilding_escape().
+ */
+ gchar escape_char;
-class StateControl : public StateCaseInsensitive {
-public:
- StateControl();
+ /**
+ * Q-Register table for local registers.
+ * This is stored here only to be passed to the Q-Reg spec machine.
+ */
+ teco_qreg_table_t *qreg_table_locals;
-private:
- State *custom(gchar chr);
-};
+ /**
+ * A QRegister specification parser.
+ * It is allocated since it in turn contains a string building machine.
+ */
+ teco_machine_qregspec_t *machine_qregspec;
-class StateASCII : public State {
-public:
- StateASCII();
+ /**
+ * A string to append characters to or NULL in parse-only mode.
+ *
+ * @bug As a side-effect, rubbing out in parse-only mode is severely limited
+ * (see teco_state_stringbuilding_start_process_edit_cmd()).
+ */
+ teco_string_t *result;
+} teco_machine_stringbuilding_t;
-private:
- State *custom(gchar chr);
-};
+void teco_machine_stringbuilding_init(teco_machine_stringbuilding_t *ctx, gchar escape_char,
+ teco_qreg_table_t *locals, gboolean must_undo);
-class StateEscape : public StateCaseInsensitive {
-public:
- StateEscape();
+void teco_machine_stringbuilding_reset(teco_machine_stringbuilding_t *ctx);
-private:
- State *custom(gchar chr);
+/**
+ * Parse a string building character.
+ *
+ * @param ctx The string building machine.
+ * @param chr The character to parse.
+ * @param result String to append characters to or NULL in parse-only mode.
+ * @param error GError.
+ * @return FALSE in case of error.
+ */
+static inline gboolean
+teco_machine_stringbuilding_input(teco_machine_stringbuilding_t *ctx, gchar chr,
+ teco_string_t *result, GError **error)
+{
+ ctx->result = result;
+ return teco_machine_input(&ctx->parent, chr, error);
+}
- void end_of_macro(void);
+void teco_machine_stringbuilding_escape(teco_machine_stringbuilding_t *ctx, const gchar *str, gsize len,
+ teco_string_t *target);
- /*
- * The state should behave like StateStart
- * when it comes to function key macro masking.
- */
- fnmacroMask
- get_fnmacro_mask(void) const
- {
- return FNMACRO_MASK_START;
- }
-};
+void teco_machine_stringbuilding_clear(teco_machine_stringbuilding_t *ctx);
-class StateFCommand : public StateCaseInsensitive {
-public:
- StateFCommand();
+/**
+ * Peristent state for teco_state_expectstring_input().
+ *
+ * This is part of the main machine instead of being a global variable,
+ * so that parsers can be run in parallel.
+ *
+ * Since it will also be part of a macro invocation frame, it will allow
+ * for tricks like macro-hooks while in "expectstring" states or calling
+ * macros as part of string building characters or macro string arguments.
+ */
+typedef struct {
+ teco_string_t string;
+ gsize insert_len;
+ gint nesting;
-private:
- State *custom(gchar chr);
-};
+ teco_machine_stringbuilding_t machine;
+} teco_machine_expectstring_t;
-class UndoTokenChangeDir : public UndoToken {
- gchar *dir;
+/**
+ * Scintilla message for collection by ES commands.
+ *
+ * @fixme This is a "forward" declaration, so that we don't introduce cyclic
+ * header dependencies.
+ * Could presumably be avoided by splitting parser.h in two.
+ */
+typedef struct {
+ unsigned int iMessage;
+ uptr_t wParam;
+ sptr_t lParam;
+} teco_machine_scintilla_t;
+
+typedef enum {
+ /** Normal parsing - ie. execute while parsing */
+ TECO_MODE_NORMAL = 0,
+ /** Parse, but don't execute until reaching not-yet-defined Goto-label */
+ TECO_MODE_PARSE_ONLY_GOTO,
+ /** Parse, but don't execute until reaching end of loop */
+ TECO_MODE_PARSE_ONLY_LOOP,
+ /** Parse, but don't execute until reaching end of conditional or its else-clause */
+ TECO_MODE_PARSE_ONLY_COND,
+ /** Parse, but don't execute until reaching the very end of conditional */
+ TECO_MODE_PARSE_ONLY_COND_FORCE
+} teco_mode_t;
+
+/** @extends teco_machine_t */
+struct teco_machine_main_t {
+ teco_machine_t parent;
+
+ gint macro_pc;
-public:
/**
- * Construct undo token.
- *
- * This passes ownership of the directory string
- * to the undo token object.
+ * Aliases bitfield with an integer.
+ * This allows teco_undo_guint(__flags),
+ * while still supporting easy-to-access flags.
*/
- UndoTokenChangeDir(gchar *_dir) : dir(_dir) {}
- ~UndoTokenChangeDir()
- {
- g_free(dir);
- }
-
- void run(void);
-};
+ union {
+ struct {
+ teco_mode_t mode : 8;
+
+ gboolean modifier_colon : 1;
+ gboolean modifier_at : 1;
+ };
+ guint __flags;
+ };
-class StateChangeDir : public StateExpectDir {
-private:
- State *got_file(const gchar *filename);
-};
+ /** The nesting level of braces */
+ guint brace_level;
+ /** The nesting level of loops and control structures */
+ gint nest_level;
+ /**
+ * Loop frame pointer: The number of elements on
+ * the loop stack when a macro invocation frame is
+ * created.
+ * This is used to perform checks for flow control
+ * commands to avoid jumping with invalid PCs while
+ * not creating a new stack per macro frame.
+ */
+ guint loop_stack_fp;
-class StateCondCommand : public StateCaseInsensitive {
-public:
- StateCondCommand();
+ teco_goto_table_t goto_table;
+ teco_qreg_table_t *qreg_table_locals;
-private:
- State *custom(gchar chr);
+ /*
+ * 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.
+ */
+ teco_machine_expectstring_t expectstring;
+ union {
+ teco_string_t goto_label;
+ teco_machine_qregspec_t *expectqreg;
+ teco_machine_scintilla_t scintilla;
+ };
};
-class StateECommand : public StateCaseInsensitive {
-public:
- StateECommand();
-
-private:
- State *custom(gchar chr);
-};
+void teco_machine_main_init(teco_machine_main_t *ctx,
+ teco_qreg_table_t *qreg_table_locals,
+ gboolean must_undo);
-class StateScintilla_symbols : public StateExpectString {
-public:
- StateScintilla_symbols() : StateExpectString(true, false) {}
+gboolean teco_machine_main_eval_colon(teco_machine_main_t *ctx);
-private:
- State *done(const gchar *str);
+gboolean teco_machine_main_step(teco_machine_main_t *ctx,
+ const gchar *macro, gint stop_pos, GError **error);
-protected:
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
+gboolean teco_execute_macro(const gchar *macro, gsize macro_len,
+ teco_qreg_table_t *qreg_table_locals, GError **error);
+gboolean teco_execute_file(const gchar *filename, teco_qreg_table_t *qreg_table_locals, GError **error);
-class StateScintilla_lParam : public StateExpectString {
-private:
- State *done(const gchar *str);
-};
+typedef const struct {
+ teco_state_t *next;
+ void (*transition_cb)(teco_machine_main_t *ctx, GError **error);
+} teco_machine_main_transition_t;
/*
- * also serves as base class for replace-insertion states
+ * FIXME: There should probably be a teco_state_plain with
+ * the transitions and their length being stored in
+ * teco_state_t::transitions.
+ * This does not exclude the possibility of overwriting input_cb.
*/
-class StateInsert : public StateExpectString {
-public:
- StateInsert(bool building = true)
- : StateExpectString(building) {}
-
-protected:
- void initial(void);
- void process(const gchar *str, gint new_chars);
- State *done(const gchar *str);
-
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-class StateInsertIndent : public StateInsert {
-protected:
- void initial(void);
-};
-
-namespace States {
- extern StateStart start;
- extern StateControl control;
- extern StateASCII ascii;
- extern StateEscape escape;
- extern StateFCommand fcommand;
- extern StateChangeDir changedir;
- extern StateCondCommand condcommand;
- extern StateECommand ecommand;
- extern StateScintilla_symbols scintilla_symbols;
- extern StateScintilla_lParam scintilla_lparam;
- extern StateInsert insert_building;
- extern StateInsert insert_nobuilding;
- extern StateInsertIndent insert_indent;
-
- extern State *current;
-
- static inline bool
- is_start(void)
- {
- /*
- * StateEscape should behave very much like StateStart.
- */
- return current == &start || current == &escape;
- }
-}
+teco_state_t *teco_machine_main_transition_input(teco_machine_main_t *ctx,
+ teco_machine_main_transition_t *transitions,
+ guint len, gchar chr, GError **error);
-extern enum Mode {
- MODE_NORMAL = 0,
- MODE_PARSE_ONLY_GOTO,
- MODE_PARSE_ONLY_LOOP,
- MODE_PARSE_ONLY_COND
-} mode;
+void teco_machine_main_clear(teco_machine_main_t *ctx);
-#define BEGIN_EXEC(STATE) G_STMT_START { \
- if (mode > MODE_NORMAL) \
- return STATE; \
-} G_STMT_END
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_machine_main_t, teco_machine_main_clear);
-extern gint macro_pc;
+teco_state_t *teco_state_expectstring_input(teco_machine_main_t *ctx, gchar chr, GError **error);
+gboolean teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error);
-extern gchar *strings[2];
-extern gchar escape_char;
+/* in cmdline.c */
+gboolean teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error);
-struct LoopContext {
- /** how many iterations are left */
- tecoInt counter;
- /** Program counter of loop start command */
- guint pc : sizeof(guint)*8 - 1;
- /**
- * Whether the loop represents an argument
- * barrier or not (it "passes through"
- * stack arguments).
- *
- * Since the program counter is usually
- * a signed integer, it's ok steal one
- * bit for the pass_through flag.
- */
- bool pass_through : 1;
-};
-typedef ValueStack<LoopContext> LoopStack;
-extern LoopStack loop_stack;
+/**
+ * @interface TECO_DEFINE_STATE_EXPECTSTRING
+ * @implements TECO_DEFINE_STATE
+ * @ingroup states
+ *
+ * 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, gchar chr, GError **error) \
+ { \
+ return teco_state_expectstring_input(ctx, chr, error); \
+ } \
+ TECO_DEFINE_STATE(NAME, \
+ .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, \
+ .fnmacro_mask = TECO_FNMACRO_MASK_STRING, \
+ .expectstring.string_building = TRUE, \
+ .expectstring.last = TRUE, \
+ .expectstring.process_cb = NULL, /* do nothing */ \
+ .expectstring.done_cb = NAME##_done, /* always required */ \
+ ##__VA_ARGS__ \
+ )
+
+gboolean teco_state_expectfile_process(teco_machine_main_t *ctx, const 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, gchar key, GError **error);
-namespace Execute {
- void step(const gchar *macro, gint stop_pos);
- void macro(const gchar *macro, bool locals = true);
- void file(const gchar *filename, bool locals = true);
-}
+/**
+ * @interface TECO_DEFINE_STATE_EXPECTFILE
+ * @implements TECO_DEFINE_STATE_EXPECTSTRING
+ * @ingroup states
+ */
+#define TECO_DEFINE_STATE_EXPECTFILE(NAME, ...) \
+ TECO_DEFINE_STATE_EXPECTSTRING(NAME, \
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
+ teco_state_expectfile_process_edit_cmd, \
+ .expectstring.process_cb = teco_state_expectfile_process, \
+ ##__VA_ARGS__ \
+ )
-} /* namespace SciTECO */
+/* in cmdline.c */
+gboolean teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error);
-#endif
+/**
+ * @interface TECO_DEFINE_STATE_EXPECTDIR
+ * @implements TECO_DEFINE_STATE_EXPECTFILE
+ * @ingroup states
+ */
+#define TECO_DEFINE_STATE_EXPECTDIR(NAME, ...) \
+ TECO_DEFINE_STATE_EXPECTFILE(NAME, \
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
+ teco_state_expectdir_process_edit_cmd, \
+ ##__VA_ARGS__ \
+ )
diff --git a/src/qreg-commands.c b/src/qreg-commands.c
new file mode 100644
index 0000000..35508d7
--- /dev/null
+++ b/src/qreg-commands.c
@@ -0,0 +1,760 @@
+/*
+ * 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 <Scintilla.h>
+
+#include "sciteco.h"
+#include "error.h"
+#include "file-utils.h"
+#include "expressions.h"
+#include "interface.h"
+#include "ring.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "qreg.h"
+#include "qreg-commands.h"
+
+gboolean
+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.
+ */
+ ctx->expectqreg = teco_machine_qregspec_new(current->expectqreg.type, ctx->qreg_table_locals,
+ ctx->parent.must_undo);
+ if (ctx->parent.must_undo)
+ undo__teco_machine_qregspec_free(ctx->expectqreg);
+ return TRUE;
+}
+
+teco_state_t *
+teco_state_expectqreg_input(teco_machine_main_t *ctx, gchar chr, GError **error)
+{
+ teco_state_t *current = ctx->parent.current;
+
+ teco_qreg_t *qreg;
+ teco_qreg_table_t *table;
+
+ switch (teco_machine_qregspec_input(ctx->expectqreg, chr,
+ ctx->mode == TECO_MODE_NORMAL ? &qreg : NULL, &table, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+ case TECO_MACHINE_QREGSPEC_MORE:
+ return current;
+ case TECO_MACHINE_QREGSPEC_DONE:
+ break;
+ }
+
+ /*
+ * 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:
+ * teco_state_expectqreg_reset(ctx);
+ */
+ return current->expectqreg.got_register_cb(ctx, qreg, table, error);
+}
+
+static teco_state_t *
+teco_state_pushqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ return ctx->mode == TECO_MODE_NORMAL &&
+ !teco_qreg_stack_push(qreg, error) ? NULL : &teco_state_start;
+}
+
+/*$ "[" "[q" push
+ * [q -- Save Q-Register
+ *
+ * Save Q-Register <q> contents on the global Q-Register push-down
+ * stack.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_pushqreg);
+
+static teco_state_t *
+teco_state_popqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ return ctx->mode == TECO_MODE_NORMAL &&
+ !teco_qreg_stack_pop(qreg, error) ? NULL : &teco_state_start;
+}
+
+/*$ "]" "]q" pop
+ * ]q -- Restore Q-Register
+ *
+ * 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.
+ *
+ * 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
+);
+
+static teco_state_t *
+teco_state_eqcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ /*
+ * NOTE: We will query ctx->expectqreg later in teco_state_loadqreg_done().
+ */
+ return &teco_state_loadqreg;
+}
+
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_eqcommand,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);
+
+static teco_state_t *
+teco_state_loadqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ teco_qreg_t *qreg;
+ teco_qreg_table_t *table;
+
+ teco_machine_qregspec_get_results(ctx->expectqreg, &qreg, &table);
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (str->len > 0) {
+ /* Load file into Q-Register */
+ g_autofree gchar *filename = teco_file_expand_path(str->data);
+ if (!teco_qreg_load(qreg, filename, error))
+ return NULL;
+ } else {
+ /* Edit Q-Register */
+ if (!teco_current_doc_undo_edit(error) ||
+ !teco_qreg_table_edit(table, qreg, error))
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ EQ EQq
+ * EQq$ -- Edit or load Q-Register
+ * EQq[file]$
+ *
+ * When specified with an empty <file> string argument,
+ * EQ makes <q> the currently edited Q-Register.
+ * Otherwise, when <file> is specified, it is the
+ * name of a file to read into Q-Register <q>.
+ * When loading a file, the currently edited
+ * buffer/register is not changed and the edit position
+ * of register <q> is reset to 0.
+ *
+ * Undefined Q-Registers will be defined.
+ * The command fails if <file> could not be read.
+ */
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_loadqreg);
+
+static teco_state_t *
+teco_state_epctcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ /*
+ * NOTE: We will query ctx->expectqreg later in teco_state_saveqreg_done().
+ */
+ return &teco_state_saveqreg;
+}
+
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_epctcommand);
+
+static teco_state_t *
+teco_state_saveqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ teco_qreg_t *qreg;
+
+ teco_machine_qregspec_get_results(ctx->expectqreg, &qreg, NULL);
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ g_autofree gchar *filename = teco_file_expand_path(str->data);
+ return teco_qreg_save(qreg, filename, error) ? &teco_state_start : NULL;
+}
+
+/*$ E% E%q
+ * E%q<file>$ -- Save Q-Register string to file
+ *
+ * Saves the string contents of Q-Register <q> to
+ * <file>.
+ * The <file> must always be specified, as Q-Registers
+ * have no notion of associated file names.
+ *
+ * In interactive mode, the E% command may be rubbed out,
+ * restoring the previous state of <file>.
+ * This follows the same rules as with the \fBEW\fP command.
+ *
+ * File names may also be tab-completed and string building
+ * characters are enabled by default.
+ */
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_saveqreg);
+
+static gboolean
+teco_state_queryqreg_initial(teco_machine_main_t *ctx, GError **error)
+{
+ /*
+ * This prevents teco_state_queryqreg_got_register() from having to check
+ * for Q-Register existence, resulting in better error messages in case of
+ * required Q-Registers.
+ * In parse-only mode, the type does not matter.
+ */
+ teco_qreg_type_t type = ctx->modifier_colon ? TECO_QREG_OPTIONAL : TECO_QREG_REQUIRED;
+
+ /*
+ * NOTE: We have to allocate a new instance always since `expectqreg`
+ * is part of an union.
+ */
+ ctx->expectqreg = teco_machine_qregspec_new(type, ctx->qreg_table_locals,
+ ctx->parent.must_undo);
+ if (ctx->parent.must_undo)
+ undo__teco_machine_qregspec_free(ctx->expectqreg);
+ return TRUE;
+}
+
+static teco_state_t *
+teco_state_queryqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (!teco_expressions_eval(FALSE, error))
+ return NULL;
+
+ if (teco_machine_main_eval_colon(ctx)) {
+ /* Query Q-Register's existence or string size */
+ if (qreg) {
+ gsize len;
+
+ if (!qreg->vtable->get_string(qreg, NULL, &len, error))
+ return NULL;
+ teco_expressions_push(len);
+ } else {
+ teco_expressions_push(-1);
+ }
+
+ return &teco_state_start;
+ }
+
+ 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;
+ if (pos < 0) {
+ teco_error_range_set(error, "Q");
+ return NULL;
+ }
+
+ gint c = qreg->vtable->get_character(qreg, pos, error);
+ if (c < 0)
+ return NULL;
+
+ teco_expressions_push(c);
+ } else {
+ /* Query integer */
+ teco_int_t value;
+
+ if (!qreg->vtable->get_integer(qreg, &value, error))
+ return NULL;
+ teco_expressions_push(value);
+ }
+
+ return &teco_state_start;
+}
+
+/*$ Q Qq query
+ * Qq -> n -- Query Q-Register existence, its integer or string characters
+ * <position>Qq -> character
+ * :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>
+ * 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.
+ * Both non-colon-modified forms of Q require register <q>
+ * to be defined and fail otherwise.
+ *
+ * When colon-modified, Q does not pop any arguments from
+ * the expression stack and returns the <size> of the string
+ * in Q-Register <q> if register <q> exists (i.e. is defined).
+ * Naturally, for empty strings, 0 is returned.
+ * When colon-modified and Q-Register <q> is undefined,
+ * -1 is returned instead.
+ * Therefore checking the return value \fB:Q\fP for values smaller
+ * 0 allows checking the existence of a register.
+ * Note that if <q> exists, its string part is not initialized,
+ * so \fB:Q\fP may be used to handle purely numeric data structures
+ * without creating Scintilla documents by accident.
+ * These semantics allow the useful idiom \(lq:Q\fIq\fP">\(rq for
+ * checking whether a Q-Register exists and has a non-empty string.
+ * Note also that the return value of \fB:Q\fP may be interpreted
+ * as a condition boolean that represents the non-existence of <q>.
+ * If <q> is undefined, it returns \fIsuccess\fP, else a \fIfailure\fP
+ * boolean.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_queryqreg,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_queryqreg_initial
+);
+
+static teco_state_t *
+teco_state_ctlucommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ /*
+ * NOTE: We will query ctx->expectqreg later in teco_state_setqregstring_nobuilding_done().
+ */
+ return &teco_state_setqregstring_nobuilding;
+}
+
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_ctlucommand,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);
+
+static teco_state_t *
+teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx,
+ const teco_string_t *str, GError **error)
+{
+ teco_qreg_t *qreg;
+
+ teco_machine_qregspec_get_results(ctx->expectqreg, &qreg, NULL);
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ gboolean colon_modified = teco_machine_main_eval_colon(ctx);
+
+ if (!teco_expressions_eval(FALSE, error))
+ return NULL;
+ gint args = teco_expressions_args();
+
+ if (args > 0) {
+ g_autofree gchar *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;
+ buffer[i-1] = (gchar)v;
+ }
+
+ if (colon_modified) {
+ /* append to register */
+ if (!qreg->vtable->undo_append_string(qreg, error) ||
+ !qreg->vtable->append_string(qreg, buffer, args, error))
+ return NULL;
+ } else {
+ /* set register */
+ if (!qreg->vtable->undo_set_string(qreg, error) ||
+ !qreg->vtable->set_string(qreg, buffer, args, error))
+ return NULL;
+ }
+ }
+
+ if (args > 0 || colon_modified) {
+ /* append to register */
+ if (!qreg->vtable->undo_append_string(qreg, error) ||
+ !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, error))
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ ^Uq
+ * [c1,c2,...]^Uq[string]$ -- Set or append to Q-Register string without string building
+ * [c1,c2,...]:^Uq[string]$
+ *
+ * If not colon-modified, it first fills the Q-Register <q>
+ * with all the values on the expression stack (interpreted as
+ * codepoints).
+ * It does so in the order of the arguments, i.e.
+ * <c1> will be the first character in <q>, <c2> the second, etc.
+ * Eventually the <string> argument is appended to the
+ * register.
+ * Any existing string value in <q> is overwritten by this operation.
+ *
+ * In the colon-modified form ^U does not overwrite existing
+ * contents of <q> but only appends to it.
+ *
+ * If <q> is undefined, it will be defined.
+ *
+ * String-building characters are \fBdisabled\fP for ^U
+ * commands.
+ * Therefore they are especially well-suited for defining
+ * \*(ST macros, since string building characters in the
+ * desired Q-Register contents do not have to be escaped.
+ * The \fBEU\fP command may be used where string building
+ * is desired.
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_setqregstring_nobuilding,
+ .expectstring.string_building = FALSE
+);
+
+static teco_state_t *
+teco_state_eucommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ /*
+ * NOTE: We will query ctx->expectqreg later in teco_state_setqregstring_building_done().
+ */
+ return &teco_state_setqregstring_building;
+}
+
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_eucommand,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);
+
+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
+ * [c1,c2,...]EUq[string]$ -- Set or append to Q-Register string with string building characters
+ * [c1,c2,...]:EUq[string]$
+ *
+ * This command sets or appends to the contents of
+ * Q-Register \fIq\fP.
+ * It is identical to the \fB^U\fP command, except
+ * that this form of the command has string building
+ * characters \fBenabled\fP.
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_setqregstring_building,
+ .expectstring.string_building = TRUE
+);
+
+static teco_state_t *
+teco_state_getqregstring_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!qreg->vtable->get_string(qreg, &str.data, &str.len, error))
+ return NULL;
+
+ if (str.len > 0) {
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ teco_interface_ssm(SCI_ADDTEXT, str.len, (sptr_t)str.data);
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+ }
+
+ return &teco_state_start;
+}
+
+/*$ G Gq get
+ * Gq -- Insert Q-Register string
+ *
+ * Inserts the string of Q-Register <q> into the buffer
+ * at its current position.
+ * Specifying an undefined <q> yields an error.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_getqregstring);
+
+static teco_state_t *
+teco_state_setqreginteger_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (!teco_expressions_eval(FALSE, error))
+ return NULL;
+ if (teco_expressions_args() || teco_num_sign < 0) {
+ teco_int_t v;
+ if (!teco_expressions_pop_num_calc(&v, 0, error) ||
+ !qreg->vtable->undo_set_integer(qreg, error) ||
+ !qreg->vtable->set_integer(qreg, v, error))
+ return NULL;
+
+ if (teco_machine_main_eval_colon(ctx))
+ teco_expressions_push(TECO_SUCCESS);
+ } else if (teco_machine_main_eval_colon(ctx)) {
+ teco_expressions_push(TECO_FAILURE);
+ } else {
+ teco_error_argexpected_set(error, "U");
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ U Uq
+ * nUq -- Set Q-Register integer
+ * -Uq
+ * [n]:Uq -> Success|Failure
+ *
+ * Sets the integer-part of Q-Register <q> to <n>.
+ * \(lq-U\(rq is equivalent to \(lq-1U\(rq, otherwise
+ * the command fails if <n> is missing.
+ *
+ * If the command is colon-modified, it returns a success
+ * boolean if <n> or \(lq-\(rq is given.
+ * Otherwise it returns a failure boolean and does not
+ * modify <q>.
+ *
+ * The register is defined if it does not exist.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_setqreginteger,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);
+
+static teco_state_t *
+teco_state_increaseqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_int_t value, add;
+
+ if (!qreg->vtable->undo_set_integer(qreg, error) ||
+ !qreg->vtable->get_integer(qreg, &value, error) ||
+ !teco_expressions_pop_num_calc(&add, teco_num_sign, error) ||
+ !qreg->vtable->set_integer(qreg, value += add, error))
+ return NULL;
+ teco_expressions_push(value);
+
+ return &teco_state_start;
+}
+
+/*$ % %q increment
+ * [n]%q -> q+n -- Increase Q-Register integer
+ *
+ * Add <n> to the integer part of register <q>, returning
+ * its new value.
+ * <q> will be defined if it does not exist.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_increaseqreg,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);
+
+static teco_state_t *
+teco_state_macro_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (teco_machine_main_eval_colon(ctx)) {
+ /* don't create new local Q-Registers if colon modifier is given */
+ if (!teco_qreg_execute(qreg, ctx->qreg_table_locals, error))
+ return NULL;
+ } else {
+ g_auto(teco_qreg_table_t) table;
+ teco_qreg_table_init(&table, FALSE);
+ if (!teco_qreg_execute(qreg, &table, error))
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ M Mq eval
+ * Mq -- Execute macro
+ * :Mq
+ *
+ * Execute macro stored in string of Q-Register <q>.
+ * The command itself does not push or pop and arguments from the stack
+ * but the macro executed might well do so.
+ * The new macro invocation level will contain its own go-to label table
+ * and local Q-Register table.
+ * Except when the command is colon-modified - in this case, local
+ * Q-Registers referenced in the macro refer to the parent macro-level's
+ * local Q-Register table (or whatever level defined one last).
+ *
+ * Errors during the macro execution will propagate to the M command.
+ * In other words if a command in the macro fails, the M command will fail
+ * and this failure propagates until the top-level macro (e.g.
+ * the command-line macro).
+ *
+ * Note that the string of <q> will be copied upon macro execution,
+ * so subsequent changes to Q-Register <q> from inside the macro do
+ * not modify the executed code.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_macro);
+
+static teco_state_t *
+teco_state_macrofile_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_machine_main_eval_colon(ctx)) {
+ /* don't create new local Q-Registers if colon modifier is given */
+ if (!teco_execute_file(filename, ctx->qreg_table_locals, error))
+ return NULL;
+ } else {
+ g_auto(teco_qreg_table_t) table;
+ teco_qreg_table_init(&table, FALSE);
+ if (!teco_execute_file(filename, &table, error))
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ EM
+ * EMfile$ -- Execute macro from file
+ * :EMfile$
+ *
+ * Read the file with name <file> into memory and execute its contents
+ * as a macro.
+ * It is otherwise similar to the \(lqM\(rq command.
+ *
+ * If <file> could not be read, the command yields an error.
+ */
+TECO_DEFINE_STATE_EXPECTFILE(teco_state_macrofile);
+
+static teco_state_t *
+teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_int_t from, len;
+
+ if (!teco_expressions_eval(FALSE, 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 {
+ teco_int_t to = teco_expressions_pop_num(0);
+ from = teco_expressions_pop_num(0);
+
+ len = to - from;
+
+ if (len < 0 || !teco_validate_pos(from) || !teco_validate_pos(to)) {
+ teco_error_range_set(error, "X");
+ return NULL;
+ }
+ }
+
+ g_autofree gchar *str = g_malloc(len + 1);
+
+ struct Sci_TextRange text_range = {
+ .chrg = {.cpMin = from, .cpMax = from + len},
+ .lpstrText = str
+ };
+ teco_interface_ssm(SCI_GETTEXTRANGE, 0, (sptr_t)&text_range);
+
+ if (teco_machine_main_eval_colon(ctx)) {
+ if (!qreg->vtable->undo_append_string(qreg, error) ||
+ !qreg->vtable->append_string(qreg, str, len, error))
+ return NULL;
+ } else {
+ if (!qreg->vtable->undo_set_string(qreg, error) ||
+ !qreg->vtable->set_string(qreg, str, len, error))
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*$ X Xq
+ * [lines]Xq -- Copy into or append to Q-Register
+ * -Xq
+ * from,toXq
+ * [lines]:Xq
+ * -:Xq
+ * from,to:Xq
+ *
+ * Copy the next or previous number of <lines> from the buffer
+ * into the Q-Register <q> string.
+ * 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 K
+ * command's arguments.
+ * If the command is colon-modified, the characters will be
+ * appended to the end of register <q> instead.
+ *
+ * Register <q> will be created if it is undefined.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_copytoqreg,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);
diff --git a/src/qreg-commands.h b/src/qreg-commands.h
new file mode 100644
index 0000000..e91b19a
--- /dev/null
+++ b/src/qreg-commands.h
@@ -0,0 +1,90 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include "sciteco.h"
+#include "parser.h"
+#include "qreg.h"
+
+static inline void
+teco_state_expectqreg_reset(teco_machine_main_t *ctx)
+{
+ if (ctx->parent.must_undo)
+ teco_undo_qregspec_own(ctx->expectqreg);
+ else
+ teco_machine_qregspec_free(ctx->expectqreg);
+}
+
+gboolean teco_state_expectqreg_initial(teco_machine_main_t *ctx, GError **error);
+
+teco_state_t *teco_state_expectqreg_input(teco_machine_main_t *ctx, gchar chr, GError **error);
+
+/* in cmdline.c */
+gboolean teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error);
+
+/**
+ * @interface TECO_DEFINE_STATE_EXPECTQREG
+ * @implements TECO_DEFINE_STATE
+ * @ingroup states
+ *
+ * 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, gchar 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, \
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \
+ teco_state_expectqreg_process_edit_cmd, \
+ .expectqreg.type = TECO_QREG_REQUIRED, \
+ .expectqreg.got_register_cb = NAME##_got_register, /* always required */ \
+ ##__VA_ARGS__ \
+ )
+
+/*
+ * 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);
+
+TECO_DECLARE_STATE(teco_state_eqcommand);
+TECO_DECLARE_STATE(teco_state_loadqreg);
+
+TECO_DECLARE_STATE(teco_state_epctcommand);
+TECO_DECLARE_STATE(teco_state_saveqreg);
+
+TECO_DECLARE_STATE(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);
+
+TECO_DECLARE_STATE(teco_state_getqregstring);
+TECO_DECLARE_STATE(teco_state_setqreginteger);
+TECO_DECLARE_STATE(teco_state_increaseqreg);
+
+TECO_DECLARE_STATE(teco_state_macro);
+TECO_DECLARE_STATE(teco_state_macrofile);
+
+TECO_DECLARE_STATE(teco_state_copytoqreg);
diff --git a/src/qreg.c b/src/qreg.c
new file mode 100644
index 0000000..5c39409
--- /dev/null
+++ b/src/qreg.c
@@ -0,0 +1,1542 @@
+/*
+ * 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 <string.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "file-utils.h"
+#include "interface.h"
+#include "cmdline.h"
+#include "view.h"
+#include "undo.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "expressions.h"
+#include "doc.h"
+#include "ring.h"
+#include "eol.h"
+#include "error.h"
+#include "qreg.h"
+
+/**
+ * View used for editing Q-Registers.
+ * Initialized in main.c after the interface.
+ */
+teco_view_t *teco_qreg_view = NULL;
+/** Currently edited Q-Register */
+teco_qreg_t *teco_qreg_current = NULL;
+
+/**
+ * Table for global Q-Registers.
+ * Initialized in main.c after the interface.
+ */
+teco_qreg_table_t teco_qreg_table_globals;
+
+/** @private @static @memberof teco_qreg_t */
+static teco_qreg_t *
+teco_qreg_new(teco_qreg_vtable_t *vtable, const gchar *name, gsize len)
+{
+ /*
+ * FIXME: Test with g_slice_new()...
+ * It could however cause problems upon command-line termination
+ * and may not be measurably faster.
+ */
+ teco_qreg_t *qreg = g_new0(teco_qreg_t, 1);
+ qreg->vtable = vtable;
+ /*
+ * NOTE: This does not use GStringChunk/teco_string_init_chunk()
+ * since we want to implement Q-Register removing soon.
+ * Even without that, individual Q-Regs can be removed on rubout.
+ */
+ teco_string_init(&qreg->head.name, name, len);
+ teco_doc_init(&qreg->string);
+ return qreg;
+}
+
+/** @memberof teco_qreg_t */
+gboolean
+teco_qreg_execute(teco_qreg_t *qreg, teco_qreg_table_t *qreg_table_locals, GError **error)
+{
+ g_auto(teco_string_t) macro = {NULL, 0};
+
+ if (!qreg->vtable->get_string(qreg, &macro.data, &macro.len, error) ||
+ !teco_execute_macro(macro.data, macro.len, qreg_table_locals, error)) {
+ teco_error_add_frame_qreg(qreg->head.name.data, qreg->head.name.len);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/** @memberof teco_qreg_t */
+void
+teco_qreg_undo_set_eol_mode(teco_qreg_t *qreg)
+{
+ if (!qreg->must_undo)
+ return;
+
+ /*
+ * Necessary, so that upon rubout the
+ * string's parameters are restored.
+ */
+ teco_doc_update(&qreg->string, teco_qreg_view);
+
+ if (teco_qreg_current && teco_qreg_current->must_undo) // FIXME
+ teco_doc_undo_edit(&teco_qreg_current->string);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_SETEOLMODE,
+ teco_view_ssm(teco_qreg_view, SCI_GETEOLMODE, 0, 0), 0);
+
+ teco_doc_undo_edit(&qreg->string);
+}
+
+/** @memberof teco_qreg_t */
+void
+teco_qreg_set_eol_mode(teco_qreg_t *qreg, gint mode)
+{
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+ teco_view_ssm(teco_qreg_view, SCI_SETEOLMODE, mode, 0);
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+}
+
+/** @memberof teco_qreg_t */
+gboolean
+teco_qreg_load(teco_qreg_t *qreg, const gchar *filename, GError **error)
+{
+ if (!qreg->vtable->undo_set_string(qreg, error))
+ return FALSE;
+
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+ teco_doc_reset(&qreg->string);
+
+ /*
+ * teco_view_load() might change the EOL style.
+ */
+ teco_qreg_undo_set_eol_mode(qreg);
+
+ /*
+ * undo_set_string() pushes undo tokens that restore
+ * the previous document in the view.
+ * So if loading fails, teco_qreg_current will be
+ * made the current document again.
+ */
+ if (!teco_view_load(teco_qreg_view, filename, error))
+ return FALSE;
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+
+ return TRUE;
+}
+
+/** @memberof teco_qreg_t */
+gboolean
+teco_qreg_save(teco_qreg_t *qreg, const gchar *filename, GError **error)
+{
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+
+ if (!teco_view_save(teco_qreg_view, filename, error)) {
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+ return FALSE;
+ }
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_set_integer(teco_qreg_t *qreg, teco_int_t value, GError **error)
+{
+ qreg->integer = value;
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_undo_set_integer(teco_qreg_t *qreg, GError **error)
+{
+ if (qreg->must_undo) // FIXME
+ teco_undo_int(qreg->integer);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_get_integer(teco_qreg_t *qreg, teco_int_t *ret, GError **error)
+{
+ *ret = qreg->integer;
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ teco_doc_set_string(&qreg->string, str, len);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_undo_set_string(teco_qreg_t *qreg, GError **error)
+{
+ if (qreg->must_undo) // FIXME
+ teco_doc_undo_set_string(&qreg->string);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ /*
+ * NOTE: Will not create undo action if string is empty.
+ * Also, appending preserves the string's parameters.
+ */
+ if (!len)
+ return TRUE;
+
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_APPENDTEXT, len, (sptr_t)str);
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error)
+{
+ teco_doc_get_string(&qreg->string, str, len);
+ return TRUE;
+}
+
+static gint
+teco_qreg_plain_get_character(teco_qreg_t *qreg, guint position, GError **error)
+{
+ gint ret = -1;
+
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+
+ if (position < teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0))
+ ret = teco_view_ssm(teco_qreg_view, SCI_GETCHARAT, position, 0);
+ else
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Position %u out of range", position);
+ /* make sure we still restore the current Q-Register */
+
+ if (teco_qreg_current)
+ teco_doc_edit(&teco_qreg_current->string);
+
+ return ret;
+}
+
+static gboolean
+teco_qreg_plain_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ teco_doc_exchange(&qreg->string, src);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ if (qreg->must_undo) // FIXME
+ teco_doc_undo_exchange(&qreg->string);
+ teco_doc_undo_exchange(src);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_edit(teco_qreg_t *qreg, GError **error)
+{
+ if (teco_qreg_current)
+ teco_doc_update(&teco_qreg_current->string, teco_qreg_view);
+
+ teco_doc_edit(&qreg->string);
+ teco_interface_show_view(teco_qreg_view);
+ teco_interface_info_update(qreg);
+
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_plain_undo_edit(teco_qreg_t *qreg, GError **error)
+{
+ /*
+ * We might be switching the current document
+ * to a buffer.
+ */
+ teco_doc_update(&qreg->string, teco_qreg_view);
+
+ if (!qreg->must_undo) // FIXME
+ return TRUE;
+
+ undo__teco_interface_info_update_qreg(qreg);
+ teco_doc_undo_edit(&qreg->string);
+ undo__teco_interface_show_view(teco_qreg_view);
+ return TRUE;
+}
+
+#define TECO_INIT_QREG(...) { \
+ .set_integer = teco_qreg_plain_set_integer, \
+ .undo_set_integer = teco_qreg_plain_undo_set_integer, \
+ .get_integer = teco_qreg_plain_get_integer, \
+ .set_string = teco_qreg_plain_set_string, \
+ .undo_set_string = teco_qreg_plain_undo_set_string, \
+ .append_string = teco_qreg_plain_append_string, \
+ .undo_append_string = teco_qreg_plain_undo_set_string, \
+ .get_string = teco_qreg_plain_get_string, \
+ .get_character = teco_qreg_plain_get_character, \
+ .exchange_string = teco_qreg_plain_exchange_string, \
+ .undo_exchange_string = teco_qreg_plain_undo_exchange_string, \
+ .edit = teco_qreg_plain_edit, \
+ .undo_edit = teco_qreg_plain_undo_edit, \
+ ##__VA_ARGS__ \
+}
+
+/** @static @memberof teco_qreg_t */
+teco_qreg_t *
+teco_qreg_plain_new(const gchar *name, gsize len)
+{
+ static teco_qreg_vtable_t vtable = TECO_INIT_QREG();
+
+ return teco_qreg_new(&vtable, name, len);
+}
+
+/*
+ * NOTE: The integer-component is currently unused on the "*" special register.
+ */
+static gboolean
+teco_qreg_bufferinfo_set_integer(teco_qreg_t *qreg, teco_int_t value, GError **error)
+{
+ return teco_ring_edit(value, error);
+}
+
+static gboolean
+teco_qreg_bufferinfo_undo_set_integer(teco_qreg_t *qreg, GError **error)
+{
+ return teco_current_doc_undo_edit(error);
+}
+
+static gboolean
+teco_qreg_bufferinfo_get_integer(teco_qreg_t *qreg, teco_int_t *ret, GError **error)
+{
+ *ret = teco_ring_get_id(teco_ring_current);
+ return TRUE;
+}
+
+/*
+ * FIXME: These operations can and should be implemented.
+ * Setting the "*" register could for instance rename the file.
+ */
+static gboolean
+teco_qreg_bufferinfo_set_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_bufferinfo_undo_set_string(teco_qreg_t *qreg, GError **error)
+{
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_bufferinfo_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_bufferinfo_undo_append_string(teco_qreg_t *qreg, GError **error)
+{
+ return TRUE;
+}
+
+/*
+ * NOTE: The `string` component is currently unused on the "*" register.
+ */
+static gboolean
+teco_qreg_bufferinfo_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error)
+{
+ /*
+ * On platforms with a default non-forward-slash directory
+ * separator (i.e. Windows), Buffer::filename will have
+ * the wrong separator.
+ * To make the life of macros that evaluate "*" easier,
+ * the directory separators are normalized to "/" here.
+ */
+ if (str)
+ *str = teco_file_normalize_path(g_strdup(teco_ring_current->filename ? : ""));
+ /*
+ * NOTE: teco_file_normalize_path() does not change the size of the string.
+ */
+ *len = teco_ring_current->filename ? strlen(teco_ring_current->filename) : 0;
+ return TRUE;
+}
+
+static gint
+teco_qreg_bufferinfo_get_character(teco_qreg_t *qreg, guint position, GError **error)
+{
+ gsize max_len;
+
+ if (!teco_qreg_bufferinfo_get_string(qreg, NULL, &max_len, error))
+ return -1;
+
+ if (position >= max_len) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Position %u out of range", position);
+ return -1;
+ }
+
+ return teco_ring_current->filename[position];
+}
+
+static gboolean
+teco_qreg_bufferinfo_edit(teco_qreg_t *qreg, GError **error)
+{
+ if (!teco_qreg_plain_edit(qreg, error))
+ return FALSE;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_bufferinfo_get_string(qreg, &str.data, &str.len, error))
+ return FALSE;
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_ADDTEXT, str.len, (sptr_t)str.data);
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0);
+ return TRUE;
+}
+
+/** @static @memberof teco_qreg_t */
+teco_qreg_t *
+teco_qreg_bufferinfo_new(void)
+{
+ static teco_qreg_vtable_t vtable = TECO_INIT_QREG(
+ .set_integer = teco_qreg_bufferinfo_set_integer,
+ .undo_set_integer = teco_qreg_bufferinfo_undo_set_integer,
+ .get_integer = teco_qreg_bufferinfo_get_integer,
+ .set_string = teco_qreg_bufferinfo_set_string,
+ .undo_set_string = teco_qreg_bufferinfo_undo_set_string,
+ .append_string = teco_qreg_bufferinfo_append_string,
+ .undo_append_string = teco_qreg_bufferinfo_undo_append_string,
+ .get_string = teco_qreg_bufferinfo_get_string,
+ .get_character = teco_qreg_bufferinfo_get_character,
+ .edit = teco_qreg_bufferinfo_edit
+ );
+
+ return teco_qreg_new(&vtable, "*", 1);
+}
+
+static gboolean
+teco_qreg_workingdir_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ /*
+ * NOTE: Makes sure that `dir` will be null-terminated as str[len] may not be '\0'.
+ */
+ g_auto(teco_string_t) dir;
+ teco_string_init(&dir, str, len);
+
+ if (teco_string_contains(&dir, '\0')) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Directory contains null-character");
+ return FALSE;
+ }
+
+ int ret = g_chdir(dir.data);
+ if (ret) {
+ /* FIXME: Is errno usable on Windows here? */
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Cannot change working directory to \"%s\"", dir.data);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_workingdir_undo_set_string(teco_qreg_t *qreg, GError **error)
+{
+ teco_undo_change_dir_to_current();
+ 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_undo_append_string(teco_qreg_t *qreg, GError **error)
+{
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_workingdir_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error)
+{
+ /*
+ * On platforms with a default non-forward-slash directory
+ * separator (i.e. Windows), teco_buffer_t::filename will have
+ * the wrong separator.
+ * To make the life of macros that evaluate "$" easier,
+ * the directory separators are normalized to "/" here.
+ * This does not change the size of the string, so
+ * the return value for str == NULL is still correct.
+ */
+ gchar *dir = g_get_current_dir();
+ *len = strlen(dir);
+ if (str)
+ *str = teco_file_normalize_path(dir);
+ else
+ g_free(dir);
+
+ return TRUE;
+}
+
+static gint
+teco_qreg_workingdir_get_character(teco_qreg_t *qreg, guint position, GError **error)
+{
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_workingdir_get_string(qreg, &str.data, &str.len, error))
+ return -1;
+
+ if (position >= str.len) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Position %u out of range", position);
+ return -1;
+ }
+
+ return str.data[position];
+}
+
+static gboolean
+teco_qreg_workingdir_edit(teco_qreg_t *qreg, GError **error)
+{
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_plain_edit(qreg, error) ||
+ !teco_qreg_workingdir_get_string(qreg, &str.data, &str.len, error))
+ return FALSE;
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_ADDTEXT, str.len, (sptr_t)str.data);
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_workingdir_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ g_auto(teco_string_t) other_str, own_str = {NULL, 0};
+
+ teco_doc_get_string(src, &other_str.data, &other_str.len);
+
+ if (!teco_qreg_workingdir_get_string(qreg, &own_str.data, &own_str.len, error) ||
+ /* FIXME: Why is teco_qreg_plain_set_string() sufficient? */
+ !teco_qreg_plain_set_string(qreg, other_str.data, other_str.len, error))
+ return FALSE;
+
+ teco_doc_set_string(src, own_str.data, own_str.len);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_workingdir_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ teco_undo_change_dir_to_current();
+ if (qreg->must_undo) // FIXME
+ teco_doc_undo_set_string(src);
+ return TRUE;
+}
+
+/** @static @memberof teco_qreg_t */
+teco_qreg_t *
+teco_qreg_workingdir_new(void)
+{
+ static teco_qreg_vtable_t vtable = TECO_INIT_QREG(
+ .set_string = teco_qreg_workingdir_set_string,
+ .undo_set_string = teco_qreg_workingdir_undo_set_string,
+ .append_string = teco_qreg_workingdir_append_string,
+ .undo_append_string = teco_qreg_workingdir_undo_append_string,
+ .get_string = teco_qreg_workingdir_get_string,
+ .get_character = teco_qreg_workingdir_get_character,
+ .edit = teco_qreg_workingdir_edit,
+ .exchange_string = teco_qreg_workingdir_exchange_string,
+ .undo_exchange_string = teco_qreg_workingdir_undo_exchange_string
+ );
+
+ /*
+ * FIXME: Dollar is not the best name for it since it is already
+ * heavily overloaded in the language and easily confused with Escape and
+ * 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.
+ * Perhaps "@"...
+ */
+ return teco_qreg_new(&vtable, "$", 1);
+}
+
+static gboolean
+teco_qreg_clipboard_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error)
+{
+ g_assert(!teco_string_contains(&qreg->head.name, '\0'));
+ const gchar *clipboard_name = qreg->head.name.data + 1;
+
+ if (teco_ed & TECO_ED_AUTOEOL) {
+ /*
+ * NOTE: Currently uses GString instead of teco_string_t to make use of
+ * preallocation.
+ * On the other hand GString has a higher overhead.
+ */
+ g_autoptr(GString) str_converted = g_string_sized_new(len);
+
+ /*
+ * This will convert to the Q-Register view's EOL mode.
+ */
+ g_auto(teco_eol_writer_t) writer;
+ teco_eol_writer_init_mem(&writer, teco_view_ssm(teco_qreg_view, SCI_GETEOLMODE, 0, 0),
+ str_converted);
+
+ gssize bytes_written = teco_eol_writer_convert(&writer, str, len, error);
+ if (bytes_written < 0)
+ return FALSE;
+ g_assert(bytes_written == len);
+
+ return teco_interface_set_clipboard(clipboard_name, str_converted->str,
+ str_converted->len, error);
+ } else {
+ /*
+ * No EOL conversion necessary. The teco_eol_writer_t can handle
+ * this as well, but will result in unnecessary allocations.
+ */
+ return teco_interface_set_clipboard(clipboard_name, str, len, error);
+ }
+
+ /* should not be reached */
+ 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_append_string(teco_qreg_t *qreg, GError **error)
+{
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_clipboard_undo_set_string(teco_qreg_t *qreg, GError **error)
+{
+ /*
+ * Upon rubout, the current contents of the clipboard are
+ * restored.
+ * We are checking for teco_undo_enabled instead of relying on
+ * teco_undo_push(), since getting the clipboard
+ * is an expensive operation that we want to avoid.
+ */
+ if (!teco_undo_enabled)
+ return TRUE;
+
+ g_assert(!teco_string_contains(&qreg->head.name, '\0'));
+ const gchar *clipboard_name = qreg->head.name.data + 1;
+
+ /*
+ * Ownership of str is passed to the undo token.
+ * This avoids any EOL translation as that would be cumbersome
+ * and could also modify the clipboard in unexpected ways.
+ */
+ teco_string_t str;
+ if (!teco_interface_get_clipboard(clipboard_name, &str.data, &str.len, error))
+ return FALSE;
+ teco_interface_undo_set_clipboard(clipboard_name, str.data, str.len);
+ return TRUE;
+}
+
+static gboolean
+teco_qreg_clipboard_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error)
+{
+ g_assert(!teco_string_contains(&qreg->head.name, '\0'));
+ const gchar *clipboard_name = qreg->head.name.data + 1;
+
+ if (!(teco_ed & TECO_ED_AUTOEOL))
+ /*
+ * No auto-eol conversion - avoid unnecessary copying and allocations.
+ */
+ return teco_interface_get_clipboard(clipboard_name, str, len, error);
+
+ g_auto(teco_string_t) temp = {NULL, 0};
+ if (!teco_interface_get_clipboard(clipboard_name, &temp.data, &temp.len, error))
+ return FALSE;
+
+ g_auto(teco_eol_reader_t) reader;
+ teco_eol_reader_init_mem(&reader, temp.data, temp.len);
+
+ /*
+ * FIXME: Could be simplified if teco_eol_reader_convert_all() had the
+ * same conventions for passing NULL pointers.
+ */
+ teco_string_t str_converted;
+ if (teco_eol_reader_convert_all(&reader, &str_converted.data,
+ &str_converted.len, error) == G_IO_STATUS_ERROR)
+ return FALSE;
+
+ if (str)
+ *str = str_converted.data;
+ else
+ teco_string_clear(&str_converted);
+ *len = str_converted.len;
+
+ return TRUE;
+}
+
+static gint
+teco_qreg_clipboard_get_character(teco_qreg_t *qreg, guint position, GError **error)
+{
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_clipboard_get_string(qreg, &str.data, &str.len, error))
+ return -1;
+
+ if (position >= str.len) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE,
+ "Position %u out of range", position);
+ return -1;
+ }
+
+ return str.data[position];
+}
+
+static gboolean
+teco_qreg_clipboard_edit(teco_qreg_t *qreg, GError **error)
+{
+ if (!teco_qreg_plain_edit(qreg, error))
+ return FALSE;
+
+ g_auto(teco_string_t) str = {NULL, 0};
+
+ if (!teco_qreg_clipboard_get_string(qreg, &str.data, &str.len, error))
+ return FALSE;
+
+ teco_view_ssm(teco_qreg_view, SCI_BEGINUNDOACTION, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_CLEARALL, 0, 0);
+ teco_view_ssm(teco_qreg_view, SCI_APPENDTEXT, str.len, (sptr_t)str.data);
+ teco_view_ssm(teco_qreg_view, SCI_ENDUNDOACTION, 0, 0);
+
+ undo__teco_view_ssm(teco_qreg_view, SCI_UNDO, 0, 0);
+ return TRUE;
+}
+
+/*
+ * FIXME: Very similar to teco_qreg_workingdir_exchange_string().
+ */
+static gboolean
+teco_qreg_clipboard_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ g_auto(teco_string_t) other_str, own_str = {NULL, 0};
+
+ teco_doc_get_string(src, &other_str.data, &other_str.len);
+
+ if (!teco_qreg_clipboard_get_string(qreg, &own_str.data, &own_str.len, error) ||
+ /* FIXME: Why is teco_qreg_plain_set_string() sufficient? */
+ !teco_qreg_plain_set_string(qreg, other_str.data, other_str.len, error))
+ return FALSE;
+
+ teco_doc_set_string(src, own_str.data, own_str.len);
+ return TRUE;
+}
+
+/*
+ * FIXME: Very similar to teco_qreg_workingdir_undo_exchange_string().
+ */
+static gboolean
+teco_qreg_clipboard_undo_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error)
+{
+ if (!teco_qreg_clipboard_undo_set_string(qreg, error))
+ return FALSE;
+ if (qreg->must_undo) // FIXME
+ teco_doc_undo_set_string(src);
+ return TRUE;
+}
+
+/** @static @memberof teco_qreg_t */
+teco_qreg_t *
+teco_qreg_clipboard_new(const gchar *name)
+{
+ static teco_qreg_vtable_t vtable = TECO_INIT_QREG(
+ .set_string = teco_qreg_clipboard_set_string,
+ .undo_set_string = teco_qreg_clipboard_undo_set_string,
+ .append_string = teco_qreg_clipboard_append_string,
+ .undo_append_string = teco_qreg_clipboard_undo_append_string,
+ .get_string = teco_qreg_clipboard_get_string,
+ .get_character = teco_qreg_clipboard_get_character,
+ .edit = teco_qreg_clipboard_edit,
+ .exchange_string = teco_qreg_clipboard_exchange_string,
+ .undo_exchange_string = teco_qreg_clipboard_undo_exchange_string
+ );
+
+ teco_qreg_t *qreg = teco_qreg_new(&vtable, "~", 1);
+ teco_string_append(&qreg->head.name, name, strlen(name));
+ return qreg;
+}
+
+/** @memberof teco_qreg_table_t */
+void
+teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo)
+{
+ rb3_reset_tree(&table->tree);
+ table->must_undo = must_undo;
+
+ /* general purpose registers */
+ for (gchar q = 'A'; q <= 'Z'; q++)
+ teco_qreg_table_insert(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)));
+}
+
+static inline void
+teco_qreg_table_remove(teco_qreg_t *reg)
+{
+ rb3_unlink_and_rebalance(&reg->head.head);
+ teco_qreg_free(reg);
+}
+TECO_DEFINE_UNDO_CALL(teco_qreg_table_remove, teco_qreg_t *);
+
+static inline void
+teco_qreg_table_undo_remove(teco_qreg_t *qreg)
+{
+ if (qreg->must_undo)
+ undo__teco_qreg_table_remove(qreg);
+}
+
+/** @memberof teco_qreg_table_t */
+teco_qreg_t *
+teco_qreg_table_edit_name(teco_qreg_table_t *table, const gchar *name, gsize len, GError **error)
+{
+ teco_qreg_t *qreg = teco_qreg_table_find(table, name, len);
+ if (!qreg) {
+ g_autofree gchar *name_printable = teco_string_echo(name, len);
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Q-Register \"%s\" not found", name_printable);
+ return NULL;
+ }
+
+ return teco_qreg_table_edit(table, qreg, error) ? qreg : NULL;
+}
+
+/**
+ * Import process environment into table
+ * by setting environment registers for every
+ * environment variable.
+ * It is assumed that the table does not yet
+ * contain any environment register.
+ *
+ * In general this method is only safe to call
+ * at startup.
+ *
+ * @memberof teco_qreg_table_t
+ */
+gboolean
+teco_qreg_table_set_environ(teco_qreg_table_t *table, GError **error)
+{
+ /*
+ * NOTE: Using g_get_environ() would be more efficient,
+ * but it appears to be broken, at least on Windows 2000.
+ */
+ g_auto(GStrv) env = g_listenv();
+
+ for (gchar **key = env; *key; key++) {
+ g_autofree gchar *name = g_strconcat("$", *key, NULL);
+
+ /*
+ * FIXME: It might be a good idea to wrap this into
+ * a convenience function.
+ */
+ teco_qreg_t *qreg = teco_qreg_plain_new(name, strlen(name));
+ teco_qreg_t *found = teco_qreg_table_insert(table, qreg);
+ if (found) {
+ teco_qreg_free(qreg);
+ qreg = found;
+ }
+
+ const gchar *value = g_getenv(*key);
+ g_assert(value != NULL);
+ if (!qreg->vtable->set_string(qreg, value, strlen(value), error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Export environment registers as a list of environment
+ * variables compatible with `g_get_environ()`.
+ *
+ * @return Zero-terminated list of strings in the form
+ * `NAME=VALUE`. Should be freed with `g_strfreev()`.
+ * NULL in case of errors.
+ *
+ * @memberof teco_qreg_table_t
+ */
+gchar **
+teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error)
+{
+ teco_qreg_t *first = (teco_qreg_t *)teco_rb3str_nfind(&table->tree, TRUE, "$", 1);
+
+ gint envp_len = 1;
+
+ /*
+ * Iterate over all registers beginning with "$" to
+ * guess the size required for the environment array.
+ * This may waste a few bytes because not __every__
+ * register beginning with "$" is an environment
+ * register.
+ */
+ for (teco_qreg_t *cur = first;
+ cur && cur->head.name.data[0] == '$';
+ cur = (teco_qreg_t *)teco_rb3str_get_next(&cur->head))
+ envp_len++;
+
+ gchar **envp, **p;
+ p = envp = g_new(gchar *, envp_len);
+
+ 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;
+
+ /*
+ * Ignore the "$" register (not an environment
+ * variable register) and registers whose
+ * name contains "=" or null (not allowed in environment
+ * variable names).
+ */
+ if (name->len == 1 ||
+ teco_string_contains(name, '=') || teco_string_contains(name, '\0'))
+ continue;
+
+ g_auto(teco_string_t) value = {NULL, 0};
+ if (!cur->vtable->get_string(cur, &value.data, &value.len, error)) {
+ g_strfreev(envp);
+ return NULL;
+ }
+ 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);
+ return NULL;
+ }
+
+ /* more efficient than g_environ_setenv() */
+ *p++ = g_strconcat(name->data+1, "=", value.data, NULL);
+ }
+
+ *p = NULL;
+
+ return envp;
+}
+
+/**
+ * Empty Q-Register table except the currently edited register.
+ * If the table contains the currently edited register, it will
+ * throw an error and the table might be left half-emptied.
+ *
+ * @memberof teco_qreg_table_t
+ */
+gboolean
+teco_qreg_table_empty(teco_qreg_table_t *table, GError **error)
+{
+ struct rb3_head *cur;
+
+ while ((cur = rb3_get_root(&table->tree))) {
+ if ((teco_qreg_t *)cur == 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,
+ "Currently edited Q-Register \"%s\" cannot be discarded", name_printable);
+ return FALSE;
+ }
+
+ rb3_unlink_and_rebalance(cur);
+ teco_qreg_free((teco_qreg_t *)cur);
+ }
+
+ return TRUE;
+}
+
+/** @memberof teco_qreg_table_t */
+void
+teco_qreg_table_clear(teco_qreg_table_t *table)
+{
+ struct rb3_head *cur;
+
+ while ((cur = rb3_get_root(&table->tree))) {
+ rb3_unlink_and_rebalance(cur);
+ teco_qreg_free((teco_qreg_t *)cur);
+ }
+}
+
+typedef struct {
+ teco_int_t integer;
+ teco_doc_t string;
+} teco_qreg_stack_entry_t;
+
+static inline void
+teco_qreg_stack_entry_clear(teco_qreg_stack_entry_t *entry)
+{
+ teco_doc_clear(&entry->string);
+}
+
+static GArray *teco_qreg_stack;
+
+static void __attribute__((constructor))
+teco_qreg_stack_init(void)
+{
+ teco_qreg_stack = g_array_sized_new(FALSE, FALSE, sizeof(teco_qreg_stack_entry_t), 1024);
+}
+
+static inline void
+teco_qreg_stack_remove_last(void)
+{
+ teco_qreg_stack_entry_clear(&g_array_index(teco_qreg_stack, teco_qreg_stack_entry_t,
+ teco_qreg_stack->len-1));
+ g_array_remove_index(teco_qreg_stack, teco_qreg_stack->len-1);
+}
+TECO_DEFINE_UNDO_CALL(teco_qreg_stack_remove_last);
+
+gboolean
+teco_qreg_stack_push(teco_qreg_t *qreg, GError **error)
+{
+ teco_qreg_stack_entry_t entry;
+ g_auto(teco_string_t) string = {NULL, 0};
+
+ if (!qreg->vtable->get_integer(qreg, &entry.integer, error) ||
+ !qreg->vtable->get_string(qreg, &string.data, &string.len, error))
+ return FALSE;
+ teco_doc_init(&entry.string);
+ teco_doc_set_string(&entry.string, string.data, string.len);
+ teco_doc_update(&entry.string, &qreg->string);
+
+ /* pass ownership of entry to teco_qreg_stack */
+ g_array_append_val(teco_qreg_stack, entry);
+ undo__teco_qreg_stack_remove_last();
+ return TRUE;
+}
+
+static void
+teco_qreg_stack_entry_action(teco_qreg_stack_entry_t *entry, gboolean run)
+{
+ if (run)
+ g_array_append_val(teco_qreg_stack, *entry);
+ else
+ teco_qreg_stack_entry_clear(entry);
+}
+
+static void
+teco_undo_qreg_stack_push_own(teco_qreg_stack_entry_t *entry)
+{
+ teco_qreg_stack_entry_t *ctx = teco_undo_push(teco_qreg_stack_entry);
+ if (ctx)
+ memcpy(ctx, entry, sizeof(*ctx));
+ else
+ teco_qreg_stack_entry_clear(entry);
+}
+
+gboolean
+teco_qreg_stack_pop(teco_qreg_t *qreg, GError **error)
+{
+ if (!teco_qreg_stack->len) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Q-Register stack empty");
+ return FALSE;
+ }
+
+ teco_qreg_stack_entry_t *entry;
+ entry = &g_array_index(teco_qreg_stack, teco_qreg_stack_entry_t, teco_qreg_stack->len-1);
+
+ if (!qreg->vtable->undo_set_integer(qreg, error) ||
+ !qreg->vtable->set_integer(qreg, entry->integer, error))
+ return FALSE;
+
+ /* exchange document ownership between stack entry and Q-Register */
+ if (!qreg->vtable->undo_exchange_string(qreg, &entry->string, error) ||
+ !qreg->vtable->exchange_string(qreg, &entry->string, error))
+ return FALSE;
+
+ /* pass entry ownership to undo stack. */
+ teco_undo_qreg_stack_push_own(entry);
+
+ g_array_remove_index(teco_qreg_stack, teco_qreg_stack->len-1);
+ return TRUE;
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_qreg_stack_clear(void)
+{
+ g_array_set_clear_func(teco_qreg_stack, (GDestroyNotify)teco_qreg_stack_entry_clear);
+ g_array_free(teco_qreg_stack, TRUE);
+}
+#endif
+
+gboolean
+teco_ed_hook(teco_ed_hook_t type, GError **error)
+{
+ if (!(teco_ed & TECO_ED_HOOKS))
+ return TRUE;
+
+ /*
+ * NOTE: It is crucial to declare this before the first goto,
+ * since it runs all destructors.
+ */
+ g_auto(teco_qreg_table_t) locals;
+ teco_qreg_table_init(&locals, FALSE);
+
+ teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "ED", 2);
+ if (!qreg) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Undefined ED-hook register (\"ED\")");
+ goto error_add_frame;
+ }
+
+ /*
+ * ED-hook execution should not see any
+ * integer parameters but the hook type.
+ * Such parameters could confuse the ED macro
+ * and macro authors do not expect side effects
+ * of ED macros on the expression stack.
+ * Also make sure it does not leave behind
+ * additional arguments on the stack.
+ *
+ * So this effectively executes:
+ * (typeM[ED]^[)
+ *
+ * FIXME: Temporarily stashing away the expression
+ * stack may be a more elegant solution.
+ */
+ teco_expressions_brace_open();
+ teco_expressions_push_int(type);
+
+ if (!teco_qreg_execute(qreg, &locals, error))
+ goto error_add_frame;
+
+ 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"
+ };
+
+error_add_frame:
+ g_assert(0 <= type-1 && type-1 < G_N_ELEMENTS(type2name));
+ teco_error_add_frame_edhook(type2name[type-1]);
+ return FALSE;
+}
+
+/** @extends teco_machine_t */
+struct teco_machine_qregspec_t {
+ teco_machine_t parent;
+
+ /**
+ * Aliases bitfield with an integer.
+ * This allows teco_undo_guint(__flags),
+ * while still supporting easy-to-access flags.
+ */
+ union {
+ struct {
+ teco_qreg_type_t type : 8;
+ gboolean parse_only : 1;
+ };
+ guint __flags;
+ };
+
+ /** Local Q-Register table of the macro invocation frame. */
+ teco_qreg_table_t *qreg_table_locals;
+
+ teco_machine_stringbuilding_t machine_stringbuilding;
+ /*
+ * FIXME: Does it make sense to allow nested braces?
+ * Perhaps it's sufficient to support ^Q].
+ */
+ gint nesting;
+ teco_string_t name;
+
+ teco_qreg_t *result;
+ teco_qreg_table_t *result_table;
+};
+
+/*
+ * 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_firstchar);
+TECO_DECLARE_STATE(teco_state_qregspec_secondchar);
+TECO_DECLARE_STATE(teco_state_qregspec_string);
+
+static teco_state_t *teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx,
+ gchar chr, GError **error);
+
+static teco_state_t *
+teco_state_qregspec_done(teco_machine_qregspec_t *ctx, GError **error)
+{
+ if (ctx->parse_only)
+ return &teco_state_qregspec_start;
+
+ ctx->result = teco_qreg_table_find(ctx->result_table, ctx->name.data, ctx->name.len);
+
+ switch (ctx->type) {
+ case TECO_QREG_REQUIRED:
+ if (!ctx->result) {
+ teco_error_invalidqreg_set(error, ctx->name.data, ctx->name.len,
+ ctx->result_table != &teco_qreg_table_globals);
+ return NULL;
+ }
+ break;
+
+ case TECO_QREG_OPTIONAL:
+ break;
+
+ 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_undo_remove(ctx->result);
+ }
+ break;
+ }
+
+ return &teco_state_qregspec_start;
+}
+
+static teco_state_t *
+teco_state_qregspec_start_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * FIXME: We're using teco_state_qregspec_start as a success condition,
+ * so either '.' goes into its own state or we re-introduce a state attribute.
+ */
+ if (chr == '.') {
+ if (ctx->parent.must_undo)
+ teco_undo_ptr(ctx->result_table);
+ ctx->result_table = ctx->qreg_table_locals;
+ return &teco_state_qregspec_start_global;
+ }
+
+ return teco_state_qregspec_start_global_input(ctx, chr, error);
+}
+
+/* in cmdline.c */
+gboolean teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error);
+
+TECO_DEFINE_STATE(teco_state_qregspec_start,
+ .is_start = TRUE,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * FIXME: Disallow space characters?
+ */
+ switch (chr) {
+ case '#':
+ return &teco_state_qregspec_firstchar;
+
+ case '[':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nesting);
+ ctx->nesting++;
+ return &teco_state_qregspec_string;
+ }
+
+ if (!ctx->parse_only) {
+ if (ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->name, ctx->name.len);
+ teco_string_append_c(&ctx->name, g_ascii_toupper(chr));
+ }
+ return teco_state_qregspec_done(ctx, error);
+}
+
+/*
+ * NOTE: This state mainly exists so that we don't have to go back to teco_state_qregspec_start after
+ * an initial `.` -- this is currently used in teco_machine_qregspec_input() to check for completeness.
+ * 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,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_qregspec_firstchar_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * FIXME: Disallow space characters?
+ */
+ if (!ctx->parse_only) {
+ if (ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->name, ctx->name.len);
+ teco_string_append_c(&ctx->name, g_ascii_toupper(chr));
+ }
+ return &teco_state_qregspec_secondchar;
+}
+
+TECO_DEFINE_STATE(teco_state_qregspec_firstchar,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_qregspec_secondchar_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * FIXME: Disallow space characters?
+ */
+ if (!ctx->parse_only) {
+ if (ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->name, ctx->name.len);
+ teco_string_append_c(&ctx->name, g_ascii_toupper(chr));
+ }
+ return teco_state_qregspec_done(ctx, error);
+}
+
+TECO_DEFINE_STATE(teco_state_qregspec_secondchar,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd
+);
+
+static teco_state_t *
+teco_state_qregspec_string_input(teco_machine_qregspec_t *ctx, gchar chr, GError **error)
+{
+ /*
+ * Makes sure that braces within string building constructs do not have to be
+ * escaped and that ^Q/^R can be used to escape braces.
+ *
+ * FIXME: Perhaps that's sufficient and we don't have to keep track of nesting?
+ */
+ if (ctx->machine_stringbuilding.parent.current->is_start) {
+ switch (chr) {
+ case '[':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nesting);
+ ctx->nesting++;
+ break;
+ case ']':
+ if (ctx->parent.must_undo)
+ teco_undo_gint(ctx->nesting);
+ ctx->nesting--;
+ if (!ctx->nesting)
+ return teco_state_qregspec_done(ctx, error);
+ break;
+ }
+ }
+
+ if (!ctx->parse_only && ctx->parent.must_undo)
+ undo__teco_string_truncate(&ctx->name, ctx->name.len);
+
+ /*
+ * NOTE: machine_stringbuilding gets notified about parse-only mode by passing NULL
+ * as the target string.
+ */
+ if (!teco_machine_stringbuilding_input(&ctx->machine_stringbuilding, chr,
+ ctx->parse_only ? NULL : &ctx->name, error))
+ return NULL;
+
+ return &teco_state_qregspec_string;
+}
+
+/* in cmdline.c */
+gboolean teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx,
+ gchar key, GError **error);
+
+TECO_DEFINE_STATE(teco_state_qregspec_string,
+ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_string_process_edit_cmd
+);
+
+/** @static @memberof teco_machine_qregspec_t */
+teco_machine_qregspec_t *
+teco_machine_qregspec_new(teco_qreg_type_t type, teco_qreg_table_t *locals, gboolean must_undo)
+{
+ /*
+ * FIXME: Allocate via g_slice?
+ */
+ teco_machine_qregspec_t *ctx = g_new0(teco_machine_qregspec_t, 1);
+ teco_machine_init(&ctx->parent, &teco_state_qregspec_start, must_undo);
+ ctx->type = type;
+ ctx->qreg_table_locals = locals;
+ teco_machine_stringbuilding_init(&ctx->machine_stringbuilding, '[', locals, must_undo);
+ ctx->result_table = &teco_qreg_table_globals;
+ return ctx;
+}
+
+/** @memberof teco_machine_qregspec_t */
+void
+teco_machine_qregspec_reset(teco_machine_qregspec_t *ctx)
+{
+ teco_machine_reset(&ctx->parent, &teco_state_qregspec_start);
+ teco_machine_stringbuilding_reset(&ctx->machine_stringbuilding);
+ if (ctx->parent.must_undo) {
+ teco_undo_string_own(ctx->name);
+ teco_undo_gint(ctx->nesting);
+ teco_undo_guint(ctx->__flags);
+ } else {
+ teco_string_clear(&ctx->name);
+ }
+ memset(&ctx->name, 0, sizeof(ctx->name));
+ ctx->nesting = 0;
+ ctx->result_table = &teco_qreg_table_globals;
+}
+
+/** @memberof teco_machine_qregspec_t */
+teco_machine_stringbuilding_t *
+teco_machine_qregspec_get_stringbuilding(teco_machine_qregspec_t *ctx)
+{
+ return &ctx->machine_stringbuilding;
+}
+
+/**
+ * Pass a character to the QRegister specification machine.
+ *
+ * @param ctx QRegister specification machine.
+ * @param chr Character to parse.
+ * @param result Pointer to QRegister or NULL in parse-only mode.
+ * If non-NULL it will be set once a specification is successfully parsed.
+ * @param result_table Pointer to QRegister table. May be NULL in parse-only mode.
+ * @param error GError or NULL.
+ * @return Returns TECO_MACHINE_QREGSPEC_DONE in case of complete specs.
+ *
+ * @memberof teco_machine_qregspec_t
+ */
+teco_machine_qregspec_status_t
+teco_machine_qregspec_input(teco_machine_qregspec_t *ctx, gchar chr,
+ teco_qreg_t **result, teco_qreg_table_t **result_table, GError **error)
+{
+ ctx->parse_only = result == NULL;
+
+ if (!teco_machine_input(&ctx->parent, chr, error))
+ return TECO_MACHINE_QREGSPEC_ERROR;
+
+ teco_machine_qregspec_get_results(ctx, result, result_table);
+ return ctx->parent.current == &teco_state_qregspec_start
+ ? TECO_MACHINE_QREGSPEC_DONE : TECO_MACHINE_QREGSPEC_MORE;
+}
+
+/** @memberof teco_machine_qregspec_t */
+void
+teco_machine_qregspec_get_results(teco_machine_qregspec_t *ctx,
+ teco_qreg_t **result, teco_qreg_table_t **result_table)
+{
+ if (result)
+ *result = ctx->result;
+ if (result_table)
+ *result_table = ctx->result_table;
+}
+
+/** @memberof teco_machine_qregspec_t */
+gboolean
+teco_machine_qregspec_auto_complete(teco_machine_qregspec_t *ctx, teco_string_t *insert)
+{
+ gsize restrict_len = 0;
+
+ /*
+ * NOTE: We could have separate process_edit_cmd_cb() for
+ * teco_state_qregspec_firstchar/teco_state_qregspec_secondchar
+ * and pass down restrict_len instead.
+ */
+ if (ctx->parent.current == &teco_state_qregspec_start ||
+ ctx->parent.current == &teco_state_qregspec_start_global)
+ /* single-letter Q-Reg */
+ restrict_len = 1;
+ else if (ctx->parent.current != &teco_state_qregspec_string)
+ /* two-letter Q-Reg */
+ restrict_len = 2;
+
+ return teco_rb3str_auto_complete(&ctx->result_table->tree, !restrict_len,
+ ctx->name.data, ctx->name.len, restrict_len, insert) &&
+ ctx->nesting == 1;
+}
+
+/** @memberof teco_machine_qregspec_t */
+void
+teco_machine_qregspec_free(teco_machine_qregspec_t *ctx)
+{
+ teco_machine_stringbuilding_clear(&ctx->machine_stringbuilding);
+ teco_string_clear(&ctx->name);
+ g_free(ctx);
+}
+
+TECO_DEFINE_UNDO_CALL(teco_machine_qregspec_free, teco_machine_qregspec_t *);
+TECO_DEFINE_UNDO_OBJECT_OWN(qregspec, teco_machine_qregspec_t *, teco_machine_qregspec_free);
diff --git a/src/qreg.h b/src/qreg.h
new file mode 100644
index 0000000..4797a01
--- /dev/null
+++ b/src/qreg.h
@@ -0,0 +1,238 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include "sciteco.h"
+#include "view.h"
+#include "doc.h"
+#include "undo.h"
+#include "string-utils.h"
+#include "rb3str.h"
+
+/*
+ * Forward declarations.
+ */
+typedef struct teco_qreg_t teco_qreg_t;
+/* Could be avoided by moving teco_qreg_execute() to the end of the file instead... */
+typedef struct teco_qreg_table_t teco_qreg_table_t;
+
+extern teco_view_t *teco_qreg_view;
+
+/*
+ * NOTE: This is not "hidden" in qreg.c, so that we won't need wrapper
+ * functions for every vtable method.
+ *
+ * FIXME: Use TECO_DECLARE_VTABLE_METHOD(gboolean, teco_qreg, set_integer, teco_qreg_t *, teco_int_t, GError **);
+ * ...
+ * teco_qreg_set_integer_t set_integer;
+ */
+typedef const struct {
+ gboolean (*set_integer)(teco_qreg_t *qreg, teco_int_t value, GError **error);
+ gboolean (*undo_set_integer)(teco_qreg_t *qreg, GError **error);
+ gboolean (*get_integer)(teco_qreg_t *qreg, teco_int_t *ret, GError **error);
+
+ gboolean (*set_string)(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error);
+ gboolean (*undo_set_string)(teco_qreg_t *qreg, GError **error);
+ gboolean (*append_string)(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error);
+ gboolean (*undo_append_string)(teco_qreg_t *qreg, GError **error);
+
+ gboolean (*get_string)(teco_qreg_t *qreg, gchar **str, gsize *len, GError **error);
+ gint (*get_character)(teco_qreg_t *qreg, guint position, GError **error);
+
+ /*
+ * These callbacks exist only to optimize teco_qreg_stack_push|pop()
+ * for plain Q-Registers making [q and ]q quite efficient operations even on rubout.
+ * On the other hand, this unnecessarily complicates teco_qreg_t derivations.
+ */
+ gboolean (*exchange_string)(teco_qreg_t *qreg, teco_doc_t *src, GError **error);
+ gboolean (*undo_exchange_string)(teco_qreg_t *qreg, teco_doc_t *src, GError **error);
+
+ gboolean (*edit)(teco_qreg_t *qreg, GError **error);
+ gboolean (*undo_edit)(teco_qreg_t *qreg, GError **error);
+} teco_qreg_vtable_t;
+
+/** @extends teco_rb3str_head_t */
+struct teco_qreg_t {
+ /*
+ * NOTE: Must be the first member since we "upcast" to teco_qreg_t
+ */
+ teco_rb3str_head_t head;
+
+ teco_qreg_vtable_t *vtable;
+
+ teco_int_t integer;
+ teco_doc_t string;
+
+ /*
+ * Whether to generate undo tokens (unnecessary for registers
+ * in local qreg tables in macro invocations).
+ *
+ * FIXME: Every QRegister has this field, but it only differs
+ * between local and global QRegisters. This wastes space.
+ * Or by deferring any decision about undo token creation to a layer
+ * that knows which table it is accessing.
+ * On the other hand, we will need another flag like
+ * teco_qreg_current_must_undo.
+ *
+ * Otherwise, it might be possible to use a least significant bit
+ * in one of the pointers...
+ */
+ gboolean must_undo;
+};
+
+teco_qreg_t *teco_qreg_plain_new(const gchar *name, gsize len);
+teco_qreg_t *teco_qreg_bufferinfo_new(void);
+teco_qreg_t *teco_qreg_workingdir_new(void);
+teco_qreg_t *teco_qreg_clipboard_new(const gchar *name);
+
+gboolean teco_qreg_execute(teco_qreg_t *qreg, teco_qreg_table_t *qreg_table_locals, GError **error);
+
+void teco_qreg_undo_set_eol_mode(teco_qreg_t *qreg);
+void teco_qreg_set_eol_mode(teco_qreg_t *qreg, gint mode);
+
+/*
+ * Load and save already care about undo token
+ * creation.
+ */
+gboolean teco_qreg_load(teco_qreg_t *qreg, const gchar *filename, GError **error);
+gboolean teco_qreg_save(teco_qreg_t *qreg, const gchar *filename, GError **error);
+
+/** @memberof teco_qreg_t */
+static inline void
+teco_qreg_free(teco_qreg_t *qreg)
+{
+ teco_doc_clear(&qreg->string);
+ teco_string_clear(&qreg->head.name);
+ g_free(qreg);
+}
+
+extern teco_qreg_t *teco_qreg_current;
+
+/** @extends teco_rb3str_tree_t */
+struct teco_qreg_table_t {
+ teco_rb3str_tree_t tree;
+
+ /*
+ * FIXME: Probably even this property can be eliminated.
+ * The only two tables with undo in the system are
+ * a) The global register table
+ * b) The top-level local register table.
+ */
+ gboolean must_undo;
+};
+
+void teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo);
+
+/** @memberof teco_qreg_table_t */
+static inline teco_qreg_t *
+teco_qreg_table_insert(teco_qreg_table_t *table, teco_qreg_t *qreg)
+{
+ qreg->must_undo = table->must_undo; // FIXME
+ return (teco_qreg_t *)teco_rb3str_insert(&table->tree, TRUE, &qreg->head);
+}
+
+/** @memberof teco_qreg_table_t */
+static inline teco_qreg_t *
+teco_qreg_table_find(teco_qreg_table_t *table, const gchar *name, gsize len)
+{
+ return (teco_qreg_t *)teco_rb3str_find(&table->tree, TRUE, name, len);
+}
+
+teco_qreg_t *teco_qreg_table_edit_name(teco_qreg_table_t *table, const gchar *name,
+ gsize len, GError **error);
+
+/** @memberof teco_qreg_table_t */
+static inline gboolean
+teco_qreg_table_edit(teco_qreg_table_t *table, teco_qreg_t *qreg, GError **error)
+{
+ if (!qreg->vtable->edit(qreg, error))
+ return FALSE;
+ teco_qreg_current = qreg;
+ return TRUE;
+}
+
+gboolean teco_qreg_table_set_environ(teco_qreg_table_t *table, GError **error);
+gchar **teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error);
+
+gboolean teco_qreg_table_empty(teco_qreg_table_t *table, GError **error);
+void teco_qreg_table_clear(teco_qreg_table_t *table);
+
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_qreg_table_t, teco_qreg_table_clear);
+
+extern teco_qreg_table_t teco_qreg_table_globals;
+
+gboolean teco_qreg_stack_push(teco_qreg_t *qreg, GError **error);
+gboolean teco_qreg_stack_pop(teco_qreg_t *qreg, GError **error);
+
+typedef enum {
+ TECO_ED_HOOK_ADD = 1,
+ TECO_ED_HOOK_EDIT,
+ TECO_ED_HOOK_CLOSE,
+ TECO_ED_HOOK_QUIT
+} teco_ed_hook_t;
+
+gboolean teco_ed_hook(teco_ed_hook_t type, GError **error);
+
+typedef enum {
+ TECO_MACHINE_QREGSPEC_ERROR = 0,
+ TECO_MACHINE_QREGSPEC_MORE,
+ TECO_MACHINE_QREGSPEC_DONE
+} teco_machine_qregspec_status_t;
+
+typedef enum {
+ /** Register must exist, else fail */
+ TECO_QREG_REQUIRED,
+ /**
+ * Return NULL if register does not exist.
+ * You can still call QRegSpecMachine::fail() to require it.
+ */
+ TECO_QREG_OPTIONAL,
+ /** Initialize register if it does not already exist */
+ TECO_QREG_OPTIONAL_INIT
+} teco_qreg_type_t;
+
+typedef struct teco_machine_qregspec_t teco_machine_qregspec_t;
+
+teco_machine_qregspec_t *teco_machine_qregspec_new(teco_qreg_type_t type,
+ teco_qreg_table_t *locals, gboolean must_undo);
+
+void teco_machine_qregspec_reset(teco_machine_qregspec_t *ctx);
+
+/*
+ * FIXME: This uses a forward declaration since we must not include parser.h
+ */
+struct teco_machine_stringbuilding_t *teco_machine_qregspec_get_stringbuilding(teco_machine_qregspec_t *ctx);
+
+teco_machine_qregspec_status_t teco_machine_qregspec_input(teco_machine_qregspec_t *ctx, gchar chr,
+ teco_qreg_t **result,
+ teco_qreg_table_t **result_table, GError **error);
+
+void teco_machine_qregspec_get_results(teco_machine_qregspec_t *ctx,
+ teco_qreg_t **result, teco_qreg_table_t **result_table);
+
+gboolean teco_machine_qregspec_auto_complete(teco_machine_qregspec_t *ctx, teco_string_t *insert);
+
+void teco_machine_qregspec_free(teco_machine_qregspec_t *ctx);
+
+/** @memberof teco_machine_qregspec_t */
+void undo__teco_machine_qregspec_free(teco_machine_qregspec_t *);
+TECO_DECLARE_UNDO_OBJECT(qregspec, teco_machine_qregspec_t *);
+
+#define teco_undo_qregspec_own(VAR) \
+ (*teco_undo_object_qregspec_push(&(VAR)))
diff --git a/src/qregisters.cpp b/src/qregisters.cpp
deleted file mode 100644
index c23656c..0000000
--- a/src/qregisters.cpp
+++ /dev/null
@@ -1,1680 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <string.h>
-#include <bsd/sys/queue.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-#include <glib/gstdio.h>
-
-#include <Scintilla.h>
-
-#include "sciteco.h"
-#include "string-utils.h"
-#include "interface.h"
-#include "undo.h"
-#include "parser.h"
-#include "expressions.h"
-#include "document.h"
-#include "ring.h"
-#include "ioview.h"
-#include "eol.h"
-#include "error.h"
-#include "qregisters.h"
-
-namespace SciTECO {
-
-namespace States {
- StatePushQReg pushqreg;
- StatePopQReg popqreg;
- StateEQCommand eqcommand;
- StateLoadQReg loadqreg;
- StateEPctCommand epctcommand;
- StateSaveQReg saveqreg;
- StateQueryQReg queryqreg;
- StateCtlUCommand ctlucommand;
- StateEUCommand eucommand;
- StateSetQRegString setqregstring_nobuilding(false);
- StateSetQRegString setqregstring_building(true);
- StateGetQRegString getqregstring;
- StateSetQRegInteger setqreginteger;
- StateIncreaseQReg increaseqreg;
- StateMacro macro;
- StateMacroFile macro_file;
- StateCopyToQReg copytoqreg;
-}
-
-namespace QRegisters {
- QRegisterTable *locals = NULL;
- QRegister *current = NULL;
-
- static QRegisterStack stack;
-}
-
-static QRegister *register_argument = NULL;
-
-void
-QRegisterData::set_string(const gchar *str, gsize len)
-{
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.reset();
- string.edit(QRegisters::view);
-
- QRegisters::view.ssm(SCI_BEGINUNDOACTION);
- QRegisters::view.ssm(SCI_CLEARALL);
- QRegisters::view.ssm(SCI_APPENDTEXT,
- len, (sptr_t)(str ? : ""));
- QRegisters::view.ssm(SCI_ENDUNDOACTION);
-
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
-}
-
-void
-QRegisterData::undo_set_string(void)
-{
- if (!must_undo)
- return;
-
- /*
- * Necessary, so that upon rubout the
- * string's parameters are restored.
- */
- string.update(QRegisters::view);
-
- if (QRegisters::current && QRegisters::current->must_undo)
- QRegisters::current->string.undo_edit(QRegisters::view);
-
- string.undo_reset();
- QRegisters::view.undo_ssm(SCI_UNDO);
-
- string.undo_edit(QRegisters::view);
-}
-
-void
-QRegisterData::append_string(const gchar *str, gsize len)
-{
- /*
- * NOTE: Will not create undo action
- * if string is empty.
- * Also, appending preserves the string's
- * parameters.
- */
- if (!len)
- return;
-
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.edit(QRegisters::view);
-
- QRegisters::view.ssm(SCI_BEGINUNDOACTION);
- QRegisters::view.ssm(SCI_APPENDTEXT,
- len, (sptr_t)str);
- QRegisters::view.ssm(SCI_ENDUNDOACTION);
-
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
-}
-
-gchar *
-QRegisterData::get_string(void)
-{
- gint size;
- gchar *str;
-
- if (!string.is_initialized())
- return g_strdup("");
-
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.edit(QRegisters::view);
-
- size = QRegisters::view.ssm(SCI_GETLENGTH) + 1;
- str = (gchar *)g_malloc(size);
- QRegisters::view.ssm(SCI_GETTEXT, size, (sptr_t)str);
-
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
-
- return str;
-}
-
-gsize
-QRegisterData::get_string_size(void)
-{
- gsize size;
-
- if (!string.is_initialized())
- return 0;
-
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.edit(QRegisters::view);
-
- size = QRegisters::view.ssm(SCI_GETLENGTH);
-
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
-
- return size;
-}
-
-gint
-QRegisterData::get_character(gint position)
-{
- gint ret = -1;
-
- if (position < 0)
- return -1;
-
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.edit(QRegisters::view);
-
- if (position < QRegisters::view.ssm(SCI_GETLENGTH))
- ret = QRegisters::view.ssm(SCI_GETCHARAT, position);
-
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
-
- return ret;
-}
-
-void
-QRegisterData::undo_exchange_string(QRegisterData &reg)
-{
- if (must_undo)
- string.undo_exchange();
- if (reg.must_undo)
- reg.string.undo_exchange();
-}
-
-void
-QRegister::edit(void)
-{
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.edit(QRegisters::view);
- interface.show_view(&QRegisters::view);
- interface.info_update(this);
-}
-
-void
-QRegister::undo_edit(void)
-{
- /*
- * We might be switching the current document
- * to a buffer.
- */
- string.update(QRegisters::view);
-
- if (!must_undo)
- return;
-
- interface.undo_info_update(this);
- string.undo_edit(QRegisters::view);
- interface.undo_show_view(&QRegisters::view);
-}
-
-void
-QRegister::execute(bool locals)
-{
- gchar *str = get_string();
-
- try {
- Execute::macro(str, locals);
- } catch (Error &error) {
- error.add_frame(new Error::QRegFrame(name));
-
- g_free(str);
- throw; /* forward */
- } catch (...) {
- g_free(str);
- throw; /* forward */
- }
-
- g_free(str);
-}
-
-void
-QRegister::undo_set_eol_mode(void)
-{
- if (!must_undo)
- return;
-
- /*
- * Necessary, so that upon rubout the
- * string's parameters are restored.
- */
- string.update(QRegisters::view);
-
- if (QRegisters::current && QRegisters::current->must_undo)
- QRegisters::current->string.undo_edit(QRegisters::view);
-
- QRegisters::view.undo_ssm(SCI_SETEOLMODE,
- QRegisters::view.ssm(SCI_GETEOLMODE));
-
- string.undo_edit(QRegisters::view);
-}
-
-void
-QRegister::set_eol_mode(gint mode)
-{
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.edit(QRegisters::view);
- QRegisters::view.ssm(SCI_SETEOLMODE, mode);
-
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
-}
-
-void
-QRegister::load(const gchar *filename)
-{
- undo_set_string();
-
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.edit(QRegisters::view);
- string.reset();
-
- /*
- * IOView::load() might change the EOL style.
- */
- undo_set_eol_mode();
-
- /*
- * undo_set_string() pushes undo tokens that restore
- * the previous document in the view.
- * So if loading fails, QRegisters::current will be
- * made the current document again.
- */
- QRegisters::view.load(filename);
-
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
-}
-
-void
-QRegister::save(const gchar *filename)
-{
- if (QRegisters::current)
- QRegisters::current->string.update(QRegisters::view);
-
- string.edit(QRegisters::view);
-
- try {
- QRegisters::view.save(filename);
- } catch (...) {
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
- throw; /* forward */
- }
-
- if (QRegisters::current)
- QRegisters::current->string.edit(QRegisters::view);
-}
-
-tecoInt
-QRegisterBufferInfo::set_integer(tecoInt v)
-{
- if (!ring.edit(v))
- throw Error("Invalid buffer id %" TECO_INTEGER_FORMAT, v);
-
- return v;
-}
-
-void
-QRegisterBufferInfo::undo_set_integer(void)
-{
- current_doc_undo_edit();
-}
-
-tecoInt
-QRegisterBufferInfo::get_integer(void)
-{
- return ring.get_id();
-}
-
-gchar *
-QRegisterBufferInfo::get_string(void)
-{
- gchar *str = g_strdup(ring.current->filename ? : "");
-
- /*
- * On platforms with a default non-forward-slash directory
- * separator (i.e. Windows), Buffer::filename will have
- * the wrong separator.
- * To make the life of macros that evaluate "*" easier,
- * the directory separators are normalized to "/" here.
- * This does not change the size of the string, so
- * get_string_size() still works.
- */
- return normalize_path(str);
-}
-
-gsize
-QRegisterBufferInfo::get_string_size(void)
-{
- return ring.current->filename ? strlen(ring.current->filename) : 0;
-}
-
-gint
-QRegisterBufferInfo::get_character(gint position)
-{
- if (position < 0 ||
- position >= (gint)QRegisterBufferInfo::get_string_size())
- return -1;
-
- return ring.current->filename[position];
-}
-
-void
-QRegisterBufferInfo::edit(void)
-{
- gchar *str;
-
- QRegister::edit();
-
- QRegisters::view.ssm(SCI_BEGINUNDOACTION);
- str = QRegisterBufferInfo::get_string();
- QRegisters::view.ssm(SCI_SETTEXT, 0, (sptr_t)str);
- g_free(str);
- QRegisters::view.ssm(SCI_ENDUNDOACTION);
-
- QRegisters::view.undo_ssm(SCI_UNDO);
-}
-
-void
-QRegisterWorkingDir::set_string(const gchar *str, gsize len)
-{
- /* str is not null-terminated */
- gchar *dir = g_strndup(str, len);
- int ret = g_chdir(dir);
-
- g_free(dir);
-
- if (ret)
- /* FIXME: Is errno usable on Windows here? */
- throw Error("Cannot change working directory "
- "to \"%.*s\"", (int)len, str);
-}
-
-void
-QRegisterWorkingDir::undo_set_string(void)
-{
- /* pass ownership of current dir string */
- undo.push_own<UndoTokenChangeDir>(g_get_current_dir());
-}
-
-gchar *
-QRegisterWorkingDir::get_string(void)
-{
- /*
- * On platforms with a default non-forward-slash directory
- * separator (i.e. Windows), Buffer::filename will have
- * the wrong separator.
- * To make the life of macros that evaluate "$" easier,
- * the directory separators are normalized to "/" here.
- * This does not change the size of the string, so
- * get_string_size() still works.
- */
- return normalize_path(g_get_current_dir());
-}
-
-gsize
-QRegisterWorkingDir::get_string_size(void)
-{
- gchar *str = g_get_current_dir();
- gsize len = strlen(str);
-
- g_free(str);
- return len;
-}
-
-gint
-QRegisterWorkingDir::get_character(gint position)
-{
- gchar *str = QRegisterWorkingDir::get_string();
- gint ret = -1;
-
- if (position >= 0 &&
- position < (gint)strlen(str))
- ret = str[position];
-
- g_free(str);
- return ret;
-}
-
-void
-QRegisterWorkingDir::edit(void)
-{
- gchar *str;
-
- QRegister::edit();
-
- QRegisters::view.ssm(SCI_BEGINUNDOACTION);
- str = QRegisterWorkingDir::get_string();
- QRegisters::view.ssm(SCI_SETTEXT, 0, (sptr_t)str);
- g_free(str);
- QRegisters::view.ssm(SCI_ENDUNDOACTION);
-
- QRegisters::view.undo_ssm(SCI_UNDO);
-}
-
-void
-QRegisterWorkingDir::exchange_string(QRegisterData &reg)
-{
- gchar *own_str = QRegisterWorkingDir::get_string();
- gchar *other_str = reg.get_string();
-
- QRegisterData::set_string(other_str);
- g_free(other_str);
- reg.set_string(own_str);
- g_free(own_str);
-}
-
-void
-QRegisterWorkingDir::undo_exchange_string(QRegisterData &reg)
-{
- QRegisterWorkingDir::undo_set_string();
- reg.undo_set_string();
-}
-
-void
-QRegisterClipboard::UndoTokenSetClipboard::run(void)
-{
- interface.set_clipboard(name, str, str_len);
-}
-
-void
-QRegisterClipboard::set_string(const gchar *str, gsize len)
-{
- if (Flags::ed & Flags::ED_AUTOEOL) {
- GString *str_converted = g_string_sized_new(len);
- /*
- * This will convert to the Q-Register view's EOL mode.
- */
- EOLWriterMem writer(str_converted,
- QRegisters::view.ssm(SCI_GETEOLMODE));
- gsize bytes_written;
-
- /*
- * NOTE: Shouldn't throw any error, ever.
- */
- bytes_written = writer.convert(str, len);
- g_assert(bytes_written == len);
-
- interface.set_clipboard(get_clipboard_name(),
- str_converted->str, str_converted->len);
-
- g_string_free(str_converted, TRUE);
- } else {
- /*
- * No EOL conversion necessary. The EOLWriter can handle
- * this as well, but will result in unnecessary allocations.
- */
- interface.set_clipboard(get_clipboard_name(), str, len);
- }
-}
-
-void
-QRegisterClipboard::undo_set_string(void)
-{
- gchar *str;
- gsize str_len;
-
- /*
- * Upon rubout, the current contents of the clipboard are
- * restored.
- * We are checking for undo.enabled instead of relying on
- * undo.push_own(), since getting the clipboard
- * is an expensive operation that we want to avoid.
- */
- if (!undo.enabled)
- return;
-
- /*
- * Ownership of str (may be NULL) is passed to
- * the undo token. We do not need undo.push_own() since
- * we checked for undo.enabled before.
- * This avoids any EOL translation as that would be cumbersome
- * and could also modify the clipboard in unexpected ways.
- */
- str = interface.get_clipboard(get_clipboard_name(), &str_len);
- undo.push<UndoTokenSetClipboard>(get_clipboard_name(), str, str_len);
-}
-
-gchar *
-QRegisterClipboard::get_string(gsize *out_len)
-{
- if (!(Flags::ed & Flags::ED_AUTOEOL)) {
- /*
- * No auto-eol conversion - avoid unnecessary copying
- * and allocations.
- * NOTE: get_clipboard() already returns a null-terminated string.
- */
- return interface.get_clipboard(get_clipboard_name(), out_len);
- }
-
- gsize str_len;
- gchar *str = interface.get_clipboard(get_clipboard_name(), &str_len);
- EOLReaderMem reader(str, str_len);
- gchar *str_converted;
-
- try {
- str_converted = reader.convert_all(out_len);
- } catch (...) {
- g_free(str);
- throw; /* forward */
- }
- g_free(str);
-
- return str_converted;
-}
-
-gchar *
-QRegisterClipboard::get_string(void)
-{
- return QRegisterClipboard::get_string(NULL);
-}
-
-gsize
-QRegisterClipboard::get_string_size(void)
-{
- gsize str_len;
- gchar *str = interface.get_clipboard(get_clipboard_name(), &str_len);
- /*
- * Using the EOLReader does not hurt much if AutoEOL is disabled
- * since we use it only for counting the bytes.
- */
- EOLReaderMem reader(str, str_len);
- gsize data_len;
- gsize converted_len = 0;
-
- try {
- while (reader.convert(data_len))
- converted_len += data_len;
- } catch (...) {
- g_free(str);
- throw; /* forward */
- }
- g_free(str);
-
- return converted_len;
-}
-
-gint
-QRegisterClipboard::get_character(gint position)
-{
- gsize str_len;
- gchar *str = QRegisterClipboard::get_string(&str_len);
- gint ret = -1;
-
- /*
- * `str` may be NULL, but only if str_len == 0 as well.
- */
- if (position >= 0 &&
- position < (gint)str_len)
- ret = str[position];
-
- g_free(str);
- return ret;
-}
-
-void
-QRegisterClipboard::edit(void)
-{
- gchar *str;
- gsize str_len;
-
- QRegister::edit();
-
- QRegisters::view.ssm(SCI_BEGINUNDOACTION);
- QRegisters::view.ssm(SCI_CLEARALL);
- str = QRegisterClipboard::get_string(&str_len);
- QRegisters::view.ssm(SCI_APPENDTEXT, str_len, (sptr_t)str);
- g_free(str);
- QRegisters::view.ssm(SCI_ENDUNDOACTION);
-
- QRegisters::view.undo_ssm(SCI_UNDO);
-}
-
-void
-QRegisterClipboard::exchange_string(QRegisterData &reg)
-{
- gchar *own_str;
- gsize own_str_len;
- gchar *other_str = reg.get_string();
- gsize other_str_len = reg.get_string_size();
-
- /*
- * FIXME: What if `reg` is a clipboard and it changes
- * between the two calls?
- * QRegister::get_string() should always return the length as well.
- */
- QRegisterData::set_string(other_str, other_str_len);
- g_free(other_str);
- own_str = QRegisterClipboard::get_string(&own_str_len);
- reg.set_string(own_str, own_str_len);
- g_free(own_str);
-}
-
-void
-QRegisterClipboard::undo_exchange_string(QRegisterData &reg)
-{
- QRegisterClipboard::undo_set_string();
- reg.undo_set_string();
-}
-
-void
-QRegisterTable::UndoTokenRemoveGlobal::run(void)
-{
- delete (QRegister *)QRegisters::globals.remove(reg);
-}
-
-void
-QRegisterTable::UndoTokenRemoveLocal::run(void)
-{
- /*
- * NOTE: QRegisters::locals should point
- * to the correct table when the token is
- * executed.
- */
- delete (QRegister *)QRegisters::locals->remove(reg);
-}
-
-void
-QRegisterTable::undo_remove(QRegister *reg)
-{
- if (!must_undo)
- return;
-
- /*
- * NOTE: Could also be solved using a virtual
- * method and subclasses...
- */
- if (this == &QRegisters::globals)
- undo.push<UndoTokenRemoveGlobal>(reg);
- else
- undo.push<UndoTokenRemoveLocal>(reg);
-}
-
-void
-QRegisterTable::insert_defaults(void)
-{
- /* general purpose registers */
- for (gchar q = 'A'; q <= 'Z'; q++)
- insert(q);
- for (gchar q = '0'; q <= '9'; q++)
- insert(q);
-}
-
-/*
- * NOTE: by not making this inline,
- * we can access QRegisters::current
- */
-void
-QRegisterTable::edit(QRegister *reg)
-{
- reg->edit();
- QRegisters::current = reg;
-}
-
-/**
- * Import process environment into table
- * by setting environment registers for every
- * environment variable.
- * It is assumed that the table does not yet
- * contain any environment register.
- *
- * In general this method is only safe to call
- * at startup.
- */
-void
-QRegisterTable::set_environ(void)
-{
- /*
- * NOTE: Using g_get_environ() would be more efficient,
- * but it appears to be broken, at least on Wine
- * and Windows 2000.
- */
- gchar **env = g_listenv();
-
- for (gchar **key = env; *key; key++) {
- gchar name[1 + strlen(*key) + 1];
- QRegister *reg;
-
- name[0] = '$';
- strcpy(name + 1, *key);
-
- reg = insert(name);
- reg->set_string(g_getenv(*key));
- }
-
- g_strfreev(env);
-}
-
-/**
- * Export environment registers as a list of environment
- * variables compatible with `g_get_environ()`.
- *
- * @return Zero-terminated list of strings in the form
- * `NAME=VALUE`. Should be freed with `g_strfreev()`.
- */
-gchar **
-QRegisterTable::get_environ(void)
-{
- QRegister *first = nfind("$");
-
- gint envp_len = 1;
- gchar **envp, **p;
-
- /*
- * Iterate over all registers beginning with "$" to
- * guess the size required for the environment array.
- * This may waste a few bytes because not __every__
- * register beginning with "$" is an environment
- * register.
- */
- for (QRegister *cur = first;
- cur && cur->name[0] == '$';
- cur = (QRegister *)cur->next())
- envp_len++;
-
- p = envp = (gchar **)g_malloc(sizeof(gchar *)*envp_len);
-
- for (QRegister *cur = first;
- cur && cur->name[0] == '$';
- cur = (QRegister *)cur->next()) {
- gchar *value;
-
- /*
- * Ignore the "$" register (not an environment
- * variable register) and registers whose
- * name contains "=" (not allowed in environment
- * variable names).
- */
- if (!cur->name[1] || strchr(cur->name+1, '='))
- continue;
-
- value = cur->get_string();
- /* more efficient than g_environ_setenv() */
- *p++ = g_strconcat(cur->name+1, "=", value, NIL);
- g_free(value);
- }
-
- *p = NULL;
-
- return envp;
-}
-
-/**
- * Update process environment with environment registers
- * using `g_setenv()`.
- * It does not try to unset environment variables that
- * are no longer in the Q-Register table.
- *
- * This method may be dangerous in a multi-threaded environment
- * but may be necessary for libraries that access important
- * environment variables internally without providing alternative
- * APIs.
- */
-void
-QRegisterTable::update_environ(void)
-{
- for (QRegister *cur = nfind("$");
- cur && cur->name[0] == '$';
- cur = (QRegister *)cur->next()) {
- gchar *value;
-
- /*
- * Ignore the "$" register (not an environment
- * variable register) and registers whose
- * name contains "=" (not allowed in environment
- * variable names).
- */
- if (!cur->name[1] || strchr(cur->name+1, '='))
- continue;
-
- value = cur->get_string();
- g_setenv(cur->name+1, value, TRUE);
- g_free(value);
- }
-}
-
-/**
- * Free resources associated with table.
- *
- * This is similar to the destructor but
- * has the advantage that we can check whether some
- * register is currently edited.
- * Since this is not a destructor, we can throw
- * errors.
- * Therefore this method should be called before
- * a (local) QRegisterTable is deleted.
- */
-void
-QRegisterTable::clear(void)
-{
- QRegister *cur;
-
- while ((cur = (QRegister *)root())) {
- if (cur == QRegisters::current)
- throw Error("Currently edited Q-Register \"%s\" "
- "cannot be discarded", cur->name);
-
- delete (QRegister *)remove(cur);
- }
-}
-
-void
-QRegisterStack::UndoTokenPush::run(void)
-{
- SLIST_INSERT_HEAD(&stack->head, entry, entries);
- entry = NULL;
-}
-
-void
-QRegisterStack::UndoTokenPop::run(void)
-{
- Entry *entry = SLIST_FIRST(&stack->head);
-
- SLIST_REMOVE_HEAD(&stack->head, entries);
- delete entry;
-}
-
-void
-QRegisterStack::push(QRegister &reg)
-{
- Entry *entry = new Entry();
-
- gchar *str = reg.get_string();
- if (*str)
- entry->set_string(str);
- g_free(str);
- entry->string.update(reg.string);
- entry->set_integer(reg.get_integer());
-
- SLIST_INSERT_HEAD(&head, entry, entries);
- undo.push<UndoTokenPop>(this);
-}
-
-bool
-QRegisterStack::pop(QRegister &reg)
-{
- Entry *entry = SLIST_FIRST(&head);
-
- if (!entry)
- return false;
-
- reg.undo_set_integer();
- reg.set_integer(entry->get_integer());
-
- /* exchange document ownership between Stack entry and Q-Register */
- reg.undo_exchange_string(*entry);
- reg.exchange_string(*entry);
-
- SLIST_REMOVE_HEAD(&head, entries);
- /* Pass entry ownership to undo stack. */
- undo.push_own<UndoTokenPush>(this, entry);
-
- return true;
-}
-
-QRegisterStack::~QRegisterStack()
-{
- Entry *entry, *next;
-
- SLIST_FOREACH_SAFE(entry, &head, entries, next)
- delete entry;
-}
-
-void
-QRegisters::hook(Hook type)
-{
- static const gchar *type2name[] = {
- /* [HOOK_ADD-1] = */ "ADD",
- /* [HOOK_EDIT-1] = */ "EDIT",
- /* [HOOK_CLOSE-1] = */ "CLOSE",
- /* [HOOK_QUIT-1] = */ "QUIT",
- };
-
- QRegister *reg;
-
- if (!(Flags::ed & Flags::ED_HOOKS))
- return;
-
- try {
- reg = globals["ED"];
- if (!reg)
- throw Error("Undefined ED-hook register (\"ED\")");
-
- /*
- * ED-hook execution should not see any
- * integer parameters but the hook type.
- * Such parameters could confuse the ED macro
- * and macro authors do not expect side effects
- * of ED macros on the expression stack.
- * Also make sure it does not leave behind
- * additional arguments on the stack.
- *
- * So this effectively executes:
- * (typeM[ED]^[)
- */
- expressions.brace_open();
- expressions.push(type);
- reg->execute();
- expressions.discard_args();
- expressions.brace_close();
- } catch (Error &error) {
- const gchar *type_str = type2name[type-1];
-
- error.add_frame(new Error::EDHookFrame(type_str));
- throw; /* forward */
- }
-}
-
-void
-QRegSpecMachine::reset(void)
-{
- MicroStateMachine<QRegister *>::reset();
- string_machine.reset();
- undo.push_var(is_local) = false;
- undo.push_var(nesting) = 0;
- undo.push_str(name);
- g_free(name);
- name = NULL;
-}
-
-bool
-QRegSpecMachine::input(gchar chr, QRegister *&result)
-{
- gchar *insert;
-
-MICROSTATE_START;
- switch (chr) {
- case '#':
- set(&&StateFirstChar);
- break;
- case '[':
- set(&&StateString);
- undo.push_var(nesting)++;
- break;
- case '.':
- if (!is_local) {
- undo.push_var(is_local) = true;
- break;
- }
- /* fall through */
- default:
- undo.push_str(name) = String::chrdup(String::toupper(chr));
- goto done;
- }
-
- return false;
-
-StateFirstChar:
- undo.push_str(name) = (gchar *)g_malloc(3);
- name[0] = String::toupper(chr);
- name[1] = '\0';
- set(&&StateSecondChar);
- return false;
-
-StateSecondChar:
- name[1] = String::toupper(chr);
- name[2] = '\0';
- goto done;
-
-StateString:
- switch (chr) {
- case '[':
- undo.push_var(nesting)++;
- break;
- case ']':
- undo.push_var(nesting)--;
- if (!nesting)
- goto done;
- break;
- }
-
- if (mode > MODE_NORMAL)
- return false;
-
- if (!string_machine.input(chr, insert))
- return false;
-
- undo.push_str(name);
- String::append(name, insert);
- g_free(insert);
- return false;
-
-done:
- if (mode > MODE_NORMAL) {
- /*
- * StateExpectQRegs with type != OPTIONAL
- * will never see this NULL pointer beyond
- * BEGIN_EXEC()
- */
- result = NULL;
- return true;
- }
-
- QRegisterTable &table = is_local ? *QRegisters::locals
- : QRegisters::globals;
-
- switch (type) {
- case QREG_REQUIRED:
- result = table[name];
- if (!result)
- fail();
- break;
-
- case QREG_OPTIONAL:
- result = table[name];
- break;
-
- case QREG_OPTIONAL_INIT:
- result = table[name];
- if (!result) {
- result = table.insert(name);
- table.undo_remove(result);
- }
- break;
- }
-
- return true;
-}
-
-gchar *
-QRegSpecMachine::auto_complete(void)
-{
- gsize restrict_len = 0;
-
- if (string_machine.qregspec_machine)
- /* nested Q-Reg definition */
- return string_machine.qregspec_machine->auto_complete();
-
- if (state == StateStart)
- /* single-letter Q-Reg */
- restrict_len = 1;
- else if (!nesting)
- /* two-letter Q-Reg */
- restrict_len = 2;
-
- QRegisterTable &table = is_local ? *QRegisters::locals
- : QRegisters::globals;
- return table.auto_complete(name, nesting == 1 ? ']' : '\0',
- restrict_len);
-}
-
-/*
- * Command states
- */
-
-StateExpectQReg::StateExpectQReg(QRegSpecType type) : machine(type)
-{
- transitions['\0'] = this;
-}
-
-State *
-StateExpectQReg::custom(gchar chr)
-{
- QRegister *reg;
-
- if (!machine.input(chr, reg))
- return this;
-
- return got_register(reg);
-}
-
-/*$ "[" "[q" push
- * [q -- Save Q-Register
- *
- * Save Q-Register <q> contents on the global Q-Register push-down
- * stack.
- */
-State *
-StatePushQReg::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::start);
- QRegisters::stack.push(*reg);
- return &States::start;
-}
-
-/*$ "]" "]q" pop
- * ]q -- Restore Q-Register
- *
- * 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.
- *
- * 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.
- */
-State *
-StatePopQReg::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::start);
-
- if (!QRegisters::stack.pop(*reg))
- throw Error("Q-Register stack is empty");
-
- return &States::start;
-}
-
-/*$ EQ EQq
- * EQq$ -- Edit or load Q-Register
- * EQq[file]$
- *
- * When specified with an empty <file> string argument,
- * EQ makes <q> the currently edited Q-Register.
- * Otherwise, when <file> is specified, it is the
- * name of a file to read into Q-Register <q>.
- * When loading a file, the currently edited
- * buffer/register is not changed and the edit position
- * of register <q> is reset to 0.
- *
- * Undefined Q-Registers will be defined.
- * The command fails if <file> could not be read.
- */
-State *
-StateEQCommand::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::loadqreg);
- undo.push_var(register_argument) = reg;
- return &States::loadqreg;
-}
-
-State *
-StateLoadQReg::got_file(const gchar *filename)
-{
- BEGIN_EXEC(&States::start);
-
- if (*filename) {
- /* Load file into Q-Register */
- register_argument->load(filename);
- } else {
- /* Edit Q-Register */
- current_doc_undo_edit();
- QRegisters::globals.edit(register_argument);
- }
-
- return &States::start;
-}
-
-/*$ E% E%q
- * E%q<file>$ -- Save Q-Register string to file
- *
- * Saves the string contents of Q-Register <q> to
- * <file>.
- * The <file> must always be specified, as Q-Registers
- * have no notion of associated file names.
- *
- * In interactive mode, the E% command may be rubbed out,
- * restoring the previous state of <file>.
- * This follows the same rules as with the \fBEW\fP command.
- *
- * File names may also be tab-completed and string building
- * characters are enabled by default.
- */
-State *
-StateEPctCommand::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::saveqreg);
- undo.push_var(register_argument) = reg;
- return &States::saveqreg;
-}
-
-State *
-StateSaveQReg::got_file(const gchar *filename)
-{
- BEGIN_EXEC(&States::start);
- register_argument->save(filename);
- return &States::start;
-}
-
-/*$ Q Qq query
- * Qq -> n -- Query Q-Register existence, its integer or string characters
- * <position>Qq -> character
- * :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>
- * 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.
- * Both non-colon-modified forms of Q require register <q>
- * to be defined and fail otherwise.
- *
- * When colon-modified, Q does not pop any arguments from
- * the expression stack and returns the <size> of the string
- * in Q-Register <q> if register <q> exists (i.e. is defined).
- * Naturally, for empty strings, 0 is returned.
- * When colon-modified and Q-Register <q> is undefined,
- * -1 is returned instead.
- * Therefore checking the return value \fB:Q\fP for values smaller
- * 0 allows checking the existence of a register.
- * Note that if <q> exists, its string part is not initialized,
- * so \fB:Q\fP may be used to handle purely numeric data structures
- * without creating Scintilla documents by accident.
- * These semantics allow the useful idiom \(lq:Q\fIq\fP">\(rq for
- * checking whether a Q-Register exists and has a non-empty string.
- * Note also that the return value of \fB:Q\fP may be interpreted
- * as a condition boolean that represents the non-existence of <q>.
- * If <q> is undefined, it returns \fIsuccess\fP, else a \fIfailure\fP
- * boolean.
- */
-State *
-StateQueryQReg::got_register(QRegister *reg)
-{
- /* like BEGIN_EXEC(&States::start), but resets machine */
- if (mode > MODE_NORMAL)
- goto reset;
-
- expressions.eval();
-
- if (eval_colon()) {
- /* Query Q-Register's existence or string size */
- expressions.push(reg ? reg->get_string_size()
- : (tecoInt)-1);
- goto reset;
- }
-
- /*
- * NOTE: This command is special since the QRegister is required
- * without colon and otherwise optional.
- * While it may be clearer to model this as two States,
- * we cannot currently let parsing depend on the colon-modifier.
- * That's why we have to declare the Q-Reg machine as QREG_OPTIONAL
- * and care about exception throwing on our own.
- */
- if (!reg)
- machine.fail();
-
- if (expressions.args() > 0) {
- /* Query character from Q-Register string */
- gint c = reg->get_character(expressions.pop_num_calc());
- if (c < 0)
- throw RangeError('Q');
- expressions.push(c);
- } else {
- /* Query integer */
- expressions.push(reg->get_integer());
- }
-
-reset:
- machine.reset();
- return &States::start;
-}
-
-/*$ ^Uq
- * [c1,c2,...]^Uq[string]$ -- Set or append to Q-Register string without string building
- * [c1,c2,...]:^Uq[string]$
- *
- * If not colon-modified, it first fills the Q-Register <q>
- * with all the values on the expression stack (interpreted as
- * codepoints).
- * It does so in the order of the arguments, i.e.
- * <c1> will be the first character in <q>, <c2> the second, etc.
- * Eventually the <string> argument is appended to the
- * register.
- * Any existing string value in <q> is overwritten by this operation.
- *
- * In the colon-modified form ^U does not overwrite existing
- * contents of <q> but only appends to it.
- *
- * If <q> is undefined, it will be defined.
- *
- * String-building characters are \fBdisabled\fP for ^U
- * commands.
- * Therefore they are especially well-suited for defining
- * \*(ST macros, since string building characters in the
- * desired Q-Register contents do not have to be escaped.
- * The \fBEU\fP command may be used where string building
- * is desired.
- */
-State *
-StateCtlUCommand::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::setqregstring_nobuilding);
- undo.push_var(register_argument) = reg;
- return &States::setqregstring_nobuilding;
-}
-
-/*$ EU EUq
- * [c1,c2,...]EUq[string]$ -- Set or append to Q-Register string with string building characters
- * [c1,c2,...]:EUq[string]$
- *
- * This command sets or appends to the contents of
- * Q-Register \fIq\fP.
- * It is identical to the \fB^U\fP command, except
- * that this form of the command has string building
- * characters \fBenabled\fP.
- */
-State *
-StateEUCommand::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::setqregstring_building);
- undo.push_var(register_argument) = reg;
- return &States::setqregstring_building;
-}
-
-void
-StateSetQRegString::initial(void)
-{
- int args;
-
- expressions.eval();
- args = expressions.args();
- text_added = args > 0;
- if (!args)
- return;
-
- gchar buffer[args+1];
-
- buffer[args] = '\0';
- while (args--)
- buffer[args] = (gchar)expressions.pop_num_calc();
-
- if (eval_colon()) {
- /* append to register */
- register_argument->undo_append_string();
- register_argument->append_string(buffer);
- } else {
- /* set register */
- register_argument->undo_set_string();
- register_argument->set_string(buffer);
- }
-}
-
-State *
-StateSetQRegString::done(const gchar *str)
-{
- BEGIN_EXEC(&States::start);
-
- if (text_added || eval_colon()) {
- /*
- * Append to register:
- * Note that append_string() does not create an UNDOACTION
- * if str == NULL
- */
- if (str) {
- register_argument->undo_append_string();
- register_argument->append_string(str);
- }
- } else {
- /* set register */
- register_argument->undo_set_string();
- register_argument->set_string(str);
- }
-
- return &States::start;
-}
-
-/*$ G Gq get
- * Gq -- Insert Q-Register string
- *
- * Inserts the string of Q-Register <q> into the buffer
- * at its current position.
- * Specifying an undefined <q> yields an error.
- */
-State *
-StateGetQRegString::got_register(QRegister *reg)
-{
- gchar *str;
-
- machine.reset();
-
- BEGIN_EXEC(&States::start);
-
- str = reg->get_string();
- if (*str) {
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_ADDTEXT, strlen(str), (sptr_t)str);
- interface.ssm(SCI_SCROLLCARET);
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
-
- interface.undo_ssm(SCI_UNDO);
- }
- g_free(str);
-
- return &States::start;
-}
-
-/*$ U Uq
- * nUq -- Set Q-Register integer
- * -Uq
- * [n]:Uq -> Success|Failure
- *
- * Sets the integer-part of Q-Register <q> to <n>.
- * \(lq-U\(rq is equivalent to \(lq-1U\(rq, otherwise
- * the command fails if <n> is missing.
- *
- * If the command is colon-modified, it returns a success
- * boolean if <n> or \(lq-\(rq is given.
- * Otherwise it returns a failure boolean and does not
- * modify <q>.
- *
- * The register is defined if it does not exist.
- */
-State *
-StateSetQRegInteger::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::start);
-
- expressions.eval();
- if (expressions.args() || expressions.num_sign < 0) {
- reg->undo_set_integer();
- reg->set_integer(expressions.pop_num_calc());
-
- if (eval_colon())
- expressions.push(SUCCESS);
- } else if (eval_colon()) {
- expressions.push(FAILURE);
- } else {
- throw ArgExpectedError('U');
- }
-
- return &States::start;
-}
-
-/*$ % %q increment
- * [n]%q -> q+n -- Increase Q-Register integer
- *
- * Add <n> to the integer part of register <q>, returning
- * its new value.
- * <q> will be defined if it does not exist.
- */
-State *
-StateIncreaseQReg::got_register(QRegister *reg)
-{
- tecoInt res;
-
- machine.reset();
-
- BEGIN_EXEC(&States::start);
-
- reg->undo_set_integer();
- res = reg->get_integer() + expressions.pop_num_calc();
- expressions.push(reg->set_integer(res));
-
- return &States::start;
-}
-
-/*$ M Mq eval
- * Mq -- Execute macro
- * :Mq
- *
- * Execute macro stored in string of Q-Register <q>.
- * The command itself does not push or pop and arguments from the stack
- * but the macro executed might well do so.
- * The new macro invocation level will contain its own go-to label table
- * and local Q-Register table.
- * Except when the command is colon-modified - in this case, local
- * Q-Registers referenced in the macro refer to the parent macro-level's
- * local Q-Register table (or whatever level defined one last).
- *
- * Errors during the macro execution will propagate to the M command.
- * In other words if a command in the macro fails, the M command will fail
- * and this failure propagates until the top-level macro (e.g.
- * the command-line macro).
- *
- * Note that the string of <q> will be copied upon macro execution,
- * so subsequent changes to Q-Register <q> from inside the macro do
- * not modify the executed code.
- */
-State *
-StateMacro::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::start);
- /* don't create new local Q-Registers if colon modifier is given */
- reg->execute(!eval_colon());
- return &States::start;
-}
-
-/*$ EM
- * EMfile$ -- Execute macro from file
- * :EMfile$
- *
- * Read the file with name <file> into memory and execute its contents
- * as a macro.
- * It is otherwise similar to the \(lqM\(rq command.
- *
- * If <file> could not be read, the command yields an error.
- */
-State *
-StateMacroFile::got_file(const gchar *filename)
-{
- BEGIN_EXEC(&States::start);
- /* don't create new local Q-Registers if colon modifier is given */
- Execute::file(filename, !eval_colon());
- return &States::start;
-}
-
-/*$ X Xq
- * [lines]Xq -- Copy into or append to Q-Register
- * -Xq
- * from,toXq
- * [lines]:Xq
- * -:Xq
- * from,to:Xq
- *
- * Copy the next or previous number of <lines> from the buffer
- * into the Q-Register <q> string.
- * 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 K
- * command's arguments.
- * If the command is colon-modified, the characters will be
- * appended to the end of register <q> instead.
- *
- * Register <q> will be created if it is undefined.
- */
-State *
-StateCopyToQReg::got_register(QRegister *reg)
-{
- tecoInt from, len;
- Sci_TextRange tr;
-
- machine.reset();
-
- BEGIN_EXEC(&States::start);
- expressions.eval();
-
- if (expressions.args() <= 1) {
- from = interface.ssm(SCI_GETCURRENTPOS);
- sptr_t line = interface.ssm(SCI_LINEFROMPOSITION, from) +
- expressions.pop_num_calc();
-
- if (!Validate::line(line))
- throw RangeError("X");
-
- len = interface.ssm(SCI_POSITIONFROMLINE, line) - from;
-
- if (len < 0) {
- from += len;
- len *= -1;
- }
- } else {
- tecoInt to = expressions.pop_num();
- from = expressions.pop_num();
-
- len = to - from;
-
- if (len < 0 || !Validate::pos(from) || !Validate::pos(to))
- throw RangeError("X");
- }
-
- tr.chrg.cpMin = from;
- tr.chrg.cpMax = from + len;
- tr.lpstrText = (char *)g_malloc(len + 1);
- interface.ssm(SCI_GETTEXTRANGE, 0, (sptr_t)&tr);
-
- if (eval_colon()) {
- reg->undo_append_string();
- reg->append_string(tr.lpstrText);
- } else {
- reg->undo_set_string();
- reg->set_string(tr.lpstrText);
- }
- g_free(tr.lpstrText);
-
- return &States::start;
-}
-
-} /* namespace SciTECO */
diff --git a/src/qregisters.h b/src/qregisters.h
deleted file mode 100644
index a08bafe..0000000
--- a/src/qregisters.h
+++ /dev/null
@@ -1,666 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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/>.
- */
-
-#ifndef __QREGISTERS_H
-#define __QREGISTERS_H
-
-#include <string.h>
-
-#include <bsd/sys/queue.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include <Scintilla.h>
-
-#include "sciteco.h"
-#include "memory.h"
-#include "error.h"
-#include "interface.h"
-#include "ioview.h"
-#include "undo.h"
-#include "rbtree.h"
-#include "parser.h"
-#include "document.h"
-
-namespace SciTECO {
-
-namespace QRegisters {
- /* initialized after Interface::main() in main() */
- extern IOView view;
-}
-
-/*
- * Classes
- */
-
-class QRegisterData : public Object {
-protected:
- tecoInt integer;
-
- class QRegisterString : public Document {
- public:
- ~QRegisterString()
- {
- release_document();
- }
-
- private:
- ViewCurrent &
- get_create_document_view(void)
- {
- return QRegisters::view;
- }
- } string;
-
-public:
- /*
- * Whether to generate UndoTokens (unnecessary in macro invocations).
- *
- * FIXME: Every QRegister has this field, but it only differs
- * between local and global QRegisters. This wastes space.
- * There must be a more clever way to inherit this property, e.g.
- * by setting QRegisters::current_must_undo.
- */
- bool must_undo;
-
- QRegisterData() : integer(0), must_undo(true) {}
- virtual ~QRegisterData() {}
-
- virtual tecoInt
- set_integer(tecoInt i)
- {
- return integer = i;
- }
- virtual void
- undo_set_integer(void)
- {
- if (must_undo)
- undo.push_var(integer);
- }
- virtual tecoInt
- get_integer(void)
- {
- return integer;
- }
-
- virtual void set_string(const gchar *str, gsize len);
- inline void
- set_string(const gchar *str)
- {
- set_string(str, str ? strlen(str) : 0);
- }
- virtual void undo_set_string(void);
-
- virtual void append_string(const gchar *str, gsize len);
- inline void
- append_string(const gchar *str)
- {
- append_string(str, str ? strlen(str) : 0);
- }
- virtual inline void
- undo_append_string(void)
- {
- undo_set_string();
- }
- virtual gchar *get_string(void);
- virtual gsize get_string_size(void);
- virtual gint get_character(gint position);
-
- virtual void
- exchange_string(QRegisterData &reg)
- {
- string.exchange(reg.string);
- }
- virtual void undo_exchange_string(QRegisterData &reg);
-
- /*
- * The QRegisterStack must currently still access the
- * string fields directly to exchange data efficiently.
- */
- friend class QRegisterStack;
-};
-
-class QRegister : public RBTreeString::RBEntryOwnString, public QRegisterData {
-protected:
- /**
- * The default constructor for subclasses.
- * This leaves the name uninitialized.
- */
- QRegister() {}
-
-public:
- QRegister(const gchar *name)
- : RBTreeString::RBEntryOwnString(name) {}
-
- virtual ~QRegister() {}
-
- virtual void edit(void);
- virtual void undo_edit(void);
-
- void execute(bool locals = true);
-
- void undo_set_eol_mode(void);
- void set_eol_mode(gint mode);
-
- /*
- * Load and save already care about undo token
- * creation.
- */
- void load(const gchar *filename);
- void save(const gchar *filename);
-};
-
-class QRegisterBufferInfo : public QRegister {
-public:
- QRegisterBufferInfo() : QRegister("*") {}
-
- /* setting "*" is equivalent to nEB */
- tecoInt set_integer(tecoInt v);
- void undo_set_integer(void);
-
- tecoInt get_integer(void);
-
- void
- set_string(const gchar *str, gsize len)
- {
- throw QRegOpUnsupportedError(name);
- }
- void undo_set_string(void) {}
-
- void
- append_string(const gchar *str, gsize len)
- {
- throw QRegOpUnsupportedError(name);
- }
- void undo_append_string(void) {}
-
- gchar *get_string(void);
- gsize get_string_size(void);
- gint get_character(gint pos);
-
- void edit(void);
-};
-
-class QRegisterWorkingDir : public QRegister {
-public:
- QRegisterWorkingDir() : QRegister("$") {}
-
- void set_string(const gchar *str, gsize len);
- void undo_set_string(void);
-
- void
- append_string(const gchar *str, gsize len)
- {
- throw QRegOpUnsupportedError(name);
- }
- void undo_append_string(void) {}
-
- gchar *get_string(void);
- gsize get_string_size(void);
- gint get_character(gint pos);
-
- void edit(void);
-
- void exchange_string(QRegisterData &reg);
- void undo_exchange_string(QRegisterData &reg);
-};
-
-class QRegisterClipboard : public QRegister {
- class UndoTokenSetClipboard : public UndoToken {
- gchar *name;
- gchar *str;
- gsize str_len;
-
- public:
- /**
- * Construct undo token.
- *
- * This passes ownership of the clipboard content string
- * to the undo token object.
- */
- UndoTokenSetClipboard(const gchar *_name, gchar *_str, gsize _str_len)
- : name(g_strdup(_name)), str(_str), str_len(_str_len) {}
- ~UndoTokenSetClipboard()
- {
- g_free(str);
- g_free(name);
- }
-
- void run(void);
- };
-
- /**
- * Gets the clipboard name.
- * Can be easily derived from the Q-Register name.
- */
- inline const gchar *
- get_clipboard_name(void) const
- {
- return name+1;
- }
-
-public:
- QRegisterClipboard(const gchar *_name = NULL)
- {
- name = g_strconcat("~", _name, NIL);
- }
-
- void set_string(const gchar *str, gsize len);
- void undo_set_string(void);
-
- /*
- * FIXME: We could support that.
- */
- void
- append_string(const gchar *str, gsize len)
- {
- throw QRegOpUnsupportedError(name);
- }
- void undo_append_string(void) {}
-
- gchar *get_string(gsize *out_len);
- gchar *get_string(void);
- gsize get_string_size(void);
- gint get_character(gint pos);
-
- void edit(void);
-
- void exchange_string(QRegisterData &reg);
- void undo_exchange_string(QRegisterData &reg);
-};
-
-class QRegisterTable : private RBTreeString, public Object {
- class UndoTokenRemoveGlobal : public UndoToken {
- protected:
- QRegister *reg;
-
- public:
- UndoTokenRemoveGlobal(QRegister *_reg)
- : reg(_reg) {}
-
- void run(void);
- };
-
- class UndoTokenRemoveLocal : public UndoTokenRemoveGlobal {
- public:
- UndoTokenRemoveLocal(QRegister *reg)
- : UndoTokenRemoveGlobal(reg) {}
-
- void run(void);
- };
-
- bool must_undo;
-
-public:
- QRegisterTable(bool _must_undo = true)
- : must_undo(_must_undo) {}
-
- ~QRegisterTable()
- {
- QRegister *cur;
-
- while ((cur = (QRegister *)root()))
- delete (QRegister *)remove(cur);
- }
-
- void undo_remove(QRegister *reg);
-
- inline QRegister *
- insert(QRegister *reg)
- {
- reg->must_undo = must_undo;
- RBTreeString::insert(reg);
- return reg;
- }
- inline QRegister *
- insert(const gchar *name)
- {
- return insert(new QRegister(name));
- }
- inline QRegister *
- insert(gchar name)
- {
- gchar buf[] = {name, '\0'};
- return insert(buf);
- }
-
- void insert_defaults(void);
-
- inline QRegister *
- find(const gchar *name)
- {
- return (QRegister *)RBTreeString::find(name);
- }
- inline QRegister *
- operator [](const gchar *name)
- {
- return find(name);
- }
- inline QRegister *
- operator [](gchar chr)
- {
- gchar buf[] = {chr, '\0'};
- return find(buf);
- }
-
- inline QRegister *
- nfind(const gchar *name)
- {
- return (QRegister *)RBTreeString::nfind(name);
- }
-
- void edit(QRegister *reg);
- inline QRegister *
- edit(const gchar *name)
- {
- QRegister *reg = find(name);
-
- if (!reg)
- return NULL;
- edit(reg);
- return reg;
- }
-
- void set_environ(void);
- gchar **get_environ(void);
- void update_environ(void);
-
- void clear(void);
-
- inline gchar *
- auto_complete(const gchar *name, gchar completed = '\0', gsize max_len = 0)
- {
- return RBTreeString::auto_complete(name, completed, max_len);
- }
-};
-
-class QRegisterStack : public Object {
- class Entry : public QRegisterData {
- public:
- SLIST_ENTRY(Entry) entries;
-
- Entry() : QRegisterData() {}
- };
-
- class UndoTokenPush : public UndoToken {
- QRegisterStack *stack;
- /* only remaining reference to stack entry */
- Entry *entry;
-
- public:
- UndoTokenPush(QRegisterStack *_stack, Entry *_entry)
- : UndoToken(), stack(_stack), entry(_entry) {}
-
- ~UndoTokenPush()
- {
- delete entry;
- }
-
- void run(void);
- };
-
- class UndoTokenPop : public UndoToken {
- QRegisterStack *stack;
-
- public:
- UndoTokenPop(QRegisterStack *_stack)
- : stack(_stack) {}
-
- void run(void);
- };
-
- SLIST_HEAD(Head, Entry) head;
-
-public:
- QRegisterStack()
- {
- SLIST_INIT(&head);
- }
- ~QRegisterStack();
-
- void push(QRegister &reg);
- bool pop(QRegister &reg);
-};
-
-enum QRegSpecType {
- /** Register must exist, else fail */
- QREG_REQUIRED,
- /**
- * Return NULL if register does not exist.
- * You can still call QRegSpecMachine::fail() to require it.
- */
- QREG_OPTIONAL,
- /** Initialize register if it does not already exist */
- QREG_OPTIONAL_INIT
-};
-
-class QRegSpecMachine : public MicroStateMachine<QRegister *> {
- StringBuildingMachine string_machine;
- QRegSpecType type;
-
- bool is_local;
- gint nesting;
- gchar *name;
-
-public:
- QRegSpecMachine(QRegSpecType _type = QREG_REQUIRED)
- : MicroStateMachine<QRegister *>(),
- type(_type),
- is_local(false), nesting(0), name(NULL) {}
-
- ~QRegSpecMachine()
- {
- g_free(name);
- }
-
- void reset(void);
-
- bool input(gchar chr, QRegister *&result);
-
- inline void
- fail(void) G_GNUC_NORETURN
- {
- throw InvalidQRegError(name, is_local);
- }
-
- gchar *auto_complete(void);
-};
-
-/*
- * Command states
- */
-
-/**
- * Super class for states accepting Q-Register specifications
- */
-class StateExpectQReg : public State {
-protected:
- QRegSpecMachine machine;
-
-public:
- StateExpectQReg(QRegSpecType type = QREG_REQUIRED);
-
-private:
- State *custom(gchar chr);
-
-protected:
- /**
- * Called when a register specification has been
- * successfully parsed.
- * The QRegSpecMachine is not reset automatically.
- *
- * @param reg Register of the parsed Q-Reg
- * specification. May be NULL in
- * parse-only mode or with QREG_OPTIONAL.
- * @returns Next parser state.
- */
- virtual State *got_register(QRegister *reg) = 0;
-
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-class StatePushQReg : public StateExpectQReg {
-private:
- State *got_register(QRegister *reg);
-};
-
-class StatePopQReg : public StateExpectQReg {
-public:
- StatePopQReg() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateEQCommand : public StateExpectQReg {
-public:
- StateEQCommand() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateLoadQReg : public StateExpectFile {
-private:
- State *got_file(const gchar *filename);
-};
-
-class StateEPctCommand : public StateExpectQReg {
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateSaveQReg : public StateExpectFile {
-private:
- State *got_file(const gchar *filename);
-};
-
-class StateQueryQReg : public StateExpectQReg {
-public:
- StateQueryQReg() : StateExpectQReg(QREG_OPTIONAL) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateCtlUCommand : public StateExpectQReg {
-public:
- StateCtlUCommand() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateEUCommand : public StateExpectQReg {
-public:
- StateEUCommand() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateSetQRegString : public StateExpectString {
- bool text_added;
-
-public:
- StateSetQRegString(bool building)
- : StateExpectString(building) {}
-
-private:
- void initial(void);
- State *done(const gchar *str);
-};
-
-class StateGetQRegString : public StateExpectQReg {
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateSetQRegInteger : public StateExpectQReg {
-public:
- StateSetQRegInteger() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateIncreaseQReg : public StateExpectQReg {
-public:
- StateIncreaseQReg() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateMacro : public StateExpectQReg {
-private:
- State *got_register(QRegister *reg);
-};
-
-class StateMacroFile : public StateExpectFile {
-private:
- State *got_file(const gchar *filename);
-};
-
-class StateCopyToQReg : public StateExpectQReg {
-public:
- StateCopyToQReg() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-namespace States {
- extern StatePushQReg pushqreg;
- extern StatePopQReg popqreg;
- extern StateEQCommand eqcommand;
- extern StateLoadQReg loadqreg;
- extern StateEPctCommand epctcommand;
- extern StateSaveQReg saveqreg;
- extern StateQueryQReg queryqreg;
- extern StateCtlUCommand ctlucommand;
- extern StateEUCommand eucommand;
- extern StateSetQRegString setqregstring_nobuilding;
- extern StateSetQRegString setqregstring_building;
- extern StateGetQRegString getqregstring;
- extern StateSetQRegInteger setqreginteger;
- extern StateIncreaseQReg increaseqreg;
- extern StateMacro macro;
- extern StateMacroFile macro_file;
- extern StateCopyToQReg copytoqreg;
-}
-
-namespace QRegisters {
- /* object declared in main.cpp */
- extern QRegisterTable globals;
- extern QRegisterTable *locals;
- extern QRegister *current;
-
- enum Hook {
- HOOK_ADD = 1,
- HOOK_EDIT,
- HOOK_CLOSE,
- HOOK_QUIT
- };
- void hook(Hook type);
-}
-
-} /* namespace SciTECO */
-
-#endif
diff --git a/src/rb3str.c b/src/rb3str.c
new file mode 100644
index 0000000..37dff79
--- /dev/null
+++ b/src/rb3str.c
@@ -0,0 +1,150 @@
+/*
+ * 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>
+
+/*
+ * NOTE: Must be included only once.
+ */
+//#include <rb3ptr.h>
+
+#include "sciteco.h"
+#include "interface.h"
+#include "string-utils.h"
+#include "rb3str.h"
+
+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);
+}
+
+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);
+}
+
+/** @memberof teco_rb3str_tree_t */
+teco_rb3str_head_t *
+teco_rb3str_insert(teco_rb3str_tree_t *tree, gboolean case_sensitive, teco_rb3str_head_t *head)
+{
+ rb3_cmp *cmp = case_sensitive ? (rb3_cmp *)teco_rb3str_cmp : (rb3_cmp *)teco_rb3str_casecmp;
+ return (teco_rb3str_head_t *)rb3_insert(tree, &head->head, cmp, &head->key);
+}
+
+/** @memberof teco_rb3str_tree_t */
+teco_rb3str_head_t *
+teco_rb3str_find(teco_rb3str_tree_t *tree, gboolean case_sensitive, const gchar *str, gsize len)
+{
+ rb3_cmp *cmp = case_sensitive ? (rb3_cmp *)teco_rb3str_cmp : (rb3_cmp *)teco_rb3str_casecmp;
+ teco_string_t data = {(gchar *)str, len};
+ return (teco_rb3str_head_t *)rb3_find(tree, cmp, &data);
+}
+
+/** @memberof teco_rb3str_tree_t */
+teco_rb3str_head_t *
+teco_rb3str_nfind(teco_rb3str_tree_t *tree, gboolean case_sensitive, const gchar *str, gsize len)
+{
+ rb3_cmp *cmp = case_sensitive ? (rb3_cmp *)teco_rb3str_cmp : (rb3_cmp *)teco_rb3str_casecmp;
+ teco_string_t data = {(gchar *)str, len};
+
+ /*
+ * This is based on rb3_INLINE_find() in rb3ptr.h.
+ * Alternatively, we might adapt/wrap teco_rb3str_cmp() in order to store
+ * the last element > data.
+ */
+ struct rb3_head *parent = rb3_get_base(tree);
+ struct rb3_head *res = NULL;
+ int dir = RB3_LEFT;
+ while (rb3_has_child(parent, dir)) {
+ parent = rb3_get_child(parent, dir);
+ int r = cmp(parent, &data);
+ if (r == 0)
+ return (teco_rb3str_head_t *)parent;
+ dir = (r < 0) ? RB3_RIGHT : RB3_LEFT;
+ if (dir == RB3_LEFT)
+ res = parent;
+ }
+
+ return (teco_rb3str_head_t *)res;
+}
+
+/**
+ * Auto-complete string given the entries of a RB tree.
+ *
+ * @param tree The RB tree (root).
+ * @param case_sensitive Whether to match case-sensitive.
+ * @param str String to complete (not necessarily null-terminated).
+ * @param str_len Length of characters in `str`.
+ * @param restrict_len Limit completions to this size.
+ * @param insert String to set with characters that can be autocompleted.
+ * @return TRUE if the completion was unambiguous, else FALSE.
+ *
+ * @memberof teco_rb3str_tree_t
+ */
+gboolean
+teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive,
+ const gchar *str, gsize str_len, gsize restrict_len, teco_string_t *insert)
+{
+ memset(insert, 0, sizeof(*insert));
+
+ teco_string_diff_t diff = case_sensitive ? teco_string_diff : teco_string_casediff;
+ teco_rb3str_head_t *first = NULL;
+ gsize prefix_len = 0;
+ 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 = teco_rb3str_get_next(cur)) {
+ if (restrict_len && cur->key.len != restrict_len)
+ continue;
+
+ if (G_UNLIKELY(!first)) {
+ first = cur;
+ prefix_len = cur->key.len - str_len;
+ } else {
+ gsize len = diff(&cur->key, first->key.data, first->key.len) - str_len;
+ if (len < prefix_len)
+ prefix_len = len;
+ }
+
+ prefixed_entries++;
+ }
+
+ if (prefix_len > 0) {
+ 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 = teco_rb3str_get_next(cur)) {
+ if (restrict_len && cur->key.len != restrict_len)
+ continue;
+
+ teco_interface_popup_add(TECO_POPUP_PLAIN,
+ cur->key.data, cur->key.len, FALSE);
+ }
+
+ teco_interface_popup_show();
+ }
+
+ return prefixed_entries == 1;
+}
diff --git a/src/rb3str.h b/src/rb3str.h
new file mode 100644
index 0000000..a34362e
--- /dev/null
+++ b/src/rb3str.h
@@ -0,0 +1,69 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include <rb3ptr.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+
+/**
+ * A RB-tree with teco_string_t keys.
+ *
+ * NOTE: If the tree's keys do not change and you will never have to free
+ * an individual node, consider storing the keys in GStringChunk or
+ * allocating them via teco_string_init_chunk(), as this is faster
+ * and more memory efficient.
+ *
+ * FIXME: Perhaps we should simply directly import and tweak rb3tree.c
+ * instead of trying to wrap it.
+ */
+typedef struct rb3_tree teco_rb3str_tree_t;
+
+/** @extends rb3_head */
+typedef struct {
+ struct rb3_head head;
+ /**
+ * The union exists only to allow a "name" alias for "key".
+ */
+ union {
+ teco_string_t name;
+ teco_string_t key;
+ };
+} teco_rb3str_head_t;
+
+/** @memberof teco_rb3str_head_t */
+static inline teco_rb3str_head_t *
+teco_rb3str_get_next(teco_rb3str_head_t *head)
+{
+ return (teco_rb3str_head_t *)rb3_get_next(&head->head);
+}
+
+teco_rb3str_head_t *teco_rb3str_insert(teco_rb3str_tree_t *tree, gboolean case_sensitive,
+ teco_rb3str_head_t *head);
+
+teco_rb3str_head_t *teco_rb3str_find(teco_rb3str_tree_t *tree, gboolean case_sensitive,
+ const gchar *str, gsize len);
+
+teco_rb3str_head_t *teco_rb3str_nfind(teco_rb3str_tree_t *tree, gboolean case_sensitive,
+ const gchar *str, gsize len);
+
+gboolean teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive,
+ const gchar *str, gsize str_len, gsize restrict_len,
+ teco_string_t *insert);
diff --git a/src/rbtree.cpp b/src/rbtree.cpp
deleted file mode 100644
index d2e9450..0000000
--- a/src/rbtree.cpp
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <string.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include "sciteco.h"
-#include "rbtree.h"
-#include "interface.h"
-#include "string-utils.h"
-
-namespace SciTECO {
-
-template <StringCmpFunc StringCmp, StringNCmpFunc StringNCmp>
-gchar *
-RBTreeStringT<StringCmp, StringNCmp>::
-auto_complete(const gchar *key, gchar completed, gsize restrict_len)
-{
- gsize key_len;
- RBEntryString *first = NULL;
- gsize prefix_len = 0;
- gint prefixed_entries = 0;
- gchar *insert = NULL;
-
- if (!key)
- key = "";
- key_len = strlen(key);
-
- for (RBEntryString *cur = nfind(key);
- cur && !StringNCmp(cur->key, key, key_len);
- cur = cur->next()) {
- if (restrict_len && strlen(cur->key) != restrict_len)
- continue;
-
- if (!first)
- first = cur;
-
- gsize len = String::diff(first->key + key_len,
- cur->key + key_len);
- if (!prefix_len || len < prefix_len)
- prefix_len = len;
-
- prefixed_entries++;
- }
- if (prefix_len > 0)
- insert = g_strndup(first->key + key_len, prefix_len);
-
- if (!insert && prefixed_entries > 1) {
- for (RBEntryString *cur = first;
- cur && !StringNCmp(cur->key, key, key_len);
- cur = cur->next()) {
- if (restrict_len && strlen(cur->key) != restrict_len)
- continue;
-
- interface.popup_add(InterfaceCurrent::POPUP_PLAIN,
- cur->key);
- }
-
- interface.popup_show();
- } else if (prefixed_entries == 1) {
- String::append(insert, completed);
- }
-
- return insert;
-}
-
-template class RBTreeStringT<strcmp, strncmp>;
-template class RBTreeStringT<g_ascii_strcasecmp, g_ascii_strncasecmp>;
-
-} /* namespace SciTECO */
diff --git a/src/rbtree.h b/src/rbtree.h
deleted file mode 100644
index edb3c56..0000000
--- a/src/rbtree.h
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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/>.
- */
-
-#ifndef __RBTREE_H
-#define __RBTREE_H
-
-#include <string.h>
-
-#include <bsd/sys/tree.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include "memory.h"
-
-namespace SciTECO {
-
-/*
- * NOTE: RBTree is not derived from Object,
- * so we can derive from RBTree privately.
- * Add Object to every RBTree subclass explicitly.
- */
-template <class RBEntryType>
-class RBTree {
-public:
- class RBEntry : public Object {
- public:
- RB_ENTRY(RBEntry) nodes;
-
- inline RBEntryType *
- next(void)
- {
- return (RBEntryType *)RBTree::Tree_RB_NEXT(this);
- }
- inline RBEntryType *
- prev(void)
- {
- return (RBEntryType *)RBTree::Tree_RB_PREV(this);
- }
- };
-
-private:
- RB_HEAD(Tree, RBEntry) head;
-
- static inline int
- compare_entries(RBEntry *e1, RBEntry *e2)
- {
- return ((RBEntryType *)e1)->compare(*(RBEntryType *)e2);
- }
-
- /*
- * All generated functions are plain-C, so they can be
- * static methods.
- */
- RB_GENERATE_INTERNAL(Tree, RBEntry, nodes, compare_entries, static);
-
-public:
- RBTree()
- {
- RB_INIT(&head);
- }
- ~RBTree()
- {
- /*
- * Keeping the clean up out of this wrapper class
- * means we can avoid declaring EBEntry implementations
- * virtual.
- */
- g_assert(root() == NULL);
- }
-
- inline RBEntryType *
- root(void)
- {
- return (RBEntryType *)RB_ROOT(&head);
- }
- inline RBEntryType *
- insert(RBEntryType *entry)
- {
- RB_INSERT(Tree, &head, entry);
- return entry;
- }
- inline RBEntryType *
- remove(RBEntryType *entry)
- {
- return (RBEntryType *)RB_REMOVE(Tree, &head, entry);
- }
- inline RBEntryType *
- find(RBEntryType *entry)
- {
- return (RBEntryType *)RB_FIND(Tree, &head, entry);
- }
- inline RBEntryType *
- operator [](RBEntryType *entry)
- {
- return find(entry);
- }
- inline RBEntryType *
- nfind(RBEntryType *entry)
- {
- return (RBEntryType *)RB_NFIND(Tree, &head, entry);
- }
- inline RBEntryType *
- min(void)
- {
- return (RBEntryType *)RB_MIN(Tree, &head);
- }
- inline RBEntryType *
- max(void)
- {
- return (RBEntryType *)RB_MAX(Tree, &head);
- }
-};
-
-typedef gint (*StringCmpFunc)(const gchar *str1, const gchar *str2);
-typedef gint (*StringNCmpFunc)(const gchar *str1, const gchar *str2, gsize n);
-
-template <StringCmpFunc StringCmp>
-class RBEntryStringT : public RBTree<RBEntryStringT<StringCmp>>::RBEntry {
-public:
- /*
- * It is convenient to be able to access the string
- * key with various attribute names.
- */
- union {
- gchar *key;
- gchar *name;
- };
-
- RBEntryStringT(gchar *_key) : key(_key) {}
-
- inline gint
- compare(RBEntryStringT &other)
- {
- return StringCmp(key, other.key);
- }
-};
-
-template <StringCmpFunc StringCmp, StringNCmpFunc StringNCmp>
-class RBTreeStringT : public RBTree<RBEntryStringT<StringCmp>> {
-public:
- typedef RBEntryStringT<StringCmp> RBEntryString;
-
- class RBEntryOwnString : public RBEntryString {
- public:
- RBEntryOwnString(const gchar *key = NULL)
- : RBEntryString(key ? g_strdup(key) : NULL) {}
-
- ~RBEntryOwnString()
- {
- g_free(RBEntryString::key);
- }
- };
-
- inline RBEntryString *
- find(const gchar *str)
- {
- RBEntryString entry((gchar *)str);
- return RBTree<RBEntryString>::find(&entry);
- }
- inline RBEntryString *
- operator [](const gchar *name)
- {
- return find(name);
- }
-
- inline RBEntryString *
- nfind(const gchar *str)
- {
- RBEntryString entry((gchar *)str);
- return RBTree<RBEntryString>::nfind(&entry);
- }
-
- gchar *auto_complete(const gchar *key, gchar completed = '\0',
- gsize restrict_len = 0);
-};
-
-typedef RBTreeStringT<strcmp, strncmp> RBTreeString;
-typedef RBTreeStringT<g_ascii_strcasecmp, g_ascii_strncasecmp> RBTreeStringCase;
-
-/*
- * Only these two instantiations of RBTreeStringT are ever used,
- * so it is more efficient to explicitly instantiate them.
- * NOTE: The insane rules of C++ prevent using the typedefs here...
- */
-extern template class RBTreeStringT<strcmp, strncmp>;
-extern template class RBTreeStringT<g_ascii_strcasecmp, g_ascii_strncasecmp>;
-
-} /* namespace SciTECO */
-
-#endif
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);
diff --git a/src/ring.cpp b/src/ring.cpp
deleted file mode 100644
index ac5faf6..0000000
--- a/src/ring.cpp
+++ /dev/null
@@ -1,461 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <bsd/sys/queue.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include <Scintilla.h>
-
-#include "sciteco.h"
-#include "interface.h"
-#include "ioview.h"
-#include "undo.h"
-#include "parser.h"
-#include "expressions.h"
-#include "qregisters.h"
-#include "glob.h"
-#include "error.h"
-#include "ring.h"
-
-namespace SciTECO {
-
-namespace States {
- StateEditFile editfile;
- StateSaveFile savefile;
-}
-
-void
-Buffer::UndoTokenClose::run(void)
-{
- ring.close(buffer);
- /* NOTE: the buffer is NOT deleted on Token destruction */
- delete buffer;
-}
-
-void
-Buffer::save(const gchar *filename)
-{
- if (!filename && !Buffer::filename)
- throw Error("Cannot save the unnamed file "
- "without providing a file name");
-
- IOView::save(filename ? : Buffer::filename);
-
- /*
- * Undirtify
- * NOTE: info update is performed by set_filename()
- */
- interface.undo_info_update(this);
- undo.push_var(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
- */
- undo.push_str(Buffer::filename);
- set_filename(filename ? : Buffer::filename);
-}
-
-void
-Ring::UndoTokenEdit::run(void)
-{
- /*
- * assumes that buffer still has correct prev/next
- * pointers
- */
- if (buffer->next())
- TAILQ_INSERT_BEFORE(buffer->next(), buffer, buffers);
- else
- TAILQ_INSERT_TAIL(&ring->head, buffer, buffers);
-
- ring->current = buffer;
- buffer->edit();
- buffer = NULL;
-}
-
-tecoInt
-Ring::get_id(Buffer *buffer)
-{
- tecoInt ret = 0;
- Buffer *cur;
-
- TAILQ_FOREACH(cur, &head, buffers) {
- ret++;
- if (cur == buffer)
- break;
- }
-
- return ret;
-}
-
-Buffer *
-Ring::find(const gchar *filename)
-{
- gchar *resolved = get_absolute_path(filename);
- Buffer *cur;
-
- TAILQ_FOREACH(cur, &head, buffers)
- if (!g_strcmp0(cur->filename, resolved))
- break;
-
- g_free(resolved);
- return cur;
-}
-
-Buffer *
-Ring::find(tecoInt id)
-{
- Buffer *cur;
-
- TAILQ_FOREACH(cur, &head, buffers)
- if (!--id)
- break;
-
- return cur;
-}
-
-void
-Ring::dirtify(void)
-{
- if (QRegisters::current || current->dirty)
- return;
-
- interface.undo_info_update(current);
- undo.push_var(current->dirty);
- current->dirty = true;
- interface.info_update(current);
-}
-
-bool
-Ring::is_any_dirty(void)
-{
- Buffer *cur;
-
- TAILQ_FOREACH(cur, &head, buffers)
- if (cur->dirty)
- return true;
-
- return false;
-}
-
-void
-Ring::save_all_dirty_buffers(void)
-{
- Buffer *cur;
-
- TAILQ_FOREACH(cur, &head, buffers)
- if (cur->dirty)
- /* NOTE: Will fail for the unnamed file */
- cur->save();
-}
-
-bool
-Ring::edit(tecoInt id)
-{
- Buffer *buffer = find(id);
-
- if (!buffer)
- return false;
-
- QRegisters::current = NULL;
- current = buffer;
- buffer->edit();
-
- QRegisters::hook(QRegisters::HOOK_EDIT);
-
- return true;
-}
-
-void
-Ring::edit(const gchar *filename)
-{
- Buffer *buffer = find(filename);
-
- QRegisters::current = NULL;
- if (buffer) {
- current = buffer;
- buffer->edit();
-
- QRegisters::hook(QRegisters::HOOK_EDIT);
- } else {
- buffer = new Buffer();
- TAILQ_INSERT_TAIL(&head, buffer, buffers);
-
- current = buffer;
- undo_close();
-
- if (filename && g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
- buffer->edit();
- buffer->load(filename);
-
- interface.msg(InterfaceCurrent::MSG_INFO,
- "Added file \"%s\" to ring", filename);
- } else {
- buffer->edit();
- buffer->set_filename(filename);
-
- if (filename)
- interface.msg(InterfaceCurrent::MSG_INFO,
- "Added new file \"%s\" to ring",
- filename);
- else
- interface.msg(InterfaceCurrent::MSG_INFO,
- "Added new unnamed file to ring.");
- }
-
- QRegisters::hook(QRegisters::HOOK_ADD);
- }
-}
-
-void
-Ring::close(Buffer *buffer)
-{
- TAILQ_REMOVE(&head, buffer, buffers);
-
- if (buffer->filename)
- interface.msg(InterfaceCurrent::MSG_INFO,
- "Removed file \"%s\" from the ring",
- buffer->filename);
- else
- interface.msg(InterfaceCurrent::MSG_INFO,
- "Removed unnamed file from the ring.");
-}
-
-void
-Ring::close(void)
-{
- Buffer *buffer = current;
-
- QRegisters::hook(QRegisters::HOOK_CLOSE);
- close(buffer);
- current = buffer->next() ? : buffer->prev();
- /* Transfer responsibility to UndoToken object. */
- undo.push_own<UndoTokenEdit>(this, buffer);
-
- if (current) {
- current->edit();
- QRegisters::hook(QRegisters::HOOK_EDIT);
- } else {
- edit((const gchar *)NULL);
- }
-}
-
-void
-Ring::set_scintilla_undo(bool state)
-{
- Buffer *cur;
-
- TAILQ_FOREACH(cur, &head, buffers)
- cur->set_scintilla_undo(state);
-}
-
-Ring::~Ring()
-{
- Buffer *buffer, *next;
-
- TAILQ_FOREACH_SAFE(buffer, &head, buffers, next)
- delete buffer;
-}
-
-/*
- * Command states
- */
-
-void
-StateEditFile::do_edit(const gchar *filename)
-{
- current_doc_undo_edit();
- ring.edit(filename);
-}
-
-void
-StateEditFile::do_edit(tecoInt id)
-{
- current_doc_undo_edit();
- if (!ring.edit(id))
- throw Error("Invalid buffer id %" TECO_INTEGER_FORMAT, id);
-}
-
-/*$ 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.
- */
-void
-StateEditFile::initial(void)
-{
- tecoInt id = expressions.pop_num_calc(0, -1);
-
- allowFilename = true;
-
- if (id == 0) {
- for (Buffer *cur = ring.first(); cur; cur = cur->next())
- interface.popup_add(InterfaceCurrent::POPUP_FILE,
- cur->filename ? : "(Unnamed)",
- cur == ring.current);
-
- interface.popup_show();
- } else if (id > 0) {
- allowFilename = false;
- do_edit(id);
- }
-}
-
-State *
-StateEditFile::got_file(const gchar *filename)
-{
- BEGIN_EXEC(&States::start);
-
- if (!allowFilename) {
- if (*filename)
- throw Error("If a buffer is selected by id, the <EB> "
- "string argument must be empty");
-
- return &States::start;
- }
-
- if (Globber::is_pattern(filename)) {
- Globber globber(filename, G_FILE_TEST_IS_REGULAR);
- gchar *globbed_filename;
-
- while ((globbed_filename = globber.next()))
- do_edit(globbed_filename);
- } else {
- do_edit(*filename ? filename : NULL);
- }
-
- return &States::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.
- */
-State *
-StateSaveFile::got_file(const gchar *filename)
-{
- BEGIN_EXEC(&States::start);
-
- if (QRegisters::current)
- QRegisters::current->save(filename);
- else
- ring.current->save(*filename ? filename : NULL);
-
- return &States::start;
-}
-
-void
-current_doc_undo_edit(void)
-{
- if (!QRegisters::current)
- ring.undo_edit();
- else
- undo.push_var(QRegisters::current)->undo_edit();
-}
-
-} /* namespace SciTECO */
diff --git a/src/ring.h b/src/ring.h
index 79c74f5..d1a45d7 100644
--- a/src/ring.h
+++ b/src/ring.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,241 +14,115 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __RING_H
-#define __RING_H
-
-#include <bsd/sys/queue.h>
+#pragma once
#include <glib.h>
-#include <Scintilla.h>
-
#include "sciteco.h"
-#include "memory.h"
-#include "interface.h"
#include "undo.h"
-#include "qregisters.h"
+#include "qreg.h"
+#include "view.h"
#include "parser.h"
-#include "ioview.h"
-
-namespace SciTECO {
-
-/*
- * Classes
- */
+#include "list.h"
-class Buffer : private IOView {
- TAILQ_ENTRY(Buffer) buffers;
+typedef struct teco_buffer_t {
+ teco_tailq_entry_t entry;
- class UndoTokenClose : public UndoToken {
- Buffer *buffer;
+ teco_view_t *view;
- public:
- UndoTokenClose(Buffer *_buffer)
- : buffer(_buffer) {}
-
- void run(void);
- };
-
- inline void
- undo_close(void)
- {
- undo.push<UndoTokenClose>(this);
- }
-
-public:
gchar *filename;
- bool dirty;
+ gboolean dirty;
+} teco_buffer_t;
- Buffer() : filename(NULL), dirty(false)
- {
- initialize();
- /* only have to do this once: */
- set_representations();
- }
-
- ~Buffer()
- {
- g_free(filename);
- }
-
- inline Buffer *&
- next(void)
- {
- return TAILQ_NEXT(this, buffers);
- }
- inline Buffer *&
- prev(void)
- {
- TAILQ_HEAD(Head, Buffer);
-
- return TAILQ_PREV(this, Head, buffers);
- }
-
- inline void
- set_filename(const gchar *filename)
- {
- gchar *resolved = get_absolute_path(filename);
- g_free(Buffer::filename);
- Buffer::filename = resolved;
- interface.info_update(this);
- }
-
- inline void
- edit(void)
- {
- interface.show_view(this);
- interface.info_update(this);
- }
- inline void
- undo_edit(void)
- {
- interface.undo_info_update(this);
- interface.undo_show_view(this);
- }
-
- inline void
- load(const gchar *filename)
- {
- IOView::load(filename);
-
-#if 0 /* NOTE: currently buffer cannot be dirty */
- interface.undo_info_update(this);
- undo.push_var(dirty) = false;
-#endif
-
- set_filename(filename);
- }
- void save(const gchar *filename = NULL);
+/** @memberof teco_buffer_t */
+static inline teco_buffer_t *
+teco_buffer_next(teco_buffer_t *ctx)
+{
+ return (teco_buffer_t *)ctx->entry.next;
+}
- /*
- * Ring manages the buffer list and has privileged
- * access.
- */
- friend class Ring;
-};
+/** @memberof teco_buffer_t */
+static inline teco_buffer_t *
+teco_buffer_prev(teco_buffer_t *ctx)
+{
+ return (teco_buffer_t *)ctx->entry.prev->prev->next;
+}
-/* object declared in main.cpp */
-extern class Ring : public Object {
- /*
- * Emitted after a buffer close
- * The pointer is the only remaining reference to the buffer!
- */
- class UndoTokenEdit : public UndoToken {
- Ring *ring;
- Buffer *buffer;
+void teco_buffer_edit(teco_buffer_t *ctx);
+void teco_buffer_undo_edit(teco_buffer_t *ctx);
- public:
- UndoTokenEdit(Ring *_ring, Buffer *_buffer)
- : UndoToken(), ring(_ring), buffer(_buffer) {}
- ~UndoTokenEdit()
- {
- delete buffer;
- }
+extern teco_buffer_t *teco_ring_current;
- void run(void);
- };
+teco_buffer_t *teco_ring_first(void);
+teco_buffer_t *teco_ring_last(void);
- TAILQ_HEAD(Head, Buffer) head;
+teco_int_t teco_ring_get_id(teco_buffer_t *buffer);
-public:
- Buffer *current;
+teco_buffer_t *teco_ring_find_by_name(const gchar *filename);
+teco_buffer_t *teco_ring_find_by_id(teco_int_t id);
- Ring() : current(NULL)
- {
- TAILQ_INIT(&head);
- }
- ~Ring();
+#define teco_ring_find(X) \
+ (_Generic((X), gchar * : teco_ring_find_by_name, \
+ const gchar * : teco_ring_find_by_name, \
+ teco_int_t : teco_ring_find_by_id)(X))
- inline Buffer *
- first(void)
- {
- return TAILQ_FIRST(&head);
- }
- inline Buffer *
- last(void)
- {
- return TAILQ_LAST(&head, Head);
- }
+void teco_ring_dirtify(void);
+gboolean teco_ring_is_any_dirty(void);
+gboolean teco_ring_save_all_dirty_buffers(GError **error);
- tecoInt get_id(Buffer *buffer);
- inline tecoInt
- get_id(void)
- {
- return get_id(current);
- }
+gboolean teco_ring_edit_by_name(const gchar *filename, GError **error);
+gboolean teco_ring_edit_by_id(teco_int_t id, GError **error);
- Buffer *find(const gchar *filename);
- Buffer *find(tecoInt id);
+#define teco_ring_edit(X, ERROR) \
+ (_Generic((X), gchar * : teco_ring_edit_by_name, \
+ const gchar * : teco_ring_edit_by_name, \
+ teco_int_t : teco_ring_edit_by_id)((X), (ERROR)))
- void dirtify(void);
- bool is_any_dirty(void);
- void save_all_dirty_buffers(void);
+static inline void
+teco_ring_undo_edit(void)
+{
+ teco_undo_ptr(teco_qreg_current);
+ teco_undo_ptr(teco_ring_current);
+ teco_buffer_undo_edit(teco_ring_current);
+}
- bool edit(tecoInt id);
- void edit(const gchar *filename);
- inline void
- undo_edit(void)
- {
- undo.push_var(QRegisters::current);
- undo.push_var(current)->undo_edit();
- }
+gboolean teco_ring_close(GError **error);
+void teco_ring_undo_close(void);
- void close(Buffer *buffer);
- void close(void);
- inline void
- undo_close(void)
- {
- current->undo_close();
- }
+void teco_ring_set_scintilla_undo(gboolean state);
- void set_scintilla_undo(bool state);
-} ring;
+void teco_ring_cleanup(void);
/*
* Command states
*/
-class StateEditFile : public StateExpectFile {
-private:
- bool allowFilename;
-
- void do_edit(const gchar *filename);
- void do_edit(tecoInt id);
-
- void initial(void);
- State *got_file(const gchar *filename);
-};
-
-class StateSaveFile : public StateExpectFile {
-private:
- State *got_file(const gchar *filename);
-};
-
-namespace States {
- extern StateEditFile editfile;
- extern StateSaveFile savefile;
-}
+TECO_DECLARE_STATE(teco_state_edit_file);
+TECO_DECLARE_STATE(teco_state_save_file);
/*
* Helper functions applying to any current
* document (whether a buffer or QRegister).
* There's currently no better place to put them.
*/
-void current_doc_undo_edit(void);
+static inline gboolean
+teco_current_doc_undo_edit(GError **error)
+{
+ if (!teco_qreg_current) {
+ teco_ring_undo_edit();
+ return TRUE;
+ }
+
+ teco_undo_ptr(teco_qreg_current);
+ return teco_qreg_current->vtable->undo_edit(teco_qreg_current, error);
+}
-static inline bool
-current_doc_must_undo(void)
+static inline gboolean
+teco_current_doc_must_undo(void)
{
/*
* If there's no currently edited Q-Register
* we must be editing the current buffer
*/
- return !QRegisters::current ||
- QRegisters::current->must_undo;
+ return !teco_qreg_current || teco_qreg_current->must_undo;
}
-
-} /* namespace SciTECO */
-
-#endif
diff --git a/src/scintilla.c b/src/scintilla.c
new file mode 100644
index 0000000..f58b6f4
--- /dev/null
+++ b/src/scintilla.c
@@ -0,0 +1,349 @@
+/*
+ * 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 <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "error.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "undo.h"
+#include "expressions.h"
+#include "interface.h"
+#include "scintilla.h"
+
+teco_symbol_list_t teco_symbol_list_scintilla = {NULL, 0};
+teco_symbol_list_t teco_symbol_list_scilexer = {NULL, 0};
+
+/*
+ * FIXME: Could be static.
+ */
+TECO_DEFINE_UNDO_OBJECT_OWN(scintilla_message, teco_machine_scintilla_t, /* don't delete */);
+
+/** @memberof teco_symbol_list_t */
+void
+teco_symbol_list_init(teco_symbol_list_t *ctx, const teco_symbol_entry_t *entries, gint size,
+ gboolean case_sensitive)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->entries = entries;
+ ctx->size = size;
+ ctx->cmp_fnc = case_sensitive ? strncmp : g_ascii_strncasecmp;
+}
+
+/**
+ * @note Since symbol lists are presorted constant arrays we can do a simple
+ * binary search.
+ * This does not use bsearch() since we'd have to prepend `prefix` in front of
+ * the name.
+ *
+ * @memberof teco_symbol_list_t
+ */
+gint
+teco_symbol_list_lookup(teco_symbol_list_t *ctx, const gchar *name, const gchar *prefix)
+{
+ gsize name_len = strlen(name);
+
+ gsize prefix_skip = strlen(prefix);
+ if (!ctx->cmp_fnc(name, prefix, prefix_skip))
+ prefix_skip = 0;
+
+ gint left = 0;
+ gint right = ctx->size - 1;
+
+ while (left <= right) {
+ gint cur = left + (right-left)/2;
+ gint cmp = ctx->cmp_fnc(ctx->entries[cur].name + prefix_skip,
+ name, name_len + 1);
+
+ if (!cmp)
+ return ctx->entries[cur].value;
+
+ if (cmp > 0)
+ right = cur-1;
+ else /* cmp < 0 */
+ left = cur+1;
+ }
+
+ return -1;
+}
+
+/**
+ * Auto-complete a Scintilla symbol.
+ *
+ * @param ctx The symbol list.
+ * @param symbol The symbol to auto-complete or NULL.
+ * @param insert String to initialize with the completion.
+ * @return TRUE in case of an unambiguous completion.
+ *
+ * @memberof teco_symbol_list_t
+ */
+gboolean
+teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, teco_string_t *insert)
+{
+ memset(insert, 0, sizeof(*insert));
+
+ if (!symbol)
+ symbol = "";
+ gsize symbol_len = strlen(symbol);
+
+ if (G_UNLIKELY(!ctx->list))
+ for (gint i = ctx->size; i; i--)
+ ctx->list = g_list_prepend(ctx->list, (gchar *)ctx->entries[i-1].name);
+
+ /* NOTE: element data must not be freed */
+ g_autoptr(GList) glist = g_list_copy(ctx->list);
+ guint glist_len = 0;
+
+ gsize prefix_len = 0;
+
+ for (GList *entry = g_list_first(glist), *next = g_list_next(entry);
+ entry != NULL;
+ entry = next, next = entry ? g_list_next(entry) : NULL) {
+ if (g_ascii_strncasecmp(entry->data, symbol, symbol_len) != 0) {
+ glist = g_list_delete_link(glist, entry);
+ continue;
+ }
+
+ teco_string_t glist_str;
+ 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,
+ strlen(entry->data) - symbol_len);
+ if (!prefix_len || len < prefix_len)
+ prefix_len = len;
+
+ glist_len++;
+ }
+
+ if (prefix_len > 0) {
+ teco_string_init(insert, (gchar *)glist->data + symbol_len, prefix_len);
+ } else if (glist_len > 1) {
+ for (GList *entry = g_list_first(glist);
+ entry != NULL;
+ entry = g_list_next(entry)) {
+ teco_interface_popup_add(TECO_POPUP_PLAIN, entry->data,
+ strlen(entry->data), FALSE);
+ }
+
+ teco_interface_popup_show();
+ }
+
+ return glist_len == 1;
+}
+
+/*
+ * Command states
+ */
+
+/*
+ * FIXME: This state could be static.
+ */
+TECO_DECLARE_STATE(teco_state_scintilla_lparam);
+
+static gboolean
+teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_string_t *str, GError **error)
+{
+ if (teco_string_contains(str, '\0')) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Scintilla symbol names must not contain null-byte");
+ return FALSE;
+ }
+
+ g_auto(GStrv) symbols = g_strsplit(str->data, ",", -1);
+
+ if (!symbols[0])
+ return TRUE;
+ if (*symbols[0]) {
+ gint v = teco_symbol_list_lookup(&teco_symbol_list_scintilla, symbols[0], "SCI_");
+ if (v < 0) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Unknown Scintilla message symbol \"%s\"",
+ symbols[0]);
+ return FALSE;
+ }
+ scintilla->iMessage = v;
+ }
+
+ if (!symbols[1])
+ return TRUE;
+ if (*symbols[1]) {
+ gint v = teco_symbol_list_lookup(&teco_symbol_list_scilexer, symbols[1], "");
+ if (v < 0) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Unknown Scintilla Lexer symbol \"%s\"",
+ symbols[1]);
+ return FALSE;
+ }
+ scintilla->wParam = v;
+ }
+
+ if (!symbols[2])
+ return TRUE;
+ if (*symbols[2]) {
+ gint v = teco_symbol_list_lookup(&teco_symbol_list_scilexer, symbols[2], "");
+ if (v < 0) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Unknown Scintilla Lexer symbol \"%s\"",
+ symbols[2]);
+ return FALSE;
+ }
+ scintilla->lParam = v;
+ }
+
+ return TRUE;
+}
+
+static teco_state_t *
+teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_scintilla_lparam;
+
+ /*
+ * NOTE: This is more memory efficient than pushing the individual
+ * members of teco_machine_scintilla_t and we won't need to define
+ * undo methods for the Scintilla types.
+ */
+ if (ctx->parent.must_undo)
+ teco_undo_object_scintilla_message_push(&ctx->scintilla);
+ memset(&ctx->scintilla, 0, sizeof(ctx->scintilla));
+
+ 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,
+ "<ES> command requires at least a message code");
+ return NULL;
+ }
+
+ if (!teco_expressions_pop_num_calc(&value, 0, error))
+ return NULL;
+ ctx->scintilla.iMessage = value;
+ }
+ if (!ctx->scintilla.wParam) {
+ if (!teco_expressions_pop_num_calc(&value, 0, error))
+ return NULL;
+ ctx->scintilla.wParam = value;
+ }
+
+ return &teco_state_scintilla_lparam;
+}
+
+/* in cmdline.c */
+gboolean teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error);
+
+/*$ ES scintilla message
+ * -- Send Scintilla message
+ * [lParam[,wParam]]ESmessage[,wParam[,lParam]]$[lParam]$ -> result
+ *
+ * Send Scintilla message with code specified by symbolic
+ * name <message>, <wParam> and <lParam>.
+ * <wParam> may be symbolic when specified as part of the
+ * first string argument.
+ * If not it is popped from the stack.
+ * <lParam> may be specified as a constant string whose
+ * pointer is passed to Scintilla if specified as the second
+ * string argument.
+ * If the second string argument is empty, <lParam> is popped
+ * from the stack instead.
+ * Parameters popped from the stack may be omitted, in which
+ * case 0 is implied.
+ * The message's return value is pushed onto the stack.
+ *
+ * All messages defined by Scintilla (as C macros) can be
+ * used by passing their name as a string to ES
+ * (e.g. ESSCI_LINESONSCREEN...).
+ * The \(lqSCI_\(rq prefix may be omitted and message symbols
+ * are case-insensitive.
+ * Only the Scintilla lexer symbols (SCLEX_..., SCE_...)
+ * may be used symbolically with the ES command as <wParam>,
+ * other values must be passed as integers on the stack.
+ * In interactive mode, symbols may be auto-completed by
+ * pressing Tab.
+ * String-building characters are by default interpreted
+ * in the string arguments.
+ *
+ * .BR Warning :
+ * Almost all Scintilla messages may be dispatched using
+ * this command.
+ * \*(ST does not keep track of the editor state changes
+ * performed by these commands and cannot undo them.
+ * You should never use it to change the editor state
+ * (position changes, deletions, etc.) or otherwise
+ * rub out will result in an inconsistent editor state.
+ * There are however exceptions:
+ * - In the editor profile and batch mode in general,
+ * the ES command may be used freely.
+ * - In the ED hook macro (register \(lqED\(rq),
+ * when a file is added to the ring, most destructive
+ * operations can be performed since rubbing out the
+ * EB command responsible for the hook execution also
+ * removes the buffer from the ring again.
+ * - As part of function key macros that immediately
+ * terminate the command line.
+ */
+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,
+ .expectstring.last = FALSE
+);
+
+static teco_state_t *
+teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (teco_string_contains(str, '\0')) {
+ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Scintilla lParam string must not contain null-byte.");
+ return NULL;
+ }
+
+ if (!ctx->scintilla.lParam) {
+ if (str->len > 0) {
+ ctx->scintilla.lParam = (sptr_t)str->data;
+ } else {
+ teco_int_t v;
+ if (!teco_expressions_pop_num_calc(&v, 0, error))
+ return NULL;
+ ctx->scintilla.lParam = v;
+ }
+ }
+
+ teco_expressions_push(teco_interface_ssm(ctx->scintilla.iMessage,
+ ctx->scintilla.wParam,
+ ctx->scintilla.lParam));
+
+ return &teco_state_start;
+}
+
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_lparam);
diff --git a/src/scintilla.h b/src/scintilla.h
new file mode 100644
index 0000000..607a3ff
--- /dev/null
+++ b/src/scintilla.h
@@ -0,0 +1,64 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "parser.h"
+
+typedef struct {
+ const gchar *name;
+ gint value;
+} teco_symbol_entry_t;
+
+typedef struct {
+ const teco_symbol_entry_t *entries;
+ gint size;
+
+ int (*cmp_fnc)(const char *, const char *, size_t);
+
+ /**
+ * For auto-completions.
+ * The list is allocated only ondemand.
+ */
+ GList *list;
+} teco_symbol_list_t;
+
+void teco_symbol_list_init(teco_symbol_list_t *ctx, const teco_symbol_entry_t *entries, gint size,
+ gboolean case_sensitive);
+
+gint teco_symbol_list_lookup(teco_symbol_list_t *ctx, const gchar *name, const gchar *prefix);
+
+gboolean teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, teco_string_t *insert);
+
+/** @memberof teco_symbol_list_t */
+static inline void
+teco_symbol_list_clear(teco_symbol_list_t *ctx)
+{
+ g_list_free(ctx->list);
+}
+
+extern teco_symbol_list_t teco_symbol_list_scintilla;
+extern teco_symbol_list_t teco_symbol_list_scilexer;
+
+/*
+ * Command states
+ */
+
+TECO_DECLARE_STATE(teco_state_scintilla_symbols);
diff --git a/src/sciteco.h b/src/sciteco.h
index 664b69d..dcf5359 100644
--- a/src/sciteco.h
+++ b/src/sciteco.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,100 +14,119 @@
* 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
-#ifndef __SCITECO_H
-#define __SCITECO_H
-
+#include <stdio.h>
#include <signal.h>
#include <glib.h>
-#include "interface.h"
-
-namespace SciTECO {
-
#if TECO_INTEGER == 32
-typedef gint32 tecoInt;
-#define TECO_INTEGER_FORMAT G_GINT32_FORMAT
+typedef gint32 teco_int_t;
+#define TECO_INT_FORMAT G_GINT32_FORMAT
#elif TECO_INTEGER == 64
-typedef gint64 tecoInt;
-#define TECO_INTEGER_FORMAT G_GINT64_FORMAT
+typedef gint64 teco_int_t;
+#define TECO_INT_FORMAT G_GINT64_FORMAT
#else
#error Invalid TECO integer storage size
#endif
-typedef tecoInt tecoBool;
-
-namespace Flags {
- enum {
- ED_AUTOCASEFOLD = (1 << 3),
- ED_AUTOEOL = (1 << 4),
- ED_HOOKS = (1 << 5),
- ED_FNKEYS = (1 << 6),
- ED_SHELLEMU = (1 << 7),
- ED_XTERM_CLIPBOARD = (1 << 8)
- };
-
- extern tecoInt ed;
-}
-
-extern sig_atomic_t sigint_occurred;
/**
- * For sentinels: NULL might not be defined as a
- * pointer type (LLVM/CLang)
+ * A TECO boolean - this differs from C booleans.
+ * See teco_is_success()/teco_is_failure().
*/
-#define NIL ((void *)0)
+typedef teco_int_t teco_bool_t;
+
+#define TECO_SUCCESS ((teco_bool_t)-1)
+#define TECO_FAILURE ((teco_bool_t)0)
+
+static inline teco_bool_t
+teco_bool(gboolean x)
+{
+ return x ? TECO_SUCCESS : TECO_FAILURE;
+}
+
+static inline gboolean
+teco_is_success(teco_bool_t x)
+{
+ return x < 0;
+}
-/** true if C is a control character */
-#define IS_CTL(C) ((C) < ' ')
+static inline gboolean
+teco_is_failure(teco_bool_t x)
+{
+ return x >= 0;
+}
+
+/** TRUE if C is a control character */
+#define TECO_IS_CTL(C) ((C) < ' ')
/** ASCII character to echo control character C */
-#define CTL_ECHO(C) ((C) | 0x40)
+#define TECO_CTL_ECHO(C) ((C) | 0x40)
/**
* Control character of ASCII C, i.e.
* control character corresponding to CTRL+C keypress.
*/
-#define CTL_KEY(C) ((C) & ~0x40)
-/**
- * Control character of the escape key.
- * Equivalent to CTL_KEY('[') or '\\e',
- * but more portable.
- */
-#define CTL_KEY_ESC 27
+#define TECO_CTL_KEY(C) ((C) & ~0x40)
+
/**
- * String containing the escape character.
- * There is "\e", but it's not really standard C/C++.
+ * ED flags.
+ * This is not a bitfield, since it is set from SciTECO.
*/
-#define CTL_KEY_ESC_STR "\x1B"
-
-#define SUCCESS ((tecoBool)-1)
-#define FAILURE ((tecoBool)0)
-#define TECO_BOOL(X) ((X) ? SUCCESS : FAILURE)
-
-#define IS_SUCCESS(X) ((X) < 0)
-#define IS_FAILURE(X) (!IS_SUCCESS(X))
+enum {
+ TECO_ED_AUTOCASEFOLD = (1 << 3),
+ TECO_ED_AUTOEOL = (1 << 4),
+ TECO_ED_HOOKS = (1 << 5),
+ TECO_ED_FNKEYS = (1 << 6),
+ TECO_ED_SHELLEMU = (1 << 7),
+ TECO_ED_XTERM_CLIPBOARD = (1 << 8)
+};
-/* in main.cpp */
-void interrupt(void);
+/* in main.c */
+extern teco_int_t teco_ed;
-/* in main.cpp */
-const gchar *get_eol_seq(gint eol_mode);
+/* in main.c */
+extern volatile sig_atomic_t teco_sigint_occurred;
-namespace Validate {
+/* in main.c */
+void teco_interrupt(void);
-static inline bool
-pos(gint n)
-{
- return n >= 0 && n <= interface.ssm(SCI_GETLENGTH);
-}
-
-static inline bool
-line(gint n)
-{
- return n >= 0 && n < interface.ssm(SCI_GETLINECOUNT);
-}
+/*
+ * Allows automatic cleanup of FILE pointers.
+ */
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(FILE, fclose);
-} /* namespace Validate */
+/*
+ * BEWARE DRAGONS!
+ */
+#define __TECO_FE_0(WHAT, WHAT_LAST)
+#define __TECO_FE_1(WHAT, WHAT_LAST, X) WHAT_LAST(1,X)
+#define __TECO_FE_2(WHAT, WHAT_LAST, X, ...) WHAT(2,X)__TECO_FE_1(WHAT, WHAT_LAST, __VA_ARGS__)
+#define __TECO_FE_3(WHAT, WHAT_LAST, X, ...) WHAT(3,X)__TECO_FE_2(WHAT, WHAT_LAST, __VA_ARGS__)
+#define __TECO_FE_4(WHAT, WHAT_LAST, X, ...) WHAT(4,X)__TECO_FE_3(WHAT, WHAT_LAST, __VA_ARGS__)
+#define __TECO_FE_5(WHAT, WHAT_LAST, X, ...) WHAT(5,X)__TECO_FE_4(WHAT, WHAT_LAST, __VA_ARGS__)
+//... repeat as needed
-} /* namespace SciTECO */
+#define __TECO_GET_MACRO(_0,_1,_2,_3,_4,_5,NAME,...) NAME
-#endif
+/**
+ * Invoke macro `action(ID, ARG)` on every argument
+ * and `action_last(ID, ARG)` on the very last one.
+ * Currently works only for 5 arguments,
+ * but if more are needed you can add __TECO_FE_X macros.
+ */
+#define TECO_FOR_EACH(action, action_last, ...) \
+ __TECO_GET_MACRO(_0,##__VA_ARGS__,__TECO_FE_5,__TECO_FE_4,__TECO_FE_3, \
+ __TECO_FE_2,__TECO_FE_1,__TECO_FE_0)(action,action_last,##__VA_ARGS__)
+
+#define __TECO_GEN_ARG(ID, X) X arg_##ID,
+#define __TECO_GEN_ARG_LAST(ID, X) X arg_##ID
+#define __TECO_VTABLE_GEN_CALL(ID, X) arg_##ID,
+#define __TECO_VTABLE_GEN_CALL_LAST(ID, X) arg_##ID
+
+#define TECO_DECLARE_VTABLE_METHOD(RET_TYPE, NS, NAME, OBJ_TYPE, ...) \
+ static inline RET_TYPE \
+ NS##_##NAME(OBJ_TYPE ctx, ##TECO_FOR_EACH(__TECO_GEN_ARG, __TECO_ARG_LAST, ##__VA_ARGS__)) \
+ { \
+ return ctx->vtable->NAME(ctx, ##TECO_FOR_EACH(__TECO_VTABLE_GEN_CALL, __TECO_VTABLE_GEN_CALL_LAST, ##__VA_ARGS__)); \
+ } \
+ typedef RET_TYPE (*NS##_##NAME##_t)(OBJ_TYPE, ##__VA_ARGS__)
diff --git a/src/search.c b/src/search.c
new file mode 100644
index 0000000..e5e4bd8
--- /dev/null
+++ b/src/search.c
@@ -0,0 +1,1130 @@
+/*
+ * 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 <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "expressions.h"
+#include "interface.h"
+#include "undo.h"
+#include "qreg.h"
+#include "ring.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "error.h"
+#include "search.h"
+
+typedef struct {
+ /*
+ * FIXME: Should perhaps all be teco_int_t?
+ */
+ gint dot;
+ gint from, to;
+ gint count;
+
+ teco_buffer_t *from_buffer, *to_buffer;
+} teco_search_parameters_t;
+
+TECO_DEFINE_UNDO_OBJECT_OWN(parameters, teco_search_parameters_t, /* don't delete */);
+
+/*
+ * FIXME: Global state should be part of teco_machine_main_t
+ */
+static teco_search_parameters_t teco_search_parameters;
+
+static teco_machine_qregspec_t *teco_search_qreg_machine = NULL;
+
+static gboolean
+teco_state_search_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ if (G_UNLIKELY(!teco_search_qreg_machine))
+ teco_search_qreg_machine = teco_machine_qregspec_new(TECO_QREG_REQUIRED, ctx->qreg_table_locals,
+ ctx->parent.must_undo);
+
+ teco_undo_object_parameters_push(&teco_search_parameters);
+ teco_search_parameters.dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ teco_int_t v1, v2;
+ if (!teco_expressions_pop_num_calc(&v2, teco_num_sign, error))
+ return FALSE;
+ if (teco_expressions_args()) {
+ /* TODO: optional count argument? */
+ if (!teco_expressions_pop_num_calc(&v1, 0, error))
+ return FALSE;
+ if (v1 <= v2) {
+ teco_search_parameters.count = 1;
+ teco_search_parameters.from = (gint)v1;
+ teco_search_parameters.to = (gint)v2;
+ } else {
+ teco_search_parameters.count = -1;
+ teco_search_parameters.from = (gint)v2;
+ teco_search_parameters.to = (gint)v1;
+ }
+
+ if (!teco_validate_pos(teco_search_parameters.from) ||
+ !teco_validate_pos(teco_search_parameters.to)) {
+ /*
+ * FIXME: In derived classes, the command name will
+ * no longer be correct.
+ * Better use a generic error message and prefix the error in the
+ * derived states.
+ */
+ teco_error_range_set(error, "S");
+ return FALSE;
+ }
+ } else {
+ teco_search_parameters.count = (gint)v2;
+ if (v2 >= 0) {
+ teco_search_parameters.from = teco_search_parameters.dot;
+ teco_search_parameters.to = teco_interface_ssm(SCI_GETLENGTH, 0, 0);
+ } else {
+ teco_search_parameters.from = 0;
+ teco_search_parameters.to = teco_search_parameters.dot;
+ }
+ }
+
+ teco_search_parameters.from_buffer = teco_qreg_current ? NULL : teco_ring_current;
+ teco_search_parameters.to_buffer = NULL;
+ return TRUE;
+}
+
+static const gchar *
+teco_regexp_escape_chr(gchar chr)
+{
+ static gchar escaped[] = {'\\', '\0', '\0', '\0'};
+
+ if (!chr) {
+ escaped[1] = 'c';
+ escaped[2] = '@';
+ return escaped;
+ }
+
+ escaped[1] = chr;
+ escaped[2] = '\0';
+ return g_ascii_isalnum(chr) ? escaped + 1 : escaped;
+}
+
+typedef enum {
+ TECO_SEARCH_STATE_START,
+ TECO_SEARCH_STATE_NOT,
+ TECO_SEARCH_STATE_CTL_E,
+ TECO_SEARCH_STATE_ANYQ,
+ TECO_SEARCH_STATE_MANY,
+ TECO_SEARCH_STATE_ALT
+} teco_search_state_t;
+
+/**
+ * Convert a SciTECO pattern character class to a regular
+ * expression character class.
+ * It will throw an error for wrong class specs (like invalid
+ * Q-Registers) but not incomplete ones.
+ *
+ * @param state Initial pattern converter state.
+ * May be modified on return, e.g. when ^E has been
+ * scanned without a valid class.
+ * @param pattern The character class definition to convert.
+ * This may point into a longer pattern.
+ * The pointer is modified and always left after
+ * the last character used, so it may point to the
+ * terminating null byte after the call.
+ * @param escape_default Whether to treat single characters
+ * as classes or not.
+ * @param error A GError.
+ * @return A regular expression string or NULL in case of error.
+ * An empty string signals an incomplete class specification.
+ * When a non-empty string is returned, the state has always
+ * been reset to TECO_STATE_STATE_START.
+ * Must be freed with g_free().
+ */
+static gchar *
+teco_class2regexp(teco_search_state_t *state, teco_string_t *pattern,
+ gboolean escape_default, GError **error)
+{
+ while (pattern->len > 0) {
+ switch (*state) {
+ case TECO_SEARCH_STATE_START:
+ switch (*pattern->data) {
+ case TECO_CTL_KEY('S'):
+ pattern->data++;
+ pattern->len--;
+ return g_strdup("[:^alnum:]");
+ case TECO_CTL_KEY('E'):
+ *state = TECO_SEARCH_STATE_CTL_E;
+ break;
+ default:
+ /*
+ * Either a single character "class" or
+ * not a valid class at all.
+ */
+ if (!escape_default)
+ return g_strdup("");
+ pattern->len--;
+ return g_strdup(teco_regexp_escape_chr(*pattern->data++));
+ }
+ break;
+
+ case TECO_SEARCH_STATE_CTL_E:
+ switch (teco_ascii_toupper(*pattern->data)) {
+ case 'A':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:alpha:]");
+ /* same as <CTRL/S> */
+ case 'B':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:^alnum:]");
+ case 'C':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:alnum:].$");
+ case 'D':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:digit:]");
+ case 'G':
+ *state = TECO_SEARCH_STATE_ANYQ;
+ break;
+ case 'L':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("\r\n\v\f");
+ case 'R':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:alnum:]");
+ case 'V':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:lower:]");
+ case 'W':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:upper:]");
+ default:
+ /*
+ * Not a valid ^E class, but could still
+ * be a higher-level ^E pattern.
+ */
+ return g_strdup("");
+ }
+ break;
+
+ case TECO_SEARCH_STATE_ANYQ: {
+ teco_qreg_t *reg;
+
+ switch (teco_machine_qregspec_input(teco_search_qreg_machine,
+ *pattern->data, &reg, NULL, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+
+ case TECO_MACHINE_QREGSPEC_MORE:
+ /* incomplete, but consume byte */
+ break;
+
+ case TECO_MACHINE_QREGSPEC_DONE:
+ teco_machine_qregspec_reset(teco_search_qreg_machine);
+
+ g_auto(teco_string_t) str = {NULL, 0};
+ if (!reg->vtable->get_string(reg, &str.data, &str.len, error))
+ return NULL;
+
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_regex_escape_string(str.data, str.len);
+ }
+ break;
+ }
+
+ default:
+ /*
+ * Not a class, but could still be any other
+ * high-level pattern.
+ */
+ return g_strdup("");
+ }
+
+ pattern->data++;
+ pattern->len--;
+ }
+
+ /*
+ * End of string. May happen for empty strings but also
+ * incomplete ^E or ^EG classes.
+ */
+ return g_strdup("");
+}
+
+/**
+ * Convert SciTECO pattern to regular expression.
+ * It will throw an error for definitely wrong pattern constructs
+ * but not for incomplete patterns (a necessity of search-as-you-type).
+ *
+ * @bug Incomplete patterns after a pattern has been closed (i.e. its
+ * string argument) are currently not reported as errors.
+ *
+ * @param pattern The pattern to scan through.
+ * Modifies the pointer to point after the last
+ * successfully scanned character, so it can be
+ * called recursively. It may also point to the
+ * terminating null byte after the call.
+ * @param single_expr Whether to scan a single pattern
+ * expression or an arbitrary sequence.
+ * @param error A GError.
+ * @return The regular expression string or NULL in case of GError.
+ * Must be freed with g_free().
+ */
+static gchar *
+teco_pattern2regexp(teco_string_t *pattern, gboolean single_expr, GError **error)
+{
+ teco_search_state_t state = TECO_SEARCH_STATE_START;
+ g_auto(teco_string_t) re = {NULL, 0};
+
+ do {
+ /*
+ * First check whether it is a class.
+ * This will not treat individual characters
+ * as classes, so we do not convert them to regexp
+ * classes unnecessarily.
+ */
+ g_autofree gchar *temp = teco_class2regexp(&state, pattern, FALSE, error);
+ if (!temp)
+ return NULL;
+
+ if (*temp) {
+ g_assert(state == TECO_SEARCH_STATE_START);
+
+ teco_string_append_c(&re, '[');
+ teco_string_append(&re, temp, strlen(temp));
+ teco_string_append_c(&re, ']');
+
+ /* teco_class2regexp() already consumed all the bytes */
+ continue;
+ }
+
+ if (!pattern->len)
+ /* end of pattern */
+ break;
+
+ switch (state) {
+ case TECO_SEARCH_STATE_START:
+ switch (*pattern->data) {
+ case TECO_CTL_KEY('X'): teco_string_append_c(&re, '.'); break;
+ case TECO_CTL_KEY('N'): state = TECO_SEARCH_STATE_NOT; break;
+ default: {
+ const gchar *escaped = teco_regexp_escape_chr(*pattern->data);
+ teco_string_append(&re, escaped, strlen(escaped));
+ }
+ }
+ break;
+
+ case TECO_SEARCH_STATE_NOT: {
+ state = TECO_SEARCH_STATE_START;
+ g_autofree gchar *temp = teco_class2regexp(&state, pattern, TRUE, error);
+ if (!temp)
+ return NULL;
+ if (!*temp)
+ /* a complete class is strictly required */
+ return g_strdup("");
+ g_assert(state == TECO_SEARCH_STATE_START);
+
+ teco_string_append(&re, "[^", 2);
+ teco_string_append(&re, temp, strlen(temp));
+ teco_string_append(&re, "]", 1);
+
+ /* class2regexp() already consumed all the bytes */
+ continue;
+ }
+
+ case TECO_SEARCH_STATE_CTL_E:
+ state = TECO_SEARCH_STATE_START;
+ switch (teco_ascii_toupper(*pattern->data)) {
+ case 'M': state = TECO_SEARCH_STATE_MANY; break;
+ case 'S': teco_string_append(&re, "\\s+", 3); break;
+ /* same as <CTRL/X> */
+ case 'X': teco_string_append_c(&re, '.'); break;
+ /* TODO: ASCII octal code!? */
+ case '[':
+ teco_string_append_c(&re, '(');
+ state = TECO_SEARCH_STATE_ALT;
+ break;
+ default:
+ teco_error_syntax_set(error, *pattern->data);
+ return NULL;
+ }
+ break;
+
+ case TECO_SEARCH_STATE_MANY: {
+ /* consume exactly one pattern element */
+ g_autofree gchar *temp = teco_pattern2regexp(pattern, TRUE, error);
+ if (!temp)
+ return NULL;
+ if (!*temp)
+ /* a complete expression is strictly required */
+ return g_strdup("");
+
+ teco_string_append(&re, "(", 1);
+ teco_string_append(&re, temp, strlen(temp));
+ teco_string_append(&re, ")+", 2);
+ state = TECO_SEARCH_STATE_START;
+
+ /* teco_pattern2regexp() already consumed all the bytes */
+ continue;
+ }
+
+ case TECO_SEARCH_STATE_ALT:
+ switch (*pattern->data) {
+ case ',':
+ teco_string_append_c(&re, '|');
+ break;
+ case ']':
+ teco_string_append_c(&re, ')');
+ state = TECO_SEARCH_STATE_START;
+ break;
+ default: {
+ g_autofree gchar *temp = teco_pattern2regexp(pattern, TRUE, error);
+ if (!temp)
+ return NULL;
+ if (!*temp)
+ /* a complete expression is strictly required */
+ return g_strdup("");
+
+ teco_string_append(&re, temp, strlen(temp));
+
+ /* pattern2regexp() already consumed all the bytes */
+ continue;
+ }
+ }
+ break;
+
+ default:
+ /* shouldn't happen */
+ g_assert_not_reached();
+ }
+
+ pattern->data++;
+ pattern->len--;
+ } while (!single_expr || state != TECO_SEARCH_STATE_START);
+
+ /*
+ * Complete open alternative.
+ * This could be handled like an incomplete expression,
+ * but closing it automatically improved search-as-you-type.
+ */
+ if (state == TECO_SEARCH_STATE_ALT)
+ teco_string_append_c(&re, ')');
+
+ g_assert(!teco_string_contains(&re, '\0'));
+ return g_steal_pointer(&re.data) ? : g_strdup("");
+}
+
+static gboolean
+teco_do_search(GRegex *re, gint from, gint to, gint *count, GError **error)
+{
+ g_autoptr(GMatchInfo) info = NULL;
+ const gchar *buffer = (const gchar *)teco_interface_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ GError *tmp_error = NULL;
+
+ /*
+ * NOTE: The return boolean does NOT signal whether an error was generated.
+ */
+ g_regex_match_full(re, buffer, (gssize)to, from, 0, &info, &tmp_error);
+ if (tmp_error) {
+ g_propagate_error(error, tmp_error);
+ return FALSE;
+ }
+
+ gint matched_from = -1, matched_to = -1;
+
+ if (*count >= 0) {
+ while (g_match_info_matches(info) && --(*count)) {
+ /*
+ * NOTE: The return boolean does NOT signal whether an error was generated.
+ */
+ g_match_info_next(info, &tmp_error);
+ if (tmp_error) {
+ g_propagate_error(error, tmp_error);
+ return FALSE;
+ }
+ }
+
+ if (!*count)
+ /* successful */
+ g_match_info_fetch_pos(info, 0,
+ &matched_from, &matched_to);
+ } else {
+ /* only keep the last `count' matches, in a circular stack */
+ typedef struct {
+ gint from, to;
+ } teco_range_t;
+
+ /*
+ * NOTE: It's theoretically possible that this single allocation
+ * causes an OOM if (-count) is large enough and memory limiting won't help.
+ * That's why we exceptionally have to check for allocation success.
+ */
+ g_autofree teco_range_t *matched = g_try_new(teco_range_t, -*count);
+ if (!matched) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Search count too small (%d)", *count);
+ return FALSE;
+ }
+
+ gint matched_total = 0, i = 0;
+
+ while (g_match_info_matches(info)) {
+ g_match_info_fetch_pos(info, 0,
+ &matched[i].from, &matched[i].to);
+
+ /*
+ * NOTE: The return boolean does NOT signal whether an error was generated.
+ */
+ g_match_info_next(info, &tmp_error);
+ if (tmp_error) {
+ g_propagate_error(error, tmp_error);
+ return FALSE;
+ }
+
+ i = ++matched_total % -(*count);
+ }
+
+ *count = MIN(*count + matched_total, 0);
+ if (!*count) {
+ /* successful -> i points to stack bottom */
+ matched_from = matched[i].from;
+ matched_to = matched[i].to;
+ }
+ }
+
+ if (matched_from >= 0 && matched_to >= 0)
+ /* match success */
+ teco_interface_ssm(SCI_SETSEL, matched_from, matched_to);
+
+ return TRUE;
+}
+
+static gboolean
+teco_state_search_process(teco_machine_main_t *ctx, const teco_string_t *str, gsize new_chars, GError **error)
+{
+ static const GRegexCompileFlags flags = G_REGEX_CASELESS | G_REGEX_MULTILINE |
+ G_REGEX_DOTALL | G_REGEX_RAW;
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_SETSEL,
+ teco_interface_ssm(SCI_GETANCHOR, 0, 0),
+ teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0));
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+ if (!search_reg->vtable->undo_set_integer(search_reg, error) ||
+ !search_reg->vtable->set_integer(search_reg, TECO_FAILURE, error))
+ return FALSE;
+
+ g_autoptr(GRegex) re = NULL;
+ teco_string_t pattern = *str;
+ /* NOTE: teco_pattern2regexp() modifies str pointer */
+ g_autofree gchar *re_pattern = teco_pattern2regexp(&pattern, FALSE, error);
+ if (!re_pattern)
+ return FALSE;
+ teco_machine_qregspec_reset(teco_search_qreg_machine);
+#ifdef DEBUG
+ g_printf("REGEXP: %s\n", re_pattern);
+#endif
+ if (!*re_pattern)
+ goto failure;
+ /*
+ * FIXME: Should we propagate at least some of the errors?
+ */
+ re = g_regex_new(re_pattern, flags, 0, NULL);
+ if (!re)
+ goto failure;
+
+ if (!teco_qreg_current &&
+ teco_ring_current != teco_search_parameters.from_buffer) {
+ teco_ring_undo_edit();
+ teco_buffer_edit(teco_search_parameters.from_buffer);
+ }
+
+ gint count = teco_search_parameters.count;
+
+ if (!teco_do_search(re, teco_search_parameters.from, teco_search_parameters.to, &count, error))
+ return FALSE;
+
+ if (teco_search_parameters.to_buffer && count) {
+ teco_buffer_t *buffer = teco_search_parameters.from_buffer;
+
+ if (teco_ring_current == buffer)
+ teco_ring_undo_edit();
+
+ if (count > 0) {
+ do {
+ buffer = teco_buffer_next(buffer) ? : teco_ring_first();
+ teco_buffer_edit(buffer);
+
+ if (buffer == teco_search_parameters.to_buffer) {
+ if (!teco_do_search(re, 0, teco_search_parameters.dot, &count, error))
+ return FALSE;
+ break;
+ }
+
+ if (!teco_do_search(re, 0, teco_interface_ssm(SCI_GETLENGTH, 0, 0),
+ &count, error))
+ return FALSE;
+ } while (count);
+ } else /* count < 0 */ {
+ do {
+ buffer = teco_buffer_prev(buffer) ? : teco_ring_last();
+ teco_buffer_edit(buffer);
+
+ if (buffer == teco_search_parameters.to_buffer) {
+ if (!teco_do_search(re, teco_search_parameters.dot,
+ teco_interface_ssm(SCI_GETLENGTH, 0, 0),
+ &count, error))
+ return FALSE;
+ break;
+ }
+
+ if (!teco_do_search(re, 0, teco_interface_ssm(SCI_GETLENGTH, 0, 0),
+ &count, error))
+ return FALSE;
+ } while (count);
+ }
+
+ /*
+ * FIXME: Why is this necessary?
+ */
+ teco_ring_current = buffer;
+ }
+
+ if (!search_reg->vtable->set_integer(search_reg, teco_bool(!count), error))
+ return FALSE;
+
+ if (!count)
+ return TRUE;
+
+failure:
+ teco_interface_ssm(SCI_GOTOPOS, teco_search_parameters.dot, 0);
+ return TRUE;
+}
+
+static teco_state_t *
+teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ 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, error))
+ return NULL;
+
+ teco_interface_ssm(SCI_SETANCHOR, anchor, 0);
+ } else {
+ g_auto(teco_string_t) search_str = {NULL, 0};
+ if (!search_reg->vtable->get_string(search_reg, &search_str.data, &search_str.len, error) ||
+ !teco_state_search_process(ctx, &search_str, search_str.len, error))
+ return NULL;
+ }
+
+ teco_int_t search_state;
+ if (!search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return FALSE;
+
+ if (teco_machine_main_eval_colon(ctx))
+ teco_expressions_push(search_state);
+ else if (teco_is_failure(search_state) &&
+ !teco_loop_stack->len /* not in loop */)
+ teco_interface_msg(TECO_MSG_ERROR, "Search string not found!");
+
+ return &teco_state_start;
+}
+
+/**
+ * @class TECO_DEFINE_STATE_SEARCH
+ * @implements TECO_DEFINE_STATE_EXPECTSTRING
+ * @ingroup states
+ *
+ * @fixme We need to provide a process_edit_cmd_cb since search patterns
+ * can also contain Q-Register references.
+ */
+#define TECO_DEFINE_STATE_SEARCH(NAME, ...) \
+ 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
+ * S[pattern]$ -- Search for pattern
+ * [n]S[pattern]$
+ * -S[pattern]$
+ * from,toS[pattern]$
+ * :S[pattern]$ -> Success|Failure
+ * [n]:S[pattern]$ -> Success|Failure
+ * -:S[pattern]$ -> Success|Failure
+ * from,to:S[pattern]$ -> Success|Failure
+ *
+ * Search for <pattern> in the current buffer/Q-Register.
+ * Search order and range depends on the arguments given.
+ * By default without any arguments, S will search forward
+ * from dot till file end.
+ * The optional single argument specifies the occurrence
+ * to search (1 is the first occurrence, 2 the second, etc.).
+ * Negative values for <n> perform backward searches.
+ * If missing, the sign prefix is implied for <n>.
+ * Therefore \(lq-S\(rq will search for the first occurrence
+ * of <pattern> before dot.
+ *
+ * If two arguments are specified on the command,
+ * search will be bounded in the character range <from> up to
+ * <to>, and only the first occurrence will be searched.
+ * <from> might be larger than <to> in which case a backward
+ * search is performed in the selected range.
+ *
+ * After performing the search, the search <pattern> is saved
+ * in the global search Q-Register \(lq_\(rq.
+ * A success/failure condition boolean is saved in that
+ * register's integer part.
+ * <pattern> may be omitted in which case the pattern of
+ * the last search or search and replace command will be
+ * implied by using the contents of register \(lq_\(rq
+ * (this could of course also be manually set).
+ *
+ * After a successful search, the pointer is positioned after
+ * the found text in the buffer.
+ * An unsuccessful search will display an error message but
+ * not actually yield an error.
+ * The message displaying is suppressed when executed from loops
+ * and register \(lq_\(rq is the implied argument for break-commands
+ * so that a search-break idiom can be implemented as follows:
+ * .EX
+ * <Sfoo$; ...>
+ * .EE
+ * Alternatively, S may be colon-modified in which case it returns
+ * a condition boolean that may be directly evaluated by a
+ * conditional or break-command.
+ *
+ * In interactive mode, searching will be performed immediately
+ * (\(lqsearch as you type\(rq) highlighting matched text
+ * on the fly.
+ * Changing the <pattern> results in the search being reperformed
+ * from the beginning.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_search);
+
+static gboolean
+teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ teco_undo_object_parameters_push(&teco_search_parameters);
+ teco_search_parameters.dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ teco_int_t v1, v2;
+
+ if (!teco_expressions_pop_num_calc(&v2, teco_num_sign, error))
+ return FALSE;
+ if (teco_expressions_args()) {
+ /* TODO: optional count argument? */
+ if (!teco_expressions_pop_num_calc(&v1, 0, error))
+ return FALSE;
+ if (v1 <= v2) {
+ teco_search_parameters.count = 1;
+ teco_search_parameters.from_buffer = teco_ring_find(v1);
+ teco_search_parameters.to_buffer = teco_ring_find(v2);
+ } else {
+ teco_search_parameters.count = -1;
+ teco_search_parameters.from_buffer = teco_ring_find(v2);
+ teco_search_parameters.to_buffer = teco_ring_find(v1);
+ }
+
+ if (!teco_search_parameters.from_buffer || !teco_search_parameters.to_buffer) {
+ teco_error_range_set(error, "N");
+ return FALSE;
+ }
+ } else {
+ teco_search_parameters.count = (gint)v2;
+ /* NOTE: on Q-Registers, behave like "S" */
+ if (teco_qreg_current) {
+ teco_search_parameters.from_buffer = NULL;
+ teco_search_parameters.to_buffer = NULL;
+ } else {
+ teco_search_parameters.from_buffer = teco_ring_current;
+ teco_search_parameters.to_buffer = teco_ring_current;
+ }
+ }
+
+ if (teco_search_parameters.count >= 0) {
+ teco_search_parameters.from = teco_search_parameters.dot;
+ teco_search_parameters.to = teco_interface_ssm(SCI_GETLENGTH, 0, 0);
+ } else {
+ teco_search_parameters.from = 0;
+ teco_search_parameters.to = teco_search_parameters.dot;
+ }
+
+ return TRUE;
+}
+
+static teco_state_t *
+teco_state_search_all_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode <= TECO_MODE_NORMAL &&
+ (!teco_state_search_done(ctx, str, error) ||
+ !teco_ed_hook(TECO_ED_HOOK_EDIT, error)))
+ return NULL;
+
+ return &teco_state_start;
+}
+
+/*$ N
+ * [n]N[pattern]$ -- Search over buffer-boundaries
+ * -N[pattern]$
+ * from,toN[pattern]$
+ * [n]:N[pattern]$ -> Success|Failure
+ * -:N[pattern]$ -> Success|Failure
+ * from,to:N[pattern]$ -> Success|Failure
+ *
+ * Search for <pattern> over buffer boundaries.
+ * This command is similar to the regular search command
+ * (S) but will continue to search for occurrences of
+ * pattern when the end or beginning of the current buffer
+ * is reached.
+ * Occurrences of <pattern> spanning over buffer boundaries
+ * will not be found.
+ * When searching forward N will start in the current buffer
+ * at dot, continue with the next buffer in the ring searching
+ * the entire buffer until it reaches the end of the buffer
+ * ring, continue with the first buffer in the ring until
+ * reaching the current file again where it searched from the
+ * beginning of the buffer up to its current dot.
+ * Searching backwards does the reverse.
+ *
+ * N also differs from S 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>.
+ * When specifying buffer ranges, the entire buffers are searched
+ * from beginning to end.
+ * <from> may be greater than <to> in which case, searching starts
+ * at the end of buffer <from> and continues backwards until the
+ * beginning of buffer <to> has been reached.
+ * Furthermore as with all buffer numbers, the buffer ring
+ * is considered a circular structure and it is possible
+ * to search over buffer ring boundaries by specifying
+ * buffer numbers greater than the number of buffers in the
+ * ring.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_search_all,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial
+);
+
+static teco_state_t *
+teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ teco_int_t search_state;
+ if (!teco_state_search_done(ctx, str, error) ||
+ !search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return NULL;
+
+ if (teco_is_failure(search_state))
+ return &teco_state_start;
+
+ gint dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ if (teco_search_parameters.dot < dot) {
+ /* kill forwards */
+ gint anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0);
+
+ 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,
+ anchor - teco_search_parameters.dot);
+ } else {
+ /* kill backwards */
+ teco_interface_ssm(SCI_DELETERANGE, dot, teco_search_parameters.dot - dot);
+ }
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+
+ return &teco_state_start;
+}
+
+/*$ FK
+ * FK[pattern]$ -- Delete up to occurrence of pattern
+ * [n]FK[pattern]$
+ * -FK[pattern]$
+ * from,toFK[pattern]$
+ * :FK[pattern]$ -> Success|Failure
+ * [n]:FK[pattern]$ -> Success|Failure
+ * -:FK[pattern]$ -> Success|Failure
+ * from,to:FK[pattern]$ -> Success|Failure
+ *
+ * FK searches for <pattern> just like the regular search
+ * command (S) but when found deletes all text from dot
+ * up to but not including the found text instance.
+ * When searching backwards the characters beginning after
+ * the occurrence of <pattern> up to dot are deleted.
+ *
+ * In interactive mode, deletion is not performed
+ * as-you-type but only on command termination.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_search_kill);
+
+static teco_state_t *
+teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ teco_int_t search_state;
+ if (!teco_state_search_done(ctx, str, error) ||
+ !search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return NULL;
+
+ if (teco_is_success(search_state)) {
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ teco_interface_ssm(SCI_REPLACESEL, 0, (sptr_t)"");
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+ }
+
+ return &teco_state_start;
+}
+
+/*$ FD
+ * FD[pattern]$ -- Delete occurrence of pattern
+ * [n]FD[pattern]$
+ * -FD[pattern]$
+ * from,toFD[pattern]$
+ * :FD[pattern]$ -> Success|Failure
+ * [n]:FD[pattern]$ -> Success|Failure
+ * -:FD[pattern]$ -> Success|Failure
+ * from,to:FD[pattern]$ -> Success|Failure
+ *
+ * Searches for <pattern> just like the regular search command
+ * (S) but when found deletes the entire occurrence.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_search_delete);
+
+/*
+ * FIXME: Could be static
+ */
+TECO_DEFINE_STATE_INSERT(teco_state_replace_insert,
+ .initial_cb = NULL
+);
+
+static teco_state_t *
+teco_state_replace_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ return &teco_state_start;
+}
+
+/*
+ * FIXME: Could be static
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_ignore);
+
+static teco_state_t *
+teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_replace_ignore;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ teco_int_t search_state;
+ if (!teco_state_search_delete_done(ctx, str, error) ||
+ !search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return NULL;
+
+ return teco_is_success(search_state) ? &teco_state_replace_insert
+ : &teco_state_replace_ignore;
+}
+
+/*$ FS
+ * FS[pattern]$[string]$ -- Search and replace
+ * [n]FS[pattern]$[string]$
+ * -FS[pattern]$[string]$
+ * from,toFS[pattern]$[string]$
+ * :FS[pattern]$[string]$ -> Success|Failure
+ * [n]:FS[pattern]$[string]$ -> Success|Failure
+ * -:FS[pattern]$[string]$ -> Success|Failure
+ * from,to:FS[pattern]$[string]$ -> Success|Failure
+ *
+ * Search for <pattern> just like the regular search command
+ * (S) does but replace it with <string> if found.
+ * If <string> is empty, the occurrence will always be
+ * deleted so \(lqFS[pattern]$$\(rq is similar to
+ * \(lqFD[pattern]$\(rq.
+ * The global replace register is \fBnot\fP touched
+ * by the FS command.
+ *
+ * In interactive mode, the replacement will be performed
+ * immediately and interactively.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_replace,
+ .expectstring.last = FALSE
+);
+
+/*
+ * 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)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1);
+ g_assert(replace_reg != NULL);
+
+ 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, 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, error) ||
+ !teco_state_insert_process(ctx, &replace_str, replace_str.len, error))
+ return NULL;
+ }
+
+ 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_state_t *
+teco_state_replace_default_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL ||
+ !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, error))
+ return NULL;
+
+ return &teco_state_start;
+}
+
+/*
+ * FIXME: Could be static
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_default_ignore);
+
+static teco_state_t *
+teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_replace_default_ignore;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ teco_int_t search_state;
+ if (!teco_state_search_delete_done(ctx, str, error) ||
+ !search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return NULL;
+
+ return teco_is_success(search_state) ? &teco_state_replace_default_insert
+ : &teco_state_replace_default_ignore;
+}
+
+/*$ FR
+ * FR[pattern]$[string]$ -- Search and replace with default
+ * [n]FR[pattern]$[string]$
+ * -FR[pattern]$[string]$
+ * from,toFR[pattern]$[string]$
+ * :FR[pattern]$[string]$ -> Success|Failure
+ * [n]:FR[pattern]$[string]$ -> Success|Failure
+ * -:FR[pattern]$[string]$ -> Success|Failure
+ * from,to:FR[pattern]$[string]$ -> Success|Failure
+ *
+ * The FR command is similar to the FS command.
+ * It searches for <pattern> just like the regular search
+ * command (S) and replaces the occurrence with <string>
+ * similar to what FS does.
+ * It differs from FS in the fact that the replacement
+ * string is saved in the global replacement register
+ * \(lq-\(rq.
+ * If <string> is empty the string in the global replacement
+ * register is implied instead.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_replace_default,
+ .expectstring.last = FALSE
+);
diff --git a/src/search.cpp b/src/search.cpp
deleted file mode 100644
index 63c59e9..0000000
--- a/src/search.cpp
+++ /dev/null
@@ -1,946 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <string.h>
-
-#include <glib.h>
-#include <glib/gprintf.h>
-
-#include "sciteco.h"
-#include "string-utils.h"
-#include "expressions.h"
-#include "undo.h"
-#include "qregisters.h"
-#include "ring.h"
-#include "parser.h"
-#include "search.h"
-#include "error.h"
-
-namespace SciTECO {
-
-namespace States {
- StateSearch search;
- StateSearchAll searchall;
- StateSearchKill searchkill;
- StateSearchDelete searchdelete;
-
- StateReplace replace;
- StateReplace_insert replace_insert;
- StateReplace_ignore replace_ignore;
-
- StateReplaceDefault replacedefault;
- StateReplaceDefault_insert replacedefault_insert;
- StateReplaceDefault_ignore replacedefault_ignore;
-}
-
-/*
- * Command states
- */
-
-/*$ S search pattern
- * S[pattern]$ -- Search for pattern
- * [n]S[pattern]$
- * -S[pattern]$
- * from,toS[pattern]$
- * :S[pattern]$ -> Success|Failure
- * [n]:S[pattern]$ -> Success|Failure
- * -:S[pattern]$ -> Success|Failure
- * from,to:S[pattern]$ -> Success|Failure
- *
- * Search for <pattern> in the current buffer/Q-Register.
- * Search order and range depends on the arguments given.
- * By default without any arguments, S will search forward
- * from dot till file end.
- * The optional single argument specifies the occurrence
- * to search (1 is the first occurrence, 2 the second, etc.).
- * Negative values for <n> perform backward searches.
- * If missing, the sign prefix is implied for <n>.
- * Therefore \(lq-S\(rq will search for the first occurrence
- * of <pattern> before dot.
- *
- * If two arguments are specified on the command,
- * search will be bounded in the character range <from> up to
- * <to>, and only the first occurrence will be searched.
- * <from> might be larger than <to> in which case a backward
- * search is performed in the selected range.
- *
- * After performing the search, the search <pattern> is saved
- * in the global search Q-Register \(lq_\(rq.
- * A success/failure condition boolean is saved in that
- * register's integer part.
- * <pattern> may be omitted in which case the pattern of
- * the last search or search and replace command will be
- * implied by using the contents of register \(lq_\(rq
- * (this could of course also be manually set).
- *
- * After a successful search, the pointer is positioned after
- * the found text in the buffer.
- * An unsuccessful search will display an error message but
- * not actually yield an error.
- * The message displaying is suppressed when executed from loops
- * and register \(lq_\(rq is the implied argument for break-commands
- * so that a search-break idiom can be implemented as follows:
- * .EX
- * <Sfoo$; ...>
- * .EE
- * Alternatively, S may be colon-modified in which case it returns
- * a condition boolean that may be directly evaluated by a
- * conditional or break-command.
- *
- * In interactive mode, searching will be performed immediately
- * (\(lqsearch as you type\(rq) highlighting matched text
- * on the fly.
- * Changing the <pattern> results in the search being reperformed
- * from the beginning.
- */
-void
-StateSearch::initial(void)
-{
- tecoInt v1, v2;
-
- undo.push_var(parameters);
-
- parameters.dot = interface.ssm(SCI_GETCURRENTPOS);
-
- v2 = expressions.pop_num_calc();
- if (expressions.args()) {
- /* TODO: optional count argument? */
- v1 = expressions.pop_num_calc();
- if (v1 <= v2) {
- parameters.count = 1;
- parameters.from = (gint)v1;
- parameters.to = (gint)v2;
- } else {
- parameters.count = -1;
- parameters.from = (gint)v2;
- parameters.to = (gint)v1;
- }
-
- if (!Validate::pos(parameters.from) ||
- !Validate::pos(parameters.to))
- throw RangeError("S");
- } else {
- parameters.count = (gint)v2;
- if (v2 >= 0) {
- parameters.from = parameters.dot;
- parameters.to = interface.ssm(SCI_GETLENGTH);
- } else {
- parameters.from = 0;
- parameters.to = parameters.dot;
- }
- }
-
- parameters.from_buffer = QRegisters::current ? NULL : ring.current;
- parameters.to_buffer = NULL;
-}
-
-static inline const gchar *
-regexp_escape_chr(gchar chr)
-{
- static gchar escaped[] = {'\\', '\0', '\0'};
-
- escaped[1] = chr;
- return g_ascii_isalnum(chr) ? escaped + 1 : escaped;
-}
-
-/**
- * Convert a SciTECO pattern character class to a regular
- * expression character class.
- * It will throw an error for wrong class specs (like invalid
- * Q-Registers) but not incomplete ones.
- *
- * @param state Initial pattern converter state.
- * May be modified on return, e.g. when ^E has been
- * scanned without a valid class.
- * @param pattern The character class definition to convert.
- * This may point into a longer pattern.
- * The pointer is modified and always left after
- * the last character used, so it may point to the
- * terminating null byte after the call.
- * @param escape_default Whether to treat single characters
- * as classes or not.
- * @return A regular expression string or NULL for incomplete
- * class specifications. When a string is returned, the
- * state has always been reset to STATE_START.
- * Must be freed with g_free().
- */
-gchar *
-StateSearch::class2regexp(MatchState &state, const gchar *&pattern,
- bool escape_default)
-{
- while (*pattern) {
- QRegister *reg;
- gchar *temp, *temp2;
-
- switch (state) {
- case STATE_START:
- switch (*pattern) {
- case CTL_KEY('S'):
- pattern++;
- return g_strdup("[:^alnum:]");
- case CTL_KEY('E'):
- state = STATE_CTL_E;
- break;
- default:
- /*
- * Either a single character "class" or
- * not a valid class at all.
- */
- return escape_default ? g_strdup(regexp_escape_chr(*pattern++))
- : NULL;
- }
- break;
-
- case STATE_CTL_E:
- switch (String::toupper(*pattern)) {
- case 'A':
- pattern++;
- state = STATE_START;
- return g_strdup("[:alpha:]");
- /* same as <CTRL/S> */
- case 'B':
- pattern++;
- state = STATE_START;
- return g_strdup("[:^alnum:]");
- case 'C':
- pattern++;
- state = STATE_START;
- return g_strdup("[:alnum:].$");
- case 'D':
- pattern++;
- state = STATE_START;
- return g_strdup("[:digit:]");
- case 'G':
- state = STATE_ANYQ;
- break;
- case 'L':
- pattern++;
- state = STATE_START;
- return g_strdup("\r\n\v\f");
- case 'R':
- pattern++;
- state = STATE_START;
- return g_strdup("[:alnum:]");
- case 'V':
- pattern++;
- state = STATE_START;
- return g_strdup("[:lower:]");
- case 'W':
- pattern++;
- state = STATE_START;
- return g_strdup("[:upper:]");
- default:
- /*
- * Not a valid ^E class, but could still
- * be a higher-level ^E pattern.
- */
- return NULL;
- }
- break;
-
- case STATE_ANYQ:
- /* will throw an error for invalid specs */
- if (!qreg_machine.input(*pattern, reg))
- /* incomplete, but consume byte */
- break;
- qreg_machine.reset();
-
- temp = reg->get_string();
- temp2 = g_regex_escape_string(temp, -1);
- g_free(temp);
-
- pattern++;
- state = STATE_START;
- return temp2;
-
- default:
- /*
- * Not a class, but could still be any other
- * high-level pattern.
- */
- return NULL;
- }
-
- pattern++;
- }
-
- /*
- * End of string. May happen for empty strings but also
- * incomplete ^E or ^EG classes.
- */
- return NULL;
-}
-
-/**
- * Convert SciTECO pattern to regular expression.
- * It will throw an error for definitely wrong pattern constructs
- * but not for incomplete patterns (a necessity of search-as-you-type).
- *
- * @bug Incomplete patterns after a pattern has been closed (i.e. its
- * string argument) are currently not reported as errors.
- *
- * @param pattern The pattern to scan through.
- * Modifies the pointer to point after the last
- * successfully scanned character, so it can be
- * called recursively. It may also point to the
- * terminating null byte after the call.
- * @param single_expr Whether to scan a single pattern
- * expression or an arbitrary sequence.
- * @return The regular expression string or NULL.
- * Must be freed with g_free().
- */
-gchar *
-StateSearch::pattern2regexp(const gchar *&pattern,
- bool single_expr)
-{
- MatchState state = STATE_START;
- gchar *re = NULL;
-
- do {
- gchar *temp;
-
- /*
- * First check whether it is a class.
- * This will not treat individual characters
- * as classes, so we do not convert them to regexp
- * classes unnecessarily.
- * NOTE: This might throw an error.
- */
- try {
- temp = class2regexp(state, pattern);
- } catch (...) {
- g_free(re);
- throw;
- }
- if (temp) {
- g_assert(state == STATE_START);
-
- String::append(re, "[");
- String::append(re, temp);
- String::append(re, "]");
- g_free(temp);
-
- /* class2regexp() already consumed all the bytes */
- continue;
- }
-
- if (!*pattern)
- /* end of pattern */
- break;
-
- switch (state) {
- case STATE_START:
- switch (*pattern) {
- case CTL_KEY('X'): String::append(re, "."); break;
- case CTL_KEY('N'): state = STATE_NOT; break;
- default:
- String::append(re, regexp_escape_chr(*pattern));
- }
- break;
-
- case STATE_NOT:
- state = STATE_START;
- try {
- temp = class2regexp(state, pattern, true);
- } catch (...) {
- g_free(re);
- throw;
- }
- if (!temp)
- /* a complete class is strictly required */
- goto incomplete;
- g_assert(state == STATE_START);
-
- String::append(re, "[^");
- String::append(re, temp);
- String::append(re, "]");
- g_free(temp);
-
- /* class2regexp() already consumed all the bytes */
- continue;
-
- case STATE_CTL_E:
- state = STATE_START;
- switch (String::toupper(*pattern)) {
- case 'M': state = STATE_MANY; break;
- case 'S': String::append(re, "\\s+"); break;
- /* same as <CTRL/X> */
- case 'X': String::append(re, "."); break;
- /* TODO: ASCII octal code!? */
- case '[':
- String::append(re, "(");
- state = STATE_ALT;
- break;
- default:
- g_free(re);
- throw SyntaxError(*pattern);
- }
- break;
-
- case STATE_MANY:
- /* consume exactly one pattern element */
- try {
- temp = pattern2regexp(pattern, true);
- } catch (...) {
- g_free(re);
- throw;
- }
- if (!temp)
- /* a complete expression is strictly required */
- goto incomplete;
-
- String::append(re, "(");
- String::append(re, temp);
- String::append(re, ")+");
- g_free(temp);
- state = STATE_START;
-
- /* pattern2regexp() already consumed all the bytes */
- continue;
-
- case STATE_ALT:
- switch (*pattern) {
- case ',':
- String::append(re, "|");
- break;
- case ']':
- String::append(re, ")");
- state = STATE_START;
- break;
- default:
- try {
- temp = pattern2regexp(pattern, true);
- } catch (...) {
- g_free(re);
- throw;
- }
- if (!temp)
- /* a complete expression is strictly required */
- goto incomplete;
-
- String::append(re, temp);
- g_free(temp);
-
- /* pattern2regexp() already consumed all the bytes */
- continue;
- }
- break;
-
- default:
- /* shouldn't happen */
- g_assert_not_reached();
- }
-
- pattern++;
- } while (!single_expr || state != STATE_START);
-
- /*
- * Complete open alternative.
- * This could be handled like an incomplete expression,
- * but closing it automatically improved search-as-you-type.
- */
- if (state == STATE_ALT)
- String::append(re, ")");
-
- return re;
-
-incomplete:
- g_free(re);
- return NULL;
-}
-
-void
-StateSearch::do_search(GRegex *re, gint from, gint to, gint &count)
-{
- GMatchInfo *info;
- const gchar *buffer;
-
- gint matched_from = -1, matched_to = -1;
-
- buffer = (const gchar *)interface.ssm(SCI_GETCHARACTERPOINTER);
- g_regex_match_full(re, buffer, (gssize)to, from,
- (GRegexMatchFlags)0, &info, NULL);
-
- if (count >= 0) {
- while (g_match_info_matches(info) && --count)
- g_match_info_next(info, NULL);
-
- if (!count)
- /* successful */
- g_match_info_fetch_pos(info, 0,
- &matched_from, &matched_to);
- } else {
- /* only keep the last `count' matches, in a circular stack */
- struct Range {
- gint from, to;
- };
- Range *matched = new Range[-count];
- gint matched_total = 0, i = 0;
-
- while (g_match_info_matches(info)) {
- g_match_info_fetch_pos(info, 0,
- &matched[i].from,
- &matched[i].to);
-
- g_match_info_next(info, NULL);
- i = ++matched_total % -count;
- }
-
- count = MIN(count + matched_total, 0);
- if (!count) {
- /* successful, i points to stack bottom */
- matched_from = matched[i].from;
- matched_to = matched[i].to;
- }
-
- delete[] matched;
- }
-
- g_match_info_free(info);
-
- if (matched_from >= 0 && matched_to >= 0)
- /* match success */
- interface.ssm(SCI_SETSEL, matched_from, matched_to);
-}
-
-void
-StateSearch::process(const gchar *str, gint new_chars)
-{
- static const gint flags = G_REGEX_CASELESS | G_REGEX_MULTILINE |
- G_REGEX_DOTALL | G_REGEX_RAW;
-
- QRegister *search_reg = QRegisters::globals["_"];
-
- gchar *re_pattern;
- GRegex *re;
-
- gint count = parameters.count;
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_SETSEL,
- interface.ssm(SCI_GETANCHOR),
- interface.ssm(SCI_GETCURRENTPOS));
-
- search_reg->undo_set_integer();
- search_reg->set_integer(FAILURE);
-
- /*
- * NOTE: pattern2regexp() modifies str pointer and may throw
- */
- re_pattern = pattern2regexp(str);
- qreg_machine.reset();
-#ifdef DEBUG
- g_printf("REGEXP: %s\n", re_pattern);
-#endif
- if (!re_pattern)
- goto failure;
- re = g_regex_new(re_pattern, (GRegexCompileFlags)flags,
- (GRegexMatchFlags)0, NULL);
- g_free(re_pattern);
- if (!re)
- goto failure;
-
- if (!QRegisters::current &&
- ring.current != parameters.from_buffer) {
- ring.undo_edit();
- parameters.from_buffer->edit();
- }
-
- do_search(re, parameters.from, parameters.to, count);
-
- if (parameters.to_buffer && count) {
- Buffer *buffer = parameters.from_buffer;
-
- if (ring.current == buffer)
- ring.undo_edit();
-
- if (count > 0) {
- do {
- buffer = buffer->next() ? : ring.first();
- buffer->edit();
-
- if (buffer == parameters.to_buffer) {
- do_search(re, 0, parameters.dot, count);
- break;
- }
-
- do_search(re, 0, interface.ssm(SCI_GETLENGTH),
- count);
- } while (count);
- } else /* count < 0 */ {
- do {
- buffer = buffer->prev() ? : ring.last();
- buffer->edit();
-
- if (buffer == parameters.to_buffer) {
- do_search(re, parameters.dot,
- interface.ssm(SCI_GETLENGTH),
- count);
- break;
- }
-
- do_search(re, 0, interface.ssm(SCI_GETLENGTH),
- count);
- } while (count);
- }
-
- ring.current = buffer;
- }
-
- search_reg->set_integer(TECO_BOOL(!count));
-
- g_regex_unref(re);
-
- if (!count)
- return;
-
-failure:
- interface.ssm(SCI_GOTOPOS, parameters.dot);
-}
-
-State *
-StateSearch::done(const gchar *str)
-{
- BEGIN_EXEC(&States::start);
-
- QRegister *search_reg = QRegisters::globals["_"];
-
- if (*str) {
- /* workaround: preserve selection (also on rubout) */
- gint anchor = interface.ssm(SCI_GETANCHOR);
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_SETANCHOR, anchor);
-
- search_reg->undo_set_string();
- search_reg->set_string(str);
-
- interface.ssm(SCI_SETANCHOR, anchor);
- } else {
- gchar *search_str = search_reg->get_string();
- process(search_str, 0 /* unused */);
- g_free(search_str);
- }
-
- if (eval_colon())
- expressions.push(search_reg->get_integer());
- else if (IS_FAILURE(search_reg->get_integer()) &&
- !loop_stack.items() /* not in loop */)
- interface.msg(InterfaceCurrent::MSG_ERROR, "Search string not found!");
-
- return &States::start;
-}
-
-/*$ N
- * [n]N[pattern]$ -- Search over buffer-boundaries
- * -N[pattern]$
- * from,toN[pattern]$
- * [n]:N[pattern]$ -> Success|Failure
- * -:N[pattern]$ -> Success|Failure
- * from,to:N[pattern]$ -> Success|Failure
- *
- * Search for <pattern> over buffer boundaries.
- * This command is similar to the regular search command
- * (S) but will continue to search for occurrences of
- * pattern when the end or beginning of the current buffer
- * is reached.
- * Occurrences of <pattern> spanning over buffer boundaries
- * will not be found.
- * When searching forward N will start in the current buffer
- * at dot, continue with the next buffer in the ring searching
- * the entire buffer until it reaches the end of the buffer
- * ring, continue with the first buffer in the ring until
- * reaching the current file again where it searched from the
- * beginning of the buffer up to its current dot.
- * Searching backwards does the reverse.
- *
- * N also differs from S 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>.
- * When specifying buffer ranges, the entire buffers are searched
- * from beginning to end.
- * <from> may be greater than <to> in which case, searching starts
- * at the end of buffer <from> and continues backwards until the
- * beginning of buffer <to> has been reached.
- * Furthermore as with all buffer numbers, the buffer ring
- * is considered a circular structure and it is possible
- * to search over buffer ring boundaries by specifying
- * buffer numbers greater than the number of buffers in the
- * ring.
- */
-void
-StateSearchAll::initial(void)
-{
- tecoInt v1, v2;
-
- undo.push_var(parameters);
-
- parameters.dot = interface.ssm(SCI_GETCURRENTPOS);
-
- v2 = expressions.pop_num_calc();
- if (expressions.args()) {
- /* TODO: optional count argument? */
- v1 = expressions.pop_num_calc();
- if (v1 <= v2) {
- parameters.count = 1;
- parameters.from_buffer = ring.find(v1);
- parameters.to_buffer = ring.find(v2);
- } else {
- parameters.count = -1;
- parameters.from_buffer = ring.find(v2);
- parameters.to_buffer = ring.find(v1);
- }
-
- if (!parameters.from_buffer || !parameters.to_buffer)
- throw RangeError("N");
- } else {
- parameters.count = (gint)v2;
- /* NOTE: on Q-Registers, behave like "S" */
- if (QRegisters::current)
- parameters.from_buffer = parameters.to_buffer = NULL;
- else
- parameters.from_buffer = parameters.to_buffer = ring.current;
- }
-
- if (parameters.count >= 0) {
- parameters.from = parameters.dot;
- parameters.to = interface.ssm(SCI_GETLENGTH);
- } else {
- parameters.from = 0;
- parameters.to = parameters.dot;
- }
-}
-
-State *
-StateSearchAll::done(const gchar *str)
-{
- BEGIN_EXEC(&States::start);
-
- StateSearch::done(str);
- QRegisters::hook(QRegisters::HOOK_EDIT);
-
- return &States::start;
-}
-
-/*$ FK
- * FK[pattern]$ -- Delete up to occurrence of pattern
- * [n]FK[pattern]$
- * -FK[pattern]$
- * from,toFK[pattern]$
- * :FK[pattern]$ -> Success|Failure
- * [n]:FK[pattern]$ -> Success|Failure
- * -:FK[pattern]$ -> Success|Failure
- * from,to:FK[pattern]$ -> Success|Failure
- *
- * FK searches for <pattern> just like the regular search
- * command (S) but when found deletes all text from dot
- * up to but not including the found text instance.
- * When searching backwards the characters beginning after
- * the occurrence of <pattern> up to dot are deleted.
- *
- * In interactive mode, deletion is not performed
- * as-you-type but only on command termination.
- */
-State *
-StateSearchKill::done(const gchar *str)
-{
- gint dot;
-
- BEGIN_EXEC(&States::start);
-
- QRegister *search_reg = QRegisters::globals["_"];
-
- StateSearch::done(str);
-
- if (IS_FAILURE(search_reg->get_integer()))
- return &States::start;
-
- dot = interface.ssm(SCI_GETCURRENTPOS);
-
- interface.ssm(SCI_BEGINUNDOACTION);
- if (parameters.dot < dot) {
- /* kill forwards */
- gint anchor = interface.ssm(SCI_GETANCHOR);
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS, dot);
- interface.ssm(SCI_GOTOPOS, anchor);
-
- interface.ssm(SCI_DELETERANGE,
- parameters.dot, anchor - parameters.dot);
- } else {
- /* kill backwards */
- interface.ssm(SCI_DELETERANGE, dot, parameters.dot - dot);
- }
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
-
- return &States::start;
-}
-
-/*$ FD
- * FD[pattern]$ -- Delete occurrence of pattern
- * [n]FD[pattern]$
- * -FD[pattern]$
- * from,toFD[pattern]$
- * :FD[pattern]$ -> Success|Failure
- * [n]:FD[pattern]$ -> Success|Failure
- * -:FD[pattern]$ -> Success|Failure
- * from,to:FD[pattern]$ -> Success|Failure
- *
- * Searches for <pattern> just like the regular search command
- * (S) but when found deletes the entire occurrence.
- */
-State *
-StateSearchDelete::done(const gchar *str)
-{
- BEGIN_EXEC(&States::start);
-
- QRegister *search_reg = QRegisters::globals["_"];
-
- StateSearch::done(str);
-
- if (IS_SUCCESS(search_reg->get_integer())) {
- interface.ssm(SCI_BEGINUNDOACTION);
- interface.ssm(SCI_REPLACESEL, 0, (sptr_t)"");
- interface.ssm(SCI_ENDUNDOACTION);
- ring.dirtify();
-
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
- }
-
- return &States::start;
-}
-
-/*$ FS
- * FS[pattern]$[string]$ -- Search and replace
- * [n]FS[pattern]$[string]$
- * -FS[pattern]$[string]$
- * from,toFS[pattern]$[string]$
- * :FS[pattern]$[string]$ -> Success|Failure
- * [n]:FS[pattern]$[string]$ -> Success|Failure
- * -:FS[pattern]$[string]$ -> Success|Failure
- * from,to:FS[pattern]$[string]$ -> Success|Failure
- *
- * Search for <pattern> just like the regular search command
- * (S) does but replace it with <string> if found.
- * If <string> is empty, the occurrence will always be
- * deleted so \(lqFS[pattern]$$\(rq is similar to
- * \(lqFD[pattern]$\(rq.
- * The global replace register is \fBnot\fP touched
- * by the FS command.
- *
- * In interactive mode, the replacement will be performed
- * immediately and interactively.
- */
-State *
-StateReplace::done(const gchar *str)
-{
- BEGIN_EXEC(&States::replace_ignore);
-
- QRegister *search_reg = QRegisters::globals["_"];
-
- StateSearchDelete::done(str);
-
- return IS_SUCCESS(search_reg->get_integer())
- ? (State *)&States::replace_insert
- : (State *)&States::replace_ignore;
-}
-
-State *
-StateReplace_ignore::done(const gchar *str)
-{
- return &States::start;
-}
-
-/*$ FR
- * FR[pattern]$[string]$ -- Search and replace with default
- * [n]FR[pattern]$[string]$
- * -FR[pattern]$[string]$
- * from,toFR[pattern]$[string]$
- * :FR[pattern]$[string]$ -> Success|Failure
- * [n]:FR[pattern]$[string]$ -> Success|Failure
- * -:FR[pattern]$[string]$ -> Success|Failure
- * from,to:FR[pattern]$[string]$ -> Success|Failure
- *
- * The FR command is similar to the FS command.
- * It searches for <pattern> just like the regular search
- * command (S) and replaces the occurrence with <string>
- * similar to what FS does.
- * It differs from FS in the fact that the replacement
- * string is saved in the global replacement register
- * \(lq-\(rq.
- * If <string> is empty the string in the global replacement
- * register is implied instead.
- */
-State *
-StateReplaceDefault::done(const gchar *str)
-{
- BEGIN_EXEC(&States::replacedefault_ignore);
-
- QRegister *search_reg = QRegisters::globals["_"];
-
- StateSearchDelete::done(str);
-
- return IS_SUCCESS(search_reg->get_integer())
- ? (State *)&States::replacedefault_insert
- : (State *)&States::replacedefault_ignore;
-}
-
-State *
-StateReplaceDefault_insert::done(const gchar *str)
-{
- BEGIN_EXEC(&States::start);
-
- QRegister *replace_reg = QRegisters::globals["-"];
-
- if (*str) {
- replace_reg->undo_set_string();
- replace_reg->set_string(str);
- } else {
- gchar *replace_str = replace_reg->get_string();
- StateInsert::process(replace_str, strlen(replace_str));
- g_free(replace_str);
- }
-
- return &States::start;
-}
-
-State *
-StateReplaceDefault_ignore::done(const gchar *str)
-{
- BEGIN_EXEC(&States::start);
-
- if (*str) {
- QRegister *replace_reg = QRegisters::globals["-"];
-
- replace_reg->undo_set_string();
- replace_reg->set_string(str);
- }
-
- return &States::start;
-}
-
-} /* namespace SciTECO */
diff --git a/src/search.h b/src/search.h
index 0289423..8314f06 100644
--- a/src/search.h
+++ b/src/search.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,127 +14,13 @@
* 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
-#ifndef __SEARCH_H
-#define __SEARCH_H
-
-#include <glib.h>
-
-#include "sciteco.h"
#include "parser.h"
-#include "ring.h"
-#include "qregisters.h"
-
-namespace SciTECO {
-
-/*
- * "S" command state and base class for all other search/replace commands
- */
-class StateSearch : public StateExpectString {
-public:
- StateSearch(bool last = true) : StateExpectString(true, last) {}
-
-protected:
- struct Parameters {
- gint dot;
- gint from, to;
- gint count;
-
- Buffer *from_buffer, *to_buffer;
- } parameters;
-
- QRegSpecMachine qreg_machine;
-
- enum MatchState {
- STATE_START,
- STATE_NOT,
- STATE_CTL_E,
- STATE_ANYQ,
- STATE_MANY,
- STATE_ALT
- };
-
- gchar *class2regexp(MatchState &state, const gchar *&pattern,
- bool escape_default = false);
- gchar *pattern2regexp(const gchar *&pattern, bool single_expr = false);
- void do_search(GRegex *re, gint from, gint to, gint &count);
-
- virtual void initial(void);
- virtual void process(const gchar *str, gint new_chars);
- virtual State *done(const gchar *str);
-};
-
-class StateSearchAll : public StateSearch {
-private:
- void initial(void);
- State *done(const gchar *str);
-};
-
-class StateSearchKill : public StateSearch {
-private:
- State *done(const gchar *str);
-};
-
-class StateSearchDelete : public StateSearch {
-public:
- StateSearchDelete(bool last = true) : StateSearch(last) {}
-
-protected:
- State *done(const gchar *str);
-};
-
-class StateReplace : public StateSearchDelete {
-public:
- StateReplace() : StateSearchDelete(false) {}
-
-private:
- State *done(const gchar *str);
-};
-
-class StateReplace_insert : public StateInsert {
-private:
- void initial(void) {}
-};
-
-class StateReplace_ignore : public StateExpectString {
-private:
- State *done(const gchar *str);
-};
-
-class StateReplaceDefault : public StateSearchDelete {
-public:
- StateReplaceDefault() : StateSearchDelete(false) {}
-
-private:
- State *done(const gchar *str);
-};
-
-class StateReplaceDefault_insert : public StateInsert {
-private:
- void initial(void) {}
- State *done(const gchar *str);
-};
-
-class StateReplaceDefault_ignore : public StateExpectString {
-private:
- State *done(const gchar *str);
-};
-
-namespace States {
- extern StateSearch search;
- extern StateSearchAll searchall;
- extern StateSearchKill searchkill;
- extern StateSearchDelete searchdelete;
-
- extern StateReplace replace;
- extern StateReplace_insert replace_insert;
- extern StateReplace_ignore replace_ignore;
-
- extern StateReplaceDefault replacedefault;
- extern StateReplaceDefault_insert replacedefault_insert;
- extern StateReplaceDefault_ignore replacedefault_ignore;
-}
-
-} /* namespace SciTECO */
-#endif
+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);
diff --git a/src/spawn.c b/src/spawn.c
new file mode 100644
index 0000000..1406731
--- /dev/null
+++ b/src/spawn.c
@@ -0,0 +1,667 @@
+/*
+ * 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 "sciteco.h"
+#include "interface.h"
+#include "undo.h"
+#include "expressions.h"
+#include "qreg.h"
+#include "eol.h"
+#include "ring.h"
+#include "parser.h"
+#include "memory.h"
+#include "core-commands.h"
+#include "qreg-commands.h"
+#include "error.h"
+#include "spawn.h"
+
+static void teco_spawn_child_watch_cb(GPid pid, gint status, gpointer data);
+static gboolean teco_spawn_stdin_watch_cb(GIOChannel *chan,
+ GIOCondition condition, gpointer data);
+static gboolean teco_spawn_stdout_watch_cb(GIOChannel *chan,
+ GIOCondition condition, gpointer data);
+
+/*
+ * FIXME: Global state should be part of teco_machine_main_t
+ */
+static struct {
+ GMainContext *mainctx;
+ GMainLoop *mainloop;
+ GSource *child_src;
+ GSource *stdin_src, *stdout_src;
+
+ teco_int_t from, to;
+ teco_int_t start;
+ gboolean text_added;
+
+ teco_eol_writer_t stdin_writer;
+ teco_eol_reader_t stdout_reader;
+
+ GError *error;
+ teco_bool_t rc;
+
+ teco_qreg_t *register_argument;
+} teco_spawn_ctx;
+
+static void __attribute__((constructor))
+teco_spawn_init(void)
+{
+ memset(&teco_spawn_ctx, 0, sizeof(teco_spawn_ctx));
+ /*
+ * Context and loop can be reused between EC invocations.
+ * However we should not use the default context, since it
+ * may be used by GTK
+ */
+ teco_spawn_ctx.mainctx = g_main_context_new();
+ teco_spawn_ctx.mainloop = g_main_loop_new(teco_spawn_ctx.mainctx, FALSE);
+}
+
+static gchar **
+teco_parse_shell_command_line(const gchar *cmdline, GError **error)
+{
+ gchar **argv;
+
+#ifdef G_OS_WIN32
+ if (!(teco_ed & TECO_ED_SHELLEMU)) {
+ teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, "$ComSpec", 8);
+ g_assert(reg != NULL);
+ teco_string_t comspec;
+ if (!reg->vtable->get_string(reg, &comspec.data, &comspec.len, error))
+ return NULL;
+
+ argv = g_new(gchar *, 5);
+ /*
+ * FIXME: What if $ComSpec contains null-bytes?
+ */
+ argv[0] = comspec.data;
+ argv[1] = g_strdup("/q");
+ argv[2] = g_strdup("/c");
+ argv[3] = g_strdup(cmdline);
+ argv[4] = NULL;
+ return argv;
+ }
+#elif defined(G_OS_UNIX)
+ if (!(teco_ed & TECO_ED_SHELLEMU)) {
+ teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SHELL", 6);
+ g_assert(reg != NULL);
+ teco_string_t shell;
+ if (!reg->vtable->get_string(reg, &shell.data, &shell.len, error))
+ return NULL;
+
+ argv = g_new(gchar *, 4);
+ /*
+ * FIXME: What if $SHELL contains null-bytes?
+ */
+ argv[0] = shell.data;
+ argv[1] = g_strdup("-c");
+ argv[2] = g_strdup(cmdline);
+ argv[3] = NULL;
+ return argv;
+ }
+#endif
+
+ return g_shell_parse_argv(cmdline, NULL, &argv, error) ? argv : NULL;
+}
+
+static gboolean
+teco_state_execute_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ if (!teco_expressions_eval(FALSE, error))
+ return FALSE;
+
+ teco_bool_t rc = TECO_SUCCESS;
+
+ /*
+ * By evaluating arguments here, the command may fail
+ * before the string argument is typed
+ */
+ switch (teco_expressions_args()) {
+ case 0:
+ if (teco_num_sign > 0) {
+ /* pipe nothing, insert at dot */
+ teco_spawn_ctx.from = teco_spawn_ctx.to = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ break;
+ }
+ /* fall through if prefix sign is "-" */
+
+ case 1: {
+ /* pipe and replace line range */
+ teco_int_t line;
+
+ teco_spawn_ctx.from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+ if (!teco_expressions_pop_num_calc(&line, 0, 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);
+ rc = teco_bool(teco_validate_line(line));
+
+ if (teco_spawn_ctx.to < teco_spawn_ctx.from) {
+ teco_int_t temp = teco_spawn_ctx.from;
+ teco_spawn_ctx.from = teco_spawn_ctx.to;
+ teco_spawn_ctx.to = temp;
+ }
+
+ break;
+ }
+
+ default:
+ /* pipe and replace character range */
+ if (!teco_expressions_pop_num_calc(&teco_spawn_ctx.to, 0, error) ||
+ !teco_expressions_pop_num_calc(&teco_spawn_ctx.from, 0, error))
+ return FALSE;
+ rc = teco_bool(teco_spawn_ctx.from <= teco_spawn_ctx.to &&
+ teco_validate_pos(teco_spawn_ctx.from) &&
+ teco_validate_pos(teco_spawn_ctx.to));
+ break;
+ }
+
+ if (teco_is_failure(rc)) {
+ if (!teco_machine_main_eval_colon(ctx)) {
+ teco_error_range_set(error, "EC");
+ return FALSE;
+ }
+
+ teco_expressions_push(rc);
+ teco_spawn_ctx.from = teco_spawn_ctx.to = -1;
+ /* done() will still be called */
+ }
+
+ return TRUE;
+}
+
+static teco_state_t *
+teco_state_execute_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ static const GSpawnFlags flags = G_SPAWN_DO_NOT_REAP_CHILD |
+ G_SPAWN_SEARCH_PATH |
+ G_SPAWN_STDERR_TO_DEV_NULL;
+
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ if (teco_spawn_ctx.from < 0)
+ /*
+ * teco_state_execute_initial() failed without throwing
+ * error (colon-modified)
+ */
+ return &teco_state_start;
+
+ teco_spawn_ctx.text_added = FALSE;
+ teco_spawn_ctx.rc = TECO_FAILURE;
+
+ g_autoptr(GIOChannel) stdin_chan = NULL, stdout_chan = NULL;
+ g_auto(GStrv) argv = NULL, envp = NULL;
+
+ if (teco_string_contains(str, '\0')) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Command line must not contain null-bytes");
+ goto gerror;
+ }
+
+ argv = teco_parse_shell_command_line(str->data, error);
+ if (!argv)
+ goto gerror;
+
+ envp = teco_qreg_table_get_environ(&teco_qreg_table_globals, error);
+ if (!envp)
+ goto gerror;
+
+ GPid pid;
+ gint stdin_fd, stdout_fd;
+
+ if (!g_spawn_async_with_pipes(NULL, argv, envp, flags, NULL, NULL, &pid,
+ &stdin_fd, &stdout_fd, NULL, error))
+ goto gerror;
+
+ teco_spawn_ctx.child_src = g_child_watch_source_new(pid);
+ g_source_set_callback(teco_spawn_ctx.child_src, (GSourceFunc)teco_spawn_child_watch_cb,
+ NULL, NULL);
+ g_source_attach(teco_spawn_ctx.child_src, teco_spawn_ctx.mainctx);
+
+#ifdef G_OS_WIN32
+ stdin_chan = g_io_channel_win32_new_fd(stdin_fd);
+ stdout_chan = g_io_channel_win32_new_fd(stdout_fd);
+#else /* the UNIX constructors should work everywhere else */
+ stdin_chan = g_io_channel_unix_new(stdin_fd);
+ stdout_chan = g_io_channel_unix_new(stdout_fd);
+#endif
+ g_io_channel_set_flags(stdin_chan, G_IO_FLAG_NONBLOCK, NULL);
+ g_io_channel_set_encoding(stdin_chan, NULL, NULL);
+ /*
+ * The EOL writer expects the channel to be buffered
+ * for performance reasons
+ */
+ g_io_channel_set_buffered(stdin_chan, TRUE);
+ g_io_channel_set_flags(stdout_chan, G_IO_FLAG_NONBLOCK, NULL);
+ g_io_channel_set_encoding(stdout_chan, NULL, NULL);
+ g_io_channel_set_buffered(stdout_chan, FALSE);
+
+ /*
+ * We always read from the current view,
+ * so we use its EOL mode.
+ */
+ teco_eol_writer_init_gio(&teco_spawn_ctx.stdin_writer, teco_interface_ssm(SCI_GETEOLMODE, 0, 0), stdin_chan);
+ teco_eol_reader_init_gio(&teco_spawn_ctx.stdout_reader, stdout_chan);
+
+ teco_spawn_ctx.stdin_src = g_io_create_watch(stdin_chan,
+ G_IO_OUT | G_IO_ERR | G_IO_HUP);
+ g_source_set_callback(teco_spawn_ctx.stdin_src, (GSourceFunc)teco_spawn_stdin_watch_cb,
+ NULL, NULL);
+ g_source_attach(teco_spawn_ctx.stdin_src, teco_spawn_ctx.mainctx);
+
+ teco_spawn_ctx.stdout_src = g_io_create_watch(stdout_chan,
+ G_IO_IN | G_IO_ERR | G_IO_HUP);
+ g_source_set_callback(teco_spawn_ctx.stdout_src, (GSourceFunc)teco_spawn_stdout_watch_cb,
+ NULL, NULL);
+ g_source_attach(teco_spawn_ctx.stdout_src, teco_spawn_ctx.mainctx);
+
+ if (!teco_spawn_ctx.register_argument) {
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_GOTOPOS,
+ teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0);
+ teco_interface_ssm(SCI_GOTOPOS, teco_spawn_ctx.to, 0);
+ }
+
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ teco_spawn_ctx.start = teco_spawn_ctx.from;
+ g_main_loop_run(teco_spawn_ctx.mainloop);
+ if (!teco_spawn_ctx.register_argument)
+ teco_interface_ssm(SCI_DELETERANGE, teco_spawn_ctx.from,
+ teco_spawn_ctx.to - teco_spawn_ctx.from);
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+
+ if (teco_spawn_ctx.register_argument) {
+ if (teco_spawn_ctx.stdout_reader.eol_style >= 0) {
+ teco_qreg_undo_set_eol_mode(teco_spawn_ctx.register_argument);
+ teco_qreg_set_eol_mode(teco_spawn_ctx.register_argument,
+ teco_spawn_ctx.stdout_reader.eol_style);
+ }
+ } else if (teco_spawn_ctx.from != teco_spawn_ctx.to || teco_spawn_ctx.text_added) {
+ /* undo action is only effective if it changed anything */
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+ teco_ring_dirtify();
+ }
+
+ if (!g_source_is_destroyed(teco_spawn_ctx.stdin_src))
+ g_io_channel_shutdown(stdin_chan, TRUE, NULL);
+ teco_eol_reader_clear(&teco_spawn_ctx.stdout_reader);
+ teco_eol_writer_clear(&teco_spawn_ctx.stdin_writer);
+ g_source_unref(teco_spawn_ctx.stdin_src);
+ g_io_channel_shutdown(stdout_chan, TRUE, NULL);
+ g_source_unref(teco_spawn_ctx.stdout_src);
+
+ g_source_unref(teco_spawn_ctx.child_src);
+ g_spawn_close_pid(pid);
+
+ if (teco_spawn_ctx.error) {
+ g_propagate_error(error, teco_spawn_ctx.error);
+ teco_spawn_ctx.error = NULL;
+ goto gerror;
+ }
+
+ if (teco_interface_is_interrupted()) {
+ teco_error_interrupted_set(error);
+ return NULL;
+ }
+
+ if (teco_machine_main_eval_colon(ctx))
+ teco_expressions_push(TECO_SUCCESS);
+
+ goto cleanup;
+
+gerror:
+ /* `error` has been set */
+ if (!teco_machine_main_eval_colon(ctx))
+ return NULL;
+ g_clear_error(error);
+
+ /* May contain the exit status encoded as a teco_bool_t. */
+ teco_expressions_push(teco_spawn_ctx.rc);
+ /* fall through */
+
+cleanup:
+ teco_undo_ptr(teco_spawn_ctx.register_argument) = NULL;
+ return &teco_state_start;
+}
+
+/* in cmdline.c */
+gboolean teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error);
+
+/*$ EC pipe filter
+ * EC[command]$ -- Execute operating system command and filter buffer contents
+ * linesEC[command]$
+ * -EC[command]$
+ * from,toEC[command]$
+ * :EC[command]$ -> Success|Failure
+ * lines:EC[command]$ -> Success|Failure
+ * -:EC[command]$ -> Success|Failure
+ * from,to:EC[command]$ -> Success|Failure
+ *
+ * The EC command allows you to interface with the operating
+ * system shell and external programs.
+ * The external program is spawned as a background process
+ * and its standard input stream is fed with data from the
+ * current document, i.e. text is piped into the external
+ * program.
+ * When automatic EOL translation is enabled, this will
+ * translate all end of line sequences according to the
+ * source document's EOL mode (see \fBEL\fP command).
+ * For instance when piping from a document with DOS
+ * line breaks, the receiving program will only be sent
+ * DOS line breaks.
+ * The process' standard output stream is also redirected
+ * and inserted into the current document.
+ * End of line sequences are normalized accordingly
+ * but the EOL mode guessed from the program's output is
+ * \fBnot\fP set on the document.
+ * The process' standard error stream is discarded.
+ * If data is piped into the external program, its output
+ * replaces that data in the buffer.
+ * Dot is always left at the end of the insertion.
+ *
+ * If invoked without parameters, no data is piped into
+ * the process (and no characters are removed) and its
+ * output is inserted at the current buffer position.
+ * This is equivalent to invoking \(lq.,.EC\(rq.
+ * If invoked with one parameter, the next or previous number
+ * of <lines> are piped from the buffer into the program and
+ * its output replaces these <lines>.
+ * This effectively runs <command> as a filter over <lines>.
+ * \(lq-EC\(rq may be written as a short-cut for \(lq-1EC\(rq.
+ * When invoked with two parameters, the characters beginning
+ * at position <from> up to the character at position <to>
+ * are piped into the program and replaced with its output.
+ * This effectively runs <command> as a filter over a buffer
+ * range.
+ *
+ * Errors are thrown not only for invalid buffer ranges
+ * but also for errors during process execution.
+ * If the external <command> has an unsuccessful exit code,
+ * the EC command will also fail.
+ * If the EC command is colon-modified, it will instead return
+ * a TECO boolean signifying success or failure.
+ * In case of an unsuccessful exit code, a colon-modified EC
+ * will return the absolute value of the process exit
+ * code (which is also a TECO failure boolean) and 0 for all
+ * other failures.
+ * This feature may be used to take action depending on a
+ * specific process exit code.
+ *
+ * <command> execution is by default platform-dependent.
+ * On DOS-like systems like Windows, <command> is passed to
+ * the command interpreter specified in the \fB$ComSpec\fP
+ * environment variable with the \(lq/q\(rq and \(lq/c\(rq
+ * command-line arguments.
+ * On UNIX-like systems, <command> is passed to the interpreter
+ * specified by the \fB$SHELL\fP environment variable
+ * with the \(lq-c\(rq command-line argument.
+ * Therefore the default shell can be configured using
+ * the corresponding environment registers.
+ * The operating system restrictions on the maximum
+ * length of command-line arguments apply to <command> and
+ * quoting of parameters within <command> is somewhat platform
+ * dependent.
+ * On all other platforms, \*(ST will uniformly parse
+ * <command> just as an UNIX98 \(lq/bin/sh\(rq would, but without
+ * performing any expansions.
+ * The program specified in <command> is searched for in
+ * standard locations (according to the \fB$PATH\fP environment
+ * variable).
+ * This mode of operation can also be enforced on all platforms
+ * by enabling bit 7 in the ED flag, e.g. by executing
+ * \(lq0,128ED\(rq, and is recommended when writing cross-platform
+ * macros using the EC command.
+ *
+ * When using an UNIX-compatible shell or the UNIX98 shell emulation,
+ * you might want to use the \fB^E@\fP string-building character
+ * to pass Q-Register contents reliably as single arguments to
+ * the spawned process.
+ *
+ * The spawned process inherits both \*(ST's current working
+ * directory and its environment variables.
+ * More precisely, \*(ST uses its environment registers
+ * to construct the spawned process' environment.
+ * Therefore it is also straight forward to change the working
+ * directory or some environment variable temporarily
+ * for a spawned process.
+ *
+ * Note that when run interactively and subsequently rubbed
+ * out, \*(ST can easily undo all changes to the editor
+ * state.
+ * It \fBcannot\fP however undo any other side-effects that the
+ * execution of <command> might have had on your system.
+ *
+ * Note also that the EC command blocks indefinitely until
+ * the <command> completes, which may result in editor hangs.
+ * You may however interrupt the spawned process by sending
+ * the \fBSIGINT\fP signal to \*(ST, e.g. by pressing CTRL+C.
+ *
+ * In interactive mode, \*(ST performs TAB-completion
+ * of filenames in the <command> string parameter but
+ * does not attempt any escaping of shell-relevant
+ * characters like whitespaces.
+ */
+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
+);
+
+static teco_state_t *
+teco_state_egcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg,
+ teco_qreg_table_t *table, GError **error)
+{
+ teco_state_expectqreg_reset(ctx);
+
+ if (ctx->mode <= TECO_MODE_NORMAL)
+ teco_undo_ptr(teco_spawn_ctx.register_argument) = qreg;
+ return &teco_state_execute;
+}
+
+/*$ EG EGq
+ * EGq[command]$ -- Set Q-Register to output of operating system command
+ * linesEGq[command]$
+ * -EGq[command]$
+ * from,toEGq[command]$
+ * :EGq[command]$ -> Success|Failure
+ * lines:EGq[command]$ -> Success|Failure
+ * -:EGq[command]$ -> Success|Failure
+ * from,to:EGq[command]$ -> Success|Failure
+ *
+ * Runs an operating system <command> and set Q-Register
+ * <q> to the data read from its standard output stream.
+ * Data may be fed to <command> from the current buffer/document.
+ * The interpretation of the parameters and <command> as well
+ * as the colon-modification is analoguous to the EC command.
+ *
+ * The EG command only differs from EC in not deleting any
+ * characters from the current buffer, not changing
+ * the current buffer position and writing process output
+ * to the Q-Register <q>.
+ * In other words, the current buffer is not modified by EG.
+ * Also since EG replaces the string value of <q>, the register's
+ * EOL mode is set to the mode guessed from the external program's
+ * output.
+ *
+ * The register <q> is defined if it does not already exist.
+ */
+TECO_DEFINE_STATE_EXPECTQREG(teco_state_egcommand,
+ .expectqreg.type = TECO_QREG_OPTIONAL_INIT
+);
+
+/*
+ * Glib callbacks
+ */
+
+static void
+teco_spawn_child_watch_cb(GPid pid, gint status, gpointer data)
+{
+ if (teco_spawn_ctx.error)
+ /* source has already been dispatched */
+ return;
+
+ /*
+ * There might still be data to read from stdout,
+ * but we cannot count on the stdout watcher to be ever called again.
+ */
+ teco_spawn_stdout_watch_cb(teco_spawn_ctx.stdout_reader.gio.channel, G_IO_IN, NULL);
+
+ /*
+ * teco_spawn_stdout_watch_cb() might have set the error.
+ */
+ if (!teco_spawn_ctx.error && !g_spawn_check_exit_status(status, &teco_spawn_ctx.error))
+ teco_spawn_ctx.rc = teco_spawn_ctx.error->domain == G_SPAWN_EXIT_ERROR
+ ? ABS(teco_spawn_ctx.error->code) : TECO_FAILURE;
+
+ g_main_loop_quit(teco_spawn_ctx.mainloop);
+}
+
+static gboolean
+teco_spawn_stdin_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data)
+{
+ if (teco_spawn_ctx.error)
+ /* source has already been dispatched */
+ return G_SOURCE_REMOVE;
+
+ if (!(condition & G_IO_OUT))
+ /* stdin might be closed prematurely */
+ goto remove;
+
+ /* we always read from the current view */
+ sptr_t gap = teco_interface_ssm(SCI_GETGAPPOSITION, 0, 0);
+ gsize convert_len = teco_spawn_ctx.start < gap && gap < teco_spawn_ctx.to
+ ? gap - teco_spawn_ctx.start : teco_spawn_ctx.to - teco_spawn_ctx.start;
+ const gchar *buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER,
+ teco_spawn_ctx.start, convert_len);
+
+ /*
+ * This cares about automatic EOL conversion and
+ * returns the number of consumed bytes.
+ * If it can only write a part of the EOL sequence (ie. CR of CRLF)
+ * it may return a short byte count (possibly 0) which ensures that
+ * we do not yet remove the source.
+ */
+ gssize bytes_written = teco_eol_writer_convert(&teco_spawn_ctx.stdin_writer, buffer,
+ convert_len, &teco_spawn_ctx.error);
+ if (bytes_written < 0) {
+ /* GError ocurred */
+ g_main_loop_quit(teco_spawn_ctx.mainloop);
+ return G_SOURCE_REMOVE;
+ }
+
+ teco_spawn_ctx.start += bytes_written;
+
+ if (teco_spawn_ctx.start == teco_spawn_ctx.to)
+ /* this will signal EOF to the process */
+ goto remove;
+
+ return G_SOURCE_CONTINUE;
+
+remove:
+ /*
+ * Channel is always shut down here (fd is closed),
+ * so it's always shut down IF the GSource has been
+ * destroyed. It is not guaranteed to be destroyed
+ * during the main loop run however since it quits
+ * as soon as the child was reaped and stdout was read.
+ */
+ g_io_channel_shutdown(chan, TRUE, NULL);
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+teco_spawn_stdout_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data)
+{
+ if (teco_spawn_ctx.error)
+ /* source has already been dispatched */
+ return G_SOURCE_REMOVE;
+
+ for (;;) {
+ teco_string_t buffer;
+
+ switch (teco_eol_reader_convert(&teco_spawn_ctx.stdout_reader,
+ &buffer.data, &buffer.len, &teco_spawn_ctx.error)) {
+ case G_IO_STATUS_ERROR:
+ goto error;
+
+ case G_IO_STATUS_EOF:
+ return G_SOURCE_REMOVE;
+
+ default:
+ break;
+ }
+
+ if (!buffer.len)
+ return G_SOURCE_CONTINUE;
+
+ if (teco_spawn_ctx.register_argument) {
+ if (teco_spawn_ctx.text_added) {
+ if (!teco_spawn_ctx.register_argument->vtable->undo_append_string(teco_spawn_ctx.register_argument,
+ &teco_spawn_ctx.error) ||
+ !teco_spawn_ctx.register_argument->vtable->append_string(teco_spawn_ctx.register_argument,
+ buffer.data, buffer.len,
+ &teco_spawn_ctx.error))
+ goto error;
+ } else {
+ if (!teco_spawn_ctx.register_argument->vtable->undo_set_string(teco_spawn_ctx.register_argument,
+ &teco_spawn_ctx.error) ||
+ !teco_spawn_ctx.register_argument->vtable->set_string(teco_spawn_ctx.register_argument,
+ buffer.data, buffer.len,
+ &teco_spawn_ctx.error))
+ goto error;
+ }
+ } else {
+ teco_interface_ssm(SCI_ADDTEXT, buffer.len, (sptr_t)buffer.data);
+ }
+ teco_spawn_ctx.text_added = TRUE;
+
+ /*
+ * NOTE: Since this reads from an external process, we could insert
+ * indefinitely (eg. cat /dev/zero).
+ */
+ if (!teco_memory_check(&teco_spawn_ctx.error))
+ goto error;
+ }
+
+ g_assert_not_reached();
+
+error:
+ g_main_loop_quit(teco_spawn_ctx.mainloop);
+ return G_SOURCE_REMOVE;
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_spawn_cleanup(void)
+{
+ g_main_loop_unref(teco_spawn_ctx.mainloop);
+ g_main_context_unref(teco_spawn_ctx.mainctx);
+
+ if (teco_spawn_ctx.error)
+ g_error_free(teco_spawn_ctx.error);
+}
+#endif
diff --git a/src/spawn.cpp b/src/spawn.cpp
deleted file mode 100644
index b3a8823..0000000
--- a/src/spawn.cpp
+++ /dev/null
@@ -1,662 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 "interface.h"
-#include "undo.h"
-#include "expressions.h"
-#include "qregisters.h"
-#include "eol.h"
-#include "ring.h"
-#include "parser.h"
-#include "error.h"
-#include "spawn.h"
-
-/*
- * Debian 7 is still at libglib v2.33, so
- * for the time being we support this UNIX-only
- * implementation of g_spawn_check_exit_status()
- * partially emulating libglib v2.34
- */
-#ifndef G_SPAWN_EXIT_ERROR
-#ifdef G_OS_UNIX
-#warning "libglib v2.34 or later recommended."
-#else
-#error "libglib v2.34 or later required."
-#endif
-
-#include <sys/types.h>
-#include <sys/wait.h>
-
-#define G_SPAWN_EXIT_ERROR \
- g_quark_from_static_string("g-spawn-exit-error-quark")
-
-static gboolean
-g_spawn_check_exit_status(gint exit_status, GError **error)
-{
- if (!WIFEXITED(exit_status)) {
- g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED,
- "Abnormal process termination (%d)",
- exit_status);
- return FALSE;
- }
-
- if (WEXITSTATUS(exit_status) != 0) {
- g_set_error(error, G_SPAWN_EXIT_ERROR, WEXITSTATUS(exit_status),
- "Unsuccessful exit status %d",
- WEXITSTATUS(exit_status));
- return FALSE;
- }
-
- return TRUE;
-}
-
-#endif
-
-namespace SciTECO {
-
-namespace States {
- StateExecuteCommand executecommand;
- StateEGCommand egcommand;
-}
-
-extern "C" {
-static void child_watch_cb(GPid pid, gint status, gpointer data);
-static gboolean stdin_watch_cb(GIOChannel *chan,
- GIOCondition condition, gpointer data);
-static gboolean stdout_watch_cb(GIOChannel *chan,
- GIOCondition condition, gpointer data);
-}
-
-static QRegister *register_argument = NULL;
-
-gchar **
-parse_shell_command_line(const gchar *cmdline, GError **error)
-{
- gchar **argv;
-
-#ifdef G_OS_WIN32
- if (!(Flags::ed & Flags::ED_SHELLEMU)) {
- QRegister *reg = QRegisters::globals["$COMSPEC"];
- argv = (gchar **)g_malloc(5*sizeof(gchar *));
- argv[0] = reg->get_string();
- argv[1] = g_strdup("/q");
- argv[2] = g_strdup("/c");
- argv[3] = g_strdup(cmdline);
- argv[4] = NULL;
- return argv;
- }
-#elif defined(G_OS_UNIX) || defined(G_OS_HAIKU)
- if (!(Flags::ed & Flags::ED_SHELLEMU)) {
- QRegister *reg = QRegisters::globals["$SHELL"];
- argv = (gchar **)g_malloc(4*sizeof(gchar *));
- argv[0] = reg->get_string();
- argv[1] = g_strdup("-c");
- argv[2] = g_strdup(cmdline);
- argv[3] = NULL;
- return argv;
- }
-#endif
-
- if (!g_shell_parse_argv(cmdline, NULL, &argv, error))
- return NULL;
-
- return argv;
-}
-
-/*$ EC pipe filter
- * EC[command]$ -- Execute operating system command and filter buffer contents
- * linesEC[command]$
- * -EC[command]$
- * from,toEC[command]$
- * :EC[command]$ -> Success|Failure
- * lines:EC[command]$ -> Success|Failure
- * -:EC[command]$ -> Success|Failure
- * from,to:EC[command]$ -> Success|Failure
- *
- * The EC command allows you to interface with the operating
- * system shell and external programs.
- * The external program is spawned as a background process
- * and its standard input stream is fed with data from the
- * current document, i.e. text is piped into the external
- * program.
- * When automatic EOL translation is enabled, this will
- * translate all end of line sequences according to the
- * source document's EOL mode (see \fBEL\fP command).
- * For instance when piping from a document with DOS
- * line breaks, the receiving program will only be sent
- * DOS line breaks.
- * The process' standard output stream is also redirected
- * and inserted into the current document.
- * End of line sequences are normalized accordingly
- * but the EOL mode guessed from the program's output is
- * \fBnot\fP set on the document.
- * The process' standard error stream is discarded.
- * If data is piped into the external program, its output
- * replaces that data in the buffer.
- * Dot is always left at the end of the insertion.
- *
- * If invoked without parameters, no data is piped into
- * the process (and no characters are removed) and its
- * output is inserted at the current buffer position.
- * This is equivalent to invoking \(lq.,.EC\(rq.
- * If invoked with one parameter, the next or previous number
- * of <lines> are piped from the buffer into the program and
- * its output replaces these <lines>.
- * This effectively runs <command> as a filter over <lines>.
- * \(lq-EC\(rq may be written as a short-cut for \(lq-1EC\(rq.
- * When invoked with two parameters, the characters beginning
- * at position <from> up to the character at position <to>
- * are piped into the program and replaced with its output.
- * This effectively runs <command> as a filter over a buffer
- * range.
- *
- * Errors are thrown not only for invalid buffer ranges
- * but also for errors during process execution.
- * If the external <command> has an unsuccessful exit code,
- * the EC command will also fail.
- * If the EC command is colon-modified, it will instead return
- * a TECO boolean signifying success or failure.
- * In case of an unsuccessful exit code, a colon-modified EC
- * will return the absolute value of the process exit
- * code (which is also a TECO failure boolean) and 0 for all
- * other failures.
- * This feature may be used to take action depending on a
- * specific process exit code.
- *
- * <command> execution is by default platform-dependent.
- * On DOS-like systems like Windows, <command> is passed to
- * the command interpreter specified in the \fB$COMSPEC\fP
- * environment variable with the \(lq/q\(rq and \(lq/c\(rq
- * command-line arguments.
- * On UNIX-like systems, <command> is passed to the interpreter
- * specified by the \fB$SHELL\fP environment variable
- * with the \(lq-c\(rq command-line argument.
- * Therefore the default shell can be configured using
- * the corresponding environment registers.
- * The operating system restrictions on the maximum
- * length of command-line arguments apply to <command> and
- * quoting of parameters within <command> is somewhat platform
- * dependent.
- * On all other platforms, \*(ST will uniformly parse
- * <command> just as an UNIX98 \(lq/bin/sh\(rq would, but without
- * performing any expansions.
- * The program specified in <command> is searched for in
- * standard locations (according to the \fB$PATH\fP environment
- * variable).
- * This mode of operation can also be enforced on all platforms
- * by enabling bit 7 in the ED flag, e.g. by executing
- * \(lq0,128ED\(rq, and is recommended when writing cross-platform
- * macros using the EC command.
- *
- * When using an UNIX-compatible shell or the UNIX98 shell emulation,
- * you might want to use the \fB^E@\fP string-building character
- * to pass Q-Register contents reliably as single arguments to
- * the spawned process.
- *
- * The spawned process inherits both \*(ST's current working
- * directory and its environment variables.
- * More precisely, \*(ST uses its environment registers
- * to construct the spawned process' environment.
- * Therefore it is also straight forward to change the working
- * directory or some environment variable temporarily
- * for a spawned process.
- *
- * Note that when run interactively and subsequently rubbed
- * out, \*(ST can easily undo all changes to the editor
- * state.
- * It \fBcannot\fP however undo any other side-effects that the
- * execution of <command> might have had on your system.
- *
- * Note also that the EC command blocks indefinitely until
- * the <command> completes, which may result in editor hangs.
- * You may however interrupt the spawned process by sending
- * the \fBSIGINT\fP signal to \*(ST, e.g. by pressing CTRL+C.
- *
- * In interactive mode, \*(ST performs TAB-completion
- * of filenames in the <command> string parameter but
- * does not attempt any escaping of shell-relevant
- * characters like whitespaces.
- */
-StateExecuteCommand::StateExecuteCommand() : StateExpectString()
-{
- /*
- * Context and loop can be reused between EC invocations.
- * However we should not use the default context, since it
- * may be used by GTK
- */
- ctx.mainctx = g_main_context_new();
- ctx.mainloop = g_main_loop_new(ctx.mainctx, FALSE);
-}
-
-StateExecuteCommand::~StateExecuteCommand()
-{
- g_main_loop_unref(ctx.mainloop);
-#ifndef G_OS_HAIKU
- /*
- * Apparently, there's some kind of double-free
- * bug in Haiku's glib-2.38.
- * It is unknown whether this is has
- * already been fixed and affects other platforms
- * (but I never observed any segfaults).
- */
- g_main_context_unref(ctx.mainctx);
-#endif
-
- delete ctx.error;
-}
-
-void
-StateExecuteCommand::initial(void)
-{
- tecoBool rc = SUCCESS;
-
- expressions.eval();
-
- /*
- * By evaluating arguments here, the command may fail
- * before the string argument is typed
- */
- switch (expressions.args()) {
- case 0:
- if (expressions.num_sign > 0) {
- /* pipe nothing, insert at dot */
- ctx.from = ctx.to = interface.ssm(SCI_GETCURRENTPOS);
- break;
- }
- /* fall through if prefix sign is "-" */
-
- case 1: {
- /* pipe and replace line range */
- sptr_t line;
-
- ctx.from = interface.ssm(SCI_GETCURRENTPOS);
- line = interface.ssm(SCI_LINEFROMPOSITION, ctx.from) +
- expressions.pop_num_calc();
- ctx.to = interface.ssm(SCI_POSITIONFROMLINE, line);
- rc = TECO_BOOL(Validate::line(line));
-
- if (ctx.to < ctx.from) {
- tecoInt temp = ctx.from;
- ctx.from = ctx.to;
- ctx.to = temp;
- }
-
- break;
- }
-
- default:
- /* pipe and replace character range */
- ctx.to = expressions.pop_num_calc();
- ctx.from = expressions.pop_num_calc();
- rc = TECO_BOOL(ctx.from <= ctx.to &&
- Validate::pos(ctx.from) &&
- Validate::pos(ctx.to));
- break;
- }
-
- if (IS_FAILURE(rc)) {
- if (eval_colon()) {
- expressions.push(rc);
- ctx.from = ctx.to = -1;
- /* done() will still be called */
- } else {
- throw RangeError("EC");
- }
- }
-}
-
-State *
-StateExecuteCommand::done(const gchar *str)
-{
- BEGIN_EXEC(&States::start);
-
- if (ctx.from < 0)
- /*
- * initial() failed without throwing
- * error (colon-modified)
- */
- return &States::start;
-
- GError *error = NULL;
- gchar **argv, **envp;
- static const gint flags = G_SPAWN_DO_NOT_REAP_CHILD |
- G_SPAWN_SEARCH_PATH |
- G_SPAWN_STDERR_TO_DEV_NULL;
-
- GPid pid;
- gint stdin_fd, stdout_fd;
- GIOChannel *stdin_chan, *stdout_chan;
-
- /*
- * We always read from the current view,
- * so we use its EOL mode.
- *
- * NOTE: We do not declare the writer/reader objects as part of
- * StateExecuteCommand::Context so we do not have to
- * reset it. It's only required for the life time of this call
- * anyway.
- * I do not see a more elegant way out of this.
- */
- EOLWriterGIO stdin_writer(interface.ssm(SCI_GETEOLMODE));
- EOLReaderGIO stdout_reader;
-
- ctx.text_added = false;
-
- ctx.stdin_writer = &stdin_writer;
- ctx.stdout_reader = &stdout_reader;
-
- delete ctx.error;
- ctx.error = NULL;
- ctx.rc = FAILURE;
-
- argv = parse_shell_command_line(str, &error);
- if (!argv)
- goto gerror;
-
- envp = QRegisters::globals.get_environ();
-
- g_spawn_async_with_pipes(NULL, argv, envp, (GSpawnFlags)flags,
- NULL, NULL, &pid,
- &stdin_fd, &stdout_fd, NULL,
- &error);
-
- g_strfreev(envp);
- g_strfreev(argv);
-
- if (error)
- goto gerror;
-
- ctx.child_src = g_child_watch_source_new(pid);
- g_source_set_callback(ctx.child_src, (GSourceFunc)child_watch_cb,
- &ctx, NULL);
- g_source_attach(ctx.child_src, ctx.mainctx);
-
-#ifdef G_OS_WIN32
- stdin_chan = g_io_channel_win32_new_fd(stdin_fd);
- stdout_chan = g_io_channel_win32_new_fd(stdout_fd);
-#else /* the UNIX constructors should work everywhere else */
- stdin_chan = g_io_channel_unix_new(stdin_fd);
- stdout_chan = g_io_channel_unix_new(stdout_fd);
-#endif
- g_io_channel_set_flags(stdin_chan, G_IO_FLAG_NONBLOCK, NULL);
- g_io_channel_set_encoding(stdin_chan, NULL, NULL);
- /*
- * EOLWriterGIO expects the channel to be buffered
- * for performance reasons
- */
- g_io_channel_set_buffered(stdin_chan, TRUE);
- g_io_channel_set_flags(stdout_chan, G_IO_FLAG_NONBLOCK, NULL);
- g_io_channel_set_encoding(stdout_chan, NULL, NULL);
- g_io_channel_set_buffered(stdout_chan, FALSE);
-
- stdin_writer.set_channel(stdin_chan);
- stdout_reader.set_channel(stdout_chan);
-
- ctx.stdin_src = g_io_create_watch(stdin_chan,
- (GIOCondition)(G_IO_OUT | G_IO_ERR | G_IO_HUP));
- g_source_set_callback(ctx.stdin_src, (GSourceFunc)stdin_watch_cb,
- &ctx, NULL);
- g_source_attach(ctx.stdin_src, ctx.mainctx);
-
- ctx.stdout_src = g_io_create_watch(stdout_chan,
- (GIOCondition)(G_IO_IN | G_IO_ERR | G_IO_HUP));
- g_source_set_callback(ctx.stdout_src, (GSourceFunc)stdout_watch_cb,
- &ctx, NULL);
- g_source_attach(ctx.stdout_src, ctx.mainctx);
-
- if (!register_argument) {
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS));
- interface.ssm(SCI_GOTOPOS, ctx.to);
- }
-
- interface.ssm(SCI_BEGINUNDOACTION);
- ctx.start = ctx.from;
- g_main_loop_run(ctx.mainloop);
- if (!register_argument)
- interface.ssm(SCI_DELETERANGE, ctx.from, ctx.to - ctx.from);
- interface.ssm(SCI_ENDUNDOACTION);
-
- if (register_argument) {
- if (stdout_reader.eol_style >= 0) {
- register_argument->undo_set_eol_mode();
- register_argument->set_eol_mode(stdout_reader.eol_style);
- }
- } else if (ctx.from != ctx.to || ctx.text_added) {
- /* undo action is only effective if it changed anything */
- if (current_doc_must_undo())
- interface.undo_ssm(SCI_UNDO);
- interface.ssm(SCI_SCROLLCARET);
- ring.dirtify();
- }
-
- if (!g_source_is_destroyed(ctx.stdin_src))
- g_io_channel_shutdown(stdin_chan, TRUE, NULL);
- g_io_channel_unref(stdin_chan);
- g_source_unref(ctx.stdin_src);
- g_io_channel_shutdown(stdout_chan, TRUE, NULL);
- g_io_channel_unref(stdout_chan);
- g_source_unref(ctx.stdout_src);
-
- g_source_unref(ctx.child_src);
- g_spawn_close_pid(pid);
-
- if (ctx.error) {
- if (!eval_colon())
- throw *ctx.error;
-
- /*
- * This may contain the exit status
- * encoded as a tecoBool.
- */
- expressions.push(ctx.rc);
- goto cleanup;
- }
-
- if (interface.is_interrupted())
- throw Error("Interrupted");
-
- if (eval_colon())
- expressions.push(SUCCESS);
-
- goto cleanup;
-
-gerror:
- if (!eval_colon())
- throw GlibError(error);
- g_error_free(error);
-
- expressions.push(ctx.rc);
-
-cleanup:
- undo.push_var(register_argument) = NULL;
- return &States::start;
-}
-
-/*$ EG EGq
- * EGq[command]$ -- Set Q-Register to output of operating system command
- * linesEGq[command]$
- * -EGq[command]$
- * from,toEGq[command]$
- * :EGq[command]$ -> Success|Failure
- * lines:EGq[command]$ -> Success|Failure
- * -:EGq[command]$ -> Success|Failure
- * from,to:EGq[command]$ -> Success|Failure
- *
- * Runs an operating system <command> and set Q-Register
- * <q> to the data read from its standard output stream.
- * Data may be fed to <command> from the current buffer/document.
- * The interpretation of the parameters and <command> as well
- * as the colon-modification is analoguous to the EC command.
- *
- * The EG command only differs from EC in not deleting any
- * characters from the current buffer, not changing
- * the current buffer position and writing process output
- * to the Q-Register <q>.
- * In other words, the current buffer is not modified by EG.
- * Also since EG replaces the string value of <q>, the register's
- * EOL mode is set to the mode guessed from the external program's
- * output.
- *
- * The register <q> is defined if it does not already exist.
- */
-State *
-StateEGCommand::got_register(QRegister *reg)
-{
- machine.reset();
-
- BEGIN_EXEC(&States::executecommand);
- undo.push_var(register_argument) = reg;
- return &States::executecommand;
-}
-
-/*
- * Glib callbacks
- */
-
-static void
-child_watch_cb(GPid pid, gint status, gpointer data)
-{
- StateExecuteCommand::Context &ctx =
- *(StateExecuteCommand::Context *)data;
- GError *error = NULL;
-
- /*
- * Writing stdin or reading stdout might have already
- * failed. We preserve the earliest GError.
- */
- if (!ctx.error && !g_spawn_check_exit_status(status, &error)) {
- ctx.rc = error->domain == G_SPAWN_EXIT_ERROR
- ? ABS(error->code) : FAILURE;
- ctx.error = new GlibError(error);
- }
-
- if (g_source_is_destroyed(ctx.stdout_src))
- g_main_loop_quit(ctx.mainloop);
-}
-
-static gboolean
-stdin_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data)
-{
- StateExecuteCommand::Context &ctx =
- *(StateExecuteCommand::Context *)data;
-
- sptr_t gap;
- gsize convert_len;
- const gchar *buffer;
- gsize bytes_written;
-
- if (!(condition & G_IO_OUT))
- /* stdin might be closed prematurely */
- goto remove;
-
- /* we always read from the current view */
- gap = interface.ssm(SCI_GETGAPPOSITION);
- convert_len = ctx.start < gap && gap < ctx.to
- ? gap - ctx.start : ctx.to - ctx.start;
- buffer = (const gchar *)interface.ssm(SCI_GETRANGEPOINTER,
- ctx.start, convert_len);
-
- try {
- /*
- * This cares about automatic EOL conversion and
- * returns the number of consumed bytes.
- * If it can only write a part of the EOL sequence (ie. CR of CRLF)
- * it may return a short byte count (possibly 0) which ensures that
- * we do not yet remove the source.
- */
- bytes_written = ctx.stdin_writer->convert(buffer, convert_len);
- } catch (Error &e) {
- ctx.error = new Error(e);
- /* do not yet quit -- we still have to reap the child */
- goto remove;
- }
-
- ctx.start += bytes_written;
-
- if (ctx.start == ctx.to)
- /* this will signal EOF to the process */
- goto remove;
-
- return G_SOURCE_CONTINUE;
-
-remove:
- /*
- * Channel is always shut down here (fd is closed),
- * so it's always shut down IF the GSource has been
- * destroyed. It is not guaranteed to be destroyed
- * during the main loop run however since it quits
- * as soon as the child was reaped and stdout was read.
- */
- g_io_channel_shutdown(chan, TRUE, NULL);
- return G_SOURCE_REMOVE;
-}
-
-static gboolean
-stdout_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data)
-{
- StateExecuteCommand::Context &ctx =
- *(StateExecuteCommand::Context *)data;
-
- for (;;) {
- const gchar *buffer;
- gsize data_len;
-
- try {
- buffer = ctx.stdout_reader->convert(data_len);
- } catch (Error &e) {
- ctx.error = new Error(e);
- goto remove;
- }
- if (!buffer)
- /* EOF */
- goto remove;
-
- if (!data_len)
- return G_SOURCE_CONTINUE;
-
- if (register_argument) {
- if (ctx.text_added) {
- register_argument->undo_append_string();
- register_argument->append_string(buffer, data_len);
- } else {
- register_argument->undo_set_string();
- register_argument->set_string(buffer, data_len);
- }
- } else {
- interface.ssm(SCI_ADDTEXT, data_len, (sptr_t)buffer);
- }
- ctx.text_added = true;
- }
-
- /* not reached */
- return G_SOURCE_CONTINUE;
-
-remove:
- if (g_source_is_destroyed(ctx.child_src))
- g_main_loop_quit(ctx.mainloop);
- return G_SOURCE_REMOVE;
-}
-
-} /* namespace SciTECO */
diff --git a/src/spawn.h b/src/spawn.h
index 4d7403e..f6dec38 100644
--- a/src/spawn.h
+++ b/src/spawn.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,68 +14,9 @@
* 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
-#ifndef __SPAWN_H
-#define __SPAWN_H
-
-#include <glib.h>
-
-#include "sciteco.h"
#include "parser.h"
-#include "qregisters.h"
-#include "error.h"
-#include "eol.h"
-
-namespace SciTECO {
-
-gchar **parse_shell_command_line(const gchar *cmdline, GError **error);
-
-class StateExecuteCommand : public StateExpectString {
-public:
- StateExecuteCommand();
- ~StateExecuteCommand();
-
- struct Context {
- GMainContext *mainctx;
- GMainLoop *mainloop;
- GSource *child_src;
- GSource *stdin_src, *stdout_src;
-
- tecoInt from, to;
- tecoInt start;
- bool text_added;
-
- EOLWriterGIO *stdin_writer;
- EOLReaderGIO *stdout_reader;
-
- Error *error;
- tecoBool rc;
- };
-
-private:
- Context ctx;
-
- void initial(void);
- State *done(const gchar *str);
-
-protected:
- /* in cmdline.cpp */
- void process_edit_cmd(gchar key);
-};
-
-class StateEGCommand : public StateExpectQReg {
-public:
- StateEGCommand() : StateExpectQReg(QREG_OPTIONAL_INIT) {}
-
-private:
- State *got_register(QRegister *reg);
-};
-
-namespace States {
- extern StateExecuteCommand executecommand;
- extern StateEGCommand egcommand;
-}
-
-} /* namespace SciTECO */
-#endif
+TECO_DECLARE_STATE(teco_state_execute);
+TECO_DECLARE_STATE(teco_state_egcommand);
diff --git a/src/string-utils.c b/src/string-utils.c
new file mode 100644
index 0000000..f15b307
--- /dev/null
+++ b/src/string-utils.c
@@ -0,0 +1,185 @@
+/*
+ * 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 "sciteco.h"
+#include "undo.h"
+#include "string-utils.h"
+
+/**
+ * Get echoable (printable) version of a given string.
+ *
+ * This converts all control characters to printable
+ * characters without tabs, line feeds, etc.
+ * That's also why it can safely return a null-terminated string.
+ * Useful for displaying Q-Register names and TECO code.
+ *
+ * @memberof teco_string_t
+ */
+gchar *
+teco_string_echo(const gchar *str, gsize len)
+{
+ gchar *ret, *p;
+
+ p = ret = g_malloc(len*2 + 1);
+
+ for (guint i = 0; i < len; i++) {
+ if (TECO_IS_CTL(str[i])) {
+ *p++ = '^';
+ *p++ = TECO_CTL_ECHO(str[i]);
+ } else {
+ *p++ = str[i];
+ }
+ }
+ *p = '\0';
+
+ return ret;
+}
+
+/** @memberof teco_string_t */
+void
+teco_string_get_coord(const gchar *str, guint pos, guint *line, guint *column)
+{
+ *line = *column = 1;
+
+ for (guint i = 0; i < pos; i++) {
+ switch (str[i]) {
+ case '\r':
+ if (str[i+1] == '\n')
+ i++;
+ /* fall through */
+ case '\n':
+ (*line)++;
+ (*column) = 1;
+ break;
+ default:
+ (*column)++;
+ break;
+ }
+ }
+}
+
+/** @memberof teco_string_t */
+gsize
+teco_string_diff(const 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])
+ len++;
+
+ return len;
+}
+
+/** @memberof teco_string_t */
+gsize
+teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len)
+{
+ gsize len = 0;
+
+ while (len < a->len && len < b_len &&
+ g_ascii_tolower(a->data[len]) == g_ascii_tolower(b[len]))
+ len++;
+
+ return len;
+}
+
+/** @memberof teco_string_t */
+gint
+teco_string_cmp(const teco_string_t *a, const gchar *b, gsize b_len)
+{
+ 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];
+ if (ret != 0)
+ /* a and b have a common prefix of length i */
+ return ret;
+ }
+
+ 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)
+{
+ 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]);
+ if (ret != 0)
+ /* a and b have a common prefix of length i */
+ return ret;
+ }
+
+ return a->len == b_len ? 0 : -1;
+}
+
+/**
+ * Find string after the last occurrence of any in a set of characters.
+ *
+ * @param str String to search through.
+ * @param chars Null-terminated set of characters.
+ * The null-byte itself is always considered part of the set.
+ * @return A null-terminated suffix of str or NULL.
+ *
+ * @memberof teco_string_t
+ */
+const gchar *
+teco_string_last_occurrence(const teco_string_t *str, const gchar *chars)
+{
+ teco_string_t ret = *str;
+
+ if (!ret.len)
+ return NULL;
+
+ do {
+ gint i = teco_string_rindex(&ret, *chars);
+ if (i >= 0) {
+ ret.data += i+1;
+ ret.len -= i+1;
+ }
+ } while (*chars++);
+
+ return ret.data;
+}
+
+TECO_DEFINE_UNDO_CALL(teco_string_truncate, teco_string_t *, gsize);
+
+TECO_DEFINE_UNDO_OBJECT(cstring, gchar *, g_strdup, g_free);
+
+static inline teco_string_t
+teco_string_copy(const teco_string_t str)
+{
+ teco_string_t ret;
+ teco_string_init(&ret, str.data, str.len);
+ return ret;
+}
+
+#define DELETE(X) teco_string_clear(&(X))
+TECO_DEFINE_UNDO_OBJECT(string, teco_string_t, teco_string_copy, DELETE);
+TECO_DEFINE_UNDO_OBJECT_OWN(string_own, teco_string_t, DELETE);
+#undef DELETE
diff --git a/src/string-utils.cpp b/src/string-utils.cpp
deleted file mode 100644
index 4c3c53e..0000000
--- a/src/string-utils.cpp
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 "string-utils.h"
-
-namespace SciTECO {
-
-/**
- * Canonicalize control characters in str.
- * This converts all control characters to printable
- * characters without tabs, line feeds, etc.
- * Useful for displaying Q-Register names and
- * TECO code.
- */
-gchar *
-String::canonicalize_ctl(const gchar *str)
-{
- gsize ret_len = 1; /* for trailing 0 */
- gchar *ret, *p;
-
- /*
- * Instead of approximating size with strlen()
- * we can just as well calculate it exactly:
- */
- for (const gchar *p = str; *p; p++)
- ret_len += IS_CTL(*p) ? 2 : 1;
-
- p = ret = (gchar *)g_malloc(ret_len);
-
- while (*str) {
- if (IS_CTL(*str)) {
- *p++ = '^';
- *p++ = CTL_ECHO(*str++);
- } else {
- *p++ = *str++;
- }
- }
- *p = '\0';
-
- return ret;
-}
-
-void
-String::get_coord(const gchar *str, gint pos,
- gint &line, gint &column)
-{
- line = column = 1;
-
- for (gint i = 0; i < pos; i++) {
- switch (str[i]) {
- case '\r':
- if (str[i+1] == '\n')
- i++;
- /* fall through */
- case '\n':
- line++;
- column = 1;
- break;
- default:
- column++;
- break;
- }
- }
-}
-
-} /* namespace SciTECO */
diff --git a/src/string-utils.h b/src/string-utils.h
index 8aeb863..40f1b21 100644
--- a/src/string-utils.h
+++ b/src/string-utils.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,101 +14,173 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __STRING_UTILS_H
-#define __STRING_UTILS_H
+#pragma once
#include <string.h>
#include <glib.h>
-namespace SciTECO {
-
-namespace String {
+#include "sciteco.h"
+#include "undo.h"
/**
- * Upper-case ASCII character.
+ * Upper-case SciTECO command character.
*
- * There are implementations in glib and libc,
- * but defining it here ensures it can be inlined.
+ * There are implementations in glib (g_ascii_toupper) and libc,
+ * but this implementation is sufficient for all letters used by SciTECO commands.
*/
static inline gchar
-toupper(gchar chr)
+teco_ascii_toupper(gchar chr)
{
return chr >= 'a' && chr <= 'z' ? chr & ~0x20 : chr;
}
/**
- * Allocate a string containing a single character chr.
+ * An 8-bit clean null-terminated string.
+ *
+ * This is similar to GString, but the container does not need to be allocated
+ * and the allocation length is not stored.
+ * Just like GString, teco_string_t are always null-terminated but at the
+ * same time 8-bit clean (can contain null-characters).
+ *
+ * The API is designed such that teco_string_t operations operate on plain
+ * (null-terminated) C strings, a single character or character array as well as
+ * on other teco_string_t.
+ * Input strings will thus usually be specified using a const gchar * and gsize
+ * and are not necessarily null-terminated.
+ * A target teco_string_t::data is always null-terminated and thus safe to pass
+ * to functions expecting traditional null-terminated C strings if you can
+ * guarantee that it contains no null-character other than the trailing one.
*/
-static inline gchar *
-chrdup(gchar chr)
+typedef struct {
+ /**
+ * g_malloc() or g_string_chunk_insert()-allocated null-terminated string.
+ * The pointer is guaranteed to be non-NULL after initialization.
+ */
+ gchar *data;
+ /** Length of `data` without the trailing null-byte. */
+ gsize len;
+} teco_string_t;
+
+/** @memberof teco_string_t */
+static inline void
+teco_string_init(teco_string_t *target, const gchar *str, gsize len)
{
- gchar *ret = (gchar *)g_malloc(2);
-
- ret[0] = chr;
- ret[1] = '\0';
-
- return ret;
+ target->data = g_malloc(len + 1);
+ memcpy(target->data, str, len);
+ target->len = len;
+ target->data[target->len] = '\0';
}
/**
- * Append null-terminated str2 to non-null-terminated
- * str1 of length str1_size.
- * The result is not null-terminated.
- * This is a very efficient implementation and well
- * suited for appending lots of small strings often.
+ * Allocate a teco_string_t using GStringChunk.
+ *
+ * Such strings must not be freed/cleared individually and it is NOT allowed
+ * to call teco_string_append() and teco_string_truncate() on them.
+ * On the other hand, they are stored faster and more memory efficient.
+ *
+ * @memberof teco_string_t
*/
static inline void
-append(gchar *&str1, gsize str1_size, const gchar *str2)
+teco_string_init_chunk(teco_string_t *target, const gchar *str, gssize len, GStringChunk *chunk)
{
- size_t str2_size = strlen(str2);
- str1 = (gchar *)g_realloc(str1, str1_size + str2_size);
- if (str1)
- memcpy(str1+str1_size, str2, str2_size);
+ target->data = g_string_chunk_insert_len(chunk, str, len);
+ target->len = len;
}
/**
- * Append str2 to str1 (both null-terminated).
- * This is a very efficient implementation and well
- * suited for appending lots of small strings often.
+ * @note Rounding up the length turned out to bring no benefits,
+ * at least with glibc's malloc().
+ *
+ * @memberof teco_string_t
*/
static inline void
-append(gchar *&str1, const gchar *str2)
+teco_string_append(teco_string_t *target, const gchar *str, gsize len)
+{
+ target->data = g_realloc(target->data, target->len + len + 1);
+ memcpy(target->data + target->len, str, len);
+ target->len += len;
+ target->data[target->len] = '\0';
+}
+
+/** @memberof teco_string_t */
+static inline void
+teco_string_append_c(teco_string_t *str, gchar chr)
{
- size_t str1_size = str1 ? strlen(str1) : 0;
- str1 = (gchar *)g_realloc(str1, str1_size + strlen(str2) + 1);
- strcpy(str1+str1_size, str2);
+ teco_string_append(str, &chr, sizeof(chr));
}
/**
- * Append a single character to a null-terminated string.
+ * @fixme Should this also realloc str->data?
+ *
+ * @memberof teco_string_t
*/
static inline void
-append(gchar *&str, gchar chr)
+teco_string_truncate(teco_string_t *str, gsize len)
{
- gchar buf[] = {chr, '\0'};
- append(str, buf);
+ g_assert(len <= str->len);
+ if (len) {
+ str->data[len] = '\0';
+ } else {
+ g_free(str->data);
+ str->data = NULL;
+ }
+ str->len = len;
}
-gchar *canonicalize_ctl(const gchar *str);
+/** @memberof teco_string_t */
+void undo__teco_string_truncate(teco_string_t *, gsize);
+
+gchar *teco_string_echo(const gchar *str, gsize len);
+
+void teco_string_get_coord(const gchar *str, 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);
-void get_coord(const gchar *str, gint pos,
- gint &line, gint &column);
+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);
+
+/** @memberof teco_string_t */
+static inline gboolean
+teco_string_contains(const teco_string_t *str, gchar chr)
+{
+ return memchr(str->data, chr, str->len) != NULL;
+}
-static inline gsize
-diff(const gchar *a, const gchar *b)
+/** @memberof teco_string_t */
+static inline gint
+teco_string_rindex(const teco_string_t *str, gchar chr)
{
- gsize len = 0;
+ gint i;
+ for (i = str->len-1; i >= 0 && str->data[i] != chr; i--);
+ return i;
+}
- while (*a != '\0' && *a++ == *b++)
- len++;
+const gchar *teco_string_last_occurrence(const teco_string_t *str, const gchar *chars);
- return len;
+/** @memberof teco_string_t */
+static inline void
+teco_string_clear(teco_string_t *str)
+{
+ g_free(str->data);
}
-} /* namespace String */
+TECO_DECLARE_UNDO_OBJECT(cstring, gchar *);
+
+#define teco_undo_cstring(VAR) \
+ (*teco_undo_object_cstring_push(&(VAR)))
+
+TECO_DECLARE_UNDO_OBJECT(string, teco_string_t);
+
+#define teco_undo_string(VAR) \
+ (*teco_undo_object_string_push(&(VAR)))
+
+TECO_DECLARE_UNDO_OBJECT(string_own, teco_string_t);
-} /* namespace SciTECO */
+#define teco_undo_string_own(VAR) \
+ (*teco_undo_object_string_own_push(&(VAR)))
-#endif
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(teco_string_t, teco_string_clear);
diff --git a/src/symbols-extract.tes b/src/symbols-extract.tes
index 7d12ea8..3f71629 100755
--- a/src/symbols-extract.tes
+++ b/src/symbols-extract.tes
@@ -33,20 +33,28 @@ I/*
#include "Q#in"
#include "sciteco.h"
-#include "symbols.h"
+#include "scintilla.h"
-namespace SciTECO {
-
-static const SymbolList::Entry entries[] = {^J
+static const teco_symbol_entry_t entries[] = {^J
<
.,W.Xa 0KK
I#ifdef Qa^J^I{"Qa", Qa},^J#endif^J
.-Z;>
I};
-SymbolList Symbols::Q[getopt.n](entries, G_N_ELEMENTS(entries));
-
-} /* namespace SciTECO */^J
+static void __attribute__((constructor))
+teco_symbols_init(void)
+{
+ teco_symbol_list_init(&Q[getopt.n], entries, G_N_ELEMENTS(entries), FALSE);
+}
+
+#ifndef NDEBUG
+static void __attribute__((destructor))
+teco_cmdline_cleanup(void)
+{
+ teco_symbol_list_clear(&Q[getopt.n]);
+}
+#endif^J
!* write output file *!
EWQ#ou
diff --git a/src/symbols.cpp b/src/symbols.cpp
deleted file mode 100644
index 51046d4..0000000
--- a/src/symbols.cpp
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <string.h>
-
-#include <glib.h>
-
-#include "sciteco.h"
-#include "symbols.h"
-
-namespace SciTECO {
-
-/*
- * Since symbol lists are presorted constant arrays we can do a simple
- * binary search.
- */
-gint
-SymbolList::lookup(const gchar *name, const gchar *prefix)
-{
- gint prefix_skip = strlen(prefix);
- gint name_len = strlen(name);
-
- gint left = 0;
- gint right = size - 1;
-
- if (!cmp_fnc(name, prefix, prefix_skip))
- prefix_skip = 0;
-
- while (left <= right) {
- gint cur = left + (right-left)/2;
- gint cmp = cmp_fnc(entries[cur].name + prefix_skip,
- name, name_len + 1);
-
- if (!cmp)
- return entries[cur].value;
-
- if (cmp > 0)
- right = cur-1;
- else /* cmp < 0 */
- left = cur+1;
- }
-
- return -1;
-}
-
-GList *
-SymbolList::get_glist(void)
-{
- if (!list) {
- for (gint i = size; i; i--)
- list = g_list_prepend(list, (gchar *)entries[i-1].name);
- }
-
- return list;
-}
-
-} /* namespace SciTECO */
diff --git a/src/symbols.h b/src/symbols.h
deleted file mode 100644
index c7a9c7f..0000000
--- a/src/symbols.h
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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/>.
- */
-
-#ifndef __SYMBOLS_H
-#define __SYMBOLS_H
-
-#include <string.h>
-#include <glib.h>
-
-#include "memory.h"
-
-namespace SciTECO {
-
-class SymbolList : public Object {
-public:
- struct Entry {
- const gchar *name;
- gint value;
- };
-
-private:
- const Entry *entries;
- gint size;
- int (*cmp_fnc)(const char *, const char *, size_t);
-
- /* for auto-completions */
- GList *list;
-
-public:
- SymbolList(const Entry *_entries = NULL, gint _size = 0,
- bool case_sensitive = false)
- : entries(_entries), size(_size), list(NULL)
- {
- cmp_fnc = case_sensitive ? strncmp
- : g_ascii_strncasecmp;
- }
-
- ~SymbolList()
- {
- g_list_free(list);
- }
-
- gint lookup(const gchar *name, const gchar *prefix = "");
- GList *get_glist(void);
-};
-
-/* objects declared in symbols-minimal.cpp or auto-generated code */
-namespace Symbols {
- extern SymbolList scintilla;
- extern SymbolList scilexer;
-}
-
-} /* namespace SciTECO */
-
-#endif
diff --git a/src/undo.c b/src/undo.c
new file mode 100644
index 0000000..10e438f
--- /dev/null
+++ b/src/undo.c
@@ -0,0 +1,163 @@
+/*
+ * 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 <stdio.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include "sciteco.h"
+#include "cmdline.h"
+#include "undo.h"
+
+//#define DEBUG
+
+TECO_DEFINE_UNDO_SCALAR(gchar);
+TECO_DEFINE_UNDO_SCALAR(gint);
+TECO_DEFINE_UNDO_SCALAR(guint);
+TECO_DEFINE_UNDO_SCALAR(gsize);
+TECO_DEFINE_UNDO_SCALAR(teco_int_t);
+TECO_DEFINE_UNDO_SCALAR(gboolean);
+TECO_DEFINE_UNDO_SCALAR(gconstpointer);
+
+/**
+ * An undo token.
+ *
+ * Undo tokens are generated to revert any
+ * changes to the editor state, ie. they
+ * define an action to take upon rubout.
+ *
+ * Undo tokens are organized into an undo stack.
+ */
+typedef struct teco_undo_token_t {
+ struct teco_undo_token_t *next;
+ teco_undo_action_t action_cb;
+ guint8 user_data[];
+} teco_undo_token_t;
+
+/**
+ * Stack of teco_undo_token_t lists.
+ *
+ * Each stack element represents
+ * a command line character (the undo tokens
+ * generated by that character), so it's OK
+ * to use a data structure that may need
+ * reallocation but is space efficient.
+ * This data structure allows us to omit the
+ * command line program counter from teco_undo_token_t
+ * but wastes a few bytes for input characters
+ * that produce no undo tokens (e.g. NOPs like space).
+ */
+static GPtrArray *teco_undo_heads;
+
+gboolean teco_undo_enabled = FALSE;
+
+static void __attribute__((constructor))
+teco_undo_init(void)
+{
+ teco_undo_heads = g_ptr_array_new();
+}
+
+/**
+ * Allocate and push undo token.
+ *
+ * This does nothing if undo is disabled and should
+ * not be used when ownership of some data is to be
+ * passed to the undo token.
+ */
+gpointer
+teco_undo_push_size(teco_undo_action_t action_cb, gsize size)
+{
+ if (!teco_undo_enabled)
+ return NULL;
+
+ teco_undo_token_t *token = g_malloc(sizeof(teco_undo_token_t) + size);
+ token->action_cb = action_cb;
+
+#ifdef DEBUG
+ g_printf("UNDO PUSH %p\n", token);
+#endif
+
+ /*
+ * There can very well be 0 undo tokens
+ * per input character (e.g. NOPs like space).
+ */
+ while (teco_undo_heads->len <= teco_cmdline.pc)
+ g_ptr_array_add(teco_undo_heads, NULL);
+ g_assert(teco_undo_heads->len == teco_cmdline.pc+1);
+
+ token->next = g_ptr_array_index(teco_undo_heads,
+ teco_undo_heads->len-1);
+ g_ptr_array_index(teco_undo_heads, teco_undo_heads->len-1) = token;
+
+ return token->user_data;
+}
+
+void
+teco_undo_pop(gint pc)
+{
+ while ((gint)teco_undo_heads->len > pc) {
+ teco_undo_token_t *top =
+ g_ptr_array_remove_index(teco_undo_heads,
+ teco_undo_heads->len-1);
+
+ while (top) {
+ teco_undo_token_t *next = top->next;
+
+#ifdef DEBUG
+ g_printf("UNDO POP %p\n", top);
+ fflush(stdout);
+#endif
+ top->action_cb(top->user_data, TRUE);
+
+ g_free(top);
+ top = next;
+ }
+ }
+}
+
+void
+teco_undo_clear(void)
+{
+ while (teco_undo_heads->len) {
+ teco_undo_token_t *top =
+ g_ptr_array_remove_index(teco_undo_heads,
+ teco_undo_heads->len-1);
+
+ while (top) {
+ teco_undo_token_t *next = top->next;
+ top->action_cb(top->user_data, FALSE);
+ g_free(top);
+ top = next;
+ }
+ }
+}
+
+/*
+ * NOTE: This destructor should always be run, even with NDEBUG,
+ * as there are undo tokens that release more than memory (e.g. files).
+ */
+static void __attribute__((destructor))
+teco_undo_cleanup(void)
+{
+ teco_undo_clear();
+ g_ptr_array_free(teco_undo_heads, TRUE);
+}
diff --git a/src/undo.cpp b/src/undo.cpp
deleted file mode 100644
index 468c61a..0000000
--- a/src/undo.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2012-2017 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 <stdio.h>
-#include <bsd/sys/queue.h>
-
-#include <glib.h>
-#include <glib/gstdio.h>
-
-#include <Scintilla.h>
-
-#include "sciteco.h"
-#include "cmdline.h"
-#include "undo.h"
-
-namespace SciTECO {
-
-//#define DEBUG
-
-UndoStack undo;
-
-void
-UndoStack::push(UndoToken *token)
-{
- /*
- * All undo token allocations should take place using the
- * variadic template version of UndoStack::push(), so we
- * don't have to check `enabled` here.
- */
- g_assert(enabled == true);
-
-#ifdef DEBUG
- g_printf("UNDO PUSH %p\n", token);
-#endif
-
- /*
- * There can very well be 0 undo tokens
- * per input character (e.g. NOPs like space).
- */
- while (heads->len <= cmdline.pc)
- g_ptr_array_add(heads, NULL);
- g_assert(heads->len == cmdline.pc+1);
-
- SLIST_NEXT(token, tokens) =
- (UndoToken *)g_ptr_array_index(heads, heads->len-1);
- g_ptr_array_index(heads, heads->len-1) = token;
-}
-
-void
-UndoStack::pop(gint pc)
-{
- while ((gint)heads->len > pc) {
- UndoToken *top =
- (UndoToken *)g_ptr_array_remove_index(heads, heads->len-1);
-
- while (top) {
- UndoToken *next = SLIST_NEXT(top, tokens);
-
-#ifdef DEBUG
- g_printf("UNDO POP %p\n", top);
- fflush(stdout);
-#endif
- top->run();
-
- delete top;
- top = next;
- }
- }
-}
-
-void
-UndoStack::clear(void)
-{
- while (heads->len) {
- UndoToken *top =
- (UndoToken *)g_ptr_array_remove_index(heads, heads->len-1);
-
- while (top) {
- UndoToken *next = SLIST_NEXT(top, tokens);
- delete top;
- top = next;
- }
- }
-}
-
-} /* namespace SciTECO */
diff --git a/src/undo.h b/src/undo.h
index 93f68e5..fc5cccb 100644
--- a/src/undo.h
+++ b/src/undo.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2017 Robin Haberkorn
+ * 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
@@ -14,227 +14,234 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-
-#ifndef __UNDO_H
-#define __UNDO_H
-
-#include <string.h>
-
-#include <bsd/sys/queue.h>
+#pragma once
#include <glib.h>
-#include <glib/gprintf.h>
-#include "memory.h"
+#include "sciteco.h"
-#ifdef DEBUG
-#include "parser.h"
-#endif
-
-namespace SciTECO {
+extern gboolean teco_undo_enabled;
/**
- * Undo tokens are generated to revert any
- * changes to the editor state, ie. they
- * define an action to take upon rubout.
+ * A callback to be invoked when an undo token gets executed or cleaned up.
+ *
+ * @note Unless you want to cast user_data in every callback implementation,
+ * you may want to cast your callback type instead to teco_undo_action_t.
+ * Casting to functions of different signature is theoretically undefined behavior,
+ * but works on all major platforms including Emscripten, as long as they differ only
+ * in pointer types.
*
- * Undo tokens are organized into an undo
- * stack.
+ * @param user_data
+ * The data allocated by teco_undo_push_size() (usually a context structure).
+ * You are supposed to free any external resources (heap pointers etc.) referenced
+ * from it till the end of the callback.
+ * @param run
+ * Whether the operation should actually be performed instead of merely freeing
+ * the associated memory.
*/
-class UndoToken : public Object {
-public:
- SLIST_ENTRY(UndoToken) tokens;
-
- virtual ~UndoToken() {}
-
- virtual void run(void) = 0;
-};
-
-template <typename Type>
-class UndoTokenVariable : public UndoToken {
- Type *ptr;
- Type value;
-
-public:
- UndoTokenVariable(Type &variable, Type _value)
- : ptr(&variable), value(_value) {}
-
- void
- run(void)
- {
-#ifdef DEBUG
- if ((State **)ptr == &States::current)
- g_printf("undo state -> %p\n", (void *)value);
-#endif
- *ptr = value;
- }
-};
+typedef void (*teco_undo_action_t)(gpointer user_data, gboolean run);
-class UndoTokenString : public UndoToken {
- gchar **ptr;
- gchar *str;
+gpointer teco_undo_push_size(teco_undo_action_t action_cb, gsize size)
+ G_GNUC_ALLOC_SIZE(2);
-public:
- UndoTokenString(gchar *&variable, gchar *_str)
- : ptr(&variable)
- {
- str = _str ? g_strdup(_str) : NULL;
- }
+#define teco_undo_push(NAME) \
+ ((NAME##_t *)teco_undo_push_size((teco_undo_action_t)NAME##_action, \
+ sizeof(NAME##_t)))
- ~UndoTokenString()
- {
- g_free(str);
+/**
+ * @defgroup undo_objects Undo objects
+ *
+ * @note
+ * The general meta programming approach here is similar to C++ explicit template
+ * instantiation.
+ * A macro is expanded for every object type into some compilation unit and a declaration
+ * into the corresponding header.
+ * The object's type is mangled into the generated "push"-function's name.
+ * In case of scalars, C11 Generics and some macro magic is then used to hide the
+ * type names and for "reference" style passing.
+ *
+ * Explicit instantiation could be theoretically avoided using GCC compound expressions
+ * and nested functions. However, GCC will inevitably generate trampolines which
+ * are unportable and induce a runtime penalty.
+ * Furthermore, nested functions are not supported by Clang, where the Blocks extension
+ * would have to be used instead.
+ * Another alternative for implicit instantiation would be preprocessing of all source
+ * files with some custom M4 macros.
+ */
+/*
+ * FIXME: Due to the requirements on the variable, we could be tempted to inline
+ * references to it directly into the action()-function, saving the `ptr`
+ * in the undo token. This is however often practically not possible.
+ * We could however add a variant for true global variables.
+ *
+ * FIXME: Sometimes, the push-function is used only in a single compilation unit,
+ * so it should be declared `static` or `static inline`.
+ * Is it worth complicating our APIs in order to support that?
+ *
+ * FIXME: Perhaps better split this into TECO_DEFINE_UNDO_OBJECT() and TECO_DEFINE_UNDO_OBJECT_OWN()
+ */
+#define __TECO_DEFINE_UNDO_OBJECT(NAME, TYPE, COPY, DELETE, DELETE_IF_DISABLED, DELETE_ON_RUN) \
+ typedef struct { \
+ TYPE *ptr; \
+ TYPE value; \
+ } teco_undo_object_##NAME##_t; \
+ \
+ static void \
+ teco_undo_object_##NAME##_action(teco_undo_object_##NAME##_t *ctx, gboolean run) \
+ { \
+ if (run) { \
+ DELETE_ON_RUN(*ctx->ptr); \
+ *ctx->ptr = ctx->value; \
+ } else { \
+ DELETE(ctx->value); \
+ } \
+ } \
+ \
+ /** @ingroup undo_objects */ \
+ TYPE * \
+ teco_undo_object_##NAME##_push(TYPE *ptr) \
+ { \
+ teco_undo_object_##NAME##_t *ctx = teco_undo_push(teco_undo_object_##NAME); \
+ if (ctx) { \
+ ctx->ptr = ptr; \
+ ctx->value = COPY(*ptr); \
+ } else { \
+ DELETE_IF_DISABLED(*ptr); \
+ } \
+ return ptr; \
}
- void
- run(void)
- {
- g_free(*ptr);
- *ptr = str;
- str = NULL;
- }
-};
+/**
+ * Defines an undo token push function that when executed restores
+ * the value/state of a variable of TYPE to the value it had when this
+ * was called.
+ *
+ * This can be used to undo changes to arbitrary variables, either
+ * requiring explicit memory handling or to scalars.
+ *
+ * The lifetime of the variable must be global - a pointer to it must be valid
+ * until the undo token could be executed.
+ * This will usually exclude stack-allocated variables or objects.
+ *
+ * @param NAME C identifier used for name mangling.
+ * @param TYPE Type of variable to restore.
+ * @param COPY A global function/expression to execute in order to copy VAR.
+ * If left empty, this is an identity operation and ownership
+ * of the variable is passed to the undo token.
+ * @param DELETE A global function/expression to execute in order to destruct
+ * objects of TYPE. Leave empty if destruction is not necessary.
+ *
+ * @ingroup undo_objects
+ */
+#define TECO_DEFINE_UNDO_OBJECT(NAME, TYPE, COPY, DELETE) \
+ __TECO_DEFINE_UNDO_OBJECT(NAME, TYPE, COPY, DELETE, /* don't delete if disabled */, DELETE)
+/**
+ * @fixme _OWN variants will invalidate the variable pointer, so perhaps
+ * it will be clearer to have _SET variants instead.
+ *
+ * @ingroup undo_objects
+ */
+#define TECO_DEFINE_UNDO_OBJECT_OWN(NAME, TYPE, DELETE) \
+ __TECO_DEFINE_UNDO_OBJECT(NAME, TYPE, /* pass ownership */, DELETE, DELETE, /* don't delete if run */)
-template <class Type>
-class UndoTokenObject : public UndoToken {
- Type **ptr;
- Type *obj;
+/** @ingroup undo_objects */
+#define TECO_DECLARE_UNDO_OBJECT(NAME, TYPE) \
+ TYPE *teco_undo_object_##NAME##_push(TYPE *ptr)
-public:
- UndoTokenObject(Type *&variable, Type *_obj)
- : ptr(&variable), obj(_obj) {}
+/** @ingroup undo_objects */
+#define TECO_DEFINE_UNDO_SCALAR(TYPE) \
+ TECO_DEFINE_UNDO_OBJECT_OWN(TYPE, TYPE, /* don't delete */)
- ~UndoTokenObject()
- {
- delete obj;
- }
+/** @ingroup undo_objects */
+#define TECO_DECLARE_UNDO_SCALAR(TYPE) \
+ TECO_DECLARE_UNDO_OBJECT(TYPE, TYPE)
- void
- run(void)
- {
- delete *ptr;
- *ptr = obj;
- obj = NULL;
- }
-};
-
-extern class UndoStack : public Object {
- /**
- * Stack of UndoToken lists.
- *
- * Each stack element represents
- * a command line character (the UndoTokens
- * generated by that character), so it's OK
- * to use a data structure that may need
- * reallocation but is space efficient.
- * This data structure allows us to omit the
- * command line program counter from the UndoTokens
- * but wastes a few bytes for input characters
- * that produce no UndoToken (e.g. NOPs like space).
- */
- GPtrArray *heads;
-
- void push(UndoToken *token);
-
-public:
- bool enabled;
-
- UndoStack(bool _enabled = false)
- : heads(g_ptr_array_new()), enabled(_enabled) {}
- ~UndoStack()
- {
- clear();
- g_ptr_array_free(heads, TRUE);
- }
+/*
+ * FIXME: We had to add -Wno-unused-value to surpress warnings.
+ * Perhaps it's clearer to sacrifice the lvalue feature.
+ *
+ * TODO: Check whether generating an additional check on teco_undo_enabled here
+ * significantly improves batch-mode performance.
+ */
+TECO_DECLARE_UNDO_SCALAR(gchar);
+#define teco_undo_gchar(VAR) (*teco_undo_object_gchar_push(&(VAR)))
- /**
- * Allocate and push undo token.
- *
- * This does nothing if undo is disabled and should
- * not be used when ownership of some data is to be
- * passed to the undo token.
- */
- template <class TokenType, typename... Params>
- inline void
- push(Params && ... params)
- {
- if (enabled)
- push(new TokenType(params...));
- }
+TECO_DECLARE_UNDO_SCALAR(gint);
+#define teco_undo_gint(VAR) (*teco_undo_object_gint_push(&(VAR)))
- /**
- * Allocate and push undo token, passing ownership.
- *
- * This creates and deletes the undo token cheaply
- * if undo is disabled, so that data whose ownership
- * is passed to the undo token is correctly reclaimed.
- *
- * @bug We must know which version of push to call
- * depending on the token type. This could be hidden
- * if UndoTokens had static push methods that take care
- * of reclaiming memory.
- */
- template <class TokenType, typename... Params>
- inline void
- push_own(Params && ... params)
- {
- if (enabled) {
- push(new TokenType(params...));
- } else {
- /* ensures that all memory is reclaimed */
- TokenType dummy(params...);
- }
- }
+TECO_DECLARE_UNDO_SCALAR(guint);
+#define teco_undo_guint(VAR) (*teco_undo_object_guint_push(&(VAR)))
- template <typename Type>
- inline Type &
- push_var(Type &variable, Type value)
- {
- push<UndoTokenVariable<Type>>(variable, value);
- return variable;
- }
+TECO_DECLARE_UNDO_SCALAR(gsize);
+#define teco_undo_gsize(VAR) (*teco_undo_object_gsize_push(&(VAR)))
- template <typename Type>
- inline Type &
- push_var(Type &variable)
- {
- return push_var<Type>(variable, variable);
- }
+TECO_DECLARE_UNDO_SCALAR(teco_int_t);
+#define teco_undo_int(VAR) (*teco_undo_object_teco_int_t_push(&(VAR)))
- inline gchar *&
- push_str(gchar *&variable, gchar *str)
- {
- push<UndoTokenString>(variable, str);
- return variable;
- }
- inline gchar *&
- push_str(gchar *&variable)
- {
- return push_str(variable, variable);
- }
+TECO_DECLARE_UNDO_SCALAR(gboolean);
+#define teco_undo_gboolean(VAR) (*teco_undo_object_gboolean_push(&(VAR)))
- template <class Type>
- inline Type *&
- push_obj(Type *&variable, Type *obj)
- {
- /* pass ownership of original object */
- push_own<UndoTokenObject<Type>>(variable, obj);
- return variable;
- }
+TECO_DECLARE_UNDO_SCALAR(gconstpointer);
+#define teco_undo_ptr(VAR) \
+ (*(typeof(VAR) *)teco_undo_object_gconstpointer_push((gconstpointer *)&(VAR)))
- template <class Type>
- inline Type *&
- push_obj(Type *&variable)
- {
- return push_obj<Type>(variable, variable);
- }
+#define __TECO_GEN_STRUCT(ID, X) X arg_##ID;
+//#define __TECO_GEN_ARG(ID, X) X arg_##ID,
+//#define __TECO_GEN_ARG_LAST(ID, X) X arg_##ID
+#define __TECO_GEN_CALL(ID, X) ctx->arg_##ID,
+#define __TECO_GEN_CALL_LAST(ID, X) ctx->arg_##ID
+#define __TECO_GEN_INIT(ID, X) ctx->arg_##ID = arg_##ID;
- void pop(gint pc);
+/**
+ * @defgroup undo_calls Function calls on rubout.
+ * @{
+ */
- void clear(void);
-} undo;
+/**
+ * Create an undo token that calls FNC with arbitrary scalar parameters
+ * (maximum 5, captured at the time of the call).
+ * It defines a function undo__FNC() for actually creating the closure.
+ *
+ * All arguments must be constants or expressions evaluating to scalars though,
+ * since no memory management (copying/freeing) is performed.
+ *
+ * Tipp: In order to save memory in the undo token structures, it is
+ * often trivial to define a static inline function that calls FNC and binds
+ * "constant" parameters.
+ *
+ * @param FNC Name of a global function or macro to execute.
+ * It must be a plain C identifier.
+ * @param ... The parameter types of FNC (signature).
+ * Only the types without any variable names must be specified.
+ *
+ * @fixme Sometimes, the push-function is used only in a single compilation unit,
+ * so it should be declared `static` or `static inline`.
+ * Is it worth complicating our APIs in order to support that?
+ */
+#define TECO_DEFINE_UNDO_CALL(FNC, ...) \
+ typedef struct { \
+ TECO_FOR_EACH(__TECO_GEN_STRUCT, __TECO_GEN_STRUCT, ##__VA_ARGS__) \
+ } teco_undo_call_##FNC##_t; \
+ \
+ static void \
+ teco_undo_call_##FNC##_action(teco_undo_call_##FNC##_t *ctx, gboolean run) \
+ { \
+ if (run) \
+ FNC(TECO_FOR_EACH(__TECO_GEN_CALL, __TECO_GEN_CALL_LAST, ##__VA_ARGS__)); \
+ } \
+ \
+ /** @ingroup undo_calls */ \
+ void \
+ undo__##FNC(TECO_FOR_EACH(__TECO_GEN_ARG, __TECO_GEN_ARG_LAST, ##__VA_ARGS__)) \
+ { \
+ teco_undo_call_##FNC##_t *ctx = teco_undo_push(teco_undo_call_##FNC); \
+ if (ctx) { \
+ TECO_FOR_EACH(__TECO_GEN_INIT, __TECO_GEN_INIT, ##__VA_ARGS__) \
+ } \
+ }
-} /* namespace SciTECO */
+/** @} */
-#endif
+void teco_undo_pop(gint pc);
+void teco_undo_clear(void);
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;
+}
diff --git a/src/view.h b/src/view.h
new file mode 100644
index 0000000..b666d0a
--- /dev/null
+++ b/src/view.h
@@ -0,0 +1,75 @@
+/*
+ * 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/>.
+ */
+#pragma once
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+
+/**
+ * @interface teco_view_t
+ * Interface for all SciTECO views.
+ *
+ * Methods that must still be implemented in the user-interface
+ * layer are marked with the \@pure tag.
+ */
+typedef struct teco_view_t teco_view_t;
+
+/** @pure @static @memberof teco_view_t */
+teco_view_t *teco_view_new(void);
+
+void teco_view_setup(teco_view_t *ctx);
+
+/** @pure @memberof teco_view_t */
+sptr_t teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam);
+
+/** @memberof teco_view_t */
+void undo__teco_view_ssm(teco_view_t *, unsigned int, uptr_t, sptr_t);
+
+void teco_view_set_representations(teco_view_t *ctx);
+
+/** @memberof teco_view_t */
+void undo__teco_view_set_representations(teco_view_t *);
+
+/** @memberof teco_view_t */
+static inline void
+teco_view_set_scintilla_undo(teco_view_t *ctx, gboolean state)
+{
+ teco_view_ssm(ctx, SCI_EMPTYUNDOBUFFER, 0, 0);
+ 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);
+
+/** @memberof teco_view_t */
+#define teco_view_load(CTX, FROM, ERROR) \
+ (_Generic((FROM), GIOChannel * : teco_view_load_from_channel, \
+ const gchar * : teco_view_load_from_file)((CTX), (FROM), (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);
+
+/** @memberof teco_view_t */
+#define teco_view_save(CTX, TO, ERROR) \
+ (_Generic((TO), GIOChannel * : teco_view_save_to_channel, \
+ const gchar * : teco_view_save_to_file)((CTX), (TO), (ERROR)))
+
+/** @pure @memberof teco_view_t */
+void teco_view_free(teco_view_t *ctx);