diff options
-rw-r--r-- | src/Makefile.am | 1 | ||||
-rw-r--r-- | src/core-commands.c | 600 | ||||
-rw-r--r-- | src/move-commands.c | 635 | ||||
-rw-r--r-- | src/move-commands.h | 39 |
4 files changed, 676 insertions, 599 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 055cde7..e7a8545 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -44,6 +44,7 @@ libsciteco_base_la_SOURCES = main.c sciteco.h list.h \ ring.c ring.h \ parser.c parser.h \ core-commands.c core-commands.h \ + move-commands.c move-commands.h \ search.c search.h \ spawn.c spawn.h \ glob.c glob.h \ diff --git a/src/core-commands.c b/src/core-commands.c index 8cbb4be..f39f6f9 100644 --- a/src/core-commands.c +++ b/src/core-commands.c @@ -44,6 +44,7 @@ #include "qreg.h" #include "qreg-commands.h" #include "goto-commands.h" +#include "move-commands.h" #include "core-commands.h" static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error); @@ -516,422 +517,6 @@ teco_state_start_cmdline_pop(teco_machine_main_t *ctx, GError **error) g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CMDLINE, ""); } -/*$ J jump - * [position]J -- Go to position in buffer - * [position]:J -> Success|Failure - * - * Sets dot to <position>. - * If <position> is omitted, 0 is implied and \(lqJ\(rq will - * go to the beginning of the buffer. - * - * If <position> 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. - */ -static void -teco_state_start_jump(teco_machine_main_t *ctx, GError **error) -{ - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, 0, error)) - return; - - gssize pos = teco_interface_glyphs2bytes(v); - if (pos >= 0) { - if (teco_current_doc_must_undo()) - undo__teco_interface_ssm(SCI_GOTOPOS, - teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0); - teco_interface_ssm(SCI_GOTOPOS, pos, 0); - - if (teco_machine_main_eval_colon(ctx) > 0) - teco_expressions_push(TECO_SUCCESS); - } else if (teco_machine_main_eval_colon(ctx) > 0) { - teco_expressions_push(TECO_FAILURE); - } else { - teco_error_move_set(error, "J"); - return; - } -} - -static teco_bool_t -teco_move_chars(teco_int_t n) -{ - sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - gssize next_pos = teco_interface_glyphs2bytes_relative(pos, n); - if (next_pos < 0) - return TECO_FAILURE; - - teco_interface_ssm(SCI_GOTOPOS, next_pos, 0); - if (teco_current_doc_must_undo()) - undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); - - return TECO_SUCCESS; -} - -/*$ C move - * [n]C -- Move dot <n> characters - * -C - * [n]:C -> Success|Failure - * - * Adds <n> to dot. 1 or -1 is implied if <n> is omitted. - * Fails if <n> would move dot off-page. - * The colon modifier results in a success-boolean being - * returned instead. - */ -static void -teco_state_start_move(teco_machine_main_t *ctx, GError **error) -{ - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - - teco_bool_t rc = teco_move_chars(v); - if (teco_machine_main_eval_colon(ctx) > 0) { - teco_expressions_push(rc); - } else if (teco_is_failure(rc)) { - teco_error_move_set(error, "C"); - return; - } -} - -/*$ R reverse - * [n]R -- Move dot <n> characters backwards - * -R - * [n]:R -> Success|Failure - * - * Subtracts <n> from dot. - * It is equivalent to \(lq-nC\(rq. - */ -static void -teco_state_start_reverse(teco_machine_main_t *ctx, GError **error) -{ - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - - teco_bool_t rc = teco_move_chars(-v); - if (teco_machine_main_eval_colon(ctx) > 0) { - teco_expressions_push(rc); - } else if (teco_is_failure(rc)) { - teco_error_move_set(error, "R"); - return; - } -} - -static teco_bool_t -teco_move_lines(teco_int_t n) -{ - sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - sptr_t line = teco_interface_ssm(SCI_LINEFROMPOSITION, pos, 0) + n; - - if (!teco_validate_line(line)) - return TECO_FAILURE; - - teco_interface_ssm(SCI_GOTOLINE, line, 0); - if (teco_current_doc_must_undo()) - undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); - - return TECO_SUCCESS; -} - -/*$ L line - * [n]L -- Move dot <n> 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 <n> goes to the - * beginning of the current line, 1 will go to the - * next line, -1 to the previous line etc. - * If <n> is omitted, 1 or -1 is implied depending on - * the sign prefix. - * - * If <n> would move dot off-page, the command yields - * an error. - * The colon-modifer results in a condition boolean - * being returned instead. - */ -static void -teco_state_start_line(teco_machine_main_t *ctx, GError **error) -{ - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - - teco_bool_t rc = teco_move_lines(v); - if (teco_machine_main_eval_colon(ctx) > 0) { - teco_expressions_push(rc); - } else if (teco_is_failure(rc)) { - teco_error_move_set(error, "L"); - return; - } -} - -/*$ B backwards - * [n]B -- Move dot <n> lines backwards - * -B - * [n]:B -> Success|Failure - * - * Move dot to the beginning of the line <n> - * lines before the current one. - * It is equivalent to \(lq-nL\(rq. - */ -static void -teco_state_start_back(teco_machine_main_t *ctx, GError **error) -{ - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - - teco_bool_t rc = teco_move_lines(-v); - if (teco_machine_main_eval_colon(ctx) > 0) { - teco_expressions_push(rc); - } else if (teco_is_failure(rc)) { - teco_error_move_set(error, "B"); - return; - } -} - -/* - * NOTE: This implementation has a constant/maximum number of Scintilla - * messages, compared to using SCI_WORDENDPOSITION. - * This pays out only beginning at n > 3, though. - * But most importantly SCI_WORDENDPOSITION(p, FALSE) does not actually skip - * over all non-word characters. - */ -static gboolean -teco_find_words(gsize *pos, teco_int_t n) -{ - if (!n) - return TRUE; - - g_auto(teco_string_t) wchars; - wchars.len = teco_interface_ssm(SCI_GETWORDCHARS, 0, 0); - wchars.data = g_malloc(wchars.len + 1); - teco_interface_ssm(SCI_GETWORDCHARS, 0, (sptr_t)wchars.data); - wchars.data[wchars.len] = '\0'; - - sptr_t gap = teco_interface_ssm(SCI_GETGAPPOSITION, 0, 0); - - if (n > 0) { - /* scan forward */ - gsize len = teco_interface_ssm(SCI_GETLENGTH, 0, 0); - gsize range_len = gap > *pos ? gap - *pos : len - *pos; - if (!range_len) - return FALSE; - const gchar *buffer, *p; - p = buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, *pos, range_len); - - while (n--) { - gboolean skip_word = TRUE; - - for (;;) { - if (*pos == len) - /* end of document */ - return n == 0; - if (p-buffer >= range_len) { - g_assert(*pos == gap); - range_len = len - gap; - p = buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, gap, range_len); - } - /* - * FIXME: Is this safe or do we have to look up Unicode code points? - */ - if ((!teco_string_contains(&wchars, *p)) == skip_word) { - if (!skip_word) - break; - skip_word = !skip_word; - continue; - } - (*pos)++; - p++; - } - } - - return TRUE; - } - - /* scan backwards */ - gsize range_len = gap < *pos ? *pos - gap : *pos; - if (!range_len) - return FALSE; - const gchar *buffer, *p; - buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, *pos - range_len, range_len); - p = buffer+range_len; - - while (n++) { - gboolean skip_word = FALSE; - - for (;;) { - if (*pos == 0) - /* beginning of document */ - return n == 0; - if (p == buffer) { - g_assert(*pos == gap); - range_len = *pos; - buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, 0, range_len); - p = buffer+range_len; - } - /* - * FIXME: Is this safe or do we have to look up Unicode code points? - */ - if ((!teco_string_contains(&wchars, p[-1])) == skip_word) { - if (skip_word) - break; - skip_word = !skip_word; - continue; - } - (*pos)--; - p--; - } - } - - return TRUE; -} - -/*$ W word - * [n]W -- Move dot by words - * -W - * [n]:W -> Success|Failure - * - * Move dot <n> words forward. - * - If <n> is positive, dot is positioned at the beginning - * of the word <n> words after the current one. - * - If <n> is negative, dot is positioned at the beginning - * of the word, <-n> words before the current one. - * - If <n> is zero, dot is not moved. - * - * \(lqW\(rq uses Scintilla's definition of a word as - * configurable using the - * .B SCI_SETWORDCHARS - * message. - * - * If the requested word would lie beyond the range of the - * buffer, the command yields an error. - * If colon-modified it instead returns a condition code. - */ -static void -teco_state_start_word(teco_machine_main_t *ctx, GError **error) -{ - teco_int_t v; - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - - gsize word_pos = pos; - if (!teco_find_words(&word_pos, v)) { - if (!teco_machine_main_eval_colon(ctx)) - teco_error_move_set(error, "W"); - else - teco_expressions_push(TECO_FAILURE); - return; - } - - if (teco_current_doc_must_undo()) - undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); - teco_interface_ssm(SCI_GOTOPOS, word_pos, 0); - if (teco_machine_main_eval_colon(ctx) > 0) - teco_expressions_push(TECO_SUCCESS); -} - -static gboolean -teco_delete_words(teco_int_t n) -{ - if (!n) - return TRUE; - - sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - - gsize start_pos = pos, end_pos = pos; - if (!teco_find_words(n > 0 ? &end_pos : &start_pos, n)) - return FALSE; - g_assert(start_pos <= end_pos); - - teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); - teco_interface_ssm(SCI_DELETERANGE, start_pos, end_pos-start_pos); - teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); - - if (teco_current_doc_must_undo()) { - undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); - undo__teco_interface_ssm(SCI_UNDO, 0, 0); - } - teco_ring_dirtify(); - - return TRUE; -} - -/*$ V - * [n]V -- Delete words forward - * -V - * [n]:V -> Success|Failure - * - * Deletes the next <n> words until the beginning of the - * n'th word after the current one. - * If <n> is negative, deletes up to the beginning of the - * word, <-n> words before the current one. - * \(lq-V\(rq in the middle of a word deletes until the beginning - * of the word. - * If <n> 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. - */ -static void -teco_state_start_delete_words(teco_machine_main_t *ctx, GError **error) -{ - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - - gboolean rc = teco_delete_words(v); - if (teco_machine_main_eval_colon(ctx) > 0) { - teco_expressions_push(teco_bool(rc)); - } else if (!rc) { - teco_error_words_set(error, "V"); - return; - } -} - -/*$ Y - * [n]Y -- Delete word backwards - * -Y - * [n]:Y -> Success|Failure - * - * Delete <n> words backward. - * <n>Y is equivalent to \(lq-nV\(rq. - */ -static void -teco_state_start_delete_words_back(teco_machine_main_t *ctx, GError **error) -{ - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - - gboolean rc = teco_delete_words(-v); - if (teco_machine_main_eval_colon(ctx) > 0) { - teco_expressions_push(teco_bool(rc)); - } else if (!rc) { - teco_error_words_set(error, "Y"); - return; - } -} - /*$ "=" print * <n>= -- Show value as message * @@ -960,128 +545,6 @@ teco_state_start_print(teco_machine_main_t *ctx, GError **error) teco_interface_msg(TECO_MSG_USER, "%" TECO_INT_FORMAT, v); } -static gboolean -teco_state_start_kill(teco_machine_main_t *ctx, const gchar *cmd, gboolean by_lines, GError **error) -{ - teco_bool_t rc; - gssize from, len; /* in bytes */ - - if (!teco_expressions_eval(FALSE, error)) - return FALSE; - - if (teco_expressions_args() <= 1) { - from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - if (by_lines) { - teco_int_t line; - if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error)) - return FALSE; - line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0); - len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from; - rc = teco_bool(teco_validate_line(line)); - } else { - teco_int_t len_glyphs; - if (!teco_expressions_pop_num_calc(&len_glyphs, teco_num_sign, error)) - return FALSE; - gssize to = teco_interface_glyphs2bytes_relative(from, len_glyphs); - rc = teco_bool(to >= 0); - len = to-from; - } - if (len < 0) { - len *= -1; - from -= len; - } - } else { - teco_int_t to_glyphs = teco_expressions_pop_num(0); - gssize to = teco_interface_glyphs2bytes(to_glyphs); - teco_int_t from_glyphs = teco_expressions_pop_num(0); - from = teco_interface_glyphs2bytes(from_glyphs); - len = to - from; - rc = teco_bool(len >= 0 && from >= 0 && to >= 0); - } - - if (teco_machine_main_eval_colon(ctx) > 0) { - teco_expressions_push(rc); - } else if (teco_is_failure(rc)) { - teco_error_range_set(error, cmd); - return FALSE; - } - - if (len == 0 || teco_is_failure(rc)) - return TRUE; - - if (teco_current_doc_must_undo()) { - sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); - undo__teco_interface_ssm(SCI_UNDO, 0, 0); - } - - /* - * Should always generate an undo action. - */ - teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); - teco_interface_ssm(SCI_DELETERANGE, from, len); - teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); - teco_ring_dirtify(); - - return TRUE; -} - -/*$ K kill - * [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 <n> lines after or before the current one. - * If <n> is 0, \(lqK\(rq will delete up to the beginning - * of the current line. - * If <n> 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 <from> and <to> are available, the - * command is synonymous to <from>,<to>D. - */ -static void -teco_state_start_kill_lines(teco_machine_main_t *ctx, GError **error) -{ - teco_state_start_kill(ctx, "K", TRUE, error); -} - -/*$ D delete - * [n]D -- Delete characters - * -D - * from,to D - * [n]:D -> Success|Failure - * from,to:D -> Success|Failure - * - * If <n> is positive, the next <n> characters (up to and - * character .+<n>) are deleted. - * If <n> is negative, the previous <n> characters are - * deleted. - * If <n> 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 <from> up to <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. - */ -static void -teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error) -{ - teco_state_start_kill(ctx, "D", FALSE, error); -} - /*$ A * [n]A -> code -- Get character code from buffer * -A -> code @@ -1854,67 +1317,6 @@ teco_state_control_radix(teco_machine_main_t *ctx, GError **error) } } -/*$ ^Q lines2glyphs glyphs2lines - * [n]^Q -> glyphs -- Convert between lines and glyph lengths or positions - * [position]:^Q -> line - * - * Converts between line and glyph arguments. - * It returns the number of glyphs between dot and the <n>-th next - * line (or previous line if <n> is negative). - * Consequently \(lq^QC\(rq is equivalent to \(lqL\(rq, but less efficient. - * - * If colon-modified, an absolute buffer position is converted to the line that - * contains this position, beginning with 1. - * Without arguments, \(lq:^Q\(rq returns the current line. - */ -/* - * FIXME: Perhaps there should be a way to convert an absolute line to an - * absolute position. - */ -static void -teco_state_control_lines2glyphs(teco_machine_main_t *ctx, GError **error) -{ - if (!teco_expressions_eval(FALSE, error)) - return; - - if (teco_machine_main_eval_colon(ctx)) { - gssize pos; - - if (!teco_expressions_args()) { - pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - } else { - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, 0, error)) - return; - - pos = teco_interface_glyphs2bytes(v); - if (pos < 0) { - teco_error_range_set(error, "^Q"); - return; - } - } - - teco_expressions_push(teco_interface_ssm(SCI_LINEFROMPOSITION, pos, 0)+1); - } else { - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - - sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - sptr_t line = teco_interface_ssm(SCI_LINEFROMPOSITION, pos, 0) + v; - - if (!teco_validate_line(line)) { - teco_error_range_set(error, "^Q"); - return; - } - - sptr_t line_pos = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0); - teco_expressions_push(teco_interface_bytes2glyphs(line_pos) - teco_interface_bytes2glyphs(pos)); - } -} - /*$ ^E glyphs2bytes bytes2glyphs * glyphs^E -> bytes -- Translate between glyph and byte indexes * bytes:^E -> glyphs diff --git a/src/move-commands.c b/src/move-commands.c new file mode 100644 index 0000000..8abd8ca --- /dev/null +++ b/src/move-commands.c @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2012-2025 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/>. + */ +/** + * @file Movement \b and corresponding deletion commands. + * This also includes the lines to glyphs conversion command. + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <glib.h> +#include <glib/gstdio.h> + +#include "sciteco.h" +#include "string-utils.h" +#include "expressions.h" +#include "interface.h" +#include "parser.h" +#include "ring.h" +#include "undo.h" +#include "error.h" +#include "move-commands.h" + +/*$ J jump + * [position]J -- Go to position in buffer + * [position]:J -> Success|Failure + * + * Sets dot to <position>. + * If <position> is omitted, 0 is implied and \(lqJ\(rq will + * go to the beginning of the buffer. + * + * If <position> 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. + */ +void +teco_state_start_jump(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, 0, error)) + return; + + gssize pos = teco_interface_glyphs2bytes(v); + if (pos >= 0) { + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, + teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0); + teco_interface_ssm(SCI_GOTOPOS, pos, 0); + + if (teco_machine_main_eval_colon(ctx) > 0) + teco_expressions_push(TECO_SUCCESS); + } else if (teco_machine_main_eval_colon(ctx) > 0) { + teco_expressions_push(TECO_FAILURE); + } else { + teco_error_move_set(error, "J"); + return; + } +} + +static teco_bool_t +teco_move_chars(teco_int_t n) +{ + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + gssize next_pos = teco_interface_glyphs2bytes_relative(pos, n); + if (next_pos < 0) + return TECO_FAILURE; + + teco_interface_ssm(SCI_GOTOPOS, next_pos, 0); + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + + return TECO_SUCCESS; +} + +/*$ C move + * [n]C -- Move dot <n> characters + * -C + * [n]:C -> Success|Failure + * + * Adds <n> to dot. 1 or -1 is implied if <n> is omitted. + * Fails if <n> would move dot off-page. + * The colon modifier results in a success-boolean being + * returned instead. + */ +void +teco_state_start_move(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_move_chars(v); + if (teco_machine_main_eval_colon(ctx) > 0) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_move_set(error, "C"); + return; + } +} + +/*$ R reverse + * [n]R -- Move dot <n> characters backwards + * -R + * [n]:R -> Success|Failure + * + * Subtracts <n> from dot. + * It is equivalent to \(lq-nC\(rq. + */ +void +teco_state_start_reverse(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_move_chars(-v); + if (teco_machine_main_eval_colon(ctx) > 0) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_move_set(error, "R"); + return; + } +} + +static teco_bool_t +teco_move_lines(teco_int_t n) +{ + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + sptr_t line = teco_interface_ssm(SCI_LINEFROMPOSITION, pos, 0) + n; + + if (!teco_validate_line(line)) + return TECO_FAILURE; + + teco_interface_ssm(SCI_GOTOLINE, line, 0); + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + + return TECO_SUCCESS; +} + +/*$ L line + * [n]L -- Move dot <n> 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 <n> goes to the + * beginning of the current line, 1 will go to the + * next line, -1 to the previous line etc. + * If <n> is omitted, 1 or -1 is implied depending on + * the sign prefix. + * + * If <n> would move dot off-page, the command yields + * an error. + * The colon-modifer results in a condition boolean + * being returned instead. + */ +void +teco_state_start_line(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_move_lines(v); + if (teco_machine_main_eval_colon(ctx) > 0) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_move_set(error, "L"); + return; + } +} + +/*$ B backwards + * [n]B -- Move dot <n> lines backwards + * -B + * [n]:B -> Success|Failure + * + * Move dot to the beginning of the line <n> + * lines before the current one. + * It is equivalent to \(lq-nL\(rq. + */ +void +teco_state_start_back(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + teco_bool_t rc = teco_move_lines(-v); + if (teco_machine_main_eval_colon(ctx) > 0) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_move_set(error, "B"); + return; + } +} + +/* + * NOTE: This implementation has a constant/maximum number of Scintilla + * messages, compared to using SCI_WORDENDPOSITION. + * This pays out only beginning at n > 3, though. + * But most importantly SCI_WORDENDPOSITION(p, FALSE) does not actually skip + * over all non-word characters. + */ +static gboolean +teco_find_words(gsize *pos, teco_int_t n) +{ + if (!n) + return TRUE; + + g_auto(teco_string_t) wchars; + wchars.len = teco_interface_ssm(SCI_GETWORDCHARS, 0, 0); + wchars.data = g_malloc(wchars.len + 1); + teco_interface_ssm(SCI_GETWORDCHARS, 0, (sptr_t)wchars.data); + wchars.data[wchars.len] = '\0'; + + sptr_t gap = teco_interface_ssm(SCI_GETGAPPOSITION, 0, 0); + + if (n > 0) { + /* scan forward */ + gsize len = teco_interface_ssm(SCI_GETLENGTH, 0, 0); + gsize range_len = gap > *pos ? gap - *pos : len - *pos; + if (!range_len) + return FALSE; + const gchar *buffer, *p; + p = buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, *pos, range_len); + + while (n--) { + gboolean skip_word = TRUE; + + for (;;) { + if (*pos == len) + /* end of document */ + return n == 0; + if (p-buffer >= range_len) { + g_assert(*pos == gap); + range_len = len - gap; + p = buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, gap, range_len); + } + /* + * FIXME: Is this safe or do we have to look up Unicode code points? + */ + if ((!teco_string_contains(&wchars, *p)) == skip_word) { + if (!skip_word) + break; + skip_word = !skip_word; + continue; + } + (*pos)++; + p++; + } + } + + return TRUE; + } + + /* scan backwards */ + gsize range_len = gap < *pos ? *pos - gap : *pos; + if (!range_len) + return FALSE; + const gchar *buffer, *p; + buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, *pos - range_len, range_len); + p = buffer+range_len; + + while (n++) { + gboolean skip_word = FALSE; + + for (;;) { + if (*pos == 0) + /* beginning of document */ + return n == 0; + if (p == buffer) { + g_assert(*pos == gap); + range_len = *pos; + buffer = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, 0, range_len); + p = buffer+range_len; + } + /* + * FIXME: Is this safe or do we have to look up Unicode code points? + */ + if ((!teco_string_contains(&wchars, p[-1])) == skip_word) { + if (skip_word) + break; + skip_word = !skip_word; + continue; + } + (*pos)--; + p--; + } + } + + return TRUE; +} + +/*$ W word + * [n]W -- Move dot by words + * -W + * [n]:W -> Success|Failure + * + * Move dot <n> words forward. + * - If <n> is positive, dot is positioned at the beginning + * of the word <n> words after the current one. + * - If <n> is negative, dot is positioned at the beginning + * of the word, <-n> words before the current one. + * - If <n> is zero, dot is not moved. + * + * \(lqW\(rq uses Scintilla's definition of a word as + * configurable using the + * .B SCI_SETWORDCHARS + * message. + * + * If the requested word would lie beyond the range of the + * buffer, the command yields an error. + * If colon-modified it instead returns a condition code. + */ +void +teco_state_start_word(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + + gsize word_pos = pos; + if (!teco_find_words(&word_pos, v)) { + if (!teco_machine_main_eval_colon(ctx)) + teco_error_move_set(error, "W"); + else + teco_expressions_push(TECO_FAILURE); + return; + } + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + teco_interface_ssm(SCI_GOTOPOS, word_pos, 0); + if (teco_machine_main_eval_colon(ctx) > 0) + teco_expressions_push(TECO_SUCCESS); +} + +static gboolean +teco_delete_words(teco_int_t n) +{ + if (!n) + return TRUE; + + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + + gsize start_pos = pos, end_pos = pos; + if (!teco_find_words(n > 0 ? &end_pos : &start_pos, n)) + return FALSE; + g_assert(start_pos <= end_pos); + + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_DELETERANGE, start_pos, end_pos-start_pos); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + + if (teco_current_doc_must_undo()) { + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + } + teco_ring_dirtify(); + + return TRUE; +} + +/*$ V + * [n]V -- Delete words forward + * -V + * [n]:V -> Success|Failure + * + * Deletes the next <n> words until the beginning of the + * n'th word after the current one. + * If <n> is negative, deletes up to the beginning of the + * word, <-n> words before the current one. + * \(lq-V\(rq in the middle of a word deletes until the beginning + * of the word. + * If <n> 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. + */ +void +teco_state_start_delete_words(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + gboolean rc = teco_delete_words(v); + if (teco_machine_main_eval_colon(ctx) > 0) { + teco_expressions_push(teco_bool(rc)); + } else if (!rc) { + teco_error_words_set(error, "V"); + return; + } +} + +/*$ Y + * [n]Y -- Delete word backwards + * -Y + * [n]:Y -> Success|Failure + * + * Delete <n> words backward. + * <n>Y is equivalent to \(lq-nV\(rq. + */ +void +teco_state_start_delete_words_back(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + gboolean rc = teco_delete_words(-v); + if (teco_machine_main_eval_colon(ctx) > 0) { + teco_expressions_push(teco_bool(rc)); + } else if (!rc) { + teco_error_words_set(error, "Y"); + return; + } +} + +static gboolean +teco_state_start_kill(teco_machine_main_t *ctx, const gchar *cmd, gboolean by_lines, GError **error) +{ + teco_bool_t rc; + gssize from, len; /* in bytes */ + + if (!teco_expressions_eval(FALSE, error)) + return FALSE; + + if (teco_expressions_args() <= 1) { + from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + if (by_lines) { + teco_int_t line; + if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error)) + return FALSE; + line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0); + len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from; + rc = teco_bool(teco_validate_line(line)); + } else { + teco_int_t len_glyphs; + if (!teco_expressions_pop_num_calc(&len_glyphs, teco_num_sign, error)) + return FALSE; + gssize to = teco_interface_glyphs2bytes_relative(from, len_glyphs); + rc = teco_bool(to >= 0); + len = to-from; + } + if (len < 0) { + len *= -1; + from -= len; + } + } else { + teco_int_t to_glyphs = teco_expressions_pop_num(0); + gssize to = teco_interface_glyphs2bytes(to_glyphs); + teco_int_t from_glyphs = teco_expressions_pop_num(0); + from = teco_interface_glyphs2bytes(from_glyphs); + len = to - from; + rc = teco_bool(len >= 0 && from >= 0 && to >= 0); + } + + if (teco_machine_main_eval_colon(ctx) > 0) { + teco_expressions_push(rc); + } else if (teco_is_failure(rc)) { + teco_error_range_set(error, cmd); + return FALSE; + } + + if (len == 0 || teco_is_failure(rc)) + return TRUE; + + if (teco_current_doc_must_undo()) { + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + undo__teco_interface_ssm(SCI_GOTOPOS, pos, 0); + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + } + + /* + * Should always generate an undo action. + */ + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); + teco_interface_ssm(SCI_DELETERANGE, from, len); + teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + teco_ring_dirtify(); + + return TRUE; +} + +/*$ K kill + * [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 <n> lines after or before the current one. + * If <n> is 0, \(lqK\(rq will delete up to the beginning + * of the current line. + * If <n> 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 <from> and <to> are available, the + * command is synonymous to <from>,<to>D. + */ +void +teco_state_start_kill_lines(teco_machine_main_t *ctx, GError **error) +{ + teco_state_start_kill(ctx, "K", TRUE, error); +} + +/*$ D delete + * [n]D -- Delete characters + * -D + * from,to D + * [n]:D -> Success|Failure + * from,to:D -> Success|Failure + * + * If <n> is positive, the next <n> characters (up to and + * character .+<n>) are deleted. + * If <n> is negative, the previous <n> characters are + * deleted. + * If <n> 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 <from> up to <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. + */ +void +teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error) +{ + teco_state_start_kill(ctx, "D", FALSE, error); +} + +/*$ ^Q lines2glyphs glyphs2lines + * [n]^Q -> glyphs -- Convert between lines and glyph lengths or positions + * [position]:^Q -> line + * + * Converts between line and glyph arguments. + * It returns the number of glyphs between dot and the <n>-th next + * line (or previous line if <n> is negative). + * Consequently \(lq^QC\(rq is equivalent to \(lqL\(rq, but less efficient. + * + * If colon-modified, an absolute buffer position is converted to the line that + * contains this position, beginning with 1. + * Without arguments, \(lq:^Q\(rq returns the current line. + */ +/* + * FIXME: Perhaps there should be a way to convert an absolute line to an + * absolute position. + */ +void +teco_state_control_lines2glyphs(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + + if (teco_machine_main_eval_colon(ctx)) { + gssize pos; + + if (!teco_expressions_args()) { + pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + } else { + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, 0, error)) + return; + + pos = teco_interface_glyphs2bytes(v); + if (pos < 0) { + teco_error_range_set(error, "^Q"); + return; + } + } + + teco_expressions_push(teco_interface_ssm(SCI_LINEFROMPOSITION, pos, 0)+1); + } else { + teco_int_t v; + + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + sptr_t line = teco_interface_ssm(SCI_LINEFROMPOSITION, pos, 0) + v; + + if (!teco_validate_line(line)) { + teco_error_range_set(error, "^Q"); + return; + } + + sptr_t line_pos = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0); + teco_expressions_push(teco_interface_bytes2glyphs(line_pos) - teco_interface_bytes2glyphs(pos)); + } +} diff --git a/src/move-commands.h b/src/move-commands.h new file mode 100644 index 0000000..36acec8 --- /dev/null +++ b/src/move-commands.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012-2025 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/>. + */ +#pragma once + +#include <glib.h> + +#include "parser.h" + +void teco_state_start_jump(teco_machine_main_t *ctx, GError **error); + +void teco_state_start_move(teco_machine_main_t *ctx, GError **error); +void teco_state_start_reverse(teco_machine_main_t *ctx, GError **error); + +void teco_state_start_line(teco_machine_main_t *ctx, GError **error); +void teco_state_start_back(teco_machine_main_t *ctx, GError **error); + +void teco_state_start_word(teco_machine_main_t *ctx, GError **error); + +void teco_state_start_delete_words(teco_machine_main_t *ctx, GError **error); +void teco_state_start_delete_words_back(teco_machine_main_t *ctx, GError **error); + +void teco_state_start_kill_lines(teco_machine_main_t *ctx, GError **error); +void teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error); + +void teco_state_control_lines2glyphs(teco_machine_main_t *ctx, GError **error); |