Files
neovim/src/nvim/change.c
bfredl ea4e9c71cc refactor(plines): use a struct for chartabsize state
This is a refactor extracted from vim-patch 9.0.0067: cannot show virtual text

The logic for inline virtual text is going to be different in nvim than
text property based text in vim, but this refactor is still useful,
as calculation of displayed linesize is going to be stateful in a
similar way.
2022-08-29 12:05:34 +02:00

2208 lines
72 KiB
C

// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
/// change.c: functions related to changing text
#include "nvim/assert.h"
#include "nvim/buffer.h"
#include "nvim/buffer_updates.h"
#include "nvim/change.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/diff.h"
#include "nvim/drawscreen.h"
#include "nvim/edit.h"
#include "nvim/eval.h"
#include "nvim/extmark.h"
#include "nvim/fileio.h"
#include "nvim/fold.h"
#include "nvim/indent.h"
#include "nvim/indent_c.h"
#include "nvim/insexpand.h"
#include "nvim/mark.h"
#include "nvim/memline.h"
#include "nvim/move.h"
#include "nvim/option.h"
#include "nvim/plines.h"
#include "nvim/search.h"
#include "nvim/state.h"
#include "nvim/textformat.h"
#include "nvim/ui.h"
#include "nvim/undo.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "change.c.generated.h"
#endif
/// If the file is readonly, give a warning message with the first change.
/// Don't do this for autocommands.
/// Doesn't use emsg(), because it flushes the macro buffer.
/// If we have undone all changes b_changed will be false, but "b_did_warn"
/// will be true.
/// "col" is the column for the message; non-zero when in insert mode and
/// 'showmode' is on.
/// Careful: may trigger autocommands that reload the buffer.
void change_warning(buf_T *buf, int col)
{
static const char *w_readonly = N_("W10: Warning: Changing a readonly file");
if (buf->b_did_warn == false
&& curbufIsChanged() == 0
&& !autocmd_busy
&& buf->b_p_ro) {
buf->b_ro_locked++;
apply_autocmds(EVENT_FILECHANGEDRO, NULL, NULL, false, buf);
buf->b_ro_locked--;
if (!buf->b_p_ro) {
return;
}
// Do what msg() does, but with a column offset if the warning should
// be after the mode message.
msg_start();
if (msg_row == Rows - 1) {
msg_col = col;
}
msg_source(HL_ATTR(HLF_W));
msg_ext_set_kind("wmsg");
msg_puts_attr(_(w_readonly), HL_ATTR(HLF_W) | MSG_HIST);
set_vim_var_string(VV_WARNINGMSG, _(w_readonly), -1);
msg_clr_eos();
(void)msg_end();
if (msg_silent == 0 && !silent_mode && ui_active()) {
ui_flush();
os_delay(1002L, true); // give the user time to think about it
}
buf->b_did_warn = true;
redraw_cmdline = false; // don't redraw and erase the message
if (msg_row < Rows - 1) {
showmode();
}
}
}
/// Call this function when something in the current buffer is changed.
///
/// Most often called through changed_bytes() and changed_lines(), which also
/// mark the area of the display to be redrawn.
///
/// Careful: may trigger autocommands that reload the buffer.
void changed(void)
{
if (!curbuf->b_changed) {
int save_msg_scroll = msg_scroll;
// Give a warning about changing a read-only file. This may also
// check-out the file, thus change "curbuf"!
change_warning(curbuf, 0);
// Create a swap file if that is wanted.
// Don't do this for "nofile" and "nowrite" buffer types.
if (curbuf->b_may_swap && !bt_dontwrite(curbuf)) {
bool save_need_wait_return = need_wait_return;
need_wait_return = false;
ml_open_file(curbuf);
// The ml_open_file() can cause an ATTENTION message.
// Wait two seconds, to make sure the user reads this unexpected
// message. Since we could be anywhere, call wait_return() now,
// and don't let the emsg() set msg_scroll.
if (need_wait_return && emsg_silent == 0) {
ui_flush();
os_delay(2002L, true);
wait_return(true);
msg_scroll = save_msg_scroll;
} else {
need_wait_return = save_need_wait_return;
}
}
changed_internal();
}
buf_inc_changedtick(curbuf);
// If a pattern is highlighted, the position may now be invalid.
highlight_match = false;
}
/// Internal part of changed(), no user interaction.
/// Also used for recovery.
void changed_internal(void)
{
curbuf->b_changed = true;
curbuf->b_changed_invalid = true;
ml_setflags(curbuf);
redraw_buf_status_later(curbuf);
redraw_tabline = true;
need_maketitle = true; // set window title later
}
/// Common code for when a change was made.
/// See changed_lines() for the arguments.
/// Careful: may trigger autocommands that reload the buffer.
static void changed_common(linenr_T lnum, colnr_T col, linenr_T lnume, linenr_T xtra)
{
// mark the buffer as modified
changed();
if (curwin->w_p_diff && diff_internal()) {
curtab->tp_diff_update = true;
}
// set the '. mark
if ((cmdmod.cmod_flags & CMOD_KEEPJUMPS) == 0) {
fmarkv_T view = INIT_FMARKV;
// Set the markview only if lnum is visible, as changes might be done
// outside of the current window view.
if (lnum >= curwin->w_topline && lnum <= curwin->w_botline) {
view = mark_view_make(curwin->w_topline, curwin->w_cursor);
}
RESET_FMARK(&curbuf->b_last_change, ((pos_T) { lnum, col, 0 }), curbuf->handle, view);
// Create a new entry if a new undo-able change was started or we
// don't have an entry yet.
if (curbuf->b_new_change || curbuf->b_changelistlen == 0) {
int add;
if (curbuf->b_changelistlen == 0) {
add = true;
} else {
// Don't create a new entry when the line number is the same
// as the last one and the column is not too far away. Avoids
// creating many entries for typing "xxxxx".
pos_T *p = &curbuf->b_changelist[curbuf->b_changelistlen - 1].mark;
if (p->lnum != lnum) {
add = true;
} else {
int cols = comp_textwidth(false);
if (cols == 0) {
cols = 79;
}
add = (p->col + cols < col || col + cols < p->col);
}
}
if (add) {
// This is the first of a new sequence of undo-able changes
// and it's at some distance of the last change. Use a new
// position in the changelist.
curbuf->b_new_change = false;
if (curbuf->b_changelistlen == JUMPLISTSIZE) {
// changelist is full: remove oldest entry
curbuf->b_changelistlen = JUMPLISTSIZE - 1;
memmove(curbuf->b_changelist, curbuf->b_changelist + 1,
sizeof(curbuf->b_changelist[0]) * (JUMPLISTSIZE - 1));
FOR_ALL_TAB_WINDOWS(tp, wp) {
// Correct position in changelist for other windows on
// this buffer.
if (wp->w_buffer == curbuf && wp->w_changelistidx > 0) {
wp->w_changelistidx--;
}
}
}
FOR_ALL_TAB_WINDOWS(tp, wp) {
// For other windows, if the position in the changelist is
// at the end it stays at the end.
if (wp->w_buffer == curbuf
&& wp->w_changelistidx == curbuf->b_changelistlen) {
wp->w_changelistidx++;
}
}
curbuf->b_changelistlen++;
}
}
curbuf->b_changelist[curbuf->b_changelistlen - 1] =
curbuf->b_last_change;
// The current window is always after the last change, so that "g,"
// takes you back to it.
curwin->w_changelistidx = curbuf->b_changelistlen;
}
if (VIsual_active) {
check_visual_pos();
}
FOR_ALL_TAB_WINDOWS(tp, wp) {
if (wp->w_buffer == curbuf) {
// Mark this window to be redrawn later.
if (wp->w_redr_type < UPD_VALID) {
wp->w_redr_type = UPD_VALID;
}
// Check if a change in the buffer has invalidated the cached
// values for the cursor.
// Update the folds for this window. Can't postpone this, because
// a following operator might work on the whole fold: ">>dd".
linenr_T last = lnume + xtra - 1; // last line after the change
foldUpdate(wp, lnum, last);
// The change may cause lines above or below the change to become
// included in a fold. Set lnum/lnume to the first/last line that
// might be displayed differently.
// Set w_cline_folded here as an efficient way to update it when
// inserting lines just above a closed fold.
bool folded = hasFoldingWin(wp, lnum, &lnum, NULL, false, NULL);
if (wp->w_cursor.lnum == lnum) {
wp->w_cline_folded = folded;
}
folded = hasFoldingWin(wp, last, NULL, &last, false, NULL);
if (wp->w_cursor.lnum == last) {
wp->w_cline_folded = folded;
}
// If the changed line is in a range of previously folded lines,
// compare with the first line in that range.
if (wp->w_cursor.lnum <= lnum) {
int i = find_wl_entry(wp, lnum);
if (i >= 0 && wp->w_cursor.lnum > wp->w_lines[i].wl_lnum) {
changed_line_abv_curs_win(wp);
}
}
if (wp->w_cursor.lnum > lnum) {
changed_line_abv_curs_win(wp);
} else if (wp->w_cursor.lnum == lnum && wp->w_cursor.col >= col) {
changed_cline_bef_curs_win(wp);
}
if (wp->w_botline >= lnum) {
// Assume that botline doesn't change (inserted lines make
// other lines scroll down below botline).
approximate_botline_win(wp);
}
// Check if any w_lines[] entries have become invalid.
// For entries below the change: Correct the lnums for
// inserted/deleted lines. Makes it possible to stop displaying
// after the change.
for (int i = 0; i < wp->w_lines_valid; i++) {
if (wp->w_lines[i].wl_valid) {
if (wp->w_lines[i].wl_lnum >= lnum) {
if (wp->w_lines[i].wl_lnum < lnume) {
// line included in change
wp->w_lines[i].wl_valid = false;
} else if (xtra != 0) {
// line below change
wp->w_lines[i].wl_lnum += xtra;
wp->w_lines[i].wl_lastlnum += xtra;
}
} else if (wp->w_lines[i].wl_lastlnum >= lnum) {
// change somewhere inside this range of folded lines,
// may need to be redrawn
wp->w_lines[i].wl_valid = false;
}
}
}
// Take care of side effects for setting w_topline when folds have
// changed. Esp. when the buffer was changed in another window.
if (hasAnyFolding(wp)) {
set_topline(wp, wp->w_topline);
}
// If lines have been added or removed, relative numbering always
// requires a redraw.
if (wp->w_p_rnu && xtra != 0) {
wp->w_last_cursor_lnum_rnu = 0;
redraw_later(wp, UPD_VALID);
}
// Cursor line highlighting probably need to be updated with
// "UPD_VALID" if it's below the change.
// If the cursor line is inside the change we need to redraw more.
if (wp->w_p_cul) {
if (xtra == 0) {
redraw_later(wp, UPD_VALID);
} else if (lnum <= wp->w_last_cursorline) {
redraw_later(wp, UPD_SOME_VALID);
}
}
}
}
// Call update_screen() later, which checks out what needs to be redrawn,
// since it notices b_mod_set and then uses b_mod_*.
if (must_redraw < UPD_VALID) {
must_redraw = UPD_VALID;
}
// when the cursor line is changed always trigger CursorMoved
if (lnum <= curwin->w_cursor.lnum
&& lnume + (xtra < 0 ? -xtra : xtra) > curwin->w_cursor.lnum) {
curwin->w_last_cursormoved.lnum = 0;
}
}
static void changedOneline(buf_T *buf, linenr_T lnum)
{
if (buf->b_mod_set) {
// find the maximum area that must be redisplayed
if (lnum < buf->b_mod_top) {
buf->b_mod_top = lnum;
} else if (lnum >= buf->b_mod_bot) {
buf->b_mod_bot = lnum + 1;
}
} else {
// set the area that must be redisplayed to one line
buf->b_mod_set = true;
buf->b_mod_top = lnum;
buf->b_mod_bot = lnum + 1;
buf->b_mod_xlines = 0;
}
}
/// Changed bytes within a single line for the current buffer.
/// - marks the windows on this buffer to be redisplayed
/// - marks the buffer changed by calling changed()
/// - invalidates cached values
/// Careful: may trigger autocommands that reload the buffer.
void changed_bytes(linenr_T lnum, colnr_T col)
{
changedOneline(curbuf, lnum);
changed_common(lnum, col, lnum + 1, 0);
// notify any channels that are watching
buf_updates_send_changes(curbuf, lnum, 1, 1);
// Diff highlighting in other diff windows may need to be updated too.
if (curwin->w_p_diff) {
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (wp->w_p_diff && wp != curwin) {
redraw_later(wp, UPD_VALID);
linenr_T wlnum = diff_lnum_win(lnum, wp);
if (wlnum > 0) {
changedOneline(wp->w_buffer, wlnum);
}
}
}
}
}
/// insert/delete bytes at column
///
/// Like changed_bytes() but also adjust extmark for "new" bytes.
void inserted_bytes(linenr_T lnum, colnr_T col, int old, int new)
{
if (curbuf_splice_pending == 0) {
extmark_splice_cols(curbuf, (int)lnum - 1, col, old, new, kExtmarkUndo);
}
changed_bytes(lnum, col);
}
/// Appended "count" lines below line "lnum" in the current buffer.
/// Must be called AFTER the change and after mark_adjust().
/// Takes care of marking the buffer to be redrawn and sets the changed flag.
void appended_lines(linenr_T lnum, linenr_T count)
{
changed_lines(lnum + 1, 0, lnum + 1, count, true);
}
/// Like appended_lines(), but adjust marks first.
void appended_lines_mark(linenr_T lnum, long count)
{
// Skip mark_adjust when adding a line after the last one, there can't
// be marks there. But it's still needed in diff mode.
if (lnum + count < curbuf->b_ml.ml_line_count || curwin->w_p_diff) {
mark_adjust(lnum + 1, (linenr_T)MAXLNUM, (linenr_T)count, 0L, kExtmarkUndo);
} else {
extmark_adjust(curbuf, lnum + 1, (linenr_T)MAXLNUM, (linenr_T)count, 0L,
kExtmarkUndo);
}
changed_lines(lnum + 1, 0, lnum + 1, (linenr_T)count, true);
}
/// Deleted "count" lines at line "lnum" in the current buffer.
/// Must be called AFTER the change and after mark_adjust().
/// Takes care of marking the buffer to be redrawn and sets the changed flag.
void deleted_lines(linenr_T lnum, linenr_T count)
{
changed_lines(lnum, 0, lnum + count, -count, true);
}
/// Like deleted_lines(), but adjust marks first.
/// Make sure the cursor is on a valid line before calling, a GUI callback may
/// be triggered to display the cursor.
void deleted_lines_mark(linenr_T lnum, long count)
{
bool made_empty = (count > 0) && curbuf->b_ml.ml_flags & ML_EMPTY;
mark_adjust(lnum, (linenr_T)(lnum + count - 1), MAXLNUM, -(linenr_T)count, kExtmarkNOOP);
// if we deleted the entire buffer, we need to implicitly add a new empty line
extmark_adjust(curbuf, lnum, (linenr_T)(lnum + count - 1), MAXLNUM,
-(linenr_T)count + (made_empty ? 1 : 0), kExtmarkUndo);
changed_lines(lnum, 0, lnum + (linenr_T)count, (linenr_T)(-count), true);
}
/// Marks the area to be redrawn after a change.
///
/// @param buf the buffer where lines were changed
/// @param lnum first line with change
/// @param lnume line below last changed line
/// @param xtra number of extra lines (negative when deleting)
void changed_lines_buf(buf_T *buf, linenr_T lnum, linenr_T lnume, linenr_T xtra)
{
if (buf->b_mod_set) {
// find the maximum area that must be redisplayed
if (lnum < buf->b_mod_top) {
buf->b_mod_top = lnum;
}
if (lnum < buf->b_mod_bot) {
// adjust old bot position for xtra lines
buf->b_mod_bot += xtra;
if (buf->b_mod_bot < lnum) {
buf->b_mod_bot = lnum;
}
}
if (lnume + xtra > buf->b_mod_bot) {
buf->b_mod_bot = lnume + xtra;
}
buf->b_mod_xlines += xtra;
} else {
// set the area that must be redisplayed
buf->b_mod_set = true;
buf->b_mod_top = lnum;
buf->b_mod_bot = lnume + xtra;
buf->b_mod_xlines = xtra;
}
}
/// Changed lines for the current buffer.
/// Must be called AFTER the change and after mark_adjust().
/// - mark the buffer changed by calling changed()
/// - mark the windows on this buffer to be redisplayed
/// - invalidate cached values
/// "lnum" is the first line that needs displaying, "lnume" the first line
/// below the changed lines (BEFORE the change).
/// When only inserting lines, "lnum" and "lnume" are equal.
/// Takes care of calling changed() and updating b_mod_*.
/// Careful: may trigger autocommands that reload the buffer.
///
/// @param lnum first line with change
/// @param col column in first line with change
/// @param lnume line below last changed line
/// @param xtra number of extra lines (negative when deleting)
/// @param do_buf_event some callers like undo/redo call changed_lines() and
/// then increment changedtick *again*. This flag allows these callers to send
/// the nvim_buf_lines_event events after they're done modifying changedtick.
void changed_lines(linenr_T lnum, colnr_T col, linenr_T lnume, linenr_T xtra, bool do_buf_event)
{
changed_lines_buf(curbuf, lnum, lnume, xtra);
if (xtra == 0 && curwin->w_p_diff && !diff_internal()) {
// When the number of lines doesn't change then mark_adjust() isn't
// called and other diff buffers still need to be marked for
// displaying.
linenr_T wlnum;
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (wp->w_p_diff && wp != curwin) {
redraw_later(wp, UPD_VALID);
wlnum = diff_lnum_win(lnum, wp);
if (wlnum > 0) {
changed_lines_buf(wp->w_buffer, wlnum,
lnume - lnum + wlnum, 0L);
}
}
}
}
changed_common(lnum, col, lnume, xtra);
if (do_buf_event) {
int64_t num_added = (int64_t)(lnume + xtra - lnum);
int64_t num_removed = lnume - lnum;
buf_updates_send_changes(curbuf, lnum, num_added, num_removed);
}
}
/// Called when the changed flag must be reset for buffer `buf`.
/// When `ff` is true also reset 'fileformat'.
/// When `always_inc_changedtick` is true b:changedtick is incremented even
/// when the changed flag was off.
void unchanged(buf_T *buf, int ff, bool always_inc_changedtick)
{
if (buf->b_changed || (ff && file_ff_differs(buf, false))) {
buf->b_changed = false;
buf->b_changed_invalid = true;
ml_setflags(buf);
if (ff) {
save_file_ff(buf);
}
redraw_buf_status_later(buf);
redraw_tabline = true;
need_maketitle = true; // set window title later
buf_inc_changedtick(buf);
} else if (always_inc_changedtick) {
buf_inc_changedtick(buf);
}
}
/// Save the current values of 'fileformat' and 'fileencoding', so that we know
/// the file must be considered changed when the value is different.
void save_file_ff(buf_T *buf)
{
buf->b_start_ffc = (unsigned char)(*buf->b_p_ff);
buf->b_start_eol = buf->b_p_eol;
buf->b_start_bomb = buf->b_p_bomb;
// Only use free/alloc when necessary, they take time.
if (buf->b_start_fenc == NULL
|| STRCMP(buf->b_start_fenc, buf->b_p_fenc) != 0) {
xfree(buf->b_start_fenc);
buf->b_start_fenc = xstrdup(buf->b_p_fenc);
}
}
/// Return true if 'fileformat' and/or 'fileencoding' has a different value
/// from when editing started (save_file_ff() called).
/// Also when 'endofline' was changed and 'binary' is set, or when 'bomb' was
/// changed and 'binary' is not set.
/// Also when 'endofline' was changed and 'fixeol' is not set.
/// When "ignore_empty" is true don't consider a new, empty buffer to be
/// changed.
bool file_ff_differs(buf_T *buf, bool ignore_empty)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
// In a buffer that was never loaded the options are not valid.
if (buf->b_flags & BF_NEVERLOADED) {
return false;
}
if (ignore_empty
&& (buf->b_flags & BF_NEW)
&& buf->b_ml.ml_line_count == 1
&& *ml_get_buf(buf, (linenr_T)1, false) == NUL) {
return false;
}
if (buf->b_start_ffc != *buf->b_p_ff) {
return true;
}
if ((buf->b_p_bin || !buf->b_p_fixeol) && buf->b_start_eol != buf->b_p_eol) {
return true;
}
if (!buf->b_p_bin && buf->b_start_bomb != buf->b_p_bomb) {
return true;
}
if (buf->b_start_fenc == NULL) {
return *buf->b_p_fenc != NUL;
}
return STRCMP(buf->b_start_fenc, buf->b_p_fenc) != 0;
}
/// Insert string "p" at the cursor position. Stops at a NUL byte.
/// Handles Replace mode and multi-byte characters.
void ins_bytes(char *p)
{
ins_bytes_len((char_u *)p, STRLEN(p));
}
/// Insert string "p" with length "len" at the cursor position.
/// Handles Replace mode and multi-byte characters.
void ins_bytes_len(char_u *p, size_t len)
{
size_t n;
for (size_t i = 0; i < len; i += n) {
// avoid reading past p[len]
n = (size_t)utfc_ptr2len_len(p + i, (int)(len - i));
ins_char_bytes(p + i, n);
}
}
/// Insert or replace a single character at the cursor position.
/// When in MODE_REPLACE or MODE_VREPLACE state, replace any existing character.
/// Caller must have prepared for undo.
/// For multi-byte characters we get the whole character, the caller must
/// convert bytes to a character.
void ins_char(int c)
{
char_u buf[MB_MAXBYTES + 1];
size_t n = (size_t)utf_char2bytes(c, (char *)buf);
// When "c" is 0x100, 0x200, etc. we don't want to insert a NUL byte.
// Happens for CTRL-Vu9900.
if (buf[0] == 0) {
buf[0] = '\n';
}
ins_char_bytes((char_u *)buf, n);
}
void ins_char_bytes(char_u *buf, size_t charlen)
{
// Break tabs if needed.
if (virtual_active() && curwin->w_cursor.coladd > 0) {
coladvance_force(getviscol());
}
size_t col = (size_t)curwin->w_cursor.col;
linenr_T lnum = curwin->w_cursor.lnum;
char_u *oldp = ml_get(lnum);
size_t linelen = STRLEN(oldp) + 1; // length of old line including NUL
// The lengths default to the values for when not replacing.
size_t oldlen = 0; // nr of bytes inserted
size_t newlen = charlen; // nr of bytes deleted (0 when not replacing)
if (State & REPLACE_FLAG) {
if (State & VREPLACE_FLAG) {
// Disable 'list' temporarily, unless 'cpo' contains the 'L' flag.
// Returns the old value of list, so when finished,
// curwin->w_p_list should be set back to this.
int old_list = curwin->w_p_list;
if (old_list && vim_strchr(p_cpo, CPO_LISTWM) == NULL) {
curwin->w_p_list = false;
}
// In virtual replace mode each character may replace one or more
// characters (zero if it's a TAB). Count the number of bytes to
// be deleted to make room for the new character, counting screen
// cells. May result in adding spaces to fill a gap.
colnr_T vcol;
getvcol(curwin, &curwin->w_cursor, NULL, &vcol, NULL);
colnr_T new_vcol = vcol + win_chartabsize(curwin, (char *)buf, vcol);
while (oldp[col + oldlen] != NUL && vcol < new_vcol) {
vcol += win_chartabsize(curwin, (char *)oldp + col + oldlen, vcol);
// Don't need to remove a TAB that takes us to the right
// position.
if (vcol > new_vcol && oldp[col + oldlen] == TAB) {
break;
}
oldlen += (size_t)utfc_ptr2len((char *)oldp + col + oldlen);
// Deleted a bit too much, insert spaces.
if (vcol > new_vcol) {
newlen += (size_t)(vcol - new_vcol);
}
}
curwin->w_p_list = old_list;
} else if (oldp[col] != NUL) {
// normal replace
oldlen = (size_t)utfc_ptr2len((char *)oldp + col);
}
// Push the replaced bytes onto the replace stack, so that they can be
// put back when BS is used. The bytes of a multi-byte character are
// done the other way around, so that the first byte is popped off
// first (it tells the byte length of the character).
replace_push(NUL);
for (size_t i = 0; i < oldlen; i++) {
i += (size_t)replace_push_mb(oldp + col + i) - 1;
}
}
char_u *newp = xmalloc(linelen + newlen - oldlen);
// Copy bytes before the cursor.
if (col > 0) {
memmove(newp, oldp, col);
}
// Copy bytes after the changed character(s).
char_u *p = newp + col;
if (linelen > col + oldlen) {
memmove(p + newlen, oldp + col + oldlen,
(size_t)(linelen - col - oldlen));
}
// Insert or overwrite the new character.
memmove(p, buf, charlen);
// Fill with spaces when necessary.
for (size_t i = charlen; i < newlen; i++) {
p[i] = ' ';
}
// Replace the line in the buffer.
ml_replace(lnum, (char *)newp, false);
// mark the buffer as changed and prepare for displaying
inserted_bytes(lnum, (colnr_T)col, (int)oldlen, (int)newlen);
// If we're in Insert or Replace mode and 'showmatch' is set, then briefly
// show the match for right parens and braces.
if (p_sm && (State & MODE_INSERT)
&& msg_silent == 0
&& !ins_compl_active()) {
showmatch(utf_ptr2char((char *)buf));
}
if (!p_ri || (State & REPLACE_FLAG)) {
// Normal insert: move cursor right
curwin->w_cursor.col += (int)charlen;
}
// TODO(Bram): should try to update w_row here, to avoid recomputing it later.
}
/// Insert a string at the cursor position.
/// Note: Does NOT handle Replace mode.
/// Caller must have prepared for undo.
void ins_str(char_u *s)
{
int newlen = (int)STRLEN(s);
linenr_T lnum = curwin->w_cursor.lnum;
if (virtual_active() && curwin->w_cursor.coladd > 0) {
coladvance_force(getviscol());
}
colnr_T col = curwin->w_cursor.col;
char_u *oldp = ml_get(lnum);
int oldlen = (int)STRLEN(oldp);
char_u *newp = (char_u *)xmalloc((size_t)oldlen + (size_t)newlen + 1);
if (col > 0) {
memmove(newp, oldp, (size_t)col);
}
memmove(newp + col, s, (size_t)newlen);
int bytes = oldlen - col + 1;
assert(bytes >= 0);
memmove(newp + col + newlen, oldp + col, (size_t)bytes);
ml_replace(lnum, (char *)newp, false);
inserted_bytes(lnum, col, 0, newlen);
curwin->w_cursor.col += newlen;
}
// Delete one character under the cursor.
// If "fixpos" is true, don't leave the cursor on the NUL after the line.
// Caller must have prepared for undo.
//
// return FAIL for failure, OK otherwise
int del_char(bool fixpos)
{
// Make sure the cursor is at the start of a character.
mb_adjust_cursor();
if (*get_cursor_pos_ptr() == NUL) {
return FAIL;
}
return del_chars(1L, fixpos);
}
/// Like del_bytes(), but delete characters instead of bytes.
int del_chars(long count, int fixpos)
{
int bytes = 0;
char_u *p = get_cursor_pos_ptr();
for (long i = 0; i < count && *p != NUL; i++) {
int l = utfc_ptr2len((char *)p);
bytes += l;
p += l;
}
return del_bytes(bytes, fixpos, true);
}
/// Delete "count" bytes under the cursor.
/// If "fixpos" is true, don't leave the cursor on the NUL after the line.
/// Caller must have prepared for undo.
///
/// @param count number of bytes to be deleted
/// @param fixpos_arg leave the cursor on the NUL after the line
/// @param use_delcombine 'delcombine' option applies
///
/// @return FAIL for failure, OK otherwise
int del_bytes(colnr_T count, bool fixpos_arg, bool use_delcombine)
{
linenr_T lnum = curwin->w_cursor.lnum;
colnr_T col = curwin->w_cursor.col;
bool fixpos = fixpos_arg;
char_u *oldp = ml_get(lnum);
colnr_T oldlen = (colnr_T)STRLEN(oldp);
// Can't do anything when the cursor is on the NUL after the line.
if (col >= oldlen) {
return FAIL;
}
// If "count" is zero there is nothing to do.
if (count == 0) {
return OK;
}
// If "count" is negative the caller must be doing something wrong.
if (count < 1) {
siemsg("E292: Invalid count for del_bytes(): %ld", (int64_t)count);
return FAIL;
}
// If 'delcombine' is set and deleting (less than) one character, only
// delete the last combining character.
if (p_deco && use_delcombine
&& utfc_ptr2len((char *)oldp + col) >= count) {
int cc[MAX_MCO];
(void)utfc_ptr2char(oldp + col, cc);
if (cc[0] != NUL) {
// Find the last composing char, there can be several.
int n = col;
do {
col = n;
count = utf_ptr2len((char *)oldp + n);
n += count;
} while (utf_composinglike(oldp + col, oldp + n));
fixpos = false;
}
}
// When count is too big, reduce it.
int movelen = oldlen - col - count + 1; // includes trailing NUL
if (movelen <= 1) {
// If we just took off the last character of a non-blank line, and
// fixpos is true, we don't want to end up positioned at the NUL,
// unless "restart_edit" is set or 'virtualedit' contains "onemore".
if (col > 0 && fixpos && restart_edit == 0
&& (get_ve_flags() & VE_ONEMORE) == 0) {
curwin->w_cursor.col--;
curwin->w_cursor.coladd = 0;
curwin->w_cursor.col -= utf_head_off(oldp, oldp + curwin->w_cursor.col);
}
count = oldlen - col;
movelen = 1;
}
// If the old line has been allocated the deletion can be done in the
// existing line. Otherwise a new line has to be allocated.
bool was_alloced = ml_line_alloced(); // check if oldp was allocated
char_u *newp;
if (was_alloced) {
ml_add_deleted_len(curbuf->b_ml.ml_line_ptr, oldlen);
newp = oldp; // use same allocated memory
} else { // need to allocate a new line
newp = xmalloc((size_t)(oldlen + 1 - count));
memmove(newp, oldp, (size_t)col);
}
memmove(newp + col, oldp + col + count, (size_t)movelen);
if (!was_alloced) {
ml_replace(lnum, (char *)newp, false);
}
// mark the buffer as changed and prepare for displaying
inserted_bytes(lnum, col, count, 0);
return OK;
}
/// Copy the indent from ptr to the current line (and fill to size).
/// Leaves the cursor on the first non-blank in the line.
/// @return true if the line was changed.
int copy_indent(int size, char_u *src)
{
char_u *p = NULL;
char_u *line = NULL;
int ind_len;
int line_len = 0;
int tab_pad;
// Round 1: compute the number of characters needed for the indent
// Round 2: copy the characters.
for (int round = 1; round <= 2; round++) {
int todo = size;
ind_len = 0;
int ind_done = 0;
int ind_col = 0;
char_u *s = src;
// Count/copy the usable portion of the source line.
while (todo > 0 && ascii_iswhite(*s)) {
if (*s == TAB) {
tab_pad = tabstop_padding(ind_done,
curbuf->b_p_ts,
curbuf->b_p_vts_array);
// Stop if this tab will overshoot the target.
if (todo < tab_pad) {
break;
}
todo -= tab_pad;
ind_done += tab_pad;
ind_col += tab_pad;
} else {
todo--;
ind_done++;
ind_col++;
}
ind_len++;
if (p != NULL) {
*p++ = *s;
}
s++;
}
// Fill to next tabstop with a tab, if possible.
tab_pad = tabstop_padding(ind_done, curbuf->b_p_ts, curbuf->b_p_vts_array);
if ((todo >= tab_pad) && !curbuf->b_p_et) {
todo -= tab_pad;
ind_len++;
ind_col += tab_pad;
if (p != NULL) {
*p++ = TAB;
}
}
// Add tabs required for indent.
if (!curbuf->b_p_et) {
for (;;) {
tab_pad = tabstop_padding(ind_col,
curbuf->b_p_ts,
curbuf->b_p_vts_array);
if (todo < tab_pad) {
break;
}
todo -= tab_pad;
ind_len++;
ind_col += tab_pad;
if (p != NULL) {
*p++ = TAB;
}
}
}
// Count/add spaces required for indent.
while (todo > 0) {
todo--;
ind_len++;
if (p != NULL) {
*p++ = ' ';
}
}
if (p == NULL) {
// Allocate memory for the result: the copied indent, new indent
// and the rest of the line.
line_len = (int)STRLEN(get_cursor_line_ptr()) + 1;
assert(ind_len + line_len >= 0);
size_t line_size;
STRICT_ADD(ind_len, line_len, &line_size, size_t);
line = xmalloc(line_size);
p = line;
}
}
// Append the original line
memmove(p, get_cursor_line_ptr(), (size_t)line_len);
// Replace the line
ml_replace(curwin->w_cursor.lnum, (char *)line, false);
// Put the cursor after the indent.
curwin->w_cursor.col = ind_len;
return true;
}
/// open_line: Add a new line below or above the current line.
///
/// For MODE_VREPLACE state, we only add a new line when we get to the end of
/// the file, otherwise we just start replacing the next line.
///
/// Caller must take care of undo. Since MODE_VREPLACE may affect any number of
/// lines however, it may call u_save_cursor() again when starting to change a
/// new line.
/// "flags": OPENLINE_DELSPACES delete spaces after cursor
/// OPENLINE_DO_COM format comments
/// OPENLINE_KEEPTRAIL keep trailing spaces
/// OPENLINE_MARKFIX adjust mark positions after the line break
/// OPENLINE_COM_LIST format comments with list or 2nd line indent
///
/// "second_line_indent": indent for after ^^D in Insert mode or if flag
/// OPENLINE_COM_LIST
/// "did_do_comment" is set to true when intentionally putting the comment
/// leader in fromt of the new line.
///
/// @param dir FORWARD or BACKWARD
///
/// @return true on success, false on failure
int open_line(int dir, int flags, int second_line_indent, bool *did_do_comment)
{
char_u *next_line = NULL; // copy of the next line
char_u *p_extra = NULL; // what goes to next line
colnr_T less_cols = 0; // less columns for mark in new line
colnr_T less_cols_off = 0; // columns to skip for mark adjust
pos_T old_cursor; // old cursor position
colnr_T newcol = 0; // new cursor column
int newindent = 0; // auto-indent of the new line
bool trunc_line = false; // truncate current line afterwards
bool retval = false; // return value
int extra_len = 0; // length of p_extra string
int lead_len; // length of comment leader
int comment_start = 0; // start index of the comment leader
char *lead_flags; // position in 'comments' for comment leader
char *leader = NULL; // copy of comment leader
char_u *allocated = NULL; // allocated memory
char *p;
char_u saved_char = NUL; // init for GCC
pos_T *pos;
bool do_si = may_do_si();
bool do_cindent;
bool no_si = false; // reset did_si afterwards
int first_char = NUL; // init for GCC
int vreplace_mode;
bool did_append; // appended a new line
int saved_pi = curbuf->b_p_pi; // copy of preserveindent setting
linenr_T lnum = curwin->w_cursor.lnum;
colnr_T mincol = curwin->w_cursor.col + 1;
// make a copy of the current line so we can mess with it
char_u *saved_line = vim_strsave(get_cursor_line_ptr());
if (State & VREPLACE_FLAG) {
// With MODE_VREPLACE we make a copy of the next line, which we will be
// starting to replace. First make the new line empty and let vim play
// with the indenting and comment leader to its heart's content. Then
// we grab what it ended up putting on the new line, put back the
// original line, and call ins_char() to put each new character onto
// the line, replacing what was there before and pushing the right
// stuff onto the replace stack. -- webb.
if (curwin->w_cursor.lnum < orig_line_count) {
next_line = vim_strsave(ml_get(curwin->w_cursor.lnum + 1));
} else {
next_line = vim_strsave((char_u *)"");
}
// In MODE_VREPLACE state, a NL replaces the rest of the line, and
// starts replacing the next line, so push all of the characters left
// on the line onto the replace stack. We'll push any other characters
// that might be replaced at the start of the next line (due to
// autoindent etc) a bit later.
replace_push(NUL); // Call twice because BS over NL expects it
replace_push(NUL);
p = (char *)saved_line + curwin->w_cursor.col;
while (*p != NUL) {
p += replace_push_mb((char_u *)p);
}
saved_line[curwin->w_cursor.col] = NUL;
}
if ((State & MODE_INSERT) && (State & VREPLACE_FLAG) == 0) {
p_extra = saved_line + curwin->w_cursor.col;
if (do_si) { // need first char after new line break
p = skipwhite((char *)p_extra);
first_char = (unsigned char)(*p);
}
extra_len = (int)STRLEN(p_extra);
saved_char = *p_extra;
*p_extra = NUL;
}
u_clearline(); // cannot do "U" command when adding lines
did_si = false;
ai_col = 0;
// If we just did an auto-indent, then we didn't type anything on
// the prior line, and it should be truncated. Do this even if 'ai' is not
// set because automatically inserting a comment leader also sets did_ai.
if (dir == FORWARD && did_ai) {
trunc_line = true;
}
// If 'autoindent' and/or 'smartindent' is set, try to figure out what
// indent to use for the new line.
if (curbuf->b_p_ai || do_si) {
// count white space on current line
newindent = get_indent_str_vtab(saved_line,
curbuf->b_p_ts,
curbuf->b_p_vts_array, false);
if (newindent == 0 && !(flags & OPENLINE_COM_LIST)) {
newindent = second_line_indent; // for ^^D command in insert mode
}
// Do smart indenting.
// In insert/replace mode (only when dir == FORWARD)
// we may move some text to the next line. If it starts with '{'
// don't add an indent. Fixes inserting a NL before '{' in line
// "if (condition) {"
if (!trunc_line && do_si && *saved_line != NUL
&& (p_extra == NULL || first_char != '{')) {
char *ptr;
old_cursor = curwin->w_cursor;
ptr = (char *)saved_line;
if (flags & OPENLINE_DO_COM) {
lead_len = get_leader_len(ptr, NULL, false, true);
} else {
lead_len = 0;
}
if (dir == FORWARD) {
// Skip preprocessor directives, unless they are recognised as comments.
if (lead_len == 0 && ptr[0] == '#') {
while (ptr[0] == '#' && curwin->w_cursor.lnum > 1) {
ptr = (char *)ml_get(--curwin->w_cursor.lnum);
}
newindent = get_indent();
}
if (flags & OPENLINE_DO_COM) {
lead_len = get_leader_len(ptr, NULL, false, true);
} else {
lead_len = 0;
}
if (lead_len > 0) {
// This case gets the following right:
// \*
// * A comment (read '\' as '/').
// */
// #define IN_THE_WAY
// This should line up here;
p = skipwhite(ptr);
if (p[0] == '/' && p[1] == '*') {
p++;
}
if (p[0] == '*') {
for (p++; *p; p++) {
if (p[0] == '/' && p[-1] == '*') {
// End of C comment, indent should line up
// with the line containing the start of
// the comment
curwin->w_cursor.col = (colnr_T)(p - ptr);
if ((pos = findmatch(NULL, NUL)) != NULL) {
curwin->w_cursor.lnum = pos->lnum;
newindent = get_indent();
}
}
}
}
} else { // Not a comment line
// Find last non-blank in line
p = ptr + STRLEN(ptr) - 1;
while (p > ptr && ascii_iswhite(*p)) {
p--;
}
char last_char = *p;
// find the character just before the '{' or ';'
if (last_char == '{' || last_char == ';') {
if (p > ptr) {
p--;
}
while (p > ptr && ascii_iswhite(*p)) {
p--;
}
}
// Try to catch lines that are split over multiple
// lines. eg:
// if (condition &&
// condition) {
// Should line up here!
// }
if (*p == ')') {
curwin->w_cursor.col = (colnr_T)(p - ptr);
if ((pos = findmatch(NULL, '(')) != NULL) {
curwin->w_cursor.lnum = pos->lnum;
newindent = get_indent();
ptr = (char *)get_cursor_line_ptr();
}
}
// If last character is '{' do indent, without
// checking for "if" and the like.
if (last_char == '{') {
did_si = true; // do indent
no_si = true; // don't delete it when '{' typed
// Look for "if" and the like, use 'cinwords'.
// Don't do this if the previous line ended in ';' or
// '}'.
} else if (last_char != ';' && last_char != '}'
&& cin_is_cinword((char_u *)ptr)) {
did_si = true;
}
}
} else { // dir == BACKWARD
// Skip preprocessor directives, unless they are
// recognised as comments.
if (lead_len == 0 && ptr[0] == '#') {
bool was_backslashed = false;
while ((ptr[0] == '#' || was_backslashed)
&& curwin->w_cursor.lnum < curbuf->b_ml.ml_line_count) {
if (*ptr && ptr[STRLEN(ptr) - 1] == '\\') {
was_backslashed = true;
} else {
was_backslashed = false;
}
ptr = (char *)ml_get(++curwin->w_cursor.lnum);
}
if (was_backslashed) {
newindent = 0; // Got to end of file
} else {
newindent = get_indent();
}
}
p = skipwhite(ptr);
if (*p == '}') { // if line starts with '}': do indent
did_si = true;
} else { // can delete indent when '{' typed
can_si_back = true;
}
}
curwin->w_cursor = old_cursor;
}
if (do_si) {
can_si = true;
}
did_ai = true;
}
// May do indenting after opening a new line.
do_cindent = !p_paste && (curbuf->b_p_cin || *curbuf->b_p_inde != NUL)
&& in_cinkeys(dir == FORWARD ? KEY_OPEN_FORW : KEY_OPEN_BACK,
' ', linewhite(curwin->w_cursor.lnum));
// Find out if the current line starts with a comment leader.
// This may then be inserted in front of the new line.
end_comment_pending = NUL;
if (flags & OPENLINE_DO_COM) {
lead_len = get_leader_len((char *)saved_line, &lead_flags, dir == BACKWARD, true);
if (lead_len == 0 && curbuf->b_p_cin && do_cindent && dir == FORWARD
&& (!has_format_option(FO_NO_OPEN_COMS) || (flags & OPENLINE_FORMAT))) {
// Check for a line comment after code.
comment_start = check_linecomment(saved_line);
if (comment_start != MAXCOL) {
lead_len = get_leader_len((char *)saved_line + comment_start, &lead_flags, false, true);
if (lead_len != 0) {
lead_len += comment_start;
if (did_do_comment != NULL) {
*did_do_comment = true;
}
}
}
}
} else {
lead_len = 0;
}
if (lead_len > 0) {
char *lead_repl = NULL; // replaces comment leader
int lead_repl_len = 0; // length of *lead_repl
char_u lead_middle[COM_MAX_LEN]; // middle-comment string
char_u lead_end[COM_MAX_LEN]; // end-comment string
char_u *comment_end = NULL; // where lead_end has been found
int extra_space = false; // append extra space
int current_flag;
int require_blank = false; // requires blank after middle
char_u *p2;
// If the comment leader has the start, middle or end flag, it may not
// be used or may be replaced with the middle leader.
for (p = lead_flags; *p && *p != ':'; p++) {
if (*p == COM_BLANK) {
require_blank = true;
continue;
}
if (*p == COM_START || *p == COM_MIDDLE) {
current_flag = (unsigned char)(*p);
if (*p == COM_START) {
// Doing "O" on a start of comment does not insert leader.
if (dir == BACKWARD) {
lead_len = 0;
break;
}
// find start of middle part
(void)copy_option_part(&p, (char *)lead_middle, COM_MAX_LEN, ",");
require_blank = false;
}
// Isolate the strings of the middle and end leader.
while (*p && p[-1] != ':') { // find end of middle flags
if (*p == COM_BLANK) {
require_blank = true;
}
p++;
}
(void)copy_option_part(&p, (char *)lead_middle, COM_MAX_LEN, ",");
while (*p && p[-1] != ':') { // find end of end flags
// Check whether we allow automatic ending of comments
if (*p == COM_AUTO_END) {
end_comment_pending = -1; // means we want to set it
}
p++;
}
size_t n = copy_option_part(&p, (char *)lead_end, COM_MAX_LEN, ",");
if (end_comment_pending == -1) { // we can set it now
end_comment_pending = lead_end[n - 1];
}
// If the end of the comment is in the same line, don't use
// the comment leader.
if (dir == FORWARD) {
for (p = (char *)saved_line + lead_len; *p; p++) {
if (STRNCMP(p, lead_end, n) == 0) {
comment_end = (char_u *)p;
lead_len = 0;
break;
}
}
}
// Doing "o" on a start of comment inserts the middle leader.
if (lead_len > 0) {
if (current_flag == COM_START) {
lead_repl = (char *)lead_middle;
lead_repl_len = (int)STRLEN(lead_middle);
}
// If we have hit RETURN immediately after the start
// comment leader, then put a space after the middle
// comment leader on the next line.
if (!ascii_iswhite(saved_line[lead_len - 1])
&& ((p_extra != NULL
&& (int)curwin->w_cursor.col == lead_len)
|| (p_extra == NULL
&& saved_line[lead_len] == NUL)
|| require_blank)) {
extra_space = true;
}
}
break;
}
if (*p == COM_END) {
// Doing "o" on the end of a comment does not insert leader.
// Remember where the end is, might want to use it to find the
// start (for C-comments).
if (dir == FORWARD) {
comment_end = (char_u *)skipwhite((char *)saved_line);
lead_len = 0;
break;
}
// Doing "O" on the end of a comment inserts the middle leader.
// Find the string for the middle leader, searching backwards.
while (p > curbuf->b_p_com && *p != ',') {
p--;
}
for (lead_repl = p; lead_repl > curbuf->b_p_com
&& lead_repl[-1] != ':'; lead_repl--) {}
lead_repl_len = (int)(p - lead_repl);
// We can probably always add an extra space when doing "O" on
// the comment-end
extra_space = true;
// Check whether we allow automatic ending of comments
for (p2 = (char_u *)p; *p2 && *p2 != ':'; p2++) {
if (*p2 == COM_AUTO_END) {
end_comment_pending = -1; // means we want to set it
}
}
if (end_comment_pending == -1) {
// Find last character in end-comment string
while (*p2 && *p2 != ',') {
p2++;
}
end_comment_pending = p2[-1];
}
break;
}
if (*p == COM_FIRST) {
// Comment leader for first line only: Don't repeat leader
// when using "O", blank out leader when using "o".
if (dir == BACKWARD) {
lead_len = 0;
} else {
lead_repl = "";
lead_repl_len = 0;
}
break;
}
}
if (lead_len > 0) {
// allocate buffer (may concatenate p_extra later)
int bytes = lead_len
+ lead_repl_len
+ extra_space
+ extra_len
+ (second_line_indent > 0 ? second_line_indent : 0)
+ 1;
assert(bytes >= 0);
leader = xmalloc((size_t)bytes);
allocated = (char_u *)leader; // remember to free it later
STRLCPY(leader, saved_line, lead_len + 1);
// TODO(vim): handle multi-byte and double width chars
for (int li = 0; li < comment_start; li++) {
if (!ascii_iswhite(leader[li])) {
leader[li] = ' ';
}
}
// Replace leader with lead_repl, right or left adjusted
if (lead_repl != NULL) {
int c = 0;
int off = 0;
for (p = lead_flags; *p != NUL && *p != ':';) {
if (*p == COM_RIGHT || *p == COM_LEFT) {
c = (unsigned char)(*p++);
} else if (ascii_isdigit(*p) || *p == '-') {
off = getdigits_int(&p, true, 0);
} else {
p++;
}
}
if (c == COM_RIGHT) { // right adjusted leader
// find last non-white in the leader to line up with
for (p = leader + lead_len - 1; p > leader
&& ascii_iswhite(*p); p--) {}
p++;
// Compute the length of the replaced characters in
// screen characters, not bytes.
{
int repl_size = vim_strnsize((char_u *)lead_repl, lead_repl_len);
int old_size = 0;
char *endp = p;
int l;
while (old_size < repl_size && p > leader) {
MB_PTR_BACK(leader, p);
old_size += ptr2cells(p);
}
l = lead_repl_len - (int)(endp - p);
if (l != 0) {
memmove(endp + l, endp,
(size_t)((leader + lead_len) - endp));
}
lead_len += l;
}
memmove(p, lead_repl, (size_t)lead_repl_len);
if (p + lead_repl_len > leader + lead_len) {
p[lead_repl_len] = NUL;
}
// blank-out any other chars from the old leader.
while (--p >= leader) {
int l = utf_head_off((char_u *)leader, (char_u *)p);
if (l > 1) {
p -= l;
if (ptr2cells(p) > 1) {
p[1] = ' ';
l--;
}
memmove(p + 1, p + l + 1,
(size_t)((leader + lead_len) - (p + l + 1)));
lead_len -= l;
*p = ' ';
} else if (!ascii_iswhite(*p)) {
*p = ' ';
}
}
} else { // left adjusted leader
p = skipwhite(leader);
// Compute the length of the replaced characters in
// screen characters, not bytes. Move the part that is
// not to be overwritten.
{
int repl_size = vim_strnsize((char_u *)lead_repl, lead_repl_len);
int i;
int l;
for (i = 0; i < lead_len && p[i] != NUL; i += l) {
l = utfc_ptr2len(p + i);
if (vim_strnsize((char_u *)p, i + l) > repl_size) {
break;
}
}
if (i != lead_repl_len) {
memmove(p + lead_repl_len, p + i,
(size_t)(lead_len - i - (p - leader)));
lead_len += lead_repl_len - i;
}
}
memmove(p, lead_repl, (size_t)lead_repl_len);
// Replace any remaining non-white chars in the old
// leader by spaces. Keep Tabs, the indent must
// remain the same.
for (p += lead_repl_len; p < leader + lead_len; p++) {
if (!ascii_iswhite(*p)) {
// Don't put a space before a TAB.
if (p + 1 < leader + lead_len && p[1] == TAB) {
lead_len--;
memmove(p, p + 1, (size_t)(leader + lead_len - p));
} else {
int l = utfc_ptr2len(p);
if (l > 1) {
if (ptr2cells(p) > 1) {
// Replace a double-wide char with
// two spaces
l--;
*p++ = ' ';
}
memmove(p + 1, p + l, (size_t)(leader + lead_len - p));
lead_len -= l - 1;
}
*p = ' ';
}
}
}
*p = NUL;
}
// Recompute the indent, it may have changed.
if (curbuf->b_p_ai || do_si) {
newindent = get_indent_str_vtab((char_u *)leader,
curbuf->b_p_ts,
curbuf->b_p_vts_array, false);
}
// Add the indent offset
if (newindent + off < 0) {
off = -newindent;
newindent = 0;
} else {
newindent += off;
}
// Correct trailing spaces for the shift, so that
// alignment remains equal.
while (off > 0 && lead_len > 0
&& leader[lead_len - 1] == ' ') {
// Don't do it when there is a tab before the space
if (vim_strchr(skipwhite(leader), '\t') != NULL) {
break;
}
lead_len--;
off--;
}
// If the leader ends in white space, don't add an
// extra space
if (lead_len > 0 && ascii_iswhite(leader[lead_len - 1])) {
extra_space = false;
}
leader[lead_len] = NUL;
}
if (extra_space) {
leader[lead_len++] = ' ';
leader[lead_len] = NUL;
}
newcol = lead_len;
// if a new indent will be set below, remove the indent that
// is in the comment leader
if (newindent || did_si) {
while (lead_len && ascii_iswhite(*leader)) {
lead_len--;
newcol--;
leader++;
}
}
did_si = can_si = false;
} else if (comment_end != NULL) {
// We have finished a comment, so we don't use the leader.
// If this was a C-comment and 'ai' or 'si' is set do a normal
// indent to align with the line containing the start of the
// comment.
if (comment_end[0] == '*' && comment_end[1] == '/'
&& (curbuf->b_p_ai || do_si)) {
old_cursor = curwin->w_cursor;
curwin->w_cursor.col = (colnr_T)(comment_end - saved_line);
if ((pos = findmatch(NULL, NUL)) != NULL) {
curwin->w_cursor.lnum = pos->lnum;
newindent = get_indent();
}
curwin->w_cursor = old_cursor;
}
}
}
// (State == MODE_INSERT || State == MODE_REPLACE), only when dir == FORWARD
if (p_extra != NULL) {
*p_extra = saved_char; // restore char that NUL replaced
// When 'ai' set or "flags" has OPENLINE_DELSPACES, skip to the first
// non-blank.
//
// When in MODE_REPLACE state, put the deleted blanks on the replace
// stack, preceded by a NUL, so they can be put back when a BS is
// entered.
if (REPLACE_NORMAL(State)) {
replace_push(NUL); // end of extra blanks
}
if (curbuf->b_p_ai || (flags & OPENLINE_DELSPACES)) {
while ((*p_extra == ' ' || *p_extra == '\t')
&& !utf_iscomposing(utf_ptr2char((char *)p_extra + 1))) {
if (REPLACE_NORMAL(State)) {
replace_push(*p_extra);
}
p_extra++;
less_cols_off++;
}
}
// columns for marks adjusted for removed columns
less_cols = (int)(p_extra - saved_line);
}
if (p_extra == NULL) {
p_extra = (char_u *)""; // append empty line
}
// concatenate leader and p_extra, if there is a leader
if (lead_len > 0) {
if (flags & OPENLINE_COM_LIST && second_line_indent > 0) {
int i;
int padding = second_line_indent
- (newindent + (int)STRLEN(leader));
// Here whitespace is inserted after the comment char.
// Below, set_indent(newindent, SIN_INSERT) will insert the
// whitespace needed before the comment char.
for (i = 0; i < padding; i++) {
STRCAT(leader, " ");
less_cols--;
newcol++;
}
}
STRCAT(leader, p_extra);
p_extra = (char_u *)leader;
did_ai = true; // So truncating blanks works with comments
less_cols -= lead_len;
} else {
end_comment_pending = NUL; // turns out there was no leader
}
curbuf_splice_pending++;
old_cursor = curwin->w_cursor;
if (dir == BACKWARD) {
curwin->w_cursor.lnum--;
}
if ((State & VREPLACE_FLAG) == 0 || old_cursor.lnum >= orig_line_count) {
if (ml_append(curwin->w_cursor.lnum, (char *)p_extra, (colnr_T)0, false) == FAIL) {
goto theend;
}
// Postpone calling changed_lines(), because it would mess up folding
// with markers.
// Skip mark_adjust when adding a line after the last one, there can't
// be marks there. But still needed in diff mode.
if (curwin->w_cursor.lnum + 1 < curbuf->b_ml.ml_line_count
|| curwin->w_p_diff) {
mark_adjust(curwin->w_cursor.lnum + 1, (linenr_T)MAXLNUM, 1L, 0L,
kExtmarkNOOP);
}
did_append = true;
} else {
// In MODE_VREPLACE state we are starting to replace the next line.
curwin->w_cursor.lnum++;
if (curwin->w_cursor.lnum >= Insstart.lnum + vr_lines_changed) {
// In case we NL to a new line, BS to the previous one, and NL
// again, we don't want to save the new line for undo twice.
(void)u_save_cursor(); // errors are ignored!
vr_lines_changed++;
}
ml_replace(curwin->w_cursor.lnum, (char *)p_extra, true);
changed_bytes(curwin->w_cursor.lnum, 0);
// TODO(vigoux): extmark_splice_cols here??
curwin->w_cursor.lnum--;
did_append = false;
}
inhibit_delete_count++;
if (newindent || did_si) {
curwin->w_cursor.lnum++;
if (did_si) {
int sw = get_sw_value(curbuf);
if (p_sr) {
newindent -= newindent % sw;
}
newindent += sw;
}
// Copy the indent
if (curbuf->b_p_ci) {
(void)copy_indent(newindent, saved_line);
// Set the 'preserveindent' option so that any further screwing
// with the line doesn't entirely destroy our efforts to preserve
// it. It gets restored at the function end.
curbuf->b_p_pi = true;
} else {
(void)set_indent(newindent, SIN_INSERT|SIN_NOMARK);
}
less_cols -= curwin->w_cursor.col;
ai_col = curwin->w_cursor.col;
// In MODE_REPLACE state, for each character in the new indent, there
// must be a NUL on the replace stack, for when it is deleted with BS
if (REPLACE_NORMAL(State)) {
for (colnr_T n = 0; n < curwin->w_cursor.col; n++) {
replace_push(NUL);
}
}
newcol += curwin->w_cursor.col;
if (no_si) {
did_si = false;
}
}
inhibit_delete_count--;
// In MODE_REPLACE state, for each character in the extra leader, there
// must be a NUL on the replace stack, for when it is deleted with BS.
if (REPLACE_NORMAL(State)) {
while (lead_len-- > 0) {
replace_push(NUL);
}
}
curwin->w_cursor = old_cursor;
if (dir == FORWARD) {
if (trunc_line || (State & MODE_INSERT)) {
// truncate current line at cursor
saved_line[curwin->w_cursor.col] = NUL;
// Remove trailing white space, unless OPENLINE_KEEPTRAIL used.
if (trunc_line && !(flags & OPENLINE_KEEPTRAIL)) {
truncate_spaces(saved_line);
}
ml_replace(curwin->w_cursor.lnum, (char *)saved_line, false);
int new_len = (int)STRLEN(saved_line);
// TODO(vigoux): maybe there is issues there with expandtabs ?
int cols_spliced = 0;
if (new_len < curwin->w_cursor.col) {
extmark_splice_cols(curbuf, (int)curwin->w_cursor.lnum - 1,
new_len, curwin->w_cursor.col - new_len, 0, kExtmarkUndo);
cols_spliced = curwin->w_cursor.col - new_len;
}
saved_line = NULL;
if (did_append) {
changed_lines(curwin->w_cursor.lnum, curwin->w_cursor.col,
curwin->w_cursor.lnum + 1, 1L, true);
did_append = false;
// Move marks after the line break to the new line.
if (flags & OPENLINE_MARKFIX) {
mark_col_adjust(curwin->w_cursor.lnum,
curwin->w_cursor.col + less_cols_off,
1L, (long)-less_cols, 0);
}
// Always move extmarks - Here we move only the line where the
// cursor is, the previous mark_adjust takes care of the lines after
int cols_added = mincol - 1 + less_cols_off - less_cols;
extmark_splice(curbuf, (int)lnum - 1, mincol - 1 - cols_spliced,
0, less_cols_off, less_cols_off,
1, cols_added, 1 + cols_added, kExtmarkUndo);
} else {
changed_bytes(curwin->w_cursor.lnum, curwin->w_cursor.col);
}
}
// Put the cursor on the new line. Careful: the scrollup() above may
// have moved w_cursor, we must use old_cursor.
curwin->w_cursor.lnum = old_cursor.lnum + 1;
}
if (did_append) {
changed_lines(curwin->w_cursor.lnum, 0, curwin->w_cursor.lnum, 1L, true);
// bail out and just get the final length of the line we just manipulated
bcount_t extra = (bcount_t)STRLEN(ml_get(curwin->w_cursor.lnum));
extmark_splice(curbuf, (int)curwin->w_cursor.lnum - 1, 0,
0, 0, 0, 1, 0, 1 + extra, kExtmarkUndo);
}
curbuf_splice_pending--;
curwin->w_cursor.col = newcol;
curwin->w_cursor.coladd = 0;
// In MODE_VREPLACE state, we are handling the replace stack ourselves, so
// stop fixthisline() from doing it (via change_indent()) by telling it
// we're in normal MODE_INSERT state.
if (State & VREPLACE_FLAG) {
vreplace_mode = State; // So we know to put things right later
State = MODE_INSERT;
} else {
vreplace_mode = 0;
}
// May do lisp indenting.
if (!p_paste
&& leader == NULL
&& curbuf->b_p_lisp
&& curbuf->b_p_ai) {
fixthisline(get_lisp_indent);
ai_col = (colnr_T)getwhitecols_curline();
}
// May do indenting after opening a new line.
if (do_cindent) {
do_c_expr_indent();
ai_col = (colnr_T)getwhitecols_curline();
}
if (vreplace_mode != 0) {
State = vreplace_mode;
}
// Finally, MODE_VREPLACE gets the stuff on the new line, then puts back
// the original line, and inserts the new stuff char by char, pushing old
// stuff onto the replace stack (via ins_char()).
if (State & VREPLACE_FLAG) {
// Put new line in p_extra
p_extra = vim_strsave(get_cursor_line_ptr());
// Put back original line
ml_replace(curwin->w_cursor.lnum, (char *)next_line, false);
// Insert new stuff into line again
curwin->w_cursor.col = 0;
curwin->w_cursor.coladd = 0;
ins_bytes((char *)p_extra); // will call changed_bytes()
xfree(p_extra);
next_line = NULL;
}
retval = true; // success!
theend:
curbuf->b_p_pi = saved_pi;
xfree(saved_line);
xfree(next_line);
xfree(allocated);
return retval;
}
/// Delete from cursor to end of line.
/// Caller must have prepared for undo.
/// If "fixpos" is true fix the cursor position when done.
void truncate_line(int fixpos)
{
char *newp;
linenr_T lnum = curwin->w_cursor.lnum;
colnr_T col = curwin->w_cursor.col;
if (col == 0) {
newp = xstrdup("");
} else {
newp = (char *)vim_strnsave(ml_get(lnum), (size_t)col);
}
ml_replace(lnum, newp, false);
// mark the buffer as changed and prepare for displaying
changed_bytes(lnum, curwin->w_cursor.col);
// If "fixpos" is true we don't want to end up positioned at the NUL.
if (fixpos && curwin->w_cursor.col > 0) {
curwin->w_cursor.col--;
}
}
/// Delete "nlines" lines at the cursor.
/// Saves the lines for undo first if "undo" is true.
void del_lines(long nlines, bool undo)
{
long n;
linenr_T first = curwin->w_cursor.lnum;
if (nlines <= 0) {
return;
}
// save the deleted lines for undo
if (undo && u_savedel(first, nlines) == FAIL) {
return;
}
for (n = 0; n < nlines;) {
if (curbuf->b_ml.ml_flags & ML_EMPTY) { // nothing to delete
break;
}
ml_delete(first, true);
n++;
// If we delete the last line in the file, stop
if (first > curbuf->b_ml.ml_line_count) {
break;
}
}
// Correct the cursor position before calling deleted_lines_mark(), it may
// trigger a callback to display the cursor.
curwin->w_cursor.col = 0;
check_cursor_lnum();
// adjust marks, mark the buffer as changed and prepare for displaying
deleted_lines_mark(first, n);
}
/// Returns the length in bytes of the prefix of the given string which introduces a comment.
///
/// If this string is not a comment then 0 is returned.
/// When "flags" is not NULL, it is set to point to the flags of the recognized comment leader.
/// "backward" must be true for the "O" command.
/// If "include_space" is set, include trailing whitespace while calculating the length.
int get_leader_len(char *line, char **flags, bool backward, bool include_space)
{
int j;
int got_com = false;
char part_buf[COM_MAX_LEN]; // buffer for one option part
char *string; // pointer to comment string
char *list;
int middle_match_len = 0;
char *prev_list;
char *saved_flags = NULL;
int result = 0;
int i = 0;
while (ascii_iswhite(line[i])) { // leading white space is ignored
i++;
}
// Repeat to match several nested comment strings.
while (line[i] != NUL) {
// scan through the 'comments' option for a match
int found_one = false;
for (list = curbuf->b_p_com; *list;) {
// Get one option part into part_buf[]. Advance "list" to next
// one. Put "string" at start of string.
if (!got_com && flags != NULL) {
*flags = list; // remember where flags started
}
prev_list = list;
(void)copy_option_part(&list, part_buf, COM_MAX_LEN, ",");
string = vim_strchr(part_buf, ':');
if (string == NULL) { // missing ':', ignore this part
continue;
}
*string++ = NUL; // isolate flags from string
// If we found a middle match previously, use that match when this
// is not a middle or end.
if (middle_match_len != 0
&& vim_strchr(part_buf, COM_MIDDLE) == NULL
&& vim_strchr(part_buf, COM_END) == NULL) {
break;
}
// When we already found a nested comment, only accept further
// nested comments.
if (got_com && vim_strchr(part_buf, COM_NEST) == NULL) {
continue;
}
// When 'O' flag present and using "O" command skip this one.
if (backward && vim_strchr(part_buf, COM_NOBACK) != NULL) {
continue;
}
// Line contents and string must match.
// When string starts with white space, must have some white space
// (but the amount does not need to match, there might be a mix of
// TABs and spaces).
if (ascii_iswhite(string[0])) {
if (i == 0 || !ascii_iswhite(line[i - 1])) {
continue; // missing white space
}
while (ascii_iswhite(string[0])) {
string++;
}
}
for (j = 0; string[j] != NUL && string[j] == line[i + j]; j++) {}
if (string[j] != NUL) {
continue; // string doesn't match
}
// When 'b' flag used, there must be white space or an
// end-of-line after the string in the line.
if (vim_strchr(part_buf, COM_BLANK) != NULL
&& !ascii_iswhite(line[i + j]) && line[i + j] != NUL) {
continue;
}
// We have found a match, stop searching unless this is a middle
// comment. The middle comment can be a substring of the end
// comment in which case it's better to return the length of the
// end comment and its flags. Thus we keep searching with middle
// and end matches and use an end match if it matches better.
if (vim_strchr(part_buf, COM_MIDDLE) != NULL) {
if (middle_match_len == 0) {
middle_match_len = j;
saved_flags = prev_list;
}
continue;
}
if (middle_match_len != 0 && j > middle_match_len) {
// Use this match instead of the middle match, since it's a
// longer thus better match.
middle_match_len = 0;
}
if (middle_match_len == 0) {
i += j;
}
found_one = true;
break;
}
if (middle_match_len != 0) {
// Use the previously found middle match after failing to find a
// match with an end.
if (!got_com && flags != NULL) {
*flags = saved_flags;
}
i += middle_match_len;
found_one = true;
}
// No match found, stop scanning.
if (!found_one) {
break;
}
result = i;
// Include any trailing white space.
while (ascii_iswhite(line[i])) {
i++;
}
if (include_space) {
result = i;
}
// If this comment doesn't nest, stop here.
got_com = true;
if (vim_strchr(part_buf, COM_NEST) == NULL) {
break;
}
}
return result;
}
/// Return the offset at which the last comment in line starts. If there is no
/// comment in the whole line, -1 is returned.
///
/// When "flags" is not null, it is set to point to the flags describing the
/// recognized comment leader.
int get_last_leader_offset(char *line, char **flags)
{
int result = -1;
int j;
int lower_check_bound = 0;
char *string;
char *com_leader;
char *com_flags;
char *list;
char part_buf[COM_MAX_LEN]; // buffer for one option part
// Repeat to match several nested comment strings.
int i = (int)STRLEN(line);
while (--i >= lower_check_bound) {
// scan through the 'comments' option for a match
int found_one = false;
for (list = curbuf->b_p_com; *list;) {
char *flags_save = list;
// Get one option part into part_buf[]. Advance list to next one.
// put string at start of string.
(void)copy_option_part(&list, part_buf, COM_MAX_LEN, ",");
string = vim_strchr(part_buf, ':');
if (string == NULL) { // If everything is fine, this cannot actually
// happen.
continue;
}
*string++ = NUL; // Isolate flags from string.
com_leader = string;
// Line contents and string must match.
// When string starts with white space, must have some white space
// (but the amount does not need to match, there might be a mix of
// TABs and spaces).
if (ascii_iswhite(string[0])) {
if (i == 0 || !ascii_iswhite(line[i - 1])) {
continue;
}
while (ascii_iswhite(*string)) {
string++;
}
}
for (j = 0; string[j] != NUL && string[j] == line[i + j]; j++) {
// do nothing
}
if (string[j] != NUL) {
continue;
}
// When 'b' flag used, there must be white space or an
// end-of-line after the string in the line.
if (vim_strchr(part_buf, COM_BLANK) != NULL
&& !ascii_iswhite(line[i + j]) && line[i + j] != NUL) {
continue;
}
if (vim_strchr(part_buf, COM_MIDDLE) != NULL) {
// For a middlepart comment, only consider it to match if
// everything before the current position in the line is
// whitespace. Otherwise we would think we are inside a
// comment if the middle part appears somewhere in the middle
// of the line. E.g. for C the "*" appears often.
for (j = 0; j <= i && ascii_iswhite(line[j]); j++) {}
if (j < i) {
continue;
}
}
// We have found a match, stop searching.
found_one = true;
if (flags) {
*flags = flags_save;
}
com_flags = flags_save;
break;
}
if (found_one) {
char part_buf2[COM_MAX_LEN]; // buffer for one option part
int len1, len2, off;
result = i;
// If this comment nests, continue searching.
if (vim_strchr(part_buf, COM_NEST) != NULL) {
continue;
}
lower_check_bound = i;
// Let's verify whether the comment leader found is a substring
// of other comment leaders. If it is, let's adjust the
// lower_check_bound so that we make sure that we have determined
// the comment leader correctly.
while (ascii_iswhite(*com_leader)) {
com_leader++;
}
len1 = (int)STRLEN(com_leader);
for (list = curbuf->b_p_com; *list;) {
char *flags_save = list;
(void)copy_option_part(&list, part_buf2, COM_MAX_LEN, ",");
if (flags_save == com_flags) {
continue;
}
string = vim_strchr(part_buf2, ':');
string++;
while (ascii_iswhite(*string)) {
string++;
}
len2 = (int)STRLEN(string);
if (len2 == 0) {
continue;
}
// Now we have to verify whether string ends with a substring
// beginning the com_leader.
for (off = (len2 > i ? i : len2); off > 0 && off + len1 > len2;) {
off--;
if (!STRNCMP(string + off, com_leader, len2 - off)) {
if (i - off < lower_check_bound) {
lower_check_bound = i - off;
}
}
}
}
}
}
return result;
}