<commit_before>/*
 * Copyright (c) 2014 <NAME> <mat at brain-dump.org>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include <locale.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <ctype.h>
#include <time.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

#include "ui-curses.h"
#include "editor.h"
#include "text-util.h"
#include "text-motions.h"
#include "text-objects.h"
#include "util.h"
#include "map.h"
#include "libutf.h"

/** global variables */
static Editor *vis;         /* global editor instance, keeps track of all windows etc. */

/** operators */
static size_t op_change(OperatorContext *c);
static size_t op_yank(OperatorContext *c);
static size_t op_put(OperatorContext *c);
static size_t op_delete(OperatorContext *c);
static size_t op_shift_right(OperatorContext *c);
static size_t op_shift_left(OperatorContext *c);
static size_t op_case_change(OperatorContext *c);
static size_t op_join(OperatorContext *c);
static size_t op_repeat_insert(OperatorContext *c);
static size_t op_repeat_replace(OperatorContext *c);
static size_t op_cursor(OperatorContext *c);

/* these can be passed as int argument to operator(&(const Arg){ .i = OP_*}) */
enum {
	OP_DELETE,
	OP_CHANGE,
	OP_YANK,
	OP_PUT,
	OP_SHIFT_RIGHT,
	OP_SHIFT_LEFT,
	OP_CASE_CHANGE,
	OP_JOIN,
	OP_REPEAT_INSERT,
	OP_REPEAT_REPLACE,
	OP_CURSOR,
};

static Operator ops[] = {
	[OP_DELETE]      = { op_delete      },
	[OP_CHANGE]      = { op_change      },
	[OP_YANK]        = { op_yank        },
	[OP_PUT]         = { op_put         },
	[OP_SHIFT_RIGHT] = { op_shift_right },
	[OP_SHIFT_LEFT]  = { op_shift_left  },
	[OP_CASE_CHANGE] = { op_case_change },
	[OP_JOIN]          = { op_join          },
	[OP_REPEAT_INSERT]  = { op_repeat_insert  },
	[OP_REPEAT_REPLACE] = { op_repeat_replace },
	[OP_CURSOR]         = { op_cursor         },
};

#define PAGE      INT_MAX
#define PAGE_HALF (INT_MAX-1)

/* these can be passed as int argument to movement(&(const Arg){ .i = MOVE_* }) */
enum {
	MOVE_LINE_DOWN,
	MOVE_LINE_UP,
	MOVE_SCREEN_LINE_UP,
	MOVE_SCREEN_LINE_DOWN,
	MOVE_SCREEN_LINE_BEGIN,
	MOVE_SCREEN_LINE_MIDDLE,
	MOVE_SCREEN_LINE_END,
	MOVE_LINE_PREV,
	MOVE_LINE_BEGIN,
	MOVE_LINE_START,
	MOVE_LINE_FINISH,
	MOVE_LINE_LASTCHAR,
	MOVE_LINE_END,
	MOVE_LINE_NEXT,
	MOVE_LINE,
	MOVE_COLUMN,
	MOVE_CHAR_PREV,
	MOVE_CHAR_NEXT,
	MOVE_LINE_CHAR_PREV,
	MOVE_LINE_CHAR_NEXT,
	MOVE_WORD_START_NEXT,
	MOVE_WORD_END_PREV,
	MOVE_WORD_END_NEXT,
	MOVE_WORD_START_PREV,
	MOVE_LONGWORD_START_PREV,
	MOVE_LONGWORD_START_NEXT,
	MOVE_LONGWORD_END_PREV,
	MOVE_LONGWORD_END_NEXT,
	MOVE_SENTENCE_PREV,
	MOVE_SENTENCE_NEXT,
	MOVE_PARAGRAPH_PREV,
	MOVE_PARAGRAPH_NEXT,
	MOVE_BRACKET_MATCH,
	MOVE_LEFT_TO,
	MOVE_RIGHT_TO,
	MOVE_LEFT_TILL,
	MOVE_RIGHT_TILL,
	MOVE_FILE_BEGIN,
	MOVE_FILE_END,
	MOVE_MARK,
	MOVE_MARK_LINE,
	MOVE_SEARCH_WORD_FORWARD,
	MOVE_SEARCH_WORD_BACKWARD,
	MOVE_SEARCH_FORWARD,
	MOVE_SEARCH_BACKWARD,
	MOVE_WINDOW_LINE_TOP,
	MOVE_WINDOW_LINE_MIDDLE,
	MOVE_WINDOW_LINE_BOTTOM,
};

/** movements which can be used besides the one in text-motions.h and view.h */

/* search in forward direction for the word under the cursor */
static size_t search_word_forward(Text *txt, size_t pos);
/* search in backward direction for the word under the cursor */
static size_t search_word_backward(Text *txt, size_t pos);
/* search again for the last used search pattern */
static size_t search_forward(Text *txt, size_t pos);
static size_t search_backward(Text *txt, size_t pos);
/* goto action.mark */
static size_t mark_goto(File *txt, size_t pos);
/* goto first non-blank char on line pointed by action.mark */
static size_t mark_line_goto(File *txt, size_t pos);
/* goto to next occurence of action.key to the right */
static size_t to(Text *txt, size_t pos);
/* goto to position before next occurence of action.key to the right */
static size_t till(Text *txt, size_t pos);
/* goto to next occurence of action.key to the left */
static size_t to_left(Text *txt, size_t pos);
/* goto to position after next occurence of action.key to the left */
static size_t till_left(Text *txt, size_t pos);
/* goto line number action.count */
static size_t line(Text *txt, size_t pos);
/* goto to byte action.count on current line */
static size_t column(Text *txt, size_t pos);
/* goto the action.count-th line from top of the focused window */
static size_t view_lines_top(const Arg *arg);
/* goto the start of middle line of the focused window */
static size_t view_lines_middle(const Arg *arg);
/* goto the action.count-th line from bottom of the focused window */
static size_t view_lines_bottom(const Arg *arg);

