diff options
Diffstat (limited to 'src')
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, ¯o_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, ¯o_str.data, ¯o_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 */ @@ -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); @@ -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 */ @@ -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 +); @@ -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(¯o_locals, FALSE); + + guint parent_brace_level = teco_brace_level; + + g_auto(teco_machine_main_t) macro_machine; + teco_machine_main_init(¯o_machine, qreg_table_locals ? : ¯o_locals, FALSE); + + GError *tmp_error = NULL; + + if (!teco_machine_main_step(¯o_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(¯o_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(¯o_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(¯o_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, ¯o.data, ¯o.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 = ¯o_goto_table; - - /* - * Locals are only initialized when needed to - * improve the speed of local macro calls. - */ - if (locals) { - macro_locals.insert_defaults(); - QRegisters::locals = ¯o_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, ¯o_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, ¯o.data, ¯o.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(®->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 ®) -{ - 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 ®) -{ - 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 ®) -{ - 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 ®) -{ - 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 ®) -{ - 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 ®) -{ - 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 ®) -{ - 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 ®) - { - string.exchange(reg.string); - } - virtual void undo_exchange_string(QRegisterData ®); - - /* - * 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 ®); - void undo_exchange_string(QRegisterData ®); -}; - -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 ®); - void undo_exchange_string(QRegisterData ®); -}; - -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 ®); - bool pop(QRegister ®); -}; - -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 */ @@ -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, ®, 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 */ @@ -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); |