diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2012-12-04 17:29:01 +0100 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2012-12-04 22:07:08 +0100 |
commit | d8a316514c03d85b771a9dce4a8a51b875d955b3 (patch) | |
tree | 8966c29db767a155848f6d90f76771ce5b9de32e /src | |
parent | b120616b6da52e951097f69ad267de06081d218a (diff) | |
download | sciteco-d8a316514c03d85b771a9dce4a8a51b875d955b3.tar.gz |
autoconf preparation: move everything into src/ subdir
Diffstat (limited to 'src')
-rw-r--r-- | src/cmdline.cpp | 371 | ||||
-rw-r--r-- | src/expressions.cpp | 201 | ||||
-rw-r--r-- | src/expressions.h | 194 | ||||
-rw-r--r-- | src/goto.cpp | 147 | ||||
-rw-r--r-- | src/goto.h | 107 | ||||
-rw-r--r-- | src/gtk-info-popup.gob | 165 | ||||
-rw-r--r-- | src/interface-gtk.cpp | 248 | ||||
-rw-r--r-- | src/interface-gtk.h | 73 | ||||
-rw-r--r-- | src/interface-ncurses.cpp | 426 | ||||
-rw-r--r-- | src/interface-ncurses.h | 69 | ||||
-rw-r--r-- | src/interface.h | 112 | ||||
-rw-r--r-- | src/main.cpp | 208 | ||||
-rw-r--r-- | src/parser.cpp | 1436 | ||||
-rw-r--r-- | src/parser.h | 266 | ||||
-rw-r--r-- | src/qregisters.cpp | 522 | ||||
-rw-r--r-- | src/qregisters.h | 343 | ||||
-rw-r--r-- | src/rbtree.cpp | 5 | ||||
-rw-r--r-- | src/rbtree.h | 282 | ||||
-rw-r--r-- | src/ring.cpp | 539 | ||||
-rw-r--r-- | src/ring.h | 246 | ||||
-rw-r--r-- | src/sciteco.h | 76 | ||||
-rw-r--r-- | src/search.cpp | 571 | ||||
-rw-r--r-- | src/search.h | 102 | ||||
-rwxr-xr-x | src/symbols-extract.tes | 79 | ||||
-rw-r--r-- | src/symbols.cpp | 60 | ||||
-rw-r--r-- | src/symbols.h | 38 | ||||
-rw-r--r-- | src/undo.cpp | 78 | ||||
-rw-r--r-- | src/undo.h | 134 |
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 = ¯o_goto_table; + if (locals) { + macro_locals.initialize(); + QRegisters::locals = ¯o_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, ¯o_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(®); + } + 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 |