/*
* Copyright (C) 2012-2013 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 "interface.h"
#include "expressions.h"
#include "parser.h"
#include "qregisters.h"
#include "ring.h"
#include "goto.h"
#include "undo.h"
#include "symbols.h"
#include "cmdline.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;
gint cmdline_pos = 0;
static gchar *last_cmdline = NULL;
bool quit_requested = false;
namespace States {
StateSaveCmdline save_cmdline;
}
void
cmdline_keypress(gchar key)
{
gchar *old_cmdline = NULL;
gint repl_pos = 0;
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, one at a time so
* undo tokens get emitted for the corresponding characters.
*/
cmdline_pos = cmdline ? strlen(cmdline)+1 : 1;
String::append(cmdline, insert);
while (cmdline[cmdline_pos-1]) {
try {
Execute::step(cmdline, cmdline_pos);
} catch (ReplaceCmdline &r) {
undo.pop(r.pos);
old_cmdline = cmdline;
cmdline = r.new_cmdline;
cmdline_pos = repl_pos = r.pos;
macro_pc = r.pos-1;
continue;
} catch (...) {
if (old_cmdline) {
undo.pop(repl_pos);
g_free(cmdline);
cmdline = old_cmdline;
cmdline[strlen(cmdline)-1] = '\0';
old_cmdline = NULL;
cmdline_pos = repl_pos;
macro_pc = repl_pos-1;
continue;
}
/*
* 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_pos);
cmdline[cmdline_pos-1] = '\0';
/* program counter could be messed up */
macro_pc = cmdline_pos - 1;
break;
}
cmdline_pos++;
}
g_free(old_cmdline);
/*
* 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] = key;
insert[1] = '\0';
switch (key) {
case '\b':
if (cmdline_len) {
undo.pop(cmdline_len);
cmdline[cmdline_len - 1] = '\0';
macro_pc--;
}
*insert = '\0';
break;
case CTL_KEY('W'):
if (States::is_string()) {
gchar wchars[interface.ssm(SCI_GETWORDCHARS)];
interface.ssm(SCI_GETWORDCHARS, 0, (sptr_t)wchars);
/* rubout non-word chars */
while (strings[0] && strlen(strings[0]) > 0 &&
!strchr(wchars, cmdline[macro_pc-1]))
undo.pop(macro_pc--);
/* rubout word chars */
while (strings[0] && strlen(strings[0]) > 0 &&
strchr(wchars, cmdline[macro_pc-1]))
undo.pop(macro_pc--);
} else if (cmdline_len) {
do
undo.pop(macro_pc--);
while (States::current != &States::start);
}
cmdline[macro_pc] = '\0';
*insert = '\0';
break;
case CTL_KEY('U'):
if (States::is_string()) {
while (strings[0] && strlen(strings[0]) > 0)
undo.pop(macro_pc--);
cmdline[macro_pc] = '\0';
*insert = '\0';
}
break;
case CTL_KEY('T'):
if (States::is_string()) {
const gchar *filename = last_occurrence(strings[0]);
gchar *new_chars = filename_complete(filename);
*insert = '\0';
if (new_chars)
g_stpcpy(insert, new_chars);
g_free(new_chars);
}
break;
case '\t':
if (States::is_file()) {
gchar complete = escape_char == '{' ? ' ' : escape_char;
gchar *new_chars = filename_complete(strings[0],
complete);
*insert = '\0';
if (new_chars)
g_stpcpy(insert, new_chars);
g_free(new_chars);
} else if (States::current == &States::scintilla_symbols) {
const gchar *symbol = last_occurrence(strings[0], ",");
SymbolList &list = symbol == strings[0]
? Symbols::scintilla
: Symbols::scilexer;
gchar *new_chars = symbol_complete(list, symbol, ',');
*insert = '\0';
if (new_chars)
g_stpcpy(insert, new_chars);
g_free(new_chars);
}
break;
case '\x1B':
if (States::current == &States::start &&
cmdline && cmdline[cmdline_len - 1] == '\x1B') {
*insert = '\0';
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();
g_free(last_cmdline);
last_cmdline = cmdline;
cmdline = NULL;
macro_pc = 0;
}
break;
#ifdef SIGTSTP
case CTL_KEY('Z'):
/*
* does not raise signal if handling of
* special characters temporarily disabled in terminal
* (Curses), or command-line is detached from
* terminal (GTK+)
*/
raise(SIGTSTP);
*insert = '\0';
break;
#endif
}
return insert;
}
void
cmdline_fnmacro(const gchar *name)
{
gchar macro_name[1 + strlen(name) + 1];
QRegister *reg;
macro_name[0] = CTL_KEY('F');
g_strlcpy(macro_name + 1, name, sizeof(macro_name) - 1);
reg = QRegisters::globals[macro_name];
if (reg) {
gchar *macro = reg->get_string();
cmdline_keypress(macro);
g_free(macro);
}
}
const gchar *
get_eol(void)
{
switch (interface.ssm(SCI_GETEOLMODE)) {
case SC_EOL_CR:
return "\r";
case SC_EOL_CRLF:
return "\r\n";
case SC_EOL_LF:
default:
return "\n";
}
}
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, "");
break;
case '\n':
rp = g_stpcpy(rp, "");
break;
case '\t':
rp = g_stpcpy(rp, "");
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, NIL);
if (g_file_test(filename, G_FILE_TEST_IS_DIR)) {
gchar *new_filename;
new_filename = g_strconcat(filename,
G_DIR_SEPARATOR_S, NIL);
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;
}
/*
* Command states
*/
/*$
* *q -- Save last command line
*
* Only at the very beginning of a command-line, this command
* may be used to save the last command line as a string in
* Q-Register .
*/
State *
StateSaveCmdline::got_register(QRegister ®) throw (Error)
{
BEGIN_EXEC(&States::start);
reg.undo_set_string();
reg.set_string(last_cmdline);
return &States::start;
}
/*
* Auxiliary functions
*/
static const gchar *
last_occurrence(const gchar *str, const gchar *chars)
{
if (!str)
return NULL;
while (*chars) {
const gchar *p = strrchr(str, *chars++);
if (p)
str = p+1;
}
return str;
}
static inline gboolean
filename_is_dir(const gchar *filename)
{
return g_str_has_suffix(filename, G_DIR_SEPARATOR_S);
}