feat: multibuffer preview support for inccommand

Allows preview callbacks to modify multiple buffers in order to show the
preview. Previously, if multiple buffers were modified, only the current
buffer would have its state restored. After this change, all buffers
have their state restored after preview.

Closes #19103.
This commit is contained in:
Famiu Haque
2022-07-14 14:37:00 +06:00
parent 9a3877ff9d
commit c2f7a2a18d
3 changed files with 435 additions and 170 deletions

View File

@@ -1466,10 +1466,10 @@ results (for "inccommand=split", or nil for "inccommand=nosplit").
Your command preview routine must implement this protocol: Your command preview routine must implement this protocol:
1. Modify the current buffer as required for the preview (see 1. Modify the target buffers as required for the preview (see
|nvim_buf_set_text()| and |nvim_buf_set_lines()|). |nvim_buf_set_text()| and |nvim_buf_set_lines()|).
2. If preview buffer is provided, add necessary text to the preview buffer. 2. If preview buffer is provided, add necessary text to the preview buffer.
3. Add required highlights to the current buffer. If preview buffer is 3. Add required highlights to the target buffers. If preview buffer is
provided, add required highlights to the preview buffer as well. All provided, add required highlights to the preview buffer as well. All
highlights must be added to the preview namespace which is provided as an highlights must be added to the preview namespace which is provided as an
argument to the preview callback (see |nvim_buf_add_highlight()| and argument to the preview callback (see |nvim_buf_add_highlight()| and
@@ -1480,8 +1480,8 @@ Your command preview routine must implement this protocol:
2: Preview is shown and preview window is opened (if "inccommand=split"). 2: Preview is shown and preview window is opened (if "inccommand=split").
For "inccommand=nosplit" this is the same as 1. For "inccommand=nosplit" this is the same as 1.
After preview ends, Nvim discards all changes to the buffer and all highlights After preview ends, Nvim discards all changes to all buffers made during the
in the preview namespace. preview and clears all highlights in the preview namespace.
Here's an example of a command to trim trailing whitespace from lines that Here's an example of a command to trim trailing whitespace from lines that
supports incremental command preview: supports incremental command preview:

View File

@@ -145,7 +145,7 @@ struct cmdline_info {
/// Last value of prompt_id, incremented when doing new prompt /// Last value of prompt_id, incremented when doing new prompt
static unsigned last_prompt_id = 0; static unsigned last_prompt_id = 0;
// Struct to store the viewstate during 'incsearch' highlighting. // Struct to store the viewstate during 'incsearch' highlighting and 'inccommand' preview.
typedef struct { typedef struct {
colnr_T vs_curswant; colnr_T vs_curswant;
colnr_T vs_leftcol; colnr_T vs_leftcol;
@@ -197,6 +197,32 @@ typedef struct command_line_state {
long *b_im_ptr; long *b_im_ptr;
} CommandLineState; } CommandLineState;
typedef struct cmdpreview_win_info {
win_T *win;
pos_T save_w_cursor;
viewstate_T save_viewstate;
int save_w_p_cul;
int save_w_p_cuc;
} CpWinInfo;
typedef struct cmdpreview_buf_info {
buf_T *buf;
time_t save_b_u_time_cur;
long save_b_u_seq_cur;
u_header_T *save_b_u_newhead;
long save_b_p_ul;
int save_b_changed;
varnumber_T save_changedtick;
} CpBufInfo;
typedef struct cmdpreview_info {
kvec_t(CpWinInfo) win_info;
kvec_t(CpBufInfo) buf_info;
bool save_hls;
cmdmod_T save_cmdmod;
garray_T save_view;
} CpInfo;
typedef struct cmdline_info CmdlineInfo; typedef struct cmdline_info CmdlineInfo;
/// The current cmdline_info. It is initialized in getcmdline() and after that /// The current cmdline_info. It is initialized in getcmdline() and after that
@@ -239,26 +265,26 @@ static long cmdpreview_ns = 0;
static int cmd_hkmap = 0; // Hebrew mapping during command line static int cmd_hkmap = 0; // Hebrew mapping during command line
static void save_viewstate(viewstate_T *vs) static void save_viewstate(win_T *wp, viewstate_T *vs)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_ALL
{ {
vs->vs_curswant = curwin->w_curswant; vs->vs_curswant = wp->w_curswant;
vs->vs_leftcol = curwin->w_leftcol; vs->vs_leftcol = wp->w_leftcol;
vs->vs_topline = curwin->w_topline; vs->vs_topline = wp->w_topline;
vs->vs_topfill = curwin->w_topfill; vs->vs_topfill = wp->w_topfill;
vs->vs_botline = curwin->w_botline; vs->vs_botline = wp->w_botline;
vs->vs_empty_rows = curwin->w_empty_rows; vs->vs_empty_rows = wp->w_empty_rows;
} }
static void restore_viewstate(viewstate_T *vs) static void restore_viewstate(win_T *wp, viewstate_T *vs)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_ALL
{ {
curwin->w_curswant = vs->vs_curswant; wp->w_curswant = vs->vs_curswant;
curwin->w_leftcol = vs->vs_leftcol; wp->w_leftcol = vs->vs_leftcol;
curwin->w_topline = vs->vs_topline; wp->w_topline = vs->vs_topline;
curwin->w_topfill = vs->vs_topfill; wp->w_topfill = vs->vs_topfill;
curwin->w_botline = vs->vs_botline; wp->w_botline = vs->vs_botline;
curwin->w_empty_rows = vs->vs_empty_rows; wp->w_empty_rows = vs->vs_empty_rows;
} }
static void init_incsearch_state(incsearch_state_T *s) static void init_incsearch_state(incsearch_state_T *s)
@@ -270,8 +296,8 @@ static void init_incsearch_state(incsearch_state_T *s)
clearpos(&s->match_end); clearpos(&s->match_end);
s->save_cursor = curwin->w_cursor; // may be restored later s->save_cursor = curwin->w_cursor; // may be restored later
s->search_start = curwin->w_cursor; s->search_start = curwin->w_cursor;
save_viewstate(&s->init_viewstate); save_viewstate(curwin, &s->init_viewstate);
save_viewstate(&s->old_viewstate); save_viewstate(curwin, &s->old_viewstate);
} }
/// Completion for |:checkhealth| command. /// Completion for |:checkhealth| command.
@@ -551,7 +577,7 @@ static void may_do_incsearch_highlighting(int firstc, long count, incsearch_stat
// first restore the old curwin values, so the screen is // first restore the old curwin values, so the screen is
// positioned in the same way as the actual search command // positioned in the same way as the actual search command
restore_viewstate(&s->old_viewstate); restore_viewstate(curwin, &s->old_viewstate);
changed_cline_bef_curs(); changed_cline_bef_curs();
update_topline(curwin); update_topline(curwin);
@@ -661,7 +687,7 @@ static void finish_incsearch_highlighting(int gotesc, incsearch_state_T *s, bool
} }
curwin->w_cursor = s->search_start; // -V519 curwin->w_cursor = s->search_start; // -V519
} }
restore_viewstate(&s->old_viewstate); restore_viewstate(curwin, &s->old_viewstate);
highlight_match = false; highlight_match = false;
// by default search all lines // by default search all lines
@@ -1660,7 +1686,7 @@ static int may_do_command_line_next_incsearch(int firstc, long count, incsearch_
update_topline(curwin); update_topline(curwin);
validate_cursor(); validate_cursor();
highlight_match = true; highlight_match = true;
save_viewstate(&s->old_viewstate); save_viewstate(curwin, &s->old_viewstate);
update_screen(NOT_VALID); update_screen(NOT_VALID);
highlight_match = false; highlight_match = false;
redrawcmdline(); redrawcmdline();
@@ -2392,6 +2418,126 @@ static void cmdpreview_close_win(void)
} }
} }
/// Save current state and prepare windows and buffers for command preview.
static void cmdpreview_prepare(CpInfo *cpinfo)
{
kv_init(cpinfo->buf_info);
kv_init(cpinfo->win_info);
FOR_ALL_WINDOWS_IN_TAB(win, curtab) {
buf_T *buf = win->w_buffer;
// Don't save state of command preview buffer or preview window.
if (buf->handle == cmdpreview_bufnr) {
continue;
}
CpBufInfo cp_bufinfo;
cp_bufinfo.buf = buf;
cp_bufinfo.save_b_u_time_cur = buf->b_u_time_cur;
cp_bufinfo.save_b_u_seq_cur = buf->b_u_seq_cur;
cp_bufinfo.save_b_u_newhead = buf->b_u_newhead;
cp_bufinfo.save_b_p_ul = buf->b_p_ul;
cp_bufinfo.save_b_changed = buf->b_changed;
cp_bufinfo.save_changedtick = buf_get_changedtick(buf);
kv_push(cpinfo->buf_info, cp_bufinfo);
buf->b_p_ul = LONG_MAX; // Make sure we can undo all changes
CpWinInfo cp_wininfo;
cp_wininfo.win = win;
// Save window cursor position and viewstate
cp_wininfo.save_w_cursor = win->w_cursor;
save_viewstate(win, &cp_wininfo.save_viewstate);
// Save 'cursorline' and 'cursorcolumn'
cp_wininfo.save_w_p_cul = win->w_p_cul;
cp_wininfo.save_w_p_cuc = win->w_p_cuc;
kv_push(cpinfo->win_info, cp_wininfo);
win->w_p_cul = false; // Disable 'cursorline' so it doesn't mess up the highlights
win->w_p_cuc = false; // Disable 'cursorcolumn' so it doesn't mess up the highlights
}
cpinfo->save_hls = p_hls;
cpinfo->save_cmdmod = cmdmod;
win_size_save(&cpinfo->save_view);
save_search_patterns();
p_hls = false; // Don't show search highlighting during live substitution
cmdmod.cmod_split = 0; // Disable :leftabove/botright modifiers
cmdmod.cmod_tab = 0; // Disable :tab modifier
cmdmod.cmod_flags |= CMOD_NOSWAPFILE; // Disable swap for preview buffer
}
// Restore the state of buffers and windows before command preview.
static void cmdpreview_restore_state(CpInfo *cpinfo)
{
for (size_t i = 0; i < cpinfo->buf_info.size; i++) {
CpBufInfo cp_bufinfo = cpinfo->buf_info.items[i];
buf_T *buf = cp_bufinfo.buf;
buf->b_changed = cp_bufinfo.save_b_changed;
if (buf->b_u_seq_cur != cp_bufinfo.save_b_u_seq_cur) {
int count = 0;
// Calculate how many undo steps are necessary to restore earlier state.
for (u_header_T *uhp = buf->b_u_curhead ? buf->b_u_curhead : buf->b_u_newhead;
uhp != NULL && uhp->uh_seq > cp_bufinfo.save_b_u_seq_cur;
uhp = uhp->uh_next.ptr, ++count) {}
aco_save_T aco;
aucmd_prepbuf(&aco, buf);
// Undo invisibly. This also moves the cursor!
if (!u_undo_and_forget(count)) {
abort();
}
aucmd_restbuf(&aco);
// Restore newhead. It is meaningless when curhead is valid, but we must
// restore it so that undotree() is identical before/after the preview.
buf->b_u_newhead = cp_bufinfo.save_b_u_newhead;
buf->b_u_time_cur = cp_bufinfo.save_b_u_time_cur;
}
if (cp_bufinfo.save_changedtick != buf_get_changedtick(buf)) {
buf_set_changedtick(buf, cp_bufinfo.save_changedtick);
}
buf->b_p_ul = cp_bufinfo.save_b_p_ul; // Restore 'undolevels'
// Clear preview highlights.
extmark_clear(buf, (uint32_t)cmdpreview_ns, 0, 0, MAXLNUM, MAXCOL);
}
for (size_t i = 0; i < cpinfo->win_info.size; i++) {
CpWinInfo cp_wininfo = cpinfo->win_info.items[i];
win_T *win = cp_wininfo.win;
// Restore window cursor position and viewstate
win->w_cursor = cp_wininfo.save_w_cursor;
restore_viewstate(win, &cp_wininfo.save_viewstate);
// Restore 'cursorline' and 'cursorcolumn'
win->w_p_cul = cp_wininfo.save_w_p_cul;
win->w_p_cuc = cp_wininfo.save_w_p_cuc;
update_topline(win);
}
cmdmod = cpinfo->save_cmdmod; // Restore cmdmod
p_hls = cpinfo->save_hls; // Restore 'hlsearch'
restore_search_patterns(); // Restore search patterns
win_size_restore(&cpinfo->save_view); // Restore window sizes
ga_clear(&cpinfo->save_view);
kv_destroy(cpinfo->win_info);
kv_destroy(cpinfo->buf_info);
}
/// Show 'inccommand' preview if command is previewable. It works like this: /// Show 'inccommand' preview if command is previewable. It works like this:
/// 1. Store current undo information so we can revert to current state later. /// 1. Store current undo information so we can revert to current state later.
/// 2. Execute the preview callback with the parsed command, preview buffer number and preview /// 2. Execute the preview callback with the parsed command, preview buffer number and preview
@@ -2436,35 +2582,18 @@ static bool cmdpreview_may_show(CommandLineState *s)
ea.line2 = lnum; ea.line2 = lnum;
} }
time_t save_b_u_time_cur = curbuf->b_u_time_cur; CpInfo cpinfo;
long save_b_u_seq_cur = curbuf->b_u_seq_cur;
u_header_T *save_b_u_newhead = curbuf->b_u_newhead;
long save_b_p_ul = curbuf->b_p_ul;
int save_b_changed = curbuf->b_changed;
int save_w_p_cul = curwin->w_p_cul;
int save_w_p_cuc = curwin->w_p_cuc;
bool save_hls = p_hls;
varnumber_T save_changedtick = buf_get_changedtick(curbuf);
bool icm_split = *p_icm == 's'; // inccommand=split bool icm_split = *p_icm == 's'; // inccommand=split
buf_T *cmdpreview_buf; buf_T *cmdpreview_buf;
win_T *cmdpreview_win; win_T *cmdpreview_win;
cmdmod_T save_cmdmod = cmdmod;
cmdpreview = true;
emsg_silent++; // Block error reporting as the command may be incomplete, emsg_silent++; // Block error reporting as the command may be incomplete,
// but still update v:errmsg // but still update v:errmsg
msg_silent++; // Block messages, namely ones that prompt msg_silent++; // Block messages, namely ones that prompt
block_autocmds(); // Block events block_autocmds(); // Block events
garray_T save_view;
win_size_save(&save_view); // Save current window sizes // Save current state and prepare for command preview.
save_search_patterns(); // Save search patterns cmdpreview_prepare(&cpinfo);
curbuf->b_p_ul = LONG_MAX; // Make sure we can undo all changes
curwin->w_p_cul = false; // Disable 'cursorline' so it doesn't mess up the highlights
curwin->w_p_cuc = false; // Disable 'cursorcolumn' so it doesn't mess up the highlights
p_hls = false; // Don't show search highlighting during live substitution
cmdmod.cmod_split = 0; // Disable :leftabove/botright modifiers
cmdmod.cmod_tab = 0; // Disable :tab modifier
cmdmod.cmod_flags |= CMOD_NOSWAPFILE; // Disable swap for preview buffer
// Open preview buffer if inccommand=split. // Open preview buffer if inccommand=split.
if (!icm_split) { if (!icm_split) {
@@ -2472,12 +2601,14 @@ static bool cmdpreview_may_show(CommandLineState *s)
} else if ((cmdpreview_buf = cmdpreview_open_buf()) == NULL) { } else if ((cmdpreview_buf = cmdpreview_open_buf()) == NULL) {
abort(); abort();
} }
// Setup preview namespace if it's not already set. // Setup preview namespace if it's not already set.
if (!cmdpreview_ns) { if (!cmdpreview_ns) {
cmdpreview_ns = (int)nvim_create_namespace((String)STRING_INIT); cmdpreview_ns = (int)nvim_create_namespace((String)STRING_INIT);
} }
// Set cmdpreview state.
cmdpreview = true;
// Execute the preview callback and use its return value to determine whether to show preview or // Execute the preview callback and use its return value to determine whether to show preview or
// open the preview window. The preview callback also handles doing the changes and highlights for // open the preview window. The preview callback also handles doing the changes and highlights for
// the preview. // the preview.
@@ -2496,7 +2627,7 @@ static bool cmdpreview_may_show(CommandLineState *s)
cmdpreview_type = 1; cmdpreview_type = 1;
} }
// If preview callback is nonzero, update screen now. // If preview callback return value is nonzero, update screen now.
if (cmdpreview_type != 0) { if (cmdpreview_type != 0) {
int save_rd = RedrawingDisabled; int save_rd = RedrawingDisabled;
RedrawingDisabled = 0; RedrawingDisabled = 0;
@@ -2508,44 +2639,13 @@ static bool cmdpreview_may_show(CommandLineState *s)
if (icm_split && cmdpreview_type == 2 && cmdpreview_win != NULL) { if (icm_split && cmdpreview_type == 2 && cmdpreview_win != NULL) {
cmdpreview_close_win(); cmdpreview_close_win();
} }
// Clear preview highlights.
extmark_clear(curbuf, (uint32_t)cmdpreview_ns, 0, 0, MAXLNUM, MAXCOL);
curbuf->b_changed = save_b_changed; // Preserve 'modified' during preview // Restore state.
cmdpreview_restore_state(&cpinfo);
if (curbuf->b_u_seq_cur != save_b_u_seq_cur) {
// Undo invisibly. This also moves the cursor!
while (curbuf->b_u_seq_cur != save_b_u_seq_cur) {
if (!u_undo_and_forget(1)) {
abort();
}
}
// Restore newhead. It is meaningless when curhead is valid, but we must
// restore it so that undotree() is identical before/after the preview.
curbuf->b_u_newhead = save_b_u_newhead;
curbuf->b_u_time_cur = save_b_u_time_cur;
}
if (save_changedtick != buf_get_changedtick(curbuf)) {
buf_set_changedtick(curbuf, save_changedtick);
}
cmdmod = save_cmdmod; // Restore cmdmod
p_hls = save_hls; // Restore 'hlsearch'
curwin->w_p_cul = save_w_p_cul; // Restore 'cursorline'
curwin->w_p_cuc = save_w_p_cuc; // Restore 'cursorcolumn'
curbuf->b_p_ul = save_b_p_ul; // Restore 'undolevels'
restore_search_patterns(); // Restore search patterns
win_size_restore(&save_view); // Restore window sizes
ga_clear(&save_view);
unblock_autocmds(); // Unblock events unblock_autocmds(); // Unblock events
msg_silent--; // Unblock messages msg_silent--; // Unblock messages
emsg_silent--; // Unblock error reporting emsg_silent--; // Unblock error reporting
// Restore the window "view".
curwin->w_cursor = s->is_state.save_cursor;
restore_viewstate(&s->is_state.old_viewstate);
update_topline(curwin);
redrawcmdline(); redrawcmdline();
end: end:
xfree(cmdline); xfree(cmdline);

View File

@@ -7,61 +7,82 @@ local feed = helpers.feed
local command = helpers.command local command = helpers.command
local assert_alive = helpers.assert_alive local assert_alive = helpers.assert_alive
-- Implements a :Replace command that works like :substitute. -- Implements a :Replace command that works like :substitute and has multibuffer support.
local setup_replace_cmd = [[ local setup_replace_cmd = [[
local function show_replace_preview(buf, use_preview_win, preview_ns, preview_buf, matches) local function show_replace_preview(use_preview_win, preview_ns, preview_buf, matches)
-- Find the width taken by the largest line number, used for padding the line numbers -- Find the width taken by the largest line number, used for padding the line numbers
local highest_lnum = math.max(matches[#matches][1], 1) local highest_lnum = math.max(matches[#matches][1], 1)
local highest_lnum_width = math.floor(math.log10(highest_lnum)) local highest_lnum_width = math.floor(math.log10(highest_lnum))
local preview_buf_line = 0 local preview_buf_line = 0
local multibuffer = #matches > 1
vim.g.prevns = preview_ns
vim.g.prevbuf = preview_buf
for _, match in ipairs(matches) do for _, match in ipairs(matches) do
local lnum = match[1] local buf = match[1]
local line_matches = match[2] local buf_matches = match[2]
local prefix
if use_preview_win then if multibuffer and #buf_matches > 0 and use_preview_win then
prefix = string.format( local bufname = vim.api.nvim_buf_get_name(buf)
'|%s%d| ',
string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))), if bufname == "" then
lnum bufname = string.format("Buffer #%d", buf)
) end
vim.api.nvim_buf_set_lines( vim.api.nvim_buf_set_lines(
preview_buf, preview_buf,
preview_buf_line, preview_buf_line,
preview_buf_line, preview_buf_line,
0, 0,
{ prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] } { bufname .. ':' }
) )
preview_buf_line = preview_buf_line + 1
end end
for _, line_match in ipairs(line_matches) do for _, buf_match in ipairs(buf_matches) do
vim.api.nvim_buf_add_highlight( local lnum = buf_match[1]
buf, local line_matches = buf_match[2]
preview_ns, local prefix
'Substitute',
lnum - 1,
line_match[1],
line_match[2]
)
if use_preview_win then if use_preview_win then
vim.api.nvim_buf_add_highlight( prefix = string.format(
'|%s%d| ',
string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))),
lnum
)
vim.api.nvim_buf_set_lines(
preview_buf, preview_buf,
preview_ns,
'Substitute',
preview_buf_line, preview_buf_line,
#prefix + line_match[1], preview_buf_line,
#prefix + line_match[2] 0,
{ prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] }
) )
end end
end
preview_buf_line = preview_buf_line + 1 for _, line_match in ipairs(line_matches) do
vim.api.nvim_buf_add_highlight(
buf,
preview_ns,
'Substitute',
lnum - 1,
line_match[1],
line_match[2]
)
if use_preview_win then
vim.api.nvim_buf_add_highlight(
preview_buf,
preview_ns,
'Substitute',
preview_buf_line,
#prefix + line_match[1],
#prefix + line_match[2]
)
end
end
preview_buf_line = preview_buf_line + 1
end
end end
if use_preview_win then if use_preview_win then
@@ -72,94 +93,121 @@ local setup_replace_cmd = [[
end end
local function do_replace(opts, preview, preview_ns, preview_buf) local function do_replace(opts, preview, preview_ns, preview_buf)
local pat1 = opts.fargs[1] or '' local pat1 = opts.fargs[1]
if not pat1 then return end
local pat2 = opts.fargs[2] or '' local pat2 = opts.fargs[2] or ''
local line1 = opts.line1 local line1 = opts.line1
local line2 = opts.line2 local line2 = opts.line2
local buf = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, 0)
local matches = {} local matches = {}
for i, line in ipairs(lines) do -- Get list of valid and listed buffers
local startidx, endidx = 0, 0 local buffers = vim.tbl_filter(
local line_matches = {} function(buf)
local num = 1 if not (vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buflisted and buf ~= preview_buf)
then
return false
end
while startidx ~= -1 do -- Check if there's at least one window using the buffer
local match = vim.fn.matchstrpos(line, pat1, 0, num) for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
startidx, endidx = match[2], match[3] if vim.api.nvim_win_get_buf(win) == buf then
return true
end
end
if startidx ~= -1 then return false
line_matches[#line_matches+1] = { startidx, endidx } end,
vim.api.nvim_list_bufs()
)
for _, buf in ipairs(buffers) do
local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, false)
local buf_matches = {}
for i, line in ipairs(lines) do
local startidx, endidx = 0, 0
local line_matches = {}
local num = 1
while startidx ~= -1 do
local match = vim.fn.matchstrpos(line, pat1, 0, num)
startidx, endidx = match[2], match[3]
if startidx ~= -1 then
line_matches[#line_matches+1] = { startidx, endidx }
end
num = num + 1
end end
num = num + 1 if #line_matches > 0 then
buf_matches[#buf_matches+1] = { line1 + i - 1, line_matches }
end
end end
if #line_matches > 0 then local new_lines = {}
matches[#matches+1] = { line1 + i - 1, line_matches }
end
end
local new_lines = {} for _, buf_match in ipairs(buf_matches) do
local lnum = buf_match[1]
local line_matches = buf_match[2]
local line = lines[lnum - line1 + 1]
local pat_width_differences = {}
for _, match in ipairs(matches) do -- If previewing, only replace the text in current buffer if pat2 isn't empty
local lnum = match[1] -- Otherwise, always replace the text
local line_matches = match[2] if pat2 ~= '' or not preview then
local line = lines[lnum - line1 + 1] if preview then
local pat_width_differences = {} for _, line_match in ipairs(line_matches) do
local startidx, endidx = unpack(line_match)
local pat_match = line:sub(startidx + 1, endidx)
-- If previewing, only replace the text in current buffer if pat2 isn't empty pat_width_differences[#pat_width_differences+1] =
-- Otherwise, always replace the text #vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match
if pat2 ~= '' or not preview then end
end
new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g')
end
-- Highlight the matches if previewing
if preview then if preview then
for _, line_match in ipairs(line_matches) do local idx_offset = 0
for i, line_match in ipairs(line_matches) do
local startidx, endidx = unpack(line_match) local startidx, endidx = unpack(line_match)
local pat_match = line:sub(startidx + 1, endidx) -- Starting index of replacement text
local repl_startidx = startidx + idx_offset
-- Ending index of the replacement text (if pat2 isn't empty)
local repl_endidx
pat_width_differences[#pat_width_differences+1] = if pat2 ~= '' then
#vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match repl_endidx = endidx + idx_offset + pat_width_differences[i]
else
repl_endidx = endidx + idx_offset
end
if pat2 ~= '' then
idx_offset = idx_offset + pat_width_differences[i]
end
line_matches[i] = { repl_startidx, repl_endidx }
end end
end end
new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g')
end end
-- Highlight the matches if previewing for lnum, line in pairs(new_lines) do
if preview then vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line })
local idx_offset = 0
for i, line_match in ipairs(line_matches) do
local startidx, endidx = unpack(line_match)
-- Starting index of replacement text
local repl_startidx = startidx + idx_offset
-- Ending index of the replacement text (if pat2 isn't empty)
local repl_endidx
if pat2 ~= '' then
repl_endidx = endidx + idx_offset + pat_width_differences[i]
else
repl_endidx = endidx + idx_offset
end
if pat2 ~= '' then
idx_offset = idx_offset + pat_width_differences[i]
end
line_matches[i] = { repl_startidx, repl_endidx }
end
end end
end
for lnum, line in pairs(new_lines) do matches[#matches+1] = { buf, buf_matches }
vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line })
end end
if preview then if preview then
local lnum = vim.api.nvim_win_get_cursor(0)[1] local lnum = vim.api.nvim_win_get_cursor(0)[1]
-- Use preview window only if preview buffer is provided and range isn't just the current line -- Use preview window only if preview buffer is provided and range isn't just the current line
local use_preview_win = (preview_buf ~= nil) and (line1 ~= lnum or line2 ~= lnum) local use_preview_win = (preview_buf ~= nil) and (line1 ~= lnum or line2 ~= lnum)
return show_replace_preview(buf, use_preview_win, preview_ns, preview_buf, matches) return show_replace_preview(use_preview_win, preview_ns, preview_buf, matches)
end end
end end
@@ -354,3 +402,120 @@ describe("'inccommand' for user commands", function()
assert_alive() assert_alive()
end) end)
end) end)
describe("'inccommand' with multiple buffers", function()
local screen
before_each(function()
clear()
screen = Screen.new(40, 17)
screen:set_default_attr_ids({
[1] = {background = Screen.colors.Yellow1},
[2] = {foreground = Screen.colors.Blue1, bold = true},
[3] = {reverse = true},
[4] = {reverse = true, bold = true}
})
screen:attach()
exec_lua(setup_replace_cmd)
command('set cmdwinheight=10')
insert[[
foo bar baz
bar baz foo
baz foo bar
]]
command('vsplit | enew')
insert[[
bar baz foo
baz foo bar
foo bar baz
]]
end)
it('works', function()
command('set inccommand=nosplit')
feed(':Replace foo bar')
screen:expect([[
bar baz {1:bar} │ {1:bar} bar baz |
baz {1:bar} bar │ bar baz {1:bar} |
{1:bar} bar baz │ baz {1:bar} bar |
│ |
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{4:[No Name] [+] }{3:[No Name] [+] }|
:Replace foo bar^ |
]])
feed('<CR>')
screen:expect([[
bar baz bar │ bar bar baz |
baz bar bar │ bar baz bar |
bar bar baz │ baz bar bar |
^ │ |
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{4:[No Name] [+] }{3:[No Name] [+] }|
:Replace foo bar |
]])
end)
it('works with inccommand=split', function()
command('set inccommand=split')
feed(':Replace foo bar')
screen:expect([[
bar baz {1:bar} │ {1:bar} bar baz |
baz {1:bar} bar │ bar baz {1:bar} |
{1:bar} bar baz │ baz {1:bar} bar |
│ |
{4:[No Name] [+] }{3:[No Name] [+] }|
Buffer #1: |
|1| {1:bar} bar baz |
|2| bar baz {1:bar} |
|3| baz {1:bar} bar |
Buffer #2: |
|1| bar baz {1:bar} |
|2| baz {1:bar} bar |
|3| {1:bar} bar baz |
|
{2:~ }|
{3:[Preview] }|
:Replace foo bar^ |
]])
feed('<CR>')
screen:expect([[
bar baz bar │ bar bar baz |
baz bar bar │ bar baz bar |
bar bar baz │ baz bar bar |
^ │ |
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{2:~ }│{2:~ }|
{4:[No Name] [+] }{3:[No Name] [+] }|
:Replace foo bar |
]])
end)
end)