/* * Copyright (C) 2012-2015 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include "sciteco.h" #include "string-utils.h" #include "interface.h" #include "undo.h" #include "parser.h" #include "expressions.h" #include "document.h" #include "ring.h" #include "error.h" #include "qregisters.h" namespace SciTECO { namespace States { StatePushQReg pushqreg; StatePopQReg popqreg; StateEQCommand eqcommand; StateLoadQReg loadqreg; StateEPctCommand epctcommand; StateSaveQReg saveqreg; StateQueryQReg queryqreg; StateCtlUCommand ctlucommand; StateEUCommand eucommand; StateSetQRegString setqregstring_nobuilding(false); StateSetQRegString setqregstring_building(true); StateGetQRegString getqregstring; StateSetQRegInteger setqreginteger; StateIncreaseQReg increaseqreg; StateMacro macro; StateMacroFile macro_file; StateCopyToQReg copytoqreg; } namespace QRegisters { QRegisterTable *locals = NULL; QRegister *current = NULL; static QRegisterStack stack; } static QRegister *register_argument = NULL; void QRegisterData::set_string(const gchar *str, gsize len) { if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.reset(); string.edit(QRegisters::view); QRegisters::view.ssm(SCI_BEGINUNDOACTION); QRegisters::view.ssm(SCI_CLEARALL); QRegisters::view.ssm(SCI_APPENDTEXT, len, (sptr_t)(str ? : "")); QRegisters::view.ssm(SCI_ENDUNDOACTION); if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); } void QRegisterData::undo_set_string(void) { if (!must_undo) return; /* * Necessary, so that upon rubout the * string's parameters are restored. */ string.update(QRegisters::view); if (QRegisters::current && QRegisters::current->must_undo) QRegisters::current->string.undo_edit(QRegisters::view); string.undo_reset(); QRegisters::view.undo_ssm(SCI_UNDO); string.undo_edit(QRegisters::view); } void QRegisterData::append_string(const gchar *str, gsize len) { /* * NOTE: Will not create undo action * if string is empty. * Also, appending preserves the string's * parameters. */ if (!len) return; if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.edit(QRegisters::view); QRegisters::view.ssm(SCI_BEGINUNDOACTION); QRegisters::view.ssm(SCI_APPENDTEXT, len, (sptr_t)str); QRegisters::view.ssm(SCI_ENDUNDOACTION); if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); } gchar * QRegisterData::get_string(void) { gint size; gchar *str; if (!string.is_initialized()) return g_strdup(""); if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.edit(QRegisters::view); size = QRegisters::view.ssm(SCI_GETLENGTH) + 1; str = (gchar *)g_malloc(size); QRegisters::view.ssm(SCI_GETTEXT, size, (sptr_t)str); if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); return str; } gsize QRegisterData::get_string_size(void) { gsize size; if (!string.is_initialized()) return 0; if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.edit(QRegisters::view); size = QRegisters::view.ssm(SCI_GETLENGTH); if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); return size; } gint QRegisterData::get_character(gint position) { gint ret = -1; if (position < 0) return -1; if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.edit(QRegisters::view); if (position < QRegisters::view.ssm(SCI_GETLENGTH)) ret = QRegisters::view.ssm(SCI_GETCHARAT, position); if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); return ret; } void QRegister::edit(void) { if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.edit(QRegisters::view); interface.show_view(&QRegisters::view); interface.info_update(this); } void QRegister::undo_edit(void) { /* * We might be switching the current document * to a buffer. */ string.update(QRegisters::view); if (!must_undo) return; interface.undo_info_update(this); string.undo_edit(QRegisters::view); interface.undo_show_view(&QRegisters::view); } void QRegister::execute(bool locals) { gchar *str = get_string(); try { Execute::macro(str, locals); } catch (Error &error) { error.add_frame(new Error::QRegFrame(name)); g_free(str); throw; /* forward */ } catch (...) { g_free(str); throw; /* forward */ } g_free(str); } void QRegister::undo_set_eol_mode(void) { if (!must_undo) return; /* * Necessary, so that upon rubout the * string's parameters are restored. */ string.update(QRegisters::view); if (QRegisters::current && QRegisters::current->must_undo) QRegisters::current->string.undo_edit(QRegisters::view); QRegisters::view.undo_ssm(SCI_SETEOLMODE, QRegisters::view.ssm(SCI_GETEOLMODE)); string.undo_edit(QRegisters::view); } void QRegister::set_eol_mode(gint mode) { if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.edit(QRegisters::view); QRegisters::view.ssm(SCI_SETEOLMODE, mode); if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); } void QRegister::load(const gchar *filename) { undo_set_string(); if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.edit(QRegisters::view); string.reset(); /* * IOView::load() might change the EOL style. */ undo_set_eol_mode(); /* * undo_set_string() pushes undo tokens that restore * the previous document in the view. * So if loading fails, QRegisters::current will be * made the current document again. */ QRegisters::view.load(filename); if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); } void QRegister::save(const gchar *filename) { if (QRegisters::current) QRegisters::current->string.update(QRegisters::view); string.edit(QRegisters::view); try { QRegisters::view.save(filename); } catch (...) { if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); throw; /* forward */ } if (QRegisters::current) QRegisters::current->string.edit(QRegisters::view); } tecoInt QRegisterBufferInfo::set_integer(tecoInt v) { if (!ring.edit(v)) throw Error("Invalid buffer id %" TECO_INTEGER_FORMAT, v); return v; } void QRegisterBufferInfo::undo_set_integer(void) { current_doc_undo_edit(); } tecoInt QRegisterBufferInfo::get_integer(void) { return ring.get_id(); } gchar * QRegisterBufferInfo::get_string(void) { gchar *str = g_strdup(ring.current->filename ? : ""); /* * On platforms with a default non-forward-slash directory * separator (i.e. Windows), Buffer::filename will have * the wrong separator. * To make the life of macros that evaluate "*" easier, * the directory separators are normalized to "/" here. * This does not change the size of the string, so * get_string_size() still works. */ #if G_DIR_SEPARATOR != '/' g_strdelimit(str, G_DIR_SEPARATOR_S, '/'); #endif return str; } gsize QRegisterBufferInfo::get_string_size(void) { return ring.current->filename ? strlen(ring.current->filename) : 0; } gint QRegisterBufferInfo::get_character(gint position) { if (position < 0 || position >= (gint)QRegisterBufferInfo::get_string_size()) return -1; return ring.current->filename[position]; } void QRegisterBufferInfo::edit(void) { gchar *str; QRegister::edit(); QRegisters::view.ssm(SCI_BEGINUNDOACTION); str = QRegisterBufferInfo::get_string(); QRegisters::view.ssm(SCI_SETTEXT, 0, (sptr_t)str); g_free(str); QRegisters::view.ssm(SCI_ENDUNDOACTION); QRegisters::view.undo_ssm(SCI_UNDO); } QRegisterTable::QRegisterTable(bool _undo) : RBTree(), must_undo(_undo) { /* general purpose registers */ for (gchar q = 'A'; q <= 'Z'; q++) insert(q); for (gchar q = '0'; q <= '9'; q++) insert(q); } /* * NOTE: by not making this inline, * we can access QRegisters::current */ void QRegisterTable::edit(QRegister *reg) { reg->edit(); QRegisters::current = reg; } /* * This is similar to RBTree::clear() but * has the advantage that we can check whether some * register is currently edited. * Since this is not a destructor, we can throw * errors. * Therefore this method should be called before * a (local) QRegisterTable is deleted. */ void QRegisterTable::clear(void) { QRegister *cur; while ((cur = (QRegister *)min())) { if (cur == QRegisters::current) throw Error("Currently edited Q-Register \"%s\" " "cannot be discarded", cur->name); remove(cur); delete cur; } } void QRegisterStack::UndoTokenPush::run(void) { SLIST_INSERT_HEAD(&stack->head, entry, entries); entry = NULL; } void QRegisterStack::UndoTokenPop::run(void) { Entry *entry = SLIST_FIRST(&stack->head); SLIST_REMOVE_HEAD(&stack->head, entries); delete entry; } void QRegisterStack::push(QRegister ®) { Entry *entry = new Entry(); gchar *str = reg.get_string(); if (*str) entry->set_string(str); g_free(str); entry->string.update(reg.string); entry->set_integer(reg.get_integer()); SLIST_INSERT_HEAD(&head, entry, entries); undo.push(new UndoTokenPop(this)); } bool QRegisterStack::pop(QRegister ®) { Entry *entry = SLIST_FIRST(&head); if (!entry) return false; reg.undo_set_integer(); reg.set_integer(entry->get_integer()); /* exchange document ownership between Stack entry and Q-Register */ if (reg.must_undo) reg.string.undo_exchange(); entry->string.undo_exchange(); entry->string.exchange(reg.string); 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) { static const gchar *type2name[] = { /* [HOOK_ADD-1] = */ "ADD", /* [HOOK_EDIT-1] = */ "EDIT", /* [HOOK_CLOSE-1] = */ "CLOSE", /* [HOOK_QUIT-1] = */ "QUIT", }; QRegister *reg; if (!(Flags::ed & Flags::ED_HOOKS)) return; try { reg = globals["ED"]; if (!reg) throw Error("Undefined ED-hook register (\"ED\")"); /* * ED-hook execution should not see any * integer parameters but the hook type. * Such parameters could confuse the ED macro * and macro authors do not expect side effects * of ED macros on the expression stack. * Also make sure it does not leave behind * additional arguments on the stack. * * So this effectively executes: * (typeM[ED]^[) */ expressions.push(Expressions::OP_BRACE); expressions.push(type); reg->execute(); expressions.discard_args(); expressions.eval(true); } catch (Error &error) { const gchar *type_str = type2name[type-1]; error.add_frame(new Error::EDHookFrame(type_str)); throw; /* forward */ } } void QRegSpecMachine::reset(void) { MicroStateMachine::reset(); string_machine.reset(); undo.push_var(is_local) = false; undo.push_var(nesting) = 0; undo.push_str(name); g_free(name); name = NULL; } QRegister * QRegSpecMachine::input(gchar chr) { gchar *insert; MICROSTATE_START; switch (chr) { case '.': undo.push_var(is_local) = true; break; case '#': set(&&StateFirstChar); break; case '[': set(&&StateString); break; default: undo.push_str(name) = String::chrdup(g_ascii_toupper(chr)); goto done; } return NULL; StateFirstChar: undo.push_str(name) = (gchar *)g_malloc(3); name[0] = g_ascii_toupper(chr); set(&&StateSecondChar); return NULL; StateSecondChar: name[1] = g_ascii_toupper(chr); name[2] = '\0'; goto done; StateString: switch (chr) { case '[': undo.push_var(nesting)++; break; case ']': if (!nesting) goto done; undo.push_var(nesting)--; break; } if (mode > MODE_NORMAL) return NULL; insert = string_machine.input(chr); if (!insert) return NULL; undo.push_str(name); String::append(name, insert); g_free(insert); return NULL; done: if (mode > MODE_NORMAL) /* * FIXME: currently we must return *some* register * since got_register() expects one */ return QRegisters::globals["0"]; QRegisterTable &table = is_local ? *QRegisters::locals : QRegisters::globals; QRegister *reg = table[name]; if (!reg) { if (!initialize) throw InvalidQRegError(name, is_local); reg = table.insert(name); table.undo_remove(reg); } return reg; } /* * Command states */ StateExpectQReg::StateExpectQReg(bool initialize) : State(), machine(initialize) { transitions['\0'] = this; } State * StateExpectQReg::custom(gchar chr) { QRegister *reg = machine.input(chr); if (!reg) return this; machine.reset(); return got_register(*reg); } /*$ * [q -- Save Q-Register * * Save Q-Register contents on the global Q-Register push-down * stack. */ State * StatePushQReg::got_register(QRegister ®) { BEGIN_EXEC(&States::start); QRegisters::stack.push(reg); return &States::start; } /*$ * ]q -- Restore Q-Register * * Restore Q-Register by replacing its contents * with the contents of the register saved on top of * the Q-Register push-down stack. * The stack entry is popped. * * In interactive mode, the original contents of * are not immediately reclaimed but are kept in memory * to support rubbing out the command. * Memory is reclaimed on command-line termination. */ State * StatePopQReg::got_register(QRegister ®) { BEGIN_EXEC(&States::start); if (!QRegisters::stack.pop(reg)) throw Error("Q-Register stack is empty"); return &States::start; } /*$ * EQq$ -- Edit or load Q-Register * EQq[file]$ * * When specified with an empty string argument, * EQ makes the currently edited Q-Register. * Otherwise, when is specified, it is the * name of a file to read into Q-Register . * When loading a file, the currently edited * buffer/register is not changed and the edit position * of register is reset to 0. * * Undefined Q-Registers will be defined. * The command fails if could not be read. */ State * StateEQCommand::got_register(QRegister ®) { BEGIN_EXEC(&States::loadqreg); register_argument = ® return &States::loadqreg; } State * StateLoadQReg::done(const gchar *str) { BEGIN_EXEC(&States::start); if (*str) { /* Load file into Q-Register */ register_argument->load(str); } else { /* Edit Q-Register */ current_doc_undo_edit(); QRegisters::globals.edit(register_argument); } return &States::start; } /*$ * E%q$ -- Save Q-Register string to file * * Saves the string contents of Q-Register to * . * The must always be specified, as Q-Registers * have no notion of associated file names. * * In interactive mode, the E% command may be rubbed out, * restoring the previous state of . * This follows the same rules as with the \fBEW\fP command. * * File names may also be tab-completed and string building * characters are enabled by default. */ State * StateEPctCommand::got_register(QRegister ®) { BEGIN_EXEC(&States::saveqreg); register_argument = ® return &States::saveqreg; } State * StateSaveQReg::done(const gchar *str) { BEGIN_EXEC(&States::start); register_argument->save(str); return &States::start; } /*$ * Qq -> n -- Query Q-Register integer or string * Qq -> character * :Qq -> size * * Without any arguments, get and return the integer-part of * Q-Register . * * With one argument, return the code at * from the string-part of Q-Register . * Positions are handled like buffer positions \(em they * begin at 0 up to the length of the string minus 1. * An error is thrown for invalid positions. * * When colon-modified, Q does not pop any arguments from * the expression stack and returns the of the string * in Q-Register . * Naturally, for empty strings, 0 is returned. * * The command fails for undefined registers. */ State * StateQueryQReg::got_register(QRegister ®) { BEGIN_EXEC(&States::start); expressions.eval(); if (eval_colon()) { /* Query Q-Register string size */ expressions.push(reg.get_string_size()); } else if (expressions.args() > 0) { /* Query character from Q-Register string */ gint c = reg.get_character(expressions.pop_num_calc()); if (c < 0) throw RangeError('Q'); expressions.push(c); } else { /* Query integer */ expressions.push(reg.get_integer()); } return &States::start; } /*$ * [c1,c2,...]^Uq[string]$ -- Set or append to Q-Register string without string building * [c1,c2,...]:^Uq[string]$ * * If not colon-modified, it first fills the Q-Register * with all the values on the expression stack (interpreted as * codepoints). * It does so in the order of the arguments, i.e. * will be the first character in , the second, etc. * Eventually the argument is appended to the * register. * Any existing string value in is overwritten by this operation. * * In the colon-modified form ^U does not overwrite existing * contents of but only appends to it. * * If is undefined, it will be defined. * * String-building characters are \fBdisabled\fP for ^U * commands. * Therefore they are especially well-suited for defining * \*(ST macros, since string building characters in the * desired Q-Register contents do not have to be escaped. * The \fBEU\fP command may be used where string building * is desired. */ State * StateCtlUCommand::got_register(QRegister ®) { BEGIN_EXEC(&States::setqregstring_nobuilding); register_argument = ® return &States::setqregstring_nobuilding; } /*$ * [c1,c2,...]EUq[string]$ -- Set or append to Q-Register string with string building characters * [c1,c2,...]:EUq[string]$ * * This command sets or appends to the contents of * Q-Register \fIq\fP. * It is identical to the \fB^U\fP command, except * that this form of the command has string building * characters \fBenabled\fP. */ State * StateEUCommand::got_register(QRegister ®) { BEGIN_EXEC(&States::setqregstring_building); register_argument = ® return &States::setqregstring_building; } void StateSetQRegString::initial(void) { int args; expressions.eval(); args = expressions.args(); text_added = args > 0; if (!args) return; gchar buffer[args+1]; buffer[args] = '\0'; while (args--) buffer[args] = (gchar)expressions.pop_num_calc(); if (eval_colon()) { /* append to register */ register_argument->undo_append_string(); register_argument->append_string(buffer); } else { /* set register */ register_argument->undo_set_string(); register_argument->set_string(buffer); } } State * StateSetQRegString::done(const gchar *str) { BEGIN_EXEC(&States::start); if (text_added || eval_colon()) { /* * Append to register: * Note that append_string() does not create an UNDOACTION * if str == NULL */ if (str) { register_argument->undo_append_string(); register_argument->append_string(str); } } else { /* set register */ register_argument->undo_set_string(); register_argument->set_string(str); } return &States::start; } /*$ * Gq -- Insert Q-Register string * * Inserts the string of Q-Register into the buffer * at its current position. * Specifying an undefined yields an error. */ State * StateGetQRegString::got_register(QRegister ®) { 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(); interface.undo_ssm(SCI_UNDO); } g_free(str); return &States::start; } /*$ * nUq -- Set Q-Register integer * -Uq * [n]:Uq -> Success|Failure * * Sets the integer-part of Q-Register to . * \(lq-U\(rq is equivalent to \(lq-1U\(rq, otherwise * the command fails if is missing. * * If the command is colon-modified, it returns a success * boolean if or \(lq-\(rq is given. * Otherwise it returns a failure boolean and does not * modify . * * The register is defined if it does not exist. */ State * StateSetQRegInteger::got_register(QRegister ®) { BEGIN_EXEC(&States::start); expressions.eval(); if (expressions.args() || expressions.num_sign < 0) { reg.undo_set_integer(); reg.set_integer(expressions.pop_num_calc()); if (eval_colon()) expressions.push(SUCCESS); } else if (eval_colon()) { expressions.push(FAILURE); } else { throw ArgExpectedError('U'); } return &States::start; } /*$ * [n]%q -> q+n -- Increase Q-Register integer * * Add to the integer part of register , returning * its new value. * will be defined if it does not exist. */ State * StateIncreaseQReg::got_register(QRegister ®) { tecoInt 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; } /*$ * Mq -- Execute macro * :Mq * * Execute macro stored in string of Q-Register . * The command itself does not push or pop and arguments from the stack * but the macro executed might well do so. * The new macro invocation level will contain its own go-to label table * and local Q-Register table. * Except when the command is colon-modified - in this case, local * Q-Registers referenced in the macro refer to the parent macro-level's * local Q-Register table (or whatever level defined one last). * * Errors during the macro execution will propagate to the M command. * In other words if a command in the macro fails, the M command will fail * and this failure propagates until the top-level macro (e.g. * the command-line macro). * * Note that the string of will be copied upon macro execution, * so subsequent changes to Q-Register from inside the macro do * not modify the executed code. */ State * StateMacro::got_register(QRegister ®) { BEGIN_EXEC(&States::start); /* don't create new local Q-Registers if colon modifier is given */ reg.execute(!eval_colon()); return &States::start; } /*$ * EMfile$ -- Execute macro from file * :EMfile$ * * Read the file with name into memory and execute its contents * as a macro. * It is otherwise similar to the \(lqM\(rq command. * * If could not be read, the command yields an error. */ State * StateMacroFile::done(const gchar *str) { BEGIN_EXEC(&States::start); /* don't create new local Q-Registers if colon modifier is given */ Execute::file(str, !eval_colon()); return &States::start; } /*$ * [lines]Xq -- Copy into or append to Q-Register * -Xq * from,toXq * [lines]:Xq * -:Xq * from,to:Xq * * Copy the next or previous number of from the buffer * into the Q-Register string. * If is omitted, the sign prefix is implied. * If two arguments are specified, the characters beginning * at position up to the character at position * are copied. * The semantics of the arguments is analogous to the K * command's arguments. * If the command is colon-modified, the characters will be * appended to the end of register instead. * * Register will be created if it is undefined. */ State * StateCopyToQReg::got_register(QRegister ®) { tecoInt 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 { tecoInt to = expressions.pop_num(); from = expressions.pop_num(); len = to - from; if (len < 0 || !Validate::pos(from) || !Validate::pos(to)) throw RangeError("X"); } tr.chrg.cpMin = from; tr.chrg.cpMax = from + len; tr.lpstrText = (char *)g_malloc(len + 1); interface.ssm(SCI_GETTEXTRANGE, 0, (sptr_t)&tr); if (eval_colon()) { reg.undo_append_string(); reg.append_string(tr.lpstrText); } else { reg.undo_set_string(); reg.set_string(tr.lpstrText); } g_free(tr.lpstrText); return &States::start; } } /* namespace SciTECO */