/*
* 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 "sciteco.h"
#include "string-utils.h"
#include "interface.h"
#include "undo.h"
#include "expressions.h"
#include "goto.h"
#include "qregisters.h"
#include "ring.h"
#include "parser.h"
#include "symbols.h"
#include "search.h"
#include "spawn.h"
#include "glob.h"
#include "cmdline.h"
#include "error.h"
namespace SciTECO {
//#define DEBUG
gint macro_pc = 0;
namespace States {
StateStart start;
StateControl control;
StateASCII ascii;
StateFCommand fcommand;
StateCondCommand condcommand;
StateECommand ecommand;
StateScintilla_symbols scintilla_symbols;
StateScintilla_lParam scintilla_lparam;
StateInsert insert_building(true);
StateInsert insert_nobuilding(false);
StateInsertIndent insert_indent;
State *current = &start;
}
namespace Modifiers {
static bool colon = false;
static bool at = false;
}
enum Mode mode = MODE_NORMAL;
/* FIXME: perhaps integrate into Mode */
static bool skip_else = false;
static gint nest_level = 0;
gchar *strings[2] = {NULL, NULL};
gchar escape_char = CTL_KEY_ESC;
/**
* Handles all expected exceptions, converting them to
* SciTECO::Error and preparing them for stack frame insertion.
* This method will only throw SciTECO::Error and
* SciTECO::Cmdline *.
*/
void
Execute::step(const gchar *macro, gint stop_pos)
{
while (macro_pc < stop_pos) {
#ifdef DEBUG
g_printf("EXEC(%d): input='%c'/%x, state=%p, mode=%d\n",
macro_pc, macro[macro_pc], macro[macro_pc],
States::current, mode);
#endif
try {
/*
* convert bad_alloc and other C++ standard
* library exceptions
*/
try {
if (interface.is_interrupted())
throw Error("Interrupted");
State::input(macro[macro_pc]);
} catch (std::exception &error) {
throw StdError(error);
}
} catch (Error &error) {
error.set_coord(macro, macro_pc);
throw; /* forward */
}
macro_pc++;
}
}
/*
* may throw non SciTECO::Error exceptions which are not to be
* associated with the macro invocation stack frame
*/
void
Execute::macro(const gchar *macro, bool locals)
{
GotoTable *parent_goto_table = Goto::table;
GotoTable macro_goto_table(false);
QRegisterTable *parent_locals;
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;
/* locals are allocated so that we do not waste call stack space */
if (locals) {
parent_locals = QRegisters::locals;
QRegisters::locals = new QRegisterTable(false);
}
try {
step(macro, strlen(macro));
/*
* Subsequent errors must still be
* attached to this macro invocation
* via Error::set_coord()
*/
try {
if (Goto::skip_label)
throw Error("Label \"%s\" not found",
Goto::skip_label);
if (States::current != &States::start)
/*
* can only happen if we returned because
* of macro end
*/
throw Error("Unterminated command");
/*
* This handles the problem of Q-Registers
* local to the macro invocation being edited
* when the macro terminates.
* QRegisterTable::clear() throws an error
* if this happens and the Q-Reg editing
* is undone.
*/
if (locals)
QRegisters::locals->clear();
} catch (Error &error) {
error.set_coord(macro, strlen(macro));
throw; /* forward */
}
} catch (...) {
g_free(Goto::skip_label);
Goto::skip_label = NULL;
if (locals) {
delete QRegisters::locals;
QRegisters::locals = parent_locals;
}
Goto::table = parent_goto_table;
macro_pc = parent_pc;
States::current = parent_state;
throw; /* forward */
}
if (locals) {
delete QRegisters::locals;
QRegisters::locals = parent_locals;
}
Goto::table = parent_goto_table;
macro_pc = parent_pc;
States::current = parent_state;
}
void
Execute::file(const gchar *filename, bool locals)
{
GError *gerror = NULL;
gchar *macro_str, *p;
if (!g_file_get_contents(filename, ¯o_str, NULL, &gerror))
throw GlibError(gerror);
/* only when executing files, ignore Hash-Bang line */
if (*macro_str == '#')
p = MAX(strchr(macro_str, '\r'), strchr(macro_str, '\n'))+1;
else
p = macro_str;
try {
macro(p, locals);
} catch (Error &error) {
error.pos += p - macro_str;
if (*macro_str == '#')
error.line++;
error.add_frame(new Error::FileFrame(filename));
g_free(macro_str);
throw; /* forward */
} catch (...) {
g_free(macro_str);
throw; /* forward */
}
g_free(macro_str);
}
State::State()
{
for (guint i = 0; i < G_N_ELEMENTS(transitions); i++)
transitions[i] = NULL;
}
bool
State::eval_colon(void)
{
if (!Modifiers::colon)
return false;
undo.push_var(Modifiers::colon);
Modifiers::colon = false;
return true;
}
void
State::input(gchar chr)
{
State *state = States::current;
for (;;) {
State *next = state->get_next_state(chr);
if (next == state)
break;
state = next;
chr = '\0';
}
if (state != States::current) {
undo.push_var(States::current);
States::current = state;
}
}
State *
State::get_next_state(gchar chr)
{
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;
}
void
StringBuildingMachine::reset(void)
{
MicroStateMachine::reset();
undo.push_obj(qregspec_machine) = NULL;
undo.push_var(mode) = MODE_NORMAL;
undo.push_var(toctl) = false;
}
gchar *
StringBuildingMachine::input(gchar chr)
{
QRegister *reg;
switch (mode) {
case MODE_UPPER:
chr = g_ascii_toupper(chr);
break;
case MODE_LOWER:
chr = g_ascii_tolower(chr);
break;
default:
break;
}
if (toctl) {
if (chr != '^')
chr = CTL_KEY(g_ascii_toupper(chr));
undo.push_var(toctl) = false;
} else if (chr == '^') {
undo.push_var(toctl) = true;
return NULL;
}
MICROSTATE_START;
switch (chr) {
case CTL_KEY('Q'):
case CTL_KEY('R'): set(&&StateEscaped); break;
case CTL_KEY('V'): set(&&StateLower); break;
case CTL_KEY('W'): set(&&StateUpper); break;
case CTL_KEY('E'): set(&&StateCtlE); break;
default:
goto StateEscaped;
}
return NULL;
StateLower:
set(StateStart);
if (chr != CTL_KEY('V'))
return String::chrdup(g_ascii_tolower(chr));
undo.push_var(mode) = MODE_LOWER;
return NULL;
StateUpper:
set(StateStart);
if (chr != CTL_KEY('W'))
return String::chrdup(g_ascii_toupper(chr));
undo.push_var(mode) = MODE_UPPER;
return NULL;
StateCtlE:
switch (g_ascii_toupper(chr)) {
case '\\':
undo.push_obj(qregspec_machine) = new QRegSpecMachine;
set(&&StateCtlENum);
break;
case 'Q':
undo.push_obj(qregspec_machine) = new QRegSpecMachine;
set(&&StateCtlEQ);
break;
case 'U':
undo.push_obj(qregspec_machine) = new QRegSpecMachine;
set(&&StateCtlEU);
break;
default: {
gchar *ret = (gchar *)g_malloc(3);
set(StateStart);
ret[0] = CTL_KEY('E');
ret[1] = chr;
ret[2] = '\0';
return ret;
}
}
return NULL;
StateCtlENum:
reg = qregspec_machine->input(chr);
if (!reg)
return NULL;
undo.push_obj(qregspec_machine) = NULL;
set(StateStart);
return g_strdup(expressions.format(reg->get_integer()));
StateCtlEU:
reg = qregspec_machine->input(chr);
if (!reg)
return NULL;
undo.push_obj(qregspec_machine) = NULL;
set(StateStart);
return String::chrdup((gchar)reg->get_integer());
StateCtlEQ:
reg = qregspec_machine->input(chr);
if (!reg)
return NULL;
undo.push_obj(qregspec_machine) = NULL;
set(StateStart);
return reg->get_string();
StateEscaped:
set(StateStart);
return String::chrdup(chr);
}
StringBuildingMachine::~StringBuildingMachine()
{
delete qregspec_machine;
}
State *
StateExpectString::custom(gchar chr)
{
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 CTL_KEY_ESC:
case '{':
undo.push_var(escape_char) = g_ascii_toupper(chr);
return this;
}
}
if (escape_char == '{') {
switch (chr) {
case '{':
undo.push_var(nesting)++;
break;
case '}':
undo.push_var(nesting)--;
break;
}
} else if (g_ascii_toupper(chr) == escape_char) {
undo.push_var(nesting)--;
}
if (!nesting) {
State *next;
gchar *string = strings[0];
undo.push_str(strings[0]) = NULL;
if (last)
undo.push_var(escape_char) = CTL_KEY_ESC;
nesting = 1;
if (string_building)
machine.reset();
next = done(string ? : "");
g_free(string);
return next;
}
BEGIN_EXEC(this);
/*
* String building characters
*/
if (string_building) {
insert = machine.input(chr);
if (!insert)
return this;
} else {
insert = String::chrdup(chr);
}
/*
* String accumulation
*/
undo.push_str(strings[0]);
String::append(strings[0], insert);
process(strings[0], strlen(insert));
g_free(insert);
return this;
}
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_building;
transitions['S'] = &States::search;
transitions['N'] = &States::searchall;
transitions['['] = &States::pushqreg;
transitions[']'] = &States::popqreg;
transitions['G'] = &States::getqregstring;
transitions['Q'] = &States::queryqreg;
transitions['U'] = &States::setqreginteger;
transitions['%'] = &States::increaseqreg;
transitions['M'] = &States::macro;
transitions['X'] = &States::copytoqreg;
}
void
StateStart::insert_integer(tecoInt v)
{
const gchar *str = expressions.format(v);
interface.ssm(SCI_BEGINUNDOACTION);
interface.ssm(SCI_ADDTEXT, strlen(str), (sptr_t)str);
interface.ssm(SCI_SCROLLCARET);
interface.ssm(SCI_ENDUNDOACTION);
ring.dirtify();
if (current_doc_must_undo())
interface.undo_ssm(SCI_UNDO);
}
tecoInt
StateStart::read_integer(void)
{
uptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
gchar c = (gchar)interface.ssm(SCI_GETCHARAT, pos);
tecoInt v = 0;
gint sign = 1;
if (c == '-') {
pos++;
sign = -1;
}
for (;;) {
c = 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(tecoInt n)
{
sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
if (!Validate::pos(pos + n))
return FAILURE;
interface.ssm(SCI_GOTOPOS, pos + n);
if (current_doc_must_undo())
interface.undo_ssm(SCI_GOTOPOS, pos);
return SUCCESS;
}
tecoBool
StateStart::move_lines(tecoInt n)
{
sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
sptr_t line = interface.ssm(SCI_LINEFROMPOSITION, pos) + n;
if (!Validate::line(line))
return FAILURE;
interface.ssm(SCI_GOTOLINE, line);
if (current_doc_must_undo())
interface.undo_ssm(SCI_GOTOPOS, pos);
return SUCCESS;
}
tecoBool
StateStart::delete_words(tecoInt n)
{
sptr_t pos, size;
if (!n)
return SUCCESS;
pos = interface.ssm(SCI_GETCURRENTPOS);
size = interface.ssm(SCI_GETLENGTH);
interface.ssm(SCI_BEGINUNDOACTION);
/*
* FIXME: would be nice to do this with constant amount of
* editor messages. E.g. by using custom algorithm accessing
* the internal document buffer.
*/
if (n > 0) {
while (n--) {
sptr_t size = interface.ssm(SCI_GETLENGTH);
interface.ssm(SCI_DELWORDRIGHTEND);
if (size == interface.ssm(SCI_GETLENGTH))
break;
}
} else {
n *= -1;
while (n--) {
sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
//interface.ssm(SCI_DELWORDLEFTEND);
interface.ssm(SCI_WORDLEFTEND);
if (pos == interface.ssm(SCI_GETCURRENTPOS))
break;
interface.ssm(SCI_DELWORDRIGHTEND);
}
}
interface.ssm(SCI_ENDUNDOACTION);
if (n >= 0) {
if (size != interface.ssm(SCI_GETLENGTH)) {
interface.ssm(SCI_UNDO);
interface.ssm(SCI_GOTOPOS, pos);
}
return FAILURE;
}
interface.undo_ssm(SCI_GOTOPOS, pos);
if (current_doc_must_undo())
interface.undo_ssm(SCI_UNDO);
ring.dirtify();
return SUCCESS;
}
State *
StateStart::custom(gchar chr)
{
tecoInt v;
tecoBool rc;
/*
* commands implemented in StateCtrlCmd
*/
if (IS_CTL(chr))
return States::control.get_next_state(CTL_ECHO(chr));
/*
* arithmetics
*/
/*$
* [n]0|1|2|3|4|5|6|7|8|9 -> n*Radix+X -- Append digit
*
* Integer constants in \*(ST may be thought of and are
* technically sequences of single-digit commands.
* These commands take one argument from the stack
* (0 is implied), multiply it with the current radix
* (2, 8, 10, 16, ...), add the digit's value and
* return the resultant integer.
*
* The command-like semantics of digits may be abused
* in macros, for instance to append digits to computed
* integers.
* It is not an error to append a digit greater than the
* current radix - this may be changed in the future.
*/
if (g_ascii_isdigit(chr)) {
BEGIN_EXEC(this);
expressions.add_digit(chr);
return this;
}
chr = g_ascii_toupper(chr);
switch (chr) {
case '/':
BEGIN_EXEC(this);
expressions.push_calc(Expressions::OP_DIV);
break;
case '*':
if (cmdline.len == 1 && cmdline[0] == '*')
/* special save last commandline command */
return &States::save_cmdline;
BEGIN_EXEC(this);
expressions.push_calc(Expressions::OP_MUL);
break;
case '+':
BEGIN_EXEC(this);
expressions.push_calc(Expressions::OP_ADD);
break;
case '-':
BEGIN_EXEC(this);
if (!expressions.args())
expressions.set_num_sign(-expressions.num_sign);
else
expressions.push_calc(Expressions::OP_SUB);
break;
case '&':
BEGIN_EXEC(this);
expressions.push_calc(Expressions::OP_AND);
break;
case '#':
BEGIN_EXEC(this);
expressions.push_calc(Expressions::OP_OR);
break;
case '(':
BEGIN_EXEC(this);
if (expressions.num_sign < 0) {
expressions.set_num_sign(1);
expressions.eval();
expressions.push(-1);
expressions.push_calc(Expressions::OP_MUL);
}
expressions.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;
/*$
* \&. -> dot -- Return buffer position
*
* \(lq.\(rq pushes onto the stack, the current
* position (also called ) of the currently
* selected buffer or Q-Register.
*/
case '.':
BEGIN_EXEC(this);
expressions.eval();
expressions.push(interface.ssm(SCI_GETCURRENTPOS));
break;
/*$
* Z -> size -- Return buffer size
*
* Pushes onto the stack, the size of the currently selected
* buffer or Q-Register.
* This is value is also the buffer position of the document's
* end.
*/
case 'Z':
BEGIN_EXEC(this);
expressions.eval();
expressions.push(interface.ssm(SCI_GETLENGTH));
break;
/*$
* H -> 0,Z -- Return range for entire buffer
*
* Pushes onto the stack the integer 0 (position of buffer
* beginning) and the current buffer's size.
* It is thus often equivalent to the expression
* \(lq0,Z\(rq, or more generally \(lq(0,Z)\(rq.
*/
case 'H':
BEGIN_EXEC(this);
expressions.eval();
expressions.push(0);
expressions.push(interface.ssm(SCI_GETLENGTH));
break;
/*$
* n\\ -- Insert or read ASCII numbers
* \\ -> n
*
* Backslash pops a value from the stack, formats it
* according to the current radix and inserts it in the
* current buffer or Q-Register at dot.
* If is omitted (empty stack), it does the reverse -
* it reads from the current buffer position an integer
* in the current radix and pushes it onto the stack.
* Dot is not changed when reading integers.
*
* In other words, the command serializes or deserializes
* integers as ASCII characters.
*/
case '\\':
BEGIN_EXEC(this);
expressions.eval();
if (expressions.args())
insert_integer(expressions.pop_num_calc());
else
expressions.push(read_integer());
break;
/*
* control structures (loops)
*/
case '<':
if (mode == MODE_PARSE_ONLY_LOOP) {
undo.push_var(nest_level);
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_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_NORMAL;
} else {
undo.push_var(nest_level);
nest_level--;
}
} else {
BEGIN_EXEC(this);
tecoInt loop_pc, loop_cnt;
expressions.discard_args();
if (expressions.pop_op() != Expressions::OP_LOOP)
throw Error("Loop end without corresponding "
"loop start command");
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;
/*$
* [bool]; -- Conditionally break from loop
* [bool]:;
*
* Breaks from the current inner-most loop if
* signifies failure (non-negative value).
* If colon-modified, breaks from the loop if
* signifies success (negative value).
*
* If the condition code cannot be popped from the stack,
* the global search register's condition integer
* is implied instead.
* This way, you may break on search success/failures
* without colon-modifying the search command (or at a
* later point).
*
* Executing \(lq;\(rq outside of iterations yields an
* error.
*/
case ';':
BEGIN_EXEC(this);
v = QRegisters::globals["_"]->get_integer();
rc = expressions.pop_num_calc(0, v);
if (eval_colon())
rc = ~rc;
if (IS_FAILURE(rc)) {
expressions.discard_args();
/*
* FIXME: it would be better accroding to the
* TECO standard to throw an error
* always when we're not in a loop.
* But this is not easy to find out without
* modifying the expression stack.
*/
if (expressions.pop_op() != Expressions::OP_LOOP)
throw Error("<;> only allowed in iterations");
expressions.pop_num(); /* pc */
expressions.pop_num(); /* counter */
/* skip to end of loop */
undo.push_var(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_NORMAL;
}
return this;
}
BEGIN_EXEC(this);
/* skip to end of conditional; skip ELSE-part */
undo.push_var(mode);
mode = MODE_PARSE_ONLY_COND;
break;
case '\'':
if (mode != MODE_PARSE_ONLY_COND)
break;
if (!nest_level) {
undo.push_var(mode);
mode = MODE_NORMAL;
undo.push_var(skip_else);
skip_else = false;
} else {
undo.push_var(nest_level);
nest_level--;
}
break;
/*
* Command-line editing
*/
/*$
* { -- Edit command line
* }
*
* The opening curly bracket is a powerful command
* to edit command lines but has very simple semantics.
* It copies the current commandline into the global
* command line editing register (called Escape, i.e.
* ASCII 27) and edits this register.
* The curly bracket itself is not copied.
*
* The command line may then be edited using any
* \*(ST command or construct.
* You may switch between the command line editing
* register and other registers or buffers.
* The user will then usually reapply (called update)
* the current command-line.
*
* The closing curly bracket will update the current
* command-line with the contents of the global command
* line editing register.
* To do so it merely rubs-out the current command-line
* up to the first changed character and inserts
* all characters following from the updated command
* line into the command stream.
* To prevent the undesired rubout of the entire
* command-line, the replacement command ("}") is only
* allowed when the replacement register currently edited
* since it will otherwise be usually empty.
*
* .B Note:
* - Command line editing only works on command lines,
* but not arbitrary macros.
* It is therefore not available in batch mode and
* will yield an error if used.
* - Command line editing commands may be safely used
* from macro invocations.
* Such macros are called command line editing macros.
* - A command line update from a macro invocation will
* always yield to the outer-most macro level (i.e.
* the command line macro).
* Code following the update command in the macro
* will thus never be executed.
* - As a safe-guard against command line trashing due
* to erroneous changes at the beginning of command
* lines, a backup mechanism is implemented:
* If the updated command line yields an error at
* any command during the update, the original
* command line will be restored with an algorithm
* similar to command line updating and the update
* command will fail instead.
* That way it behaves like any other command that
* yields an error:
* The character resulting in the update is rejected
* by the command line input subsystem.
* - In the rare case that an aforementioned command line
* backup fails, the commands following the erroneous
* character will not be inserted again (will be lost).
*/
case '{':
BEGIN_EXEC(this);
if (!undo.enabled)
throw Error("Command-line editing only possible in "
"interactive mode");
current_doc_undo_edit();
QRegisters::globals.edit(CTL_KEY_ESC_STR);
interface.ssm(SCI_BEGINUNDOACTION);
interface.ssm(SCI_CLEARALL);
interface.ssm(SCI_ADDTEXT, cmdline.pc, (sptr_t)cmdline.str);
interface.ssm(SCI_SCROLLCARET);
interface.ssm(SCI_ENDUNDOACTION);
/* must always support undo on global register */
interface.undo_ssm(SCI_UNDO);
break;
case '}':
BEGIN_EXEC(this);
if (!undo.enabled)
throw Error("Command-line editing only possible in "
"interactive mode");
if (QRegisters::current != QRegisters::globals[CTL_KEY_ESC_STR])
throw Error("Command-line replacement only allowed when "
"editing the replacement register");
/* replace cmdline in the outer macro environment */
cmdline.replace();
/* never reached */
/*
* modifiers
*/
case '@':
/*
* @ modifier has syntactic significance, so set it even
* in PARSE_ONLY* modes
*/
undo.push_var(Modifiers::at);
Modifiers::at = true;
break;
case ':':
BEGIN_EXEC(this);
undo.push_var(Modifiers::colon);
Modifiers::colon = true;
break;
/*
* commands
*/
/*$
* [position]J -- Go to position in buffer
* [position]:J -> Success|Failure
*
* Sets dot to .
* If is omitted, 0 is implied and \(lqJ\(rq will
* go to the beginning of the buffer.
*
* If is outside the range of the buffer, the
* command yields an error.
* If colon-modified, the command will instead return a
* condition boolean signalling whether the position could
* be changed or not.
*/
case 'J':
BEGIN_EXEC(this);
v = expressions.pop_num_calc(0, 0);
if (Validate::pos(v)) {
if (current_doc_must_undo())
interface.undo_ssm(SCI_GOTOPOS,
interface.ssm(SCI_GETCURRENTPOS));
interface.ssm(SCI_GOTOPOS, v);
if (eval_colon())
expressions.push(SUCCESS);
} else if (eval_colon()) {
expressions.push(FAILURE);
} else {
throw MoveError("J");
}
break;
/*$
* [n]C -- Move dot characters
* -C
* [n]:C -> Success|Failure
*
* Adds to dot. 1 or -1 is implied if is omitted.
* Fails if would move dot off-page.
* The colon modifier results in a success-boolean being
* returned instead.
*/
case 'C':
BEGIN_EXEC(this);
rc = move_chars(expressions.pop_num_calc());
if (eval_colon())
expressions.push(rc);
else if (IS_FAILURE(rc))
throw MoveError("C");
break;
/*$
* [n]R -- Move dot characters backwards
* -R
* [n]:R -> Success|Failure
*
* Subtracts from dot.
* It is equivalent to \(lq-nC\(rq.
*/
case 'R':
BEGIN_EXEC(this);
rc = move_chars(-expressions.pop_num_calc());
if (eval_colon())
expressions.push(rc);
else if (IS_FAILURE(rc))
throw MoveError("R");
break;
/*$
* [n]L -- Move dot lines forwards
* -L
* [n]:L -> Success|Failure
*
* Move dot to the beginning of the line specified
* relatively to the current line.
* Therefore a value of 0 for goes to the
* beginning of the current line, 1 will go to the
* next line, -1 to the previous line etc.
* If is omitted, 1 or -1 is implied depending on
* the sign prefix.
*
* If would move dot off-page, the command yields
* an error.
* The colon-modifer results in a condition boolean
* being returned instead.
*/
case 'L':
BEGIN_EXEC(this);
rc = move_lines(expressions.pop_num_calc());
if (eval_colon())
expressions.push(rc);
else if (IS_FAILURE(rc))
throw MoveError("L");
break;
/*$
* [n]B -- Move dot lines backwards
* -B
* [n]:B -> Success|Failure
*
* Move dot to the beginning of the line
* lines before the current one.
* It is equivalent to \(lq-nL\(rq.
*/
case 'B':
BEGIN_EXEC(this);
rc = move_lines(-expressions.pop_num_calc());
if (eval_colon())
expressions.push(rc);
else if (IS_FAILURE(rc))
throw MoveError("B");
break;
/*$
* [n]W -- Move dot by words
* -W
* [n]:W -> Success|Failure
*
* Move dot words forward.
* - If is positive, dot is positioned at the beginning
* of the word words after the current one.
* - If is negative, dot is positioned at the end
* of the word words before the current one.
* - If is zero, dot is not moved.
*
* \(lqW\(rq uses Scintilla's definition of a word as
* configurable using the
* .B SCI_SETWORDCHARS
* message.
*
* Otherwise, the command's behaviour is analogous to
* the \(lqC\(rq command.
*/
case 'W': {
sptr_t pos;
unsigned int msg = SCI_WORDRIGHTEND;
BEGIN_EXEC(this);
v = expressions.pop_num_calc();
pos = interface.ssm(SCI_GETCURRENTPOS);
/*
* FIXME: would be nice to do this with constant amount of
* editor messages. E.g. by using custom algorithm accessing
* the internal document buffer.
*/
if (v < 0) {
v *= -1;
msg = SCI_WORDLEFTEND;
}
while (v--) {
sptr_t pos = interface.ssm(SCI_GETCURRENTPOS);
interface.ssm(msg);
if (pos == interface.ssm(SCI_GETCURRENTPOS))
break;
}
if (v < 0) {
if (current_doc_must_undo())
interface.undo_ssm(SCI_GOTOPOS, pos);
if (eval_colon())
expressions.push(SUCCESS);
} else {
interface.ssm(SCI_GOTOPOS, pos);
if (eval_colon())
expressions.push(FAILURE);
else
throw MoveError("W");
}
break;
}
/*$
* [n]V -- Delete words forward
* -V
* [n]:V -> Success|Failure
*
* Deletes the next words until the end of the
* n'th word after the current one.
* If is negative, deletes up to end of the
* n'th word before the current one.
* If is omitted, 1 or -1 is implied depending on the
* sign prefix.
*
* It uses Scintilla's definition of a word as configurable
* using the
* .B SCI_SETWORDCHARS
* message.
*
* If the words to delete extend beyond the range of the
* buffer, the command yields an error.
* If colon-modified it instead returns a condition code.
*/
case 'V':
BEGIN_EXEC(this);
rc = delete_words(expressions.pop_num_calc());
if (eval_colon())
expressions.push(rc);
else if (IS_FAILURE(rc))
throw Error("Not enough words to delete with ");
break;
/*$
* [n]Y -- Delete word backwards
* -Y
* [n]:Y -> Success|Failure
*
* Delete words backward.
* Y is equivalent to \(lq-nV\(rq.
*/
case 'Y':
BEGIN_EXEC(this);
rc = delete_words(-expressions.pop_num_calc());
if (eval_colon())
expressions.push(rc);
else if (IS_FAILURE(rc))
throw Error("Not enough words to delete with ");
break;
/*$
* = -- Show value as message
*
* Shows integer as a message in the message line and/or
* on the console.
* It is currently always formatted as a decimal integer and
* shown with the user-message severity.
*/
/**
* @bug makes no sense to imply the sign-prefix!
* @todo perhaps care about current radix
* @todo colon-modifier to suppress line-break on console?
*/
case '=':
BEGIN_EXEC(this);
interface.msg(InterfaceCurrent::MSG_USER,
"%" TECO_INTEGER_FORMAT,
expressions.pop_num_calc());
break;
/*$
* [n]K -- Kill lines
* -K
* from,to K
* [n]:K -> Success|Failure
* from,to:K -> Success|Failure
*
* Deletes characters up to the beginning of the
* line lines after or before the current one.
* If is 0, \(lqK\(rq will delete up to the beginning
* of the current line.
* If is omitted, the sign prefix will be implied.
* So to delete the entire line regardless of the position
* in it, one can use \(lq0KK\(rq.
*
* If the deletion is beyond the buffer's range, the command
* will yield an error unless it has been colon-modified
* so it returns a condition code.
*
* If two arguments and are available, the
* command is synonymous to ,D.
*/
case 'K':
/*$
* [n]D -- Delete characters
* -D
* from,to D
* [n]:D -> Success|Failure
* from,to:D -> Success|Failure
*
* If is positive, the next characters (up to and
* character .+) are deleted.
* If is negative, the previous characters are
* deleted.
* If is omitted, the sign prefix will be implied.
*
* If two arguments can be popped from the stack, the
* command will delete the characters with absolute
* position up to from the current buffer.
*
* If the character range to delete is beyond the buffer's
* range, the command will yield an error unless it has
* been colon-modified so it returns a condition code
* instead.
*/
case 'D': {
tecoInt from, len;
BEGIN_EXEC(this);
expressions.eval();
if (expressions.args() <= 1) {
from = interface.ssm(SCI_GETCURRENTPOS);
if (chr == 'D') {
len = expressions.pop_num_calc();
rc = TECO_BOOL(Validate::pos(from + len));
} else /* chr == 'K' */ {
sptr_t line;
line = interface.ssm(SCI_LINEFROMPOSITION, from) +
expressions.pop_num_calc();
len = interface.ssm(SCI_POSITIONFROMLINE, line)
- from;
rc = TECO_BOOL(Validate::line(line));
}
if (len < 0) {
len *= -1;
from -= len;
}
} else {
tecoInt to = expressions.pop_num();
from = expressions.pop_num();
len = to - from;
rc = TECO_BOOL(len >= 0 && Validate::pos(from) &&
Validate::pos(to));
}
if (eval_colon())
expressions.push(rc);
else if (IS_FAILURE(rc))
throw RangeError(chr);
if (len == 0 || IS_FAILURE(rc))
break;
if (current_doc_must_undo()) {
interface.undo_ssm(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS));
interface.undo_ssm(SCI_UNDO);
}
interface.ssm(SCI_BEGINUNDOACTION);
interface.ssm(SCI_DELETERANGE, from, len);
interface.ssm(SCI_ENDUNDOACTION);
ring.dirtify();
break;
}
/*$
* [n]A -> code -- Get character code from buffer
* -A -> code
*
* Returns the character of the character
* relative to dot from the buffer.
* This can be an ASCII or Unicode codepoint
* depending on Scintilla's encoding of the current
* buffer.
* - If is 0, return the of the character
* pointed to by dot.
* - If is 1, return the of the character
* immediately after dot.
* - If is -1, return the of the character
* immediately preceding dot, ecetera.
* - If is omitted, the sign prefix is implied.
*
* If the position of the queried character is off-page,
* the command will yield an error.
*/
/** @todo does Scintilla really return code points??? */
case 'A':
BEGIN_EXEC(this);
v = interface.ssm(SCI_GETCURRENTPOS) +
expressions.pop_num_calc();
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)
{
switch (chr) {
/*
* loop flow control
*/
/*$
* F< -- Go to loop start
*
* Immediately jumps to the current loop's start.
* Also works from inside conditionals.
*/
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;
/*$
* F> -- Go to loop end
*
* Jumps to the current loop's end.
* If the loop has a counter or runs idefinitely, the jump
* is performed immediately.
* If the loop has reached its last iteration, parsing
* until the loop end command has been found is performed.
*
* In interactive mode, if the loop is incomplete and must
* be exited, you can type in the loop's remaining commands
* without them being executed (but they are parsed).
*
* Calling \fBF\>\fP outside of a loop will throw an
* error.
*/
case '>': {
tecoInt loop_pc, loop_cnt;
BEGIN_EXEC(&States::start);
/* FIXME: what if in brackets? */
expressions.discard_args();
if (expressions.pop_op() != Expressions::OP_LOOP)
throw Error("Jump to loop end without corresponding "
"loop start command");
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_PARSE_ONLY_LOOP;
}
break;
}
/*
* conditional flow control
*/
/*$
* F\' -- Jump to end of conditional
*/
case '\'':
BEGIN_EXEC(&States::start);
/* skip to end of conditional */
undo.push_var(mode);
mode = MODE_PARSE_ONLY_COND;
undo.push_var(skip_else);
skip_else = true;
break;
/*$
* F| -- Jump to else-part of conditional
*
* Jump to else-part of conditional or end of
* conditional (only if invoked from inside the
* condition's else-part).
*/
case '|':
BEGIN_EXEC(&States::start);
/* skip to ELSE-part or end of conditional */
undo.push_var(mode);
mode = MODE_PARSE_ONLY_COND;
break;
default:
throw SyntaxError(chr);
}
return &States::start;
}
StateCondCommand::StateCondCommand() : State()
{
transitions['\0'] = this;
}
State *
StateCondCommand::custom(gchar chr)
{
tecoInt value = 0;
bool result;
switch (mode) {
case MODE_PARSE_ONLY_COND:
undo.push_var(nest_level)++;
break;
case MODE_NORMAL:
expressions.eval();
if (chr == '~')
/* don't pop value for ~ conditionals */
break;
if (!expressions.args())
throw ArgExpectedError('"');
value = expressions.pop_num_calc();
break;
default:
break;
}
switch (g_ascii_toupper(chr)) {
case '~':
BEGIN_EXEC(&States::start);
result = !expressions.args();
break;
case 'A':
BEGIN_EXEC(&States::start);
result = g_ascii_isalpha((gchar)value);
break;
case 'C':
BEGIN_EXEC(&States::start);
result = g_ascii_isalnum((gchar)value) ||
value == '.' || value == '$' || value == '_';
break;
case 'D':
BEGIN_EXEC(&States::start);
result = g_ascii_isdigit((gchar)value);
break;
case 'I':
BEGIN_EXEC(&States::start);
result = G_IS_DIR_SEPARATOR((gchar)value);
break;
case 'S':
case 'T':
BEGIN_EXEC(&States::start);
result = IS_SUCCESS(value);
break;
case 'F':
case 'U':
BEGIN_EXEC(&States::start);
result = IS_FAILURE(value);
break;
case 'E':
case '=':
BEGIN_EXEC(&States::start);
result = value == 0;
break;
case 'G':
case '>':
BEGIN_EXEC(&States::start);
result = value > 0;
break;
case 'L':
case '<':
BEGIN_EXEC(&States::start);
result = value < 0;
break;
case 'N':
BEGIN_EXEC(&States::start);
result = value != 0;
break;
case 'R':
BEGIN_EXEC(&States::start);
result = g_ascii_isalnum((gchar)value);
break;
case 'V':
BEGIN_EXEC(&States::start);
result = g_ascii_islower((gchar)value);
break;
case 'W':
BEGIN_EXEC(&States::start);
result = g_ascii_isupper((gchar)value);
break;
default:
throw Error("Invalid conditional type \"%c\"", chr);
}
if (!result)
/* skip to ELSE-part or end of conditional */
undo.push_var(mode) = MODE_PARSE_ONLY_COND;
return &States::start;
}
StateControl::StateControl() : State()
{
transitions['\0'] = this;
transitions['I'] = &States::insert_indent;
transitions['U'] = &States::ctlucommand;
transitions['^'] = &States::ascii;
}
State *
StateControl::custom(gchar chr)
{
switch (g_ascii_toupper(chr)) {
/*$
* ^O -- Set radix to 8 (octal)
*/
case 'O':
BEGIN_EXEC(&States::start);
expressions.set_radix(8);
break;
/*$
* ^D -- Set radix to 10 (decimal)
*/
case 'D':
BEGIN_EXEC(&States::start);
expressions.set_radix(10);
break;
/*$
* radix^R -- Set and get radix
* ^R -> radix
*
* Set current radix to arbitrary value .
* If is omitted, the command instead
* returns the current radix.
*/
case 'R':
BEGIN_EXEC(&States::start);
expressions.eval();
if (!expressions.args())
expressions.push(expressions.radix);
else
expressions.set_radix(expressions.pop_num_calc());
break;
/*
* Alternatives: ^[, ,
*/
/*$
* ^[ -- Discard all arguments
* $
*
* Pops and discards all values from the stack that
* might otherwise be used as arguments to following
* commands.
* Therefore it stops popping on stack boundaries like
* they are introduced by arithmetic brackets or loops.
*
* Note that ^[ is usually typed using the Escape key.
* CTRL+[ however is possible as well and equivalent to
* Escape in every manner.
* The Caret-[ notation however is processed like any
* ordinary command and only works as the discard-arguments
* command.
*/
case '[':
BEGIN_EXEC(&States::start);
expressions.discard_args();
break;
/*
* Additional numeric operations
*/
/*$
* n^_ -> ~n -- Binary negation
*
* Binary negates (complements) and returns
* the result.
* Binary complements are often used to negate
* \*(ST booleans.
*/
case '_':
BEGIN_EXEC(&States::start);
expressions.push(~expressions.pop_num_calc());
break;
case '*':
BEGIN_EXEC(&States::start);
expressions.push_calc(Expressions::OP_POW);
break;
case '/':
BEGIN_EXEC(&States::start);
expressions.push_calc(Expressions::OP_MOD);
break;
case '#':
BEGIN_EXEC(&States::start);
expressions.push_calc(Expressions::OP_XOR);
break;
default:
throw Error("Unsupported command <^%c>", chr);
}
return &States::start;
}
/*$
* ^^c -> n -- Get ASCII code of character
*
* Returns the ASCII code of the character
* that is part of the command.
* Can be used in place of integer constants for improved
* readability.
* For instance ^^A will return 65.
*
* Note that this command can be typed CTRL+Caret or
* Caret-Caret.
*/
StateASCII::StateASCII() : State()
{
transitions['\0'] = this;
}
State *
StateASCII::custom(gchar chr)
{
BEGIN_EXEC(&States::start);
expressions.push(chr);
return &States::start;
}
StateECommand::StateECommand() : State()
{
transitions['\0'] = this;
transitions['%'] = &States::epctcommand;
transitions['B'] = &States::editfile;
transitions['C'] = &States::executecommand;
transitions['G'] = &States::egcommand;
transitions['I'] = &States::insert_nobuilding;
transitions['M'] = &States::macro_file;
transitions['N'] = &States::glob;
transitions['S'] = &States::scintilla_symbols;
transitions['Q'] = &States::eqcommand;
transitions['U'] = &States::eucommand;
transitions['W'] = &States::savefile;
}
State *
StateECommand::custom(gchar chr)
{
switch (g_ascii_toupper(chr)) {
/*$
* [bool]EF -- Remove buffer from ring
* -EF
*
* Removes buffer from buffer ring, effectively
* closing it.
* If the buffer is dirty (modified), EF will yield
* an error.
* may be a specified to enforce closing dirty
* buffers.
* If it is a Failure condition boolean (negative),
* the buffer will be closed unconditionally.
* If is absent, the sign prefix (1 or -1) will
* be implied, so \(lq-EF\(rq will always close the buffer.
*
* It is noteworthy that EF will be executed immediately in
* interactive mode but can be rubbed out at a later time
* to reopen the file.
* Closed files are kept in memory until the command line
* is terminated.
*/
case 'F':
BEGIN_EXEC(&States::start);
if (QRegisters::current)
throw Error("Q-Register currently edited");
if (IS_FAILURE(expressions.pop_num_calc()) &&
ring.current->dirty)
throw Error("Buffer \"%s\" is dirty",
ring.current->filename ? : "(Unnamed)");
ring.close();
break;
/*$
* flags ED -- Set and get ED-flags
* [off,]on ED
* ED -> flags
*
* With arguments, the command will set the \fBED\fP flags.
* is a bitmap of flags to set.
* Specifying one argument to set the flags is a special
* case of specifying two arguments that allow to control
* which flags to enable/disable.
* is a bitmap of flags to disable (set to 0 in ED
* flags) and is a bitmap of flags that is ORed into
* the flags variable.
* If is omitted, the value 0^_ is implied.
* In otherwords, all flags are turned off before turning
* on the flags.
* Without any argument ED returns the current flags.
*
* Currently, the following flags are used by \*(ST:
* - 16: Enable/disable automatic translation of end of
* line sequences to and from line feed.
* - 32: Enable/Disable buffer editing hooks
* (via execution of macro in global Q-Register \(lqED\(rq)
* - 64: Enable/Disable function key macros
* - 128: Enable/Disable enforcement of UNIX98
* \(lq/bin/sh\(rq emulation for operating system command
* executions
*
* The features controlled thus are discribed in other sections
* of this manual.
*
* The default value of the \fBED\fP flags is 16
* (only automatic EOL translation enabled).
*/
case 'D':
BEGIN_EXEC(&States::start);
expressions.eval();
if (!expressions.args()) {
expressions.push(Flags::ed);
} else {
tecoInt on = expressions.pop_num_calc();
tecoInt off = expressions.pop_num_calc(0, ~(tecoInt)0);
undo.push_var(Flags::ed);
Flags::ed = (Flags::ed & ~off) | on;
}
break;
/*$
* [key]EJ -> value -- Get and set system properties
* -EJ -> value
* value,keyEJ
*
* This command may be used to get and set system
* properties.
* With one argument, it retrieves a numeric property
* identified by \fIkey\fP.
* If \fIkey\fP is omitted, the prefix sign is implied
* (1 or -1).
* With two arguments, it sets property \fIkey\fP to
* \fIvalue\fP and returns nothing. Properties may be
* read-only.
*
* The following property keys are defined:
* .IP 0 4
* The current user interface: 1 for Curses, 2 for GTK
* (\fBread-only\fP)
* .IP 1
* The current numbfer of buffers: Also the numeric id
* of the last buffer in the ring. This is implied if
* no argument is given, so \(lqEJ\(rq returns the number
* of buffers in the ring.
* (\fBread-only\fP)
* .IP 2
* The current undo stack memory limit in bytes.
* This limit helps to prevent dangerous out-of-memory
* conditions (e.g. resulting from infinite loops) by
* approximating the memory used by \*(ST's undo stack and is only
* effective in interactive mode.
* Commands which would exceed that limit fail instead.
* When getting, a zero value indicates that memory limiting is
* disabled.
* Setting a value less than or equal to 0 as in
* \(lq0,2EJ\(rq disables the limit.
* \fBWarning:\fP Disabling memory limiting may provoke
* uncontrollable out-of-memory errors in long running
* or infinite loops.
* Setting a new limit may fail if the current undo stack
* is too large for the new limit \(em if this happens
* you may have to clear your command-line first.
* Undo stack memory limiting is enabled by default.
*/
case 'J': {
BEGIN_EXEC(&States::start);
enum {
EJ_USER_INTERFACE = 0,
EJ_BUFFERS,
EJ_UNDO_MEMORY_LIMIT
};
tecoInt property;
expressions.eval();
property = expressions.pop_num_calc();
if (expressions.args() > 0) {
/* set property */
tecoInt value = expressions.pop_num_calc();
switch (property) {
case EJ_UNDO_MEMORY_LIMIT:
undo.set_memory_limit(MAX(0, value));
break;
default:
throw Error("Cannot set property %" TECO_INTEGER_FORMAT
" for ", property);
}
break;
}
switch (property) {
case EJ_USER_INTERFACE:
#ifdef INTERFACE_CURSES
expressions.push(1);
#elif defined(INTERFACE_GTK)
expressions.push(2);
#else
#error Missing value for current interface!
#endif
break;
case EJ_BUFFERS:
expressions.push(ring.get_id(ring.last()));
break;
case EJ_UNDO_MEMORY_LIMIT:
expressions.push(undo.memory_limit);
break;
default:
throw Error("Invalid property %" TECO_INTEGER_FORMAT
" for ", property);
}
break;
}
/*$
* 0EL -- Set or get End of Line mode
* 13,10:EL
* 1EL
* 13:EL
* 2EL
* 10:EL
* EL -> 0 | 1 | 2
* :EL -> 13,10 | 13 | 10
*
* Sets or gets the current document's End Of Line (EOL) mode.
* This is a thin wrapper around Scintilla's
* \fBSCI_SETEOLMODE\fP and \fBSCI_GETEOLMODE\fP messages but is
* shorter to type and supports restoring the EOL mode upon rubout.
* Like the Scintilla message, does \fBnot\fP change the
* characters in the current document.
* If automatic EOL translation is activated (which is the default),
* \*(ST will however use this information when saving files or
* writing to external processes.
*
* With one argument, the EOL mode is set according to these
* constants:
* .IP 0 4
* Carriage return (ASCII 13), followed by line feed (ASCII 10).
* This is the default EOL mode on DOS/Windows.
* .IP 1
* Carriage return (ASCII 13).
* The default EOL mode on old Mac OS systems.
* .IP 2
* Line feed (ASCII 10).
* The default EOL mode on POSIX/UNIX systems.
*
* In its colon-modified form, the EOL mode is set according
* to the EOL characters on the expression stack.
* \*(ST will only pop as many values as are necessary to
* determine the EOL mode.
*
* Without arguments, the current EOL mode is returned.
* When colon-modified, the current EOL mode's character sequence
* is pushed onto the expression stack.
*/
case 'L':
BEGIN_EXEC(&States::start);
expressions.eval();
if (expressions.args() > 0) {
gint eol_mode;
if (eval_colon()) {
switch (expressions.pop_num_calc()) {
case '\r':
eol_mode = SC_EOL_CR;
break;
case '\n':
if (!expressions.args()) {
eol_mode = SC_EOL_LF;
break;
}
if (expressions.pop_num_calc() == '\r') {
eol_mode = SC_EOL_CRLF;
break;
}
/* fall through */
default:
throw Error("Invalid EOL sequence for ");
}
} else {
eol_mode = expressions.pop_num_calc();
switch (eol_mode) {
case SC_EOL_CRLF:
case SC_EOL_CR:
case SC_EOL_LF:
break;
default:
throw Error("Invalid EOL mode %d for ",
eol_mode);
}
}
interface.undo_ssm(SCI_SETEOLMODE,
interface.ssm(SCI_GETEOLMODE));
interface.ssm(SCI_SETEOLMODE, eol_mode);
} else if (eval_colon()) {
expressions.push_str(get_eol_seq(interface.ssm(SCI_GETEOLMODE)));
} else {
expressions.push(interface.ssm(SCI_GETEOLMODE));
}
break;
/*$
* [bool]EX -- Exit program
* -EX
*
* Exits \*(ST.
* It is one of the few commands that is not executed
* immediately both in batch and interactive mode.
* In batch mode EX will exit the program if control
* reaches the end of the munged file, preventing
* interactive mode editing.
* In interactive mode, EX will request an exit that
* is performed on command line termination
* (i.e. after \fB$$\fP).
*
* If any buffer is dirty (modified), EX will yield
* an error.
* When specifying as a Failure condition
* boolean, EX will exit unconditionally.
* If is omitted, the sign prefix is implied
* (1 or -1).
* In other words \(lq-EX\(rq will always succeed.
*/
/** @bug what if changing file after EX? will currently still exit */
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(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};
/*$
* -- Send Scintilla message
* [lParam[,wParam]]ESmessage[,wParam]$[lParam]$ -> result
*
* Send Scintilla message with code specified by symbolic
* name , and .
* may be symbolic when specified as part of the
* first string argument.
* If not it is popped from the stack.
* may be specified as a constant string whose
* pointer is passed to Scintilla if specified as the second
* string argument.
* If the second string argument is empty, is popped
* from the stack instead.
* Parameters popped from the stack may be omitted, in which
* case 0 is implied.
* The message's return value is pushed onto the stack.
*
* All messages defined by Scintilla (as C macros) can be
* used by passing their name as a string to ES
* (e.g. ESSCI_LINESONSCREEN...).
* The \(lqSCI_\(rq prefix may be omitted and message symbols
* are case-insensitive.
* Only the Scintilla lexer symbols (SCLEX_..., SCE_...)
* may be used symbolically with the ES command as ,
* other values must be passed as integers on the stack.
* In interactive mode, symbols may be auto-completed by
* pressing Tab.
* String-building characters are by default interpreted
* in the string arguments.
*
* .BR Warning :
* Almost all Scintilla messages may be dispatched using
* this command.
* \*(ST does not keep track of the editor state changes
* performed by these commands and cannot undo them.
* You should never use it to change the editor state
* (position changes, deletions, etc.) or otherwise
* rub out will result in an inconsistent editor state.
* There are however exceptions:
* - In the editor profile and batch mode in general,
* the ES command may be used freely.
* - In the ED hook macro (register \(lqED\(rq),
* when a file is added to the ring, most destructive
* operations can be performed since rubbing out the
* EB command responsible for the hook execution also
* removes the buffer from the ring again.
*/
State *
StateScintilla_symbols::done(const gchar *str)
{
BEGIN_EXEC(&States::scintilla_lparam);
undo.push_var(scintilla_message);
if (*str) {
gchar **symbols = g_strsplit(str, ",", -1);
tecoInt v;
if (!symbols[0])
goto cleanup;
if (*symbols[0]) {
v = Symbols::scintilla.lookup(symbols[0], "SCI_");
if (v < 0)
throw Error("Unknown Scintilla message symbol \"%s\"",
symbols[0]);
scintilla_message.iMessage = v;
}
if (!symbols[1])
goto cleanup;
if (*symbols[1]) {
v = Symbols::scilexer.lookup(symbols[1]);
if (v < 0)
throw Error("Unknown Scintilla Lexer symbol \"%s\"",
symbols[1]);
scintilla_message.wParam = v;
}
if (!symbols[2])
goto cleanup;
if (*symbols[2]) {
v = Symbols::scilexer.lookup(symbols[2]);
if (v < 0)
throw Error("Unknown Scintilla Lexer symbol \"%s\"",
symbols[2]);
scintilla_message.lParam = v;
}
cleanup:
g_strfreev(symbols);
}
expressions.eval();
if (!scintilla_message.iMessage) {
if (!expressions.args())
throw Error(" command requires at least a message code");
scintilla_message.iMessage = expressions.pop_num_calc(0, 0);
}
if (!scintilla_message.wParam)
scintilla_message.wParam = expressions.pop_num_calc(0, 0);
return &States::scintilla_lparam;
}
State *
StateScintilla_lParam::done(const gchar *str)
{
BEGIN_EXEC(&States::start);
if (!scintilla_message.lParam)
scintilla_message.lParam = *str ? (sptr_t)str
: expressions.pop_num_calc(0, 0);
expressions.push(interface.ssm(scintilla_message.iMessage,
scintilla_message.wParam,
scintilla_message.lParam));
undo.push_var(scintilla_message);
memset(&scintilla_message, 0, sizeof(scintilla_message));
return &States::start;
}
/*
* NOTE: cannot support VideoTECO's I because
* beginning and end of strings must be determined
* syntactically
*/
/*$
* [c1,c2,...]I[text]$ -- Insert text with string building characters
*
* First inserts characters for all the values
* on the argument stack (interpreted as codepoints).
* It does so in the order of the arguments, i.e.
* is inserted before , ecetera.
* Secondly, the command inserts .
* In interactive mode, is inserted interactively.
*
* String building characters are \fBenabled\fP for the
* I command.
* When editing \*(ST macros, using the \fBEI\fP command
* may be better, since it has string building characters
* disabled.
*/
/*$
* [c1,c2,...]EI[text]$ -- Insert text without string building characters
*
* Inserts text at the current position in the current
* document.
* This command is identical to the \fBI\fP command,
* except that string building characters are \fBdisabled\fP.
* Therefore it may be beneficial when editing \*(ST
* macros.
*/
void
StateInsert::initial(void)
{
guint args;
expressions.eval();
args = expressions.args();
if (!args)
return;
interface.ssm(SCI_BEGINUNDOACTION);
for (int i = args; i > 0; i--) {
gchar chr = (gchar)expressions.peek_num(i-1);
interface.ssm(SCI_ADDTEXT, 1, (sptr_t)&chr);
}
for (int i = args; i > 0; i--)
expressions.pop_num_calc();
interface.ssm(SCI_SCROLLCARET);
interface.ssm(SCI_ENDUNDOACTION);
ring.dirtify();
if (current_doc_must_undo())
interface.undo_ssm(SCI_UNDO);
}
void
StateInsert::process(const gchar *str, gint new_chars)
{
interface.ssm(SCI_BEGINUNDOACTION);
interface.ssm(SCI_ADDTEXT, new_chars,
(sptr_t)(str + strlen(str) - new_chars));
interface.ssm(SCI_SCROLLCARET);
interface.ssm(SCI_ENDUNDOACTION);
ring.dirtify();
if (current_doc_must_undo())
interface.undo_ssm(SCI_UNDO);
}
State *
StateInsert::done(const gchar *str)
{
/* nothing to be done when done */
return &States::start;
}
/*
* Alternatives: ^i, ^I, ,
*/
/*$
* [char,...]^I[text]$ -- Insert with leading indention
*
* ^I (usually typed using the Tab key), first inserts
* all the chars on the stack into the buffer, then indention
* characters (one tab or multiple spaces) and eventually
* the optional is inserted interactively.
* It is thus a derivate of the \fBI\fP (insertion) command.
*
* \*(ST uses Scintilla settings to determine the indention
* characters.
* If tab use is enabled with the \fBSCI_SETUSETABS\fP message,
* a single tab character is inserted.
* Tab use is enabled by default.
* Otherwise, a number of spaces is inserted up to the
* next tab stop so that the command's argument
* is inserted at the beginning of the next tab stop.
* The size of the tab stops is configured by the
* \fBSCI_SETTABWIDTH\fP Scintilla message (8 by default).
* In combination with \*(ST's use of the tab key as an
* immediate editing command for all insertions, this
* implements support for different insertion styles.
* The Scintilla settings apply to the current Scintilla
* document and are thus local to the currently edited
* buffer or Q-Register.
*
* However for the same reason, the ^I command is not
* fully compatible with classic TECO which \fIalways\fP
* inserts a single tab character and should not be used
* for the purpose of inserting single tabs in generic
* macros.
* To insert a single tab character reliably, the idioms
* \(lq9I$\(rq or \(lqI^I$\(rq may be used.
*
* Like the I command, ^I has string building characters
* \fBenabled\fP.
*/
void
StateInsertIndent::initial(void)
{
StateInsert::initial();
interface.ssm(SCI_BEGINUNDOACTION);
if (interface.ssm(SCI_GETUSETABS)) {
interface.ssm(SCI_ADDTEXT, 1, (sptr_t)"\t");
} else {
gint len = interface.ssm(SCI_GETTABWIDTH);
len -= interface.ssm(SCI_GETCOLUMN,
interface.ssm(SCI_GETCURRENTPOS)) % len;
gchar spaces[len];
memset(spaces, ' ', sizeof(spaces));
interface.ssm(SCI_ADDTEXT, sizeof(spaces), (sptr_t)spaces);
}
interface.ssm(SCI_SCROLLCARET);
interface.ssm(SCI_ENDUNDOACTION);
ring.dirtify();
if (current_doc_must_undo())
interface.undo_ssm(SCI_UNDO);
}
} /* namespace SciTECO */