static Movement moves[] = {
	[MOVE_LINE_UP]             = { .view = view_line_up                                       },
	[MOVE_LINE_DOWN]           = { .view = view_line_down                                     },
	[MOVE_SCREEN_LINE_UP]      = { .view = view_screenline_up                                 },
	[MOVE_SCREEN_LINE_DOWN]    = { .view = view_screenline_down                               },
	[MOVE_SCREEN_LINE_BEGIN]   = { .view = view_screenline_begin,  .type = CHARWISE           },
	[MOVE_SCREEN_LINE_MIDDLE]  = { .view = view_screenline_middle, .type = CHARWISE           },
	[MOVE_SCREEN_LINE_END]     = { .view = view_screenline_end,    .type = CHARWISE|INCLUSIVE },
	[MOVE_LINE_PREV]           = { .txt = text_line_prev,           .type = LINEWISE           },
	[MOVE_LINE_BEGIN]          = { .txt = text_line_begin,          .type = LINEWISE           },
	[MOVE_LINE_START]          = { .txt = text_line_start,          .type = LINEWISE           },
	[MOVE_LINE_FINISH]         = { .txt = text_line_finish,         .type = LINEWISE|INCLUSIVE },
	[MOVE_LINE_LASTCHAR]       = { .txt = text_line_lastchar,       .type = LINEWISE|INCLUSIVE },
	[MOVE_LINE_END]            = { .txt = text_line_end,            .type = LINEWISE           },
	[MOVE_LINE_NEXT]           = { .txt = text_line_next,           .type = LINEWISE           },
	[MOVE_LINE]                = { .txt = line,                     .type = LINEWISE|IDEMPOTENT|JUMP},
	[MOVE_COLUMN]              = { .txt = column,                   .type = CHARWISE|IDEMPOTENT},
	[MOVE_CHAR_PREV]           = { .txt = text_char_prev,           .type = CHARWISE           },
	[MOVE_CHAR_NEXT]           = { .txt = text_char_next,           .type = CHARWISE           },
	[MOVE_LINE_CHAR_PREV]      = { .txt = text_line_char_prev,      .type = CHARWISE           },
	[MOVE_LINE_CHAR_NEXT]      = { .txt = text_line_char_next,      .type = CHARWISE           },
	[MOVE_WORD_START_PREV]     = { .txt = text_word_start_prev,     .type = CHARWISE           },
	[MOVE_WORD_START_NEXT]     = { .txt = text_word_start_next,     .type = CHARWISE           },
	[MOVE_WORD_END_PREV]       = { .txt = text_word_end_prev,       .type = CHARWISE|INCLUSIVE },
	[MOVE_WORD_END_NEXT]       = { .txt = text_word_end_next,       .type = CHARWISE|INCLUSIVE },
	[MOVE_LONGWORD_START_PREV] = { .txt = text_longword_start_prev, .type = CHARWISE           },
	[MOVE_LONGWORD_START_NEXT] = { .txt = text_longword_start_next, .type = CHARWISE           },
	[MOVE_LONGWORD_END_PREV]   = { .txt = text_longword_end_prev,   .type = CHARWISE|INCLUSIVE },
	[MOVE_LONGWORD_END_NEXT]   = { .txt = text_longword_end_next,   .type = CHARWISE|INCLUSIVE },
	[MOVE_SENTENCE_PREV]       = { .txt = text_sentence_prev,       .type = LINEWISE           },
	[MOVE_SENTENCE_NEXT]       = { .txt = text_sentence_next,       .type = LINEWISE           },
	[MOVE_PARAGRAPH_PREV]      = { .txt = text_paragraph_prev,      .type = LINEWISE|JUMP      },
	[MOVE_PARAGRAPH_NEXT]      = { .txt = text_paragraph_next,      .type = LINEWISE|JUMP      },
	[MOVE_BRACKET_MATCH]       = { .txt = text_bracket_match,       .type = LINEWISE|INCLUSIVE|JUMP },
	[MOVE_FILE_BEGIN]          = { .txt = text_begin,               .type = LINEWISE|JUMP      },
	[MOVE_FILE_END]            = { .txt = text_end,                 .type = LINEWISE|JUMP      },
	[MOVE_LEFT_TO]             = { .txt = to_left,                  .type = LINEWISE           },
	[MOVE_RIGHT_TO]            = { .txt = to,                       .type = LINEWISE|INCLUSIVE },
	[MOVE_LEFT_TILL]           = { .txt = till_left,                .type = LINEWISE           },
	[MOVE_RIGHT_TILL]          = { .txt = till,                     .type = LINEWISE|INCLUSIVE },
	[MOVE_MARK]                = { .file = mark_goto,             .type = LINEWISE|JUMP|IDEMPOTENT },
	[MOVE_MARK_LINE]           = { .file = mark_line_goto,        .type = LINEWISE|JUMP|IDEMPOTENT },
	[MOVE_SEARCH_WORD_FORWARD] = { .txt = search_word_forward,      .type = LINEWISE|JUMP      },
	[MOVE_SEARCH_WORD_BACKWARD]= { .txt = search_word_backward,     .type = LINEWISE|JUMP      },
	[MOVE_SEARCH_FORWARD]      = { .txt = search_forward,           .type = LINEWISE|JUMP      },
	[MOVE_SEARCH_BACKWARD]     = { .txt = search_backward,          .type = LINEWISE|JUMP      },
	[MOVE_WINDOW_LINE_TOP]     = { .cmd = view_lines_top,         .type = LINEWISE|JUMP|IDEMPOTENT },
	[MOVE_WINDOW_LINE_MIDDLE]  = { .cmd = view_lines_middle,      .type = LINEWISE|JUMP|IDEMPOTENT },
	[MOVE_WINDOW_LINE_BOTTOM]  = { .cmd = view_lines_bottom,      .type = LINEWISE|JUMP|IDEMPOTENT },
};

/* these can be passed as int argument to textobj(&(const Arg){ .i = TEXT_OBJ_* }) */
enum {
	TEXT_OBJ_INNER_WORD,
	TEXT_OBJ_OUTER_WORD,
	TEXT_OBJ_INNER_LONGWORD,
	TEXT_OBJ_OUTER_LONGWORD,
	TEXT_OBJ_LINE_UP,
	TEXT_OBJ_LINE_DOWN,
	TEXT_OBJ_SENTENCE,
	TEXT_OBJ_PARAGRAPH,
	TEXT_OBJ_OUTER_SQUARE_BRACKET,
	TEXT_OBJ_INNER_SQUARE_BRACKET,
	TEXT_OBJ_OUTER_CURLY_BRACKET,
	TEXT_OBJ_INNER_CURLY_BRACKET,
	TEXT_OBJ_OUTER_ANGLE_BRACKET,
	TEXT_OBJ_INNER_ANGLE_BRACKET,
	TEXT_OBJ_OUTER_PARANTHESE,
	TEXT_OBJ_INNER_PARANTHESE,
	TEXT_OBJ_OUTER_QUOTE,
	TEXT_OBJ_INNER_QUOTE,
	TEXT_OBJ_OUTER_SINGLE_QUOTE,
	TEXT_OBJ_INNER_SINGLE_QUOTE,
	TEXT_OBJ_OUTER_BACKTICK,
	TEXT_OBJ_INNER_BACKTICK,
};

static TextObject textobjs[] = {
	[TEXT_OBJ_INNER_WORD]           = { text_object_word                  },
	[TEXT_OBJ_OUTER_WORD]           = { text_object_word_outer            },
	[TEXT_OBJ_INNER_LONGWORD]       = { text_object_longword              },
	[TEXT_OBJ_OUTER_LONGWORD]       = { text_object_longword_outer        },
	[TEXT_OBJ_LINE_UP]              = { text_object_line                  },
	[TEXT_OBJ_LINE_DOWN]            = { text_object_line                  },
	[TEXT_OBJ_SENTENCE]             = { text_object_sentence              },
	[TEXT_OBJ_PARAGRAPH]            = { text_object_paragraph             },
	[TEXT_OBJ_OUTER_SQUARE_BRACKET] = { text_object_square_bracket, OUTER },
	[TEXT_OBJ_INNER_SQUARE_BRACKET] = { text_object_square_bracket, INNER },
	[TEXT_OBJ_OUTER_CURLY_BRACKET]  = { text_object_curly_bracket,  OUTER },
	[TEXT_OBJ_INNER_CURLY_BRACKET]  = { text_object_curly_bracket,  INNER },
	[TEXT_OBJ_OUTER_ANGLE_BRACKET]  = { text_object_angle_bracket,  OUTER },
	[TEXT_OBJ_INNER_ANGLE_BRACKET]  = { text_object_angle_bracket,  INNER },
	[TEXT_OBJ_OUTER_PARANTHESE]     = { text_object_paranthese,     OUTER },
	[TEXT_OBJ_INNER_PARANTHESE]     = { text_object_paranthese,     INNER },
	[TEXT_OBJ_OUTER_QUOTE]          = { text_object_quote,          OUTER },
	[TEXT_OBJ_INNER_QUOTE]          = { text_object_quote,          INNER },
	[TEXT_OBJ_OUTER_SINGLE_QUOTE]   = { text_object_single_quote,   OUTER },
	[TEXT_OBJ_INNER_SINGLE_QUOTE]   = { text_object_single_quote,   INNER },
	[TEXT_OBJ_OUTER_BACKTICK]       = { text_object_backtick,       OUTER },
	[TEXT_OBJ_INNER_BACKTICK]       = { text_object_backtick,       INNER },
};

