aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/search.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/search.c')
-rw-r--r--src/search.c1130
1 files changed, 1130 insertions, 0 deletions
diff --git a/src/search.c b/src/search.c
new file mode 100644
index 0000000..e5e4bd8
--- /dev/null
+++ b/src/search.c
@@ -0,0 +1,1130 @@
+/*
+ * Copyright (C) 2012-2021 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string.h>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "sciteco.h"
+#include "string-utils.h"
+#include "expressions.h"
+#include "interface.h"
+#include "undo.h"
+#include "qreg.h"
+#include "ring.h"
+#include "parser.h"
+#include "core-commands.h"
+#include "error.h"
+#include "search.h"
+
+typedef struct {
+ /*
+ * FIXME: Should perhaps all be teco_int_t?
+ */
+ gint dot;
+ gint from, to;
+ gint count;
+
+ teco_buffer_t *from_buffer, *to_buffer;
+} teco_search_parameters_t;
+
+TECO_DEFINE_UNDO_OBJECT_OWN(parameters, teco_search_parameters_t, /* don't delete */);
+
+/*
+ * FIXME: Global state should be part of teco_machine_main_t
+ */
+static teco_search_parameters_t teco_search_parameters;
+
+static teco_machine_qregspec_t *teco_search_qreg_machine = NULL;
+
+static gboolean
+teco_state_search_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ if (G_UNLIKELY(!teco_search_qreg_machine))
+ teco_search_qreg_machine = teco_machine_qregspec_new(TECO_QREG_REQUIRED, ctx->qreg_table_locals,
+ ctx->parent.must_undo);
+
+ teco_undo_object_parameters_push(&teco_search_parameters);
+ teco_search_parameters.dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ teco_int_t v1, v2;
+ if (!teco_expressions_pop_num_calc(&v2, teco_num_sign, error))
+ return FALSE;
+ if (teco_expressions_args()) {
+ /* TODO: optional count argument? */
+ if (!teco_expressions_pop_num_calc(&v1, 0, error))
+ return FALSE;
+ if (v1 <= v2) {
+ teco_search_parameters.count = 1;
+ teco_search_parameters.from = (gint)v1;
+ teco_search_parameters.to = (gint)v2;
+ } else {
+ teco_search_parameters.count = -1;
+ teco_search_parameters.from = (gint)v2;
+ teco_search_parameters.to = (gint)v1;
+ }
+
+ if (!teco_validate_pos(teco_search_parameters.from) ||
+ !teco_validate_pos(teco_search_parameters.to)) {
+ /*
+ * FIXME: In derived classes, the command name will
+ * no longer be correct.
+ * Better use a generic error message and prefix the error in the
+ * derived states.
+ */
+ teco_error_range_set(error, "S");
+ return FALSE;
+ }
+ } else {
+ teco_search_parameters.count = (gint)v2;
+ if (v2 >= 0) {
+ teco_search_parameters.from = teco_search_parameters.dot;
+ teco_search_parameters.to = teco_interface_ssm(SCI_GETLENGTH, 0, 0);
+ } else {
+ teco_search_parameters.from = 0;
+ teco_search_parameters.to = teco_search_parameters.dot;
+ }
+ }
+
+ teco_search_parameters.from_buffer = teco_qreg_current ? NULL : teco_ring_current;
+ teco_search_parameters.to_buffer = NULL;
+ return TRUE;
+}
+
+static const gchar *
+teco_regexp_escape_chr(gchar chr)
+{
+ static gchar escaped[] = {'\\', '\0', '\0', '\0'};
+
+ if (!chr) {
+ escaped[1] = 'c';
+ escaped[2] = '@';
+ return escaped;
+ }
+
+ escaped[1] = chr;
+ escaped[2] = '\0';
+ return g_ascii_isalnum(chr) ? escaped + 1 : escaped;
+}
+
+typedef enum {
+ TECO_SEARCH_STATE_START,
+ TECO_SEARCH_STATE_NOT,
+ TECO_SEARCH_STATE_CTL_E,
+ TECO_SEARCH_STATE_ANYQ,
+ TECO_SEARCH_STATE_MANY,
+ TECO_SEARCH_STATE_ALT
+} teco_search_state_t;
+
+/**
+ * Convert a SciTECO pattern character class to a regular
+ * expression character class.
+ * It will throw an error for wrong class specs (like invalid
+ * Q-Registers) but not incomplete ones.
+ *
+ * @param state Initial pattern converter state.
+ * May be modified on return, e.g. when ^E has been
+ * scanned without a valid class.
+ * @param pattern The character class definition to convert.
+ * This may point into a longer pattern.
+ * The pointer is modified and always left after
+ * the last character used, so it may point to the
+ * terminating null byte after the call.
+ * @param escape_default Whether to treat single characters
+ * as classes or not.
+ * @param error A GError.
+ * @return A regular expression string or NULL in case of error.
+ * An empty string signals an incomplete class specification.
+ * When a non-empty string is returned, the state has always
+ * been reset to TECO_STATE_STATE_START.
+ * Must be freed with g_free().
+ */
+static gchar *
+teco_class2regexp(teco_search_state_t *state, teco_string_t *pattern,
+ gboolean escape_default, GError **error)
+{
+ while (pattern->len > 0) {
+ switch (*state) {
+ case TECO_SEARCH_STATE_START:
+ switch (*pattern->data) {
+ case TECO_CTL_KEY('S'):
+ pattern->data++;
+ pattern->len--;
+ return g_strdup("[:^alnum:]");
+ case TECO_CTL_KEY('E'):
+ *state = TECO_SEARCH_STATE_CTL_E;
+ break;
+ default:
+ /*
+ * Either a single character "class" or
+ * not a valid class at all.
+ */
+ if (!escape_default)
+ return g_strdup("");
+ pattern->len--;
+ return g_strdup(teco_regexp_escape_chr(*pattern->data++));
+ }
+ break;
+
+ case TECO_SEARCH_STATE_CTL_E:
+ switch (teco_ascii_toupper(*pattern->data)) {
+ case 'A':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:alpha:]");
+ /* same as <CTRL/S> */
+ case 'B':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:^alnum:]");
+ case 'C':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:alnum:].$");
+ case 'D':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:digit:]");
+ case 'G':
+ *state = TECO_SEARCH_STATE_ANYQ;
+ break;
+ case 'L':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("\r\n\v\f");
+ case 'R':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:alnum:]");
+ case 'V':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:lower:]");
+ case 'W':
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_strdup("[:upper:]");
+ default:
+ /*
+ * Not a valid ^E class, but could still
+ * be a higher-level ^E pattern.
+ */
+ return g_strdup("");
+ }
+ break;
+
+ case TECO_SEARCH_STATE_ANYQ: {
+ teco_qreg_t *reg;
+
+ switch (teco_machine_qregspec_input(teco_search_qreg_machine,
+ *pattern->data, &reg, NULL, error)) {
+ case TECO_MACHINE_QREGSPEC_ERROR:
+ return NULL;
+
+ case TECO_MACHINE_QREGSPEC_MORE:
+ /* incomplete, but consume byte */
+ break;
+
+ case TECO_MACHINE_QREGSPEC_DONE:
+ teco_machine_qregspec_reset(teco_search_qreg_machine);
+
+ g_auto(teco_string_t) str = {NULL, 0};
+ if (!reg->vtable->get_string(reg, &str.data, &str.len, error))
+ return NULL;
+
+ pattern->data++;
+ pattern->len--;
+ *state = TECO_SEARCH_STATE_START;
+ return g_regex_escape_string(str.data, str.len);
+ }
+ break;
+ }
+
+ default:
+ /*
+ * Not a class, but could still be any other
+ * high-level pattern.
+ */
+ return g_strdup("");
+ }
+
+ pattern->data++;
+ pattern->len--;
+ }
+
+ /*
+ * End of string. May happen for empty strings but also
+ * incomplete ^E or ^EG classes.
+ */
+ return g_strdup("");
+}
+
+/**
+ * Convert SciTECO pattern to regular expression.
+ * It will throw an error for definitely wrong pattern constructs
+ * but not for incomplete patterns (a necessity of search-as-you-type).
+ *
+ * @bug Incomplete patterns after a pattern has been closed (i.e. its
+ * string argument) are currently not reported as errors.
+ *
+ * @param pattern The pattern to scan through.
+ * Modifies the pointer to point after the last
+ * successfully scanned character, so it can be
+ * called recursively. It may also point to the
+ * terminating null byte after the call.
+ * @param single_expr Whether to scan a single pattern
+ * expression or an arbitrary sequence.
+ * @param error A GError.
+ * @return The regular expression string or NULL in case of GError.
+ * Must be freed with g_free().
+ */
+static gchar *
+teco_pattern2regexp(teco_string_t *pattern, gboolean single_expr, GError **error)
+{
+ teco_search_state_t state = TECO_SEARCH_STATE_START;
+ g_auto(teco_string_t) re = {NULL, 0};
+
+ do {
+ /*
+ * First check whether it is a class.
+ * This will not treat individual characters
+ * as classes, so we do not convert them to regexp
+ * classes unnecessarily.
+ */
+ g_autofree gchar *temp = teco_class2regexp(&state, pattern, FALSE, error);
+ if (!temp)
+ return NULL;
+
+ if (*temp) {
+ g_assert(state == TECO_SEARCH_STATE_START);
+
+ teco_string_append_c(&re, '[');
+ teco_string_append(&re, temp, strlen(temp));
+ teco_string_append_c(&re, ']');
+
+ /* teco_class2regexp() already consumed all the bytes */
+ continue;
+ }
+
+ if (!pattern->len)
+ /* end of pattern */
+ break;
+
+ switch (state) {
+ case TECO_SEARCH_STATE_START:
+ switch (*pattern->data) {
+ case TECO_CTL_KEY('X'): teco_string_append_c(&re, '.'); break;
+ case TECO_CTL_KEY('N'): state = TECO_SEARCH_STATE_NOT; break;
+ default: {
+ const gchar *escaped = teco_regexp_escape_chr(*pattern->data);
+ teco_string_append(&re, escaped, strlen(escaped));
+ }
+ }
+ break;
+
+ case TECO_SEARCH_STATE_NOT: {
+ state = TECO_SEARCH_STATE_START;
+ g_autofree gchar *temp = teco_class2regexp(&state, pattern, TRUE, error);
+ if (!temp)
+ return NULL;
+ if (!*temp)
+ /* a complete class is strictly required */
+ return g_strdup("");
+ g_assert(state == TECO_SEARCH_STATE_START);
+
+ teco_string_append(&re, "[^", 2);
+ teco_string_append(&re, temp, strlen(temp));
+ teco_string_append(&re, "]", 1);
+
+ /* class2regexp() already consumed all the bytes */
+ continue;
+ }
+
+ case TECO_SEARCH_STATE_CTL_E:
+ state = TECO_SEARCH_STATE_START;
+ switch (teco_ascii_toupper(*pattern->data)) {
+ case 'M': state = TECO_SEARCH_STATE_MANY; break;
+ case 'S': teco_string_append(&re, "\\s+", 3); break;
+ /* same as <CTRL/X> */
+ case 'X': teco_string_append_c(&re, '.'); break;
+ /* TODO: ASCII octal code!? */
+ case '[':
+ teco_string_append_c(&re, '(');
+ state = TECO_SEARCH_STATE_ALT;
+ break;
+ default:
+ teco_error_syntax_set(error, *pattern->data);
+ return NULL;
+ }
+ break;
+
+ case TECO_SEARCH_STATE_MANY: {
+ /* consume exactly one pattern element */
+ g_autofree gchar *temp = teco_pattern2regexp(pattern, TRUE, error);
+ if (!temp)
+ return NULL;
+ if (!*temp)
+ /* a complete expression is strictly required */
+ return g_strdup("");
+
+ teco_string_append(&re, "(", 1);
+ teco_string_append(&re, temp, strlen(temp));
+ teco_string_append(&re, ")+", 2);
+ state = TECO_SEARCH_STATE_START;
+
+ /* teco_pattern2regexp() already consumed all the bytes */
+ continue;
+ }
+
+ case TECO_SEARCH_STATE_ALT:
+ switch (*pattern->data) {
+ case ',':
+ teco_string_append_c(&re, '|');
+ break;
+ case ']':
+ teco_string_append_c(&re, ')');
+ state = TECO_SEARCH_STATE_START;
+ break;
+ default: {
+ g_autofree gchar *temp = teco_pattern2regexp(pattern, TRUE, error);
+ if (!temp)
+ return NULL;
+ if (!*temp)
+ /* a complete expression is strictly required */
+ return g_strdup("");
+
+ teco_string_append(&re, temp, strlen(temp));
+
+ /* pattern2regexp() already consumed all the bytes */
+ continue;
+ }
+ }
+ break;
+
+ default:
+ /* shouldn't happen */
+ g_assert_not_reached();
+ }
+
+ pattern->data++;
+ pattern->len--;
+ } while (!single_expr || state != TECO_SEARCH_STATE_START);
+
+ /*
+ * Complete open alternative.
+ * This could be handled like an incomplete expression,
+ * but closing it automatically improved search-as-you-type.
+ */
+ if (state == TECO_SEARCH_STATE_ALT)
+ teco_string_append_c(&re, ')');
+
+ g_assert(!teco_string_contains(&re, '\0'));
+ return g_steal_pointer(&re.data) ? : g_strdup("");
+}
+
+static gboolean
+teco_do_search(GRegex *re, gint from, gint to, gint *count, GError **error)
+{
+ g_autoptr(GMatchInfo) info = NULL;
+ const gchar *buffer = (const gchar *)teco_interface_ssm(SCI_GETCHARACTERPOINTER, 0, 0);
+ GError *tmp_error = NULL;
+
+ /*
+ * NOTE: The return boolean does NOT signal whether an error was generated.
+ */
+ g_regex_match_full(re, buffer, (gssize)to, from, 0, &info, &tmp_error);
+ if (tmp_error) {
+ g_propagate_error(error, tmp_error);
+ return FALSE;
+ }
+
+ gint matched_from = -1, matched_to = -1;
+
+ if (*count >= 0) {
+ while (g_match_info_matches(info) && --(*count)) {
+ /*
+ * NOTE: The return boolean does NOT signal whether an error was generated.
+ */
+ g_match_info_next(info, &tmp_error);
+ if (tmp_error) {
+ g_propagate_error(error, tmp_error);
+ return FALSE;
+ }
+ }
+
+ 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 */
+ typedef struct {
+ gint from, to;
+ } teco_range_t;
+
+ /*
+ * NOTE: It's theoretically possible that this single allocation
+ * causes an OOM if (-count) is large enough and memory limiting won't help.
+ * That's why we exceptionally have to check for allocation success.
+ */
+ g_autofree teco_range_t *matched = g_try_new(teco_range_t, -*count);
+ if (!matched) {
+ g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
+ "Search count too small (%d)", *count);
+ return FALSE;
+ }
+
+ 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);
+
+ /*
+ * NOTE: The return boolean does NOT signal whether an error was generated.
+ */
+ g_match_info_next(info, &tmp_error);
+ if (tmp_error) {
+ g_propagate_error(error, tmp_error);
+ return FALSE;
+ }
+
+ 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;
+ }
+ }
+
+ if (matched_from >= 0 && matched_to >= 0)
+ /* match success */
+ teco_interface_ssm(SCI_SETSEL, matched_from, matched_to);
+
+ return TRUE;
+}
+
+static gboolean
+teco_state_search_process(teco_machine_main_t *ctx, const teco_string_t *str, gsize new_chars, GError **error)
+{
+ static const GRegexCompileFlags flags = G_REGEX_CASELESS | G_REGEX_MULTILINE |
+ G_REGEX_DOTALL | G_REGEX_RAW;
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_SETSEL,
+ teco_interface_ssm(SCI_GETANCHOR, 0, 0),
+ teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0));
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+ if (!search_reg->vtable->undo_set_integer(search_reg, error) ||
+ !search_reg->vtable->set_integer(search_reg, TECO_FAILURE, error))
+ return FALSE;
+
+ g_autoptr(GRegex) re = NULL;
+ teco_string_t pattern = *str;
+ /* NOTE: teco_pattern2regexp() modifies str pointer */
+ g_autofree gchar *re_pattern = teco_pattern2regexp(&pattern, FALSE, error);
+ if (!re_pattern)
+ return FALSE;
+ teco_machine_qregspec_reset(teco_search_qreg_machine);
+#ifdef DEBUG
+ g_printf("REGEXP: %s\n", re_pattern);
+#endif
+ if (!*re_pattern)
+ goto failure;
+ /*
+ * FIXME: Should we propagate at least some of the errors?
+ */
+ re = g_regex_new(re_pattern, flags, 0, NULL);
+ if (!re)
+ goto failure;
+
+ if (!teco_qreg_current &&
+ teco_ring_current != teco_search_parameters.from_buffer) {
+ teco_ring_undo_edit();
+ teco_buffer_edit(teco_search_parameters.from_buffer);
+ }
+
+ gint count = teco_search_parameters.count;
+
+ if (!teco_do_search(re, teco_search_parameters.from, teco_search_parameters.to, &count, error))
+ return FALSE;
+
+ if (teco_search_parameters.to_buffer && count) {
+ teco_buffer_t *buffer = teco_search_parameters.from_buffer;
+
+ if (teco_ring_current == buffer)
+ teco_ring_undo_edit();
+
+ if (count > 0) {
+ do {
+ buffer = teco_buffer_next(buffer) ? : teco_ring_first();
+ teco_buffer_edit(buffer);
+
+ if (buffer == teco_search_parameters.to_buffer) {
+ if (!teco_do_search(re, 0, teco_search_parameters.dot, &count, error))
+ return FALSE;
+ break;
+ }
+
+ if (!teco_do_search(re, 0, teco_interface_ssm(SCI_GETLENGTH, 0, 0),
+ &count, error))
+ return FALSE;
+ } while (count);
+ } else /* count < 0 */ {
+ do {
+ buffer = teco_buffer_prev(buffer) ? : teco_ring_last();
+ teco_buffer_edit(buffer);
+
+ if (buffer == teco_search_parameters.to_buffer) {
+ if (!teco_do_search(re, teco_search_parameters.dot,
+ teco_interface_ssm(SCI_GETLENGTH, 0, 0),
+ &count, error))
+ return FALSE;
+ break;
+ }
+
+ if (!teco_do_search(re, 0, teco_interface_ssm(SCI_GETLENGTH, 0, 0),
+ &count, error))
+ return FALSE;
+ } while (count);
+ }
+
+ /*
+ * FIXME: Why is this necessary?
+ */
+ teco_ring_current = buffer;
+ }
+
+ if (!search_reg->vtable->set_integer(search_reg, teco_bool(!count), error))
+ return FALSE;
+
+ if (!count)
+ return TRUE;
+
+failure:
+ teco_interface_ssm(SCI_GOTOPOS, teco_search_parameters.dot, 0);
+ return TRUE;
+}
+
+static teco_state_t *
+teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ if (str->len > 0) {
+ /* workaround: preserve selection (also on rubout) */
+ gint anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0);
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_SETANCHOR, anchor, 0);
+
+ if (!search_reg->vtable->undo_set_string(search_reg, error) ||
+ !search_reg->vtable->set_string(search_reg, str->data, str->len, error))
+ return NULL;
+
+ teco_interface_ssm(SCI_SETANCHOR, anchor, 0);
+ } else {
+ g_auto(teco_string_t) search_str = {NULL, 0};
+ if (!search_reg->vtable->get_string(search_reg, &search_str.data, &search_str.len, error) ||
+ !teco_state_search_process(ctx, &search_str, search_str.len, error))
+ return NULL;
+ }
+
+ teco_int_t search_state;
+ if (!search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return FALSE;
+
+ if (teco_machine_main_eval_colon(ctx))
+ teco_expressions_push(search_state);
+ else if (teco_is_failure(search_state) &&
+ !teco_loop_stack->len /* not in loop */)
+ teco_interface_msg(TECO_MSG_ERROR, "Search string not found!");
+
+ return &teco_state_start;
+}
+
+/**
+ * @class TECO_DEFINE_STATE_SEARCH
+ * @implements TECO_DEFINE_STATE_EXPECTSTRING
+ * @ingroup states
+ *
+ * @fixme We need to provide a process_edit_cmd_cb since search patterns
+ * can also contain Q-Register references.
+ */
+#define TECO_DEFINE_STATE_SEARCH(NAME, ...) \
+ TECO_DEFINE_STATE_EXPECTSTRING(NAME, \
+ .initial_cb = (teco_state_initial_cb_t)teco_state_search_initial, \
+ .expectstring.process_cb = teco_state_search_process, \
+ .expectstring.done_cb = NAME##_done, \
+ ##__VA_ARGS__ \
+ )
+
+/*$ S search pattern
+ * S[pattern]$ -- Search for pattern
+ * [n]S[pattern]$
+ * -S[pattern]$
+ * from,toS[pattern]$
+ * :S[pattern]$ -> Success|Failure
+ * [n]:S[pattern]$ -> Success|Failure
+ * -:S[pattern]$ -> Success|Failure
+ * from,to:S[pattern]$ -> Success|Failure
+ *
+ * Search for <pattern> in the current buffer/Q-Register.
+ * Search order and range depends on the arguments given.
+ * By default without any arguments, S will search forward
+ * from dot till file end.
+ * The optional single argument specifies the occurrence
+ * to search (1 is the first occurrence, 2 the second, etc.).
+ * Negative values for <n> perform backward searches.
+ * If missing, the sign prefix is implied for <n>.
+ * Therefore \(lq-S\(rq will search for the first occurrence
+ * of <pattern> before dot.
+ *
+ * If two arguments are specified on the command,
+ * search will be bounded in the character range <from> up to
+ * <to>, and only the first occurrence will be searched.
+ * <from> might be larger than <to> in which case a backward
+ * search is performed in the selected range.
+ *
+ * After performing the search, the search <pattern> is saved
+ * in the global search Q-Register \(lq_\(rq.
+ * A success/failure condition boolean is saved in that
+ * register's integer part.
+ * <pattern> may be omitted in which case the pattern of
+ * the last search or search and replace command will be
+ * implied by using the contents of register \(lq_\(rq
+ * (this could of course also be manually set).
+ *
+ * After a successful search, the pointer is positioned after
+ * the found text in the buffer.
+ * An unsuccessful search will display an error message but
+ * not actually yield an error.
+ * The message displaying is suppressed when executed from loops
+ * and register \(lq_\(rq is the implied argument for break-commands
+ * so that a search-break idiom can be implemented as follows:
+ * .EX
+ * <Sfoo$; ...>
+ * .EE
+ * Alternatively, S may be colon-modified in which case it returns
+ * a condition boolean that may be directly evaluated by a
+ * conditional or break-command.
+ *
+ * In interactive mode, searching will be performed immediately
+ * (\(lqsearch as you type\(rq) highlighting matched text
+ * on the fly.
+ * Changing the <pattern> results in the search being reperformed
+ * from the beginning.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_search);
+
+static gboolean
+teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return TRUE;
+
+ teco_undo_object_parameters_push(&teco_search_parameters);
+ teco_search_parameters.dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ teco_int_t v1, v2;
+
+ if (!teco_expressions_pop_num_calc(&v2, teco_num_sign, error))
+ return FALSE;
+ if (teco_expressions_args()) {
+ /* TODO: optional count argument? */
+ if (!teco_expressions_pop_num_calc(&v1, 0, error))
+ return FALSE;
+ if (v1 <= v2) {
+ teco_search_parameters.count = 1;
+ teco_search_parameters.from_buffer = teco_ring_find(v1);
+ teco_search_parameters.to_buffer = teco_ring_find(v2);
+ } else {
+ teco_search_parameters.count = -1;
+ teco_search_parameters.from_buffer = teco_ring_find(v2);
+ teco_search_parameters.to_buffer = teco_ring_find(v1);
+ }
+
+ if (!teco_search_parameters.from_buffer || !teco_search_parameters.to_buffer) {
+ teco_error_range_set(error, "N");
+ return FALSE;
+ }
+ } else {
+ teco_search_parameters.count = (gint)v2;
+ /* NOTE: on Q-Registers, behave like "S" */
+ if (teco_qreg_current) {
+ teco_search_parameters.from_buffer = NULL;
+ teco_search_parameters.to_buffer = NULL;
+ } else {
+ teco_search_parameters.from_buffer = teco_ring_current;
+ teco_search_parameters.to_buffer = teco_ring_current;
+ }
+ }
+
+ if (teco_search_parameters.count >= 0) {
+ teco_search_parameters.from = teco_search_parameters.dot;
+ teco_search_parameters.to = teco_interface_ssm(SCI_GETLENGTH, 0, 0);
+ } else {
+ teco_search_parameters.from = 0;
+ teco_search_parameters.to = teco_search_parameters.dot;
+ }
+
+ return TRUE;
+}
+
+static teco_state_t *
+teco_state_search_all_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode <= TECO_MODE_NORMAL &&
+ (!teco_state_search_done(ctx, str, error) ||
+ !teco_ed_hook(TECO_ED_HOOK_EDIT, error)))
+ return NULL;
+
+ return &teco_state_start;
+}
+
+/*$ N
+ * [n]N[pattern]$ -- Search over buffer-boundaries
+ * -N[pattern]$
+ * from,toN[pattern]$
+ * [n]:N[pattern]$ -> Success|Failure
+ * -:N[pattern]$ -> Success|Failure
+ * from,to:N[pattern]$ -> Success|Failure
+ *
+ * Search for <pattern> over buffer boundaries.
+ * This command is similar to the regular search command
+ * (S) but will continue to search for occurrences of
+ * pattern when the end or beginning of the current buffer
+ * is reached.
+ * Occurrences of <pattern> spanning over buffer boundaries
+ * will not be found.
+ * When searching forward N will start in the current buffer
+ * at dot, continue with the next buffer in the ring searching
+ * the entire buffer until it reaches the end of the buffer
+ * ring, continue with the first buffer in the ring until
+ * reaching the current file again where it searched from the
+ * beginning of the buffer up to its current dot.
+ * Searching backwards does the reverse.
+ *
+ * N also differs from S in the interpretation of two arguments.
+ * Using two arguments the search will be bounded between the
+ * buffer with number <from>, up to the buffer with number
+ * <to>.
+ * When specifying buffer ranges, the entire buffers are searched
+ * from beginning to end.
+ * <from> may be greater than <to> in which case, searching starts
+ * at the end of buffer <from> and continues backwards until the
+ * beginning of buffer <to> has been reached.
+ * Furthermore as with all buffer numbers, the buffer ring
+ * is considered a circular structure and it is possible
+ * to search over buffer ring boundaries by specifying
+ * buffer numbers greater than the number of buffers in the
+ * ring.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_search_all,
+ .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial
+);
+
+static teco_state_t *
+teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ teco_int_t search_state;
+ if (!teco_state_search_done(ctx, str, error) ||
+ !search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return NULL;
+
+ if (teco_is_failure(search_state))
+ return &teco_state_start;
+
+ gint dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ if (teco_search_parameters.dot < dot) {
+ /* kill forwards */
+ gint anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0);
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_GOTOPOS, dot, 0);
+ teco_interface_ssm(SCI_GOTOPOS, anchor, 0);
+
+ teco_interface_ssm(SCI_DELETERANGE, teco_search_parameters.dot,
+ anchor - teco_search_parameters.dot);
+ } else {
+ /* kill backwards */
+ teco_interface_ssm(SCI_DELETERANGE, dot, teco_search_parameters.dot - dot);
+ }
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+
+ return &teco_state_start;
+}
+
+/*$ FK
+ * FK[pattern]$ -- Delete up to occurrence of pattern
+ * [n]FK[pattern]$
+ * -FK[pattern]$
+ * from,toFK[pattern]$
+ * :FK[pattern]$ -> Success|Failure
+ * [n]:FK[pattern]$ -> Success|Failure
+ * -:FK[pattern]$ -> Success|Failure
+ * from,to:FK[pattern]$ -> Success|Failure
+ *
+ * FK searches for <pattern> just like the regular search
+ * command (S) but when found deletes all text from dot
+ * up to but not including the found text instance.
+ * When searching backwards the characters beginning after
+ * the occurrence of <pattern> up to dot are deleted.
+ *
+ * In interactive mode, deletion is not performed
+ * as-you-type but only on command termination.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_search_kill);
+
+static teco_state_t *
+teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ teco_int_t search_state;
+ if (!teco_state_search_done(ctx, str, error) ||
+ !search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return NULL;
+
+ if (teco_is_success(search_state)) {
+ teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0);
+ teco_interface_ssm(SCI_REPLACESEL, 0, (sptr_t)"");
+ teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0);
+ teco_ring_dirtify();
+
+ if (teco_current_doc_must_undo())
+ undo__teco_interface_ssm(SCI_UNDO, 0, 0);
+ }
+
+ return &teco_state_start;
+}
+
+/*$ FD
+ * FD[pattern]$ -- Delete occurrence of pattern
+ * [n]FD[pattern]$
+ * -FD[pattern]$
+ * from,toFD[pattern]$
+ * :FD[pattern]$ -> Success|Failure
+ * [n]:FD[pattern]$ -> Success|Failure
+ * -:FD[pattern]$ -> Success|Failure
+ * from,to:FD[pattern]$ -> Success|Failure
+ *
+ * Searches for <pattern> just like the regular search command
+ * (S) but when found deletes the entire occurrence.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_search_delete);
+
+/*
+ * FIXME: Could be static
+ */
+TECO_DEFINE_STATE_INSERT(teco_state_replace_insert,
+ .initial_cb = NULL
+);
+
+static teco_state_t *
+teco_state_replace_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ return &teco_state_start;
+}
+
+/*
+ * FIXME: Could be static
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_ignore);
+
+static teco_state_t *
+teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_replace_ignore;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ teco_int_t search_state;
+ if (!teco_state_search_delete_done(ctx, str, error) ||
+ !search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return NULL;
+
+ return teco_is_success(search_state) ? &teco_state_replace_insert
+ : &teco_state_replace_ignore;
+}
+
+/*$ FS
+ * FS[pattern]$[string]$ -- Search and replace
+ * [n]FS[pattern]$[string]$
+ * -FS[pattern]$[string]$
+ * from,toFS[pattern]$[string]$
+ * :FS[pattern]$[string]$ -> Success|Failure
+ * [n]:FS[pattern]$[string]$ -> Success|Failure
+ * -:FS[pattern]$[string]$ -> Success|Failure
+ * from,to:FS[pattern]$[string]$ -> Success|Failure
+ *
+ * Search for <pattern> just like the regular search command
+ * (S) does but replace it with <string> if found.
+ * If <string> is empty, the occurrence will always be
+ * deleted so \(lqFS[pattern]$$\(rq is similar to
+ * \(lqFD[pattern]$\(rq.
+ * The global replace register is \fBnot\fP touched
+ * by the FS command.
+ *
+ * In interactive mode, the replacement will be performed
+ * immediately and interactively.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_replace,
+ .expectstring.last = FALSE
+);
+
+/*
+ * FIXME: TECO_DEFINE_STATE_INSERT() already defines a done_cb(),
+ * so we had to name this differently.
+ * Perhaps it simply shouldn't define it.
+ */
+static teco_state_t *
+teco_state_replace_default_insert_done_overwrite(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_start;
+
+ teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1);
+ g_assert(replace_reg != NULL);
+
+ if (str->len > 0) {
+ if (!replace_reg->vtable->undo_set_string(replace_reg, error) ||
+ !replace_reg->vtable->set_string(replace_reg, str->data, str->len, error))
+ return NULL;
+ } else {
+ g_auto(teco_string_t) replace_str = {NULL, 0};
+ if (!replace_reg->vtable->get_string(replace_reg, &replace_str.data, &replace_str.len, error) ||
+ !teco_state_insert_process(ctx, &replace_str, replace_str.len, error))
+ return NULL;
+ }
+
+ return &teco_state_start;
+}
+
+/*
+ * FIXME: Could be static
+ */
+TECO_DEFINE_STATE_INSERT(teco_state_replace_default_insert,
+ .initial_cb = NULL,
+ .expectstring.done_cb = teco_state_replace_default_insert_done_overwrite
+);
+
+static teco_state_t *
+teco_state_replace_default_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL ||
+ !str->len)
+ return &teco_state_start;
+
+ teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1);
+ g_assert(replace_reg != NULL);
+
+ if (!replace_reg->vtable->undo_set_string(replace_reg, error) ||
+ !replace_reg->vtable->set_string(replace_reg, str->data, str->len, error))
+ return NULL;
+
+ return &teco_state_start;
+}
+
+/*
+ * FIXME: Could be static
+ */
+TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_default_ignore);
+
+static teco_state_t *
+teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
+{
+ if (ctx->mode > TECO_MODE_NORMAL)
+ return &teco_state_replace_default_ignore;
+
+ teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1);
+ g_assert(search_reg != NULL);
+
+ teco_int_t search_state;
+ if (!teco_state_search_delete_done(ctx, str, error) ||
+ !search_reg->vtable->get_integer(search_reg, &search_state, error))
+ return NULL;
+
+ return teco_is_success(search_state) ? &teco_state_replace_default_insert
+ : &teco_state_replace_default_ignore;
+}
+
+/*$ FR
+ * FR[pattern]$[string]$ -- Search and replace with default
+ * [n]FR[pattern]$[string]$
+ * -FR[pattern]$[string]$
+ * from,toFR[pattern]$[string]$
+ * :FR[pattern]$[string]$ -> Success|Failure
+ * [n]:FR[pattern]$[string]$ -> Success|Failure
+ * -:FR[pattern]$[string]$ -> Success|Failure
+ * from,to:FR[pattern]$[string]$ -> Success|Failure
+ *
+ * The FR command is similar to the FS command.
+ * It searches for <pattern> just like the regular search
+ * command (S) and replaces the occurrence with <string>
+ * similar to what FS does.
+ * It differs from FS in the fact that the replacement
+ * string is saved in the global replacement register
+ * \(lq-\(rq.
+ * If <string> is empty the string in the global replacement
+ * register is implied instead.
+ */
+TECO_DEFINE_STATE_SEARCH(teco_state_replace_default,
+ .expectstring.last = FALSE
+);