aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorRobin Haberkorn <robin.haberkorn@googlemail.com>2012-12-04 17:29:01 +0100
committerRobin Haberkorn <robin.haberkorn@googlemail.com>2012-12-04 22:07:08 +0100
commitd8a316514c03d85b771a9dce4a8a51b875d955b3 (patch)
tree8966c29db767a155848f6d90f76771ce5b9de32e /src
parentb120616b6da52e951097f69ad267de06081d218a (diff)
downloadsciteco-d8a316514c03d85b771a9dce4a8a51b875d955b3.tar.gz
autoconf preparation: move everything into src/ subdir
Diffstat (limited to 'src')
-rw-r--r--src/cmdline.cpp371
-rw-r--r--src/expressions.cpp201
-rw-r--r--src/expressions.h194
-rw-r--r--src/goto.cpp147
-rw-r--r--src/goto.h107
-rw-r--r--src/gtk-info-popup.gob165
-rw-r--r--src/interface-gtk.cpp248
-rw-r--r--src/interface-gtk.h73
-rw-r--r--src/interface-ncurses.cpp426
-rw-r--r--src/interface-ncurses.h69
-rw-r--r--src/interface.h112
-rw-r--r--src/main.cpp208
-rw-r--r--src/parser.cpp1436
-rw-r--r--src/parser.h266
-rw-r--r--src/qregisters.cpp522
-rw-r--r--src/qregisters.h343
-rw-r--r--src/rbtree.cpp5
-rw-r--r--src/rbtree.h282
-rw-r--r--src/ring.cpp539
-rw-r--r--src/ring.h246
-rw-r--r--src/sciteco.h76
-rw-r--r--src/search.cpp571
-rw-r--r--src/search.h102
-rwxr-xr-xsrc/symbols-extract.tes79
-rw-r--r--src/symbols.cpp60
-rw-r--r--src/symbols.h38
-rw-r--r--src/undo.cpp78
-rw-r--r--src/undo.h134
28 files changed, 7098 insertions, 0 deletions
diff --git a/src/cmdline.cpp b/src/cmdline.cpp
new file mode 100644
index 0000000..212722a
--- /dev/null
+++ b/src/cmdline.cpp
@@ -0,0 +1,371 @@
+#include <string.h>
+#include <stdlib.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include "sciteco.h"
+#include "interface.h"
+#include "expressions.h"
+#include "parser.h"
+#include "qregisters.h"
+#include "ring.h"
+#include "goto.h"
+#include "undo.h"
+#include "symbols.h"
+
+static inline const gchar *process_edit_cmd(gchar key);
+static gchar *macro_echo(const gchar *macro);
+
+static gchar *filename_complete(const gchar *filename, gchar completed = ' ');
+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);
+
+gchar *cmdline = NULL;
+
+bool quit_requested = false;
+
+void
+cmdline_keypress(gchar key)
+{
+ gchar *cmdline_p;
+ const gchar *insert;
+ gchar *echo;
+
+ /*
+ * Cleanup messages, popups, etc...
+ */
+ interface.popup_clear();
+ interface.msg_clear();
+
+ /*
+ * Process immediate editing commands
+ */
+ insert = process_edit_cmd(key);
+
+ /*
+ * Parse/execute characters
+ */
+ if (cmdline) {
+ gint len = strlen(cmdline);
+ cmdline = (gchar *)g_realloc(cmdline, len + strlen(insert) + 1);
+ cmdline_p = cmdline + len;
+ } else {
+ cmdline = (gchar *)g_malloc(strlen(insert) + 1);
+ *cmdline = '\0';
+ cmdline_p = cmdline;
+ }
+
+ /*
+ * Execute one insertion character, extending cmdline, at a time so
+ * undo tokens get emitted for the corresponding characters.
+ */
+ for (const gchar *p = insert; *p; p++) {
+ *cmdline_p++ = *p;
+ *cmdline_p = '\0';
+
+ try {
+ Execute::step(cmdline);
+ } catch (...) {
+ /*
+ * 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(cmdline_p - cmdline);
+ cmdline_p[-1] = '\0';
+ break;
+ }
+ }
+
+ /*
+ * Echo command line
+ */
+ echo = macro_echo(cmdline);
+ interface.cmdline_update(echo);
+ g_free(echo);
+}
+
+static inline const gchar *
+process_edit_cmd(gchar key)
+{
+ static gchar insert[255];
+ gint cmdline_len = cmdline ? strlen(cmdline) : 0;
+
+ insert[0] = '\0';
+
+ switch (key) {
+ case '\b':
+ if (cmdline_len) {
+ undo.pop(cmdline_len);
+ cmdline[cmdline_len - 1] = '\0';
+ macro_pc--;
+ }
+ break;
+
+ case CTL_KEY('T'): {
+ const gchar *filename = cmdline ? last_occurrence(cmdline) + 1
+ : NULL;
+ gchar *new_chars = filename_complete(filename);
+ if (new_chars)
+ g_stpcpy(insert, new_chars);
+ g_free(new_chars);
+ break;
+ }
+
+ case '\t':
+ if (States::current == &States::editfile ||
+ States::current == &States::savefile ||
+ States::current == &States::loadqreg) {
+ gchar complete = escape_char == '{' ? ' ' : escape_char;
+ gchar *new_chars = filename_complete(strings[0],
+ complete);
+ if (new_chars)
+ g_stpcpy(insert, new_chars);
+ g_free(new_chars);
+ } else if (States::current == &States::scintilla_symbols) {
+ const gchar *symbol = NULL;
+ SymbolList &list = Symbols::scintilla;
+ gchar *new_chars;
+
+ if (strings[0]) {
+ symbol = last_occurrence(strings[0], ",");
+ if (*symbol == ',') {
+ symbol++;
+ list = Symbols::scilexer;
+ }
+ }
+
+ new_chars = symbol_complete(list, symbol, ',');
+ if (new_chars)
+ g_stpcpy(insert, new_chars);
+ g_free(new_chars);
+ } else {
+ insert[0] = key;
+ insert[1] = '\0';
+ }
+ break;
+
+ case '\x1B':
+ if (States::current == &States::start &&
+ cmdline && cmdline[cmdline_len - 1] == '\x1B') {
+ if (Goto::skip_label) {
+ interface.msg(Interface::MSG_ERROR,
+ "Label \"%s\" not found",
+ Goto::skip_label);
+ break;
+ }
+
+ if (quit_requested) {
+ /* FIXME */
+ exit(EXIT_SUCCESS);
+ }
+
+ undo.clear();
+ interface.ssm(SCI_EMPTYUNDOBUFFER);
+ Goto::table->clear();
+ expressions.clear();
+
+ *cmdline = '\0';
+ macro_pc = 0;
+ break;
+ }
+ /* fall through */
+ default:
+ insert[0] = key;
+ insert[1] = '\0';
+ }
+
+ return insert;
+}
+
+static gchar *
+macro_echo(const gchar *macro)
+{
+ gchar *result, *rp;
+
+ if (!macro)
+ return g_strdup("");
+
+ rp = result = (gchar *)g_malloc(strlen(macro)*5 + 1);
+
+ for (const gchar *p = macro; *p; p++) {
+ switch (*p) {
+ case '\x1B':
+ *rp++ = '$';
+ break;
+ case '\r':
+ rp = g_stpcpy(rp, "<CR>");
+ break;
+ case '\n':
+ rp = g_stpcpy(rp, "<LF>");
+ break;
+ case '\t':
+ rp = g_stpcpy(rp, "<TAB>");
+ break;
+ default:
+ if (IS_CTL(*p)) {
+ *rp++ = '^';
+ *rp++ = CTL_ECHO(*p);
+ } else {
+ *rp++ = *p;
+ }
+ }
+ }
+ *rp = '\0';
+
+ return result;
+}
+
+static gchar *
+filename_complete(const gchar *filename, gchar completed)
+{
+ gchar *dirname, *basename;
+ GDir *dir;
+ GList *files = NULL, *matching;
+ GCompletion *completion;
+ gchar *new_prefix;
+ gchar *insert = NULL;
+
+ if (!filename)
+ filename = "";
+
+ if (is_glob_pattern(filename))
+ return NULL;
+
+ dirname = g_path_get_dirname(filename);
+ dir = g_dir_open(dirname, 0, NULL);
+ if (!dir) {
+ g_free(dirname);
+ return NULL;
+ }
+ if (*dirname != *filename)
+ *dirname = '\0';
+
+ while ((basename = (gchar *)g_dir_read_name(dir))) {
+ gchar *filename = g_build_filename(dirname, basename, NULL);
+
+ if (g_file_test(filename, G_FILE_TEST_IS_DIR)) {
+ gchar *new_filename;
+ new_filename = g_strconcat(filename,
+ G_DIR_SEPARATOR_S, NULL);
+ g_free(filename);
+ filename = new_filename;
+ }
+
+ files = g_list_prepend(files, filename);
+ }
+
+ g_free(dirname);
+ g_dir_close(dir);
+
+ completion = g_completion_new(NULL);
+ g_completion_add_items(completion, files);
+
+ matching = g_completion_complete(completion, filename, &new_prefix);
+ if (new_prefix && strlen(new_prefix) > strlen(filename))
+ insert = g_strdup(new_prefix + strlen(filename));
+ g_free(new_prefix);
+
+ if (!insert && g_list_length(matching) > 1) {
+ matching = g_list_sort(matching, (GCompareFunc)g_strcmp0);
+
+ for (GList *file = g_list_first(matching);
+ file != NULL;
+ file = g_list_next(file)) {
+ Interface::PopupEntryType type;
+ bool in_buffer = false;
+
+ if (filename_is_dir((gchar *)file->data)) {
+ type = Interface::POPUP_DIRECTORY;
+ } else {
+ type = Interface::POPUP_FILE;
+ /* FIXME: inefficient */
+ in_buffer = ring.find((gchar *)file->data);
+ }
+
+ interface.popup_add(type, (gchar *)file->data,
+ in_buffer);
+ }
+
+ interface.popup_show();
+ } else if (g_list_length(matching) == 1 &&
+ !filename_is_dir((gchar *)g_list_first(matching)->data)) {
+ String::append(insert, completed);
+ }
+
+ g_completion_free(completion);
+
+ for (GList *file = g_list_first(files); file; file = g_list_next(file))
+ g_free(file->data);
+ g_list_free(files);
+
+ return insert;
+}
+
+static gchar *
+symbol_complete(SymbolList &list, const gchar *symbol, gchar completed)
+{
+ GList *glist;
+ GList *matching;
+ GCompletion *completion;
+ gchar *new_prefix;
+ gchar *insert = NULL;
+
+ if (!symbol)
+ symbol = "";
+
+ glist = list.get_glist();
+ if (!glist)
+ return NULL;
+
+ completion = g_completion_new(NULL);
+ g_completion_add_items(completion, glist);
+
+ matching = g_completion_complete(completion, symbol, &new_prefix);
+ if (new_prefix && strlen(new_prefix) > strlen(symbol))
+ insert = g_strdup(new_prefix + strlen(symbol));
+ g_free(new_prefix);
+
+ if (!insert && g_list_length(matching) > 1) {
+ for (GList *entry = g_list_first(matching);
+ entry != NULL;
+ entry = g_list_next(entry)) {
+ interface.popup_add(Interface::POPUP_PLAIN,
+ (gchar *)entry->data);
+ }
+
+ interface.popup_show();
+ } else if (g_list_length(matching) == 1) {
+ String::append(insert, completed);
+ }
+
+ g_completion_free(completion);
+
+ return insert;
+}
+
+/*
+ * Auxiliary functions
+ */
+
+static const gchar *
+last_occurrence(const gchar *str, const gchar *chars)
+{
+ while (*chars)
+ str = strrchr(str, *chars++) ? : str;
+
+ return str;
+}
+
+static inline gboolean
+filename_is_dir(const gchar *filename)
+{
+ return g_str_has_suffix(filename, G_DIR_SEPARATOR_S);
+}
diff --git a/src/expressions.cpp b/src/expressions.cpp
new file mode 100644
index 0000000..ac06b43
--- /dev/null
+++ b/src/expressions.cpp
@@ -0,0 +1,201 @@
+#include <glib.h>
+
+#include "sciteco.h"
+#include "undo.h"
+#include "expressions.h"
+
+Expressions expressions;
+
+void
+Expressions::set_num_sign(gint sign)
+{
+ undo.push_var<gint>(num_sign);
+ num_sign = sign;
+}
+
+void
+Expressions::set_radix(gint r)
+{
+ undo.push_var<gint>(radix);
+ radix = r;
+}
+
+gint64
+Expressions::push(gint64 number)
+{
+ while (operators.items() && operators.peek() == OP_NEW)
+ pop_op();
+
+ push(OP_NUMBER);
+
+ if (num_sign < 0) {
+ set_num_sign(1);
+ number *= -1;
+ }
+
+ numbers.undo_pop();
+ return numbers.push(number);
+}
+
+gint64
+Expressions::pop_num(int index)
+{
+ gint64 n = 0;
+
+ pop_op();
+ if (numbers.items() > 0) {
+ n = numbers.pop(index);
+ numbers.undo_push(n, index);
+ }
+
+ return n;
+}
+
+gint64
+Expressions::pop_num_calc(int index, gint64 imply)
+{
+ eval();
+ if (num_sign < 0)
+ set_num_sign(1);
+
+ return args() > 0 ? pop_num(index) : imply;
+}
+
+gint64
+Expressions::add_digit(gchar digit)
+{
+ gint64 n = args() > 0 ? pop_num() : 0;
+
+ return push(n*radix + (n < 0 ? -1 : 1)*(digit - '0'));
+}
+
+Expressions::Operator
+Expressions::push(Expressions::Operator op)
+{
+ operators.undo_pop();
+ return operators.push(op);
+}
+
+Expressions::Operator
+Expressions::push_calc(Expressions::Operator op)
+{
+ int first = first_op();
+
+ /* calculate if op has lower precedence than op on stack */
+ if (first && operators.peek(first) <= op)
+ calc();
+
+ return push(op);
+}
+
+Expressions::Operator
+Expressions::pop_op(int index)
+{
+ Operator op = OP_NIL;
+
+ if (operators.items() > 0) {
+ op = operators.pop(index);
+ operators.undo_push(op, index);
+ }
+
+ return op;
+}
+
+void
+Expressions::calc(void)
+{
+ gint64 result;
+
+ gint64 vright = pop_num();
+ Operator op = pop_op();
+ gint64 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: result = vleft / vright; break;
+ case OP_MOD: 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_OR: result = vleft | vright; break;
+ default:
+ /* shouldn't happen */
+ g_assert(false);
+ }
+
+ push(result);
+}
+
+void
+Expressions::eval(bool pop_brace)
+{
+ for (;;) {
+ gint n = first_op();
+ Operator op;
+
+ if (n < 2)
+ break;
+
+ op = operators.peek(n);
+ if (op == OP_LOOP)
+ break;
+ if (op == OP_BRACE) {
+ if (pop_brace)
+ pop_op(n);
+ break;
+ }
+
+ calc();
+ }
+}
+
+int
+Expressions::args(void)
+{
+ int n = 0;
+ int items = operators.items();
+
+ while (n < items && operators.peek(n+1) == OP_NUMBER)
+ n++;
+
+ return n;
+}
+
+int
+Expressions::find_op(Operator op)
+{
+ int items = operators.items();
+
+ for (int i = 1; i <= items; i++)
+ if (operators.peek(i) == op)
+ return i;
+
+ return 0;
+}
+
+int
+Expressions::first_op(void)
+{
+ int items = operators.items();
+
+ for (int i = 1; i <= items; i++) {
+ switch (operators.peek(i)) {
+ case OP_NUMBER:
+ case OP_NEW:
+ break;
+ default:
+ return i;
+ }
+ }
+
+ return 0;
+}
+
+void
+Expressions::discard_args(void)
+{
+ eval();
+ for (int i = args(); i; i--)
+ pop_num_calc();
+}
diff --git a/src/expressions.h b/src/expressions.h
new file mode 100644
index 0000000..a441e90
--- /dev/null
+++ b/src/expressions.h
@@ -0,0 +1,194 @@
+#ifndef __EXPRESSIONS_H
+#define __EXPRESSIONS_H
+
+#include <glib.h>
+
+#include "undo.h"
+
+template <typename Type>
+class ValueStack {
+ class UndoTokenPush : public UndoToken {
+ ValueStack<Type> *stack;
+
+ Type value;
+ int index;
+
+ public:
+ UndoTokenPush(ValueStack<Type> *_stack,
+ Type _value, int _index = 1)
+ : stack(_stack), value(_value), index(_index) {}
+
+ void
+ run(void)
+ {
+ stack->push(value, index);
+ }
+ };
+
+ class UndoTokenPop : public UndoToken {
+ ValueStack<Type> *stack;
+
+ int index;
+
+ public:
+ UndoTokenPop(ValueStack<Type> *_stack, int _index = 1)
+ : stack(_stack), index(_index) {}
+
+ void
+ run(void)
+ {
+ stack->pop(index);
+ }
+ };
+
+ int size;
+
+ Type *stack;
+ Type *top;
+
+public:
+ ValueStack(int _size = 1024) : size(_size)
+ {
+ top = stack = new Type[size];
+ }
+
+ ~ValueStack()
+ {
+ delete stack;
+ }
+
+ inline int
+ items(void)
+ {
+ return top - stack;
+ }
+
+ inline Type &
+ push(Type value, int index = 1)
+ {
+ for (int i = -index + 1; i; i++)
+ top[i+1] = top[i];
+
+ top++;
+ return peek(index) = value;
+ }
+ inline void
+ undo_push(Type value, int index = 1)
+ {
+ undo.push(new UndoTokenPush(this, value, index));
+ }
+
+ inline Type
+ pop(int index = 1)
+ {
+ Type v = peek(index);
+
+ top--;
+ while (--index)
+ top[-index] = top[-index + 1];
+
+ return v;
+ }
+ inline void
+ undo_pop(int index = 1)
+ {
+ undo.push(new UndoTokenPop(this, index));
+ }
+
+ inline Type &
+ peek(int index = 1)
+ {
+ return top[-index];
+ }
+
+ inline void
+ clear(void)
+ {
+ top = stack;
+ }
+};
+
+/*
+ * Arithmetic expression stacks
+ */
+extern class Expressions {
+public:
+ /* reflects also operator precedence */
+ enum Operator {
+ OP_NIL = 0,
+ OP_POW, // ^*
+ OP_MUL, // *
+ OP_DIV, // /
+ OP_MOD, // ^/
+ OP_SUB, // -
+ OP_ADD, // +
+ OP_AND, // &
+ OP_OR, // #
+ // pseudo operators:
+ OP_NEW,
+ OP_BRACE,
+ OP_LOOP,
+ OP_NUMBER
+ };
+
+private:
+ ValueStack<gint64> numbers;
+ ValueStack<Operator> operators;
+
+public:
+ Expressions() : num_sign(1), radix(10) {}
+
+ gint num_sign;
+ void set_num_sign(gint sign);
+
+ gint radix;
+ void set_radix(gint r);
+
+ gint64 push(gint64 number);
+
+ inline gint64
+ peek_num(int index = 1)
+ {
+ return numbers.peek(index);
+ }
+ gint64 pop_num(int index = 1);
+ gint64 pop_num_calc(int index, gint64 imply);
+ inline gint64
+ pop_num_calc(int index = 1)
+ {
+ return pop_num_calc(index, num_sign);
+ }
+
+ gint64 add_digit(gchar digit);
+
+ Operator push(Operator op);
+ Operator push_calc(Operator op);
+ inline Operator
+ peek_op(int index = 1)
+ {
+ return operators.peek(index);
+ }
+ Operator pop_op(int index = 1);
+
+ void eval(bool pop_brace = false);
+
+ int args(void);
+
+ void discard_args(void);
+
+ int find_op(Operator op);
+
+ inline void
+ clear(void)
+ {
+ numbers.clear();
+ operators.clear();
+ }
+
+private:
+ void calc(void);
+
+ int first_op(void);
+} expressions;
+
+#endif
diff --git a/src/goto.cpp b/src/goto.cpp
new file mode 100644
index 0000000..7af5152
--- /dev/null
+++ b/src/goto.cpp
@@ -0,0 +1,147 @@
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "sciteco.h"
+#include "expressions.h"
+#include "parser.h"
+#include "undo.h"
+#include "goto.h"
+
+namespace States {
+ StateLabel label;
+ StateGotoCmd gotocmd;
+}
+
+namespace Goto {
+ GotoTable *table = NULL;
+ gchar *skip_label = NULL;
+}
+
+gint
+GotoTable::remove(gchar *name)
+{
+ gint existing_pc = -1;
+
+ Label label(name);
+ Label *existing = (Label *)RBTree::find(&label);
+
+ if (existing) {
+ existing_pc = existing->pc;
+ RBTree::remove(existing);
+ delete existing;
+ }
+
+ return existing_pc;
+}
+
+gint
+GotoTable::find(gchar *name)
+{
+ Label label(name);
+ Label *existing = (Label *)RBTree::find(&label);
+
+ return existing ? existing->pc : -1;
+}
+
+gint
+GotoTable::set(gchar *name, gint pc)
+{
+ if (pc < 0)
+ return remove(name);
+
+ Label *label = new Label(name, pc);
+ Label *existing;
+ gint existing_pc = -1;
+
+ existing = (Label *)RBTree::find(label);
+ if (existing) {
+ existing_pc = existing->pc;
+ g_free(existing->name);
+ existing->name = label->name;
+ existing->pc = label->pc;
+
+ label->name = NULL;
+ delete label;
+ } else {
+ RBTree::insert(label);
+ }
+
+#ifdef DEBUG
+ dump();
+#endif
+
+ return existing_pc;
+}
+
+#ifdef DEBUG
+void
+GotoTable::dump(void)
+{
+ for (Label *cur = (Label *)RBTree::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) throw (Error)
+{
+ 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;
+}
+
+State *
+StateGotoCmd::done(const gchar *str) throw (Error)
+{
+ gint64 value;
+ gchar **labels;
+
+ BEGIN_EXEC(&States::start);
+
+ value = expressions.pop_num_calc();
+ labels = g_strsplit(str, ",", -1);
+
+ if (value > 0 && value <= 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;
+}
diff --git a/src/goto.h b/src/goto.h
new file mode 100644
index 0000000..6275d34
--- /dev/null
+++ b/src/goto.h
@@ -0,0 +1,107 @@
+#ifndef __GOTO_H
+#define __GOTO_H
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "sciteco.h"
+#include "parser.h"
+#include "undo.h"
+#include "rbtree.h"
+
+class GotoTable : public RBTree {
+ class UndoTokenSet : public UndoToken {
+ GotoTable *table;
+
+ gchar *name;
+ gint pc;
+
+ public:
+ UndoTokenSet(GotoTable *_table, gchar *_name, gint _pc = -1)
+ : table(_table), name(g_strdup(_name)), pc(_pc) {}
+ ~UndoTokenSet()
+ {
+ g_free(name);
+ }
+
+ void
+ run(void)
+ {
+ table->set(name, pc);
+#ifdef DEBUG
+ table->dump();
+#endif
+ }
+ };
+
+ class Label : public RBEntry {
+ public:
+ gchar *name;
+ gint pc;
+
+ Label(gchar *_name, gint _pc = -1)
+ : name(g_strdup(_name)), pc(_pc) {}
+ ~Label()
+ {
+ g_free(name);
+ }
+
+ int
+ operator <(RBEntry &l2)
+ {
+ return g_strcmp0(name, ((Label &)l2).name);
+ }
+ };
+
+ /*
+ * whether to generate UndoTokens (unnecessary in macro invocations)
+ */
+ bool must_undo;
+
+public:
+ GotoTable(bool _undo = true) : RBTree(), must_undo(_undo) {}
+
+ gint remove(gchar *name);
+ gint find(gchar *name);
+
+ gint set(gchar *name, gint pc);
+ inline void
+ undo_set(gchar *name, gint pc = -1)
+ {
+ if (must_undo)
+ undo.push(new UndoTokenSet(this, name, pc));
+ }
+
+#ifdef DEBUG
+ void dump(void);
+#endif
+};
+
+namespace Goto {
+ extern GotoTable *table;
+ extern gchar *skip_label;
+}
+
+/*
+ * Command states
+ */
+
+class StateLabel : public State {
+public:
+ StateLabel();
+
+private:
+ State *custom(gchar chr) throw (Error);
+};
+
+class StateGotoCmd : public StateExpectString {
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+namespace States {
+ extern StateLabel label;
+ extern StateGotoCmd gotocmd;
+}
+
+#endif
diff --git a/src/gtk-info-popup.gob b/src/gtk-info-popup.gob
new file mode 100644
index 0000000..914647a
--- /dev/null
+++ b/src/gtk-info-popup.gob
@@ -0,0 +1,165 @@
+requires 2.0.16
+
+%privateheader{
+#include <gdk/gdk.h>
+%}
+
+%h{
+#include <gtk/gtk.h>
+%}
+
+enum GTK_INFO_POPUP {
+ PLAIN,
+ FILE,
+ DIRECTORY
+} Gtk:Info:Popup:Entry:Type;
+
+class Gtk:Info:Popup from Gtk:Window {
+ private GtkWidget *parent;
+
+ init(self)
+ {
+ GtkWidget *vbox;
+
+ //gtk_window_set_gravity(GTK_WINDOW(self), GDK_GRAVITY_SOUTH_WEST);
+ gtk_container_set_border_width(GTK_CONTAINER(self), 10);
+
+ vbox = gtk_vbox_new(FALSE, 5);
+ gtk_container_set_resize_mode(GTK_CONTAINER(vbox), GTK_RESIZE_PARENT);
+ gtk_container_add(GTK_CONTAINER(self), vbox);
+ gtk_widget_show(vbox);
+ }
+
+ public GtkWidget *
+ new(Gtk:Widget *parent)
+ {
+ Self *widget;
+ GtkWidget *toplevel;
+
+ widget = GET_NEW_VARG("type", GTK_WINDOW_POPUP, NULL);
+ widget->_priv->parent = parent;
+
+ g_signal_connect(parent, "size-allocate",
+ G_CALLBACK(self_size_allocate_cb), widget);
+ toplevel = gtk_widget_get_toplevel(parent);
+ g_signal_connect(toplevel, "configure-event",
+ G_CALLBACK(self_configure_cb), widget);
+
+ return GTK_WIDGET(widget);
+ }
+
+ private void
+ position(self)
+ {
+ GdkWindow *window = gtk_widget_get_window(self->_priv->parent);
+ gint x, y;
+ gint w, h;
+
+ if (!window)
+ return;
+
+ gdk_window_get_origin(window, &x, &y);
+ gtk_window_get_size(GTK_WINDOW(self), &w, &h);
+
+ gtk_window_move(GTK_WINDOW(self), x, y - h);
+ }
+
+ private void
+ size_allocate_cb(Gtk:Widget *parent,
+ Gtk:Allocation *alloc, gpointer user_data)
+ {
+ self_position(SELF(user_data));
+ }
+
+ private gboolean
+ configure_cb(Gtk:Widget *toplevel,
+ Gdk:Event:Configure *event, gpointer user_data)
+ {
+ self_position(SELF(user_data));
+ return FALSE;
+ }
+
+ override (Gtk:Widget) void
+ show(Gtk:Widget *self)
+ {
+ GtkRequisition req;
+
+ gtk_widget_size_request(self, &req);
+ gtk_window_resize(GTK_WINDOW(self), req.width, req.height);
+ self_position(SELF(self));
+
+ PARENT_HANDLER(self);
+ }
+
+ public void
+ add(self, Gtk:Info:Popup:Entry:Type type,
+ const gchar *name, gboolean highlight)
+ {
+ static const gchar *type2stock[] = {
+ [GTK_INFO_POPUP_PLAIN] = NULL,
+ [GTK_INFO_POPUP_FILE] = GTK_STOCK_FILE,
+ [GTK_INFO_POPUP_DIRECTORY] = GTK_STOCK_DIRECTORY
+ };
+
+ GtkWidget *vbox = gtk_bin_get_child(GTK_BIN(self));
+ GdkScreen *screen = gtk_window_get_screen(GTK_WINDOW(self));
+ GtkRequisition req;
+
+ GtkWidget *hbox;
+ GtkWidget *label;
+ gchar *markup;
+
+ gtk_widget_size_request(GTK_WIDGET(self), &req);
+ if (req.height > gdk_screen_get_height(screen))
+ return;
+
+ hbox = gtk_hbox_new(FALSE, 5);
+
+ if (type2stock[type]) {
+ GtkWidget *image;
+
+ image = gtk_image_new_from_stock(type2stock[type],
+ GTK_ICON_SIZE_MENU);
+ gtk_box_pack_start(GTK_BOX(hbox), image,
+ FALSE, FALSE, 0);
+ }
+
+ /*
+ * FIXME: setting Pango attributes directly would be
+ * much more efficient
+ */
+ label = gtk_label_new(NULL);
+ markup = g_markup_printf_escaped("<span weight=\"%s\">%s</span>",
+ highlight ? "bold" : "normal",
+ name);
+ gtk_label_set_markup(GTK_LABEL(label), markup);
+ g_free(markup);
+ gtk_misc_set_alignment(GTK_MISC(label), 0., 0.5);
+ gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
+
+ gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
+ gtk_widget_show_all(hbox);
+
+ gtk_widget_size_request(GTK_WIDGET(self), &req);
+ if (req.height > gdk_screen_get_height(screen)) {
+ label = gtk_label_new("...");
+ gtk_box_pack_start(GTK_BOX(vbox), label,
+ FALSE, FALSE, 0);
+ gtk_widget_show(label);
+ }
+ }
+
+ public void
+ clear(self)
+ {
+ GtkWidget *vbox = gtk_bin_get_child(GTK_BIN(self));
+ GList *children;
+
+ children = gtk_container_get_children(GTK_CONTAINER(vbox));
+ 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.cpp b/src/interface-gtk.cpp
new file mode 100644
index 0000000..7515b2f
--- /dev/null
+++ b/src/interface-gtk.cpp
@@ -0,0 +1,248 @@
+#include <stdarg.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include <gtk/gtk.h>
+#include "gtk-info-popup.h"
+
+#include <Scintilla.h>
+#include <ScintillaWidget.h>
+
+#include "sciteco.h"
+#include "qregisters.h"
+#include "ring.h"
+#include "interface.h"
+#include "interface-gtk.h"
+
+InterfaceGtk interface;
+
+extern "C" {
+static void scintilla_notify(ScintillaObject *sci, uptr_t idFrom,
+ SCNotification *notify, gpointer user_data);
+static gboolean cmdline_key_pressed(GtkWidget *widget, GdkEventKey *event,
+ gpointer user_data);
+static gboolean exit_app(GtkWidget *w, GdkEventAny *e, gpointer p);
+}
+
+#define UNNAMED_FILE "(Unnamed)"
+
+InterfaceGtk::InterfaceGtk()
+{
+ GtkWidget *vbox;
+ GtkWidget *info_content;
+
+ gtk_init(NULL, NULL);
+
+ window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW(window), PACKAGE_NAME);
+ g_signal_connect(G_OBJECT(window), "delete-event",
+ G_CALLBACK(exit_app), NULL);
+
+ vbox = gtk_vbox_new(FALSE, 0);
+
+ editor_widget = scintilla_new();
+ scintilla_set_id(SCINTILLA(editor_widget), 0);
+ gtk_widget_set_usize(editor_widget, 500, 300);
+ gtk_widget_set_can_focus(editor_widget, FALSE);
+ g_signal_connect(G_OBJECT(editor_widget), SCINTILLA_NOTIFY,
+ G_CALLBACK(scintilla_notify), NULL);
+ gtk_box_pack_start(GTK_BOX(vbox), editor_widget, TRUE, TRUE, 0);
+
+ info_widget = gtk_info_bar_new();
+ info_content = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_widget));
+ message_widget = gtk_label_new("");
+ gtk_misc_set_alignment(GTK_MISC(message_widget), 0., 0.);
+ gtk_container_add(GTK_CONTAINER(info_content), message_widget);
+ gtk_box_pack_start(GTK_BOX(vbox), info_widget, FALSE, FALSE, 0);
+
+ cmdline_widget = gtk_entry_new();
+ gtk_entry_set_has_frame(GTK_ENTRY(cmdline_widget), FALSE);
+ gtk_editable_set_editable(GTK_EDITABLE(cmdline_widget), FALSE);
+ widget_set_font(cmdline_widget, "Courier");
+ g_signal_connect(G_OBJECT(cmdline_widget), "key-press-event",
+ G_CALLBACK(cmdline_key_pressed), NULL);
+ gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0);
+
+ gtk_container_add(GTK_CONTAINER(window), vbox);
+
+ popup_widget = gtk_info_popup_new(cmdline_widget);
+
+ gtk_widget_grab_focus(cmdline_widget);
+
+ ssm(SCI_SETFOCUS, TRUE);
+
+ cmdline_update("");
+}
+
+void
+InterfaceGtk::vmsg(MessageType type, const gchar *fmt, va_list ap)
+{
+ static const GtkMessageType type2gtk[] = {
+ /* [MSG_USER] = */ GTK_MESSAGE_OTHER,
+ /* [MSG_INFO] = */ GTK_MESSAGE_INFO,
+ /* [MSG_WARNING] = */ GTK_MESSAGE_WARNING,
+ /* [MSG_ERROR] = */ GTK_MESSAGE_ERROR
+ };
+
+ va_list aq;
+ gchar buf[255];
+
+ va_copy(aq, ap);
+ stdio_vmsg(type, fmt, ap);
+ g_vsnprintf(buf, sizeof(buf), fmt, aq);
+ va_end(aq);
+
+ gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget),
+ type2gtk[type]);
+ gtk_label_set_text(GTK_LABEL(message_widget), buf);
+}
+
+void
+InterfaceGtk::msg_clear(void)
+{
+ gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget),
+ GTK_MESSAGE_OTHER);
+ gtk_label_set_text(GTK_LABEL(message_widget), "");
+}
+
+void
+InterfaceGtk::info_update(QRegister *reg)
+{
+ gchar buf[255];
+
+ g_snprintf(buf, sizeof(buf), "%s - <QRegister> %s", PACKAGE_NAME,
+ reg->name);
+ gtk_window_set_title(GTK_WINDOW(window), buf);
+}
+
+void
+InterfaceGtk::info_update(Buffer *buffer)
+{
+ gchar buf[255];
+
+ g_snprintf(buf, sizeof(buf), "%s - <Buffer> %s%s", PACKAGE_NAME,
+ buffer->filename ? : UNNAMED_FILE,
+ buffer->dirty ? "*" : "");
+ gtk_window_set_title(GTK_WINDOW(window), buf);
+}
+
+void
+InterfaceGtk::cmdline_update(const gchar *cmdline)
+{
+ gint pos = 1;
+
+ if (!cmdline)
+ /* widget automatically redrawn */
+ return;
+
+ gtk_entry_set_text(GTK_ENTRY(cmdline_widget), "*");
+ gtk_editable_insert_text(GTK_EDITABLE(cmdline_widget),
+ cmdline, -1, &pos);
+ gtk_editable_set_position(GTK_EDITABLE(cmdline_widget), pos);
+}
+
+void
+InterfaceGtk::popup_add(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
+ };
+
+ gtk_info_popup_add(GTK_INFO_POPUP(popup_widget),
+ type2gtk[type], name, highlight);
+}
+
+void
+InterfaceGtk::popup_clear(void)
+{
+ if (gtk_widget_get_visible(popup_widget)) {
+ gtk_widget_hide(popup_widget);
+ gtk_info_popup_clear(GTK_INFO_POPUP(popup_widget));
+ }
+}
+
+void
+InterfaceGtk::widget_set_font(GtkWidget *widget, const gchar *font_name)
+{
+ PangoFontDescription *font_desc;
+
+ font_desc = pango_font_description_from_string(font_name);
+ gtk_widget_modify_font(widget, font_desc);
+ pango_font_description_free(font_desc);
+}
+
+InterfaceGtk::~InterfaceGtk()
+{
+ gtk_widget_destroy(popup_widget);
+ gtk_widget_destroy(window);
+
+ scintilla_release_resources();
+}
+
+/*
+ * GTK+ callbacks
+ */
+
+static void
+scintilla_notify(ScintillaObject *sci __attribute__((unused)),
+ uptr_t idFrom __attribute__((unused)),
+ SCNotification *notify,
+ gpointer user_data __attribute__((unused)))
+{
+ interface.process_notify(notify);
+}
+
+static gboolean
+cmdline_key_pressed(GtkWidget *widget, GdkEventKey *event,
+ gpointer user_data __attribute__((unused)))
+{
+#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
+
+ switch (event->keyval) {
+ case GDK_BackSpace:
+ cmdline_keypress('\b');
+ break;
+ case GDK_Tab:
+ cmdline_keypress('\t');
+ break;
+ case GDK_Return:
+ switch (interface.ssm(SCI_GETEOLMODE)) {
+ case SC_EOL_CR:
+ cmdline_keypress('\r');
+ break;
+ case SC_EOL_CRLF:
+ cmdline_keypress('\r');
+ /* fall through */
+ case SC_EOL_LF:
+ default:
+ cmdline_keypress('\n');
+ }
+ break;
+ default:
+ if (*event->string)
+ cmdline_keypress(*event->string);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+exit_app(GtkWidget *w __attribute__((unused)),
+ GdkEventAny *e __attribute__((unused)),
+ gpointer p __attribute__((unused)))
+{
+ gtk_main_quit();
+ return TRUE;
+}
diff --git a/src/interface-gtk.h b/src/interface-gtk.h
new file mode 100644
index 0000000..b46f821
--- /dev/null
+++ b/src/interface-gtk.h
@@ -0,0 +1,73 @@
+#ifndef __INTERFACE_GTK_H
+#define __INTERFACE_GTK_H
+
+#include <stdarg.h>
+
+#include <glib.h>
+#include <gtk/gtk.h>
+
+#include <Scintilla.h>
+#include <ScintillaWidget.h>
+
+#include "interface.h"
+
+extern class InterfaceGtk : public Interface {
+ GtkWidget *window;
+ GtkWidget *editor_widget;
+ GtkWidget *cmdline_widget;
+ GtkWidget *info_widget, *message_widget;
+
+ GtkWidget *popup_widget;
+
+public:
+ InterfaceGtk();
+ ~InterfaceGtk();
+
+ inline GOptionGroup *
+ get_options(void)
+ {
+ return gtk_get_option_group(TRUE);
+ }
+ inline void
+ parse_args(int &argc, char **&argv)
+ {
+ gtk_parse_args(&argc, &argv);
+ }
+
+ void vmsg(MessageType type, const gchar *fmt, va_list ap);
+ void msg_clear(void);
+
+ inline sptr_t
+ ssm(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0)
+ {
+ return scintilla_send_message(SCINTILLA(editor_widget),
+ iMessage, wParam, lParam);
+ }
+
+ void info_update(QRegister *reg);
+ void info_update(Buffer *buffer);
+
+ void cmdline_update(const gchar *cmdline = NULL);
+
+ void popup_add(PopupEntryType type,
+ const gchar *name, bool highlight = false);
+ inline void
+ popup_show(void)
+ {
+ gtk_widget_show(popup_widget);
+ }
+ void popup_clear(void);
+
+ /* main entry point */
+ inline void
+ event_loop(void)
+ {
+ gtk_widget_show_all(window);
+ gtk_main();
+ }
+
+private:
+ static void widget_set_font(GtkWidget *widget, const gchar *font_name);
+} interface;
+
+#endif
diff --git a/src/interface-ncurses.cpp b/src/interface-ncurses.cpp
new file mode 100644
index 0000000..0e08914
--- /dev/null
+++ b/src/interface-ncurses.cpp
@@ -0,0 +1,426 @@
+#include <string.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <locale.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include <curses.h>
+/* only a hack until we have Autoconf checks */
+#if defined(__PDCURSES__) && CHTYPE_LONG >= 2
+#define PDCURSES_WIN32A
+#endif
+
+#include <Scintilla.h>
+#include <ScintillaTerm.h>
+
+#include "sciteco.h"
+#include "qregisters.h"
+#include "ring.h"
+#include "interface.h"
+#include "interface-ncurses.h"
+
+InterfaceNCurses interface;
+
+extern "C" {
+static void scintilla_notify(Scintilla *sci, int idFrom,
+ void *notify, void *user_data);
+}
+
+#define UNNAMED_FILE "(Unnamed)"
+
+/* FIXME: should be configurable in TECO (Function key substitutes) */
+#define ESCAPE_SURROGATE KEY_DC
+
+#ifndef SCI_COLOR_PAIR
+/* from ScintillaTerm.cxx */
+#define SCI_COLOR_PAIR(f, b) \
+ ((b) * 8 + (f) + 1)
+#endif
+
+#define SCI_COLOR_ATTR(f, b) \
+ COLOR_PAIR(SCI_COLOR_PAIR(f, b))
+
+InterfaceNCurses::InterfaceNCurses()
+{
+ init_screen();
+ raw();
+ cbreak();
+ noecho();
+ curs_set(0); /* Scintilla draws its own cursor */
+
+ setlocale(LC_CTYPE, ""); /* for displaying UTF-8 characters properly */
+
+ info_window = newwin(1, 0, 0, 0);
+ info_current = g_strdup(PACKAGE_NAME);
+
+ /* NOTE: Scintilla initializes color pairs */
+ sci = scintilla_new(scintilla_notify);
+ sci_window = scintilla_get_window(sci);
+ wresize(sci_window, LINES - 3, COLS);
+ mvwin(sci_window, 1, 0);
+
+ msg_window = newwin(1, 0, LINES - 2, 0);
+
+ cmdline_window = newwin(0, 0, LINES - 1, 0);
+ keypad(cmdline_window, TRUE);
+ cmdline_current = NULL;
+
+ ssm(SCI_SETFOCUS, TRUE);
+
+ draw_info();
+ /* scintilla will be refreshed in event loop */
+ msg_clear();
+ cmdline_update("");
+
+#ifndef PDCURSES_WIN32A
+ /* workaround: endwin() is somewhat broken in the win32a port */
+ endwin();
+#endif
+}
+
+#ifdef __PDCURSES__
+
+void
+InterfaceNCurses::init_screen(void)
+{
+#ifdef PDCURSES_WIN32A
+ /* enables window resizing on Win32a port */
+ PDC_set_resize_limits(25, 0xFFFF, 80, 0xFFFF);
+#endif
+
+ initscr();
+
+ screen_tty = NULL;
+ screen = NULL;
+}
+
+#else
+
+void
+InterfaceNCurses::init_screen(void)
+{
+ /*
+ * Prevent the initial redraw and any escape sequences that may
+ * interfere with stdout, so we may use the terminal in
+ * cooked mode, for commandline help and batch processing.
+ * Scintilla must be initialized for batch processing to work.
+ * (Frankly I have no idea why this works!)
+ */
+ screen_tty = g_fopen("/dev/tty", "r+b");
+ screen = newterm(NULL, screen_tty, screen_tty);
+ set_term(screen);
+}
+
+#endif /* !__PDCURSES__ */
+
+void
+InterfaceNCurses::resize_all_windows(void)
+{
+ int lines, cols; /* screen dimensions */
+
+ getmaxyx(stdscr, lines, cols);
+
+ wresize(info_window, 1, cols);
+ wresize(sci_window, 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();
+ /* scintilla will be refreshed in event loop */
+ msg_clear(); /* FIXME: use saved message */
+ cmdline_update();
+}
+
+void
+InterfaceNCurses::vmsg(MessageType type, const gchar *fmt, va_list ap)
+{
+ static const chtype type2attr[] = {
+ SCI_COLOR_ATTR(COLOR_BLACK, COLOR_WHITE), /* MSG_USER */
+ SCI_COLOR_ATTR(COLOR_BLACK, COLOR_GREEN), /* MSG_INFO */
+ SCI_COLOR_ATTR(COLOR_BLACK, COLOR_YELLOW), /* MSG_WARNING */
+ SCI_COLOR_ATTR(COLOR_BLACK, COLOR_RED) /* MSG_ERROR */
+ };
+
+#ifdef PDCURSES_WIN32A
+ stdio_vmsg(type, fmt, ap);
+ if (isendwin()) /* batch mode */
+ return;
+#else
+ if (isendwin()) { /* batch mode */
+ stdio_vmsg(type, fmt, ap);
+ return;
+ }
+#endif
+
+ wmove(msg_window, 0, 0);
+ wbkgdset(msg_window, ' ' | type2attr[type]);
+ vw_printw(msg_window, fmt, ap);
+ wclrtoeol(msg_window);
+
+ wrefresh(msg_window);
+}
+
+void
+InterfaceNCurses::msg_clear(void)
+{
+ if (isendwin()) /* batch mode */
+ return;
+
+ wmove(msg_window, 0, 0);
+ wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(COLOR_BLACK, COLOR_WHITE));
+ wclrtoeol(msg_window);
+
+ wrefresh(msg_window);
+}
+
+void
+InterfaceNCurses::draw_info(void)
+{
+ if (isendwin()) /* batch mode */
+ return;
+
+ wmove(info_window, 0, 0);
+ wbkgdset(info_window, ' ' | SCI_COLOR_ATTR(COLOR_BLACK, COLOR_WHITE));
+ waddstr(info_window, info_current);
+ wclrtoeol(info_window);
+
+ wrefresh(info_window);
+}
+
+void
+InterfaceNCurses::info_update(QRegister *reg)
+{
+ g_free(info_current);
+ info_current = g_strdup_printf("%s - <QRegister> %s", PACKAGE_NAME,
+ reg->name);
+
+ draw_info();
+}
+
+void
+InterfaceNCurses::info_update(Buffer *buffer)
+{
+ g_free(info_current);
+ info_current = g_strdup_printf("%s - <Buffer> %s%s", PACKAGE_NAME,
+ buffer->filename ? : UNNAMED_FILE,
+ buffer->dirty ? "*" : "");
+
+ draw_info();
+}
+
+void
+InterfaceNCurses::cmdline_update(const gchar *cmdline)
+{
+ size_t len;
+ int half_line = (getmaxx(stdscr) - 2) / 2;
+ const gchar *line;
+
+ if (cmdline) {
+ g_free(cmdline_current);
+ cmdline_current = g_strdup(cmdline);
+ } else {
+ cmdline = cmdline_current;
+ }
+ len = strlen(cmdline);
+
+ /* FIXME: optimize */
+ line = cmdline + len - MIN(len, half_line + len % half_line);
+
+ mvwaddch(cmdline_window, 0, 0, '*');
+ waddstr(cmdline_window, line);
+ waddch(cmdline_window, ' ' | A_REVERSE);
+ wclrtoeol(cmdline_window);
+
+ wrefresh(cmdline_window);
+}
+
+void
+InterfaceNCurses::popup_add(PopupEntryType type __attribute__((unused)),
+ const gchar *name, bool highlight)
+{
+ gchar *entry;
+
+ if (isendwin()) /* batch mode */
+ return;
+
+ entry = g_strconcat(highlight ? "*" : " ", name, NULL);
+
+ popup.longest = MAX(popup.longest, (gint)strlen(name));
+ popup.length++;
+
+ popup.list = g_slist_prepend(popup.list, entry);
+}
+
+void
+InterfaceNCurses::popup_show(void)
+{
+ int lines, cols; /* screen dimensions */
+ int popup_lines;
+ gint popup_cols;
+ gint cur_file, cur_line;
+
+ if (isendwin()) /* batch mode */
+ goto cleanup;
+
+ getmaxyx(stdscr, lines, cols);
+
+ popup.longest += 3;
+ popup.list = g_slist_reverse(popup.list);
+
+ /* popup_cols = floor(cols / popup.longest) */
+ popup_cols = MAX(cols / popup.longest, 1);
+ /* popup_lines = ceil(popup.length / popup_cols) */
+ popup_lines = popup.length / popup_cols;
+ if ((popup.length % popup_cols))
+ popup_lines++;
+ popup_lines = MIN(popup_lines, lines - 1);
+
+ /* window covers message, scintilla and info windows */
+ popup.window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0);
+ wbkgdset(popup.window, ' ' | SCI_COLOR_ATTR(COLOR_BLACK, COLOR_BLUE));
+
+ cur_file = 0;
+ cur_line = 1;
+ for (GSList *cur = popup.list; cur; cur = g_slist_next(cur)) {
+ gchar *entry = (gchar *)cur->data;
+
+ if (cur_file && !(cur_file % popup_cols)) {
+ wclrtoeol(popup.window);
+ waddch(popup.window, '\n');
+ cur_line++;
+ }
+
+ cur_file++;
+
+ if (cur_line == popup_lines && !(cur_file % popup_cols) &&
+ cur_file < popup.length) {
+ (void)wattrset(popup.window, A_BOLD);
+ waddstr(popup.window, "...");
+ break;
+ }
+
+ (void)wattrset(popup.window, *entry == '*' ? A_BOLD : A_NORMAL);
+ waddstr(popup.window, entry + 1);
+ for (int i = popup.longest - strlen(entry) + 1; i; i--)
+ waddch(popup.window, ' ');
+
+ g_free(cur->data);
+ }
+ wclrtoeol(popup.window);
+
+cleanup:
+ g_slist_free(popup.list);
+ popup.list = NULL;
+ popup.longest = popup.length = 0;
+}
+
+void
+InterfaceNCurses::popup_clear(void)
+{
+ if (!popup.window)
+ return;
+
+ redrawwin(info_window);
+ wrefresh(info_window);
+ redrawwin(sci_window);
+ scintilla_refresh(sci);
+ redrawwin(msg_window);
+ wrefresh(msg_window);
+
+ delwin(popup.window);
+ popup.window = NULL;
+}
+
+void
+InterfaceNCurses::event_loop(void)
+{
+ /* in commandline (visual) mode, enforce redraw */
+ wrefresh(curscr);
+ draw_info();
+
+ for (;;) {
+ int key;
+
+ /* also handles initial refresh (styles are configured...) */
+ scintilla_refresh(sci);
+ if (popup.window)
+ wrefresh(popup.window);
+
+ key = wgetch(cmdline_window);
+ switch (key) {
+#ifdef KEY_RESIZE
+ case ERR:
+ case KEY_RESIZE:
+#ifdef PDCURSES
+ resize_term(0, 0);
+#endif
+ resize_all_windows();
+ break;
+#endif
+ case ESCAPE_SURROGATE:
+ cmdline_keypress('\x1B');
+ break;
+ case KEY_BACKSPACE:
+ cmdline_keypress('\b');
+ break;
+ case KEY_ENTER:
+ case '\r':
+ switch (ssm(SCI_GETEOLMODE)) {
+ case SC_EOL_CR:
+ cmdline_keypress('\r');
+ break;
+ case SC_EOL_CRLF:
+ cmdline_keypress('\r');
+ /* fall through */
+ case SC_EOL_LF:
+ default:
+ cmdline_keypress('\n');
+ }
+ break;
+ default:
+ if (key <= 0xFF)
+ cmdline_keypress((gchar)key);
+ }
+ }
+}
+
+InterfaceNCurses::Popup::~Popup()
+{
+ if (window)
+ delwin(window);
+ if (list)
+ g_slist_free(list);
+}
+
+InterfaceNCurses::~InterfaceNCurses()
+{
+ delwin(info_window);
+ g_free(info_current);
+ /* also deletes curses window */
+ scintilla_delete(sci);
+ delwin(cmdline_window);
+ g_free(cmdline_current);
+ delwin(msg_window);
+
+ if (!isendwin())
+ endwin();
+
+ delscreen(screen);
+ if (screen_tty)
+ fclose(screen_tty);
+}
+
+/*
+ * Callbacks
+ */
+
+static void
+scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data)
+{
+ interface.process_notify((SCNotification *)notify);
+}
diff --git a/src/interface-ncurses.h b/src/interface-ncurses.h
new file mode 100644
index 0000000..97269a4
--- /dev/null
+++ b/src/interface-ncurses.h
@@ -0,0 +1,69 @@
+#ifndef __INTERFACE_NCURSES_H
+#define __INTERFACE_NCURSES_H
+
+#include <stdarg.h>
+
+#include <glib.h>
+
+#include <curses.h>
+
+#include <Scintilla.h>
+#include <ScintillaTerm.h>
+
+#include "interface.h"
+
+extern class InterfaceNCurses : public Interface {
+ SCREEN *screen;
+ FILE *screen_tty;
+
+ Scintilla *sci;
+
+ WINDOW *info_window;
+ gchar *info_current;
+ WINDOW *sci_window;
+ WINDOW *msg_window;
+ WINDOW *cmdline_window;
+ gchar *cmdline_current;
+
+ struct Popup {
+ WINDOW *window;
+ GSList *list;
+ gint longest;
+ gint length;
+
+ Popup() : window(NULL), list(NULL), longest(0), length(0) {}
+ ~Popup();
+ } popup;
+
+ void init_screen(void);
+ void resize_all_windows(void);
+ void draw_info(void);
+
+public:
+ InterfaceNCurses();
+ ~InterfaceNCurses();
+
+ void vmsg(MessageType type, const gchar *fmt, va_list ap);
+ void msg_clear(void);
+
+ inline sptr_t
+ ssm(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0)
+ {
+ return scintilla_send_message(sci, iMessage, wParam, lParam);
+ }
+
+ void info_update(QRegister *reg);
+ void info_update(Buffer *buffer);
+
+ void cmdline_update(const gchar *cmdline = NULL);
+
+ void popup_add(PopupEntryType type,
+ const gchar *name, bool highlight = false);
+ void popup_show(void);
+ void popup_clear(void);
+
+ /* main entry point */
+ void event_loop(void);
+} interface;
+
+#endif
diff --git a/src/interface.h b/src/interface.h
new file mode 100644
index 0000000..35ee1f7
--- /dev/null
+++ b/src/interface.h
@@ -0,0 +1,112 @@
+#ifndef __INTERFACE_H
+#define __INTERFACE_H
+
+#include <stdarg.h>
+
+#include <glib.h>
+
+#include <Scintilla.h>
+
+#include "undo.h"
+
+/* avoid include dependency conflict */
+class QRegister;
+class Buffer;
+
+/*
+ * Base class for all user interfaces - used mereley as a class interface.
+ * The actual instance of the interface has the platform-specific type
+ * (e.g. InterfaceGtk) since we would like to have the benefits of using
+ * classes but avoid the calling overhead when invoking virtual methods
+ * on Interface pointers.
+ * There's only one Interface* instance in the system.
+ */
+class Interface {
+ template <class Type>
+ class UndoTokenInfoUpdate : public UndoToken {
+ Interface *iface;
+ Type *obj;
+
+ public:
+ UndoTokenInfoUpdate(Interface *_iface, Type *_obj)
+ : iface(_iface), obj(_obj) {}
+
+ void
+ run(void)
+ {
+ iface->info_update(obj);
+ }
+ };
+
+public:
+ virtual GOptionGroup *
+ get_options(void)
+ {
+ return NULL;
+ }
+ virtual void parse_args(int &argc, char **&argv) {}
+
+ enum MessageType {
+ MSG_USER,
+ MSG_INFO,
+ MSG_WARNING,
+ MSG_ERROR
+ };
+ virtual void vmsg(MessageType type, const gchar *fmt, va_list ap) = 0;
+ inline void
+ msg(MessageType type, const gchar *fmt, ...) G_GNUC_PRINTF(3, 4)
+ {
+ va_list ap;
+
+ va_start(ap, fmt);
+ vmsg(type, fmt, ap);
+ va_end(ap);
+ }
+ virtual void msg_clear(void) {}
+
+ virtual sptr_t ssm(unsigned int iMessage,
+ uptr_t wParam = 0, sptr_t lParam = 0) = 0;
+
+ virtual void info_update(QRegister *reg) = 0;
+ virtual void info_update(Buffer *buffer) = 0;
+
+ template <class Type>
+ inline void
+ undo_info_update(Type *obj)
+ {
+ undo.push(new UndoTokenInfoUpdate<Type>(this, obj));
+ }
+
+ /* NULL means to redraw the current cmdline if necessary */
+ virtual void cmdline_update(const gchar *cmdline = NULL) = 0;
+
+ enum PopupEntryType {
+ POPUP_PLAIN,
+ POPUP_FILE,
+ POPUP_DIRECTORY
+ };
+ virtual void popup_add(PopupEntryType type,
+ const gchar *name, bool highlight = false) = 0;
+ virtual void popup_show(void) = 0;
+ virtual void popup_clear(void) = 0;
+
+ /* main entry point */
+ virtual void event_loop(void) = 0;
+
+ /*
+ * Interfacing to the external SciTECO world
+ * See main.cpp
+ */
+protected:
+ void stdio_vmsg(MessageType type, const gchar *fmt, va_list ap);
+public:
+ void process_notify(SCNotification *notify);
+};
+
+#ifdef INTERFACE_GTK
+#include "interface-gtk.h"
+#elif defined(INTERFACE_NCURSES)
+#include "interface-ncurses.h"
+#endif
+
+#endif
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..4526da3
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,208 @@
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+#include <SciLexer.h>
+
+#include "sciteco.h"
+#include "interface.h"
+#include "parser.h"
+#include "goto.h"
+#include "qregisters.h"
+#include "ring.h"
+#include "undo.h"
+
+#ifdef G_OS_UNIX
+#define INI_FILE ".teco_ini"
+#else
+#define INI_FILE "teco.ini"
+#endif
+
+namespace Flags {
+ gint64 ed = 0;
+}
+
+static gchar *mung_file = NULL;
+
+void
+Interface::stdio_vmsg(MessageType type, const gchar *fmt, va_list ap)
+{
+ gchar buf[255];
+
+ g_vsnprintf(buf, sizeof(buf), fmt, ap);
+
+ switch (type) {
+ case MSG_USER:
+ g_printf("%s\n", buf);
+ break;
+ case MSG_INFO:
+ g_printf("Info: %s\n", buf);
+ break;
+ case MSG_WARNING:
+ g_fprintf(stderr, "Warning: %s\n", buf);
+ break;
+ case MSG_ERROR:
+ g_fprintf(stderr, "Error: %s\n", buf);
+ break;
+ }
+}
+
+void
+Interface::process_notify(SCNotification *notify)
+{
+#ifdef DEBUG
+ g_printf("SCINTILLA NOTIFY: code=%d\n", notify->nmhdr.code);
+#endif
+}
+
+#ifdef G_OS_WIN32
+
+/*
+ * keep program self-contained under Windows
+ * (look for profile in current directory)
+ */
+static inline gchar *
+get_teco_ini(void)
+{
+ return g_strdup(INI_FILE);
+}
+
+/*
+ * Windows sometimes sets the current working directory to very obscure
+ * paths when opening files in the Explorer, but we have to read the
+ * teco.ini from the directory where our binary resides.
+ */
+static inline void
+fix_cwd(const gchar *program)
+{
+ gchar *bin_dir = g_path_get_dirname(program);
+ g_chdir(bin_dir);
+ g_free(bin_dir);
+}
+
+#else
+
+static inline gchar *
+get_teco_ini(void)
+{
+ const gchar *home;
+
+#ifdef G_OS_UNIX
+ home = g_get_home_dir();
+#else
+ home = g_get_user_config_dir();
+#endif
+ return g_build_filename(home, INI_FILE, NULL);
+}
+
+static inline void fix_cwd(const gchar *program __attribute__((unused))) {}
+
+#endif /* !G_OS_WIN32 */
+
+static inline void
+process_options(int &argc, char **&argv)
+{
+ static const GOptionEntry option_entries[] = {
+ {"mung", 'm', 0, G_OPTION_ARG_FILENAME, &mung_file,
+ "Mung file instead of " INI_FILE, "filename"},
+ {NULL}
+ };
+
+ GOptionContext *options;
+ GOptionGroup *interface_group = interface.get_options();
+
+ options = g_option_context_new("- " PACKAGE_STRING);
+
+ g_option_context_add_main_entries(options, option_entries, NULL);
+ if (interface_group)
+ g_option_context_add_group(options, interface_group);
+
+ if (!g_option_context_parse(options, &argc, &argv, NULL)) {
+ g_printf("Option parsing failed!\n");
+ exit(EXIT_FAILURE);
+ }
+
+ g_option_context_free(options);
+
+ if (mung_file) {
+ if (!g_file_test(mung_file, G_FILE_TEST_IS_REGULAR)) {
+ g_printf("Cannot mung \"%s\". File does not exist!\n",
+ mung_file);
+ exit(EXIT_FAILURE);
+ }
+ } else {
+ mung_file = get_teco_ini();
+ }
+
+ interface.parse_args(argc, argv);
+
+ /* remaining arguments, are arguments to the munged file */
+}
+
+int
+main(int argc, char **argv)
+{
+ static GotoTable cmdline_goto_table;
+ static QRegisterTable local_qregs;
+
+ fix_cwd(argv[0]);
+
+ process_options(argc, argv);
+
+ interface.ssm(SCI_SETCARETSTYLE, CARETSTYLE_BLOCK);
+ interface.ssm(SCI_SETCARETFORE, 0xFFFFFF);
+
+ /*
+ * FIXME: Default styles should probably be set interface-based
+ * (system defaults) and be changeable by TECO macros
+ */
+ interface.ssm(SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF);
+ interface.ssm(SCI_STYLESETBACK, STYLE_DEFAULT, 0x000000);
+ interface.ssm(SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)"Courier");
+ interface.ssm(SCI_STYLECLEARALL);
+
+ QRegisters::globals.initialize();
+ /* search string and status register */
+ QRegisters::globals.initialize("_");
+ /* replacement string register */
+ QRegisters::globals.initialize("-");
+ /* current buffer name and number ("*") */
+ QRegisters::globals.insert(new QRegisterBufferInfo());
+
+ local_qregs.initialize();
+ QRegisters::locals = &local_qregs;
+
+ ring.edit((const gchar *)NULL);
+
+ /* add remaining arguments to unnamed buffer */
+ for (int i = 1; i < argc; i++) {
+ interface.ssm(SCI_APPENDTEXT, strlen(argv[i]), (sptr_t)argv[i]);
+ interface.ssm(SCI_APPENDTEXT, 1, (sptr_t)"\n");
+ }
+
+ if (g_file_test(mung_file, G_FILE_TEST_IS_REGULAR)) {
+ if (!Execute::file(mung_file, false))
+ exit(EXIT_FAILURE);
+
+ /* FIXME: make quit immediate in batch/macro mode (non-UNDO)? */
+ if (quit_requested) {
+ /* FIXME */
+ exit(EXIT_SUCCESS);
+ }
+ }
+ g_free(mung_file);
+
+ Goto::table = &cmdline_goto_table;
+ interface.ssm(SCI_EMPTYUNDOBUFFER);
+ undo.enabled = true;
+
+ interface.event_loop();
+
+ return 0;
+}
diff --git a/src/parser.cpp b/src/parser.cpp
new file mode 100644
index 0000000..5ba96fa
--- /dev/null
+++ b/src/parser.cpp
@@ -0,0 +1,1436 @@
+#include <stdarg.h>
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+
+#include "sciteco.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"
+
+//#define DEBUG
+
+gint macro_pc = 0;
+
+namespace States {
+ StateStart start;
+ StateControl control;
+ StateFCommand fcommand;
+ StateCondCommand condcommand;
+ StateECommand ecommand;
+ StateScintilla_symbols scintilla_symbols;
+ StateScintilla_lParam scintilla_lparam;
+ StateInsert insert;
+
+ 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 = '\x1B';
+
+void
+Execute::step(const gchar *macro) throw (State::Error)
+{
+ while (macro[macro_pc]) {
+#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
+
+ State::input(macro[macro_pc]);
+ macro_pc++;
+ }
+}
+
+void
+Execute::macro(const gchar *macro, bool locals) throw (State::Error)
+{
+ GotoTable *parent_goto_table = Goto::table;
+ GotoTable macro_goto_table(false);
+
+ QRegisterTable *parent_locals = QRegisters::locals;
+ QRegisterTable macro_locals(false);
+
+ State *parent_state = States::current;
+ gint parent_pc = macro_pc;
+
+ /*
+ * 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;
+
+ Goto::table = &macro_goto_table;
+ if (locals) {
+ macro_locals.initialize();
+ QRegisters::locals = &macro_locals;
+ }
+
+ try {
+ step(macro);
+ if (Goto::skip_label)
+ throw State::Error("Label \"%s\" not found",
+ Goto::skip_label);
+ } catch (...) {
+ g_free(Goto::skip_label);
+ Goto::skip_label = NULL;
+
+ QRegisters::locals = parent_locals;
+ Goto::table = parent_goto_table;
+
+ macro_pc = parent_pc;
+ States::current = parent_state;
+
+ throw; /* forward */
+ }
+
+ QRegisters::locals = parent_locals;
+ Goto::table = parent_goto_table;
+
+ macro_pc = parent_pc;
+ States::current = parent_state;
+}
+
+bool
+Execute::file(const gchar *filename, bool locals)
+{
+ gchar *macro_str, *p = NULL;
+
+ if (!g_file_get_contents(filename, &macro_str, NULL, NULL))
+ return false;
+ /* only when executing files, ignore Hash-Bang line */
+ if (*macro_str == '#')
+ p = MAX(strchr(macro_str, '\r'), strchr(macro_str, '\n'));
+
+ try {
+ macro(p ? p+1 : macro_str, locals);
+ } catch (...) {
+ g_free(macro_str);
+ return false;
+ }
+
+ g_free(macro_str);
+ return true;
+}
+
+State::Error::Error(const gchar *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ interface.vmsg(Interface::MSG_ERROR, fmt, ap);
+ va_end(ap);
+}
+
+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) throw (Error)
+{
+ 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) throw (Error)
+{
+ State *next = NULL;
+ guint upper = g_ascii_toupper(chr);
+
+ if (upper < G_N_ELEMENTS(transitions))
+ next = transitions[upper];
+ if (!next)
+ next = custom(chr);
+ if (!next)
+ throw SyntaxError(chr);
+
+ return next;
+}
+
+gchar *
+StateExpectString::machine_input(gchar chr) throw (Error)
+{
+ switch (machine.mode) {
+ case Machine::MODE_UPPER:
+ chr = g_ascii_toupper(chr);
+ break;
+ case Machine::MODE_LOWER:
+ chr = g_ascii_tolower(chr);
+ break;
+ default:
+ break;
+ }
+
+ if (machine.toctl) {
+ chr = CTL_KEY(g_ascii_toupper(chr));
+ machine.toctl = false;
+ }
+
+ if (machine.state == Machine::STATE_ESCAPED) {
+ machine.state = Machine::STATE_START;
+ goto original;
+ }
+
+ if (chr == '^') {
+ machine.toctl = true;
+ return NULL;
+ }
+
+ switch (machine.state) {
+ case Machine::STATE_START:
+ switch (chr) {
+ case CTL_KEY('Q'):
+ case CTL_KEY('R'): machine.state = Machine::STATE_ESCAPED; break;
+ case CTL_KEY('V'): machine.state = Machine::STATE_LOWER; break;
+ case CTL_KEY('W'): machine.state = Machine::STATE_UPPER; break;
+ case CTL_KEY('E'): machine.state = Machine::STATE_CTL_E; break;
+ default:
+ goto original;
+ }
+ break;
+
+ case Machine::STATE_LOWER:
+ machine.state = Machine::STATE_START;
+ if (chr != CTL_KEY('V'))
+ return g_strdup((gchar []){g_ascii_tolower(chr), '\0'});
+ machine.mode = Machine::MODE_LOWER;
+ break;
+
+ case Machine::STATE_UPPER:
+ machine.state = Machine::STATE_START;
+ if (chr != CTL_KEY('W'))
+ return g_strdup((gchar []){g_ascii_toupper(chr), '\0'});
+ machine.mode = Machine::MODE_UPPER;
+ break;
+
+ case Machine::STATE_CTL_E:
+ switch (g_ascii_toupper(chr)) {
+ case 'Q': machine.state = Machine::STATE_CTL_EQ; break;
+ case 'U': machine.state = Machine::STATE_CTL_EU; break;
+ default:
+ machine.state = Machine::STATE_START;
+ return g_strdup((gchar []){CTL_KEY('E'), chr, '\0'});
+ }
+ break;
+
+ case Machine::STATE_CTL_EU:
+ if (chr == '.') {
+ machine.state = Machine::STATE_CTL_EU_LOCAL;
+ } else {
+ QRegister *reg;
+
+ machine.state = Machine::STATE_START;
+ reg = QRegisters::globals[g_ascii_toupper(chr)];
+ if (!reg)
+ throw InvalidQRegError(chr);
+ return g_strdup(CHR2STR(reg->get_integer()));
+ }
+ break;
+
+ case Machine::STATE_CTL_EU_LOCAL: {
+ QRegister *reg;
+
+ machine.state = Machine::STATE_START;
+ reg = (*QRegisters::locals)[g_ascii_toupper(chr)];
+ if (!reg)
+ throw InvalidQRegError(chr, true);
+ return g_strdup(CHR2STR(reg->get_integer()));
+ }
+
+ case Machine::STATE_CTL_EQ:
+ if (chr == '.') {
+ machine.state = Machine::STATE_CTL_EQ_LOCAL;
+ } else {
+ QRegister *reg;
+
+ machine.state = Machine::STATE_START;
+ reg = QRegisters::globals[g_ascii_toupper(chr)];
+ if (!reg)
+ throw InvalidQRegError(chr);
+ return reg->get_string();
+ }
+ break;
+
+ case Machine::STATE_CTL_EQ_LOCAL: {
+ QRegister *reg;
+
+ machine.state = Machine::STATE_START;
+ reg = (*QRegisters::locals)[g_ascii_toupper(chr)];
+ if (!reg)
+ throw InvalidQRegError(chr, true);
+ return reg->get_string();
+ }
+
+ default:
+ g_assert(TRUE);
+ }
+
+ return NULL;
+
+original:
+ return g_strdup((gchar []){chr, '\0'});
+}
+
+State *
+StateExpectString::custom(gchar chr) throw (Error)
+{
+ gchar *insert;
+
+ 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 '\x1B':
+ case '{':
+ undo.push_var(escape_char) = g_ascii_toupper(chr);
+ return this;
+ }
+ }
+
+ if (escape_char == '{') {
+ switch (chr) {
+ case '{':
+ undo.push_var(nesting);
+ nesting++;
+ break;
+ case '}':
+ undo.push_var(nesting);
+ nesting--;
+ break;
+ }
+ } else if (g_ascii_toupper(chr) == escape_char) {
+ undo.push_var(nesting);
+ nesting--;
+ }
+
+ if (!nesting) {
+ State *next;
+ gchar *string = strings[0];
+
+ undo.push_str(strings[0]) = NULL;
+ if (last)
+ undo.push_var(escape_char) = '\x1B';
+ nesting = 1;
+
+ if (string_building) {
+ undo.push_var(machine);
+ machine.state = Machine::STATE_START;
+ machine.mode = Machine::MODE_NORMAL;
+ machine.toctl = false;
+ }
+
+ next = done(string ? : "");
+ g_free(string);
+ return next;
+ }
+
+ BEGIN_EXEC(this);
+
+ /*
+ * String building characters
+ */
+ if (string_building) {
+ undo.push_var(machine);
+ insert = machine_input(chr);
+ if (!insert)
+ return this;
+ } else {
+ insert = g_strdup((gchar []){chr, '\0'});
+ }
+
+ /*
+ * String accumulation
+ */
+ undo.push_str(strings[0]);
+ String::append(strings[0], insert);
+
+ process(strings[0], strlen(insert));
+ g_free(insert);
+ return this;
+}
+
+StateExpectQReg::StateExpectQReg() : State(), got_local(false)
+{
+ transitions['\0'] = this;
+}
+
+State *
+StateExpectQReg::custom(gchar chr) throw (Error)
+{
+ QRegister *reg;
+
+ if (chr == '.') {
+ undo.push_var(got_local) = true;
+ return this;
+ }
+ chr = g_ascii_toupper(chr);
+
+ if (got_local) {
+ undo.push_var(got_local) = false;
+ reg = (*QRegisters::locals)[chr];
+ } else {
+ reg = QRegisters::globals[chr];
+ }
+ if (!reg)
+ throw InvalidQRegError(chr, got_local);
+
+ return got_register(reg);
+}
+
+StateStart::StateStart() : State()
+{
+ transitions['\0'] = this;
+ init(" \f\r\n\v");
+
+ 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;
+ transitions['S'] = &States::search;
+ transitions['N'] = &States::searchall;
+
+ transitions['['] = &States::pushqreg;
+ transitions[']'] = &States::popqreg;
+ transitions['G'] = &States::getqregstring;
+ transitions['Q'] = &States::getqreginteger;
+ transitions['U'] = &States::setqreginteger;
+ transitions['%'] = &States::increaseqreg;
+ transitions['M'] = &States::macro;
+ transitions['X'] = &States::copytoqreg;
+}
+
+void
+StateStart::insert_integer(gint64 v)
+{
+ gchar buf[64+1]; /* maximum length if radix = 2 */
+ gchar *p = buf + sizeof(buf);
+
+ *--p = '\0';
+ interface.ssm(SCI_BEGINUNDOACTION);
+ if (v < 0) {
+ interface.ssm(SCI_ADDTEXT, 1, (sptr_t)"-");
+ v *= -1;
+ }
+ do {
+ *--p = '0' + (v % expressions.radix);
+ if (*p > '9')
+ *p += 'A' - '9';
+ } while ((v /= expressions.radix));
+ interface.ssm(SCI_ADDTEXT, buf + sizeof(buf) - p - 1,
+ (sptr_t)p);
+ interface.ssm(SCI_SCROLLCARET);
+ interface.ssm(SCI_ENDUNDOACTION);
+ ring.dirtify();
+
+ undo.push_msg(SCI_UNDO);
+}
+
+gint64
+StateStart::read_integer(void)
+{
+ uptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
+ gchar c = (gchar)interface.ssm(SCI_GETCHARAT, pos);
+ gint64 v = 0;
+ gint sign = 1;
+
+ if (c == '-') {
+ pos++;
+ sign = -1;
+ }
+
+ for (;;) {
+ c = g_ascii_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(gint64 n)
+{
+ sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
+
+ if (!Validate::pos(pos + n))
+ return FAILURE;
+
+ interface.ssm(SCI_GOTOPOS, pos + n);
+ undo.push_msg(SCI_GOTOPOS, pos);
+ return SUCCESS;
+}
+
+tecoBool
+StateStart::move_lines(gint64 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);
+ undo.push_msg(SCI_GOTOPOS, pos);
+ return SUCCESS;
+}
+
+tecoBool
+StateStart::delete_words(gint64 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;
+ }
+
+ undo.push_msg(SCI_GOTOPOS, pos);
+ undo.push_msg(SCI_UNDO);
+ ring.dirtify();
+
+ return SUCCESS;
+}
+
+State *
+StateStart::custom(gchar chr) throw (Error)
+{
+ gint64 v;
+ tecoBool rc;
+
+ /*
+ * <CTRL/x> commands implemented in StateCtrlCmd
+ */
+ if (IS_CTL(chr))
+ return States::control.get_next_state(CTL_ECHO(chr));
+
+ /*
+ * arithmetics
+ */
+ if (g_ascii_isdigit(chr)) {
+ BEGIN_EXEC(this);
+ expressions.add_digit(chr);
+ return this;
+ }
+
+ chr = g_ascii_toupper(chr);
+ switch (chr) {
+ case '/':
+ BEGIN_EXEC(this);
+ expressions.push_calc(Expressions::OP_DIV);
+ break;
+
+ case '*':
+ 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.push(Expressions::OP_BRACE);
+ break;
+
+ case ')':
+ BEGIN_EXEC(this);
+ expressions.eval(true);
+ break;
+
+ case ',':
+ BEGIN_EXEC(this);
+ expressions.eval();
+ expressions.push(Expressions::OP_NEW);
+ break;
+
+ case '.':
+ BEGIN_EXEC(this);
+ expressions.eval();
+ expressions.push(interface.ssm(SCI_GETCURRENTPOS));
+ break;
+
+ case 'Z':
+ BEGIN_EXEC(this);
+ expressions.eval();
+ expressions.push(interface.ssm(SCI_GETLENGTH));
+ break;
+
+ case 'H':
+ BEGIN_EXEC(this);
+ expressions.eval();
+ expressions.push(0);
+ expressions.push(interface.ssm(SCI_GETLENGTH));
+ break;
+
+ 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<gint>(nest_level);
+ nest_level++;
+ return this;
+ }
+ BEGIN_EXEC(this);
+
+ expressions.eval();
+ if (!expressions.args())
+ /* infinite loop */
+ expressions.push(-1);
+
+ if (!expressions.peek_num()) {
+ expressions.pop_num();
+
+ /* skip to end of loop */
+ undo.push_var<Mode>(mode);
+ mode = MODE_PARSE_ONLY_LOOP;
+ } else {
+ expressions.push(macro_pc);
+ expressions.push(Expressions::OP_LOOP);
+ }
+ break;
+
+ case '>':
+ if (mode == MODE_PARSE_ONLY_LOOP) {
+ if (!nest_level) {
+ undo.push_var<Mode>(mode);
+ mode = MODE_NORMAL;
+ } else {
+ undo.push_var<gint>(nest_level);
+ nest_level--;
+ }
+ } else {
+ BEGIN_EXEC(this);
+ gint64 loop_pc, loop_cnt;
+
+ expressions.discard_args();
+ g_assert(expressions.pop_op() == Expressions::OP_LOOP);
+ loop_pc = expressions.pop_num();
+ loop_cnt = expressions.pop_num();
+
+ if (loop_cnt != 1) {
+ /* repeat loop */
+ macro_pc = loop_pc;
+ expressions.push(MAX(loop_cnt - 1, -1));
+ expressions.push(loop_pc);
+ expressions.push(Expressions::OP_LOOP);
+ }
+ }
+ break;
+
+ case ';':
+ BEGIN_EXEC(this);
+
+ v = QRegisters::globals["_"]->get_integer();
+ rc = expressions.pop_num_calc(1, v);
+ if (eval_colon())
+ rc = ~rc;
+
+ if (IS_FAILURE(rc)) {
+ expressions.discard_args();
+ g_assert(expressions.pop_op() == Expressions::OP_LOOP);
+ expressions.pop_num(); /* pc */
+ expressions.pop_num(); /* counter */
+
+ /* skip to end of loop */
+ undo.push_var<Mode>(mode);
+ 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;
+
+ /*
+ * 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
+ */
+ case 'J':
+ BEGIN_EXEC(this);
+ v = expressions.pop_num_calc(1, 0);
+ if (Validate::pos(v)) {
+ undo.push_msg(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;
+
+ 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;
+
+ 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;
+
+ 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;
+
+ 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;
+
+ 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) {
+ undo.push_msg(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;
+ }
+
+ 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;
+
+ 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;
+
+ case '=':
+ BEGIN_EXEC(this);
+ interface.msg(Interface::MSG_USER, "%" G_GINT64_FORMAT,
+ expressions.pop_num_calc());
+ break;
+
+ case 'K':
+ case 'D': {
+ gint64 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 {
+ gint64 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;
+
+ undo.push_msg(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS));
+ undo.push_msg(SCI_UNDO);
+
+ interface.ssm(SCI_BEGINUNDOACTION);
+ interface.ssm(SCI_DELETERANGE, from, len);
+ interface.ssm(SCI_ENDUNDOACTION);
+ ring.dirtify();
+ break;
+ }
+
+ case 'A':
+ BEGIN_EXEC(this);
+ v = interface.ssm(SCI_GETCURRENTPOS) +
+ expressions.pop_num_calc();
+ if (!Validate::pos(v))
+ throw RangeError("A");
+ expressions.push(interface.ssm(SCI_GETCHARAT, v));
+ break;
+
+ default:
+ throw SyntaxError(chr);
+ }
+
+ return this;
+}
+
+StateFCommand::StateFCommand() : State()
+{
+ transitions['\0'] = this;
+ transitions['K'] = &States::searchkill;
+ transitions['D'] = &States::searchdelete;
+ transitions['S'] = &States::replace;
+ transitions['R'] = &States::replacedefault;
+}
+
+State *
+StateFCommand::custom(gchar chr) throw (Error)
+{
+ switch (chr) {
+ /*
+ * loop flow control
+ */
+ case '<':
+ BEGIN_EXEC(&States::start);
+ /* FIXME: what if in brackets? */
+ expressions.discard_args();
+ if (expressions.peek_op() == Expressions::OP_LOOP)
+ /* repeat loop */
+ macro_pc = expressions.peek_num();
+ else
+ macro_pc = -1;
+ break;
+
+ case '>': {
+ gint64 loop_pc, loop_cnt;
+
+ BEGIN_EXEC(&States::start);
+ /* FIXME: what if in brackets? */
+ expressions.discard_args();
+ g_assert(expressions.pop_op() == Expressions::OP_LOOP);
+ loop_pc = expressions.pop_num();
+ loop_cnt = expressions.pop_num();
+
+ if (loop_cnt != 1) {
+ /* repeat loop */
+ macro_pc = loop_pc;
+ expressions.push(MAX(loop_cnt - 1, -1));
+ expressions.push(loop_pc);
+ expressions.push(Expressions::OP_LOOP);
+ } else {
+ /* skip to end of loop */
+ undo.push_var<Mode>(mode);
+ mode = MODE_PARSE_ONLY_LOOP;
+ }
+ break;
+ }
+
+ /*
+ * conditional flow control
+ */
+ 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;
+
+ 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;
+}
+
+StateCondCommand::StateCondCommand() : State()
+{
+ transitions['\0'] = this;
+}
+
+State *
+StateCondCommand::custom(gchar chr) throw (Error)
+{
+ gint64 value = 0;
+ bool result;
+
+ switch (mode) {
+ case MODE_PARSE_ONLY_COND:
+ undo.push_var<gint>(nest_level);
+ nest_level++;
+ break;
+ case MODE_NORMAL:
+ value = expressions.pop_num_calc();
+ break;
+ default:
+ break;
+ }
+
+ switch (g_ascii_toupper(chr)) {
+ 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 'E':
+ case 'F':
+ case 'U':
+ case '=':
+ BEGIN_EXEC(&States::start);
+ result = value == 0;
+ break;
+ case 'G':
+ case '>':
+ BEGIN_EXEC(&States::start);
+ result = value > 0;
+ break;
+ case 'L':
+ case 'S':
+ case 'T':
+ 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);
+ mode = MODE_PARSE_ONLY_COND;
+ }
+
+ return &States::start;
+}
+
+StateControl::StateControl() : State()
+{
+ transitions['\0'] = this;
+ transitions['U'] = &States::ctlucommand;
+}
+
+State *
+StateControl::custom(gchar chr) throw (Error)
+{
+ switch (g_ascii_toupper(chr)) {
+ case 'O':
+ BEGIN_EXEC(&States::start);
+ expressions.set_radix(8);
+ break;
+
+ case 'D':
+ BEGIN_EXEC(&States::start);
+ expressions.set_radix(10);
+ break;
+
+ case 'R':
+ BEGIN_EXEC(&States::start);
+ expressions.eval();
+ if (!expressions.args())
+ expressions.push(expressions.radix);
+ else
+ expressions.set_radix(expressions.pop_num_calc());
+ break;
+
+ /*
+ * Alternatives: ^i, ^I, <CTRL/I>, <TAB>
+ */
+ case 'I':
+ BEGIN_EXEC(&States::insert);
+ expressions.eval();
+ expressions.push('\t');
+ return &States::insert;
+
+ /*
+ * Alternatives: ^[, <CTRL/[>, <ESC>
+ */
+ case '[':
+ BEGIN_EXEC(&States::start);
+ expressions.discard_args();
+ break;
+
+ /*
+ * Additional numeric operations
+ */
+ 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;
+
+ default:
+ throw Error("Unsupported command <^%c>", chr);
+ }
+
+ return &States::start;
+}
+
+StateECommand::StateECommand() : State()
+{
+ transitions['\0'] = this;
+ transitions['B'] = &States::editfile;
+ transitions['S'] = &States::scintilla_symbols;
+ transitions['Q'] = &States::eqcommand;
+ transitions['W'] = &States::savefile;
+}
+
+State *
+StateECommand::custom(gchar chr) throw (Error)
+{
+ switch (g_ascii_toupper(chr)) {
+ case 'F':
+ BEGIN_EXEC(&States::start);
+ if (!ring.current)
+ throw Error("No buffer selected");
+
+ if (IS_FAILURE(expressions.pop_num_calc()) &&
+ ring.current->dirty)
+ throw Error("Buffer \"%s\" is dirty",
+ ring.current->filename ? : "(Unnamed)");
+
+ ring.close();
+ break;
+
+ case 'D':
+ BEGIN_EXEC(&States::start);
+ expressions.eval();
+ if (!expressions.args()) {
+ expressions.push(Flags::ed);
+ } else {
+ gint64 on = expressions.pop_num_calc();
+ gint64 off = expressions.pop_num_calc(1, ~(gint64)0);
+
+ undo.push_var(Flags::ed);
+ Flags::ed = (Flags::ed & ~off) | on;
+ }
+ break;
+
+ case 'X':
+ BEGIN_EXEC(&States::start);
+
+ if (IS_FAILURE(expressions.pop_num_calc()) &&
+ ring.is_any_dirty())
+ throw Error("Modified buffers exist");
+
+ undo.push_var<bool>(quit_requested);
+ 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};
+
+State *
+StateScintilla_symbols::done(const gchar *str) throw (Error)
+{
+ BEGIN_EXEC(&States::scintilla_lparam);
+
+ undo.push_var(scintilla_message);
+ if (*str) {
+ gchar **symbols = g_strsplit(str, ",", -1);
+ gint64 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(1, 0);
+ }
+ if (!scintilla_message.wParam)
+ scintilla_message.wParam = expressions.pop_num_calc(1, 0);
+
+ return &States::scintilla_lparam;
+}
+
+State *
+StateScintilla_lParam::done(const gchar *str) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ if (!scintilla_message.lParam)
+ scintilla_message.lParam = *str ? (sptr_t)str
+ : expressions.pop_num_calc(1, 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
+ */
+void
+StateInsert::initial(void) throw (Error)
+{
+ int 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);
+ 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();
+
+ undo.push_msg(SCI_UNDO);
+}
+
+void
+StateInsert::process(const gchar *str, gint new_chars) throw (Error)
+{
+ 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();
+
+ undo.push_msg(SCI_UNDO);
+}
+
+State *
+StateInsert::done(const gchar *str __attribute__((unused))) throw (Error)
+{
+ /* nothing to be done when done */
+ return &States::start;
+}
diff --git a/src/parser.h b/src/parser.h
new file mode 100644
index 0000000..4ae9db6
--- /dev/null
+++ b/src/parser.h
@@ -0,0 +1,266 @@
+#ifndef __PARSER_H
+#define __PARSER_H
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "sciteco.h"
+
+/* TECO uses only lower 7 bits for commands */
+#define MAX_TRANSITIONS 127
+
+class State {
+public:
+ class Error {
+ public:
+ Error(const gchar *fmt, ...);
+ };
+
+ class SyntaxError : public Error {
+ public:
+ SyntaxError(gchar chr)
+ : Error("Syntax error \"%c\" (%d)", chr, chr) {}
+ };
+
+ 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) {}
+ };
+
+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);
+ }
+
+public:
+ State();
+
+ static void input(gchar chr) throw (Error);
+ State *get_next_state(gchar chr) throw (Error);
+
+protected:
+ static bool eval_colon(void);
+
+ virtual State *
+ custom(gchar chr) throw (Error)
+ {
+ throw SyntaxError(chr);
+ return NULL;
+ }
+};
+
+/*
+ * Super-class for states accepting string arguments
+ * Opaquely cares about alternative-escape characters,
+ * string building commands and accumulation into a string
+ */
+class StateExpectString : public State {
+ struct Machine {
+ enum State {
+ STATE_START,
+ STATE_ESCAPED,
+ STATE_LOWER,
+ STATE_UPPER,
+ STATE_CTL_E,
+ STATE_CTL_EQ,
+ STATE_CTL_EQ_LOCAL,
+ STATE_CTL_EU,
+ STATE_CTL_EU_LOCAL
+ } state;
+
+ enum Mode {
+ MODE_NORMAL,
+ MODE_UPPER,
+ MODE_LOWER
+ } mode;
+
+ bool toctl;
+
+ Machine() : state(STATE_START),
+ mode(MODE_NORMAL), toctl(false) {}
+ } machine;
+
+ gint nesting;
+
+ bool string_building;
+ bool last;
+
+public:
+ StateExpectString(bool _building = true, bool _last = true)
+ : State(), nesting(1),
+ string_building(_building), last(_last) {}
+
+private:
+ gchar *machine_input(gchar key) throw (Error);
+ State *custom(gchar chr) throw (Error);
+
+protected:
+ virtual void initial(void) throw (Error) {}
+ virtual void process(const gchar *str, gint new_chars) throw (Error) {}
+ virtual State *done(const gchar *str) throw (Error) = 0;
+};
+
+class QRegister;
+
+/*
+ * Super class for states accepting Q-Register specifications
+ */
+class StateExpectQReg : public State {
+ bool got_local;
+
+public:
+ StateExpectQReg();
+
+private:
+ State *custom(gchar chr) throw (Error);
+
+protected:
+ /*
+ * FIXME: would be nice to pass reg as reference, but there are
+ * circular header dependencies...
+ */
+ virtual State *got_register(QRegister *reg) throw (Error) = 0;
+};
+
+class StateStart : public State {
+public:
+ StateStart();
+
+private:
+ void insert_integer(gint64 v);
+ gint64 read_integer(void);
+
+ tecoBool move_chars(gint64 n);
+ tecoBool move_lines(gint64 n);
+
+ tecoBool delete_words(gint64 n);
+
+ State *custom(gchar chr) throw (Error);
+};
+
+class StateControl : public State {
+public:
+ StateControl();
+
+private:
+ State *custom(gchar chr) throw (Error);
+};
+
+class StateFCommand : public State {
+public:
+ StateFCommand();
+
+private:
+ State *custom(gchar chr) throw (Error);
+};
+
+class StateCondCommand : public State {
+public:
+ StateCondCommand();
+
+private:
+ State *custom(gchar chr) throw (Error);
+};
+
+class StateECommand : public State {
+public:
+ StateECommand();
+
+private:
+ State *custom(gchar chr) throw (Error);
+};
+
+class StateScintilla_symbols : public StateExpectString {
+public:
+ StateScintilla_symbols() : StateExpectString(true, false) {}
+
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateScintilla_lParam : public StateExpectString {
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+/*
+ * also serves as base class for replace-insertion states
+ */
+class StateInsert : public StateExpectString {
+protected:
+ void initial(void) throw (Error);
+ void process(const gchar *str, gint new_chars) throw (Error);
+ State *done(const gchar *str) throw (Error);
+};
+
+namespace States {
+ extern StateStart start;
+ extern StateControl control;
+ extern StateFCommand fcommand;
+ extern StateCondCommand condcommand;
+ extern StateECommand ecommand;
+ extern StateScintilla_symbols scintilla_symbols;
+ extern StateScintilla_lParam scintilla_lparam;
+ extern StateInsert insert;
+
+ extern State *current;
+}
+
+extern enum Mode {
+ MODE_NORMAL = 0,
+ MODE_PARSE_ONLY_GOTO,
+ MODE_PARSE_ONLY_LOOP,
+ MODE_PARSE_ONLY_COND
+} mode;
+
+#define BEGIN_EXEC(STATE) G_STMT_START { \
+ if (mode > MODE_NORMAL) \
+ return STATE; \
+} G_STMT_END
+
+extern gint macro_pc;
+
+extern gchar *strings[2];
+extern gchar escape_char;
+
+namespace Execute {
+ void step(const gchar *macro) throw (State::Error);
+ void macro(const gchar *macro, bool locals = true) throw (State::Error);
+ bool file(const gchar *filename, bool locals = true);
+}
+
+#endif
diff --git a/src/qregisters.cpp b/src/qregisters.cpp
new file mode 100644
index 0000000..06947f7
--- /dev/null
+++ b/src/qregisters.cpp
@@ -0,0 +1,522 @@
+#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 "interface.h"
+#include "undo.h"
+#include "parser.h"
+#include "expressions.h"
+#include "ring.h"
+#include "qregisters.h"
+
+namespace States {
+ StatePushQReg pushqreg;
+ StatePopQReg popqreg;
+ StateEQCommand eqcommand;
+ StateLoadQReg loadqreg;
+ StateCtlUCommand ctlucommand;
+ StateSetQRegString setqregstring;
+ StateGetQRegString getqregstring;
+ StateGetQRegInteger getqreginteger;
+ StateSetQRegInteger setqreginteger;
+ StateIncreaseQReg increaseqreg;
+ StateMacro macro;
+ StateCopyToQReg copytoqreg;
+}
+
+namespace QRegisters {
+ QRegisterTable globals;
+ QRegisterTable *locals = NULL;
+ QRegister *current = NULL;
+
+ static QRegisterStack stack;
+}
+
+static QRegister *register_argument = NULL;
+
+static inline void
+current_edit(void)
+{
+ if (ring.current)
+ ring.current->edit();
+ else if (QRegisters::current)
+ QRegisters::current->edit();
+}
+
+void
+QRegisterData::set_string(const gchar *str)
+{
+ edit();
+ dot = 0;
+
+ interface.ssm(SCI_BEGINUNDOACTION);
+ interface.ssm(SCI_SETTEXT, 0, (sptr_t)(str ? : ""));
+ interface.ssm(SCI_ENDUNDOACTION);
+
+ current_edit();
+}
+
+void
+QRegisterData::undo_set_string(void)
+{
+ /* set_string() assumes that dot has been saved */
+ current_save_dot();
+
+ if (!must_undo)
+ return;
+
+ if (ring.current)
+ ring.current->undo_edit();
+ else if (QRegisters::current)
+ QRegisters::current->undo_edit();
+
+ undo.push_var<gint>(dot);
+ undo.push_msg(SCI_UNDO);
+
+ undo_edit();
+}
+
+void
+QRegisterData::append_string(const gchar *str)
+{
+ if (!str)
+ return;
+
+ edit();
+
+ interface.ssm(SCI_BEGINUNDOACTION);
+ interface.ssm(SCI_APPENDTEXT, strlen(str), (sptr_t)str);
+ interface.ssm(SCI_ENDUNDOACTION);
+
+ current_edit();
+}
+
+gchar *
+QRegisterData::get_string(void)
+{
+ gint size;
+ gchar *str;
+
+ current_save_dot();
+ edit();
+
+ size = interface.ssm(SCI_GETLENGTH) + 1;
+ str = (gchar *)g_malloc(size);
+ interface.ssm(SCI_GETTEXT, size, (sptr_t)str);
+
+ current_edit();
+
+ return str;
+}
+
+void
+QRegisterData::edit(void)
+{
+ interface.ssm(SCI_SETDOCPOINTER, 0, (sptr_t)get_document());
+ interface.ssm(SCI_GOTOPOS, dot);
+}
+
+void
+QRegisterData::undo_edit(void)
+{
+ if (!must_undo)
+ return;
+
+ undo.push_msg(SCI_GOTOPOS, dot);
+ undo.push_msg(SCI_SETDOCPOINTER, 0, (sptr_t)get_document());
+}
+
+void
+QRegister::edit(void)
+{
+ QRegisterData::edit();
+ interface.info_update(this);
+}
+
+void
+QRegister::undo_edit(void)
+{
+ if (!must_undo)
+ return;
+
+ interface.undo_info_update(this);
+ QRegisterData::undo_edit();
+}
+
+void
+QRegister::execute(bool locals) throw (State::Error)
+{
+ gchar *str = get_string();
+
+ try {
+ Execute::macro(str, locals);
+ } catch (...) {
+ g_free(str);
+ throw; /* forward */
+ }
+
+ g_free(str);
+}
+
+bool
+QRegister::load(const gchar *filename)
+{
+ gchar *contents;
+ gsize size;
+
+ /* FIXME: prevent excessive allocations by reading file into buffer */
+ if (!g_file_get_contents(filename, &contents, &size, NULL))
+ return false;
+
+ edit();
+ dot = 0;
+
+ interface.ssm(SCI_BEGINUNDOACTION);
+ interface.ssm(SCI_CLEARALL);
+ interface.ssm(SCI_APPENDTEXT, size, (sptr_t)contents);
+ interface.ssm(SCI_ENDUNDOACTION);
+
+ g_free(contents);
+
+ current_edit();
+
+ return true;
+}
+
+gint64
+QRegisterBufferInfo::get_integer(void)
+{
+ gint64 id = 1;
+
+ if (!ring.current)
+ return 0;
+
+ for (Buffer *buffer = ring.first();
+ buffer != ring.current;
+ buffer = buffer->next())
+ id++;
+
+ return id;
+}
+
+gchar *
+QRegisterBufferInfo::get_string(void)
+{
+ gchar *filename = ring.current ? ring.current->filename : NULL;
+
+ return g_strdup(filename ? : "");
+}
+
+void
+QRegisterBufferInfo::edit(void)
+{
+ gchar *filename = ring.current ? ring.current->filename : NULL;
+
+ QRegister::edit();
+
+ interface.ssm(SCI_BEGINUNDOACTION);
+ interface.ssm(SCI_SETTEXT, 0, (sptr_t)(filename ? : ""));
+ interface.ssm(SCI_ENDUNDOACTION);
+
+ undo.push_msg(SCI_UNDO);
+}
+
+void
+QRegisterTable::initialize(void)
+{
+ /* general purpose registers */
+ for (gchar q = 'A'; q <= 'Z'; q++)
+ initialize(q);
+ for (gchar q = '0'; q <= '9'; q++)
+ initialize(q);
+}
+
+void
+QRegisterTable::edit(QRegister *reg)
+{
+ current_save_dot();
+ reg->edit();
+
+ ring.current = NULL;
+ QRegisters::current = reg;
+}
+
+void
+QRegisterStack::UndoTokenPush::run(void)
+{
+ SLIST_INSERT_HEAD(&stack->head, entry, entries);
+ entry = NULL;
+}
+
+void
+QRegisterStack::UndoTokenPop::run(void)
+{
+ Entry *entry = SLIST_FIRST(&stack->head);
+
+ SLIST_REMOVE_HEAD(&stack->head, entries);
+ delete entry;
+}
+
+void
+QRegisterStack::push(QRegister *reg)
+{
+ Entry *entry = new Entry();
+
+ entry->set_integer(reg->get_integer());
+ if (reg->string) {
+ gchar *str = reg->get_string();
+ entry->set_string(str);
+ g_free(str);
+ }
+ entry->dot = reg->dot;
+
+ SLIST_INSERT_HEAD(&head, entry, entries);
+ undo.push(new UndoTokenPop(this));
+}
+
+bool
+QRegisterStack::pop(QRegister *reg)
+{
+ Entry *entry = SLIST_FIRST(&head);
+ QRegisterData::document *string;
+
+ if (!entry)
+ return false;
+
+ reg->undo_set_integer();
+ reg->set_integer(entry->get_integer());
+
+ /* exchange document ownership between Stack entry and Q-Register */
+ string = reg->string;
+ if (reg->must_undo)
+ undo.push_var(reg->string);
+ reg->string = entry->string;
+ undo.push_var(entry->string);
+ entry->string = string;
+
+ if (reg->must_undo)
+ undo.push_var(reg->dot);
+ reg->dot = entry->dot;
+
+ SLIST_REMOVE_HEAD(&head, entries);
+ /* pass entry ownership to undo stack */
+ undo.push(new UndoTokenPush(this, entry));
+
+ return true;
+}
+
+QRegisterStack::~QRegisterStack()
+{
+ Entry *entry, *next;
+
+ SLIST_FOREACH_SAFE(entry, &head, entries, next)
+ delete entry;
+}
+
+void
+QRegisters::hook(Hook type)
+{
+ if (!(Flags::ed & Flags::ED_HOOKS))
+ return;
+
+ expressions.push(type);
+ globals["0"]->execute();
+}
+
+/*
+ * Command states
+ */
+
+State *
+StatePushQReg::got_register(QRegister *reg) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ QRegisters::stack.push(reg);
+
+ return &States::start;
+}
+
+State *
+StatePopQReg::got_register(QRegister *reg) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ if (!QRegisters::stack.pop(reg))
+ throw Error("Q-Register stack is empty");
+
+ return &States::start;
+}
+
+State *
+StateEQCommand::got_register(QRegister *reg) throw (Error)
+{
+ BEGIN_EXEC(&States::loadqreg);
+ register_argument = reg;
+ return &States::loadqreg;
+}
+
+State *
+StateLoadQReg::done(const gchar *str) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ if (*str) {
+ register_argument->undo_load();
+ if (!register_argument->load(str))
+ throw Error("Cannot load \"%s\" into Q-Register \"%s\"",
+ str, register_argument->name);
+ } else {
+ if (ring.current)
+ ring.undo_edit();
+ else /* QRegisters::current != NULL */
+ QRegisters::undo_edit();
+ QRegisters::globals.edit(register_argument);
+ }
+
+ return &States::start;
+}
+
+State *
+StateCtlUCommand::got_register(QRegister *reg) throw (Error)
+{
+ BEGIN_EXEC(&States::setqregstring);
+ register_argument = reg;
+ return &States::setqregstring;
+}
+
+State *
+StateSetQRegString::done(const gchar *str) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ register_argument->undo_set_string();
+ register_argument->set_string(str);
+
+ return &States::start;
+}
+
+State *
+StateGetQRegString::got_register(QRegister *reg) throw (Error)
+{
+ gchar *str;
+
+ 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();
+
+ undo.push_msg(SCI_UNDO);
+ }
+ g_free(str);
+
+ return &States::start;
+}
+
+State *
+StateGetQRegInteger::got_register(QRegister *reg) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ expressions.eval();
+ expressions.push(reg->get_integer());
+
+ return &States::start;
+}
+
+State *
+StateSetQRegInteger::got_register(QRegister *reg) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ reg->undo_set_integer();
+ reg->set_integer(expressions.pop_num_calc());
+
+ return &States::start;
+}
+
+State *
+StateIncreaseQReg::got_register(QRegister *reg) throw (Error)
+{
+ gint64 res;
+
+ 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;
+}
+
+State *
+StateMacro::got_register(QRegister *reg) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ /* don't create new local Q-Registers if colon modifier is given */
+ reg->execute(!eval_colon());
+
+ return &States::start;
+}
+
+State *
+StateCopyToQReg::got_register(QRegister *reg) throw (Error)
+{
+ gint64 from, len;
+ Sci_TextRange tr;
+
+ 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 {
+ gint64 to = expressions.pop_num();
+ from = expressions.pop_num();
+
+ if (!Validate::pos(from) || !Validate::pos(to))
+ throw RangeError("X");
+
+ len = to - from;
+ }
+
+ 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;
+}
diff --git a/src/qregisters.h b/src/qregisters.h
new file mode 100644
index 0000000..e810b7f
--- /dev/null
+++ b/src/qregisters.h
@@ -0,0 +1,343 @@
+#ifndef __QREGISTERS_H
+#define __QREGISTERS_H
+
+#include <bsd/sys/queue.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+#include "interface.h"
+#include "undo.h"
+#include "rbtree.h"
+#include "parser.h"
+
+/*
+ * Classes
+ */
+
+class QRegisterData {
+ gint64 integer;
+
+public:
+ typedef void document;
+ document *string;
+ gint dot;
+
+ /*
+ * whether to generate UndoTokens (unnecessary in macro invocations)
+ */
+ bool must_undo;
+
+ QRegisterData() : integer(0), string(NULL), dot(0), must_undo(true) {}
+ virtual
+ ~QRegisterData()
+ {
+ if (string)
+ interface.ssm(SCI_RELEASEDOCUMENT, 0, (sptr_t)string);
+ }
+
+ inline document *
+ get_document(void)
+ {
+ if (!string)
+ string = (document *)interface.ssm(SCI_CREATEDOCUMENT);
+ return string;
+ }
+
+ virtual gint64
+ set_integer(gint64 i)
+ {
+ return integer = i;
+ }
+ virtual void
+ undo_set_integer(void)
+ {
+ if (must_undo)
+ undo.push_var(integer);
+ }
+ virtual gint64
+ get_integer(void)
+ {
+ return integer;
+ }
+
+ virtual void set_string(const gchar *str);
+ virtual void undo_set_string(void);
+ virtual void append_string(const gchar *str);
+ virtual inline void
+ undo_append_string(void)
+ {
+ undo_set_string();
+ }
+ virtual gchar *get_string(void);
+
+ virtual void edit(void);
+ virtual void undo_edit(void);
+};
+
+class QRegister : public RBTree::RBEntry, public QRegisterData {
+public:
+ gchar *name;
+
+ QRegister(const gchar *_name)
+ : QRegisterData(), name(g_strdup(_name)) {}
+ virtual
+ ~QRegister()
+ {
+ g_free(name);
+ }
+
+ int
+ operator <(RBEntry &entry)
+ {
+ return g_strcmp0(name, ((QRegister &)entry).name);
+ }
+
+ virtual void edit(void);
+ virtual void undo_edit(void);
+
+ void execute(bool locals = true) throw (State::Error);
+
+ bool load(const gchar *filename);
+ inline void
+ undo_load(void)
+ {
+ undo_set_string();
+ }
+};
+
+class QRegisterBufferInfo : public QRegister {
+public:
+ QRegisterBufferInfo() : QRegister("*")
+ {
+ get_document();
+ }
+
+ gint64
+ set_integer(gint64 v)
+ {
+ return v;
+ }
+ void undo_set_integer(void) {}
+
+ gint64 get_integer(void);
+
+ void set_string(const gchar *str) {}
+ void undo_set_string(void) {}
+ void append_string(const gchar *str) {}
+ void undo_append_string(void) {}
+
+ gchar *get_string(void);
+
+ void edit(void);
+};
+
+class QRegisterTable : public RBTree {
+ bool must_undo;
+
+public:
+ QRegisterTable(bool _undo = true) : RBTree(), must_undo(_undo) {}
+
+ inline QRegister *
+ insert(QRegister *reg)
+ {
+ reg->must_undo = must_undo;
+ RBTree::insert(reg);
+ return reg;
+ }
+
+ inline void
+ initialize(const gchar *name)
+ {
+ QRegister *reg = new QRegister(name);
+ insert(reg);
+ /* make sure document is initialized */
+ reg->get_document();
+ }
+ inline void
+ initialize(gchar name)
+ {
+ initialize((gchar []){name, '\0'});
+ }
+ void initialize(void);
+
+ inline QRegister *
+ operator [](const gchar *name)
+ {
+ QRegister reg(name);
+ return (QRegister *)find(&reg);
+ }
+ inline QRegister *
+ operator [](gchar chr)
+ {
+ return operator []((gchar []){chr, '\0'});
+ }
+
+ void edit(QRegister *reg);
+ inline QRegister *
+ edit(const gchar *name)
+ {
+ QRegister *reg = operator [](name);
+
+ if (!reg)
+ return NULL;
+ edit(reg);
+ return reg;
+ }
+};
+
+class QRegisterStack {
+ 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()
+ {
+ if (entry)
+ delete entry;
+ }
+
+ void run(void);
+ };
+
+ class UndoTokenPop : public UndoToken {
+ QRegisterStack *stack;
+
+ public:
+ UndoTokenPop(QRegisterStack *_stack)
+ : UndoToken(), stack(_stack) {}
+
+ void run(void);
+ };
+
+ SLIST_HEAD(Head, Entry) head;
+
+public:
+ QRegisterStack()
+ {
+ SLIST_INIT(&head);
+ }
+ ~QRegisterStack();
+
+ void push(QRegister *reg);
+ bool pop(QRegister *reg);
+};
+
+/*
+ * Command states
+ */
+
+class StatePushQReg : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StatePopQReg : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StateEQCommand : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StateLoadQReg : public StateExpectString {
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateCtlUCommand : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StateSetQRegString : public StateExpectString {
+public:
+ StateSetQRegString() : StateExpectString(false) {}
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateGetQRegString : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StateGetQRegInteger : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StateSetQRegInteger : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StateIncreaseQReg : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StateMacro : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+class StateCopyToQReg : public StateExpectQReg {
+private:
+ State *got_register(QRegister *reg) throw (Error);
+};
+
+namespace States {
+ extern StatePushQReg pushqreg;
+ extern StatePopQReg popqreg;
+ extern StateEQCommand eqcommand;
+ extern StateLoadQReg loadqreg;
+ extern StateCtlUCommand ctlucommand;
+ extern StateSetQRegString setqregstring;
+ extern StateGetQRegString getqregstring;
+ extern StateGetQRegInteger getqreginteger;
+ extern StateSetQRegInteger setqreginteger;
+ extern StateIncreaseQReg increaseqreg;
+ extern StateMacro macro;
+ extern StateCopyToQReg copytoqreg;
+}
+
+namespace QRegisters {
+ extern QRegisterTable globals;
+ extern QRegisterTable *locals;
+ extern QRegister *current;
+
+ static inline void
+ undo_edit(void)
+ {
+ current->dot = interface.ssm(SCI_GETCURRENTPOS);
+ undo.push_var(current)->undo_edit();
+ }
+
+ enum Hook {
+ HOOK_ADD = 1,
+ HOOK_EDIT,
+ HOOK_CLOSE,
+ HOOK_QUIT
+ };
+ void hook(Hook type);
+}
+
+#endif
diff --git a/src/rbtree.cpp b/src/rbtree.cpp
new file mode 100644
index 0000000..28df67b
--- /dev/null
+++ b/src/rbtree.cpp
@@ -0,0 +1,5 @@
+#include <bsd/sys/tree.h>
+
+#include "rbtree.h"
+
+RB_GENERATE(RBTree::Tree, RBTree::RBEntry, nodes, RBTree::compare_entries);
diff --git a/src/rbtree.h b/src/rbtree.h
new file mode 100644
index 0000000..d5f2a6e
--- /dev/null
+++ b/src/rbtree.h
@@ -0,0 +1,282 @@
+#ifndef __RBTREE_H
+#define __RBTREE_H
+
+#include <bsd/sys/tree.h>
+
+#include <glib.h>
+
+#include "undo.h"
+
+class RBTree {
+public:
+ class RBEntry;
+
+private:
+ RB_HEAD(Tree, RBEntry) head;
+
+ RB_PROTOTYPE_INTERNAL(Tree, RBEntry, nodes, /* unused */, static);
+
+public:
+ class RBEntry {
+ public:
+ RB_ENTRY(RBEntry) nodes;
+
+ virtual ~RBEntry() {}
+
+ inline RBEntry *
+ next(void)
+ {
+ return RBTree::Tree_RB_NEXT(this);
+ }
+ inline RBEntry *
+ prev(void)
+ {
+ return RBTree::Tree_RB_PREV(this);
+ }
+
+ virtual int operator <(RBEntry &entry) = 0;
+ };
+
+private:
+ static inline int
+ compare_entries(RBEntry *e1, RBEntry *e2)
+ {
+ return *e1 < *e2;
+ }
+
+public:
+ RBTree()
+ {
+ RB_INIT(&head);
+ }
+ virtual
+ ~RBTree()
+ {
+ clear();
+ }
+
+ inline RBEntry *
+ insert(RBEntry *entry)
+ {
+ RB_INSERT(Tree, &head, entry);
+ return entry;
+ }
+ inline RBEntry *
+ remove(RBEntry *entry)
+ {
+ return RB_REMOVE(Tree, &head, entry);
+ }
+ inline RBEntry *
+ find(RBEntry *entry)
+ {
+ return RB_FIND(Tree, &head, entry);
+ }
+ inline RBEntry *
+ nfind(RBEntry *entry)
+ {
+ return RB_NFIND(Tree, &head, entry);
+ }
+ inline RBEntry *
+ min(void)
+ {
+ return RB_MIN(Tree, &head);
+ }
+ inline RBEntry *
+ max(void)
+ {
+ return RB_MAX(Tree, &head);
+ }
+
+ inline void
+ clear(void)
+ {
+ RBEntry *cur;
+
+ while ((cur = min())) {
+ remove(cur);
+ delete cur;
+ }
+ }
+};
+
+template <typename KeyType, typename ValueType>
+class Table : public RBTree {
+ class TableEntry : public RBEntry {
+ public:
+ KeyType key;
+ ValueType value;
+
+ TableEntry(KeyType &_key, ValueType &_value)
+ : key(_key), value(_value) {}
+
+ int
+ operator <(RBEntry &entry)
+ {
+ return key < ((TableEntry &)entry).key;
+ }
+ };
+
+public:
+ ValueType nil;
+
+ Table(ValueType &_nil) : RBTree(), nil(_nil) {}
+
+ inline bool
+ hasEntry(KeyType &key)
+ {
+ return find(&TableEntry(key, nil)) != NULL;
+ }
+
+ ValueType &
+ operator [](KeyType &key)
+ {
+ TableEntry *entry = new TableEntry(key, nil);
+ TableEntry *existing = (TableEntry *)find(entry);
+
+ if (existing)
+ delete entry;
+ else
+ existing = (TableEntry *)insert(entry);
+
+ return existing->value;
+ }
+
+ inline void
+ remove(KeyType &key)
+ {
+ TableEntry entry(key, nil);
+ TableEntry *existing = (TableEntry *)find(&entry);
+
+ if (existing)
+ RBTree::remove(existing);
+ }
+
+#if 0
+ void
+ dump(void)
+ {
+ RBEntry *cur;
+
+ RB_FOREACH(cur, Tree, &head)
+ g_printf("tree[\"%s\"] = %d\n", cur->name, cur->pc);
+ g_printf("---END---\n");
+ }
+#endif
+
+ void
+ clear(void)
+ {
+ RBEntry *cur;
+
+ while ((cur = min())) {
+ RBTree::remove(cur);
+ delete cur;
+ }
+ }
+};
+
+class CString {
+public:
+ gchar *str;
+
+ CString(const gchar *_str) : str(g_strdup(_str)) {}
+ ~CString()
+ {
+ g_free(str);
+ }
+
+ inline int
+ operator <(CString &obj)
+ {
+ return (int)g_strcmp0(str, obj.str);
+ }
+};
+
+template <typename ValueType>
+class StringTable : public Table<CString, ValueType> {
+public:
+ StringTable(ValueType &nil) : Table<CString, ValueType>(nil) {}
+
+ inline bool
+ hasEntry(const gchar *key)
+ {
+ CString str(key);
+ return Table<CString, ValueType>::hasEntry(str);
+ }
+
+ inline ValueType &
+ operator [](const gchar *key)
+ {
+ CString str(key);
+ return (Table<CString, ValueType>::operator [])(str);
+ }
+
+ inline void
+ remove(const gchar *key)
+ {
+ CString str(key);
+ Table<CString, ValueType>::remove(str);
+ }
+};
+
+template <typename ValueType>
+class StringTableUndo : public StringTable<ValueType> {
+ class UndoTokenSet : public UndoToken {
+ StringTableUndo *table;
+
+ CString name;
+ ValueType value;
+
+ public:
+ UndoTokenSet(StringTableUndo *_table, CString &_name, ValueType &_value)
+ : table(_table), name(_name), value(_value) {}
+
+ void
+ run(void)
+ {
+ table->Table<CString, ValueType>::operator [](name) = value;
+ name.str = NULL;
+#if 0
+ table->dump();
+#endif
+ }
+ };
+
+ class UndoTokenRemove : public UndoToken {
+ StringTableUndo *table;
+
+ CString name;
+
+ public:
+ UndoTokenRemove(StringTableUndo *_table, CString &_name)
+ : table(_table), name(_name) {}
+
+ void
+ run(void)
+ {
+ table->Table<CString, ValueType>::remove(name);
+#if 0
+ table->dump();
+#endif
+ }
+ };
+
+public:
+ StringTableUndo(ValueType &nil) : StringTable<ValueType>(nil) {}
+
+ void
+ set(const gchar *key, ValueType &value)
+ {
+ ValueType &old = (StringTable<ValueType>::operator [])(key);
+ CString str(key);
+
+ if (old == StringTable<ValueType>::nil)
+ undo.push(new UndoTokenRemove(this, str));
+ else
+ undo.push(new UndoTokenSet(this, str, old));
+
+ old = value;
+ }
+};
+
+#endif
diff --git a/src/ring.cpp b/src/ring.cpp
new file mode 100644
index 0000000..f1b051b
--- /dev/null
+++ b/src/ring.cpp
@@ -0,0 +1,539 @@
+#include <limits.h>
+#include <stdlib.h>
+#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 "interface.h"
+#include "undo.h"
+#include "parser.h"
+#include "expressions.h"
+#include "ring.h"
+
+#ifdef G_OS_WIN32
+/* here it shouldn't cause conflicts with other headers */
+#include <windows.h>
+
+/* still need to clean up */
+#ifdef interface
+#undef interface
+#endif
+#endif
+
+namespace States {
+ StateEditFile editfile;
+ StateSaveFile savefile;
+}
+
+Ring ring;
+
+void
+Buffer::UndoTokenClose::run(void)
+{
+ ring.close(buffer);
+ /* NOTE: the buffer is NOT deleted on Token destruction */
+ delete buffer;
+}
+
+bool
+Buffer::load(const gchar *filename)
+{
+ gchar *contents;
+ gsize size;
+
+ /* FIXME: prevent excessive allocations by reading file into buffer */
+ if (!g_file_get_contents(filename, &contents, &size, NULL))
+ return false;
+
+ edit();
+
+ interface.ssm(SCI_BEGINUNDOACTION);
+ interface.ssm(SCI_CLEARALL);
+ interface.ssm(SCI_APPENDTEXT, size, (sptr_t)contents);
+ interface.ssm(SCI_ENDUNDOACTION);
+
+ g_free(contents);
+
+ /* NOTE: currently buffer cannot be dirty */
+#if 0
+ interface.undo_info_update(this);
+ undo.push_var(dirty);
+ dirty = false;
+#endif
+
+ set_filename(filename);
+
+ return true;
+}
+
+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;
+}
+
+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(gint64 id)
+{
+ Buffer *cur;
+
+ TAILQ_FOREACH(cur, &head, buffers)
+ if (!--id)
+ break;
+
+ return cur;
+}
+
+void
+Ring::dirtify(void)
+{
+ if (!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;
+}
+
+bool
+Ring::edit(gint64 id)
+{
+ Buffer *buffer = find(id);
+
+ if (!buffer)
+ return false;
+
+ current_save_dot();
+
+ QRegisters::current = NULL;
+ current = buffer;
+ buffer->edit();
+
+ QRegisters::hook(QRegisters::HOOK_EDIT);
+
+ return true;
+}
+
+void
+Ring::edit(const gchar *filename)
+{
+ Buffer *buffer = find(filename);
+
+ current_save_dot();
+
+ 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->load(filename);
+
+ interface.msg(Interface::MSG_INFO,
+ "Added file \"%s\" to ring", filename);
+ } else {
+ buffer->edit();
+ buffer->set_filename(filename);
+
+ if (filename)
+ interface.msg(Interface::MSG_INFO,
+ "Added new file \"%s\" to ring",
+ filename);
+ else
+ interface.msg(Interface::MSG_INFO,
+ "Added new unnamed file to ring.");
+ }
+
+ QRegisters::hook(QRegisters::HOOK_ADD);
+ }
+}
+
+#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
+
+class UndoTokenRestoreSavePoint : public UndoToken {
+ gchar *savepoint;
+ Buffer *buffer;
+
+public:
+#ifdef G_OS_WIN32
+ DWORD attributes;
+#endif
+
+ UndoTokenRestoreSavePoint(gchar *_savepoint, Buffer *_buffer)
+ : savepoint(_savepoint), buffer(_buffer) {}
+ ~UndoTokenRestoreSavePoint()
+ {
+ if (savepoint)
+ g_unlink(savepoint);
+ g_free(savepoint);
+ buffer->savepoint_id--;
+ }
+
+ void
+ run(void)
+ {
+ if (!g_rename(savepoint, buffer->filename)) {
+ g_free(savepoint);
+ savepoint = NULL;
+#ifdef G_OS_WIN32
+ SetFileAttributes((LPCTSTR)buffer->filename,
+ attributes);
+#endif
+ } else {
+ interface.msg(Interface::MSG_WARNING,
+ "Unable to restore save point file \"%s\"",
+ savepoint);
+ }
+ }
+};
+
+static inline void
+make_savepoint(Buffer *buffer)
+{
+ gchar *dirname, *basename, *savepoint;
+ gchar savepoint_basename[FILENAME_MAX];
+
+ basename = g_path_get_basename(buffer->filename);
+ g_snprintf(savepoint_basename, sizeof(savepoint_basename),
+ ".teco-%s-%d", basename, buffer->savepoint_id);
+ g_free(basename);
+ dirname = g_path_get_dirname(buffer->filename);
+ savepoint = g_build_filename(dirname, savepoint_basename, NULL);
+ g_free(dirname);
+
+ if (!g_rename(buffer->filename, savepoint)) {
+ UndoTokenRestoreSavePoint *token;
+
+ buffer->savepoint_id++;
+ token = new UndoTokenRestoreSavePoint(savepoint, buffer);
+#ifdef G_OS_WIN32
+ token->attributes = GetFileAttributes((LPCTSTR)savepoint);
+ if (token->attributes != INVALID_FILE_ATTRIBUTES)
+ SetFileAttributes((LPCTSTR)savepoint,
+ token->attributes |
+ FILE_ATTRIBUTE_HIDDEN);
+#endif
+ undo.push(token);
+ } else {
+ interface.msg(Interface::MSG_WARNING,
+ "Unable to create save point file \"%s\"",
+ savepoint);
+ g_free(savepoint);
+ }
+}
+
+#endif /* !G_OS_UNIX */
+
+bool
+Ring::save(const gchar *filename)
+{
+ const gchar *buffer;
+ gssize size;
+
+ if (!current)
+ return false;
+
+ if (!filename)
+ filename = current->filename;
+ if (!filename)
+ return false;
+
+ if (undo.enabled) {
+ if (current->filename &&
+ g_file_test(current->filename, G_FILE_TEST_IS_REGULAR))
+ make_savepoint(current);
+ else
+ undo.push(new UndoTokenRemoveFile(filename));
+ }
+
+ /* FIXME: improve by writing before and after document gap */
+ buffer = (const gchar *)interface.ssm(SCI_GETCHARACTERPOINTER);
+ size = interface.ssm(SCI_GETLENGTH);
+
+ if (!g_file_set_contents(filename, buffer, size, NULL))
+ return false;
+
+ interface.undo_info_update(current);
+ undo.push_var(current->dirty);
+ current->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)
+ */
+ //if (filename) {
+ undo.push_str(current->filename);
+ current->set_filename(filename);
+ //}
+
+ return true;
+}
+
+void
+Ring::close(Buffer *buffer)
+{
+ TAILQ_REMOVE(&head, buffer, buffers);
+
+ if (buffer->filename)
+ interface.msg(Interface::MSG_INFO,
+ "Removed file \"%s\" from the ring",
+ buffer->filename);
+ else
+ interface.msg(Interface::MSG_INFO,
+ "Removed unnamed file from the ring.");
+}
+
+void
+Ring::close(void)
+{
+ Buffer *buffer = current;
+
+ buffer->dot = interface.ssm(SCI_GETCURRENTPOS);
+ close(buffer);
+ current = buffer->next() ? : buffer->prev();
+ /* transfer responsibility to UndoToken object */
+ undo.push(new UndoTokenEdit(this, buffer));
+
+ if (current) {
+ current->edit();
+ QRegisters::hook(QRegisters::HOOK_EDIT);
+ } else {
+ edit((const gchar *)NULL);
+ }
+}
+
+Ring::~Ring()
+{
+ Buffer *buffer, *next;
+
+ TAILQ_FOREACH_SAFE(buffer, &head, buffers, next)
+ delete buffer;
+}
+
+/*
+ * Auxiliary functions
+ */
+#ifdef G_OS_UNIX
+
+gchar *
+get_absolute_path(const gchar *path)
+{
+ gchar buf[PATH_MAX];
+ gchar *resolved;
+
+ if (!path)
+ return NULL;
+
+ if (!realpath(path, buf)) {
+ if (g_path_is_absolute(path)) {
+ resolved = g_strdup(path);
+ } else {
+ gchar *cwd = g_get_current_dir();
+ resolved = g_build_filename(cwd, path, NULL);
+ g_free(cwd);
+ }
+ } else {
+ resolved = g_strdup(buf);
+ }
+
+ return resolved;
+}
+
+#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;
+}
+
+#else
+
+/*
+ * FIXME: I doubt that works on any platform...
+ */
+gchar *
+get_absolute_path(const gchar *path)
+{
+ return path ? g_file_read_link(path, NULL) : NULL;
+}
+
+#endif /* !G_OS_UNIX && !G_OS_WIN32 */
+
+/*
+ * Command states
+ */
+
+void
+StateEditFile::do_edit(const gchar *filename) throw (Error)
+{
+ if (ring.current)
+ ring.undo_edit();
+ else /* QRegisters::current != NULL */
+ QRegisters::undo_edit();
+ ring.edit(filename);
+}
+
+void
+StateEditFile::do_edit(gint64 id) throw (Error)
+{
+ if (ring.current)
+ ring.undo_edit();
+ else /* QRegisters::current != NULL */
+ QRegisters::undo_edit();
+ if (!ring.edit(id))
+ throw Error("Invalid buffer id %" G_GINT64_FORMAT, id);
+}
+
+void
+StateEditFile::initial(void) throw (Error)
+{
+ gint64 id = expressions.pop_num_calc(1, -1);
+
+ allowFilename = true;
+
+ if (id == 0) {
+ for (Buffer *cur = ring.first(); cur; cur = cur->next())
+ interface.popup_add(Interface::POPUP_FILE,
+ cur->filename ? : "(Unnamed)",
+ cur == ring.current);
+
+ interface.popup_show();
+ } else if (id > 0) {
+ allowFilename = false;
+ do_edit(id);
+ }
+}
+
+State *
+StateEditFile::done(const gchar *str) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ if (!allowFilename) {
+ if (*str)
+ throw Error("If a buffer is selected by id, the <EB> "
+ "string argument must be empty");
+
+ return &States::start;
+ }
+
+ if (is_glob_pattern(str)) {
+ gchar *dirname;
+ GDir *dir;
+
+ dirname = g_path_get_dirname(str);
+ dir = g_dir_open(dirname, 0, NULL);
+
+ if (dir) {
+ const gchar *basename;
+ GPatternSpec *pattern;
+
+ basename = g_path_get_basename(str);
+ pattern = g_pattern_spec_new(basename);
+ g_free((gchar *)basename);
+
+ while ((basename = g_dir_read_name(dir))) {
+ if (g_pattern_match_string(pattern, basename)) {
+ gchar *filename;
+
+ filename = g_build_filename(dirname,
+ basename,
+ NULL);
+ do_edit(filename);
+ g_free(filename);
+ }
+ }
+
+ g_pattern_spec_free(pattern);
+ g_dir_close(dir);
+ }
+
+ g_free(dirname);
+ } else {
+ do_edit(*str ? str : NULL);
+ }
+
+ return &States::start;
+}
+
+State *
+StateSaveFile::done(const gchar *str) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ if (!ring.save(*str ? str : NULL))
+ throw Error("Unable to save file");
+
+ return &States::start;
+}
diff --git a/src/ring.h b/src/ring.h
new file mode 100644
index 0000000..da4f322
--- /dev/null
+++ b/src/ring.h
@@ -0,0 +1,246 @@
+#ifndef __RING_H
+#define __RING_H
+
+#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 "interface.h"
+#include "undo.h"
+#include "qregisters.h"
+#include "parser.h"
+
+/*
+ * Auxiliary functions
+ */
+static inline bool
+is_glob_pattern(const gchar *str)
+{
+ return strchr(str, '*') || strchr(str, '?');
+}
+
+/*
+ * Get absolute/full version of a possibly relative path.
+ * Works with existing and non-existing paths (in the latter case,
+ * heuristics may be applied.)
+ */
+gchar *get_absolute_path(const gchar *path);
+
+/*
+ * Classes
+ */
+
+class Buffer {
+ class UndoTokenClose : public UndoToken {
+ Buffer *buffer;
+
+ public:
+ UndoTokenClose(Buffer *_buffer)
+ : UndoToken(), buffer(_buffer) {}
+
+ void run(void);
+ };
+
+public:
+ TAILQ_ENTRY(Buffer) buffers;
+
+ gchar *filename;
+ gint dot;
+
+ gint savepoint_id;
+
+ bool dirty;
+
+private:
+ typedef void document;
+ document *doc;
+
+public:
+ Buffer() : filename(NULL), dot(0), savepoint_id(0), dirty(false)
+ {
+ doc = (document *)interface.ssm(SCI_CREATEDOCUMENT);
+ }
+ ~Buffer()
+ {
+ interface.ssm(SCI_RELEASEDOCUMENT, 0, (sptr_t)doc);
+ 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.ssm(SCI_SETDOCPOINTER, 0, (sptr_t)doc);
+ interface.ssm(SCI_GOTOPOS, dot);
+ interface.info_update(this);
+ }
+ inline void
+ undo_edit(void)
+ {
+ interface.undo_info_update(this);
+ undo.push_msg(SCI_GOTOPOS, dot);
+ undo.push_msg(SCI_SETDOCPOINTER, 0, (sptr_t)doc);
+ }
+
+ bool load(const gchar *filename);
+
+ inline void
+ undo_close(void)
+ {
+ undo.push(new UndoTokenClose(this));
+ }
+};
+
+extern class Ring {
+ /*
+ * Emitted after a buffer close
+ * The pointer is the only remaining reference to the buffer!
+ */
+ class UndoTokenEdit : public UndoToken {
+ Ring *ring;
+ Buffer *buffer;
+
+ public:
+ UndoTokenEdit(Ring *_ring, Buffer *_buffer)
+ : UndoToken(), ring(_ring), buffer(_buffer) {}
+ ~UndoTokenEdit()
+ {
+ if (buffer)
+ delete buffer;
+ }
+
+ void run(void);
+ };
+
+ 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);
+ }
+ };
+
+ TAILQ_HEAD(Head, Buffer) head;
+
+public:
+ Buffer *current;
+
+ Ring() : current(NULL)
+ {
+ TAILQ_INIT(&head);
+ }
+ ~Ring();
+
+ inline Buffer *
+ first(void)
+ {
+ return TAILQ_FIRST(&head);
+ }
+ inline Buffer *
+ last(void)
+ {
+ return TAILQ_LAST(&head, Head);
+ }
+
+ Buffer *find(const gchar *filename);
+ Buffer *find(gint64 id);
+
+ void dirtify(void);
+ bool is_any_dirty(void);
+
+ bool edit(gint64 id);
+ void edit(const gchar *filename);
+ inline void
+ undo_edit(void)
+ {
+ current->dot = interface.ssm(SCI_GETCURRENTPOS);
+ undo.push_var(current);
+ current->undo_edit();
+ }
+
+ bool save(const gchar *filename);
+
+ void close(Buffer *buffer);
+ void close(void);
+ inline void
+ undo_close(void)
+ {
+ current->undo_close();
+ }
+} ring;
+
+/*
+ * Command states
+ */
+
+class StateEditFile : public StateExpectString {
+private:
+ bool allowFilename;
+
+ void do_edit(const gchar *filename) throw (Error);
+ void do_edit(gint64 id) throw (Error);
+
+ void initial(void) throw (Error);
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateSaveFile : public StateExpectString {
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+namespace States {
+ extern StateEditFile editfile;
+ extern StateSaveFile savefile;
+}
+
+/* FIXME: clean up current_save_dot() usage */
+static inline void
+current_save_dot(void)
+{
+ gint dot = interface.ssm(SCI_GETCURRENTPOS);
+
+ if (ring.current)
+ ring.current->dot = dot;
+ else if (QRegisters::current)
+ QRegisters::current->dot = dot;
+}
+
+#endif
diff --git a/src/sciteco.h b/src/sciteco.h
new file mode 100644
index 0000000..88ab109
--- /dev/null
+++ b/src/sciteco.h
@@ -0,0 +1,76 @@
+#ifndef __SCITECO_H
+#define __SCITECO_H
+
+#include <glib.h>
+
+#include "interface.h"
+
+/* Autoconf-like */
+#define PACKAGE_VERSION "0.1"
+#define PACKAGE_NAME "SciTECO"
+#define PACKAGE_STRING PACKAGE_NAME " " PACKAGE_VERSION
+
+namespace Flags {
+ enum {
+ ED_HOOKS = (1 << 5)
+ };
+
+ extern gint64 ed;
+}
+
+extern gchar *cmdline;
+extern bool quit_requested;
+
+void cmdline_keypress(gchar key);
+
+#define IS_CTL(C) ((C) < ' ')
+#define CTL_ECHO(C) ((C) | 0x40)
+#define CTL_KEY(C) ((C) & ~0x40)
+
+typedef gint64 tecoBool;
+
+#define SUCCESS (-1)
+#define FAILURE (0)
+#define TECO_BOOL(X) ((X) ? SUCCESS : FAILURE)
+
+#define IS_SUCCESS(X) ((X) < 0)
+#define IS_FAILURE(X) (!IS_SUCCESS(X))
+
+#define CHR2STR(X) ((gchar []){X, '\0'})
+
+namespace String {
+
+static inline void
+append(gchar *&str1, const gchar *str2)
+{
+ /* FIXME: optimize */
+ gchar *new_str = g_strconcat(str1 ? : "", str2, NULL);
+ g_free(str1);
+ str1 = new_str;
+}
+
+static inline void
+append(gchar *&str, gchar chr)
+{
+ append(str, CHR2STR(chr));
+}
+
+} /* namespace String */
+
+namespace Validate {
+
+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);
+}
+
+} /* namespace Validate */
+
+#endif
diff --git a/src/search.cpp b/src/search.cpp
new file mode 100644
index 0000000..701f36c
--- /dev/null
+++ b/src/search.cpp
@@ -0,0 +1,571 @@
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "sciteco.h"
+#include "expressions.h"
+#include "undo.h"
+#include "qregisters.h"
+#include "ring.h"
+#include "parser.h"
+#include "search.h"
+
+namespace States {
+ StateSearch search;
+ StateSearchAll searchall;
+ StateSearchKill searchkill;
+ StateSearchDelete searchdelete;
+ StateReplace replace;
+ StateReplace_insert replace_insert;
+ StateReplaceDefault replacedefault;
+ StateReplaceDefault_insert replacedefault_insert;
+}
+
+/*
+ * Command states
+ */
+
+void
+StateSearch::initial(void) throw (Error)
+{
+ gint64 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 = 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;
+}
+
+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'):
+ return g_strdup("[:^alnum:]");
+ case CTL_KEY('E'):
+ state = STATE_CTL_E;
+ break;
+ default:
+ temp = escape_default
+ ? g_strdup(regexp_escape_chr(*pattern))
+ : NULL;
+ return temp;
+ }
+ break;
+
+ case STATE_CTL_E:
+ switch (g_ascii_toupper(*pattern)) {
+ case 'A':
+ state = STATE_START;
+ return g_strdup("[:alpha:]");
+ /* same as <CTRL/S> */
+ case 'B':
+ state = STATE_START;
+ return g_strdup("[:^alnum:]");
+ case 'C':
+ state = STATE_START;
+ return g_strdup("[:alnum:].$");
+ case 'D':
+ state = STATE_START;
+ return g_strdup("[:digit:]");
+ case 'G':
+ state = STATE_ANYQ;
+ break;
+ case 'L':
+ state = STATE_START;
+ return g_strdup("\r\n\v\f");
+ case 'R':
+ state = STATE_START;
+ return g_strdup("[:alnum:]");
+ case 'V':
+ state = STATE_START;
+ return g_strdup("[:lower:]");
+ case 'W':
+ state = STATE_START;
+ return g_strdup("[:upper:]");
+ default:
+ return NULL;
+ }
+ break;
+
+ case STATE_ANYQ:
+ /* FIXME: Q-Register spec might get more complicated */
+ reg = QRegisters::globals[g_ascii_toupper(*pattern)];
+ if (!reg)
+ return NULL;
+
+ temp = reg->get_string();
+ temp2 = g_regex_escape_string(temp, -1);
+ g_free(temp);
+ state = STATE_START;
+ return temp2;
+
+ default:
+ return NULL;
+ }
+
+ pattern++;
+ }
+
+ return NULL;
+}
+
+gchar *
+StateSearch::pattern2regexp(const gchar *&pattern,
+ bool single_expr)
+{
+ MatchState state = STATE_START;
+ gchar *re = NULL;
+
+ while (*pattern) {
+ gchar *new_re, *temp;
+
+ temp = class2regexp(state, pattern);
+ if (temp) {
+ new_re = g_strconcat(re ? : "", "[", temp, "]", NULL);
+ g_free(temp);
+ g_free(re);
+ re = new_re;
+
+ goto next;
+ }
+ if (!*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;
+ temp = class2regexp(state, pattern, true);
+ if (!temp)
+ goto error;
+ new_re = g_strconcat(re ? : "", "[^", temp, "]", NULL);
+ g_free(temp);
+ g_free(re);
+ re = new_re;
+ g_assert(state == STATE_START);
+ break;
+
+ case STATE_CTL_E:
+ state = STATE_START;
+ switch (g_ascii_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:
+ goto error;
+ }
+ break;
+
+ case STATE_MANY:
+ temp = pattern2regexp(pattern, true);
+ if (!temp)
+ goto error;
+ new_re = g_strconcat(re ? : "", "(", temp, ")+", NULL);
+ g_free(temp);
+ g_free(re);
+ re = new_re;
+ state = STATE_START;
+ break;
+
+ case STATE_ALT:
+ switch (*pattern) {
+ case ',':
+ String::append(re, "|");
+ break;
+ case ']':
+ String::append(re, ")");
+ state = STATE_START;
+ break;
+ default:
+ temp = pattern2regexp(pattern, true);
+ if (!temp)
+ goto error;
+ String::append(re, temp);
+ g_free(temp);
+ }
+ break;
+
+ default:
+ /* shouldn't happen */
+ g_assert(true);
+ }
+
+next:
+ if (single_expr && state == STATE_START)
+ return re;
+
+ pattern++;
+ }
+
+ if (state == STATE_ALT)
+ String::append(re, ")");
+
+ return re;
+
+error:
+ 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 __attribute__((unused))) throw (Error)
+{
+ 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;
+
+ undo.push_msg(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 */
+ re_pattern = pattern2regexp(str);
+#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 (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) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ QRegister *search_reg = QRegisters::globals["_"];
+
+ if (*str) {
+ /* workaround: preserve selection (also on rubout) */
+ gint anchor = interface.ssm(SCI_GETANCHOR);
+ undo.push_msg(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()) &&
+ !expressions.find_op(Expressions::OP_LOOP) /* not in loop */)
+ interface.msg(Interface::MSG_ERROR, "Search string not found!");
+
+ return &States::start;
+}
+
+void
+StateSearchAll::initial(void) throw (Error)
+{
+ gint64 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" */
+ 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) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ StateSearch::done(str);
+ QRegisters::hook(QRegisters::HOOK_EDIT);
+
+ return &States::start;
+}
+
+State *
+StateSearchKill::done(const gchar *str) throw (Error)
+{
+ gint anchor;
+
+ BEGIN_EXEC(&States::start);
+
+ StateSearch::done(str);
+
+ undo.push_msg(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS));
+ anchor = interface.ssm(SCI_GETANCHOR);
+ interface.ssm(SCI_GOTOPOS, anchor);
+
+ interface.ssm(SCI_BEGINUNDOACTION);
+ interface.ssm(SCI_DELETERANGE,
+ parameters.dot, anchor - parameters.dot);
+ interface.ssm(SCI_ENDUNDOACTION);
+ ring.dirtify();
+
+ undo.push_msg(SCI_UNDO);
+
+ return &States::start;
+}
+
+State *
+StateSearchDelete::done(const gchar *str) throw (Error)
+{
+ BEGIN_EXEC(&States::start);
+
+ StateSearch::done(str);
+
+ interface.ssm(SCI_BEGINUNDOACTION);
+ interface.ssm(SCI_REPLACESEL, 0, (sptr_t)"");
+ interface.ssm(SCI_ENDUNDOACTION);
+ ring.dirtify();
+
+ undo.push_msg(SCI_UNDO);
+
+ return &States::start;
+}
+
+State *
+StateReplace::done(const gchar *str) throw (Error)
+{
+ StateSearchDelete::done(str);
+ return &States::replace_insert;
+}
+
+State *
+StateReplaceDefault::done(const gchar *str) throw (Error)
+{
+ StateSearchDelete::done(str);
+ return &States::replacedefault_insert;
+}
+
+State *
+StateReplaceDefault_insert::done(const gchar *str) throw (Error)
+{
+ 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;
+}
diff --git a/src/search.h b/src/search.h
new file mode 100644
index 0000000..2bddedd
--- /dev/null
+++ b/src/search.h
@@ -0,0 +1,102 @@
+#ifndef __SEARCH_H
+#define __SEARCH_H
+
+#include <glib.h>
+
+#include "sciteco.h"
+#include "parser.h"
+#include "ring.h"
+
+/*
+ * "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;
+
+ 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) throw (Error);
+ virtual void process(const gchar *str, gint new_chars) throw (Error);
+ virtual State *done(const gchar *str) throw (Error);
+};
+
+class StateSearchAll : public StateSearch {
+private:
+ void initial(void) throw (Error);
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateSearchKill : public StateSearch {
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateSearchDelete : public StateSearch {
+public:
+ StateSearchDelete(bool last = true) : StateSearch(last) {}
+
+protected:
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateReplace : public StateSearchDelete {
+public:
+ StateReplace() : StateSearchDelete(false) {}
+
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateReplace_insert : public StateInsert {
+private:
+ void initial(void) throw (Error) {}
+};
+
+class StateReplaceDefault : public StateSearchDelete {
+public:
+ StateReplaceDefault() : StateSearchDelete(false) {}
+
+private:
+ State *done(const gchar *str) throw (Error);
+};
+
+class StateReplaceDefault_insert : public StateInsert {
+private:
+ void initial(void) throw (Error) {}
+ State *done(const gchar *str) throw (Error);
+};
+
+namespace States {
+ extern StateSearch search;
+ extern StateSearchAll searchall;
+ extern StateSearchKill searchkill;
+ extern StateSearchDelete searchdelete;
+ extern StateReplace replace;
+ extern StateReplace_insert replace_insert;
+ extern StateReplaceDefault replacedefault;
+ extern StateReplaceDefault_insert replacedefault_insert;
+}
+
+#endif
diff --git a/src/symbols-extract.tes b/src/symbols-extract.tes
new file mode 100755
index 0000000..6497324
--- /dev/null
+++ b/src/symbols-extract.tes
@@ -0,0 +1,79 @@
+#!./sciteco-minimal -m
+! ./symbols-extract.tes <input file> <output file> <prefix pattern list> <array name> !
+
+! <pos1,pos2>Mc - Compare string at pos1 with string at pos2 !
+@^Uc{
+ U.2U.1 -.%.1^[ -.%.2^[
+ <Q.1A-(Q.2A)U.c Q.1A"C|Q.2A"C|;'' Q.c"N;' %.1^[%.2>
+ Q.c
+}
+
+! <i,j>Mx - Exchange line at I with line at J (I < J), returning new J !
+@^Ux{
+ U.jU.i
+ Q.jJ X.xK
+ Q.iJ G.x .-Q.i%.j^[ .-(X.xL.)%.j^[ -K
+ Q.jJ G.x
+ Q.j
+}
+
+! <i,j>Mq - Sort lines beginning at I until J using Quicksort algorithm !
+@^Uq{
+ U.rU.l
+
+ Q.l-Q.r"<
+ Q.lU.i Q.rJB .U.j
+
+ <
+ Q.iJ <.,Q.rMc-1; .-Q.r; L> .U.i
+ Q.jJ <.,Q.rMc:; .-Q.l-1:; B> .U.j
+
+ Q.i-Q.j;
+
+ Q.i,Q.jMxU.j
+ >
+
+ Q.i,Q.rMc"> Q.i,Q.rMxU.r '
+
+ Q.l-Q.i"< Q.iJB Q.l,.Mq '
+ Q.i-Q.r"< Q.iJL .,Q.rMq '
+ '
+}
+
+! read commandline arguments !
+LR 0Xi 2LR 0Xo 2LR 0Xp 2LR 0Xn HK
+
+! copy all defines in input file beginning with prefix !
+EBQi <S#defineS[Qp]; -SS :Xa> EF
+
+! sort all defines !
+Ga ZJB 0,.Mq J
+
+! format as C/C++ array !
+I/*
+ * AUTOGENERATED - DO NOT EDIT
+ */
+#include <glib.h>
+
+#include "Qi"
+#include "symbols.h"
+
+static const SymbolList::Entry entries[] = {
+
+<
+ .,W.Xa 0KK
+ I#ifdef Qa
+ {"Qa", ^EQa},
+#endif
+
+.-Z;>
+I};
+
+/* overwrites weak object in symbols.cpp */
+SymbolList Symbols::Qn(entries, G_N_ELEMENTS(entries));
+
+
+! write output file !
+EWQo
+
+EX
diff --git a/src/symbols.cpp b/src/symbols.cpp
new file mode 100644
index 0000000..c05d153
--- /dev/null
+++ b/src/symbols.cpp
@@ -0,0 +1,60 @@
+#include <string.h>
+
+#include <glib.h>
+
+#include "symbols.h"
+
+/*
+ * defaults for sciteco-minimal
+ */
+namespace Symbols {
+ SymbolList __attribute__((weak)) scintilla;
+ SymbolList __attribute__((weak)) scilexer;
+}
+
+/*
+ * Since symbol lists are presorted constant arrays we can do a simple
+ * binary search.
+ */
+gint
+SymbolList::lookup(const gchar *name, const gchar *prefix, bool case_sensitive)
+{
+ int (*cmp_fnc)(const char *, const char *, size_t);
+ gint prefix_skip = strlen(prefix);
+ gint name_len = strlen(name);
+
+ gint left = 0;
+ gint right = size - 1;
+
+ cmp_fnc = case_sensitive ? strncmp : g_ascii_strncasecmp;
+
+ 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;
+}
diff --git a/src/symbols.h b/src/symbols.h
new file mode 100644
index 0000000..1c48ddf
--- /dev/null
+++ b/src/symbols.h
@@ -0,0 +1,38 @@
+#ifndef __SYMBOLS_H
+#define __SYMBOLS_H
+
+#include <glib.h>
+
+class SymbolList {
+public:
+ struct Entry {
+ const gchar *name;
+ gint value;
+ };
+
+private:
+ const Entry *entries;
+ gint size;
+
+ /* for auto-completions */
+ GList *list;
+
+public:
+ SymbolList(const Entry *_entries = NULL, gint _size = 0)
+ : entries(_entries), size(_size), list(NULL) {}
+ ~SymbolList()
+ {
+ g_list_free(list);
+ }
+
+ gint lookup(const gchar *name, const gchar *prefix = "",
+ bool case_sensitive = false);
+ GList *get_glist(void);
+};
+
+namespace Symbols {
+ extern SymbolList __attribute__((weak)) scintilla;
+ extern SymbolList __attribute__((weak)) scilexer;
+}
+
+#endif
diff --git a/src/undo.cpp b/src/undo.cpp
new file mode 100644
index 0000000..8072540
--- /dev/null
+++ b/src/undo.cpp
@@ -0,0 +1,78 @@
+#include <stdio.h>
+#include <string.h>
+#include <bsd/sys/queue.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <Scintilla.h>
+
+#include "sciteco.h"
+#include "interface.h"
+#include "undo.h"
+
+//#define DEBUG
+
+UndoStack undo;
+
+void
+UndoTokenMessage::run(void)
+{
+ interface.ssm(iMessage, wParam, lParam);
+}
+
+void
+UndoStack::push(UndoToken *token)
+{
+ if (enabled) {
+#ifdef DEBUG
+ g_printf("UNDO PUSH %p\n", token);
+#endif
+ token->pos = strlen(cmdline);
+ SLIST_INSERT_HEAD(&head, token, tokens);
+ } else {
+ delete token;
+ }
+}
+
+void
+UndoStack::push_msg(unsigned int iMessage, uptr_t wParam, sptr_t lParam)
+{
+ push(new UndoTokenMessage(iMessage, wParam, lParam));
+}
+
+void
+UndoStack::pop(gint pos)
+{
+ while (!SLIST_EMPTY(&head) && SLIST_FIRST(&head)->pos >= pos) {
+ UndoToken *top = SLIST_FIRST(&head);
+#ifdef DEBUG
+ g_printf("UNDO POP %p\n", top);
+ fflush(stdout);
+#endif
+
+ top->run();
+
+ SLIST_REMOVE_HEAD(&head, tokens);
+ delete top;
+ }
+}
+
+void
+UndoStack::clear(void)
+{
+ UndoToken *cur;
+
+ while ((cur = SLIST_FIRST(&head))) {
+ SLIST_REMOVE_HEAD(&head, tokens);
+ delete cur;
+ }
+}
+
+UndoStack::~UndoStack()
+{
+ UndoToken *token, *next;
+
+ SLIST_FOREACH_SAFE(token, &head, tokens, next)
+ delete token;
+}
diff --git a/src/undo.h b/src/undo.h
new file mode 100644
index 0000000..43692e9
--- /dev/null
+++ b/src/undo.h
@@ -0,0 +1,134 @@
+#ifndef __UNDO_H
+#define __UNDO_H
+
+#include <bsd/sys/queue.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include <Scintilla.h>
+
+#ifdef DEBUG
+#include "parser.h"
+#endif
+
+class UndoToken {
+public:
+ SLIST_ENTRY(UndoToken) tokens;
+
+ gint pos;
+
+ virtual ~UndoToken() {}
+
+ virtual void run() = 0;
+};
+
+class UndoTokenMessage : public UndoToken {
+ unsigned int iMessage;
+ uptr_t wParam;
+ sptr_t lParam;
+
+public:
+ UndoTokenMessage(unsigned int _iMessage,
+ uptr_t _wParam = 0, sptr_t _lParam = 0)
+ : UndoToken(), iMessage(_iMessage),
+ wParam(_wParam), lParam(_lParam) {}
+
+ void run(void);
+};
+
+template <typename Type>
+class UndoTokenVariable : public UndoToken {
+ Type *ptr;
+ Type value;
+
+public:
+ UndoTokenVariable(Type &variable, Type _value)
+ : UndoToken(), 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;
+ }
+};
+
+class UndoTokenString : public UndoToken {
+ gchar **ptr;
+ gchar *str;
+
+public:
+ UndoTokenString(gchar *&variable, gchar *_str)
+ : UndoToken(), ptr(&variable)
+ {
+ str = _str ? g_strdup(_str) : NULL;
+ }
+
+ ~UndoTokenString()
+ {
+ g_free(str);
+ }
+
+ void
+ run(void)
+ {
+ g_free(*ptr);
+ *ptr = str;
+ str = NULL;
+ }
+};
+
+extern class UndoStack {
+ SLIST_HEAD(Head, UndoToken) head;
+
+public:
+ bool enabled;
+
+ UndoStack(bool _enabled = false) : enabled(_enabled)
+ {
+ SLIST_INIT(&head);
+ }
+ ~UndoStack();
+
+ void push(UndoToken *token);
+
+ void push_msg(unsigned int iMessage,
+ uptr_t wParam = 0, sptr_t lParam = 0);
+
+ template <typename Type>
+ inline Type &
+ push_var(Type &variable, Type value)
+ {
+ push(new UndoTokenVariable<Type>(variable, value));
+ return variable;
+ }
+
+ template <typename Type>
+ inline Type &
+ push_var(Type &variable)
+ {
+ return push_var<Type>(variable, variable);
+ }
+
+ inline gchar *&
+ push_str(gchar *&variable, gchar *str)
+ {
+ push(new UndoTokenString(variable, str));
+ return variable;
+ }
+ inline gchar *&
+ push_str(gchar *&variable)
+ {
+ return push_str(variable, variable);
+ }
+
+ void pop(gint pos);
+
+ void clear(void);
+} undo;
+
+#endif \ No newline at end of file