/* if some movements are forced to be linewise, they are translated to text objects */
static TextObject *moves_linewise[] = {
	[MOVE_LINE_UP]          = &textobjs[TEXT_OBJ_LINE_UP],
	[MOVE_LINE_DOWN]        = &textobjs[TEXT_OBJ_LINE_DOWN],
	[MOVE_SCREEN_LINE_UP]   = &textobjs[TEXT_OBJ_LINE_UP],
	[MOVE_SCREEN_LINE_DOWN] = &textobjs[TEXT_OBJ_LINE_DOWN],
};

/** functions to be called from keybindings */
/* navigate jump list either in forward (arg->i>0) or backward (arg->i<0) direction */
static void jumplist(const Arg *arg);
/* navigate change list either in forward (arg->i>0) or backward (arg->i<0) direction */
static void changelist(const Arg *arg);
static void macro_record(const Arg *arg);
static void macro_replay(const Arg *arg);
/* temporarily suspend the editor and return to the shell, type 'fg' to get back */
static void suspend(const Arg *arg);
/* switch to mode indicated by arg->i */
static void switchmode(const Arg *arg);
/* set mark indicated by arg->i to current cursor position */
static void mark_set(const Arg *arg);
/* insert arg->s at the current cursor position */
static void insert(const Arg *arg);
/* insert a tab or the needed amount of spaces at the current cursor position */
static void insert_tab(const Arg *arg);
/* inserts a newline (either \n or \r\n depending on file type) */
static void insert_newline(const Arg *arg);
/* put register content either before (if arg->i < 0) or after (if arg->i > 0)
 * current cursor position */
static void put(const Arg *arg);
/* add a new line either before or after the one where the cursor currently is */
static void openline(const Arg *arg);
/* join lines from current cursor position to movement indicated by arg */
static void join(const Arg *arg);
/* execute arg->s as if it was typed on command prompt */
static void cmd(const Arg *arg);
/* perform last action i.e. action_prev again */
static void repeat(const Arg *arg);
/* repeat last to/till movement */
static void totill_repeat(const Arg *arg);
/* repeat last to/till movement but in opposite direction */
static void totill_reverse(const Arg *arg);
/* replace character at cursor with one read form keyboard */
static void replace(const Arg *arg);
/* create a new cursor on the previous (arg->i < 0) or next (arg->i > 0) line */
static void cursors_new(const Arg *arg);
/* try to align all cursors on the same column */
static void cursors_align(const Arg *arg);
/* remove all but the primary cursor and their selections */
static void cursors_clear(const Arg *arg);
/* remove the least recently added cursor */
static void cursors_remove(const Arg *arg);
/* select the word the cursor is currently over */
static void cursors_select(const Arg *arg);
/* select the next region matching the current selection */
static void cursors_select_next(const Arg *arg);
/* clear current selection but select next match */
static void cursors_select_skip(const Arg *arg);
/* adjust action.count by arg->i */
static void count(const Arg *arg);
/* move to the action.count-th line or if not given either to the first (arg->i < 0)
 *  or last (arg->i > 0) line of file */
static void gotoline(const Arg *arg);
/* force operator to linewise (if arg->b is set) */
static void linewise(const Arg *arg);
/* make the current action use the operator indicated by arg->i */
static void operator(const Arg *arg);
/* execute operator twice useful for synonyms (e.g. 'cc') */
static void operator_twice(const Arg *arg);
/* change case of a file range to upper (arg->i > 0) or lowercase (arg->i < 0) */
static void changecase(const Arg *arg);
/* blocks to read a key and performs movement indicated by arg->i which
 * should be one of MOVE_{RIGHT,LEFT}_{TO,TILL} */
static void movement_key(const Arg *arg);
/* perform the movement as indicated by arg->i */
static void movement(const Arg *arg);
/* let the current operator affect the range indicated by the text object arg->i */
static void textobj(const Arg *arg);
/* move to the other end of selected text */
static void selection_end(const Arg *arg);
/* use register indicated by arg->i for the current operator */
static void reg(const Arg *arg);
/* perform a movement to mark arg->i */
static void mark(const Arg *arg);
/* perform a movement to the first non-blank on the line pointed by mark arg->i */
static void mark_line(const Arg *arg);
/* {un,re}do last action, redraw window */
static void undo(const Arg *arg);
static void redo(const Arg *arg);
/* earlier, later action chronologically, redraw window */
static void earlier(const Arg *arg);
static void later(const Arg *arg);
/* either part of multiplier or a movement to begin of line */
static void zero(const Arg *arg);
/* hange/delete from the current cursor position to the end of
 * movement as indicated by arg->i */
static void change(const Arg *arg);
static void delete(const Arg *arg);
/* perform movement according to arg->i, then switch to insert mode */
static void insertmode(const Arg *arg);
/* insert register content indicated by arg->i at current cursor position */
static void insert_register(const Arg *arg);
/* show a user prompt to get input with title arg->s */
static void prompt_search(const Arg *arg);
static void prompt_cmd(const Arg *arg);
/* evaluate user input at prompt, perform search or execute a command */
static void prompt_enter(const Arg *arg);
/* cycle through past user inputs */
static void prompt_up(const Arg *arg);
static void prompt_down(const Arg *arg);
/* exit command mode if the last char is deleted */
static void prompt_backspace(const Arg *arg);
/* blocks to read 3 consecutive digits and inserts the corresponding byte value */
static void insert_verbatim(const Arg *arg);
/* scroll window content according to arg->i which can be either PAGE, PAGE_HALF,
 * or an arbitrary number of lines. a multiplier overrides what is given in arg->i.
 * negative values scroll back, positive forward. */
static void wscroll(const Arg *arg);
/* similar to scroll, but do only move window content not cursor position */
static void wslide(const Arg *arg);
/* call editor function as indicated by arg->f */
static void call(const Arg *arg);
/* call window function as indicated by arg->w */
static void window(const Arg *arg);
/* quit editor, discard all changes */
static void quit(const Arg *arg);

/** commands to enter at the ':'-prompt */
/* set various runtime options */
static bool cmd_set(Filerange*, enum CmdOpt, const char *argv[]);
/* for each argument create a new window and open the corresponding file */
static bool cmd_open(Filerange*, enum CmdOpt, const char *argv[]);
/* close current window (discard modifications if forced ) and open argv[1],
 * if no argv[1] is given re-read to current file from disk */
