Files
neovim/src/nvim/edit.c
Shadman 5ae41ddde3 feat(prompt): prompt_getinput() gets current input #34491
Problem:
Not easy to get user-input in prompt-buffer before the user submits the
input. Under the current system user/plugin needs to read the buffer
contents, figure out where the prompt is, then extract the text.

Solution:
- Add prompt_getinput().
- Extract prompt text extraction logic to a separate function
2025-06-24 12:42:16 -07:00

4791 lines
139 KiB
C

// edit.c: functions for Insert mode
#include <assert.h>
#include <ctype.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <uv.h>
#include "klib/kvec.h"
#include "nvim/api/private/defs.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/change.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/decoration.h"
#include "nvim/digraph.h"
#include "nvim/drawscreen.h"
#include "nvim/edit.h"
#include "nvim/errors.h"
#include "nvim/eval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/extmark.h"
#include "nvim/extmark_defs.h"
#include "nvim/fileio.h"
#include "nvim/fold.h"
#include "nvim/getchar.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
#include "nvim/grid.h"
#include "nvim/grid_defs.h"
#include "nvim/highlight.h"
#include "nvim/highlight_defs.h"
#include "nvim/highlight_group.h"
#include "nvim/indent.h"
#include "nvim/indent_c.h"
#include "nvim/insexpand.h"
#include "nvim/keycodes.h"
#include "nvim/macros_defs.h"
#include "nvim/mapping.h"
#include "nvim/mark.h"
#include "nvim/mark_defs.h"
#include "nvim/marktree_defs.h"
#include "nvim/mbyte.h"
#include "nvim/mbyte_defs.h"
#include "nvim/memline.h"
#include "nvim/memline_defs.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/mouse.h"
#include "nvim/move.h"
#include "nvim/normal.h"
#include "nvim/normal_defs.h"
#include "nvim/ops.h"
#include "nvim/option.h"
#include "nvim/option_vars.h"
#include "nvim/os/input.h"
#include "nvim/plines.h"
#include "nvim/popupmenu.h"
#include "nvim/pos_defs.h"
#include "nvim/search.h"
#include "nvim/state.h"
#include "nvim/state_defs.h"
#include "nvim/strings.h"
#include "nvim/syntax.h"
#include "nvim/terminal.h"
#include "nvim/textformat.h"
#include "nvim/textobject.h"
#include "nvim/types_defs.h"
#include "nvim/ui.h"
#include "nvim/ui_defs.h"
#include "nvim/undo.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
typedef struct {
VimState state;
cmdarg_T *ca;
int mincol;
int cmdchar;
int cmdchar_todo; // cmdchar to handle once in init_prompt
int startln;
int count;
int c;
int lastc;
int i;
bool did_backspace; // previous char was backspace
bool line_is_white; // line is empty before insert
linenr_T old_topline; // topline before insertion
int old_topfill;
int inserted_space; // just inserted a space
int replaceState;
int did_restart_edit; // remember if insert mode was restarted
// after a ctrl+o
bool nomove;
} InsertState;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "edit.c.generated.h"
#endif
enum {
BACKSPACE_CHAR = 1,
BACKSPACE_WORD = 2,
BACKSPACE_WORD_NOT_SPACE = 3,
BACKSPACE_LINE = 4,
};
/// Set when doing something for completion that may call edit() recursively,
/// which is not allowed.
static bool compl_busy = false;
static colnr_T Insstart_textlen; // length of line when insert started
static colnr_T Insstart_blank_vcol; // vcol for first inserted blank
static bool update_Insstart_orig = true; // set Insstart_orig to Insstart
/// the text of the previous insert, K_SPECIAL is escaped
static String last_insert = STRING_INIT;
static int last_insert_skip; // nr of chars in front of previous insert
static int new_insert_skip; // nr of chars in front of current insert
static int did_restart_edit; // "restart_edit" when calling edit()
static bool can_cindent; // may do cindenting on this line
static bool revins_on; // reverse insert mode on
static int revins_chars; // how much to skip after edit
static int revins_legal; // was the last char 'legal'?
static int revins_scol; // start column of revins session
static bool ins_need_undo; // call u_save() before inserting a
// char. Set when edit() is called.
// after that arrow_used is used.
static TriState dont_sync_undo = kFalse; // CTRL-G U prevents syncing undo
// for the next left/right cursor key
static linenr_T o_lnum = 0;
static kvec_t(char) replace_stack = KV_INITIAL_VALUE;
static void insert_enter(InsertState *s)
{
s->did_backspace = true;
s->old_topfill = -1;
s->replaceState = MODE_REPLACE;
s->cmdchar_todo = s->cmdchar;
// Remember whether editing was restarted after CTRL-O
did_restart_edit = restart_edit;
// sleep before redrawing, needed for "CTRL-O :" that results in an
// error message
msg_check_for_delay(true);
// set Insstart_orig to Insstart
update_Insstart_orig = true;
ins_compl_clear(); // clear stuff for CTRL-X mode
// Trigger InsertEnter autocommands. Do not do this for "r<CR>" or "grx".
if (s->cmdchar != 'r' && s->cmdchar != 'v') {
pos_T save_cursor = curwin->w_cursor;
const char *const ptr = s->cmdchar == 'R' ? "r" : s->cmdchar == 'V' ? "v" : "i";
set_vim_var_string(VV_INSERTMODE, ptr, 1);
set_vim_var_string(VV_CHAR, NULL, -1);
ins_apply_autocmds(EVENT_INSERTENTER);
// Check for changed highlighting, e.g. for ModeMsg.
if (need_highlight_changed) {
highlight_changed();
}
// Make sure the cursor didn't move. Do call check_cursor_col() in
// case the text was modified. Since Insert mode was not started yet
// a call to check_cursor_col() may move the cursor, especially with
// the "A" command, thus set State to avoid that. Also check that the
// line number is still valid (lines may have been deleted).
// Do not restore if v:char was set to a non-empty string.
if (!equalpos(curwin->w_cursor, save_cursor)
&& *get_vim_var_str(VV_CHAR) == NUL
&& save_cursor.lnum <= curbuf->b_ml.ml_line_count) {
int save_state = State;
curwin->w_cursor = save_cursor;
State = MODE_INSERT;
check_cursor_col(curwin);
State = save_state;
}
}
// When doing a paste with the middle mouse button, Insstart is set to
// where the paste started.
if (where_paste_started.lnum != 0) {
Insstart = where_paste_started;
} else {
Insstart = curwin->w_cursor;
if (s->startln) {
Insstart.col = 0;
}
}
Insstart_textlen = linetabsize_str(get_cursor_line_ptr());
Insstart_blank_vcol = MAXCOL;
if (!did_ai) {
ai_col = 0;
}
if (s->cmdchar != NUL && restart_edit == 0) {
ResetRedobuff();
AppendNumberToRedobuff(s->count);
if (s->cmdchar == 'V' || s->cmdchar == 'v') {
// "gR" or "gr" command
AppendCharToRedobuff('g');
AppendCharToRedobuff((s->cmdchar == 'v') ? 'r' : 'R');
} else {
AppendCharToRedobuff(s->cmdchar);
if (s->cmdchar == 'g') { // "gI" command
AppendCharToRedobuff('I');
} else if (s->cmdchar == 'r') { // "r<CR>" command
s->count = 1; // insert only one <CR>
}
}
}
if (s->cmdchar == 'R') {
State = MODE_REPLACE;
} else if (s->cmdchar == 'V' || s->cmdchar == 'v') {
State = MODE_VREPLACE;
s->replaceState = MODE_VREPLACE;
orig_line_count = curbuf->b_ml.ml_line_count;
vr_lines_changed = 1;
} else {
State = MODE_INSERT;
}
may_trigger_modechanged();
stop_insert_mode = false;
// need to position cursor again when on a TAB and
// when on a char with inline virtual text
if (gchar_cursor() == TAB || buf_meta_total(curbuf, kMTMetaInline) > 0) {
curwin->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_VIRTCOL);
}
// Enable langmap or IME, indicated by 'iminsert'.
// Note that IME may enabled/disabled without us noticing here, thus the
// 'iminsert' value may not reflect what is actually used. It is updated
// when hitting <Esc>.
if (curbuf->b_p_iminsert == B_IMODE_LMAP) {
State |= MODE_LANGMAP;
}
setmouse();
clear_showcmd();
// there is no reverse replace mode
revins_on = (State == MODE_INSERT && p_ri);
if (revins_on) {
undisplay_dollar();
}
revins_chars = 0;
revins_legal = 0;
revins_scol = -1;
// Handle restarting Insert mode.
// Don't do this for "CTRL-O ." (repeat an insert): we get here with
// restart_edit non-zero, and something in the stuff buffer.
if (restart_edit != 0 && stuff_empty()) {
// After a paste we consider text typed to be part of the insert for
// the pasted text. You can backspace over the pasted text too.
arrow_used = where_paste_started.lnum == 0;
restart_edit = 0;
// If the cursor was after the end-of-line before the CTRL-O and it is
// now at the end-of-line, put it after the end-of-line (this is not
// correct in very rare cases).
// Also do this if curswant is greater than the current virtual
// column. Eg after "^O$" or "^O80|".
validate_virtcol(curwin);
update_curswant();
const char *ptr;
if (((ins_at_eol && curwin->w_cursor.lnum == o_lnum)
|| curwin->w_curswant > curwin->w_virtcol)
&& *(ptr = get_cursor_line_ptr() + curwin->w_cursor.col) != NUL) {
if (ptr[1] == NUL) {
curwin->w_cursor.col++;
} else {
s->i = utfc_ptr2len(ptr);
if (ptr[s->i] == NUL) {
curwin->w_cursor.col += s->i;
}
}
}
ins_at_eol = false;
} else {
arrow_used = false;
}
// we are in insert mode now, don't need to start it anymore
need_start_insertmode = false;
// Need to save the line for undo before inserting the first char.
ins_need_undo = true;
where_paste_started.lnum = 0;
can_cindent = true;
// The cursor line is not in a closed fold, unless restarting.
if (did_restart_edit == 0) {
foldOpenCursor();
}
// If 'showmode' is set, show the current (insert/replace/..) mode.
// A warning message for changing a readonly file is given here, before
// actually changing anything. It's put after the mode, if any.
s->i = 0;
if (p_smd && msg_silent == 0) {
s->i = showmode();
}
if (did_restart_edit == 0) {
change_warning(curbuf, s->i == 0 ? 0 : s->i + 1);
}
ui_cursor_shape(); // may show different cursor shape
do_digraph(-1); // clear digraphs
// Get the current length of the redo buffer, those characters have to be
// skipped if we want to get to the inserted characters.
String inserted = get_inserted();
new_insert_skip = (int)inserted.size;
if (inserted.data != NULL) {
xfree(inserted.data);
}
old_indent = 0;
do {
state_enter(&s->state);
// If s->count != 0, `ins_esc` will prepare the redo buffer for reprocessing
// and return false, causing `state_enter` to be called again.
} while (!ins_esc(&s->count, s->cmdchar, s->nomove));
// Always update o_lnum, so that a "CTRL-O ." that adds a line
// still puts the cursor back after the inserted text.
if (ins_at_eol) {
o_lnum = curwin->w_cursor.lnum;
}
pum_check_clear();
foldUpdateAfterInsert();
// When CTRL-C was typed got_int will be set, with the result
// that the autocommands won't be executed. When mapped got_int
// is not set, but let's keep the behavior the same.
if (s->cmdchar != 'r' && s->cmdchar != 'v' && s->c != Ctrl_C) {
ins_apply_autocmds(EVENT_INSERTLEAVE);
}
did_cursorhold = false;
// ins_redraw() triggers TextChangedI only when no characters
// are in the typeahead buffer, so reset curbuf->b_last_changedtick
// if the TextChangedI was not blocked by char_avail() (e.g. using :norm!)
// and the TextChangedI autocommand has been triggered.
if (!char_avail() && curbuf->b_last_changedtick_i == buf_get_changedtick(curbuf)) {
curbuf->b_last_changedtick = buf_get_changedtick(curbuf);
}
}
static int insert_check(VimState *state)
{
InsertState *s = (InsertState *)state;
// If typed something may trigger CursorHoldI again.
if (s->c != K_EVENT
// but not in CTRL-X mode, a script can't restore the state
&& ctrl_x_mode_normal()) {
did_cursorhold = false;
}
// Check if we need to cancel completion mode because the window
// or tab page was changed
if (ins_compl_active() && !ins_compl_win_active(curwin)) {
ins_compl_cancel();
}
// If the cursor was moved we didn't just insert a space
if (arrow_used) {
s->inserted_space = false;
}
if (can_cindent
&& cindent_on()
&& ctrl_x_mode_normal()
&& !ins_compl_active()) {
insert_do_cindent(s);
}
if (!revins_legal) {
revins_scol = -1; // reset on illegal motions
} else {
revins_legal = 0;
}
if (arrow_used) { // don't repeat insert when arrow key used
s->count = 0;
}
if (update_Insstart_orig) {
Insstart_orig = Insstart;
}
if (curbuf->terminal && !stop_insert_mode) {
// Exit Insert mode and go to Terminal mode.
stop_insert_mode = true;
restart_edit = 'I';
stuffcharReadbuff(K_NOP);
}
if (stop_insert_mode && !ins_compl_active()) {
// ":stopinsert" used
s->count = 0;
return 0; // exit insert mode
}
// set curwin->w_curswant for next K_DOWN or K_UP
if (!arrow_used) {
curwin->w_set_curswant = true;
}
// If there is no typeahead may check for timestamps (e.g., for when a
// menu invoked a shell command).
if (stuff_empty()) {
did_check_timestamps = false;
if (need_check_timestamps) {
check_timestamps(false);
}
}
// When emsg() was called msg_scroll will have been set.
msg_scroll = false;
// Open fold at the cursor line, according to 'foldopen'.
if (fdo_flags & kOptFdoFlagInsert) {
foldOpenCursor();
}
// Close folds where the cursor isn't, according to 'foldclose'
if (!char_avail()) {
foldCheckClose();
}
if (bt_prompt(curbuf)) {
init_prompt(s->cmdchar_todo);
s->cmdchar_todo = NUL;
}
// If we inserted a character at the last position of the last line in the
// window, scroll the window one line up. This avoids an extra redraw. This
// is detected when the cursor column is smaller after inserting something.
// Don't do this when the topline changed already, it has already been
// adjusted (by insertchar() calling open_line())).
// Also don't do this when 'smoothscroll' is set, as the window should then
// be scrolled by screen lines.
if (curbuf->b_mod_set
&& curwin->w_p_wrap
&& !curwin->w_p_sms
&& !s->did_backspace
&& curwin->w_topline == s->old_topline
&& curwin->w_topfill == s->old_topfill
&& s->count <= 1) {
s->mincol = curwin->w_wcol;
validate_cursor_col(curwin);
if (curwin->w_wcol < s->mincol - tabstop_at(get_nolist_virtcol(),
curbuf->b_p_ts,
curbuf->b_p_vts_array,
false)
&& curwin->w_wrow == curwin->w_view_height - 1 - get_scrolloff_value(curwin)
&& (curwin->w_cursor.lnum != curwin->w_topline
|| curwin->w_topfill > 0)) {
if (curwin->w_topfill > 0) {
curwin->w_topfill--;
} else if (hasFolding(curwin, curwin->w_topline, NULL, &s->old_topline)) {
set_topline(curwin, s->old_topline + 1);
} else {
set_topline(curwin, curwin->w_topline + 1);
}
}
}
// May need to adjust w_topline to show the cursor.
if (s->count <= 1) {
update_topline(curwin);
}
s->did_backspace = false;
if (s->count <= 1) {
validate_cursor(curwin); // may set must_redraw
}
// Redraw the display when no characters are waiting.
// Also shows mode, ruler and positions cursor.
ins_redraw(true);
if (curwin->w_p_scb) {
do_check_scrollbind(true);
}
if (curwin->w_p_crb) {
do_check_cursorbind();
}
if (s->count <= 1) {
update_curswant();
}
s->old_topline = curwin->w_topline;
s->old_topfill = curwin->w_topfill;
if (s->c != K_EVENT) {
s->lastc = s->c; // remember previous char for CTRL-D
}
// After using CTRL-G U the next cursor key will not break undo.
if (dont_sync_undo == kNone) {
dont_sync_undo = kTrue;
} else {
dont_sync_undo = kFalse;
}
return 1;
}
static int insert_execute(VimState *state, int key)
{
InsertState *const s = (InsertState *)state;
if (stop_insert_mode) {
// Insert mode ended, possibly from a callback.
if (key != K_IGNORE && key != K_NOP) {
vungetc(key);
}
s->count = 0;
s->nomove = true;
ins_compl_prep(ESC);
return 0;
}
if (key == K_IGNORE || key == K_NOP) {
return -1; // get another key
}
s->c = key;
// Don't want K_EVENT with cursorhold for the second key, e.g., after CTRL-V.
if (key != K_EVENT) {
did_cursorhold = true;
}
// Special handling of keys while the popup menu is visible or wanted
// and the cursor is still in the completed word. Only when there is
// a match, skip this when no matches were found.
if (ins_compl_active() && curwin->w_cursor.col >= ins_compl_col()
&& ins_compl_has_shown_match() && pum_wanted()) {
// BS: Delete one character from "compl_leader".
if ((s->c == K_BS || s->c == Ctrl_H)
&& curwin->w_cursor.col > ins_compl_col()
&& (s->c = ins_compl_bs()) == NUL) {
return 1; // continue
}
// When no match was selected or it was edited.
if (!ins_compl_used_match()) {
// CTRL-L: Add one character from the current match to
// "compl_leader". Except when at the original match and
// there is nothing to add, CTRL-L works like CTRL-P then.
if (s->c == Ctrl_L
&& (!ctrl_x_mode_line_or_eval()
|| ins_compl_long_shown_match())) {
ins_compl_addfrommatch();
return 1; // continue
}
// A non-white character that fits in with the current
// completion: Add to "compl_leader".
if (ins_compl_accept_char(s->c)) {
// Trigger InsertCharPre.
char *str = do_insert_char_pre(s->c);
if (str != NULL) {
for (char *p = str; *p != NUL; MB_PTR_ADV(p)) {
ins_compl_addleader(utf_ptr2char(p));
}
xfree(str);
} else {
ins_compl_addleader(s->c);
}
return 1; // continue
}
// Pressing CTRL-Y selects the current match. When
// compl_enter_selects is set the Enter key does the same.
if ((s->c == Ctrl_Y
|| (ins_compl_enter_selects()
&& (s->c == CAR || s->c == K_KENTER || s->c == NL)))
&& stop_arrow() == OK) {
ins_compl_delete(false);
ins_compl_insert(false);
} else if (ascii_iswhite_nl_or_nul(s->c) && ins_compl_preinsert_effect()) {
// Delete preinserted text when typing special chars
ins_compl_delete(false);
}
}
}
// Prepare for or stop CTRL-X mode. This doesn't do completion, but it does
// fix up the text when finishing completion.
ins_compl_init_get_longest();
if (ins_compl_prep(s->c)) {
return 1; // continue
}
// CTRL-\ CTRL-N goes to Normal mode,
// CTRL-\ CTRL-O is like CTRL-O but without moving the cursor
if (s->c == Ctrl_BSL) {
// may need to redraw when no more chars available now
ins_redraw(false);
no_mapping++;
allow_keys++;
s->c = plain_vgetc();
no_mapping--;
allow_keys--;
if (s->c != Ctrl_N && s->c != Ctrl_G && s->c != Ctrl_O) {
// it's something else
vungetc(s->c);
s->c = Ctrl_BSL;
} else {
if (s->c == Ctrl_O) {
ins_ctrl_o();
ins_at_eol = false; // cursor keeps its column
s->nomove = true;
}
s->count = 0;
return 0;
}
}
if (s->c != K_EVENT) {
s->c = do_digraph(s->c);
}
if ((s->c == Ctrl_V || s->c == Ctrl_Q) && ctrl_x_mode_cmdline()) {
insert_do_complete(s);
return 1;
}
if (s->c == Ctrl_V || s->c == Ctrl_Q) {
ins_ctrl_v();
s->c = Ctrl_V; // pretend CTRL-V is last typed character
return 1; // continue
}
if (cindent_on() && ctrl_x_mode_none()) {
s->line_is_white = inindent(0);
// A key name preceded by a bang means this key is not to be
// inserted. Skip ahead to the re-indenting below.
if (in_cinkeys(s->c, '!', s->line_is_white)
&& stop_arrow() == OK) {
do_c_expr_indent();
return 1; // continue
}
// A key name preceded by a star means that indenting has to be
// done before inserting the key.
if (can_cindent && in_cinkeys(s->c, '*', s->line_is_white)
&& stop_arrow() == OK) {
do_c_expr_indent();
}
}
if (curwin->w_p_rl) {
switch (s->c) {
case K_LEFT:
s->c = K_RIGHT; break;
case K_S_LEFT:
s->c = K_S_RIGHT; break;
case K_C_LEFT:
s->c = K_C_RIGHT; break;
case K_RIGHT:
s->c = K_LEFT; break;
case K_S_RIGHT:
s->c = K_S_LEFT; break;
case K_C_RIGHT:
s->c = K_C_LEFT; break;
}
}
// If 'keymodel' contains "startsel", may start selection. If it
// does, a CTRL-O and c will be stuffed, we need to get these
// characters.
if (ins_start_select(s->c)) {
return 1; // continue
}
return insert_handle_key(s);
}
static int insert_handle_key(InsertState *s)
{
// The big switch to handle a character in insert mode.
// TODO(tarruda): This could look better if a lookup table is used.
// (similar to normal mode `nv_cmds[]`)
switch (s->c) {
case ESC: // End input mode
if (echeck_abbr(ESC + ABBR_OFF)) {
break;
}
FALLTHROUGH;
case Ctrl_C: // End input mode
if (s->c == Ctrl_C && cmdwin_type != 0) {
// Close the cmdline window.
cmdwin_result = K_IGNORE;
got_int = false; // don't stop executing autocommands et al
s->nomove = true;
return 0; // exit insert mode
}
if (s->c == Ctrl_C && bt_prompt(curbuf)) {
if (invoke_prompt_interrupt()) {
if (!bt_prompt(curbuf)) {
// buffer changed to a non-prompt buffer, get out of
// Insert mode
return 0;
}
break;
}
}
return 0; // exit insert mode
case Ctrl_Z:
goto normalchar; // insert CTRL-Z as normal char
case Ctrl_O: // execute one command
if (ctrl_x_mode_omni()) {
insert_do_complete(s);
break;
}
if (echeck_abbr(Ctrl_O + ABBR_OFF)) {
break;
}
ins_ctrl_o();
// don't move the cursor left when 'virtualedit' has "onemore".
if (get_ve_flags(curwin) & kOptVeFlagOnemore) {
ins_at_eol = false;
s->nomove = true;
}
s->count = 0;
return 0; // exit insert mode
case K_INS: // toggle insert/replace mode
case K_KINS:
ins_insert(s->replaceState);
break;
case K_SELECT: // end of Select mode mapping - ignore
break;
case K_HELP: // Help key works like <ESC> <Help>
case K_F1:
case K_XF1:
stuffcharReadbuff(K_HELP);
return 0; // exit insert mode
case ' ':
if (mod_mask != MOD_MASK_CTRL) {
goto normalchar;
}
FALLTHROUGH;
case K_ZERO: // Insert the previously inserted text.
case NUL:
case Ctrl_A:
// For ^@ the trailing ESC will end the insert, unless there is an
// error.
if (stuff_inserted(NUL, 1, (s->c == Ctrl_A)) == FAIL
&& s->c != Ctrl_A) {
return 0; // exit insert mode
}
s->inserted_space = false;
break;
case Ctrl_R: // insert the contents of a register
if (ctrl_x_mode_register() && !ins_compl_active()) {
insert_do_complete(s);
break;
}
ins_reg();
auto_format(false, true);
s->inserted_space = false;
break;
case Ctrl_G: // commands starting with CTRL-G
ins_ctrl_g();
break;
case Ctrl_HAT: // switch input mode and/or langmap
ins_ctrl_hat();
break;
case Ctrl__: // switch between languages
if (!p_ari) {
goto normalchar;
}
ins_ctrl_();
break;
case Ctrl_D: // Make indent one shiftwidth smaller.
if (ctrl_x_mode_path_defines()) {
insert_do_complete(s);
break;
}
FALLTHROUGH;
case Ctrl_T: // Make indent one shiftwidth greater.
if (s->c == Ctrl_T && ctrl_x_mode_thesaurus()) {
if (check_compl_option(false)) {
insert_do_complete(s);
}
break;
}
ins_shift(s->c, s->lastc);
auto_format(false, true);
s->inserted_space = false;
break;
case K_DEL: // delete character under the cursor
case K_KDEL:
ins_del();
auto_format(false, true);
break;
case K_BS: // delete character before the cursor
case Ctrl_H:
s->did_backspace = ins_bs(s->c, BACKSPACE_CHAR, &s->inserted_space);
auto_format(false, true);
break;
case Ctrl_W: // delete word before the cursor
if (bt_prompt(curbuf) && (mod_mask & MOD_MASK_SHIFT) == 0) {
// In a prompt window CTRL-W is used for window commands.
// Use Shift-CTRL-W to delete a word.
stuffcharReadbuff(Ctrl_W);
restart_edit = 'A';
s->nomove = true;
s->count = 0;
return 0;
}
s->did_backspace = ins_bs(s->c, BACKSPACE_WORD, &s->inserted_space);
auto_format(false, true);
break;
case Ctrl_U: // delete all inserted text in current line
// CTRL-X CTRL-U completes with 'completefunc'.
if (ctrl_x_mode_function()) {
insert_do_complete(s);
} else {
s->did_backspace = ins_bs(s->c, BACKSPACE_LINE, &s->inserted_space);
auto_format(false, true);
s->inserted_space = false;
}
break;
case K_LEFTMOUSE: // mouse keys
case K_LEFTMOUSE_NM:
case K_LEFTDRAG:
case K_LEFTRELEASE:
case K_LEFTRELEASE_NM:
case K_MOUSEMOVE:
case K_MIDDLEMOUSE:
case K_MIDDLEDRAG:
case K_MIDDLERELEASE:
case K_RIGHTMOUSE:
case K_RIGHTDRAG:
case K_RIGHTRELEASE:
case K_X1MOUSE:
case K_X1DRAG:
case K_X1RELEASE:
case K_X2MOUSE:
case K_X2DRAG:
case K_X2RELEASE:
ins_mouse(s->c);
break;
case K_MOUSEDOWN: // Default action for scroll wheel up: scroll up
ins_mousescroll(MSCR_DOWN);
break;
case K_MOUSEUP: // Default action for scroll wheel down: scroll down
ins_mousescroll(MSCR_UP);
break;
case K_MOUSELEFT: // Scroll wheel left
ins_mousescroll(MSCR_LEFT);
break;
case K_MOUSERIGHT: // Scroll wheel right
ins_mousescroll(MSCR_RIGHT);
break;
case K_IGNORE: // Something mapped to nothing
break;
case K_PASTE_START:
paste_repeat(1);
goto check_pum;
case K_EVENT: // some event
state_handle_k_event();
// If CTRL-G U was used apply it to the next typed key.
if (dont_sync_undo == kTrue) {
dont_sync_undo = kNone;
}
goto check_pum;
case K_COMMAND: // <Cmd>command<CR>
do_cmdline(NULL, getcmdkeycmd, NULL, 0);
goto check_pum;
case K_LUA:
map_execute_lua(false);
check_pum:
// nvim_select_popupmenu_item() can be called from the handling of
// K_EVENT, K_COMMAND, or K_LUA.
// TODO(bfredl): Not entirely sure this indirection is necessary
// but doing like this ensures using nvim_select_popupmenu_item is
// equivalent to selecting the item with a typed key.
if (pum_want.active) {
if (pum_visible()) {
// Set this to NULL so that ins_complete() will update the message.
edit_submode_extra = NULL;
insert_do_complete(s);
if (pum_want.finish) {
// accept the item and stop completion
ins_compl_prep(Ctrl_Y);
}
}
pum_want.active = false;
}
if (curbuf->b_u_synced) {
// The K_EVENT, K_COMMAND, or K_LUA caused undo to be synced.
// Need to save the line for undo before inserting the next char.
ins_need_undo = true;
}
break;
case K_HOME: // <Home>
case K_KHOME:
case K_S_HOME:
case K_C_HOME:
ins_home(s->c);
break;
case K_END: // <End>
case K_KEND:
case K_S_END:
case K_C_END:
ins_end(s->c);
break;
case K_LEFT: // <Left>
if (mod_mask & (MOD_MASK_SHIFT|MOD_MASK_CTRL)) {
ins_s_left();
} else {
ins_left();
}
break;
case K_S_LEFT: // <S-Left>
case K_C_LEFT:
ins_s_left();
break;
case K_RIGHT: // <Right>
if (mod_mask & (MOD_MASK_SHIFT|MOD_MASK_CTRL)) {
ins_s_right();
} else {
ins_right();
}
break;
case K_S_RIGHT: // <S-Right>
case K_C_RIGHT:
ins_s_right();
break;
case K_UP: // <Up>
if (pum_visible()) {
insert_do_complete(s);
} else if (mod_mask & MOD_MASK_SHIFT) {
ins_pageup();
} else {
ins_up(false);
}
break;
case K_S_UP: // <S-Up>
case K_PAGEUP:
case K_KPAGEUP:
if (pum_visible()) {
insert_do_complete(s);
} else {
ins_pageup();
}
break;
case K_DOWN: // <Down>
if (pum_visible()) {
insert_do_complete(s);
} else if (mod_mask & MOD_MASK_SHIFT) {
ins_pagedown();
} else {
ins_down(false);
}
break;
case K_S_DOWN: // <S-Down>
case K_PAGEDOWN:
case K_KPAGEDOWN:
if (pum_visible()) {
insert_do_complete(s);
} else {
ins_pagedown();
}
break;
case K_S_TAB: // When not mapped, use like a normal TAB
s->c = TAB;
FALLTHROUGH;
case TAB: // TAB or Complete patterns along path
if (ctrl_x_mode_path_patterns()) {
insert_do_complete(s);
break;
}
s->inserted_space = false;
if (ins_tab()) {
goto normalchar; // insert TAB as a normal char
}
auto_format(false, true);
break;
case K_KENTER: // <Enter>
s->c = CAR;
FALLTHROUGH;
case CAR:
case NL:
// In a quickfix window a <CR> jumps to the error under the
// cursor.
if (bt_quickfix(curbuf) && s->c == CAR) {
if (curwin->w_llist_ref == NULL) { // quickfix window
do_cmdline_cmd(".cc");
} else { // location list window
do_cmdline_cmd(".ll");
}
break;
}
if (cmdwin_type != 0) {
// Execute the command in the cmdline window.
cmdwin_result = CAR;
return 0;
}
if ((mod_mask & MOD_MASK_SHIFT) == 0 && bt_prompt(curbuf)) {
prompt_invoke_callback();
if (!bt_prompt(curbuf)) {
// buffer changed to a non-prompt buffer, get out of
// Insert mode
return 0;
}
break;
}
if (!ins_eol(s->c)) {
return 0; // out of memory
}
auto_format(false, false);
s->inserted_space = false;
break;
case Ctrl_K: // digraph or keyword completion
if (ctrl_x_mode_dictionary()) {
if (check_compl_option(true)) {
insert_do_complete(s);
}
break;
}
s->c = ins_digraph();
if (s->c == NUL) {
break;
}
goto normalchar;
case Ctrl_X: // Enter CTRL-X mode
ins_ctrl_x();
break;
case Ctrl_RSB: // Tag name completion after ^X
if (!ctrl_x_mode_tags()) {
goto normalchar;
} else {
insert_do_complete(s);
}
break;
case Ctrl_F: // File name completion after ^X
if (!ctrl_x_mode_files()) {
goto normalchar;
} else {
insert_do_complete(s);
}
break;
case 's': // Spelling completion after ^X
case Ctrl_S:
if (!ctrl_x_mode_spell()) {
goto normalchar;
} else {
insert_do_complete(s);
}
break;
case Ctrl_L: // Whole line completion after ^X
if (!ctrl_x_mode_whole_line()) {
goto normalchar;
}
FALLTHROUGH;
case Ctrl_P: // Do previous/next pattern completion
case Ctrl_N:
// if 'complete' is empty then plain ^P is no longer special,
// but it is under other ^X modes
if (*curbuf->b_p_cpt == NUL
&& (ctrl_x_mode_normal() || ctrl_x_mode_whole_line())
&& !compl_status_local()) {
goto normalchar;
}
insert_do_complete(s);
break;
case Ctrl_Y: // copy from previous line or scroll down
case Ctrl_E: // copy from next line or scroll up
s->c = ins_ctrl_ey(s->c);
break;
default:
normalchar:
// Insert a normal character.
if (!p_paste) {
// Trigger InsertCharPre.
char *str = do_insert_char_pre(s->c);
if (str != NULL) {
if (*str != NUL && stop_arrow() != FAIL) {
// Insert the new value of v:char literally.
for (char *p = str; *p != NUL; MB_PTR_ADV(p)) {
s->c = utf_ptr2char(p);
if (s->c == CAR || s->c == K_KENTER || s->c == NL) {
ins_eol(s->c);
} else {
ins_char(s->c);
}
}
AppendToRedobuffLit(str, -1);
}
xfree(str);
s->c = NUL;
}
// If the new value is already inserted or an empty string
// then don't insert any character.
if (s->c == NUL) {
break;
}
}
// Try to perform smart-indenting.
ins_try_si(s->c);
if (s->c == ' ') {
s->inserted_space = true;
if (inindent(0)) {
can_cindent = false;
}
if (Insstart_blank_vcol == MAXCOL
&& curwin->w_cursor.lnum == Insstart.lnum) {
Insstart_blank_vcol = get_nolist_virtcol();
}
}
// Insert a normal character and check for abbreviations on a
// special character. Let CTRL-] expand abbreviations without
// inserting it.
if (vim_iswordc(s->c)
// Add ABBR_OFF for characters above 0x100, this is
// what check_abbr() expects.
|| (!echeck_abbr((s->c >= 0x100) ? (s->c + ABBR_OFF) : s->c)
&& s->c != Ctrl_RSB)) {
insert_special(s->c, false, false);
revins_legal++;
revins_chars++;
}
auto_format(false, true);
// When inserting a character the cursor line must never be in a
// closed fold.
foldOpenCursor();
break;
} // end of switch (s->c)
return 1; // continue
}
static void insert_do_complete(InsertState *s)
{
compl_busy = true;
disable_fold_update++; // don't redraw folds here
if (ins_complete(s->c, true) == FAIL) {
compl_status_clear();
}
disable_fold_update--;
compl_busy = false;
can_si = may_do_si(); // allow smartindenting
}
static void insert_do_cindent(InsertState *s)
{
// Indent now if a key was typed that is in 'cinkeys'.
if (in_cinkeys(s->c, ' ', s->line_is_white)) {
if (stop_arrow() == OK) {
// re-indent the current line
do_c_expr_indent();
}
}
}
/// edit(): Start inserting text.
///
/// "cmdchar" can be:
/// 'i' normal insert command
/// 'a' normal append command
/// 'R' replace command
/// 'r' "r<CR>" command: insert one <CR>.
/// Note: count can be > 1, for redo, but still only one <CR> is inserted.
/// <Esc> is not used for redo.
/// 'g' "gI" command.
/// 'V' "gR" command for Virtual Replace mode.
/// 'v' "gr" command for single character Virtual Replace mode.
///
/// This function is not called recursively. For CTRL-O commands, it returns
/// and lets the caller handle the Normal-mode command.
///
/// @param cmdchar command that started the insert
/// @param startln if true, insert at start of line
/// @param count repeat count for the command
///
/// @return true if a CTRL-O command caused the return (insert mode pending).
bool edit(int cmdchar, bool startln, int count)
{
if (curbuf->terminal) {
if (ex_normal_busy) {
// Do not enter terminal mode from ex_normal(), which would cause havoc
// (such as terminal-mode recursiveness). Instead set a flag to force-set
// the value of `restart_edit` before `ex_normal` returns.
restart_edit = 'i';
force_restart_edit = true;
return false;
}
return terminal_enter();
}
// Don't allow inserting in the sandbox.
if (sandbox != 0) {
emsg(_(e_sandbox));
return false;
}
// Don't allow changes in the buffer while editing the cmdline. The
// caller of getcmdline() may get confused.
// Don't allow recursive insert mode when busy with completion.
// Allow in dummy buffers since they are only used internally
if (textlock != 0 || ins_compl_active() || compl_busy || pum_visible()
|| expr_map_locked()) {
emsg(_(e_textlock));
return false;
}
InsertState s[1];
memset(s, 0, sizeof(InsertState));
s->state.execute = insert_execute;
s->state.check = insert_check;
s->cmdchar = cmdchar;
s->startln = startln;
s->count = count;
insert_enter(s);
return s->c == Ctrl_O;
}
bool ins_need_undo_get(void)
{
return ins_need_undo;
}
/// Redraw for Insert mode.
/// This is postponed until getting the next character to make '$' in the 'cpo'
/// option work correctly.
/// Only redraw when there are no characters available. This speeds up
/// inserting sequences of characters (e.g., for CTRL-R).
///
/// @param ready not busy with something
void ins_redraw(bool ready)
{
if (char_avail()) {
return;
}
// Trigger CursorMoved if the cursor moved. Not when the popup menu is
// visible, the command might delete it.
if (ready && has_event(EVENT_CURSORMOVEDI)
&& (last_cursormoved_win != curwin
|| !equalpos(last_cursormoved, curwin->w_cursor))
&& !pum_visible()) {
// Need to update the screen first, to make sure syntax
// highlighting is correct after making a change (e.g., inserting
// a "(". The autocommand may also require a redraw, so it's done
// again below, unfortunately.
if (syntax_present(curwin) && must_redraw) {
update_screen();
}
// Make sure curswant is correct, an autocommand may call
// getcurpos()
update_curswant();
ins_apply_autocmds(EVENT_CURSORMOVEDI);
last_cursormoved_win = curwin;
last_cursormoved = curwin->w_cursor;
}
// Trigger TextChangedI if changedtick_i differs.
if (ready && has_event(EVENT_TEXTCHANGEDI)
&& curbuf->b_last_changedtick_i != buf_get_changedtick(curbuf)
&& !pum_visible()) {
aco_save_T aco;
varnumber_T tick = buf_get_changedtick(curbuf);
// save and restore curwin and curbuf, in case the autocmd changes them
aucmd_prepbuf(&aco, curbuf);
apply_autocmds(EVENT_TEXTCHANGEDI, NULL, NULL, false, curbuf);
aucmd_restbuf(&aco);
curbuf->b_last_changedtick_i = buf_get_changedtick(curbuf);
if (tick != buf_get_changedtick(curbuf)) { // see ins_apply_autocmds()
u_save(curwin->w_cursor.lnum,
(linenr_T)(curwin->w_cursor.lnum + 1));
}
}
// Trigger TextChangedP if changedtick_pum differs. When the popupmenu
// closes TextChangedI will need to trigger for backwards compatibility,
// thus use different b_last_changedtick* variables.
if (ready && has_event(EVENT_TEXTCHANGEDP)
&& curbuf->b_last_changedtick_pum != buf_get_changedtick(curbuf)
&& pum_visible()) {
aco_save_T aco;
varnumber_T tick = buf_get_changedtick(curbuf);
// save and restore curwin and curbuf, in case the autocmd changes them
aucmd_prepbuf(&aco, curbuf);
apply_autocmds(EVENT_TEXTCHANGEDP, NULL, NULL, false, curbuf);
aucmd_restbuf(&aco);
curbuf->b_last_changedtick_pum = buf_get_changedtick(curbuf);
if (tick != buf_get_changedtick(curbuf)) { // see ins_apply_autocmds()
u_save(curwin->w_cursor.lnum,
(linenr_T)(curwin->w_cursor.lnum + 1));
}
}
if (ready) {
may_trigger_win_scrolled_resized();
}
// Trigger BufModified if b_changed_invalid is set.
if (ready && has_event(EVENT_BUFMODIFIEDSET)
&& curbuf->b_changed_invalid == true
&& !pum_visible()) {
apply_autocmds(EVENT_BUFMODIFIEDSET, NULL, NULL, false, curbuf);
curbuf->b_changed_invalid = false;
}
// Trigger SafeState if nothing is pending.
may_trigger_safestate(ready
&& !ins_compl_active()
&& !pum_visible());
pum_check_clear();
show_cursor_info_later(false);
if (must_redraw) {
update_screen();
} else {
redraw_statuslines();
if (clear_cmdline || redraw_cmdline || redraw_mode) {
showmode(); // clear cmdline and show mode
}
}
setcursor();
emsg_on_display = false; // may remove error message now
}
// Handle a CTRL-V or CTRL-Q typed in Insert mode.
static void ins_ctrl_v(void)
{
bool did_putchar = false;
// may need to redraw when no more chars available now
ins_redraw(false);
if (redrawing() && !char_avail()) {
edit_putchar('^', true);
did_putchar = true;
}
AppendToRedobuff(CTRL_V_STR);
add_to_showcmd_c(Ctrl_V);
// Do not include modifiers into the key for CTRL-SHIFT-V.
int c = get_literal(mod_mask & MOD_MASK_SHIFT);
if (did_putchar) {
// when the line fits in 'columns' the '^' is at the start of the next
// line and will not removed by the redraw
edit_unputchar();
}
clear_showcmd();
insert_special(c, true, true);
revins_chars++;
revins_legal++;
}
// Put a character directly onto the screen. It's not stored in a buffer.
// Used while handling CTRL-K, CTRL-V, etc. in Insert mode.
static int pc_status;
#define PC_STATUS_UNSET 0 // nothing was put on screen
#define PC_STATUS_RIGHT 1 // right half of double-wide char
#define PC_STATUS_LEFT 2 // left half of double-wide char
#define PC_STATUS_SET 3 // pc_schar was filled
static schar_T pc_schar; // saved char
static int pc_attr;
static int pc_row;
static int pc_col;
void edit_putchar(int c, bool highlight)
{
if (curwin->w_grid_alloc.chars == NULL && default_grid.chars == NULL) {
return;
}
int attr;
update_topline(curwin); // just in case w_topline isn't valid
validate_cursor(curwin);
if (highlight) {
attr = HL_ATTR(HLF_8);
} else {
attr = 0;
}
pc_row = curwin->w_wrow;
pc_status = PC_STATUS_UNSET;
grid_line_start(&curwin->w_grid, pc_row);
if (curwin->w_p_rl) {
pc_col = curwin->w_view_width - 1 - curwin->w_wcol;
if (grid_line_getchar(pc_col, NULL) == NUL) {
grid_line_put_schar(pc_col - 1, schar_from_ascii(' '), attr);
curwin->w_wcol--;
pc_status = PC_STATUS_RIGHT;
}
} else {
pc_col = curwin->w_wcol;
if (grid_line_getchar(pc_col + 1, NULL) == NUL) {
// pc_col is the left half of a double-width char
pc_status = PC_STATUS_LEFT;
}
}
// save the character to be able to put it back
if (pc_status == PC_STATUS_UNSET) {
pc_schar = grid_line_getchar(pc_col, &pc_attr);
pc_status = PC_STATUS_SET;
}
char buf[MB_MAXCHAR + 1];
grid_line_puts(pc_col, buf, utf_char2bytes(c, buf), attr);
grid_line_flush();
}
/// @return the effective prompt for the specified buffer.
char *buf_prompt_text(const buf_T *const buf)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE
{
if (buf->b_prompt_text == NULL) {
return "% ";
}
return buf->b_prompt_text;
}
/// @return the effective prompt for the current buffer.
char *prompt_text(void)
FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE
{
return buf_prompt_text(curbuf);
}
// Prepare for prompt mode: Make sure the last line has the prompt text.
// Move the cursor to this line.
static void init_prompt(int cmdchar_todo)
{
char *prompt = prompt_text();
if (curwin->w_cursor.lnum < curbuf->b_prompt_start.mark.lnum) {
curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count;
coladvance(curwin, MAXCOL);
}
char *text = get_cursor_line_ptr();
if ((curbuf->b_prompt_start.mark.lnum == curwin->w_cursor.lnum
&& strncmp(text, prompt, strlen(prompt)) != 0)
|| curbuf->b_prompt_start.mark.lnum > curwin->w_cursor.lnum) {
// prompt is missing, insert it or append a line with it
if (*text == NUL) {
ml_replace(curbuf->b_ml.ml_line_count, prompt, true);
} else {
ml_append(curbuf->b_ml.ml_line_count, prompt, 0, false);
}
curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count;
coladvance(curwin, MAXCOL);
inserted_bytes(curbuf->b_ml.ml_line_count, 0, 0, (colnr_T)strlen(prompt));
}
// Insert always starts after the prompt, allow editing text after it.
if (Insstart_orig.lnum != curbuf->b_prompt_start.mark.lnum
|| Insstart_orig.col != (colnr_T)strlen(prompt)) {
Insstart.lnum = curbuf->b_prompt_start.mark.lnum;
Insstart.col = (colnr_T)strlen(prompt);
Insstart_orig = Insstart;
Insstart_textlen = Insstart.col;
Insstart_blank_vcol = MAXCOL;
arrow_used = false;
}
if (cmdchar_todo == 'A') {
coladvance(curwin, MAXCOL);
}
if (curbuf->b_prompt_start.mark.lnum == curwin->w_cursor.lnum) {
curwin->w_cursor.col = MAX(curwin->w_cursor.col, (colnr_T)strlen(prompt));
}
// Make sure the cursor is in a valid position.
check_cursor(curwin);
}
/// @return true if the cursor is in the editable position of the prompt line.
bool prompt_curpos_editable(void)
FUNC_ATTR_PURE
{
return curwin->w_cursor.lnum > curbuf->b_prompt_start.mark.lnum
|| (curwin->w_cursor.lnum == curbuf->b_prompt_start.mark.lnum
&& curwin->w_cursor.col >= (int)strlen(prompt_text()));
}
// Undo the previous edit_putchar().
void edit_unputchar(void)
{
if (pc_status != PC_STATUS_UNSET) {
if (pc_status == PC_STATUS_RIGHT) {
curwin->w_wcol++;
}
if (pc_status == PC_STATUS_RIGHT || pc_status == PC_STATUS_LEFT) {
redrawWinline(curwin, curwin->w_cursor.lnum);
} else {
// TODO(bfredl): this could be smarter and also handle the dubyawidth case
grid_line_start(&curwin->w_grid, pc_row);
grid_line_put_schar(pc_col, pc_schar, pc_attr);
grid_line_flush();
}
}
}
/// Called when "$" is in 'cpoptions': display a '$' at the end of the changed
/// text. Only works when cursor is in the line that changes.
void display_dollar(colnr_T col_arg)
{
colnr_T col = MAX(col_arg, 0);
if (!redrawing()) {
return;
}
colnr_T save_col = curwin->w_cursor.col;
curwin->w_cursor.col = col;
// If on the last byte of a multi-byte move to the first byte.
char *p = get_cursor_line_ptr();
curwin->w_cursor.col -= utf_head_off(p, p + col);
curs_columns(curwin, false); // Recompute w_wrow and w_wcol
if (curwin->w_wcol < curwin->w_view_width) {
edit_putchar('$', false);
dollar_vcol = curwin->w_virtcol;
}
curwin->w_cursor.col = save_col;
}
// Call this function before moving the cursor from the normal insert position
// in insert mode.
void undisplay_dollar(void)
{
if (dollar_vcol < 0) {
return;
}
dollar_vcol = -1;
redrawWinline(curwin, curwin->w_cursor.lnum);
}
/// Insert an indent (for <Tab> or CTRL-T) or delete an indent (for CTRL-D).
/// Keep the cursor on the same character.
/// type == INDENT_INC increase indent (for CTRL-T or <Tab>)
/// type == INDENT_DEC decrease indent (for CTRL-D)
/// type == INDENT_SET set indent to "amount"
///
/// @param round if true, round the indent to 'shiftwidth' (only with _INC and _Dec).
/// @param call_changed_bytes call changed_bytes()
void change_indent(int type, int amount, int round, bool call_changed_bytes)
{
int insstart_less; // reduction for Insstart.col
colnr_T orig_col = 0; // init for GCC
char *orig_line = NULL; // init for GCC
// MODE_VREPLACE state needs to know what the line was like before changing
if (State & VREPLACE_FLAG) {
orig_line = xstrnsave(get_cursor_line_ptr(), (size_t)get_cursor_line_len());
orig_col = curwin->w_cursor.col;
}
// for the following tricks we don't want list mode
int save_p_list = curwin->w_p_list;
curwin->w_p_list = false;
colnr_T vc = getvcol_nolist(&curwin->w_cursor);
int vcol = vc;
// For Replace mode we need to fix the replace stack later, which is only
// possible when the cursor is in the indent. Remember the number of
// characters before the cursor if it's possible.
int start_col = curwin->w_cursor.col;
// determine offset from first non-blank
int new_cursor_col = curwin->w_cursor.col;
beginline(BL_WHITE);
new_cursor_col -= curwin->w_cursor.col;
insstart_less = curwin->w_cursor.col;
// If the cursor is in the indent, compute how many screen columns the
// cursor is to the left of the first non-blank.
if (new_cursor_col < 0) {
vcol = get_indent() - vcol;
}
if (new_cursor_col > 0) { // can't fix replace stack
start_col = -1;
}
// Set the new indent. The cursor will be put on the first non-blank.
if (type == INDENT_SET) {
set_indent(amount, call_changed_bytes ? SIN_CHANGED : 0);
} else {
int save_State = State;
// Avoid being called recursively.
if (State & VREPLACE_FLAG) {
State = MODE_INSERT;
}
shift_line(type == INDENT_DEC, round, 1, call_changed_bytes);
State = save_State;
}
insstart_less -= curwin->w_cursor.col;
// Try to put cursor on same character.
// If the cursor is at or after the first non-blank in the line,
// compute the cursor column relative to the column of the first
// non-blank character.
// If we are not in insert mode, leave the cursor on the first non-blank.
// If the cursor is before the first non-blank, position it relative
// to the first non-blank, counted in screen columns.
if (new_cursor_col >= 0) {
// When changing the indent while the cursor is touching it, reset
// Insstart_col to 0.
if (new_cursor_col == 0) {
insstart_less = MAXCOL;
}
new_cursor_col += curwin->w_cursor.col;
} else if (!(State & MODE_INSERT)) {
new_cursor_col = curwin->w_cursor.col;
} else {
// Compute the screen column where the cursor should be.
vcol = get_indent() - vcol;
int const end_vcol = (colnr_T)((vcol < 0) ? 0 : vcol);
curwin->w_virtcol = end_vcol;
// Advance the cursor until we reach the right screen column.
new_cursor_col = 0;
char *const line = get_cursor_line_ptr();
vcol = 0;
if (*line != NUL) {
CharsizeArg csarg;
CSType cstype = init_charsize_arg(&csarg, curwin, 0, line);
StrCharInfo ci = utf_ptr2StrCharInfo(line);
while (true) {
int next_vcol = vcol + win_charsize(cstype, vcol, ci.ptr, ci.chr.value, &csarg).width;
if (next_vcol > end_vcol) {
break;
}
vcol = next_vcol;
ci = utfc_next(ci);
if (*ci.ptr == NUL) {
break;
}
}
new_cursor_col = (int)(ci.ptr - line);
}
// May need to insert spaces to be able to position the cursor on
// the right screen column.
if (vcol != (int)curwin->w_virtcol) {
curwin->w_cursor.col = (colnr_T)new_cursor_col;
const size_t ptrlen = (size_t)(curwin->w_virtcol - vcol);
char *ptr = xmallocz(ptrlen);
memset(ptr, ' ', ptrlen);
new_cursor_col += (int)ptrlen;
ins_str(ptr, ptrlen);
xfree(ptr);
}
// When changing the indent while the cursor is in it, reset
// Insstart_col to 0.
insstart_less = MAXCOL;
}
curwin->w_p_list = save_p_list;
curwin->w_cursor.col = MAX(0, (colnr_T)new_cursor_col);
curwin->w_set_curswant = true;
changed_cline_bef_curs(curwin);
// May have to adjust the start of the insert.
if (State & MODE_INSERT) {
if (curwin->w_cursor.lnum == Insstart.lnum && Insstart.col != 0) {
if ((int)Insstart.col <= insstart_less) {
Insstart.col = 0;
} else {
Insstart.col -= insstart_less;
}
}
if ((int)ai_col <= insstart_less) {
ai_col = 0;
} else {
ai_col -= insstart_less;
}
}
// For MODE_REPLACE state, may have to fix the replace stack, if it's
// possible. If the number of characters before the cursor decreased, need
// to pop a few characters from the replace stack.
// If the number of characters before the cursor increased, need to push a
// few NULs onto the replace stack.
if (REPLACE_NORMAL(State) && start_col >= 0) {
while (start_col > (int)curwin->w_cursor.col) {
replace_join(0); // remove a NUL from the replace stack
start_col--;
}
while (start_col < (int)curwin->w_cursor.col) {
replace_push_nul();
start_col++;
}
}
// For MODE_VREPLACE state, we also have to fix the replace stack. In this
// case it is always possible because we backspace over the whole line and
// then put it back again the way we wanted it.
if (State & VREPLACE_FLAG) {
// Save new line
char *new_line = xstrnsave(get_cursor_line_ptr(), (size_t)get_cursor_line_len());
// We only put back the new line up to the cursor
new_line[curwin->w_cursor.col] = NUL;
int new_col = curwin->w_cursor.col;
// Put back original line
ml_replace(curwin->w_cursor.lnum, orig_line, false);
curwin->w_cursor.col = orig_col;
curbuf_splice_pending++;
// Backspace from cursor to start of line
backspace_until_column(0);
// Insert new stuff into line again
ins_bytes(new_line);
xfree(new_line);
curbuf_splice_pending--;
// TODO(bfredl): test for crazy edge cases, like we stand on a TAB or
// something? does this even do the right text change then?
int delta = orig_col - new_col;
extmark_splice_cols(curbuf, (int)curwin->w_cursor.lnum - 1, new_col,
delta < 0 ? -delta : 0,
delta > 0 ? delta : 0,
kExtmarkUndo);
}
}
/// Truncate the space at the end of a line. This is to be used only in an
/// insert mode. It handles fixing the replace stack for MODE_REPLACE and
/// MODE_VREPLACE modes.
void truncate_spaces(char *line, size_t len)
{
int i;
// find start of trailing white space
for (i = (int)len - 1; i >= 0 && ascii_iswhite(line[i]); i--) {
if (State & REPLACE_FLAG) {
replace_join(0); // remove a NUL from the replace stack
}
}
line[i + 1] = NUL;
}
/// Backspace the cursor until the given column. Handles MODE_REPLACE and
/// MODE_VREPLACE modes correctly. May also be used when not in insert mode at
/// all. Will attempt not to go before "col" even when there is a composing
/// character.
void backspace_until_column(int col)
{
while ((int)curwin->w_cursor.col > col) {
curwin->w_cursor.col--;
if (State & REPLACE_FLAG) {
replace_do_bs(col);
} else if (!del_char_after_col(col)) {
break;
}
}
}
/// Like del_char(), but make sure not to go before column "limit_col".
/// Only matters when there are composing characters.
///
/// @param limit_col only delete the character if it is after this column
//
/// @return true when something was deleted.
static bool del_char_after_col(int limit_col)
{
if (limit_col >= 0) {
colnr_T ecol = curwin->w_cursor.col + 1;
// Make sure the cursor is at the start of a character, but
// skip forward again when going too far back because of a
// composing character.
mb_adjust_cursor();
while (curwin->w_cursor.col < (colnr_T)limit_col) {
int l = utf_ptr2len(get_cursor_pos_ptr());
if (l == 0) { // end of line
break;
}
curwin->w_cursor.col += l;
}
if (*get_cursor_pos_ptr() == NUL || curwin->w_cursor.col == ecol) {
return false;
}
del_bytes(ecol - curwin->w_cursor.col, false, true);
} else {
del_char(false);
}
return true;
}
/// Next character is interpreted literally.
/// A one, two or three digit decimal number is interpreted as its byte value.
/// If one or two digits are entered, the next character is given to vungetc().
/// For Unicode a character > 255 may be returned.
///
/// @param no_simplify do not include modifiers into the key
int get_literal(bool no_simplify)
{
int nc;
bool hex = false;
bool octal = false;
int unicode = 0;
if (got_int) {
return Ctrl_C;
}
no_mapping++; // don't map the next key hits
int cc = 0;
int i = 0;
while (true) {
nc = plain_vgetc();
if (!no_simplify) {
nc = merge_modifiers(nc, &mod_mask);
}
if ((mod_mask & ~MOD_MASK_SHIFT) != 0) {
// A character with non-Shift modifiers should not be a valid
// character for i_CTRL-V_digit.
break;
}
if ((State & MODE_CMDLINE) == 0 && MB_BYTE2LEN_CHECK(nc) == 1) {
add_to_showcmd(nc);
}
if (nc == 'x' || nc == 'X') {
hex = true;
} else if (nc == 'o' || nc == 'O') {
octal = true;
} else if (nc == 'u' || nc == 'U') {
unicode = nc;
} else {
if (hex
|| unicode != 0) {
if (!ascii_isxdigit(nc)) {
break;
}
cc = cc * 16 + hex2nr(nc);
} else if (octal) {
if (nc < '0' || nc > '7') {
break;
}
cc = cc * 8 + nc - '0';
} else {
if (!ascii_isdigit(nc)) {
break;
}
cc = cc * 10 + nc - '0';
}
i++;
}
if (cc > 255
&& unicode == 0) {
cc = 255; // limit range to 0-255
}
nc = 0;
if (hex) { // hex: up to two chars
if (i >= 2) {
break;
}
} else if (unicode) { // Unicode: up to four or eight chars
if ((unicode == 'u' && i >= 4) || (unicode == 'U' && i >= 8)) {
break;
}
} else if (i >= 3) { // decimal or octal: up to three chars
break;
}
}
if (i == 0) { // no number entered
if (nc == K_ZERO) { // NUL is stored as NL
cc = '\n';
nc = 0;
} else {
cc = nc;
nc = 0;
}
}
if (cc == 0) { // NUL is stored as NL
cc = '\n';
}
no_mapping--;
if (nc) {
vungetc(nc);
// A character typed with i_CTRL-V_digit cannot have modifiers.
mod_mask = 0;
}
got_int = false; // CTRL-C typed after CTRL-V is not an interrupt
return cc;
}
/// Insert character, taking care of special keys and mod_mask
///
/// @param ctrlv `c` was typed after CTRL-V
static void insert_special(int c, int allow_modmask, int ctrlv)
{
// Special function key, translate into "<Key>". Up to the last '>' is
// inserted with ins_str(), so as not to replace characters in replace
// mode.
// Only use mod_mask for special keys, to avoid things like <S-Space>,
// unless 'allow_modmask' is true.
if (mod_mask & MOD_MASK_CMD) { // Command-key never produces a normal key.
allow_modmask = true;
}
if (IS_SPECIAL(c) || (mod_mask && allow_modmask)) {
char *p = get_special_key_name(c, mod_mask);
int len = (int)strlen(p);
c = (uint8_t)p[len - 1];
if (len > 2) {
if (stop_arrow() == FAIL) {
return;
}
p[len - 1] = NUL;
ins_str(p, (size_t)(len - 1));
AppendToRedobuffLit(p, -1);
ctrlv = false;
}
}
if (stop_arrow() == OK) {
insertchar(c, ctrlv ? INSCHAR_CTRLV : 0, -1);
}
}
// Special characters in this context are those that need processing other
// than the simple insertion that can be performed here. This includes ESC
// which terminates the insert, and CR/NL which need special processing to
// open up a new line. This routine tries to optimize insertions performed by
// the "redo", "undo" or "put" commands, so it needs to know when it should
// stop and defer processing to the "normal" mechanism.
// '0' and '^' are special, because they can be followed by CTRL-D.
#define ISSPECIAL(c) ((c) < ' ' || (c) >= DEL || (c) == '0' || (c) == '^')
/// "flags": INSCHAR_FORMAT - force formatting
/// INSCHAR_CTRLV - char typed just after CTRL-V
/// INSCHAR_NO_FEX - don't use 'formatexpr'
///
/// NOTE: passes the flags value straight through to internal_format() which,
/// beside INSCHAR_FORMAT (above), is also looking for these:
/// INSCHAR_DO_COM - format comments
/// INSCHAR_COM_LIST - format comments with num list or 2nd line indent
///
/// @param c character to insert or NUL
/// @param flags INSCHAR_FORMAT, etc.
/// @param second_indent indent for second line if >= 0
void insertchar(int c, int flags, int second_indent)
{
char *p;
int force_format = flags & INSCHAR_FORMAT;
const int textwidth = comp_textwidth(force_format);
const bool fo_ins_blank = has_format_option(FO_INS_BLANK);
// Try to break the line in two or more pieces when:
// - Always do this if we have been called to do formatting only.
// - Always do this when 'formatoptions' has the 'a' flag and the line
// ends in white space.
// - Otherwise:
// - Don't do this if inserting a blank
// - Don't do this if an existing character is being replaced, unless
// we're in MODE_VREPLACE state.
// - Do this if the cursor is not on the line where insert started
// or - 'formatoptions' doesn't have 'l' or the line was not too long
// before the insert.
// - 'formatoptions' doesn't have 'b' or a blank was inserted at or
// before 'textwidth'
if (textwidth > 0
&& (force_format
|| (!ascii_iswhite(c)
&& !((State & REPLACE_FLAG)
&& !(State & VREPLACE_FLAG)
&& *get_cursor_pos_ptr() != NUL)
&& (curwin->w_cursor.lnum != Insstart.lnum
|| ((!has_format_option(FO_INS_LONG)
|| Insstart_textlen <= (colnr_T)textwidth)
&& (!fo_ins_blank
|| Insstart_blank_vcol <= (colnr_T)textwidth)))))) {
// Format with 'formatexpr' when it's set. Use internal formatting
// when 'formatexpr' isn't set or it returns non-zero.
bool do_internal = true;
colnr_T virtcol = get_nolist_virtcol()
+ char2cells(c != NUL ? c : gchar_cursor());
if (*curbuf->b_p_fex != NUL && (flags & INSCHAR_NO_FEX) == 0
&& (force_format || virtcol > (colnr_T)textwidth)) {
do_internal = (fex_format(curwin->w_cursor.lnum, 1, c) != 0);
// It may be required to save for undo again, e.g. when setline()
// was called.
ins_need_undo = true;
}
if (do_internal) {
internal_format(textwidth, second_indent, flags, c == NUL, c);
}
}
if (c == NUL) { // only formatting was wanted
return;
}
// Check whether this character should end a comment.
if (did_ai && c == end_comment_pending) {
char lead_end[COM_MAX_LEN]; // end-comment string
// Need to remove existing (middle) comment leader and insert end
// comment leader. First, check what comment leader we can find.
char *line = get_cursor_line_ptr();
int i = get_leader_len(line, &p, false, true);
if (i > 0 && vim_strchr(p, COM_MIDDLE) != NULL) { // Just checking
// Skip middle-comment string
while (*p && p[-1] != ':') { // find end of middle flags
p++;
}
int middle_len = (int)copy_option_part(&p, lead_end, COM_MAX_LEN, ",");
// Don't count trailing white space for middle_len
while (middle_len > 0 && ascii_iswhite(lead_end[middle_len - 1])) {
middle_len--;
}
// Find the end-comment string
while (*p && p[-1] != ':') { // find end of end flags
p++;
}
int end_len = (int)copy_option_part(&p, lead_end, COM_MAX_LEN, ",");
// Skip white space before the cursor
i = curwin->w_cursor.col;
while (--i >= 0 && ascii_iswhite(line[i])) {}
i++;
// Skip to before the middle leader
i -= middle_len;
// Check some expected things before we go on
if (i >= 0 && end_len > 0
&& (uint8_t)lead_end[end_len - 1] == end_comment_pending) {
// Backspace over all the stuff we want to replace
backspace_until_column(i);
// Insert the end-comment string, except for the last
// character, which will get inserted as normal later.
ins_bytes_len(lead_end, (size_t)(end_len - 1));
}
}
}
end_comment_pending = NUL;
did_ai = false;
did_si = false;
can_si = false;
can_si_back = false;
// If there's any pending input, grab up to INPUT_BUFLEN at once.
// This speeds up normal text input considerably.
// Don't do this when 'cindent' or 'indentexpr' is set, because we might
// need to re-indent at a ':', or any other character (but not what
// 'paste' is set)..
// Don't do this when there an InsertCharPre autocommand is defined,
// because we need to fire the event for every character.
// Do the check for InsertCharPre before the call to vpeekc() because the
// InsertCharPre autocommand could change the input buffer.
if (!ISSPECIAL(c)
&& (utf_char2len(c) == 1)
&& !has_event(EVENT_INSERTCHARPRE)
&& vpeekc() != NUL
&& !(State & REPLACE_FLAG)
&& !cindent_on()
&& !p_ri) {
#define INPUT_BUFLEN 100
char buf[INPUT_BUFLEN + 1];
colnr_T virtcol = 0;
buf[0] = (char)c;
int i = 1;
if (textwidth > 0) {
virtcol = get_nolist_virtcol();
}
// Stop the string when:
// - no more chars available
// - finding a special character (command key)
// - buffer is full
// - running into the 'textwidth' boundary
// - need to check for abbreviation: A non-word char after a word-char
while ((c = vpeekc()) != NUL
&& !ISSPECIAL(c)
&& MB_BYTE2LEN(c) == 1
&& i < INPUT_BUFLEN
&& (textwidth == 0
|| (virtcol += byte2cells((uint8_t)buf[i - 1])) < (colnr_T)textwidth)
&& !(!no_abbr && !vim_iswordc(c) && vim_iswordc((uint8_t)buf[i - 1]))) {
c = vgetc();
buf[i++] = (char)c;
}
do_digraph(-1); // clear digraphs
do_digraph((uint8_t)buf[i - 1]); // may be the start of a digraph
buf[i] = NUL;
ins_str(buf, (size_t)i);
if (flags & INSCHAR_CTRLV) {
redo_literal((uint8_t)(*buf));
i = 1;
} else {
i = 0;
}
if (buf[i] != NUL) {
AppendToRedobuffLit(buf + i, -1);
}
} else {
int cc;
if ((cc = utf_char2len(c)) > 1) {
char buf[MB_MAXCHAR + 1];
utf_char2bytes(c, buf);
buf[cc] = NUL;
ins_char_bytes(buf, (size_t)cc);
AppendCharToRedobuff(c);
} else {
ins_char(c);
if (flags & INSCHAR_CTRLV) {
redo_literal(c);
} else {
AppendCharToRedobuff(c);
}
}
}
}
// Put a character in the redo buffer, for when just after a CTRL-V.
static void redo_literal(int c)
{
char buf[10];
// Only digits need special treatment. Translate them into a string of
// three digits.
if (ascii_isdigit(c)) {
vim_snprintf(buf, sizeof(buf), "%03d", c);
AppendToRedobuff(buf);
} else {
AppendCharToRedobuff(c);
}
}
/// start_arrow() is called when an arrow key is used in insert mode.
/// For undo/redo it resembles hitting the <ESC> key.
///
/// @param end_insert_pos can be NULL
void start_arrow(pos_T *end_insert_pos)
{
start_arrow_common(end_insert_pos, true);
}
/// Like start_arrow() but with end_change argument.
/// Will prepare for redo of CTRL-G U if "end_change" is false.
///
/// @param end_insert_pos can be NULL
/// @param end_change end undoable change
static void start_arrow_with_change(pos_T *end_insert_pos, bool end_change)
{
start_arrow_common(end_insert_pos, end_change);
if (!end_change) {
AppendCharToRedobuff(Ctrl_G);
AppendCharToRedobuff('U');
}
}
/// @param end_insert_pos can be NULL
/// @param end_change end undoable change
static void start_arrow_common(pos_T *end_insert_pos, bool end_change)
{
if (!arrow_used && end_change) { // something has been inserted
AppendToRedobuff(ESC_STR);
stop_insert(end_insert_pos, false, false);
arrow_used = true; // This means we stopped the current insert.
}
check_spell_redraw();
}
// If we skipped highlighting word at cursor, do it now.
// It may be skipped again, thus reset spell_redraw_lnum first.
static void check_spell_redraw(void)
{
if (spell_redraw_lnum != 0) {
linenr_T lnum = spell_redraw_lnum;
spell_redraw_lnum = 0;
redrawWinline(curwin, lnum);
}
}
// stop_arrow() is called before a change is made in insert mode.
// If an arrow key has been used, start a new insertion.
// Returns FAIL if undo is impossible, shouldn't insert then.
int stop_arrow(void)
{
if (arrow_used) {
Insstart = curwin->w_cursor; // new insertion starts here
if (Insstart.col > Insstart_orig.col && !ins_need_undo) {
// Don't update the original insert position when moved to the
// right, except when nothing was inserted yet.
update_Insstart_orig = false;
}
Insstart_textlen = linetabsize_str(get_cursor_line_ptr());
if (u_save_cursor() == OK) {
arrow_used = false;
ins_need_undo = false;
}
ai_col = 0;
if (State & VREPLACE_FLAG) {
orig_line_count = curbuf->b_ml.ml_line_count;
vr_lines_changed = 1;
}
ResetRedobuff();
AppendToRedobuff("1i"); // Pretend we start an insertion.
new_insert_skip = 2;
} else if (ins_need_undo) {
if (u_save_cursor() == OK) {
ins_need_undo = false;
}
}
// Always open fold at the cursor line when inserting something.
foldOpenCursor();
return arrow_used || ins_need_undo ? FAIL : OK;
}
/// Do a few things to stop inserting.
/// "end_insert_pos" is where insert ended. It is NULL when we already jumped
/// to another window/buffer.
///
/// @param esc called by ins_esc()
/// @param nomove <c-\><c-o>, don't move cursor
static void stop_insert(pos_T *end_insert_pos, int esc, int nomove)
{
stop_redo_ins();
kv_destroy(replace_stack); // abandon replace stack (reinitializes)
// Save the inserted text for later redo with ^@ and CTRL-A.
// Don't do it when "restart_edit" was set and nothing was inserted,
// otherwise CTRL-O w and then <Left> will clear "last_insert".
String inserted = get_inserted();
int added = inserted.data == NULL ? 0 : (int)inserted.size - new_insert_skip;
if (did_restart_edit == 0 || added > 0) {
xfree(last_insert.data);
last_insert = inserted; // structure copy
last_insert_skip = added < 0 ? 0 : new_insert_skip;
} else {
xfree(inserted.data);
}
if (!arrow_used && end_insert_pos != NULL) {
int cc;
// Auto-format now. It may seem strange to do this when stopping an
// insertion (or moving the cursor), but it's required when appending
// a line and having it end in a space. But only do it when something
// was actually inserted, otherwise undo won't work.
if (!ins_need_undo && has_format_option(FO_AUTO)) {
pos_T tpos = curwin->w_cursor;
// When the cursor is at the end of the line after a space the
// formatting will move it to the following word. Avoid that by
// moving the cursor onto the space.
cc = 'x';
if (curwin->w_cursor.col > 0 && gchar_cursor() == NUL) {
dec_cursor();
cc = gchar_cursor();
if (!ascii_iswhite(cc)) {
curwin->w_cursor = tpos;
}
}
auto_format(true, false);
if (ascii_iswhite(cc)) {
if (gchar_cursor() != NUL) {
inc_cursor();
}
// If the cursor is still at the same character, also keep
// the "coladd".
if (gchar_cursor() == NUL
&& curwin->w_cursor.lnum == tpos.lnum
&& curwin->w_cursor.col == tpos.col) {
curwin->w_cursor.coladd = tpos.coladd;
}
}
}
// If a space was inserted for auto-formatting, remove it now.
check_auto_format(true);
// If we just did an auto-indent, remove the white space from the end
// of the line, and put the cursor back.
// Do this when ESC was used or moving the cursor up/down.
// Check for the old position still being valid, just in case the text
// got changed unexpectedly.
if (!nomove && did_ai && (esc || (vim_strchr(p_cpo, CPO_INDENT) == NULL
&& curwin->w_cursor.lnum !=
end_insert_pos->lnum))
&& end_insert_pos->lnum <= curbuf->b_ml.ml_line_count) {
pos_T tpos = curwin->w_cursor;
colnr_T prev_col = end_insert_pos->col;
curwin->w_cursor = *end_insert_pos;
check_cursor_col(curwin); // make sure it is not past the line
while (true) {
if (gchar_cursor() == NUL && curwin->w_cursor.col > 0) {
curwin->w_cursor.col--;
}
cc = gchar_cursor();
if (!ascii_iswhite(cc)) {
break;
}
if (del_char(true) == FAIL) {
break; // should not happen
}
}
if (curwin->w_cursor.lnum != tpos.lnum) {
curwin->w_cursor = tpos;
} else if (curwin->w_cursor.col < prev_col) {
// reset tpos, could have been invalidated in the loop above
tpos = curwin->w_cursor;
tpos.col++;
if (cc != NUL && gchar_pos(&tpos) == NUL) {
curwin->w_cursor.col++; // put cursor back on the NUL
}
}
// <C-S-Right> may have started Visual mode, adjust the position for
// deleted characters.
if (VIsual_active) {
check_visual_pos();
}
}
}
did_ai = false;
did_si = false;
can_si = false;
can_si_back = false;
// Set '[ and '] to the inserted text. When end_insert_pos is NULL we are
// now in a different buffer.
if (end_insert_pos != NULL) {
curbuf->b_op_start = Insstart;
curbuf->b_op_start_orig = Insstart_orig;
curbuf->b_op_end = *end_insert_pos;
}
}
// Set the last inserted text to a single character.
// Used for the replace command.
void set_last_insert(int c)
{
xfree(last_insert.data);
last_insert.data = xmalloc(MB_MAXBYTES * 3 + 5);
char *s = last_insert.data;
// Use the CTRL-V only when entering a special char
if (c < ' ' || c == DEL) {
*s++ = Ctrl_V;
}
s = add_char2buf(c, s);
*s++ = ESC;
*s = NUL;
last_insert.size = (size_t)(s - last_insert.data);
last_insert_skip = 0;
}
#if defined(EXITFREE)
void free_last_insert(void)
{
API_CLEAR_STRING(last_insert);
}
#endif
// move cursor to start of line
// if flags & BL_WHITE move to first non-white
// if flags & BL_SOL move to first non-white if startofline is set,
// otherwise keep "curswant" column
// if flags & BL_FIX don't leave the cursor on a NUL.
void beginline(int flags)
{
if ((flags & BL_SOL) && !p_sol) {
coladvance(curwin, curwin->w_curswant);
} else {
curwin->w_cursor.col = 0;
curwin->w_cursor.coladd = 0;
if (flags & (BL_WHITE | BL_SOL)) {
for (char *ptr = get_cursor_line_ptr(); ascii_iswhite(*ptr)
&& !((flags & BL_FIX) && ptr[1] == NUL); ptr++) {
curwin->w_cursor.col++;
}
}
curwin->w_set_curswant = true;
}
adjust_skipcol();
}
// oneright oneleft cursor_down cursor_up
//
// Move one char {right,left,down,up}.
// Doesn't move onto the NUL past the end of the line, unless it is allowed.
// Return OK when successful, FAIL when we hit a line of file boundary.
int oneright(void)
{
char *ptr;
if (virtual_active(curwin)) {
pos_T prevpos = curwin->w_cursor;
// Adjust for multi-wide char (excluding TAB)
ptr = get_cursor_pos_ptr();
coladvance(curwin, getviscol() + ((*ptr != TAB && vim_isprintc(utf_ptr2char(ptr)))
? ptr2cells(ptr) : 1));
curwin->w_set_curswant = true;
// Return OK if the cursor moved, FAIL otherwise (at window edge).
return (prevpos.col != curwin->w_cursor.col
|| prevpos.coladd != curwin->w_cursor.coladd) ? OK : FAIL;
}
ptr = get_cursor_pos_ptr();
if (*ptr == NUL) {
return FAIL; // already at the very end
}
int l = utfc_ptr2len(ptr);
// move "l" bytes right, but don't end up on the NUL, unless 'virtualedit'
// contains "onemore".
if (ptr[l] == NUL && (get_ve_flags(curwin) & kOptVeFlagOnemore) == 0) {
return FAIL;
}
curwin->w_cursor.col += l;
curwin->w_set_curswant = true;
adjust_skipcol();
return OK;
}
int oneleft(void)
{
if (virtual_active(curwin)) {
int v = getviscol();
if (v == 0) {
return FAIL;
}
// We might get stuck on 'showbreak', skip over it.
int width = 1;
while (true) {
coladvance(curwin, v - width);
// getviscol() is slow, skip it when 'showbreak' is empty,
// 'breakindent' is not set and there are no multi-byte
// characters
if (getviscol() < v) {
break;
}
width++;
}
if (curwin->w_cursor.coladd == 1) {
// Adjust for multi-wide char (not a TAB)
char *ptr = get_cursor_pos_ptr();
if (*ptr != TAB && vim_isprintc(utf_ptr2char(ptr)) && ptr2cells(ptr) > 1) {
curwin->w_cursor.coladd = 0;
}
}
curwin->w_set_curswant = true;
adjust_skipcol();
return OK;
}
if (curwin->w_cursor.col == 0) {
return FAIL;
}
curwin->w_set_curswant = true;
curwin->w_cursor.col--;
// if the character on the left of the current cursor is a multi-byte
// character, move to its first byte
mb_adjust_cursor();
adjust_skipcol();
return OK;
}
/// Move the cursor up "n" lines in window "wp". Takes care of closed folds.
/// Skips over concealed lines when "skip_conceal" is true.
void cursor_up_inner(win_T *wp, linenr_T n, bool skip_conceal)
{
linenr_T lnum = wp->w_cursor.lnum;
if (n >= lnum) {
lnum = 1;
} else if (win_lines_concealed(wp)) {
// Count each sequence of folded lines as one logical line.
// go to the start of the current fold
hasFolding(wp, lnum, &lnum, NULL);
while (n--) {
// move up one line
lnum--;
n += skip_conceal && decor_conceal_line(wp, lnum - 1, true);
if (lnum <= 1) {
break;
}
// If we entered a fold, move to the beginning, unless in
// Insert mode or when 'foldopen' contains "all": it will open
// in a moment.
if (n > 0 || !((State & MODE_INSERT) || (fdo_flags & kOptFdoFlagAll))) {
hasFolding(wp, lnum, &lnum, NULL);
}
}
lnum = MAX(lnum, 1);
} else {
lnum -= n;
}
wp->w_cursor.lnum = lnum;
}
/// @param upd_topline When true: update topline
int cursor_up(linenr_T n, bool upd_topline)
{
// This fails if the cursor is already in the first line.
if (n > 0 && curwin->w_cursor.lnum <= 1) {
return FAIL;
}
cursor_up_inner(curwin, n, false);
// try to advance to the column we want to be at
coladvance(curwin, curwin->w_curswant);
if (upd_topline) {
update_topline(curwin); // make sure curwin->w_topline is valid
}
return OK;
}
/// Move the cursor down "n" lines in window "wp". Takes care of closed folds.
/// Skips over concealed lines when "skip_conceal" is true.
void cursor_down_inner(win_T *wp, int n, bool skip_conceal)
{
linenr_T lnum = wp->w_cursor.lnum;
linenr_T line_count = wp->w_buffer->b_ml.ml_line_count;
if (lnum + n >= line_count) {
lnum = line_count;
} else if (win_lines_concealed(wp)) {
linenr_T last;
// count each sequence of folded lines as one logical line
while (n--) {
if (hasFoldingWin(wp, lnum, NULL, &last, true, NULL)) {
lnum = last + 1;
} else {
lnum++;
}
n += skip_conceal && decor_conceal_line(wp, lnum - 1, true);
if (lnum >= line_count) {
break;
}
}
lnum = MIN(lnum, line_count);
} else {
lnum += (linenr_T)n;
}
wp->w_cursor.lnum = lnum;
}
/// @param upd_topline When true: update topline
int cursor_down(int n, bool upd_topline)
{
linenr_T lnum = curwin->w_cursor.lnum;
// This fails if the cursor is already in the last (folded) line.
hasFoldingWin(curwin, lnum, NULL, &lnum, true, NULL);
if (n > 0 && lnum >= curwin->w_buffer->b_ml.ml_line_count) {
return FAIL;
}
cursor_down_inner(curwin, n, false);
// try to advance to the column we want to be at
coladvance(curwin, curwin->w_curswant);
if (upd_topline) {
update_topline(curwin); // make sure curwin->w_topline is valid
}
return OK;
}
/// Stuff the last inserted text in the read buffer.
/// Last_insert actually is a copy of the redo buffer, so we
/// first have to remove the command.
///
/// @param c Command character to be inserted
/// @param count Repeat this many times
/// @param no_esc Don't add an ESC at the end
int stuff_inserted(int c, int count, int no_esc)
{
char last = NUL;
String insert = get_last_insert(); // text to be inserted
if (insert.data == NULL) {
emsg(_(e_noinstext));
return FAIL;
}
// may want to stuff the command character, to start Insert mode
if (c != NUL) {
stuffcharReadbuff(c);
}
if (insert.size > 0) {
// look for the last ESC in 'insert'
for (char *p = insert.data + insert.size - 1; p >= insert.data; p--) {
if (*p == ESC) {
insert.size = (size_t)(p - insert.data);
break;
}
}
}
if (insert.size > 0) {
char *p = insert.data + insert.size - 1;
// when the last char is either "0" or "^" it will be quoted if no ESC
// comes after it OR if it will inserted more than once and "ptr"
// starts with ^D. -- Acevedo
if ((*p == '0' || *p == '^')
&& (no_esc || (*insert.data == Ctrl_D && count > 1))) {
last = *p;
insert.size--;
}
}
do {
stuffReadbuffLen(insert.data, (ptrdiff_t)insert.size);
// A trailing "0" is inserted as "<C-V>048", "^" as "<C-V>^".
switch (last) {
case '0':
stuffReadbuffLen(S_LEN("\026\060\064\070"));
break;
case '^':
stuffReadbuffLen(S_LEN("\026^"));
break;
default:
break;
}
} while (--count > 0);
// may want to stuff a trailing ESC, to get out of Insert mode
if (!no_esc) {
stuffcharReadbuff(ESC);
}
return OK;
}
String get_last_insert(void)
FUNC_ATTR_PURE
{
return last_insert.data == NULL ? NULL_STRING : (String){
.data = last_insert.data + last_insert_skip,
.size = last_insert.size - (size_t)last_insert_skip,
};
}
// Get last inserted string, and remove trailing <Esc>.
// Returns pointer to allocated memory (must be freed) or NULL.
char *get_last_insert_save(void)
{
String insert = get_last_insert();
if (insert.data == NULL) {
return NULL;
}
char *s = xmemdupz(insert.data, insert.size);
if (insert.size > 0 && s[insert.size - 1] == ESC) { // remain trailing ESC
s[--insert.size] = NUL;
}
return s;
}
/// Check the word in front of the cursor for an abbreviation.
/// Called when the non-id character "c" has been entered.
/// When an abbreviation is recognized it is removed from the text and
/// the replacement string is inserted in typebuf.tb_buf[], followed by "c".
///
/// @param c character
///
/// @return true if the word is a known abbreviation.
static bool echeck_abbr(int c)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
// Don't check for abbreviation in paste mode, when disabled and just
// after moving around with cursor keys.
if (p_paste || no_abbr || arrow_used) {
return false;
}
return check_abbr(c, get_cursor_line_ptr(), curwin->w_cursor.col,
curwin->w_cursor.lnum == Insstart.lnum ? Insstart.col : 0);
}
// replace-stack functions
//
// When replacing characters, the replaced characters are remembered for each
// new character. This is used to re-insert the old text when backspacing.
//
// There is a NUL headed list of characters for each character that is
// currently in the file after the insertion point. When BS is used, one NUL
// headed list is put back for the deleted character.
//
// For a newline, there are two NUL headed lists. One contains the characters
// that the NL replaced. The extra one stores the characters after the cursor
// that were deleted (always white space).
/// Push character that is replaced onto the replace stack.
///
/// replace_offset is normally 0, in which case replace_push will add a new
/// character at the end of the stack. If replace_offset is not 0, that many
/// characters will be left on the stack above the newly inserted character.
///
/// @param str character that is replaced (NUL is none)
/// @param len length of character in bytes
void replace_push(char *str, size_t len)
{
// TODO(bfredl): replace_offset is suss af, if we don't need it, this
// function is just kv_concat() :p
if (kv_size(replace_stack) < (size_t)replace_offset) { // nothing to do
return;
}
kv_ensure_space(replace_stack, len);
char *p = replace_stack.items + kv_size(replace_stack) - replace_offset;
if (replace_offset) {
memmove(p + len, p, (size_t)replace_offset);
}
memcpy(p, str, len);
kv_size(replace_stack) += len;
}
/// push NUL as separator between entries in the stack
void replace_push_nul(void)
{
replace_push("", 1);
}
/// Check top of replace stack, pop it if it was NUL
///
/// when a non-NUL byte is found, use mb_replace_pop_ins() to
/// pop one complete multibyte character.
///
/// @return -1 if stack is empty, last byte of char or NUL otherwise
static int replace_pop_if_nul(void)
{
int ch = (kv_size(replace_stack)) ? (uint8_t)kv_A(replace_stack, kv_size(replace_stack) - 1) : -1;
if (ch == NUL) {
kv_size(replace_stack)--;
}
return ch;
}
/// Join the top two items on the replace stack. This removes to "off"'th NUL
/// encountered.
///
/// @param off offset for which NUL to remove
static void replace_join(int off)
{
for (ssize_t i = (ssize_t)kv_size(replace_stack); --i >= 0;) {
if (kv_A(replace_stack, i) == NUL && off-- <= 0) {
kv_size(replace_stack)--;
memmove(&kv_A(replace_stack, i), &kv_A(replace_stack, i + 1),
(kv_size(replace_stack) - (size_t)i));
return;
}
}
}
/// Pop bytes from the replace stack until a NUL is found, and insert them
/// before the cursor. Can only be used in MODE_REPLACE or MODE_VREPLACE state.
static void replace_pop_ins(void)
{
int oldState = State;
State = MODE_NORMAL; // don't want MODE_REPLACE here
while ((replace_pop_if_nul()) > 0) {
mb_replace_pop_ins();
dec_cursor();
}
State = oldState;
}
/// Insert multibyte char popped from the replace stack.
///
/// caller must already have checked the top of the stack is not NUL!!
static void mb_replace_pop_ins(void)
{
int len = utf_head_off(&kv_A(replace_stack, 0),
&kv_A(replace_stack, kv_size(replace_stack) - 1)) + 1;
kv_size(replace_stack) -= (size_t)len;
ins_bytes_len(&kv_A(replace_stack, kv_size(replace_stack)), (size_t)len);
}
// Handle doing a BS for one character.
// cc < 0: replace stack empty, just move cursor
// cc == 0: character was inserted, delete it
// cc > 0: character was replaced, put cc (first byte of original char) back
// and check for more characters to be put back
// When "limit_col" is >= 0, don't delete before this column. Matters when
// using composing characters, use del_char_after_col() instead of del_char().
static void replace_do_bs(int limit_col)
{
colnr_T start_vcol;
const int l_State = State;
int cc = replace_pop_if_nul();
if (cc > 0) {
int orig_len = 0;
int orig_vcols = 0;
if (l_State & VREPLACE_FLAG) {
// Get the number of screen cells used by the character we are
// going to delete.
getvcol(curwin, &curwin->w_cursor, NULL, &start_vcol, NULL);
orig_vcols = win_chartabsize(curwin, get_cursor_pos_ptr(), start_vcol);
}
del_char_after_col(limit_col);
if (l_State & VREPLACE_FLAG) {
orig_len = get_cursor_pos_len();
}
replace_pop_ins();
if (l_State & VREPLACE_FLAG) {
// Get the number of screen cells used by the inserted characters
char *p = get_cursor_pos_ptr();
int ins_len = get_cursor_pos_len() - orig_len;
int vcol = start_vcol;
for (int i = 0; i < ins_len; i++) {
vcol += win_chartabsize(curwin, p + i, vcol);
i += utfc_ptr2len(p) - 1;
}
vcol -= start_vcol;
// Delete spaces that were inserted after the cursor to keep the
// text aligned.
curwin->w_cursor.col += ins_len;
while (vcol > orig_vcols && gchar_cursor() == ' ') {
del_char(false);
orig_vcols++;
}
curwin->w_cursor.col -= ins_len;
}
// mark the buffer as changed and prepare for displaying
changed_bytes(curwin->w_cursor.lnum, curwin->w_cursor.col);
} else if (cc == 0) {
del_char_after_col(limit_col);
}
}
/// Check that C-indenting is on.
bool cindent_on(void)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
return !p_paste && (curbuf->b_p_cin || *curbuf->b_p_inde != NUL);
}
/// Check that "cinkeys" contains the key "keytyped",
/// when == '*': Only if key is preceded with '*' (indent before insert)
/// when == '!': Only if key is preceded with '!' (don't insert)
/// when == ' ': Only if key is not preceded with '*' or '!' (indent afterwards)
///
/// "keytyped" can have a few special values:
/// KEY_OPEN_FORW :
/// KEY_OPEN_BACK :
/// KEY_COMPLETE : Just finished completion.
///
/// @param keytyped key that was typed
/// @param when condition on when to perform the check
/// @param line_is_empty when true, accept keys with '0' before them.
bool in_cinkeys(int keytyped, int when, bool line_is_empty)
{
char *look;
bool try_match;
bool try_match_word;
char *p;
bool icase;
if (keytyped == NUL) {
// Can happen with CTRL-Y and CTRL-E on a short line.
return false;
}
if (*curbuf->b_p_inde != NUL) {
look = curbuf->b_p_indk; // 'indentexpr' set: use 'indentkeys'
} else {
look = curbuf->b_p_cink; // 'indentexpr' empty: use 'cinkeys'
}
while (*look) {
// Find out if we want to try a match with this key, depending on
// 'when' and a '*' or '!' before the key.
switch (when) {
case '*':
try_match = (*look == '*'); break;
case '!':
try_match = (*look == '!'); break;
default:
try_match = (*look != '*') && (*look != '!'); break;
}
if (*look == '*' || *look == '!') {
look++;
}
// If there is a '0', only accept a match if the line is empty.
// But may still match when typing last char of a word.
if (*look == '0') {
try_match_word = try_match;
if (!line_is_empty) {
try_match = false;
}
look++;
} else {
try_match_word = false;
}
// Does it look like a control character?
if (*look == '^' && look[1] >= '?' && look[1] <= '_') {
if (try_match && keytyped == CTRL_CHR(look[1])) {
return true;
}
look += 2;
// 'o' means "o" command, open forward.
// 'O' means "O" command, open backward.
} else if (*look == 'o') {
if (try_match && keytyped == KEY_OPEN_FORW) {
return true;
}
look++;
} else if (*look == 'O') {
if (try_match && keytyped == KEY_OPEN_BACK) {
return true;
}
look++;
// 'e' means to check for "else" at start of line and just before the
// cursor.
} else if (*look == 'e') {
if (try_match && keytyped == 'e' && curwin->w_cursor.col >= 4) {
p = get_cursor_line_ptr();
if (skipwhite(p) == p + curwin->w_cursor.col - 4
&& strncmp(p + curwin->w_cursor.col - 4, "else", 4) == 0) {
return true;
}
}
look++;
// ':' only causes an indent if it is at the end of a label or case
// statement, or when it was before typing the ':' (to fix
// class::method for C++).
} else if (*look == ':') {
if (try_match && keytyped == ':') {
p = get_cursor_line_ptr();
if (cin_iscase(p, false) || cin_isscopedecl(p) || cin_islabel()) {
return true;
}
// Need to get the line again after cin_islabel().
p = get_cursor_line_ptr();
if (curwin->w_cursor.col > 2
&& p[curwin->w_cursor.col - 1] == ':'
&& p[curwin->w_cursor.col - 2] == ':') {
p[curwin->w_cursor.col - 1] = ' ';
const bool i = cin_iscase(p, false)
|| cin_isscopedecl(p)
|| cin_islabel();
p = get_cursor_line_ptr();
p[curwin->w_cursor.col - 1] = ':';
if (i) {
return true;
}
}
}
look++;
// Is it a key in <>, maybe?
} else if (*look == '<') {
if (try_match) {
// make up some named keys <o>, <O>, <e>, <0>, <>>, <<>, <*>,
// <:> and <!> so that people can re-indent on o, O, e, 0, <,
// >, *, : and ! keys if they really really want to.
if (vim_strchr("<>!*oOe0:", (uint8_t)look[1]) != NULL
&& keytyped == look[1]) {
return true;
}
if (keytyped == get_special_key_code(look + 1)) {
return true;
}
}
while (*look && *look != '>') {
look++;
}
while (*look == '>') {
look++;
}
// Is it a word: "=word"?
} else if (*look == '=' && look[1] != ',' && look[1] != NUL) {
look++;
if (*look == '~') {
icase = true;
look++;
} else {
icase = false;
}
p = vim_strchr(look, ',');
if (p == NULL) {
p = look + strlen(look);
}
if ((try_match || try_match_word)
&& curwin->w_cursor.col >= (colnr_T)(p - look)) {
bool match = false;
if (keytyped == KEY_COMPLETE) {
char *n, *s;
// Just completed a word, check if it starts with "look".
// search back for the start of a word.
char *line = get_cursor_line_ptr();
for (s = line + curwin->w_cursor.col; s > line; s = n) {
n = mb_prevptr(line, s);
if (!vim_iswordp(n)) {
break;
}
}
assert(p >= look && (uintmax_t)(p - look) <= SIZE_MAX);
if (s + (p - look) <= line + curwin->w_cursor.col
&& (icase
? mb_strnicmp(s, look, (size_t)(p - look))
: strncmp(s, look, (size_t)(p - look))) == 0) {
match = true;
}
} else {
// TODO(@brammool): multi-byte
if (keytyped == (int)(uint8_t)p[-1]
|| (icase && keytyped < 256 && keytyped >= 0
&& TOLOWER_LOC(keytyped) == TOLOWER_LOC((uint8_t)p[-1]))) {
char *line = get_cursor_pos_ptr();
assert(p >= look && (uintmax_t)(p - look) <= SIZE_MAX);
if ((curwin->w_cursor.col == (colnr_T)(p - look)
|| !vim_iswordc((uint8_t)line[-(p - look) - 1]))
&& (icase
? mb_strnicmp(line - (p - look), look, (size_t)(p - look))
: strncmp(line - (p - look), look, (size_t)(p - look))) == 0) {
match = true;
}
}
}
if (match && try_match_word && !try_match) {
// "0=word": Check if there are only blanks before the
// word.
if (getwhitecols_curline() !=
(int)(curwin->w_cursor.col - (p - look))) {
match = false;
}
}
if (match) {
return true;
}
}
look = p;
// Ok, it's a boring generic character.
} else {
if (try_match && (uint8_t)(*look) == keytyped) {
return true;
}
if (*look != NUL) {
look++;
}
}
// Skip over ", ".
look = skip_to_option_part(look);
}
return false;
}
static void ins_reg(void)
{
bool need_redraw = false;
int literally = 0;
int vis_active = VIsual_active;
// If we are going to wait for a character, show a '"'.
pc_status = PC_STATUS_UNSET;
if (redrawing() && !char_avail()) {
// may need to redraw when no more chars available now
ins_redraw(false);
edit_putchar('"', true);
add_to_showcmd_c(Ctrl_R);
}
// Don't map the register name. This also prevents the mode message to be
// deleted when ESC is hit.
no_mapping++;
allow_keys++;
int regname = plain_vgetc();
LANGMAP_ADJUST(regname, true);
if (regname == Ctrl_R || regname == Ctrl_O || regname == Ctrl_P) {
// Get a third key for literal register insertion
literally = regname;
add_to_showcmd_c(literally);
regname = plain_vgetc();
LANGMAP_ADJUST(regname, true);
}
no_mapping--;
allow_keys--;
// Don't call u_sync() while typing the expression or giving an error
// message for it. Only call it explicitly.
no_u_sync++;
if (regname == '=') {
pos_T curpos = curwin->w_cursor;
// Sync undo when evaluating the expression calls setline() or
// append(), so that it can be undone separately.
u_sync_once = 2;
regname = get_expr_register();
// Cursor may be moved back a column.
curwin->w_cursor = curpos;
check_cursor(curwin);
}
if (regname == NUL || !valid_yank_reg(regname, false)) {
vim_beep(kOptBoFlagRegister);
need_redraw = true; // remove the '"'
} else {
if (literally == Ctrl_O || literally == Ctrl_P) {
// Append the command to the redo buffer.
AppendCharToRedobuff(Ctrl_R);
AppendCharToRedobuff(literally);
AppendCharToRedobuff(regname);
do_put(regname, NULL, BACKWARD, 1,
(literally == Ctrl_P ? PUT_FIXINDENT : 0) | PUT_CURSEND);
} else if (insert_reg(regname, NULL, literally) == FAIL) {
vim_beep(kOptBoFlagRegister);
need_redraw = true; // remove the '"'
} else if (stop_insert_mode) {
// When the '=' register was used and a function was invoked that
// did ":stopinsert" then stuff_empty() returns false but we won't
// insert anything, need to remove the '"'
need_redraw = true;
}
}
no_u_sync--;
if (u_sync_once == 1) {
ins_need_undo = true;
}
u_sync_once = 0;
// If the inserted register is empty, we need to remove the '"'. Do this before
// clearing showcmd, which emits an event that can also update the screen.
if (need_redraw || stuff_empty()) {
edit_unputchar();
}
clear_showcmd();
// Disallow starting Visual mode here, would get a weird mode.
if (!vis_active && VIsual_active) {
end_visual_mode();
}
}
// CTRL-G commands in Insert mode.
static void ins_ctrl_g(void)
{
// Right after CTRL-X the cursor will be after the ruler.
setcursor();
// Don't map the second key. This also prevents the mode message to be
// deleted when ESC is hit.
no_mapping++;
allow_keys++;
int c = plain_vgetc();
no_mapping--;
allow_keys--;
switch (c) {
// CTRL-G k and CTRL-G <Up>: cursor up to Insstart.col
case K_UP:
case Ctrl_K:
case 'k':
ins_up(true);
break;
// CTRL-G j and CTRL-G <Down>: cursor down to Insstart.col
case K_DOWN:
case Ctrl_J:
case 'j':
ins_down(true);
break;
// CTRL-G u: start new undoable edit
case 'u':
u_sync(true);
ins_need_undo = true;
// Need to reset Insstart, esp. because a BS that joins
// a line to the previous one must save for undo.
update_Insstart_orig = false;
Insstart = curwin->w_cursor;
break;
// CTRL-G U: do not break undo with the next char.
case 'U':
// Allow one left/right cursor movement with the next char,
// without breaking undo.
dont_sync_undo = kNone;
break;
case ESC:
// Esc after CTRL-G cancels it.
break;
// Unknown CTRL-G command, reserved for future expansion.
default:
vim_beep(kOptBoFlagCtrlg);
}
}
// CTRL-^ in Insert mode.
static void ins_ctrl_hat(void)
{
if (map_to_exists_mode("", MODE_LANGMAP, false)) {
// ":lmap" mappings exists, Toggle use of ":lmap" mappings.
if (State & MODE_LANGMAP) {
curbuf->b_p_iminsert = B_IMODE_NONE;
State &= ~MODE_LANGMAP;
} else {
curbuf->b_p_iminsert = B_IMODE_LMAP;
State |= MODE_LANGMAP;
}
}
set_iminsert_global(curbuf);
showmode();
// Show/unshow value of 'keymap' in status lines.
status_redraw_curbuf();
}
/// Handle ESC in insert mode.
///
/// @param[in,out] count repeat count of the insert command
/// @param cmdchar command that started the insert
/// @param nomove when true, don't move the cursor
///
/// @return true when leaving insert mode, false when repeating the insert.
static bool ins_esc(int *count, int cmdchar, bool nomove)
FUNC_ATTR_NONNULL_ARG(1)
{
static bool disabled_redraw = false;
check_spell_redraw();
int temp = curwin->w_cursor.col;
if (disabled_redraw) {
RedrawingDisabled--;
disabled_redraw = false;
}
if (!arrow_used) {
// Don't append the ESC for "r<CR>" and "grx".
if (cmdchar != 'r' && cmdchar != 'v') {
AppendToRedobuff(ESC_STR);
}
// Repeating insert may take a long time. Check for
// interrupt now and then.
if (*count > 0) {
line_breakcheck();
if (got_int) {
*count = 0;
}
}
if (--*count > 0) { // repeat what was typed
// Vi repeats the insert without replacing characters.
if (vim_strchr(p_cpo, CPO_REPLCNT) != NULL) {
State &= ~REPLACE_FLAG;
}
start_redo_ins();
if (cmdchar == 'r' || cmdchar == 'v') {
stuffRedoReadbuff(ESC_STR); // No ESC in redo buffer
}
RedrawingDisabled++;
disabled_redraw = true;
// Repeat the insert
return false;
}
stop_insert(&curwin->w_cursor, true, nomove);
undisplay_dollar();
}
if (cmdchar != 'r' && cmdchar != 'v') {
ins_apply_autocmds(EVENT_INSERTLEAVEPRE);
}
// When an autoindent was removed, curswant stays after the
// indent
if (restart_edit == NUL && (colnr_T)temp == curwin->w_cursor.col) {
curwin->w_set_curswant = true;
}
// Remember the last Insert position in the '^ mark.
if ((cmdmod.cmod_flags & CMOD_KEEPJUMPS) == 0) {
fmarkv_T view = mark_view_make(curwin->w_topline, curwin->w_cursor);
RESET_FMARK(&curbuf->b_last_insert, curwin->w_cursor, curbuf->b_fnum, view);
}
// The cursor should end up on the last inserted character.
// Don't do it for CTRL-O, unless past the end of the line.
if (!nomove
&& (curwin->w_cursor.col != 0 || curwin->w_cursor.coladd > 0)
&& (restart_edit == NUL || (gchar_cursor() == NUL && !VIsual_active))
&& !revins_on) {
if (curwin->w_cursor.coladd > 0 || get_ve_flags(curwin) == kOptVeFlagAll) {
oneleft();
if (restart_edit != NUL) {
curwin->w_cursor.coladd++;
}
} else {
curwin->w_cursor.col--;
curwin->w_valid &= ~(VALID_WCOL|VALID_VIRTCOL);
// Correct cursor for multi-byte character.
mb_adjust_cursor();
}
}
State = MODE_NORMAL;
may_trigger_modechanged();
// need to position cursor again when on a TAB and
// when on a char with inline virtual text
if (gchar_cursor() == TAB || buf_meta_total(curbuf, kMTMetaInline) > 0) {
curwin->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_VIRTCOL);
}
setmouse();
ui_cursor_shape(); // may show different cursor shape
// When recording or for CTRL-O, need to display the new mode.
// Otherwise remove the mode message.
if (reg_recording != 0 || restart_edit != NUL) {
showmode();
} else if (p_smd && (got_int || !skip_showmode())
&& !(p_ch == 0 && !ui_has(kUIMessages))) {
unshowmode(false);
}
// Exit Insert mode
return true;
}
// Toggle language: revins_on.
// Move to end of reverse inserted text.
static void ins_ctrl_(void)
{
if (revins_on && revins_chars && revins_scol >= 0) {
while (gchar_cursor() != NUL && revins_chars--) {
curwin->w_cursor.col++;
}
}
p_ri = !p_ri;
revins_on = (State == MODE_INSERT && p_ri);
if (revins_on) {
revins_scol = curwin->w_cursor.col;
revins_legal++;
revins_chars = 0;
undisplay_dollar();
} else {
revins_scol = -1;
}
showmode();
}
/// If 'keymodel' contains "startsel", may start selection.
///
/// @param c character to check
//
/// @return true when a CTRL-O and other keys stuffed.
static bool ins_start_select(int c)
FUNC_ATTR_WARN_UNUSED_RESULT
{
if (!km_startsel) {
return false;
}
switch (c) {
case K_KHOME:
case K_KEND:
case K_PAGEUP:
case K_KPAGEUP:
case K_PAGEDOWN:
case K_KPAGEDOWN:
if (!(mod_mask & MOD_MASK_SHIFT)) {
break;
}
FALLTHROUGH;
case K_S_LEFT:
case K_S_RIGHT:
case K_S_UP:
case K_S_DOWN:
case K_S_END:
case K_S_HOME:
// Start selection right away, the cursor can move with CTRL-O when
// beyond the end of the line.
start_selection();
// Execute the key in (insert) Select mode.
stuffcharReadbuff(Ctrl_O);
if (mod_mask) {
const char buf[] = { (char)K_SPECIAL, (char)KS_MODIFIER,
(char)(uint8_t)mod_mask, NUL };
stuffReadbuffLen(buf, 3);
}
stuffcharReadbuff(c);
return true;
}
return false;
}
// <Insert> key in Insert mode: toggle insert/replace mode.
static void ins_insert(int replaceState)
{
set_vim_var_string(VV_INSERTMODE, ((State & REPLACE_FLAG)
? "i"
: replaceState == MODE_VREPLACE ? "v" : "r"), 1);
ins_apply_autocmds(EVENT_INSERTCHANGE);
if (State & REPLACE_FLAG) {
State = MODE_INSERT | (State & MODE_LANGMAP);
} else {
State = replaceState | (State & MODE_LANGMAP);
}
may_trigger_modechanged();
AppendCharToRedobuff(K_INS);
showmode();
ui_cursor_shape(); // may show different cursor shape
}
// Pressed CTRL-O in Insert mode.
static void ins_ctrl_o(void)
{
restart_VIsual_select = 0;
if (State & VREPLACE_FLAG) {
restart_edit = 'V';
} else if (State & REPLACE_FLAG) {
restart_edit = 'R';
} else {
restart_edit = 'I';
}
if (virtual_active(curwin)) {
ins_at_eol = false; // cursor always keeps its column
} else {
ins_at_eol = (gchar_cursor() == NUL);
}
}
// If the cursor is on an indent, ^T/^D insert/delete one
// shiftwidth. Otherwise ^T/^D behave like a "<<" or ">>".
// Always round the indent to 'shiftwidth', this is compatible
// with vi. But vi only supports ^T and ^D after an
// autoindent, we support it everywhere.
static void ins_shift(int c, int lastc)
{
if (stop_arrow() == FAIL) {
return;
}
AppendCharToRedobuff(c);
// 0^D and ^^D: remove all indent.
if (c == Ctrl_D && (lastc == '0' || lastc == '^')
&& curwin->w_cursor.col > 0) {
curwin->w_cursor.col--;
del_char(false); // delete the '^' or '0'
// In Replace mode, restore the characters that '^' or '0' replaced.
if (State & REPLACE_FLAG) {
replace_pop_ins();
}
if (lastc == '^') {
old_indent = get_indent(); // remember curr. indent
}
change_indent(INDENT_SET, 0, true, true);
} else {
change_indent(c == Ctrl_D ? INDENT_DEC : INDENT_INC, 0, true, true);
}
if (did_ai && *skipwhite(get_cursor_line_ptr()) != NUL) {
did_ai = false;
}
did_si = false;
can_si = false;
can_si_back = false;
can_cindent = false; // no cindenting after ^D or ^T
}
static void ins_del(void)
{
if (stop_arrow() == FAIL) {
return;
}
if (gchar_cursor() == NUL) { // delete newline
const int temp = curwin->w_cursor.col;
if (!can_bs(BS_EOL) // only if "eol" included
|| do_join(2, false, true, false, false) == FAIL) {
vim_beep(kOptBoFlagBackspace);
} else {
curwin->w_cursor.col = temp;
// Adjust orig_line_count in case more lines have been deleted than
// have been added. That makes sure, that open_line() later
// can access all buffer lines correctly
if (State & VREPLACE_FLAG
&& orig_line_count > curbuf->b_ml.ml_line_count) {
orig_line_count = curbuf->b_ml.ml_line_count;
}
}
} else if (del_char(false) == FAIL) { // delete char under cursor
vim_beep(kOptBoFlagBackspace);
}
did_ai = false;
did_si = false;
can_si = false;
can_si_back = false;
AppendCharToRedobuff(K_DEL);
}
/// Handle Backspace, delete-word and delete-line in Insert mode.
///
/// @param c character that was typed
/// @param mode backspace mode to use
/// @param[in,out] inserted_space_p whether a space was the last
// character inserted
///
/// @return true when backspace was actually used.
static bool ins_bs(int c, int mode, int *inserted_space_p)
FUNC_ATTR_NONNULL_ARG(3)
{
int cc;
int temp = 0; // init for GCC
bool did_backspace = false;
bool call_fix_indent = false;
// can't delete anything in an empty file
// can't backup past first character in buffer
// can't backup past starting point unless 'backspace' > 1
// can backup to a previous line if 'backspace' == 0
if (buf_is_empty(curbuf)
|| (!revins_on
&& ((curwin->w_cursor.lnum == 1 && curwin->w_cursor.col == 0)
|| (!can_bs(BS_START)
&& ((arrow_used && !bt_prompt(curbuf))
|| (curwin->w_cursor.lnum == Insstart_orig.lnum
&& curwin->w_cursor.col <= Insstart_orig.col)))
|| (!can_bs(BS_INDENT) && !arrow_used && ai_col > 0
&& curwin->w_cursor.col <= ai_col)
|| (!can_bs(BS_EOL) && curwin->w_cursor.col == 0)))) {
vim_beep(kOptBoFlagBackspace);
return false;
}
if (stop_arrow() == FAIL) {
return false;
}
bool in_indent = inindent(0);
if (in_indent) {
can_cindent = false;
}
end_comment_pending = NUL; // After BS, don't auto-end comment
if (revins_on) { // put cursor after last inserted char
inc_cursor();
}
// Virtualedit:
// BACKSPACE_CHAR eats a virtual space
// BACKSPACE_WORD eats all coladd
// BACKSPACE_LINE eats all coladd and keeps going
if (curwin->w_cursor.coladd > 0) {
if (mode == BACKSPACE_CHAR) {
curwin->w_cursor.coladd--;
return true;
}
if (mode == BACKSPACE_WORD) {
curwin->w_cursor.coladd = 0;
return true;
}
curwin->w_cursor.coladd = 0;
}
// Delete newline!
if (curwin->w_cursor.col == 0) {
linenr_T lnum = Insstart.lnum;
if (curwin->w_cursor.lnum == lnum || revins_on) {
if (u_save((linenr_T)(curwin->w_cursor.lnum - 2),
(linenr_T)(curwin->w_cursor.lnum + 1)) == FAIL) {
return false;
}
Insstart.lnum--;
Insstart.col = ml_get_len(Insstart.lnum);
}
// In replace mode:
// cc < 0: NL was inserted, delete it
// cc >= 0: NL was replaced, put original characters back
cc = -1;
if (State & REPLACE_FLAG) {
cc = replace_pop_if_nul(); // returns -1 if NL was inserted
}
// In replace mode, in the line we started replacing, we only move the
// cursor.
if ((State & REPLACE_FLAG) && curwin->w_cursor.lnum <= lnum) {
dec_cursor();
} else {
if (!(State & VREPLACE_FLAG)
|| curwin->w_cursor.lnum > orig_line_count) {
temp = gchar_cursor(); // remember current char
curwin->w_cursor.lnum--;
// When "aw" is in 'formatoptions' we must delete the space at
// the end of the line, otherwise the line will be broken
// again when auto-formatting.
if (has_format_option(FO_AUTO)
&& has_format_option(FO_WHITE_PAR)) {
char *ptr = ml_get_buf_mut(curbuf, curwin->w_cursor.lnum);
int len = get_cursor_line_len();
if (len > 0 && ptr[len - 1] == ' ') {
ptr[len - 1] = NUL;
curbuf->b_ml.ml_line_len--;
}
}
do_join(2, false, false, false, false);
if (temp == NUL && gchar_cursor() != NUL) {
inc_cursor();
}
} else {
dec_cursor();
}
// In MODE_REPLACE mode we have to put back the text that was
// replaced by the NL. On the replace stack is first a
// NUL-terminated sequence of characters that were deleted and then
// the characters that NL replaced.
if (State & REPLACE_FLAG) {
// Do the next ins_char() in MODE_NORMAL state, to
// prevent ins_char() from replacing characters and
// avoiding showmatch().
int oldState = State;
State = MODE_NORMAL;
// restore characters (blanks) deleted after cursor
while (cc > 0) {
colnr_T save_col = curwin->w_cursor.col;
mb_replace_pop_ins();
curwin->w_cursor.col = save_col;
cc = replace_pop_if_nul();
}
// restore the characters that NL replaced
replace_pop_ins();
State = oldState;
}
}
did_ai = false;
} else {
// Delete character(s) before the cursor.
if (revins_on) { // put cursor on last inserted char
dec_cursor();
}
colnr_T mincol = 0;
// keep indent
if (mode == BACKSPACE_LINE
&& (curbuf->b_p_ai || cindent_on())
&& !revins_on) {
colnr_T save_col = curwin->w_cursor.col;
beginline(BL_WHITE);
if (curwin->w_cursor.col < save_col) {
mincol = curwin->w_cursor.col;
// should now fix the indent to match with the previous line
call_fix_indent = true;
}
curwin->w_cursor.col = save_col;
}
// Handle deleting one 'shiftwidth' or 'softtabstop'.
if (mode == BACKSPACE_CHAR
&& ((p_sta && in_indent)
|| ((get_sts_value() != 0 || tabstop_count(curbuf->b_p_vsts_array))
&& curwin->w_cursor.col > 0
&& (*(get_cursor_pos_ptr() - 1) == TAB
|| (*(get_cursor_pos_ptr() - 1) == ' '
&& (!*inserted_space_p || arrow_used)))))) {
*inserted_space_p = false;
bool const use_ts = !curwin->w_p_list || curwin->w_p_lcs_chars.tab1;
char *const line = get_cursor_line_ptr();
char *const cursor_ptr = line + curwin->w_cursor.col;
colnr_T vcol = 0;
colnr_T space_vcol = 0;
StrCharInfo sci = utf_ptr2StrCharInfo(line);
StrCharInfo space_sci = sci;
bool prev_space = false;
// Compute virtual column of cursor position, and find the last
// whitespace before cursor that is preceded by non-whitespace.
// Use charsize_nowrap() so that virtual text and wrapping are ignored.
while (sci.ptr < cursor_ptr) {
bool cur_space = ascii_iswhite(sci.chr.value);
if (!prev_space && cur_space) {
space_sci = sci;
space_vcol = vcol;
}
vcol += charsize_nowrap(curbuf, sci.ptr, use_ts, vcol, sci.chr.value);
sci = utfc_next(sci);
prev_space = cur_space;
}
// Compute the virtual column where we want to be.
colnr_T want_vcol = vcol > 0 ? vcol - 1 : 0;
if (p_sta && in_indent) {
want_vcol -= want_vcol % get_sw_value(curbuf);
} else {
want_vcol = tabstop_start(want_vcol, get_sts_value(), curbuf->b_p_vsts_array);
}
// Find the position to stop backspacing.
// Use charsize_nowrap() so that virtual text and wrapping are ignored.
while (true) {
int size = charsize_nowrap(curbuf, space_sci.ptr, use_ts, space_vcol, space_sci.chr.value);
if (space_vcol + size > want_vcol) {
break;
}
space_vcol += size;
space_sci = utfc_next(space_sci);
}
colnr_T const want_col = (int)(space_sci.ptr - line);
// Delete characters until we are at or before want_col.
while (curwin->w_cursor.col > want_col) {
dec_cursor();
if (State & REPLACE_FLAG) {
// Don't delete characters before the insert point when in Replace mode.
if (curwin->w_cursor.lnum != Insstart.lnum
|| curwin->w_cursor.col >= Insstart.col) {
replace_do_bs(-1);
}
} else {
del_char(false);
}
}
// Insert extra spaces until we are at want_vcol.
for (; space_vcol < want_vcol; space_vcol++) {
// Remember the first char we inserted.
if (curwin->w_cursor.lnum == Insstart_orig.lnum
&& curwin->w_cursor.col < Insstart_orig.col) {
Insstart_orig.col = curwin->w_cursor.col;
}
if (State & VREPLACE_FLAG) {
ins_char(' ');
} else {
ins_str(S_LEN(" "));
if ((State & REPLACE_FLAG)) {
replace_push_nul();
}
}
}
} else {
// Delete up to starting point, start of line or previous word.
int cclass = mb_get_class(get_cursor_pos_ptr());
do {
if (!revins_on) { // put cursor on char to be deleted
dec_cursor();
}
cc = gchar_cursor();
// look multi-byte character class
int prev_cclass = cclass;
cclass = mb_get_class(get_cursor_pos_ptr());
if (mode == BACKSPACE_WORD && !ascii_isspace(cc)) { // start of word?
mode = BACKSPACE_WORD_NOT_SPACE;
temp = vim_iswordc(cc);
} else if (mode == BACKSPACE_WORD_NOT_SPACE
&& ((ascii_isspace(cc) || vim_iswordc(cc) != temp)
|| prev_cclass != cclass)) { // end of word?
if (!revins_on) {
inc_cursor();
} else if (State & REPLACE_FLAG) {
dec_cursor();
}
break;
}
if (State & REPLACE_FLAG) {
replace_do_bs(-1);
} else {
bool has_composing = false;
if (p_deco) {
char *p0 = get_cursor_pos_ptr();
has_composing = utf_composinglike(p0, p0 + utf_ptr2len(p0), NULL);
}
del_char(false);
// If there are combining characters and 'delcombine' is set
// move the cursor back. Don't back up before the base character.
if (has_composing) {
inc_cursor();
}
if (revins_chars) {
revins_chars--;
revins_legal++;
}
if (revins_on && gchar_cursor() == NUL) {
break;
}
}
// Just a single backspace?:
if (mode == BACKSPACE_CHAR) {
break;
}
} while (revins_on
|| (curwin->w_cursor.col > mincol
&& (can_bs(BS_NOSTOP)
|| (curwin->w_cursor.lnum != Insstart_orig.lnum
|| curwin->w_cursor.col != Insstart_orig.col))));
}
did_backspace = true;
}
did_si = false;
can_si = false;
can_si_back = false;
if (curwin->w_cursor.col <= 1) {
did_ai = false;
}
if (call_fix_indent) {
fix_indent();
}
// It's a little strange to put backspaces into the redo
// buffer, but it makes auto-indent a lot easier to deal
// with.
AppendCharToRedobuff(c);
// If deleted before the insertion point, adjust it
if (curwin->w_cursor.lnum == Insstart_orig.lnum
&& curwin->w_cursor.col < Insstart_orig.col) {
Insstart_orig.col = curwin->w_cursor.col;
}
// vi behaviour: the cursor moves backward but the character that
// was there remains visible
// Vim behaviour: the cursor moves backward and the character that
// was there is erased from the screen.
// We can emulate the vi behaviour by pretending there is a dollar
// displayed even when there isn't.
// --pkv Sun Jan 19 01:56:40 EST 2003
if (vim_strchr(p_cpo, CPO_BACKSPACE) != NULL && dollar_vcol == -1) {
dollar_vcol = curwin->w_virtcol;
}
// When deleting a char the cursor line must never be in a closed fold.
// E.g., when 'foldmethod' is indent and deleting the first non-white
// char before a Tab.
if (did_backspace) {
foldOpenCursor();
}
return did_backspace;
}
static void ins_left(void)
{
const bool end_change = dont_sync_undo == kFalse; // end undoable change
if ((fdo_flags & kOptFdoFlagHor) && KeyTyped) {
foldOpenCursor();
}
undisplay_dollar();
pos_T tpos = curwin->w_cursor;
if (oneleft() == OK) {
start_arrow_with_change(&tpos, end_change);
if (!end_change) {
AppendCharToRedobuff(K_LEFT);
}
// If exit reversed string, position is fixed
if (revins_scol != -1 && (int)curwin->w_cursor.col >= revins_scol) {
revins_legal++;
}
revins_chars++;
} else if (vim_strchr(p_ww, '[') != NULL && curwin->w_cursor.lnum > 1) {
// if 'whichwrap' set for cursor in insert mode may go to previous line.
// always break undo when moving upwards/downwards, else undo may break
start_arrow(&tpos);
curwin->w_cursor.lnum--;
coladvance(curwin, MAXCOL);
curwin->w_set_curswant = true; // so we stay at the end
} else {
vim_beep(kOptBoFlagCursor);
}
dont_sync_undo = kFalse;
}
static void ins_home(int c)
{
if ((fdo_flags & kOptFdoFlagHor) && KeyTyped) {
foldOpenCursor();
}
undisplay_dollar();
pos_T tpos = curwin->w_cursor;
if (c == K_C_HOME) {
curwin->w_cursor.lnum = 1;
}
curwin->w_cursor.col = 0;
curwin->w_cursor.coladd = 0;
curwin->w_curswant = 0;
start_arrow(&tpos);
}
static void ins_end(int c)
{
if ((fdo_flags & kOptFdoFlagHor) && KeyTyped) {
foldOpenCursor();
}
undisplay_dollar();
pos_T tpos = curwin->w_cursor;
if (c == K_C_END) {
curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count;
}
coladvance(curwin, MAXCOL);
curwin->w_curswant = MAXCOL;
start_arrow(&tpos);
}
static void ins_s_left(void)
{
const bool end_change = dont_sync_undo == kFalse; // end undoable change
if ((fdo_flags & kOptFdoFlagHor) && KeyTyped) {
foldOpenCursor();
}
undisplay_dollar();
if (curwin->w_cursor.lnum > 1 || curwin->w_cursor.col > 0) {
start_arrow_with_change(&curwin->w_cursor, end_change);
if (!end_change) {
AppendCharToRedobuff(K_S_LEFT);
}
bck_word(1, false, false);
curwin->w_set_curswant = true;
} else {
vim_beep(kOptBoFlagCursor);
}
dont_sync_undo = kFalse;
}
/// @param end_change end undoable change
static void ins_right(void)
{
const bool end_change = dont_sync_undo == kFalse; // end undoable change
if ((fdo_flags & kOptFdoFlagHor) && KeyTyped) {
foldOpenCursor();
}
undisplay_dollar();
if (gchar_cursor() != NUL || virtual_active(curwin)) {
start_arrow_with_change(&curwin->w_cursor, end_change);
if (!end_change) {
AppendCharToRedobuff(K_RIGHT);
}
curwin->w_set_curswant = true;
if (virtual_active(curwin)) {
oneright();
} else {
curwin->w_cursor.col += utfc_ptr2len(get_cursor_pos_ptr());
}
revins_legal++;
if (revins_chars) {
revins_chars--;
}
} else if (vim_strchr(p_ww, ']') != NULL
&& curwin->w_cursor.lnum < curbuf->b_ml.ml_line_count) {
// if 'whichwrap' set for cursor in insert mode, may move the
// cursor to the next line
start_arrow(&curwin->w_cursor);
curwin->w_set_curswant = true;
curwin->w_cursor.lnum++;
curwin->w_cursor.col = 0;
} else {
vim_beep(kOptBoFlagCursor);
}
dont_sync_undo = kFalse;
}
static void ins_s_right(void)
{
const bool end_change = dont_sync_undo == kFalse; // end undoable change
if ((fdo_flags & kOptFdoFlagHor) && KeyTyped) {
foldOpenCursor();
}
undisplay_dollar();
if (curwin->w_cursor.lnum < curbuf->b_ml.ml_line_count
|| gchar_cursor() != NUL) {
start_arrow_with_change(&curwin->w_cursor, end_change);
if (!end_change) {
AppendCharToRedobuff(K_S_RIGHT);
}
fwd_word(1, false, 0);
curwin->w_set_curswant = true;
} else {
vim_beep(kOptBoFlagCursor);
}
dont_sync_undo = kFalse;
}
/// @param startcol when true move to Insstart.col
static void ins_up(bool startcol)
{
linenr_T old_topline = curwin->w_topline;
int old_topfill = curwin->w_topfill;
undisplay_dollar();
pos_T tpos = curwin->w_cursor;
if (cursor_up(1, true) == OK) {
if (startcol) {
coladvance(curwin, getvcol_nolist(&Insstart));
}
if (old_topline != curwin->w_topline
|| old_topfill != curwin->w_topfill) {
redraw_later(curwin, UPD_VALID);
}
start_arrow(&tpos);
can_cindent = true;
} else {
vim_beep(kOptBoFlagCursor);
}
}
static void ins_pageup(void)
{
undisplay_dollar();
if (mod_mask & MOD_MASK_CTRL) {
// <C-PageUp>: tab page back
if (first_tabpage->tp_next != NULL) {
start_arrow(&curwin->w_cursor);
goto_tabpage(-1);
}
return;
}
pos_T tpos = curwin->w_cursor;
if (pagescroll(BACKWARD, 1, false) == OK) {
start_arrow(&tpos);
can_cindent = true;
} else {
vim_beep(kOptBoFlagCursor);
}
}
/// @param startcol when true move to Insstart.col
static void ins_down(bool startcol)
{
linenr_T old_topline = curwin->w_topline;
int old_topfill = curwin->w_topfill;
undisplay_dollar();
pos_T tpos = curwin->w_cursor;
if (cursor_down(1, true) == OK) {
if (startcol) {
coladvance(curwin, getvcol_nolist(&Insstart));
}
if (old_topline != curwin->w_topline
|| old_topfill != curwin->w_topfill) {
redraw_later(curwin, UPD_VALID);
}
start_arrow(&tpos);
can_cindent = true;
} else {
vim_beep(kOptBoFlagCursor);
}
}
static void ins_pagedown(void)
{
undisplay_dollar();
if (mod_mask & MOD_MASK_CTRL) {
// <C-PageDown>: tab page forward
if (first_tabpage->tp_next != NULL) {
start_arrow(&curwin->w_cursor);
goto_tabpage(0);
}
return;
}
pos_T tpos = curwin->w_cursor;
if (pagescroll(FORWARD, 1, false) == OK) {
start_arrow(&tpos);
can_cindent = true;
} else {
vim_beep(kOptBoFlagCursor);
}
}
/// Handle TAB in Insert or Replace mode.
///
/// @return true when the TAB needs to be inserted like a normal character.
static bool ins_tab(void)
FUNC_ATTR_WARN_UNUSED_RESULT
{
int temp;
if (Insstart_blank_vcol == MAXCOL && curwin->w_cursor.lnum == Insstart.lnum) {
Insstart_blank_vcol = get_nolist_virtcol();
}
if (echeck_abbr(TAB + ABBR_OFF)) {
return false;
}
bool ind = inindent(0);
if (ind) {
can_cindent = false;
}
// When nothing special, insert TAB like a normal character.
if (!curbuf->b_p_et
&& !(
p_sta
&& ind
// These five lines mean 'tabstop' != 'shiftwidth'
&& ((tabstop_count(curbuf->b_p_vts_array) > 1)
|| (tabstop_count(curbuf->b_p_vts_array) == 1
&& tabstop_first(curbuf->b_p_vts_array)
!= get_sw_value(curbuf))
|| (tabstop_count(curbuf->b_p_vts_array) == 0
&& curbuf->b_p_ts != get_sw_value(curbuf))))
&& tabstop_count(curbuf->b_p_vsts_array) == 0 && get_sts_value() == 0) {
return true;
}
if (stop_arrow() == FAIL) {
return true;
}
did_ai = false;
did_si = false;
can_si = false;
can_si_back = false;
AppendToRedobuff("\t");
if (p_sta && ind) { // insert tab in indent, use 'shiftwidth'
temp = get_sw_value(curbuf);
temp -= get_nolist_virtcol() % temp;
} else if (tabstop_count(curbuf->b_p_vsts_array) > 0
|| curbuf->b_p_sts != 0) {
// use 'softtabstop' when set
temp = tabstop_padding(get_nolist_virtcol(),
get_sts_value(),
curbuf->b_p_vsts_array);
} else {
// otherwise use 'tabstop'
temp = tabstop_padding(get_nolist_virtcol(),
curbuf->b_p_ts,
curbuf->b_p_vts_array);
}
// Insert the first space with ins_char(). It will delete one char in
// replace mode. Insert the rest with ins_str(); it will not delete any
// chars. For MODE_VREPLACE state, we use ins_char() for all characters.
ins_char(' ');
while (--temp > 0) {
if (State & VREPLACE_FLAG) {
ins_char(' ');
} else {
ins_str(S_LEN(" "));
if (State & REPLACE_FLAG) { // no char replaced
replace_push_nul();
}
}
}
// When 'expandtab' not set: Replace spaces by TABs where possible.
if (!curbuf->b_p_et && (tabstop_count(curbuf->b_p_vsts_array) > 0
|| get_sts_value() > 0
|| (p_sta && ind))) {
char *ptr;
char *saved_line = NULL; // init for GCC
pos_T pos;
pos_T *cursor;
colnr_T want_vcol, vcol;
int change_col = -1;
int save_list = curwin->w_p_list;
// Get the current line. For MODE_VREPLACE state, don't make real
// changes yet, just work on a copy of the line.
if (State & VREPLACE_FLAG) {
pos = curwin->w_cursor;
cursor = &pos;
saved_line = xstrnsave(get_cursor_line_ptr(), (size_t)get_cursor_line_len());
ptr = saved_line + pos.col;
} else {
ptr = get_cursor_pos_ptr();
cursor = &curwin->w_cursor;
}
// When 'L' is not in 'cpoptions' a tab always takes up 'ts' spaces.
if (vim_strchr(p_cpo, CPO_LISTWM) == NULL) {
curwin->w_p_list = false;
}
// Find first white before the cursor
pos_T fpos = curwin->w_cursor;
while (fpos.col > 0 && ascii_iswhite(ptr[-1])) {
fpos.col--;
ptr--;
}
// In Replace mode, don't change characters before the insert point.
if ((State & REPLACE_FLAG)
&& fpos.lnum == Insstart.lnum
&& fpos.col < Insstart.col) {
ptr += Insstart.col - fpos.col;
fpos.col = Insstart.col;
}
// compute virtual column numbers of first white and cursor
getvcol(curwin, &fpos, &vcol, NULL, NULL);
getvcol(curwin, cursor, &want_vcol, NULL, NULL);
char *tab = "\t";
int32_t tab_v = (uint8_t)(*tab);
CharsizeArg csarg;
CSType cstype = init_charsize_arg(&csarg, curwin, 0, tab);
// Use as many TABs as possible. Beware of 'breakindent', 'showbreak'
// and 'linebreak' adding extra virtual columns.
while (ascii_iswhite(*ptr)) {
int i = win_charsize(cstype, vcol, tab, tab_v, &csarg).width;
if (vcol + i > want_vcol) {
break;
}
if (*ptr != TAB) {
*ptr = TAB;
if (change_col < 0) {
change_col = fpos.col; // Column of first change
// May have to adjust Insstart
if (fpos.lnum == Insstart.lnum && fpos.col < Insstart.col) {
Insstart.col = fpos.col;
}
}
}
fpos.col++;
ptr++;
vcol += i;
}
if (change_col >= 0) {
int repl_off = 0;
// Skip over the spaces we need.
cstype = init_charsize_arg(&csarg, curwin, 0, ptr);
while (vcol < want_vcol && *ptr == ' ') {
vcol += win_charsize(cstype, vcol, ptr, (uint8_t)(' '), &csarg).width;
ptr++;
repl_off++;
}
if (vcol > want_vcol) {
// Must have a char with 'showbreak' just before it.
ptr--;
repl_off--;
}
fpos.col += repl_off;
// Delete following spaces.
int i = cursor->col - fpos.col;
if (i > 0) {
if (!(State & VREPLACE_FLAG)) {
char *newp = xmalloc((size_t)(curbuf->b_ml.ml_line_len - i));
ptrdiff_t col = ptr - curbuf->b_ml.ml_line_ptr;
if (col > 0) {
memmove(newp, ptr - col, (size_t)col);
}
memmove(newp + col, ptr + i, (size_t)(curbuf->b_ml.ml_line_len - col - i));
if (curbuf->b_ml.ml_flags & (ML_LINE_DIRTY | ML_ALLOCATED)) {
xfree(curbuf->b_ml.ml_line_ptr);
}
curbuf->b_ml.ml_line_ptr = newp;
curbuf->b_ml.ml_line_len -= i;
curbuf->b_ml.ml_flags = (curbuf->b_ml.ml_flags | ML_LINE_DIRTY) & ~ML_EMPTY;
inserted_bytes(fpos.lnum, change_col,
cursor->col - change_col, fpos.col - change_col);
} else {
STRMOVE(ptr, ptr + i);
}
// correct replace stack.
if ((State & REPLACE_FLAG) && !(State & VREPLACE_FLAG)) {
for (temp = i; --temp >= 0;) {
replace_join(repl_off);
}
}
}
cursor->col -= i;
// In MODE_VREPLACE state, we haven't changed anything yet. Do it
// now by backspacing over the changed spacing and then inserting
// the new spacing.
if (State & VREPLACE_FLAG) {
// Backspace from real cursor to change_col
backspace_until_column(change_col);
// Insert each char in saved_line from changed_col to
// ptr-cursor
ins_bytes_len(saved_line + change_col, (size_t)(cursor->col - change_col));
}
}
if (State & VREPLACE_FLAG) {
xfree(saved_line);
}
curwin->w_p_list = save_list;
}
return false;
}
/// Handle CR or NL in insert mode.
///
/// @return false when it can't undo.
bool ins_eol(int c)
{
if (echeck_abbr(c + ABBR_OFF)) {
return true;
}
if (stop_arrow() == FAIL) {
return false;
}
undisplay_dollar();
// Strange Vi behaviour: In Replace mode, typing a NL will not delete the
// character under the cursor. Only push a NUL on the replace stack,
// nothing to put back when the NL is deleted.
if ((State & REPLACE_FLAG) && !(State & VREPLACE_FLAG)) {
replace_push_nul();
}
// In MODE_VREPLACE state, a NL replaces the rest of the line, and starts
// replacing the next line, so we push all of the characters left on the
// line onto the replace stack. This is not done here though, it is done
// in open_line().
// Put cursor on NUL if on the last char and coladd is 1 (happens after
// CTRL-O).
if (virtual_active(curwin) && curwin->w_cursor.coladd > 0) {
coladvance(curwin, getviscol());
}
// NL in reverse insert will always start in the end of current line.
if (revins_on) {
curwin->w_cursor.col += get_cursor_pos_len();
}
AppendToRedobuff(NL_STR);
bool i = open_line(FORWARD,
has_format_option(FO_RET_COMS) ? OPENLINE_DO_COM : 0,
old_indent, NULL);
old_indent = 0;
can_cindent = true;
// When inserting a line the cursor line must never be in a closed fold.
foldOpenCursor();
return i;
}
// Handle digraph in insert mode.
// Returns character still to be inserted, or NUL when nothing remaining to be
// done.
static int ins_digraph(void)
{
bool did_putchar = false;
pc_status = PC_STATUS_UNSET;
if (redrawing() && !char_avail()) {
// may need to redraw when no more chars available now
ins_redraw(false);
edit_putchar('?', true);
did_putchar = true;
add_to_showcmd_c(Ctrl_K);
}
// don't map the digraph chars. This also prevents the
// mode message to be deleted when ESC is hit
no_mapping++;
allow_keys++;
int c = plain_vgetc();
no_mapping--;
allow_keys--;
if (did_putchar) {
// when the line fits in 'columns' the '?' is at the start of the next
// line and will not be removed by the redraw
edit_unputchar();
}
if (IS_SPECIAL(c) || mod_mask) { // special key
clear_showcmd();
insert_special(c, true, false);
return NUL;
}
if (c != ESC) {
did_putchar = false;
if (redrawing() && !char_avail()) {
// may need to redraw when no more chars available now
ins_redraw(false);
if (char2cells(c) == 1) {
ins_redraw(false);
edit_putchar(c, true);
did_putchar = true;
}
add_to_showcmd_c(c);
}
no_mapping++;
allow_keys++;
int cc = plain_vgetc();
no_mapping--;
allow_keys--;
if (did_putchar) {
// when the line fits in 'columns' the '?' is at the start of the
// next line and will not be removed by a redraw
edit_unputchar();
}
if (cc != ESC) {
AppendToRedobuff(CTRL_V_STR);
c = digraph_get(c, cc, true);
clear_showcmd();
return c;
}
}
clear_showcmd();
return NUL;
}
// Handle CTRL-E and CTRL-Y in Insert mode: copy char from other line.
// Returns the char to be inserted, or NUL if none found.
int ins_copychar(linenr_T lnum)
{
if (lnum < 1 || lnum > curbuf->b_ml.ml_line_count) {
vim_beep(kOptBoFlagCopy);
return NUL;
}
// try to advance to the cursor column
validate_virtcol(curwin);
int const end_vcol = curwin->w_virtcol;
char *line = ml_get(lnum);
CharsizeArg csarg;
CSType cstype = init_charsize_arg(&csarg, curwin, lnum, line);
StrCharInfo ci = utf_ptr2StrCharInfo(line);
int vcol = 0;
while (vcol < end_vcol && *ci.ptr != NUL) {
vcol += win_charsize(cstype, vcol, ci.ptr, ci.chr.value, &csarg).width;
if (vcol > end_vcol) {
break;
}
ci = utfc_next(ci);
}
int c = ci.chr.value < 0 ? (uint8_t)(*ci.ptr) : ci.chr.value;
if (c == NUL) {
vim_beep(kOptBoFlagCopy);
}
return c;
}
// CTRL-Y or CTRL-E typed in Insert mode.
static int ins_ctrl_ey(int tc)
{
int c = tc;
if (ctrl_x_mode_scroll()) {
if (c == Ctrl_Y) {
scrolldown_clamp();
} else {
scrollup_clamp();
}
redraw_later(curwin, UPD_VALID);
} else {
c = ins_copychar(curwin->w_cursor.lnum + (c == Ctrl_Y ? -1 : 1));
if (c != NUL) {
// The character must be taken literally, insert like it
// was typed after a CTRL-V, and pretend 'textwidth'
// wasn't set. Digits, 'o' and 'x' are special after a
// CTRL-V, don't use it for these.
if (c < 256 && !isalnum(c)) {
AppendToRedobuff(CTRL_V_STR);
}
OptInt tw_save = curbuf->b_p_tw;
curbuf->b_p_tw = -1;
insert_special(c, true, false);
curbuf->b_p_tw = tw_save;
revins_chars++;
revins_legal++;
c = Ctrl_V; // pretend CTRL-V is last character
auto_format(false, true);
}
}
return c;
}
// Try to do some very smart auto-indenting.
// Used when inserting a "normal" character.
static void ins_try_si(int c)
{
pos_T *pos;
// do some very smart indenting when entering '{' or '}'
if (((did_si || can_si_back) && c == '{') || (can_si && c == '}' && inindent(0))) {
pos_T old_pos;
char *ptr;
int i;
bool temp;
// for '}' set indent equal to indent of line containing matching '{'
if (c == '}' && (pos = findmatch(NULL, '{')) != NULL) {
old_pos = curwin->w_cursor;
// If the matching '{' has a ')' immediately before it (ignoring
// white-space), then line up with the start of the line
// containing the matching '(' if there is one. This handles the
// case where an "if (..\n..) {" statement continues over multiple
// lines -- webb
ptr = ml_get(pos->lnum);
i = pos->col;
if (i > 0) { // skip blanks before '{'
while (--i > 0 && ascii_iswhite(ptr[i])) {}
}
curwin->w_cursor.lnum = pos->lnum;
curwin->w_cursor.col = i;
if (ptr[i] == ')' && (pos = findmatch(NULL, '(')) != NULL) {
curwin->w_cursor = *pos;
}
i = get_indent();
curwin->w_cursor = old_pos;
if (State & VREPLACE_FLAG) {
change_indent(INDENT_SET, i, false, true);
} else {
set_indent(i, SIN_CHANGED);
}
} else if (curwin->w_cursor.col > 0) {
// when inserting '{' after "O" reduce indent, but not
// more than indent of previous line
temp = true;
if (c == '{' && can_si_back && curwin->w_cursor.lnum > 1) {
old_pos = curwin->w_cursor;
i = get_indent();
while (curwin->w_cursor.lnum > 1) {
ptr = skipwhite(ml_get(--(curwin->w_cursor.lnum)));
// ignore empty lines and lines starting with '#'.
if (*ptr != '#' && *ptr != NUL) {
break;
}
}
if (get_indent() >= i) {
temp = false;
}
curwin->w_cursor = old_pos;
}
if (temp) {
shift_line(true, false, 1, true);
}
}
}
// set indent of '#' always to 0
if (curwin->w_cursor.col > 0 && can_si && c == '#' && inindent(0)) {
// remember current indent for next line
old_indent = get_indent();
set_indent(0, SIN_CHANGED);
}
// Adjust ai_col, the char at this position can be deleted.
ai_col = MIN(ai_col, curwin->w_cursor.col);
}
// Get the value that w_virtcol would have when 'list' is off.
// Unless 'cpo' contains the 'L' flag.
colnr_T get_nolist_virtcol(void)
{
// check validity of cursor in current buffer
if (curwin->w_buffer == NULL || curwin->w_buffer->b_ml.ml_mfp == NULL
|| curwin->w_cursor.lnum > curwin->w_buffer->b_ml.ml_line_count) {
return 0;
}
if (curwin->w_p_list && vim_strchr(p_cpo, CPO_LISTWM) == NULL) {
return getvcol_nolist(&curwin->w_cursor);
}
validate_virtcol(curwin);
return curwin->w_virtcol;
}
// Handle the InsertCharPre autocommand.
// "c" is the character that was typed.
// Return a pointer to allocated memory with the replacement string.
// Return NULL to continue inserting "c".
static char *do_insert_char_pre(int c)
{
char buf[MB_MAXBYTES + 1];
const int save_State = State;
if (c == Ctrl_RSB) {
return NULL;
}
// Return quickly when there is nothing to do.
if (!has_event(EVENT_INSERTCHARPRE)) {
return NULL;
}
size_t buflen = (size_t)utf_char2bytes(c, buf);
buf[buflen] = NUL;
// Lock the text to avoid weird things from happening.
textlock++;
set_vim_var_string(VV_CHAR, buf, (ptrdiff_t)buflen); // set v:char
char *res = NULL;
if (ins_apply_autocmds(EVENT_INSERTCHARPRE)) {
// Get the value of v:char. It may be empty or more than one
// character. Only use it when changed, otherwise continue with the
// original character to avoid breaking autoindent.
if (strcmp(buf, get_vim_var_str(VV_CHAR)) != 0) {
res = xstrdup(get_vim_var_str(VV_CHAR));
}
}
set_vim_var_string(VV_CHAR, NULL, -1);
textlock--;
// Restore the State, it may have been changed.
State = save_State;
return res;
}
bool get_can_cindent(void)
{
return can_cindent;
}
void set_can_cindent(bool val)
{
can_cindent = val;
}
/// Trigger "event" and take care of fixing undo.
int ins_apply_autocmds(event_T event)
{
varnumber_T tick = buf_get_changedtick(curbuf);
int r = apply_autocmds(event, NULL, NULL, false, curbuf);
// If u_savesub() was called then we are not prepared to start
// a new line. Call u_save() with no contents to fix that.
// Except when leaving Insert mode.
if (event != EVENT_INSERTLEAVE && tick != buf_get_changedtick(curbuf)) {
u_save(curwin->w_cursor.lnum, (linenr_T)(curwin->w_cursor.lnum + 1));
}
return r;
}