aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/core-commands.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core-commands.c')
-rw-r--r--src/core-commands.c2510
1 files changed, 2510 insertions, 0 deletions
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
+);