static bool cmd_edit(Filerange*, enum CmdOpt, const char *argv[]);
/* close the current window, discard modifications if forced */
static bool cmd_quit(Filerange*, enum CmdOpt, const char *argv[]);
/* close all windows which show current file, discard modifications if forced  */
static bool cmd_bdelete(Filerange*, enum CmdOpt, const char *argv[]);
/* close all windows, exit editor, discard modifications if forced */
static bool cmd_qall(Filerange*, enum CmdOpt, const char *argv[]);
/* for each argument try to insert the file content at current cursor postion */
static bool cmd_read(Filerange*, enum CmdOpt, const char *argv[]);
static bool cmd_substitute(Filerange*, enum CmdOpt, const char *argv[]);
/* if no argument are given, split the current window horizontally,
 * otherwise open the file */
static bool cmd_split(Filerange*, enum CmdOpt, const char *argv[]);
/* if no argument are given, split the current window vertically,
 * otherwise open the file */
static bool cmd_vsplit(Filerange*, enum CmdOpt, const char *argv[]);
/* create a new empty window and arrange all windows either horizontally or vertically */
static bool cmd_new(Filerange*, enum CmdOpt, const char *argv[]);
static bool cmd_vnew(Filerange*, enum CmdOpt, const char *argv[]);
/* save the file displayed in the current window and close it */
static bool cmd_wq(Filerange*, enum CmdOpt, const char *argv[]);
/* save the file displayed in the current window if it was changed, then close the window */
static bool cmd_xit(Filerange*, enum CmdOpt, const char *argv[]);
/* save the file displayed in the current window to the name given.
 * do not change internal filname association. further :w commands
 * without arguments will still write to the old filename */
static bool cmd_write(Filerange*, enum CmdOpt, const char *argv[]);
/* save the file displayed in the current window to the name given,
 * associate the new name with the buffer. further :w commands
 * without arguments will write to the new filename */
static bool cmd_saveas(Filerange*, enum CmdOpt, const char *argv[]);
/* filter range through external program argv[1] */
static bool cmd_filter(Filerange*, enum CmdOpt, const char *argv[]);
/* switch to the previous/next saved state of the text, chronologically */
static bool cmd_earlier_later(Filerange*, enum CmdOpt, const char *argv[]);

static void action_reset(Action *a);
static void switchmode_to(Mode *new_mode);
static bool vis_window_new(const char *file);
static bool vis_window_split(Win *win);

#include "config.h"

static Key getkey(void);
static void keypress(Key *key);
static void action_do(Action *a);
static bool exec_command(char type, const char *cmdline);

/** operator implementations of type: void (*op)(OperatorContext*) */

static size_t op_delete(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t len = c->range.end - c->range.start;
	c->reg->linewise = c->linewise;
	register_put(c->reg, txt, &c->range);
	text_delete(txt, c->range.start, len);
	size_t pos = c->range.start;
	if (c->linewise && pos == text_size(txt))
		pos = text_line_begin(txt, text_line_prev(txt, pos));
	return pos;
}

static size_t op_change(OperatorContext *c) {
	op_delete(c);
	return c->range.start;
}

static size_t op_yank(OperatorContext *c) {
	c->reg->linewise = c->linewise;
	register_put(c->reg, vis->win->file->text, &c->range);
	return c->pos;
}

static size_t op_put(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t pos = c->pos;
	if (c->arg->i > 0) {
		if (c->reg->linewise)
			pos = text_line_next(txt, pos);
		else
			pos = text_char_next(txt, pos);
	} else {
		if (c->reg->linewise)
			pos = text_line_begin(txt, pos);
	}
	text_insert(txt, pos, c->reg->data, c->reg->len);
	if (c->reg->linewise)
		return text_line_start(txt, pos);
	else
		return pos + c->reg->len;
}

static const char *expand_tab(void) {
	static char spaces[9];
	int tabwidth = editor_tabwidth_get(vis);
	tabwidth = MIN(tabwidth, LENGTH(spaces) - 1);
	for (int i = 0; i < tabwidth; i++)
		spaces[i] = ' ';
	spaces[tabwidth] = '\0';
	return vis->expandtab ? spaces : "\t";
}

static size_t op_shift_right(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t pos = text_line_begin(txt, c->range.end), prev_pos;
	const char *tab = expand_tab();
	size_t tablen = strlen(tab);

	/* if range ends at the begin of a line, skip line break */
	if (pos == c->range.end)
		pos = text_line_prev(txt, pos);

	do {
		prev_pos = pos = text_line_begin(txt, pos);
		text_insert(txt, pos, tab, tablen);
		pos = text_line_prev(txt, pos);
	}  while (pos >= c->range.start && pos != prev_pos);

	return c->pos + tablen;
}

static size_t op_shift_left(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t pos = text_line_begin(txt, c->range.end), prev_pos;
	size_t tabwidth = editor_tabwidth_get(vis), tablen;

	/* if range ends at the begin of a line, skip line break */
	if (pos == c->range.end)
		pos = text_line_prev(txt, pos);

	do {
		char c;
		size_t len = 0;
		prev_pos = pos = text_line_begin(txt, pos);
		Iterator it = text_iterator_get(txt, pos);
		if (text_iterator_byte_get(&it, &c) && c == '\t') {
			len = 1;
		} else {
			for (len = 0; text_iterator_byte_get(&it, &c) && c == ' '; len++)
				text_iterator_byte_next(&it, NULL);
		}
		tablen = MIN(len, tabwidth);
		text_delete(txt, pos, tablen);
		pos = text_line_prev(txt, pos);
	}  while (pos >= c->range.start && pos != prev_pos);

	return c->pos - tablen;
}

static size_t op_case_change(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t len = c->range.end - c->range.start;
	char *buf = malloc(len);
	if (!buf)
		return c->pos;
	len = text_bytes_get(txt, c->range.start, len, buf);
	size_t rem = len;
	for (char *cur = buf; rem > 0; cur++, rem--) {
		int ch = (unsigned char)*cur;
		if (isascii(ch)) {
			if (c->arg->i == 0)
				*cur = islower(ch) ? toupper(ch) : tolower(ch);
			else if (c->arg->i > 0)
				*cur = toupper(ch);
			else
				*cur = tolower(ch);
		}
	}

	text_delete(txt, c->range.start, len);
	text_insert(txt, c->range.start, buf, len);
	free(buf);
	return c->pos;
}

static size_t op_cursor(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	View *view = vis->win->view;
	Filerange r = text_range_linewise(txt, &c->range);
	for (size_t line = text_range_line_first(txt, &r); line != EPOS; line = text_range_line_next(txt, &r, line)) {
		Cursor *cursor = view_cursors_new(view);
		if (cursor)
			view_cursors_to(cursor, text_line_start(txt, line));
	}
	return EPOS;
}

static size_t op_join(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t pos = text_line_begin(txt, c->range.end), prev_pos;
	/* if selection is linewise, skip last line break */
	if (text_range_is_linewise(txt, &c->range)) {
		size_t line_prev = text_line_prev(txt, pos);
		size_t line_prev_prev = text_line_prev(txt, line_prev);
		if (line_prev_prev >= c->range.start)
			pos = line_prev;
	}<commit_msg>vis: fix join operator<commit_after>/*
 * Copyright (c) 2014 <NAME> <mat at brain-dump.org>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include <locale.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <ctype.h>
#include <time.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

#include "ui-curses.h"
#include "editor.h"
#include "text-util.h"
#include "text-motions.h"
#include "text-objects.h"
#include "util.h"
#include "map.h"
#include "libutf.h"

/** global variables */
static Editor *vis;         /* global editor instance, keeps track of all windows etc. */

/** operators */
static size_t op_change(OperatorContext *c);
static size_t op_yank(OperatorContext *c);
static size_t op_put(OperatorContext *c);
static size_t op_delete(OperatorContext *c);
static size_t op_shift_right(OperatorContext *c);
static size_t op_shift_left(OperatorContext *c);
static size_t op_case_change(OperatorContext *c);
static size_t op_join(OperatorContext *c);
static size_t op_repeat_insert(OperatorContext *c);
static size_t op_repeat_replace(OperatorContext *c);
static size_t op_cursor(OperatorContext *c);

/* these can be passed as int argument to operator(&(const Arg){ .i = OP_*}) */
enum {
	OP_DELETE,
	OP_CHANGE,
	OP_YANK,
	OP_PUT,
	OP_SHIFT_RIGHT,
	OP_SHIFT_LEFT,
	OP_CASE_CHANGE,
	OP_JOIN,
	OP_REPEAT_INSERT,
	OP_REPEAT_REPLACE,
	OP_CURSOR,
};

static Operator ops[] = {
	[OP_DELETE]      = { op_delete      },
	[OP_CHANGE]      = { op_change      },
	[OP_YANK]        = { op_yank        },
	[OP_PUT]         = { op_put         },
	[OP_SHIFT_RIGHT] = { op_shift_right },
	[OP_SHIFT_LEFT]  = { op_shift_left  },
	[OP_CASE_CHANGE] = { op_case_change },
	[OP_JOIN]          = { op_join          },
	[OP_REPEAT_INSERT]  = { op_repeat_insert  },
	[OP_REPEAT_REPLACE] = { op_repeat_replace },
	[OP_CURSOR]         = { op_cursor         },
};

#define PAGE      INT_MAX
#define PAGE_HALF (INT_MAX-1)

/* these can be passed as int argument to movement(&(const Arg){ .i = MOVE_* }) */
enum {
	MOVE_LINE_DOWN,
	MOVE_LINE_UP,
	MOVE_SCREEN_LINE_UP,
	MOVE_SCREEN_LINE_DOWN,
	MOVE_SCREEN_LINE_BEGIN,
	MOVE_SCREEN_LINE_MIDDLE,
	MOVE_SCREEN_LINE_END,
	MOVE_LINE_PREV,
	MOVE_LINE_BEGIN,
	MOVE_LINE_START,
	MOVE_LINE_FINISH,
	MOVE_LINE_LASTCHAR,
	MOVE_LINE_END,
	MOVE_LINE_NEXT,
	MOVE_LINE,
	MOVE_COLUMN,
	MOVE_CHAR_PREV,
	MOVE_CHAR_NEXT,
	MOVE_LINE_CHAR_PREV,
	MOVE_LINE_CHAR_NEXT,
	MOVE_WORD_START_NEXT,
	MOVE_WORD_END_PREV,
	MOVE_WORD_END_NEXT,
	MOVE_WORD_START_PREV,
	MOVE_LONGWORD_START_PREV,
	MOVE_LONGWORD_START_NEXT,
	MOVE_LONGWORD_END_PREV,
	MOVE_LONGWORD_END_NEXT,
	MOVE_SENTENCE_PREV,
	MOVE_SENTENCE_NEXT,
	MOVE_PARAGRAPH_PREV,
	MOVE_PARAGRAPH_NEXT,
	MOVE_BRACKET_MATCH,
	MOVE_LEFT_TO,
	MOVE_RIGHT_TO,
	MOVE_LEFT_TILL,
	MOVE_RIGHT_TILL,
	MOVE_FILE_BEGIN,
	MOVE_FILE_END,
	MOVE_MARK,
	MOVE_MARK_LINE,
	MOVE_SEARCH_WORD_FORWARD,
	MOVE_SEARCH_WORD_BACKWARD,
	MOVE_SEARCH_FORWARD,
	MOVE_SEARCH_BACKWARD,
	MOVE_WINDOW_LINE_TOP,
	MOVE_WINDOW_LINE_MIDDLE,
	MOVE_WINDOW_LINE_BOTTOM,
};

/** movements which can be used besides the one in text-motions.h and view.h */

/* search in forward direction for the word under the cursor */
static size_t search_word_forward(Text *txt, size_t pos);
/* search in backward direction for the word under the cursor */
static size_t search_word_backward(Text *txt, size_t pos);
/* search again for the last used search pattern */
static size_t search_forward(Text *txt, size_t pos);
static size_t search_backward(Text *txt, size_t pos);
/* goto action.mark */
static size_t mark_goto(File *txt, size_t pos);
/* goto first non-blank char on line pointed by action.mark */
static size_t mark_line_goto(File *txt, size_t pos);
/* goto to next occurence of action.key to the right */
static size_t to(Text *txt, size_t pos);
/* goto to position before next occurence of action.key to the right */
static size_t till(Text *txt, size_t pos);
/* goto to next occurence of action.key to the left */
static size_t to_left(Text *txt, size_t pos);
/* goto to position after next occurence of action.key to the left */
static size_t till_left(Text *txt, size_t pos);
/* goto line number action.count */
static size_t line(Text *txt, size_t pos);
/* goto to byte action.count on current line */
static size_t column(Text *txt, size_t pos);
/* goto the action.count-th line from top of the focused window */
static size_t view_lines_top(const Arg *arg);
/* goto the start of middle line of the focused window */
static size_t view_lines_middle(const Arg *arg);
/* goto the action.count-th line from bottom of the focused window */
static size_t view_lines_bottom(const Arg *arg);

static Movement moves[] = {
	[MOVE_LINE_UP]             = { .view = view_line_up                                       },
	[MOVE_LINE_DOWN]           = { .view = view_line_down                                     },
	[MOVE_SCREEN_LINE_UP]      = { .view = view_screenline_up                                 },
	[MOVE_SCREEN_LINE_DOWN]    = { .view = view_screenline_down                               },
	[MOVE_SCREEN_LINE_BEGIN]   = { .view = view_screenline_begin,  .type = CHARWISE           },
	[MOVE_SCREEN_LINE_MIDDLE]  = { .view = view_screenline_middle, .type = CHARWISE           },
	[MOVE_SCREEN_LINE_END]     = { .view = view_screenline_end,    .type = CHARWISE|INCLUSIVE },
	[MOVE_LINE_PREV]           = { .txt = text_line_prev,           .type = LINEWISE           },
	[MOVE_LINE_BEGIN]          = { .txt = text_line_begin,          .type = LINEWISE           },
	[MOVE_LINE_START]          = { .txt = text_line_start,          .type = LINEWISE           },
	[MOVE_LINE_FINISH]         = { .txt = text_line_finish,         .type = LINEWISE|INCLUSIVE },
	[MOVE_LINE_LASTCHAR]       = { .txt = text_line_lastchar,       .type = LINEWISE|INCLUSIVE },
	[MOVE_LINE_END]            = { .txt = text_line_end,            .type = LINEWISE           },
	[MOVE_LINE_NEXT]           = { .txt = text_line_next,           .type = LINEWISE           },
	[MOVE_LINE]                = { .txt = line,                     .type = LINEWISE|IDEMPOTENT|JUMP},
	[MOVE_COLUMN]              = { .txt = column,                   .type = CHARWISE|IDEMPOTENT},
	[MOVE_CHAR_PREV]           = { .txt = text_char_prev,           .type = CHARWISE           },
	[MOVE_CHAR_NEXT]           = { .txt = text_char_next,           .type = CHARWISE           },
	[MOVE_LINE_CHAR_PREV]      = { .txt = text_line_char_prev,      .type = CHARWISE           },
	[MOVE_LINE_CHAR_NEXT]      = { .txt = text_line_char_next,      .type = CHARWISE           },
	[MOVE_WORD_START_PREV]     = { .txt = text_word_start_prev,     .type = CHARWISE           },
	[MOVE_WORD_START_NEXT]     = { .txt = text_word_start_next,     .type = CHARWISE           },
	[MOVE_WORD_END_PREV]       = { .txt = text_word_end_prev,       .type = CHARWISE|INCLUSIVE },
	[MOVE_WORD_END_NEXT]       = { .txt = text_word_end_next,       .type = CHARWISE|INCLUSIVE },
	[MOVE_LONGWORD_START_PREV] = { .txt = text_longword_start_prev, .type = CHARWISE           },
	[MOVE_LONGWORD_START_NEXT] = { .txt = text_longword_start_next, .type = CHARWISE           },
	[MOVE_LONGWORD_END_PREV]   = { .txt = text_longword_end_prev,   .type = CHARWISE|INCLUSIVE },
	[MOVE_LONGWORD_END_NEXT]   = { .txt = text_longword_end_next,   .type = CHARWISE|INCLUSIVE },
	[MOVE_SENTENCE_PREV]       = { .txt = text_sentence_prev,       .type = LINEWISE           },
	[MOVE_SENTENCE_NEXT]       = { .txt = text_sentence_next,       .type = LINEWISE           },
	[MOVE_PARAGRAPH_PREV]      = { .txt = text_paragraph_prev,      .type = LINEWISE|JUMP      },
	[MOVE_PARAGRAPH_NEXT]      = { .txt = text_paragraph_next,      .type = LINEWISE|JUMP      },
	[MOVE_BRACKET_MATCH]       = { .txt = text_bracket_match,       .type = LINEWISE|INCLUSIVE|JUMP },
	[MOVE_FILE_BEGIN]          = { .txt = text_begin,               .type = LINEWISE|JUMP      },
	[MOVE_FILE_END]            = { .txt = text_end,                 .type = LINEWISE|JUMP      },
	[MOVE_LEFT_TO]             = { .txt = to_left,                  .type = LINEWISE           },
	[MOVE_RIGHT_TO]            = { .txt = to,                       .type = LINEWISE|INCLUSIVE },
	[MOVE_LEFT_TILL]           = { .txt = till_left,                .type = LINEWISE           },
	[MOVE_RIGHT_TILL]          = { .txt = till,                     .type = LINEWISE|INCLUSIVE },
	[MOVE_MARK]                = { .file = mark_goto,             .type = LINEWISE|JUMP|IDEMPOTENT },
	[MOVE_MARK_LINE]           = { .file = mark_line_goto,        .type = LINEWISE|JUMP|IDEMPOTENT },
	[MOVE_SEARCH_WORD_FORWARD] = { .txt = search_word_forward,      .type = LINEWISE|JUMP      },
	[MOVE_SEARCH_WORD_BACKWARD]= { .txt = search_word_backward,     .type = LINEWISE|JUMP      },
	[MOVE_SEARCH_FORWARD]      = { .txt = search_forward,           .type = LINEWISE|JUMP      },
	[MOVE_SEARCH_BACKWARD]     = { .txt = search_backward,          .type = LINEWISE|JUMP      },
	[MOVE_WINDOW_LINE_TOP]     = { .cmd = view_lines_top,         .type = LINEWISE|JUMP|IDEMPOTENT },
	[MOVE_WINDOW_LINE_MIDDLE]  = { .cmd = view_lines_middle,      .type = LINEWISE|JUMP|IDEMPOTENT },
	[MOVE_WINDOW_LINE_BOTTOM]  = { .cmd = view_lines_bottom,      .type = LINEWISE|JUMP|IDEMPOTENT },
};

/* these can be passed as int argument to textobj(&(const Arg){ .i = TEXT_OBJ_* }) */
enum {
	TEXT_OBJ_INNER_WORD,
	TEXT_OBJ_OUTER_WORD,
	TEXT_OBJ_INNER_LONGWORD,
	TEXT_OBJ_OUTER_LONGWORD,
	TEXT_OBJ_LINE_UP,
	TEXT_OBJ_LINE_DOWN,
	TEXT_OBJ_SENTENCE,
	TEXT_OBJ_PARAGRAPH,
	TEXT_OBJ_OUTER_SQUARE_BRACKET,
	TEXT_OBJ_INNER_SQUARE_BRACKET,
	TEXT_OBJ_OUTER_CURLY_BRACKET,
	TEXT_OBJ_INNER_CURLY_BRACKET,
	TEXT_OBJ_OUTER_ANGLE_BRACKET,
	TEXT_OBJ_INNER_ANGLE_BRACKET,
	TEXT_OBJ_OUTER_PARANTHESE,
	TEXT_OBJ_INNER_PARANTHESE,
	TEXT_OBJ_OUTER_QUOTE,
	TEXT_OBJ_INNER_QUOTE,
	TEXT_OBJ_OUTER_SINGLE_QUOTE,
	TEXT_OBJ_INNER_SINGLE_QUOTE,
	TEXT_OBJ_OUTER_BACKTICK,
	TEXT_OBJ_INNER_BACKTICK,
};

static TextObject textobjs[] = {
	[TEXT_OBJ_INNER_WORD]           = { text_object_word                  },
	[TEXT_OBJ_OUTER_WORD]           = { text_object_word_outer            },
	[TEXT_OBJ_INNER_LONGWORD]       = { text_object_longword              },
	[TEXT_OBJ_OUTER_LONGWORD]       = { text_object_longword_outer        },
	[TEXT_OBJ_LINE_UP]              = { text_object_line                  },
	[TEXT_OBJ_LINE_DOWN]            = { text_object_line                  },
	[TEXT_OBJ_SENTENCE]             = { text_object_sentence              },
	[TEXT_OBJ_PARAGRAPH]            = { text_object_paragraph             },
	[TEXT_OBJ_OUTER_SQUARE_BRACKET] = { text_object_square_bracket, OUTER },
	[TEXT_OBJ_INNER_SQUARE_BRACKET] = { text_object_square_bracket, INNER },
	[TEXT_OBJ_OUTER_CURLY_BRACKET]  = { text_object_curly_bracket,  OUTER },
	[TEXT_OBJ_INNER_CURLY_BRACKET]  = { text_object_curly_bracket,  INNER },
	[TEXT_OBJ_OUTER_ANGLE_BRACKET]  = { text_object_angle_bracket,  OUTER },
	[TEXT_OBJ_INNER_ANGLE_BRACKET]  = { text_object_angle_bracket,  INNER },
	[TEXT_OBJ_OUTER_PARANTHESE]     = { text_object_paranthese,     OUTER },
	[TEXT_OBJ_INNER_PARANTHESE]     = { text_object_paranthese,     INNER },
	[TEXT_OBJ_OUTER_QUOTE]          = { text_object_quote,          OUTER },
	[TEXT_OBJ_INNER_QUOTE]          = { text_object_quote,          INNER },
	[TEXT_OBJ_OUTER_SINGLE_QUOTE]   = { text_object_single_quote,   OUTER },
	[TEXT_OBJ_INNER_SINGLE_QUOTE]   = { text_object_single_quote,   INNER },
	[TEXT_OBJ_OUTER_BACKTICK]       = { text_object_backtick,       OUTER },
	[TEXT_OBJ_INNER_BACKTICK]       = { text_object_backtick,       INNER },
};

/* if some movements are forced to be linewise, they are translated to text objects */
static TextObject *moves_linewise[] = {
	[MOVE_LINE_UP]          = &textobjs[TEXT_OBJ_LINE_UP],
	[MOVE_LINE_DOWN]        = &textobjs[TEXT_OBJ_LINE_DOWN],
	[MOVE_SCREEN_LINE_UP]   = &textobjs[TEXT_OBJ_LINE_UP],
	[MOVE_SCREEN_LINE_DOWN] = &textobjs[TEXT_OBJ_LINE_DOWN],
};

/** functions to be called from keybindings */
/* navigate jump list either in forward (arg->i>0) or backward (arg->i<0) direction */
static void jumplist(const Arg *arg);
/* navigate change list either in forward (arg->i>0) or backward (arg->i<0) direction */
static void changelist(const Arg *arg);
static void macro_record(const Arg *arg);
static void macro_replay(const Arg *arg);
/* temporarily suspend the editor and return to the shell, type 'fg' to get back */
static void suspend(const Arg *arg);
/* switch to mode indicated by arg->i */
static void switchmode(const Arg *arg);
/* set mark indicated by arg->i to current cursor position */
static void mark_set(const Arg *arg);
/* insert arg->s at the current cursor position */
static void insert(const Arg *arg);
/* insert a tab or the needed amount of spaces at the current cursor position */
static void insert_tab(const Arg *arg);
/* inserts a newline (either \n or \r\n depending on file type) */
static void insert_newline(const Arg *arg);
/* put register content either before (if arg->i < 0) or after (if arg->i > 0)
 * current cursor position */
static void put(const Arg *arg);
/* add a new line either before or after the one where the cursor currently is */
static void openline(const Arg *arg);
/* join lines from current cursor position to movement indicated by arg */
static void join(const Arg *arg);
/* execute arg->s as if it was typed on command prompt */
static void cmd(const Arg *arg);
/* perform last action i.e. action_prev again */
static void repeat(const Arg *arg);
/* repeat last to/till movement */
static void totill_repeat(const Arg *arg);
/* repeat last to/till movement but in opposite direction */
static void totill_reverse(const Arg *arg);
/* replace character at cursor with one read form keyboard */
static void replace(const Arg *arg);
/* create a new cursor on the previous (arg->i < 0) or next (arg->i > 0) line */
static void cursors_new(const Arg *arg);
/* try to align all cursors on the same column */
static void cursors_align(const Arg *arg);
/* remove all but the primary cursor and their selections */
static void cursors_clear(const Arg *arg);
/* remove the least recently added cursor */
static void cursors_remove(const Arg *arg);
/* select the word the cursor is currently over */
static void cursors_select(const Arg *arg);
/* select the next region matching the current selection */
static void cursors_select_next(const Arg *arg);
/* clear current selection but select next match */
static void cursors_select_skip(const Arg *arg);
/* adjust action.count by arg->i */
static void count(const Arg *arg);
/* move to the action.count-th line or if not given either to the first (arg->i < 0)
 *  or last (arg->i > 0) line of file */
static void gotoline(const Arg *arg);
/* force operator to linewise (if arg->b is set) */
static void linewise(const Arg *arg);
/* make the current action use the operator indicated by arg->i */
static void operator(const Arg *arg);
/* execute operator twice useful for synonyms (e.g. 'cc') */
static void operator_twice(const Arg *arg);
/* change case of a file range to upper (arg->i > 0) or lowercase (arg->i < 0) */
static void changecase(const Arg *arg);
/* blocks to read a key and performs movement indicated by arg->i which
 * should be one of MOVE_{RIGHT,LEFT}_{TO,TILL} */
static void movement_key(const Arg *arg);
/* perform the movement as indicated by arg->i */
static void movement(const Arg *arg);
/* let the current operator affect the range indicated by the text object arg->i */
static void textobj(const Arg *arg);
/* move to the other end of selected text */
static void selection_end(const Arg *arg);
/* use register indicated by arg->i for the current operator */
static void reg(const Arg *arg);
/* perform a movement to mark arg->i */
static void mark(const Arg *arg);
/* perform a movement to the first non-blank on the line pointed by mark arg->i */
static void mark_line(const Arg *arg);
/* {un,re}do last action, redraw window */
static void undo(const Arg *arg);
static void redo(const Arg *arg);
/* earlier, later action chronologically, redraw window */
static void earlier(const Arg *arg);
static void later(const Arg *arg);
/* either part of multiplier or a movement to begin of line */
static void zero(const Arg *arg);
/* hange/delete from the current cursor position to the end of
 * movement as indicated by arg->i */
static void change(const Arg *arg);
static void delete(const Arg *arg);
/* perform movement according to arg->i, then switch to insert mode */
static void insertmode(const Arg *arg);
/* insert register content indicated by arg->i at current cursor position */
static void insert_register(const Arg *arg);
/* show a user prompt to get input with title arg->s */
static void prompt_search(const Arg *arg);
static void prompt_cmd(const Arg *arg);
/* evaluate user input at prompt, perform search or execute a command */
static void prompt_enter(const Arg *arg);
/* cycle through past user inputs */
static void prompt_up(const Arg *arg);
static void prompt_down(const Arg *arg);
/* exit command mode if the last char is deleted */
static void prompt_backspace(const Arg *arg);
/* blocks to read 3 consecutive digits and inserts the corresponding byte value */
static void insert_verbatim(const Arg *arg);
/* scroll window content according to arg->i which can be either PAGE, PAGE_HALF,
 * or an arbitrary number of lines. a multiplier overrides what is given in arg->i.
 * negative values scroll back, positive forward. */
static void wscroll(const Arg *arg);
/* similar to scroll, but do only move window content not cursor position */
static void wslide(const Arg *arg);
/* call editor function as indicated by arg->f */
static void call(const Arg *arg);
/* call window function as indicated by arg->w */
static void window(const Arg *arg);
/* quit editor, discard all changes */
static void quit(const Arg *arg);

/** commands to enter at the ':'-prompt */
/* set various runtime options */
static bool cmd_set(Filerange*, enum CmdOpt, const char *argv[]);
/* for each argument create a new window and open the corresponding file */
static bool cmd_open(Filerange*, enum CmdOpt, const char *argv[]);
/* close current window (discard modifications if forced ) and open argv[1],
 * if no argv[1] is given re-read to current file from disk */
static bool cmd_edit(Filerange*, enum CmdOpt, const char *argv[]);
/* close the current window, discard modifications if forced */
static bool cmd_quit(Filerange*, enum CmdOpt, const char *argv[]);
/* close all windows which show current file, discard modifications if forced  */
static bool cmd_bdelete(Filerange*, enum CmdOpt, const char *argv[]);
/* close all windows, exit editor, discard modifications if forced */
static bool cmd_qall(Filerange*, enum CmdOpt, const char *argv[]);
/* for each argument try to insert the file content at current cursor postion */
static bool cmd_read(Filerange*, enum CmdOpt, const char *argv[]);
static bool cmd_substitute(Filerange*, enum CmdOpt, const char *argv[]);
/* if no argument are given, split the current window horizontally,
 * otherwise open the file */
static bool cmd_split(Filerange*, enum CmdOpt, const char *argv[]);
/* if no argument are given, split the current window vertically,
 * otherwise open the file */
static bool cmd_vsplit(Filerange*, enum CmdOpt, const char *argv[]);
/* create a new empty window and arrange all windows either horizontally or vertically */
static bool cmd_new(Filerange*, enum CmdOpt, const char *argv[]);
static bool cmd_vnew(Filerange*, enum CmdOpt, const char *argv[]);
/* save the file displayed in the current window and close it */
static bool cmd_wq(Filerange*, enum CmdOpt, const char *argv[]);
/* save the file displayed in the current window if it was changed, then close the window */
static bool cmd_xit(Filerange*, enum CmdOpt, const char *argv[]);
/* save the file displayed in the current window to the name given.
 * do not change internal filname association. further :w commands
 * without arguments will still write to the old filename */
static bool cmd_write(Filerange*, enum CmdOpt, const char *argv[]);
/* save the file displayed in the current window to the name given,
 * associate the new name with the buffer. further :w commands
 * without arguments will write to the new filename */
static bool cmd_saveas(Filerange*, enum CmdOpt, const char *argv[]);
/* filter range through external program argv[1] */
static bool cmd_filter(Filerange*, enum CmdOpt, const char *argv[]);
/* switch to the previous/next saved state of the text, chronologically */
static bool cmd_earlier_later(Filerange*, enum CmdOpt, const char *argv[]);

static void action_reset(Action *a);
static void switchmode_to(Mode *new_mode);
static bool vis_window_new(const char *file);
static bool vis_window_split(Win *win);

#include "config.h"

static Key getkey(void);
static void keypress(Key *key);
static void action_do(Action *a);
static bool exec_command(char type, const char *cmdline);

/** operator implementations of type: void (*op)(OperatorContext*) */

static size_t op_delete(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t len = c->range.end - c->range.start;
	c->reg->linewise = c->linewise;
	register_put(c->reg, txt, &c->range);
	text_delete(txt, c->range.start, len);
	size_t pos = c->range.start;
	if (c->linewise && pos == text_size(txt))
		pos = text_line_begin(txt, text_line_prev(txt, pos));
	return pos;
}

static size_t op_change(OperatorContext *c) {
	op_delete(c);
	return c->range.start;
}

static size_t op_yank(OperatorContext *c) {
	c->reg->linewise = c->linewise;
	register_put(c->reg, vis->win->file->text, &c->range);
	return c->pos;
}

static size_t op_put(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t pos = c->pos;
	if (c->arg->i > 0) {
		if (c->reg->linewise)
			pos = text_line_next(txt, pos);
		else
			pos = text_char_next(txt, pos);
	} else {
		if (c->reg->linewise)
			pos = text_line_begin(txt, pos);
	}
	text_insert(txt, pos, c->reg->data, c->reg->len);
	if (c->reg->linewise)
		return text_line_start(txt, pos);
	else
		return pos + c->reg->len;
}

static const char *expand_tab(void) {
	static char spaces[9];
	int tabwidth = editor_tabwidth_get(vis);
	tabwidth = MIN(tabwidth, LENGTH(spaces) - 1);
	for (int i = 0; i < tabwidth; i++)
		spaces[i] = ' ';
	spaces[tabwidth] = '\0';
	return vis->expandtab ? spaces : "\t";
}

static size_t op_shift_right(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t pos = text_line_begin(txt, c->range.end), prev_pos;
	const char *tab = expand_tab();
	size_t tablen = strlen(tab);

	/* if range ends at the begin of a line, skip line break */
	if (pos == c->range.end)
		pos = text_line_prev(txt, pos);

	do {
		prev_pos = pos = text_line_begin(txt, pos);
		text_insert(txt, pos, tab, tablen);
		pos = text_line_prev(txt, pos);
	}  while (pos >= c->range.start && pos != prev_pos);

	return c->pos + tablen;
}

static size_t op_shift_left(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t pos = text_line_begin(txt, c->range.end), prev_pos;
	size_t tabwidth = editor_tabwidth_get(vis), tablen;

	/* if range ends at the begin of a line, skip line break */
	if (pos == c->range.end)
		pos = text_line_prev(txt, pos);

	do {
		char c;
		size_t len = 0;
		prev_pos = pos = text_line_begin(txt, pos);
		Iterator it = text_iterator_get(txt, pos);
		if (text_iterator_byte_get(&it, &c) && c == '\t') {
			len = 1;
		} else {
			for (len = 0; text_iterator_byte_get(&it, &c) && c == ' '; len++)
				text_iterator_byte_next(&it, NULL);
		}
		tablen = MIN(len, tabwidth);
		text_delete(txt, pos, tablen);
		pos = text_line_prev(txt, pos);
	}  while (pos >= c->range.start && pos != prev_pos);

	return c->pos - tablen;
}

static size_t op_case_change(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t len = c->range.end - c->range.start;
	char *buf = malloc(len);
	if (!buf)
		return c->pos;
	len = text_bytes_get(txt, c->range.start, len, buf);
	size_t rem = len;
	for (char *cur = buf; rem > 0; cur++, rem--) {
		int ch = (unsigned char)*cur;
		if (isascii(ch)) {
			if (c->arg->i == 0)
				*cur = islower(ch) ? toupper(ch) : tolower(ch);
			else if (c->arg->i > 0)
				*cur = toupper(ch);
			else
				*cur = tolower(ch);
		}
	}

	text_delete(txt, c->range.start, len);
	text_insert(txt, c->range.start, buf, len);
	free(buf);
	return c->pos;
}

static size_t op_cursor(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	View *view = vis->win->view;
	Filerange r = text_range_linewise(txt, &c->range);
	for (size_t line = text_range_line_first(txt, &r); line != EPOS; line = text_range_line_next(txt, &r, line)) {
		Cursor *cursor = view_cursors_new(view);
		if (cursor)
			view_cursors_to(cursor, text_line_start(txt, line));
	}
	return EPOS;
}

static size_t op_join(OperatorContext *c) {
	Text *txt = vis->win->file->text;
	size_t pos = text_line_begin(txt, c->range.end), prev_pos;
	if (c->cursor) {
		/* if selection is from visual linewiese mode, skip last line break */
		Filerange sel = view_cursors_selection_get(c->cursor);
		if (text_range_equal(&sel, &c->range) && text_range_is_linewise(txt, &sel)) {
			size_t line_prev = text_line_prev(txt, pos);
			size_t line_prev_prev = text_line_prev(txt, line_prev);
			if (line_prev_prev >= sel.start)
				pos = line_prev;
		}
	}
{'fraction_non_alphanumeric': 0.11390009367412873, 'fraction_numerical': 0.0012234988242940986, 'mean_word_length': 3.6283843567510177, 'pattern_counts': {'":': 0, '<': 59, '<?xml version=': 0, '>': 300, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3116789', 'n_tokens_mistral': 18134, 'n_tokens_neox': 17733, 'n_words': 5141}