mirror of
https://github.com/neovim/neovim.git
synced 2026-01-17 10:38:57 +10:00
This freeing is necessary in Vim since the alloc can fail. Since we're
using xcalloc, that's not possible and the freeing will never run.
(cherry picked from commit 3f2e9298bd)
5671 lines
171 KiB
C
5671 lines
171 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
|
|
|
|
//
|
|
// buffer.c: functions for dealing with the buffer structure
|
|
//
|
|
|
|
//
|
|
// The buffer list is a double linked list of all buffers.
|
|
// Each buffer can be in one of these states:
|
|
// never loaded: BF_NEVERLOADED is set, only the file name is valid
|
|
// not loaded: b_ml.ml_mfp == NULL, no memfile allocated
|
|
// hidden: b_nwindows == 0, loaded but not displayed in a window
|
|
// normal: loaded and displayed in a window
|
|
//
|
|
// Instead of storing file names all over the place, each file name is
|
|
// stored in the buffer list. It can be referenced by a number.
|
|
//
|
|
// The current implementation remembers all file names ever used.
|
|
//
|
|
|
|
#include <assert.h>
|
|
#include <inttypes.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
|
|
#include "nvim/api/private/helpers.h"
|
|
#include "nvim/api/vim.h"
|
|
#include "nvim/ascii.h"
|
|
#include "nvim/assert.h"
|
|
#include "nvim/buffer.h"
|
|
#include "nvim/buffer_updates.h"
|
|
#include "nvim/change.h"
|
|
#include "nvim/channel.h"
|
|
#include "nvim/charset.h"
|
|
#include "nvim/cursor.h"
|
|
#include "nvim/decoration.h"
|
|
#include "nvim/diff.h"
|
|
#include "nvim/digraph.h"
|
|
#include "nvim/eval.h"
|
|
#include "nvim/ex_cmds.h"
|
|
#include "nvim/ex_cmds2.h"
|
|
#include "nvim/ex_docmd.h"
|
|
#include "nvim/ex_eval.h"
|
|
#include "nvim/ex_getln.h"
|
|
#include "nvim/extmark.h"
|
|
#include "nvim/file_search.h"
|
|
#include "nvim/fileio.h"
|
|
#include "nvim/fold.h"
|
|
#include "nvim/garray.h"
|
|
#include "nvim/getchar.h"
|
|
#include "nvim/hashtab.h"
|
|
#include "nvim/highlight.h"
|
|
#include "nvim/highlight_group.h"
|
|
#include "nvim/indent.h"
|
|
#include "nvim/indent_c.h"
|
|
#include "nvim/main.h"
|
|
#include "nvim/mark.h"
|
|
#include "nvim/mbyte.h"
|
|
#include "nvim/memory.h"
|
|
#include "nvim/message.h"
|
|
#include "nvim/move.h"
|
|
#include "nvim/option.h"
|
|
#include "nvim/os/input.h"
|
|
#include "nvim/os/os.h"
|
|
#include "nvim/os/time.h"
|
|
#include "nvim/os_unix.h"
|
|
#include "nvim/path.h"
|
|
#include "nvim/plines.h"
|
|
#include "nvim/quickfix.h"
|
|
#include "nvim/regexp.h"
|
|
#include "nvim/screen.h"
|
|
#include "nvim/shada.h"
|
|
#include "nvim/sign.h"
|
|
#include "nvim/spell.h"
|
|
#include "nvim/strings.h"
|
|
#include "nvim/syntax.h"
|
|
#include "nvim/ui.h"
|
|
#include "nvim/undo.h"
|
|
#include "nvim/version.h"
|
|
#include "nvim/vim.h"
|
|
#include "nvim/window.h"
|
|
|
|
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
|
# include "buffer.c.generated.h"
|
|
#endif
|
|
|
|
// Determines how deeply nested %{} blocks will be evaluated in statusline.
|
|
#define MAX_STL_EVAL_DEPTH 100
|
|
|
|
static char *msg_loclist = N_("[Location List]");
|
|
static char *msg_qflist = N_("[Quickfix List]");
|
|
static char *e_auabort = N_("E855: Autocommands caused command to abort");
|
|
|
|
// Number of times free_buffer() was called.
|
|
static int buf_free_count = 0;
|
|
|
|
typedef enum {
|
|
kBffClearWinInfo = 1,
|
|
kBffInitChangedtick = 2,
|
|
} BufFreeFlags;
|
|
|
|
/// Read data from buffer for retrying.
|
|
///
|
|
/// @param read_stdin read file from stdin, otherwise fifo
|
|
/// @param eap for forced 'ff' and 'fenc' or NULL
|
|
/// @param flags extra flags for readfile()
|
|
static int read_buffer(int read_stdin, exarg_T *eap, int flags)
|
|
{
|
|
int retval = OK;
|
|
linenr_T line_count;
|
|
|
|
// Read from the buffer which the text is already filled in and append at
|
|
// the end. This makes it possible to retry when 'fileformat' or
|
|
// 'fileencoding' was guessed wrong.
|
|
line_count = curbuf->b_ml.ml_line_count;
|
|
retval = readfile(read_stdin ? NULL : curbuf->b_ffname,
|
|
read_stdin ? NULL : curbuf->b_fname,
|
|
line_count, (linenr_T)0, (linenr_T)MAXLNUM, eap,
|
|
flags | READ_BUFFER);
|
|
if (retval == OK) {
|
|
// Delete the binary lines.
|
|
while (--line_count >= 0) {
|
|
ml_delete((linenr_T)1, false);
|
|
}
|
|
} else {
|
|
// Delete the converted lines.
|
|
while (curbuf->b_ml.ml_line_count > line_count) {
|
|
ml_delete(line_count, false);
|
|
}
|
|
}
|
|
// Put the cursor on the first line.
|
|
curwin->w_cursor.lnum = 1;
|
|
curwin->w_cursor.col = 0;
|
|
|
|
if (read_stdin) {
|
|
// Set or reset 'modified' before executing autocommands, so that
|
|
// it can be changed there.
|
|
if (!readonlymode && !buf_is_empty(curbuf)) {
|
|
changed();
|
|
} else if (retval != FAIL) {
|
|
unchanged(curbuf, false, true);
|
|
}
|
|
|
|
apply_autocmds_retval(EVENT_STDINREADPOST, NULL, NULL, false,
|
|
curbuf, &retval);
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
/// Open current buffer, that is: open the memfile and read the file into
|
|
/// memory.
|
|
///
|
|
/// @param read_stdin read file from stdin
|
|
/// @param eap for forced 'ff' and 'fenc' or NULL
|
|
/// @param flags extra flags for readfile()
|
|
///
|
|
/// @return FAIL for failure, OK otherwise.
|
|
int open_buffer(int read_stdin, exarg_T *eap, int flags)
|
|
{
|
|
int retval = OK;
|
|
bufref_T old_curbuf;
|
|
long old_tw = curbuf->b_p_tw;
|
|
int read_fifo = false;
|
|
|
|
// The 'readonly' flag is only set when BF_NEVERLOADED is being reset.
|
|
// When re-entering the same buffer, it should not change, because the
|
|
// user may have reset the flag by hand.
|
|
if (readonlymode && curbuf->b_ffname != NULL
|
|
&& (curbuf->b_flags & BF_NEVERLOADED)) {
|
|
curbuf->b_p_ro = true;
|
|
}
|
|
|
|
if (ml_open(curbuf) == FAIL) {
|
|
// There MUST be a memfile, otherwise we can't do anything
|
|
// If we can't create one for the current buffer, take another buffer
|
|
close_buffer(NULL, curbuf, 0, false, false);
|
|
|
|
curbuf = NULL;
|
|
FOR_ALL_BUFFERS(buf) {
|
|
if (buf->b_ml.ml_mfp != NULL) {
|
|
curbuf = buf;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If there is no memfile at all, exit.
|
|
// This is OK, since there are no changes to lose.
|
|
if (curbuf == NULL) {
|
|
emsg(_("E82: Cannot allocate any buffer, exiting..."));
|
|
|
|
// Don't try to do any saving, with "curbuf" NULL almost nothing
|
|
// will work.
|
|
v_dying = 2;
|
|
getout(2);
|
|
}
|
|
|
|
emsg(_("E83: Cannot allocate buffer, using other one..."));
|
|
enter_buffer(curbuf);
|
|
if (old_tw != curbuf->b_p_tw) {
|
|
check_colorcolumn(curwin);
|
|
}
|
|
return FAIL;
|
|
}
|
|
|
|
// The autocommands in readfile() may change the buffer, but only AFTER
|
|
// reading the file.
|
|
set_bufref(&old_curbuf, curbuf);
|
|
modified_was_set = false;
|
|
|
|
// mark cursor position as being invalid
|
|
curwin->w_valid = 0;
|
|
|
|
if (curbuf->b_ffname != NULL) {
|
|
int old_msg_silent = msg_silent;
|
|
#ifdef UNIX
|
|
int save_bin = curbuf->b_p_bin;
|
|
int perm;
|
|
|
|
perm = os_getperm((const char *)curbuf->b_ffname);
|
|
if (perm >= 0 && (0 || S_ISFIFO(perm)
|
|
|| S_ISSOCK(perm)
|
|
# ifdef OPEN_CHR_FILES
|
|
|| (S_ISCHR(perm)
|
|
&& is_dev_fd_file(curbuf->b_ffname))
|
|
# endif
|
|
)) { // NOLINT(whitespace/parens)
|
|
read_fifo = true;
|
|
}
|
|
if (read_fifo) {
|
|
curbuf->b_p_bin = true;
|
|
}
|
|
#endif
|
|
if (shortmess(SHM_FILEINFO)) {
|
|
msg_silent = 1;
|
|
}
|
|
|
|
retval = readfile(curbuf->b_ffname, curbuf->b_fname,
|
|
(linenr_T)0, (linenr_T)0, (linenr_T)MAXLNUM, eap,
|
|
flags | READ_NEW | (read_fifo ? READ_FIFO : 0));
|
|
#ifdef UNIX
|
|
if (read_fifo) {
|
|
curbuf->b_p_bin = save_bin;
|
|
if (retval == OK) {
|
|
retval = read_buffer(false, eap, flags);
|
|
}
|
|
}
|
|
#endif
|
|
msg_silent = old_msg_silent;
|
|
|
|
// Help buffer is filtered.
|
|
if (bt_help(curbuf)) {
|
|
fix_help_buffer();
|
|
}
|
|
} else if (read_stdin) {
|
|
int save_bin = curbuf->b_p_bin;
|
|
|
|
// First read the text in binary mode into the buffer.
|
|
// Then read from that same buffer and append at the end. This makes
|
|
// it possible to retry when 'fileformat' or 'fileencoding' was
|
|
// guessed wrong.
|
|
curbuf->b_p_bin = true;
|
|
retval = readfile(NULL, NULL, (linenr_T)0,
|
|
(linenr_T)0, (linenr_T)MAXLNUM, NULL,
|
|
flags | (READ_NEW + READ_STDIN));
|
|
curbuf->b_p_bin = save_bin;
|
|
if (retval == OK) {
|
|
retval = read_buffer(true, eap, flags);
|
|
}
|
|
}
|
|
|
|
// if first time loading this buffer, init b_chartab[]
|
|
if (curbuf->b_flags & BF_NEVERLOADED) {
|
|
(void)buf_init_chartab(curbuf, false);
|
|
parse_cino(curbuf);
|
|
}
|
|
|
|
// Set/reset the Changed flag first, autocmds may change the buffer.
|
|
// Apply the automatic commands, before processing the modelines.
|
|
// So the modelines have priority over autocommands.
|
|
|
|
// When reading stdin, the buffer contents always needs writing, so set
|
|
// the changed flag. Unless in readonly mode: "ls | nvim -R -".
|
|
// When interrupted and 'cpoptions' contains 'i' set changed flag.
|
|
if ((got_int && vim_strchr(p_cpo, CPO_INTMOD) != NULL)
|
|
|| modified_was_set // ":set modified" used in autocmd
|
|
|| (aborting() && vim_strchr(p_cpo, CPO_INTMOD) != NULL)) {
|
|
changed();
|
|
} else if (retval != FAIL && !read_stdin && !read_fifo) {
|
|
unchanged(curbuf, false, true);
|
|
}
|
|
save_file_ff(curbuf); // keep this fileformat
|
|
|
|
// Set last_changedtick to avoid triggering a TextChanged autocommand right
|
|
// after it was added.
|
|
curbuf->b_last_changedtick = buf_get_changedtick(curbuf);
|
|
curbuf->b_last_changedtick_pum = buf_get_changedtick(curbuf);
|
|
|
|
// require "!" to overwrite the file, because it wasn't read completely
|
|
if (aborting()) {
|
|
curbuf->b_flags |= BF_READERR;
|
|
}
|
|
|
|
// Need to update automatic folding. Do this before the autocommands,
|
|
// they may use the fold info.
|
|
foldUpdateAll(curwin);
|
|
|
|
// need to set w_topline, unless some autocommand already did that.
|
|
if (!(curwin->w_valid & VALID_TOPLINE)) {
|
|
curwin->w_topline = 1;
|
|
curwin->w_topfill = 0;
|
|
}
|
|
apply_autocmds_retval(EVENT_BUFENTER, NULL, NULL, false, curbuf, &retval);
|
|
|
|
if (retval == FAIL) {
|
|
return FAIL;
|
|
}
|
|
|
|
// The autocommands may have changed the current buffer. Apply the
|
|
// modelines to the correct buffer, if it still exists and is loaded.
|
|
if (bufref_valid(&old_curbuf) && old_curbuf.br_buf->b_ml.ml_mfp != NULL) {
|
|
aco_save_T aco;
|
|
|
|
// Go to the buffer that was opened.
|
|
aucmd_prepbuf(&aco, old_curbuf.br_buf);
|
|
do_modelines(0);
|
|
curbuf->b_flags &= ~(BF_CHECK_RO | BF_NEVERLOADED);
|
|
|
|
if ((flags & READ_NOWINENTER) == 0) {
|
|
apply_autocmds_retval(EVENT_BUFWINENTER, NULL, NULL, false, curbuf,
|
|
&retval);
|
|
}
|
|
|
|
// restore curwin/curbuf and a few other things
|
|
aucmd_restbuf(&aco);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
/// Store "buf" in "bufref" and set the free count.
|
|
///
|
|
/// @param bufref Reference to be used for the buffer.
|
|
/// @param buf The buffer to reference.
|
|
void set_bufref(bufref_T *bufref, buf_T *buf)
|
|
{
|
|
bufref->br_buf = buf;
|
|
bufref->br_fnum = buf == NULL ? 0 : buf->b_fnum;
|
|
bufref->br_buf_free_count = buf_free_count;
|
|
}
|
|
|
|
/// Return true if "bufref->br_buf" points to the same buffer as when
|
|
/// set_bufref() was called and it is a valid buffer.
|
|
/// Only goes through the buffer list if buf_free_count changed.
|
|
/// Also checks if b_fnum is still the same, a :bwipe followed by :new might get
|
|
/// the same allocated memory, but it's a different buffer.
|
|
///
|
|
/// @param bufref Buffer reference to check for.
|
|
bool bufref_valid(bufref_T *bufref)
|
|
{
|
|
return bufref->br_buf_free_count == buf_free_count
|
|
? true
|
|
: buf_valid(bufref->br_buf) && bufref->br_fnum == bufref->br_buf->b_fnum;
|
|
}
|
|
|
|
/// Check that "buf" points to a valid buffer in the buffer list.
|
|
///
|
|
/// Can be slow if there are many buffers, prefer using bufref_valid().
|
|
///
|
|
/// @param buf The buffer to check for.
|
|
bool buf_valid(buf_T *buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
if (buf == NULL) {
|
|
return false;
|
|
}
|
|
// Assume that we more often have a recent buffer,
|
|
// start with the last one.
|
|
for (buf_T *bp = lastbuf; bp != NULL; bp = bp->b_prev) {
|
|
if (bp == buf) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Close the link to a buffer.
|
|
///
|
|
/// @param win If not NULL, set b_last_cursor.
|
|
/// @param buf
|
|
/// @param action Used when there is no longer a window for the buffer.
|
|
/// Possible values:
|
|
/// 0 buffer becomes hidden
|
|
/// DOBUF_UNLOAD buffer is unloaded
|
|
/// DOBUF_DELETE buffer is unloaded and removed from buffer list
|
|
/// DOBUF_WIPE buffer is unloaded and really deleted
|
|
/// When doing all but the first one on the current buffer, the
|
|
/// caller should get a new buffer very soon!
|
|
/// The 'bufhidden' option can force freeing and deleting.
|
|
/// @param abort_if_last
|
|
/// If true, do not close the buffer if autocommands cause
|
|
/// there to be only one window with this buffer. e.g. when
|
|
/// ":quit" is supposed to close the window but autocommands
|
|
/// close all other windows.
|
|
/// @param ignore_abort
|
|
/// If true, don't abort even when aborting() returns true.
|
|
/// @return true when we got to the end and b_nwindows was decremented.
|
|
bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool ignore_abort)
|
|
{
|
|
bool unload_buf = (action != 0);
|
|
bool del_buf = (action == DOBUF_DEL || action == DOBUF_WIPE);
|
|
bool wipe_buf = (action == DOBUF_WIPE);
|
|
|
|
bool is_curwin = (curwin != NULL && curwin->w_buffer == buf);
|
|
win_T *the_curwin = curwin;
|
|
tabpage_T *the_curtab = curtab;
|
|
|
|
// Force unloading or deleting when 'bufhidden' says so, but not for terminal
|
|
// buffers.
|
|
// The caller must take care of NOT deleting/freeing when 'bufhidden' is
|
|
// "hide" (otherwise we could never free or delete a buffer).
|
|
if (!buf->terminal) {
|
|
if (buf->b_p_bh[0] == 'd') { // 'bufhidden' == "delete"
|
|
del_buf = true;
|
|
unload_buf = true;
|
|
} else if (buf->b_p_bh[0] == 'w') { // 'bufhidden' == "wipe"
|
|
del_buf = true;
|
|
unload_buf = true;
|
|
wipe_buf = true;
|
|
} else if (buf->b_p_bh[0] == 'u') { // 'bufhidden' == "unload"
|
|
unload_buf = true;
|
|
}
|
|
}
|
|
|
|
if (buf->terminal && (unload_buf || del_buf || wipe_buf)) {
|
|
// terminal buffers can only be wiped
|
|
unload_buf = true;
|
|
del_buf = true;
|
|
wipe_buf = true;
|
|
}
|
|
|
|
// Disallow deleting the buffer when it is locked (already being closed or
|
|
// halfway a command that relies on it). Unloading is allowed.
|
|
if (buf->b_locked > 0 && (del_buf || wipe_buf)) {
|
|
emsg(_("E937: Attempt to delete a buffer that is in use"));
|
|
return false;
|
|
}
|
|
|
|
if (win != NULL // Avoid bogus clang warning.
|
|
&& win_valid_any_tab(win)) {
|
|
// Set b_last_cursor when closing the last window for the buffer.
|
|
// Remember the last cursor position and window options of the buffer.
|
|
// This used to be only for the current window, but then options like
|
|
// 'foldmethod' may be lost with a ":only" command.
|
|
if (buf->b_nwindows == 1) {
|
|
set_last_cursor(win);
|
|
}
|
|
buflist_setfpos(buf, win,
|
|
win->w_cursor.lnum == 1 ? 0 : win->w_cursor.lnum,
|
|
win->w_cursor.col, true);
|
|
}
|
|
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
|
|
// When the buffer is no longer in a window, trigger BufWinLeave
|
|
if (buf->b_nwindows == 1) {
|
|
buf->b_locked++;
|
|
if (apply_autocmds(EVENT_BUFWINLEAVE, buf->b_fname, buf->b_fname, false,
|
|
buf) && !bufref_valid(&bufref)) {
|
|
// Autocommands deleted the buffer.
|
|
emsg(_(e_auabort));
|
|
return false;
|
|
}
|
|
buf->b_locked--;
|
|
if (abort_if_last && last_nonfloat(win)) {
|
|
// Autocommands made this the only window.
|
|
emsg(_(e_auabort));
|
|
return false;
|
|
}
|
|
|
|
// When the buffer becomes hidden, but is not unloaded, trigger
|
|
// BufHidden
|
|
if (!unload_buf) {
|
|
buf->b_locked++;
|
|
if (apply_autocmds(EVENT_BUFHIDDEN, buf->b_fname, buf->b_fname, false,
|
|
buf) && !bufref_valid(&bufref)) {
|
|
// Autocommands deleted the buffer.
|
|
emsg(_(e_auabort));
|
|
return false;
|
|
}
|
|
buf->b_locked--;
|
|
if (abort_if_last && last_nonfloat(win)) {
|
|
// Autocommands made this the only window.
|
|
emsg(_(e_auabort));
|
|
return false;
|
|
}
|
|
}
|
|
// autocmds may abort script processing
|
|
if (!ignore_abort && aborting()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If the buffer was in curwin and the window has changed, go back to that
|
|
// window, if it still exists. This avoids that ":edit x" triggering a
|
|
// "tabnext" BufUnload autocmd leaves a window behind without a buffer.
|
|
if (is_curwin && curwin != the_curwin && win_valid_any_tab(the_curwin)) {
|
|
block_autocmds();
|
|
goto_tabpage_win(the_curtab, the_curwin);
|
|
unblock_autocmds();
|
|
}
|
|
|
|
int nwindows = buf->b_nwindows;
|
|
|
|
// decrease the link count from windows (unless not in any window)
|
|
if (buf->b_nwindows > 0) {
|
|
buf->b_nwindows--;
|
|
}
|
|
|
|
if (diffopt_hiddenoff() && !unload_buf && buf->b_nwindows == 0) {
|
|
diff_buf_delete(buf); // Clear 'diff' for hidden buffer.
|
|
}
|
|
|
|
// Return when a window is displaying the buffer or when it's not
|
|
// unloaded.
|
|
if (buf->b_nwindows > 0 || !unload_buf) {
|
|
return false;
|
|
}
|
|
|
|
if (buf->terminal) {
|
|
terminal_close(buf->terminal, -1);
|
|
}
|
|
|
|
// Always remove the buffer when there is no file name.
|
|
if (buf->b_ffname == NULL) {
|
|
del_buf = true;
|
|
}
|
|
|
|
// Free all things allocated for this buffer.
|
|
// Also calls the "BufDelete" autocommands when del_buf is true.
|
|
// Remember if we are closing the current buffer. Restore the number of
|
|
// windows, so that autocommands in buf_freeall() don't get confused.
|
|
bool is_curbuf = (buf == curbuf);
|
|
|
|
// When closing the current buffer stop Visual mode before freeing
|
|
// anything.
|
|
if (is_curbuf && VIsual_active
|
|
#if defined(EXITFREE)
|
|
&& !entered_free_all_mem
|
|
#endif
|
|
) {
|
|
end_visual_mode();
|
|
}
|
|
|
|
buf->b_nwindows = nwindows;
|
|
|
|
buf_freeall(buf, ((del_buf ? BFA_DEL : 0)
|
|
+ (wipe_buf ? BFA_WIPE : 0)
|
|
+ (ignore_abort ? BFA_IGNORE_ABORT : 0)));
|
|
|
|
if (!bufref_valid(&bufref)) {
|
|
// Autocommands may have deleted the buffer.
|
|
return false;
|
|
}
|
|
// autocmds may abort script processing.
|
|
if (!ignore_abort && aborting()) {
|
|
return false;
|
|
}
|
|
|
|
// It's possible that autocommands change curbuf to the one being deleted.
|
|
// This might cause the previous curbuf to be deleted unexpectedly. But
|
|
// in some cases it's OK to delete the curbuf, because a new one is
|
|
// obtained anyway. Therefore only return if curbuf changed to the
|
|
// deleted buffer.
|
|
if (buf == curbuf && !is_curbuf) {
|
|
return false;
|
|
}
|
|
|
|
if (win != NULL // Avoid bogus clang warning.
|
|
&& win_valid_any_tab(win)
|
|
&& win->w_buffer == buf) {
|
|
win->w_buffer = NULL; // make sure we don't use the buffer now
|
|
}
|
|
|
|
// Autocommands may have opened or closed windows for this buffer.
|
|
// Decrement the count for the close we do here.
|
|
if (buf->b_nwindows > 0) {
|
|
buf->b_nwindows--;
|
|
}
|
|
|
|
// Disable buffer-updates for the current buffer.
|
|
// No need to check `unload_buf`: in that case the function returned above.
|
|
buf_updates_unload(buf, false);
|
|
|
|
// Remove the buffer from the list.
|
|
if (wipe_buf) {
|
|
// Do not wipe out the buffer if it is used in a window.
|
|
if (buf->b_nwindows > 0) {
|
|
return false;
|
|
}
|
|
if (buf->b_sfname != buf->b_ffname) {
|
|
XFREE_CLEAR(buf->b_sfname);
|
|
} else {
|
|
buf->b_sfname = NULL;
|
|
}
|
|
XFREE_CLEAR(buf->b_ffname);
|
|
if (buf->b_prev == NULL) {
|
|
firstbuf = buf->b_next;
|
|
} else {
|
|
buf->b_prev->b_next = buf->b_next;
|
|
}
|
|
if (buf->b_next == NULL) {
|
|
lastbuf = buf->b_prev;
|
|
} else {
|
|
buf->b_next->b_prev = buf->b_prev;
|
|
}
|
|
free_buffer(buf);
|
|
} else {
|
|
if (del_buf) {
|
|
// Free all internal variables and reset option values, to make
|
|
// ":bdel" compatible with Vim 5.7.
|
|
free_buffer_stuff(buf, kBffClearWinInfo | kBffInitChangedtick);
|
|
|
|
// Make it look like a new buffer.
|
|
buf->b_flags = BF_CHECK_RO | BF_NEVERLOADED;
|
|
|
|
// Init the options when loaded again.
|
|
buf->b_p_initialized = false;
|
|
}
|
|
buf_clear_file(buf);
|
|
if (del_buf) {
|
|
buf->b_p_bl = false;
|
|
}
|
|
}
|
|
// NOTE: at this point "curbuf" may be invalid!
|
|
return true;
|
|
}
|
|
|
|
/// Make buffer not contain a file.
|
|
void buf_clear_file(buf_T *buf)
|
|
{
|
|
buf->b_ml.ml_line_count = 1;
|
|
unchanged(buf, true, true);
|
|
buf->b_p_eol = true;
|
|
buf->b_start_eol = true;
|
|
buf->b_p_bomb = false;
|
|
buf->b_start_bomb = false;
|
|
buf->b_ml.ml_mfp = NULL;
|
|
buf->b_ml.ml_flags = ML_EMPTY; // empty buffer
|
|
}
|
|
|
|
/// Clears the current buffer contents.
|
|
void buf_clear(void)
|
|
{
|
|
linenr_T line_count = curbuf->b_ml.ml_line_count;
|
|
while (!(curbuf->b_ml.ml_flags & ML_EMPTY)) {
|
|
ml_delete((linenr_T)1, false);
|
|
}
|
|
deleted_lines_mark(1, line_count); // prepare for display
|
|
ml_close(curbuf, true); // free memline_T
|
|
buf_clear_file(curbuf);
|
|
}
|
|
|
|
/// buf_freeall() - free all things allocated for a buffer that are related to
|
|
/// the file. Careful: get here with "curwin" NULL when exiting.
|
|
///
|
|
/// @param flags BFA_DEL buffer is going to be deleted
|
|
/// BFA_WIPE buffer is going to be wiped out
|
|
/// BFA_KEEP_UNDO do not free undo information
|
|
/// BFA_IGNORE_ABORT don't abort even when aborting() returns true
|
|
void buf_freeall(buf_T *buf, int flags)
|
|
{
|
|
bool is_curbuf = (buf == curbuf);
|
|
int is_curwin = (curwin != NULL && curwin->w_buffer == buf);
|
|
win_T *the_curwin = curwin;
|
|
tabpage_T *the_curtab = curtab;
|
|
|
|
// Make sure the buffer isn't closed by autocommands.
|
|
buf->b_locked++;
|
|
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
|
|
if ((buf->b_ml.ml_mfp != NULL)
|
|
&& apply_autocmds(EVENT_BUFUNLOAD, buf->b_fname, buf->b_fname, false, buf)
|
|
&& !bufref_valid(&bufref)) {
|
|
// Autocommands deleted the buffer.
|
|
return;
|
|
}
|
|
if ((flags & BFA_DEL)
|
|
&& buf->b_p_bl
|
|
&& apply_autocmds(EVENT_BUFDELETE, buf->b_fname, buf->b_fname, false, buf)
|
|
&& !bufref_valid(&bufref)) {
|
|
// Autocommands may delete the buffer.
|
|
return;
|
|
}
|
|
if ((flags & BFA_WIPE)
|
|
&& apply_autocmds(EVENT_BUFWIPEOUT, buf->b_fname, buf->b_fname, false,
|
|
buf)
|
|
&& !bufref_valid(&bufref)) {
|
|
// Autocommands may delete the buffer.
|
|
return;
|
|
}
|
|
buf->b_locked--;
|
|
|
|
// If the buffer was in curwin and the window has changed, go back to that
|
|
// window, if it still exists. This avoids that ":edit x" triggering a
|
|
// "tabnext" BufUnload autocmd leaves a window behind without a buffer.
|
|
if (is_curwin && curwin != the_curwin && win_valid_any_tab(the_curwin)) {
|
|
block_autocmds();
|
|
goto_tabpage_win(the_curtab, the_curwin);
|
|
unblock_autocmds();
|
|
}
|
|
// autocmds may abort script processing
|
|
if ((flags & BFA_IGNORE_ABORT) == 0 && aborting()) {
|
|
return;
|
|
}
|
|
|
|
// It's possible that autocommands change curbuf to the one being deleted.
|
|
// This might cause curbuf to be deleted unexpectedly. But in some cases
|
|
// it's OK to delete the curbuf, because a new one is obtained anyway.
|
|
// Therefore only return if curbuf changed to the deleted buffer.
|
|
if (buf == curbuf && !is_curbuf) {
|
|
return;
|
|
}
|
|
diff_buf_delete(buf); // Can't use 'diff' for unloaded buffer.
|
|
// Remove any ownsyntax, unless exiting.
|
|
if (curwin != NULL && curwin->w_buffer == buf) {
|
|
reset_synblock(curwin);
|
|
}
|
|
|
|
// No folds in an empty buffer.
|
|
FOR_ALL_TAB_WINDOWS(tp, win) {
|
|
if (win->w_buffer == buf) {
|
|
clearFolding(win);
|
|
}
|
|
}
|
|
|
|
ml_close(buf, true); // close and delete the memline/memfile
|
|
buf->b_ml.ml_line_count = 0; // no lines in buffer
|
|
if ((flags & BFA_KEEP_UNDO) == 0) {
|
|
u_blockfree(buf); // free the memory allocated for undo
|
|
u_clearall(buf); // reset all undo information
|
|
}
|
|
syntax_clear(&buf->b_s); // reset syntax info
|
|
buf->b_flags &= ~BF_READERR; // a read error is no longer relevant
|
|
}
|
|
|
|
/// Free a buffer structure and the things it contains related to the buffer
|
|
/// itself (not the file, that must have been done already).
|
|
static void free_buffer(buf_T *buf)
|
|
{
|
|
pmap_del(handle_T)(&buffer_handles, buf->b_fnum);
|
|
buf_free_count++;
|
|
// b:changedtick uses an item in buf_T.
|
|
free_buffer_stuff(buf, kBffClearWinInfo);
|
|
if (buf->b_vars->dv_refcount > DO_NOT_FREE_CNT) {
|
|
tv_dict_add(buf->b_vars,
|
|
tv_dict_item_copy((dictitem_T *)(&buf->changedtick_di)));
|
|
}
|
|
unref_var_dict(buf->b_vars);
|
|
aubuflocal_remove(buf);
|
|
tv_dict_unref(buf->additional_data);
|
|
xfree(buf->b_prompt_text);
|
|
callback_free(&buf->b_prompt_callback);
|
|
callback_free(&buf->b_prompt_interrupt);
|
|
clear_fmark(&buf->b_last_cursor);
|
|
clear_fmark(&buf->b_last_insert);
|
|
clear_fmark(&buf->b_last_change);
|
|
for (size_t i = 0; i < NMARKS; i++) {
|
|
free_fmark(buf->b_namedm[i]);
|
|
}
|
|
for (int i = 0; i < buf->b_changelistlen; i++) {
|
|
free_fmark(buf->b_changelist[i]);
|
|
}
|
|
if (autocmd_busy) {
|
|
// Do not free the buffer structure while autocommands are executing,
|
|
// it's still needed. Free it when autocmd_busy is reset.
|
|
memset(&buf->b_namedm[0], 0, sizeof(buf->b_namedm));
|
|
memset(&buf->b_changelist[0], 0, sizeof(buf->b_changelist));
|
|
buf->b_next = au_pending_free_buf;
|
|
au_pending_free_buf = buf;
|
|
} else {
|
|
xfree(buf);
|
|
}
|
|
}
|
|
|
|
/// Free stuff in the buffer for ":bdel" and when wiping out the buffer.
|
|
///
|
|
/// @param buf Buffer pointer
|
|
/// @param free_flags BufFreeFlags
|
|
static void free_buffer_stuff(buf_T *buf, int free_flags)
|
|
{
|
|
if (free_flags & kBffClearWinInfo) {
|
|
clear_wininfo(buf); // including window-local options
|
|
free_buf_options(buf, true);
|
|
ga_clear(&buf->b_s.b_langp);
|
|
}
|
|
{
|
|
// Avoid losing b:changedtick when deleting buffer: clearing variables
|
|
// implies using clear_tv() on b:changedtick and that sets changedtick to
|
|
// zero.
|
|
hashitem_T *const changedtick_hi = hash_find(&buf->b_vars->dv_hashtab,
|
|
(const char_u *)"changedtick");
|
|
assert(changedtick_hi != NULL);
|
|
hash_remove(&buf->b_vars->dv_hashtab, changedtick_hi);
|
|
}
|
|
vars_clear(&buf->b_vars->dv_hashtab); // free all internal variables
|
|
hash_init(&buf->b_vars->dv_hashtab);
|
|
if (free_flags & kBffInitChangedtick) {
|
|
buf_init_changedtick(buf);
|
|
}
|
|
uc_clear(&buf->b_ucmds); // clear local user commands
|
|
buf_delete_signs(buf, (char_u *)"*"); // delete any signs
|
|
extmark_free_all(buf); // delete any extmarks
|
|
map_clear_int(buf, MAP_ALL_MODES, true, false); // clear local mappings
|
|
map_clear_int(buf, MAP_ALL_MODES, true, true); // clear local abbrevs
|
|
XFREE_CLEAR(buf->b_start_fenc);
|
|
|
|
buf_updates_unload(buf, false);
|
|
}
|
|
|
|
/// Free the b_wininfo list for buffer "buf".
|
|
static void clear_wininfo(buf_T *buf)
|
|
{
|
|
wininfo_T *wip;
|
|
|
|
while (buf->b_wininfo != NULL) {
|
|
wip = buf->b_wininfo;
|
|
buf->b_wininfo = wip->wi_next;
|
|
free_wininfo(wip, buf);
|
|
}
|
|
}
|
|
|
|
/// Go to another buffer. Handles the result of the ATTENTION dialog.
|
|
void goto_buffer(exarg_T *eap, int start, int dir, int count)
|
|
{
|
|
bufref_T old_curbuf;
|
|
set_bufref(&old_curbuf, curbuf);
|
|
swap_exists_action = SEA_DIALOG;
|
|
|
|
(void)do_buffer(*eap->cmd == 's' ? DOBUF_SPLIT : DOBUF_GOTO,
|
|
start, dir, count, eap->forceit);
|
|
|
|
if (swap_exists_action == SEA_QUIT && *eap->cmd == 's') {
|
|
cleanup_T cs;
|
|
|
|
// Reset the error/interrupt/exception state here so that
|
|
// aborting() returns false when closing a window.
|
|
enter_cleanup(&cs);
|
|
|
|
// Quitting means closing the split window, nothing else.
|
|
win_close(curwin, true, false);
|
|
swap_exists_action = SEA_NONE;
|
|
swap_exists_did_quit = true;
|
|
|
|
// Restore the error/interrupt/exception state if not discarded by a
|
|
// new aborting error, interrupt, or uncaught exception.
|
|
leave_cleanup(&cs);
|
|
} else {
|
|
handle_swap_exists(&old_curbuf);
|
|
}
|
|
}
|
|
|
|
/// Handle the situation of swap_exists_action being set.
|
|
///
|
|
/// It is allowed for "old_curbuf" to be NULL or invalid.
|
|
///
|
|
/// @param old_curbuf The buffer to check for.
|
|
void handle_swap_exists(bufref_T *old_curbuf)
|
|
{
|
|
cleanup_T cs;
|
|
long old_tw = curbuf->b_p_tw;
|
|
buf_T *buf;
|
|
|
|
if (swap_exists_action == SEA_QUIT) {
|
|
// Reset the error/interrupt/exception state here so that
|
|
// aborting() returns false when closing a buffer.
|
|
enter_cleanup(&cs);
|
|
|
|
// User selected Quit at ATTENTION prompt. Go back to previous
|
|
// buffer. If that buffer is gone or the same as the current one,
|
|
// open a new, empty buffer.
|
|
swap_exists_action = SEA_NONE; // don't want it again
|
|
swap_exists_did_quit = true;
|
|
close_buffer(curwin, curbuf, DOBUF_UNLOAD, false, false);
|
|
if (old_curbuf == NULL
|
|
|| !bufref_valid(old_curbuf)
|
|
|| old_curbuf->br_buf == curbuf) {
|
|
// Block autocommands here because curwin->w_buffer is NULL.
|
|
block_autocmds();
|
|
buf = buflist_new(NULL, NULL, 1L, BLN_CURBUF | BLN_LISTED);
|
|
unblock_autocmds();
|
|
} else {
|
|
buf = old_curbuf->br_buf;
|
|
}
|
|
if (buf != NULL) {
|
|
int old_msg_silent = msg_silent;
|
|
|
|
if (shortmess(SHM_FILEINFO)) {
|
|
msg_silent = 1; // prevent fileinfo message
|
|
}
|
|
enter_buffer(buf);
|
|
// restore msg_silent, so that the command line will be shown
|
|
msg_silent = old_msg_silent;
|
|
|
|
if (old_tw != curbuf->b_p_tw) {
|
|
check_colorcolumn(curwin);
|
|
}
|
|
}
|
|
// If "old_curbuf" is NULL we are in big trouble here...
|
|
|
|
// Restore the error/interrupt/exception state if not discarded by a
|
|
// new aborting error, interrupt, or uncaught exception.
|
|
leave_cleanup(&cs);
|
|
} else if (swap_exists_action == SEA_RECOVER) {
|
|
// Reset the error/interrupt/exception state here so that
|
|
// aborting() returns false when closing a buffer.
|
|
enter_cleanup(&cs);
|
|
|
|
// User selected Recover at ATTENTION prompt.
|
|
msg_scroll = true;
|
|
ml_recover(false);
|
|
msg_puts("\n"); // don't overwrite the last message
|
|
cmdline_row = msg_row;
|
|
do_modelines(0);
|
|
|
|
// Restore the error/interrupt/exception state if not discarded by a
|
|
// new aborting error, interrupt, or uncaught exception.
|
|
leave_cleanup(&cs);
|
|
}
|
|
swap_exists_action = SEA_NONE; // -V519
|
|
}
|
|
|
|
/// do_bufdel() - delete or unload buffer(s)
|
|
///
|
|
/// addr_count == 0: ":bdel" - delete current buffer
|
|
/// addr_count == 1: ":N bdel" or ":bdel N [N ..]" - first delete
|
|
/// buffer "end_bnr", then any other arguments.
|
|
/// addr_count == 2: ":N,N bdel" - delete buffers in range
|
|
///
|
|
/// command can be DOBUF_UNLOAD (":bunload"), DOBUF_WIPE (":bwipeout") or
|
|
/// DOBUF_DEL (":bdel")
|
|
///
|
|
/// @param arg pointer to extra arguments
|
|
/// @param start_bnr first buffer number in a range
|
|
/// @param end_bnr buffer nr or last buffer nr in a range
|
|
///
|
|
/// @return error message or NULL
|
|
char *do_bufdel(int command, char_u *arg, int addr_count, int start_bnr, int end_bnr, int forceit)
|
|
{
|
|
int do_current = 0; // delete current buffer?
|
|
int deleted = 0; // number of buffers deleted
|
|
char *errormsg = NULL; // return value
|
|
int bnr; // buffer number
|
|
char_u *p;
|
|
|
|
if (addr_count == 0) {
|
|
(void)do_buffer(command, DOBUF_CURRENT, FORWARD, 0, forceit);
|
|
} else {
|
|
if (addr_count == 2) {
|
|
if (*arg) { // both range and argument is not allowed
|
|
return _(e_trailing);
|
|
}
|
|
bnr = start_bnr;
|
|
} else { // addr_count == 1
|
|
bnr = end_bnr;
|
|
}
|
|
|
|
for (; !got_int; os_breakcheck()) {
|
|
// delete the current buffer last, otherwise when the
|
|
// current buffer is deleted, the next buffer becomes
|
|
// the current one and will be loaded, which may then
|
|
// also be deleted, etc.
|
|
if (bnr == curbuf->b_fnum) {
|
|
do_current = bnr;
|
|
} else if (do_buffer(command, DOBUF_FIRST, FORWARD, bnr,
|
|
forceit) == OK) {
|
|
deleted++;
|
|
}
|
|
|
|
// find next buffer number to delete/unload
|
|
if (addr_count == 2) {
|
|
if (++bnr > end_bnr) {
|
|
break;
|
|
}
|
|
} else { // addr_count == 1
|
|
arg = skipwhite(arg);
|
|
if (*arg == NUL) {
|
|
break;
|
|
}
|
|
if (!ascii_isdigit(*arg)) {
|
|
p = skiptowhite_esc(arg);
|
|
bnr = buflist_findpat(arg, p, command == DOBUF_WIPE,
|
|
false, false);
|
|
if (bnr < 0) { // failed
|
|
break;
|
|
}
|
|
arg = p;
|
|
} else {
|
|
bnr = getdigits_int(&arg, false, 0);
|
|
}
|
|
}
|
|
}
|
|
if (!got_int && do_current
|
|
&& do_buffer(command, DOBUF_FIRST,
|
|
FORWARD, do_current, forceit) == OK) {
|
|
deleted++;
|
|
}
|
|
|
|
if (deleted == 0) {
|
|
if (command == DOBUF_UNLOAD) {
|
|
STRCPY(IObuff, _("E515: No buffers were unloaded"));
|
|
} else if (command == DOBUF_DEL) {
|
|
STRCPY(IObuff, _("E516: No buffers were deleted"));
|
|
} else {
|
|
STRCPY(IObuff, _("E517: No buffers were wiped out"));
|
|
}
|
|
errormsg = (char *)IObuff;
|
|
} else if (deleted >= p_report) {
|
|
if (command == DOBUF_UNLOAD) {
|
|
smsg(NGETTEXT("%d buffer unloaded", "%d buffers unloaded", (unsigned long)deleted),
|
|
deleted);
|
|
} else if (command == DOBUF_DEL) {
|
|
smsg(NGETTEXT("%d buffer deleted", "%d buffers deleted", (unsigned long)deleted),
|
|
deleted);
|
|
} else {
|
|
smsg(NGETTEXT("%d buffer wiped out", "%d buffers wiped out", (unsigned long)deleted),
|
|
deleted);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return errormsg;
|
|
}
|
|
|
|
|
|
/// Make the current buffer empty.
|
|
/// Used when it is wiped out and it's the last buffer.
|
|
static int empty_curbuf(int close_others, int forceit, int action)
|
|
{
|
|
int retval;
|
|
buf_T *buf = curbuf;
|
|
|
|
if (action == DOBUF_UNLOAD) {
|
|
emsg(_("E90: Cannot unload last buffer"));
|
|
return FAIL;
|
|
}
|
|
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
|
|
if (close_others) {
|
|
bool can_close_all_others = true;
|
|
if (curwin->w_floating) {
|
|
// Closing all other windows with this buffer may leave only floating windows.
|
|
can_close_all_others = false;
|
|
for (win_T *wp = firstwin; !wp->w_floating; wp = wp->w_next) {
|
|
if (wp->w_buffer != curbuf) {
|
|
// Found another non-floating window with a different (probably unlisted) buffer.
|
|
// Closing all other windows with this buffer is fine in this case.
|
|
can_close_all_others = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// If it is fine to close all other windows with this buffer, keep the current window and
|
|
// close any other windows with this buffer, then make it empty.
|
|
// Otherwise close_windows() will refuse to close the last non-floating window, so allow it
|
|
// to close the current window instead.
|
|
close_windows(buf, can_close_all_others);
|
|
}
|
|
|
|
setpcmark();
|
|
retval = do_ecmd(0, NULL, NULL, NULL, ECMD_ONE,
|
|
forceit ? ECMD_FORCEIT : 0, curwin);
|
|
|
|
// do_ecmd() may create a new buffer, then we have to delete
|
|
// the old one. But do_ecmd() may have done that already, check
|
|
// if the buffer still exists.
|
|
if (buf != curbuf && bufref_valid(&bufref) && buf->b_nwindows == 0) {
|
|
close_buffer(NULL, buf, action, false, false);
|
|
}
|
|
|
|
if (!close_others) {
|
|
need_fileinfo = false;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
/// Implementation of the commands for the buffer list.
|
|
///
|
|
/// action == DOBUF_GOTO go to specified buffer
|
|
/// action == DOBUF_SPLIT split window and go to specified buffer
|
|
/// action == DOBUF_UNLOAD unload specified buffer(s)
|
|
/// action == DOBUF_DEL delete specified buffer(s) from buffer list
|
|
/// action == DOBUF_WIPE delete specified buffer(s) really
|
|
///
|
|
/// start == DOBUF_CURRENT go to "count" buffer from current buffer
|
|
/// start == DOBUF_FIRST go to "count" buffer from first buffer
|
|
/// start == DOBUF_LAST go to "count" buffer from last buffer
|
|
/// start == DOBUF_MOD go to "count" modified buffer from current buffer
|
|
///
|
|
/// @param dir FORWARD or BACKWARD
|
|
/// @param count buffer number or number of buffers
|
|
/// @param forceit true for :...!
|
|
///
|
|
/// @return FAIL or OK.
|
|
int do_buffer(int action, int start, int dir, int count, int forceit)
|
|
{
|
|
buf_T *buf;
|
|
buf_T *bp;
|
|
int unload = (action == DOBUF_UNLOAD || action == DOBUF_DEL
|
|
|| action == DOBUF_WIPE);
|
|
|
|
switch (start) {
|
|
case DOBUF_FIRST:
|
|
buf = firstbuf; break;
|
|
case DOBUF_LAST:
|
|
buf = lastbuf; break;
|
|
default:
|
|
buf = curbuf; break;
|
|
}
|
|
if (start == DOBUF_MOD) { // find next modified buffer
|
|
while (count-- > 0) {
|
|
do {
|
|
buf = buf->b_next;
|
|
if (buf == NULL) {
|
|
buf = firstbuf;
|
|
}
|
|
} while (buf != curbuf && !bufIsChanged(buf));
|
|
}
|
|
if (!bufIsChanged(buf)) {
|
|
emsg(_("E84: No modified buffer found"));
|
|
return FAIL;
|
|
}
|
|
} else if (start == DOBUF_FIRST && count) { // find specified buffer number
|
|
while (buf != NULL && buf->b_fnum != count) {
|
|
buf = buf->b_next;
|
|
}
|
|
} else {
|
|
bp = NULL;
|
|
while (count > 0 || (!unload && !buf->b_p_bl && bp != buf)) {
|
|
// remember the buffer where we start, we come back there when all
|
|
// buffers are unlisted.
|
|
if (bp == NULL) {
|
|
bp = buf;
|
|
}
|
|
if (dir == FORWARD) {
|
|
buf = buf->b_next;
|
|
if (buf == NULL) {
|
|
buf = firstbuf;
|
|
}
|
|
} else {
|
|
buf = buf->b_prev;
|
|
if (buf == NULL) {
|
|
buf = lastbuf;
|
|
}
|
|
}
|
|
// don't count unlisted buffers
|
|
if (unload || buf->b_p_bl) {
|
|
count--;
|
|
bp = NULL; // use this buffer as new starting point
|
|
}
|
|
if (bp == buf) {
|
|
// back where we started, didn't find anything.
|
|
emsg(_("E85: There is no listed buffer"));
|
|
return FAIL;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (buf == NULL) { // could not find it
|
|
if (start == DOBUF_FIRST) {
|
|
// don't warn when deleting
|
|
if (!unload) {
|
|
semsg(_(e_nobufnr), (int64_t)count);
|
|
}
|
|
} else if (dir == FORWARD) {
|
|
emsg(_("E87: Cannot go beyond last buffer"));
|
|
} else {
|
|
emsg(_("E88: Cannot go before first buffer"));
|
|
}
|
|
return FAIL;
|
|
}
|
|
|
|
|
|
// delete buffer "buf" from memory and/or the list
|
|
if (unload) {
|
|
int forward;
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
|
|
// When unloading or deleting a buffer that's already unloaded and
|
|
// unlisted: fail silently.
|
|
if (action != DOBUF_WIPE && buf->b_ml.ml_mfp == NULL && !buf->b_p_bl) {
|
|
return FAIL;
|
|
}
|
|
|
|
if (!forceit && bufIsChanged(buf)) {
|
|
if ((p_confirm || cmdmod.confirm) && p_write) {
|
|
dialog_changed(buf, false);
|
|
if (!bufref_valid(&bufref)) {
|
|
// Autocommand deleted buffer, oops! It's not changed now.
|
|
return FAIL;
|
|
}
|
|
// If it's still changed fail silently, the dialog already
|
|
// mentioned why it fails.
|
|
if (bufIsChanged(buf)) {
|
|
return FAIL;
|
|
}
|
|
} else {
|
|
semsg(_("E89: No write since last change for buffer %" PRId64
|
|
" (add ! to override)"),
|
|
(int64_t)buf->b_fnum);
|
|
return FAIL;
|
|
}
|
|
}
|
|
|
|
if (!forceit && buf->terminal && terminal_running(buf->terminal)) {
|
|
if (p_confirm || cmdmod.confirm) {
|
|
if (!dialog_close_terminal(buf)) {
|
|
return FAIL;
|
|
}
|
|
} else {
|
|
semsg(_("E89: %s will be killed (add ! to override)"),
|
|
(char *)buf->b_fname);
|
|
return FAIL;
|
|
}
|
|
}
|
|
|
|
// When closing the current buffer stop Visual mode.
|
|
if (buf == curbuf && VIsual_active) {
|
|
end_visual_mode();
|
|
}
|
|
|
|
// If deleting the last (listed) buffer, make it empty.
|
|
// The last (listed) buffer cannot be unloaded.
|
|
bp = NULL;
|
|
FOR_ALL_BUFFERS(bp2) {
|
|
if (bp2->b_p_bl && bp2 != buf) {
|
|
bp = bp2;
|
|
break;
|
|
}
|
|
}
|
|
if (bp == NULL && buf == curbuf) {
|
|
return empty_curbuf(true, forceit, action);
|
|
}
|
|
|
|
// If the deleted buffer is the current one, close the current window
|
|
// (unless it's the only non-floating window).
|
|
// When the autocommand window is involved win_close() may need to print an error message.
|
|
// Repeat this so long as we end up in a window with this buffer.
|
|
while (buf == curbuf
|
|
&& !(curwin->w_closing || curwin->w_buffer->b_locked > 0)
|
|
&& (lastwin == aucmd_win || !last_window(curwin))) {
|
|
if (win_close(curwin, false, false) == FAIL) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If the buffer to be deleted is not the current one, delete it here.
|
|
if (buf != curbuf) {
|
|
close_windows(buf, false);
|
|
if (buf != curbuf && bufref_valid(&bufref) && buf->b_nwindows <= 0) {
|
|
close_buffer(NULL, buf, action, false, false);
|
|
}
|
|
return OK;
|
|
}
|
|
|
|
// Deleting the current buffer: Need to find another buffer to go to.
|
|
// There should be another, otherwise it would have been handled
|
|
// above. However, autocommands may have deleted all buffers.
|
|
// First use au_new_curbuf.br_buf, if it is valid.
|
|
// Then prefer the buffer we most recently visited.
|
|
// Else try to find one that is loaded, after the current buffer,
|
|
// then before the current buffer.
|
|
// Finally use any buffer.
|
|
buf = NULL; // Selected buffer.
|
|
bp = NULL; // Used when no loaded buffer found.
|
|
if (au_new_curbuf.br_buf != NULL && bufref_valid(&au_new_curbuf)) {
|
|
buf = au_new_curbuf.br_buf;
|
|
} else if (curwin->w_jumplistlen > 0) {
|
|
int jumpidx;
|
|
|
|
jumpidx = curwin->w_jumplistidx - 1;
|
|
if (jumpidx < 0) {
|
|
jumpidx = curwin->w_jumplistlen - 1;
|
|
}
|
|
|
|
forward = jumpidx;
|
|
while (jumpidx != curwin->w_jumplistidx) {
|
|
buf = buflist_findnr(curwin->w_jumplist[jumpidx].fmark.fnum);
|
|
if (buf != NULL) {
|
|
// Skip current and unlisted bufs. Also skip a quickfix
|
|
// buffer, it might be deleted soon.
|
|
if (buf == curbuf || !buf->b_p_bl || bt_quickfix(buf)) {
|
|
buf = NULL;
|
|
} else if (buf->b_ml.ml_mfp == NULL) {
|
|
// skip unloaded buf, but may keep it for later
|
|
if (bp == NULL) {
|
|
bp = buf;
|
|
}
|
|
buf = NULL;
|
|
}
|
|
}
|
|
if (buf != NULL) { // found a valid buffer: stop searching
|
|
break;
|
|
}
|
|
// advance to older entry in jump list
|
|
if (!jumpidx && curwin->w_jumplistidx == curwin->w_jumplistlen) {
|
|
break;
|
|
}
|
|
if (--jumpidx < 0) {
|
|
jumpidx = curwin->w_jumplistlen - 1;
|
|
}
|
|
if (jumpidx == forward) { // List exhausted for sure
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (buf == NULL) { // No previous buffer, Try 2'nd approach
|
|
forward = true;
|
|
buf = curbuf->b_next;
|
|
for (;;) {
|
|
if (buf == NULL) {
|
|
if (!forward) { // tried both directions
|
|
break;
|
|
}
|
|
buf = curbuf->b_prev;
|
|
forward = false;
|
|
continue;
|
|
}
|
|
// in non-help buffer, try to skip help buffers, and vv
|
|
if (buf->b_help == curbuf->b_help && buf->b_p_bl && !bt_quickfix(buf)) {
|
|
if (buf->b_ml.ml_mfp != NULL) { // found loaded buffer
|
|
break;
|
|
}
|
|
if (bp == NULL) { // remember unloaded buf for later
|
|
bp = buf;
|
|
}
|
|
}
|
|
if (forward) {
|
|
buf = buf->b_next;
|
|
} else {
|
|
buf = buf->b_prev;
|
|
}
|
|
}
|
|
}
|
|
if (buf == NULL) { // No loaded buffer, use unloaded one
|
|
buf = bp;
|
|
}
|
|
if (buf == NULL) { // No loaded buffer, find listed one
|
|
FOR_ALL_BUFFERS(buf2) {
|
|
if (buf2->b_p_bl && buf2 != curbuf && !bt_quickfix(buf2)) {
|
|
buf = buf2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (buf == NULL) { // Still no buffer, just take one
|
|
if (curbuf->b_next != NULL) {
|
|
buf = curbuf->b_next;
|
|
} else {
|
|
buf = curbuf->b_prev;
|
|
}
|
|
if (bt_quickfix(buf)) {
|
|
buf = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (buf == NULL) {
|
|
// Autocommands must have wiped out all other buffers. Only option
|
|
// now is to make the current buffer empty.
|
|
return empty_curbuf(false, forceit, action);
|
|
}
|
|
|
|
// make "buf" the current buffer
|
|
if (action == DOBUF_SPLIT) { // split window first
|
|
// If 'switchbuf' contains "useopen": jump to first window containing
|
|
// "buf" if one exists
|
|
if ((swb_flags & SWB_USEOPEN) && buf_jump_open_win(buf)) {
|
|
return OK;
|
|
}
|
|
// If 'switchbuf' contains "usetab": jump to first window in any tab
|
|
// page containing "buf" if one exists
|
|
if ((swb_flags & SWB_USETAB) && buf_jump_open_tab(buf)) {
|
|
return OK;
|
|
}
|
|
if (win_split(0, 0) == FAIL) {
|
|
return FAIL;
|
|
}
|
|
}
|
|
|
|
// go to current buffer - nothing to do
|
|
if (buf == curbuf) {
|
|
return OK;
|
|
}
|
|
|
|
// Check if the current buffer may be abandoned.
|
|
if (action == DOBUF_GOTO && !can_abandon(curbuf, forceit)) {
|
|
if ((p_confirm || cmdmod.confirm) && p_write) {
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
dialog_changed(curbuf, false);
|
|
if (!bufref_valid(&bufref)) {
|
|
// Autocommand deleted buffer, oops!
|
|
return FAIL;
|
|
}
|
|
}
|
|
if (bufIsChanged(curbuf)) {
|
|
no_write_message();
|
|
return FAIL;
|
|
}
|
|
}
|
|
|
|
// Go to the other buffer.
|
|
set_curbuf(buf, action);
|
|
|
|
if (action == DOBUF_SPLIT) {
|
|
RESET_BINDING(curwin); // reset 'scrollbind' and 'cursorbind'
|
|
}
|
|
|
|
if (aborting()) { // autocmds may abort script processing
|
|
return FAIL;
|
|
}
|
|
|
|
return OK;
|
|
}
|
|
|
|
|
|
/// Set current buffer to "buf". Executes autocommands and closes current
|
|
/// buffer.
|
|
///
|
|
/// @param action tells how to close the current buffer:
|
|
/// DOBUF_GOTO free or hide it
|
|
/// DOBUF_SPLIT nothing
|
|
/// DOBUF_UNLOAD unload it
|
|
/// DOBUF_DEL delete it
|
|
/// DOBUF_WIPE wipe it out
|
|
void set_curbuf(buf_T *buf, int action)
|
|
{
|
|
buf_T *prevbuf;
|
|
int unload = (action == DOBUF_UNLOAD || action == DOBUF_DEL
|
|
|| action == DOBUF_WIPE);
|
|
long old_tw = curbuf->b_p_tw;
|
|
|
|
setpcmark();
|
|
if (!cmdmod.keepalt) {
|
|
curwin->w_alt_fnum = curbuf->b_fnum; // remember alternate file
|
|
}
|
|
buflist_altfpos(curwin); // remember curpos
|
|
|
|
// Don't restart Select mode after switching to another buffer.
|
|
VIsual_reselect = false;
|
|
|
|
// close_windows() or apply_autocmds() may change curbuf and wipe out "buf"
|
|
prevbuf = curbuf;
|
|
bufref_T newbufref;
|
|
bufref_T prevbufref;
|
|
set_bufref(&prevbufref, prevbuf);
|
|
set_bufref(&newbufref, buf);
|
|
|
|
// Autocommands may delete the current buffer and/or the buffer we want to go
|
|
// to. In those cases don't close the buffer.
|
|
if (!apply_autocmds(EVENT_BUFLEAVE, NULL, NULL, false, curbuf)
|
|
|| (bufref_valid(&prevbufref) && bufref_valid(&newbufref)
|
|
&& !aborting())) {
|
|
if (prevbuf == curwin->w_buffer) {
|
|
reset_synblock(curwin);
|
|
}
|
|
if (unload) {
|
|
close_windows(prevbuf, false);
|
|
}
|
|
if (bufref_valid(&prevbufref) && !aborting()) {
|
|
win_T *previouswin = curwin;
|
|
|
|
// Do not sync when in Insert mode and the buffer is open in
|
|
// another window, might be a timer doing something in another
|
|
// window.
|
|
if (prevbuf == curbuf && ((State & INSERT) == 0 || curbuf->b_nwindows <= 1)) {
|
|
u_sync(false);
|
|
}
|
|
close_buffer(prevbuf == curwin->w_buffer ? curwin : NULL,
|
|
prevbuf,
|
|
unload
|
|
? action
|
|
: (action == DOBUF_GOTO && !buf_hide(prevbuf)
|
|
&& !bufIsChanged(prevbuf)) ? DOBUF_UNLOAD : 0,
|
|
false, false);
|
|
if (curwin != previouswin && win_valid(previouswin)) {
|
|
// autocommands changed curwin, Grr!
|
|
curwin = previouswin;
|
|
}
|
|
}
|
|
}
|
|
// An autocommand may have deleted "buf", already entered it (e.g., when
|
|
// it did ":bunload") or aborted the script processing!
|
|
// If curwin->w_buffer is null, enter_buffer() will make it valid again
|
|
bool valid = buf_valid(buf);
|
|
if ((valid && buf != curbuf && !aborting()) || curwin->w_buffer == NULL) {
|
|
// If the buffer is not valid but curwin->w_buffer is NULL we must
|
|
// enter some buffer. Using the last one is hopefully OK.
|
|
if (!valid) {
|
|
enter_buffer(lastbuf);
|
|
} else {
|
|
enter_buffer(buf);
|
|
}
|
|
if (old_tw != curbuf->b_p_tw) {
|
|
check_colorcolumn(curwin);
|
|
}
|
|
}
|
|
|
|
if (bufref_valid(&prevbufref) && prevbuf->terminal != NULL) {
|
|
terminal_check_size(prevbuf->terminal);
|
|
}
|
|
}
|
|
|
|
/// Enter a new current buffer.
|
|
/// Old curbuf must have been abandoned already! This also means "curbuf" may
|
|
/// be pointing to freed memory.
|
|
void enter_buffer(buf_T *buf)
|
|
{
|
|
// Get the buffer in the current window.
|
|
curwin->w_buffer = buf;
|
|
curbuf = buf;
|
|
curbuf->b_nwindows++;
|
|
|
|
// Copy buffer and window local option values. Not for a help buffer.
|
|
buf_copy_options(buf, BCO_ENTER | BCO_NOHELP);
|
|
if (!buf->b_help) {
|
|
get_winopts(buf);
|
|
} else {
|
|
// Remove all folds in the window.
|
|
clearFolding(curwin);
|
|
}
|
|
foldUpdateAll(curwin); // update folds (later).
|
|
|
|
if (curwin->w_p_diff) {
|
|
diff_buf_add(curbuf);
|
|
}
|
|
|
|
curwin->w_s = &(curbuf->b_s);
|
|
|
|
// Cursor on first line by default.
|
|
curwin->w_cursor.lnum = 1;
|
|
curwin->w_cursor.col = 0;
|
|
curwin->w_cursor.coladd = 0;
|
|
curwin->w_set_curswant = true;
|
|
curwin->w_topline_was_set = false;
|
|
|
|
// mark cursor position as being invalid
|
|
curwin->w_valid = 0;
|
|
|
|
// Make sure the buffer is loaded.
|
|
if (curbuf->b_ml.ml_mfp == NULL) { // need to load the file
|
|
// If there is no filetype, allow for detecting one. Esp. useful for
|
|
// ":ball" used in an autocommand. If there already is a filetype we
|
|
// might prefer to keep it.
|
|
if (*curbuf->b_p_ft == NUL) {
|
|
did_filetype = false;
|
|
}
|
|
|
|
open_buffer(false, NULL, 0);
|
|
} else {
|
|
if (!msg_silent && !shortmess(SHM_FILEINFO)) {
|
|
need_fileinfo = true; // display file info after redraw
|
|
}
|
|
// check if file changed
|
|
(void)buf_check_timestamp(curbuf);
|
|
|
|
curwin->w_topline = 1;
|
|
curwin->w_topfill = 0;
|
|
apply_autocmds(EVENT_BUFENTER, NULL, NULL, false, curbuf);
|
|
apply_autocmds(EVENT_BUFWINENTER, NULL, NULL, false, curbuf);
|
|
}
|
|
|
|
// If autocommands did not change the cursor position, restore cursor lnum
|
|
// and possibly cursor col.
|
|
if (curwin->w_cursor.lnum == 1 && inindent(0)) {
|
|
buflist_getfpos();
|
|
}
|
|
|
|
check_arg_idx(curwin); // check for valid arg_idx
|
|
maketitle();
|
|
// when autocmds didn't change it
|
|
if (curwin->w_topline == 1 && !curwin->w_topline_was_set) {
|
|
scroll_cursor_halfway(false); // redisplay at correct position
|
|
}
|
|
|
|
|
|
// Change directories when the 'acd' option is set.
|
|
do_autochdir();
|
|
|
|
if (curbuf->b_kmap_state & KEYMAP_INIT) {
|
|
(void)keymap_init();
|
|
}
|
|
// May need to set the spell language. Can only do this after the buffer
|
|
// has been properly setup.
|
|
if (!curbuf->b_help && curwin->w_p_spell && *curwin->w_s->b_p_spl != NUL) {
|
|
(void)did_set_spelllang(curwin);
|
|
}
|
|
curbuf->b_last_used = time(NULL);
|
|
|
|
redraw_later(curwin, NOT_VALID);
|
|
}
|
|
|
|
/// Change to the directory of the current buffer.
|
|
/// Don't do this while still starting up.
|
|
void do_autochdir(void)
|
|
{
|
|
if (p_acd) {
|
|
if (starting == 0
|
|
&& curbuf->b_ffname != NULL
|
|
&& vim_chdirfile(curbuf->b_ffname, kCdCauseAuto) == OK) {
|
|
last_chdir_reason = "autochdir";
|
|
shorten_fnames(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void no_write_message(void)
|
|
{
|
|
if (curbuf->terminal
|
|
&& channel_job_running((uint64_t)curbuf->b_p_channel)) {
|
|
emsg(_("E948: Job still running (add ! to end the job)"));
|
|
} else {
|
|
emsg(_("E37: No write since last change (add ! to override)"));
|
|
}
|
|
}
|
|
|
|
void no_write_message_nobang(const buf_T *const buf)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
if (buf->terminal
|
|
&& channel_job_running((uint64_t)buf->b_p_channel)) {
|
|
emsg(_("E948: Job still running"));
|
|
} else {
|
|
emsg(_("E37: No write since last change"));
|
|
}
|
|
}
|
|
|
|
//
|
|
// functions for dealing with the buffer list
|
|
//
|
|
|
|
static int top_file_num = 1; ///< highest file number
|
|
|
|
/// Initialize b:changedtick and changedtick_val attribute
|
|
///
|
|
/// @param[out] buf Buffer to initialize for.
|
|
static inline void buf_init_changedtick(buf_T *const buf)
|
|
FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
STATIC_ASSERT(sizeof("changedtick") <= sizeof(buf->changedtick_di.di_key),
|
|
"buf->changedtick_di cannot hold large enough keys");
|
|
buf->changedtick_di = (ChangedtickDictItem) {
|
|
.di_flags = DI_FLAGS_RO|DI_FLAGS_FIX, // Must not include DI_FLAGS_ALLOC.
|
|
.di_tv = (typval_T) {
|
|
.v_type = VAR_NUMBER,
|
|
.v_lock = VAR_FIXED,
|
|
.vval.v_number = buf_get_changedtick(buf),
|
|
},
|
|
.di_key = "changedtick",
|
|
};
|
|
tv_dict_add(buf->b_vars, (dictitem_T *)&buf->changedtick_di);
|
|
}
|
|
|
|
/// Add a file name to the buffer list.
|
|
/// If the same file name already exists return a pointer to that buffer.
|
|
/// If it does not exist, or if fname == NULL, a new entry is created.
|
|
/// If (flags & BLN_CURBUF) is true, may use current buffer.
|
|
/// If (flags & BLN_LISTED) is true, add new buffer to buffer list.
|
|
/// If (flags & BLN_DUMMY) is true, don't count it as a real buffer.
|
|
/// If (flags & BLN_NEW) is true, don't use an existing buffer.
|
|
/// If (flags & BLN_NOOPT) is true, don't copy options from the current buffer
|
|
/// if the buffer already exists.
|
|
/// This is the ONLY way to create a new buffer.
|
|
///
|
|
/// @param ffname_arg full path of fname or relative
|
|
/// @param sfname_arg short fname or NULL
|
|
/// @param lnum preferred cursor line
|
|
/// @param flags BLN_ defines
|
|
/// @param bufnr
|
|
///
|
|
/// @return pointer to the buffer
|
|
buf_T *buflist_new(char_u *ffname_arg, char_u *sfname_arg, linenr_T lnum, int flags)
|
|
{
|
|
char_u *ffname = ffname_arg;
|
|
char_u *sfname = sfname_arg;
|
|
buf_T *buf;
|
|
|
|
fname_expand(curbuf, &ffname, &sfname); // will allocate ffname
|
|
|
|
// If the file name already exists in the list, update the entry.
|
|
|
|
// We can use inode numbers when the file exists. Works better
|
|
// for hard links.
|
|
FileID file_id;
|
|
bool file_id_valid = (sfname != NULL
|
|
&& os_fileid((char *)sfname, &file_id));
|
|
if (ffname != NULL && !(flags & (BLN_DUMMY | BLN_NEW))
|
|
&& (buf = buflist_findname_file_id(ffname, &file_id,
|
|
file_id_valid)) != NULL) {
|
|
xfree(ffname);
|
|
if (lnum != 0) {
|
|
buflist_setfpos(buf, (flags & BLN_NOCURWIN) ? NULL : curwin,
|
|
lnum, (colnr_T)0, false);
|
|
}
|
|
if ((flags & BLN_NOOPT) == 0) {
|
|
// Copy the options now, if 'cpo' doesn't have 's' and not done already.
|
|
buf_copy_options(buf, 0);
|
|
}
|
|
if ((flags & BLN_LISTED) && !buf->b_p_bl) {
|
|
buf->b_p_bl = true;
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
if (!(flags & BLN_DUMMY)) {
|
|
if (apply_autocmds(EVENT_BUFADD, NULL, NULL, false, buf)
|
|
&& !bufref_valid(&bufref)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
// If the current buffer has no name and no contents, use the current
|
|
// buffer. Otherwise: Need to allocate a new buffer structure.
|
|
//
|
|
// This is the ONLY place where a new buffer structure is allocated!
|
|
// (A spell file buffer is allocated in spell.c, but that's not a normal
|
|
// buffer.)
|
|
buf = NULL;
|
|
if ((flags & BLN_CURBUF) && curbuf_reusable()) {
|
|
assert(curbuf != NULL);
|
|
buf = curbuf;
|
|
// It's like this buffer is deleted. Watch out for autocommands that
|
|
// change curbuf! If that happens, allocate a new buffer anyway.
|
|
if (curbuf->b_p_bl) {
|
|
apply_autocmds(EVENT_BUFDELETE, NULL, NULL, false, curbuf);
|
|
}
|
|
if (buf == curbuf) {
|
|
apply_autocmds(EVENT_BUFWIPEOUT, NULL, NULL, false, curbuf);
|
|
}
|
|
if (aborting()) { // autocmds may abort script processing
|
|
xfree(ffname);
|
|
return NULL;
|
|
}
|
|
if (buf == curbuf) {
|
|
// Make sure 'bufhidden' and 'buftype' are empty
|
|
clear_string_option(&buf->b_p_bh);
|
|
clear_string_option(&buf->b_p_bt);
|
|
}
|
|
}
|
|
if (buf != curbuf || curbuf == NULL) {
|
|
buf = xcalloc(1, sizeof(buf_T));
|
|
// init b: variables
|
|
buf->b_vars = tv_dict_alloc();
|
|
buf->b_signcols.valid = false;
|
|
init_var_dict(buf->b_vars, &buf->b_bufvar, VAR_SCOPE);
|
|
buf_init_changedtick(buf);
|
|
}
|
|
|
|
if (ffname != NULL) {
|
|
buf->b_ffname = ffname;
|
|
buf->b_sfname = vim_strsave(sfname);
|
|
}
|
|
|
|
clear_wininfo(buf);
|
|
buf->b_wininfo = xcalloc(1, sizeof(wininfo_T));
|
|
|
|
if (buf == curbuf) {
|
|
// free all things allocated for this buffer
|
|
buf_freeall(buf, 0);
|
|
if (buf != curbuf) { // autocommands deleted the buffer!
|
|
return NULL;
|
|
}
|
|
if (aborting()) { // autocmds may abort script processing
|
|
return NULL;
|
|
}
|
|
free_buffer_stuff(buf, kBffInitChangedtick); // delete local vars et al.
|
|
|
|
// Init the options.
|
|
buf->b_p_initialized = false;
|
|
buf_copy_options(buf, BCO_ENTER);
|
|
|
|
// need to reload lmaps and set b:keymap_name
|
|
curbuf->b_kmap_state |= KEYMAP_INIT;
|
|
} else {
|
|
// put new buffer at the end of the buffer list
|
|
buf->b_next = NULL;
|
|
if (firstbuf == NULL) { // buffer list is empty
|
|
buf->b_prev = NULL;
|
|
firstbuf = buf;
|
|
} else { // append new buffer at end of list
|
|
lastbuf->b_next = buf;
|
|
buf->b_prev = lastbuf;
|
|
}
|
|
lastbuf = buf;
|
|
|
|
buf->b_fnum = top_file_num++;
|
|
pmap_put(handle_T)(&buffer_handles, buf->b_fnum, buf);
|
|
if (top_file_num < 0) { // wrap around (may cause duplicates)
|
|
emsg(_("W14: Warning: List of file names overflow"));
|
|
if (emsg_silent == 0) {
|
|
ui_flush();
|
|
os_delay(3001L, true); // make sure it is noticed
|
|
}
|
|
top_file_num = 1;
|
|
}
|
|
|
|
// Always copy the options from the current buffer.
|
|
buf_copy_options(buf, BCO_ALWAYS);
|
|
}
|
|
|
|
buf->b_wininfo->wi_fpos.lnum = lnum;
|
|
buf->b_wininfo->wi_win = curwin;
|
|
|
|
hash_init(&buf->b_s.b_keywtab);
|
|
hash_init(&buf->b_s.b_keywtab_ic);
|
|
|
|
buf->b_fname = buf->b_sfname;
|
|
if (!file_id_valid) {
|
|
buf->file_id_valid = false;
|
|
} else {
|
|
buf->file_id_valid = true;
|
|
buf->file_id = file_id;
|
|
}
|
|
buf->b_u_synced = true;
|
|
buf->b_flags = BF_CHECK_RO | BF_NEVERLOADED;
|
|
if (flags & BLN_DUMMY) {
|
|
buf->b_flags |= BF_DUMMY;
|
|
}
|
|
buf_clear_file(buf);
|
|
clrallmarks(buf); // clear marks
|
|
fmarks_check_names(buf); // check file marks for this file
|
|
buf->b_p_bl = (flags & BLN_LISTED) ? true : false; // init 'buflisted'
|
|
kv_destroy(buf->update_channels);
|
|
kv_init(buf->update_channels);
|
|
kv_destroy(buf->update_callbacks);
|
|
kv_init(buf->update_callbacks);
|
|
if (!(flags & BLN_DUMMY)) {
|
|
// Tricky: these autocommands may change the buffer list. They could also
|
|
// split the window with re-using the one empty buffer. This may result in
|
|
// unexpectedly losing the empty buffer.
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
if (apply_autocmds(EVENT_BUFNEW, NULL, NULL, false, buf)
|
|
&& !bufref_valid(&bufref)) {
|
|
return NULL;
|
|
}
|
|
if ((flags & BLN_LISTED)
|
|
&& apply_autocmds(EVENT_BUFADD, NULL, NULL, false, buf)
|
|
&& !bufref_valid(&bufref)) {
|
|
return NULL;
|
|
}
|
|
if (aborting()) {
|
|
// Autocmds may abort script processing.
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
buf->b_prompt_callback.type = kCallbackNone;
|
|
buf->b_prompt_interrupt.type = kCallbackNone;
|
|
buf->b_prompt_text = NULL;
|
|
|
|
return buf;
|
|
}
|
|
|
|
/// Return true if the current buffer is empty, unnamed, unmodified and used in
|
|
/// only one window. That means it can be reused.
|
|
bool curbuf_reusable(void)
|
|
{
|
|
return (curbuf != NULL
|
|
&& curbuf->b_ffname == NULL
|
|
&& curbuf->b_nwindows <= 1
|
|
&& (curbuf->b_ml.ml_mfp == NULL || buf_is_empty(curbuf))
|
|
&& !bt_quickfix(curbuf)
|
|
&& !curbufIsChanged());
|
|
}
|
|
|
|
/// Free the memory for the options of a buffer.
|
|
/// If "free_p_ff" is true also free 'fileformat', 'buftype' and
|
|
/// 'fileencoding'.
|
|
void free_buf_options(buf_T *buf, int free_p_ff)
|
|
{
|
|
if (free_p_ff) {
|
|
clear_string_option(&buf->b_p_fenc);
|
|
clear_string_option(&buf->b_p_ff);
|
|
clear_string_option(&buf->b_p_bh);
|
|
clear_string_option(&buf->b_p_bt);
|
|
}
|
|
clear_string_option(&buf->b_p_def);
|
|
clear_string_option(&buf->b_p_inc);
|
|
clear_string_option(&buf->b_p_inex);
|
|
clear_string_option(&buf->b_p_inde);
|
|
clear_string_option(&buf->b_p_indk);
|
|
clear_string_option(&buf->b_p_fp);
|
|
clear_string_option(&buf->b_p_fex);
|
|
clear_string_option(&buf->b_p_kp);
|
|
clear_string_option(&buf->b_p_mps);
|
|
clear_string_option(&buf->b_p_fo);
|
|
clear_string_option(&buf->b_p_flp);
|
|
clear_string_option(&buf->b_p_isk);
|
|
clear_string_option(&buf->b_p_vsts);
|
|
XFREE_CLEAR(buf->b_p_vsts_nopaste);
|
|
XFREE_CLEAR(buf->b_p_vsts_array);
|
|
clear_string_option(&buf->b_p_vts);
|
|
XFREE_CLEAR(buf->b_p_vts_array);
|
|
clear_string_option(&buf->b_p_keymap);
|
|
keymap_ga_clear(&buf->b_kmap_ga);
|
|
ga_clear(&buf->b_kmap_ga);
|
|
clear_string_option(&buf->b_p_com);
|
|
clear_string_option(&buf->b_p_cms);
|
|
clear_string_option(&buf->b_p_nf);
|
|
clear_string_option(&buf->b_p_syn);
|
|
clear_string_option(&buf->b_s.b_syn_isk);
|
|
clear_string_option(&buf->b_s.b_p_spc);
|
|
clear_string_option(&buf->b_s.b_p_spf);
|
|
vim_regfree(buf->b_s.b_cap_prog);
|
|
buf->b_s.b_cap_prog = NULL;
|
|
clear_string_option(&buf->b_s.b_p_spl);
|
|
clear_string_option(&buf->b_s.b_p_spo);
|
|
clear_string_option(&buf->b_p_sua);
|
|
clear_string_option(&buf->b_p_ft);
|
|
clear_string_option(&buf->b_p_cink);
|
|
clear_string_option(&buf->b_p_cino);
|
|
clear_string_option(&buf->b_p_cinw);
|
|
clear_string_option(&buf->b_p_cinsd);
|
|
clear_string_option(&buf->b_p_cpt);
|
|
clear_string_option(&buf->b_p_cfu);
|
|
clear_string_option(&buf->b_p_ofu);
|
|
clear_string_option(&buf->b_p_tsrfu);
|
|
clear_string_option(&buf->b_p_gp);
|
|
clear_string_option(&buf->b_p_mp);
|
|
clear_string_option(&buf->b_p_efm);
|
|
clear_string_option(&buf->b_p_ep);
|
|
clear_string_option(&buf->b_p_path);
|
|
clear_string_option(&buf->b_p_tags);
|
|
clear_string_option(&buf->b_p_tc);
|
|
clear_string_option(&buf->b_p_tfu);
|
|
clear_string_option(&buf->b_p_dict);
|
|
clear_string_option(&buf->b_p_tsr);
|
|
clear_string_option(&buf->b_p_qe);
|
|
buf->b_p_ar = -1;
|
|
buf->b_p_ul = NO_LOCAL_UNDOLEVEL;
|
|
clear_string_option(&buf->b_p_lw);
|
|
clear_string_option(&buf->b_p_bkc);
|
|
clear_string_option(&buf->b_p_menc);
|
|
}
|
|
|
|
|
|
/// Get alternate file "n".
|
|
/// Set linenr to "lnum" or altfpos.lnum if "lnum" == 0.
|
|
/// Also set cursor column to altfpos.col if 'startofline' is not set.
|
|
/// if (options & GETF_SETMARK) call setpcmark()
|
|
/// if (options & GETF_ALT) we are jumping to an alternate file.
|
|
/// if (options & GETF_SWITCH) respect 'switchbuf' settings when jumping
|
|
///
|
|
/// Return FAIL for failure, OK for success.
|
|
int buflist_getfile(int n, linenr_T lnum, int options, int forceit)
|
|
{
|
|
buf_T *buf;
|
|
win_T *wp = NULL;
|
|
pos_T *fpos;
|
|
colnr_T col;
|
|
|
|
buf = buflist_findnr(n);
|
|
if (buf == NULL) {
|
|
if ((options & GETF_ALT) && n == 0) {
|
|
emsg(_(e_noalt));
|
|
} else {
|
|
semsg(_("E92: Buffer %" PRId64 " not found"), (int64_t)n);
|
|
}
|
|
return FAIL;
|
|
}
|
|
|
|
// if alternate file is the current buffer, nothing to do
|
|
if (buf == curbuf) {
|
|
return OK;
|
|
}
|
|
|
|
if (text_locked()) {
|
|
text_locked_msg();
|
|
return FAIL;
|
|
}
|
|
if (curbuf_locked()) {
|
|
return FAIL;
|
|
}
|
|
|
|
// altfpos may be changed by getfile(), get it now
|
|
if (lnum == 0) {
|
|
fpos = buflist_findfpos(buf);
|
|
lnum = fpos->lnum;
|
|
col = fpos->col;
|
|
} else {
|
|
col = 0;
|
|
}
|
|
|
|
if (options & GETF_SWITCH) {
|
|
// If 'switchbuf' contains "useopen": jump to first window containing
|
|
// "buf" if one exists
|
|
if (swb_flags & SWB_USEOPEN) {
|
|
wp = buf_jump_open_win(buf);
|
|
}
|
|
|
|
// If 'switchbuf' contains "usetab": jump to first window in any tab
|
|
// page containing "buf" if one exists
|
|
if (wp == NULL && (swb_flags & SWB_USETAB)) {
|
|
wp = buf_jump_open_tab(buf);
|
|
}
|
|
|
|
// If 'switchbuf' contains "split", "vsplit" or "newtab" and the
|
|
// current buffer isn't empty: open new tab or window
|
|
if (wp == NULL && (swb_flags & (SWB_VSPLIT | SWB_SPLIT | SWB_NEWTAB))
|
|
&& !buf_is_empty(curbuf)) {
|
|
if (swb_flags & SWB_NEWTAB) {
|
|
tabpage_new();
|
|
} else if (win_split(0, (swb_flags & SWB_VSPLIT) ? WSP_VERT : 0)
|
|
== FAIL) {
|
|
return FAIL;
|
|
}
|
|
RESET_BINDING(curwin);
|
|
}
|
|
}
|
|
|
|
RedrawingDisabled++;
|
|
if (GETFILE_SUCCESS(getfile(buf->b_fnum, NULL, NULL,
|
|
(options & GETF_SETMARK), lnum, forceit))) {
|
|
RedrawingDisabled--;
|
|
|
|
// cursor is at to BOL and w_cursor.lnum is checked due to getfile()
|
|
if (!p_sol && col != 0) {
|
|
curwin->w_cursor.col = col;
|
|
check_cursor_col();
|
|
curwin->w_cursor.coladd = 0;
|
|
curwin->w_set_curswant = true;
|
|
}
|
|
return OK;
|
|
}
|
|
RedrawingDisabled--;
|
|
return FAIL;
|
|
}
|
|
|
|
/// Go to the last known line number for the current buffer.
|
|
void buflist_getfpos(void)
|
|
{
|
|
pos_T *fpos;
|
|
|
|
fpos = buflist_findfpos(curbuf);
|
|
|
|
curwin->w_cursor.lnum = fpos->lnum;
|
|
check_cursor_lnum();
|
|
|
|
if (p_sol) {
|
|
curwin->w_cursor.col = 0;
|
|
} else {
|
|
curwin->w_cursor.col = fpos->col;
|
|
check_cursor_col();
|
|
curwin->w_cursor.coladd = 0;
|
|
curwin->w_set_curswant = true;
|
|
}
|
|
}
|
|
|
|
/// Find file in buffer list by name (it has to be for the current window).
|
|
///
|
|
/// @return buffer or NULL if not found
|
|
buf_T *buflist_findname_exp(char_u *fname)
|
|
{
|
|
char_u *ffname;
|
|
buf_T *buf = NULL;
|
|
|
|
// First make the name into a full path name
|
|
ffname = (char_u *)FullName_save((char *)fname,
|
|
#ifdef UNIX
|
|
// force expansion, get rid of symbolic links
|
|
true
|
|
#else
|
|
false
|
|
#endif
|
|
); // NOLINT(whitespace/parens)
|
|
if (ffname != NULL) {
|
|
buf = buflist_findname(ffname);
|
|
xfree(ffname);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
/// Find file in buffer list by name (it has to be for the current window).
|
|
/// "ffname" must have a full path.
|
|
/// Skips dummy buffers.
|
|
///
|
|
/// @return buffer or NULL if not found
|
|
buf_T *buflist_findname(char_u *ffname)
|
|
{
|
|
FileID file_id;
|
|
bool file_id_valid = os_fileid((char *)ffname, &file_id);
|
|
return buflist_findname_file_id(ffname, &file_id, file_id_valid);
|
|
}
|
|
|
|
/// Same as buflist_findname(), but pass the FileID structure to avoid
|
|
/// getting it twice for the same file.
|
|
///
|
|
/// @return buffer or NULL if not found
|
|
static buf_T *buflist_findname_file_id(char_u *ffname, FileID *file_id, bool file_id_valid)
|
|
{
|
|
// Start at the last buffer, expect to find a match sooner.
|
|
FOR_ALL_BUFFERS_BACKWARDS(buf) {
|
|
if ((buf->b_flags & BF_DUMMY) == 0
|
|
&& !otherfile_buf(buf, ffname, file_id, file_id_valid)) {
|
|
return buf;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/// Find file in buffer list by a regexp pattern.
|
|
///
|
|
/// @param pattern_end pointer to first char after pattern
|
|
/// @param unlisted find unlisted buffers
|
|
/// @param diffmode find diff-mode buffers only
|
|
/// @param curtab_only find buffers in current tab only
|
|
///
|
|
/// @return fnum of the found buffer or < 0 for error.
|
|
int buflist_findpat(const char_u *pattern, const char_u *pattern_end, bool unlisted, bool diffmode,
|
|
bool curtab_only)
|
|
FUNC_ATTR_NONNULL_ARG(1)
|
|
{
|
|
int match = -1;
|
|
int find_listed;
|
|
char_u *pat;
|
|
char_u *patend;
|
|
int attempt;
|
|
char_u *p;
|
|
int toggledollar;
|
|
|
|
if (pattern_end == pattern + 1 && (*pattern == '%' || *pattern == '#')) {
|
|
if (*pattern == '%') {
|
|
match = curbuf->b_fnum;
|
|
} else {
|
|
match = curwin->w_alt_fnum;
|
|
}
|
|
buf_T *found_buf = buflist_findnr(match);
|
|
if (diffmode && !(found_buf && diff_mode_buf(found_buf))) {
|
|
match = -1;
|
|
}
|
|
} else {
|
|
// Try four ways of matching a listed buffer:
|
|
// attempt == 0: without '^' or '$' (at any position)
|
|
// attempt == 1: with '^' at start (only at position 0)
|
|
// attempt == 2: with '$' at end (only match at end)
|
|
// attempt == 3: with '^' at start and '$' at end (only full match)
|
|
// Repeat this for finding an unlisted buffer if there was no matching
|
|
// listed buffer.
|
|
|
|
pat = file_pat_to_reg_pat(pattern, pattern_end, NULL, false);
|
|
if (pat == NULL) {
|
|
return -1;
|
|
}
|
|
patend = pat + STRLEN(pat) - 1;
|
|
toggledollar = (patend > pat && *patend == '$');
|
|
|
|
// First try finding a listed buffer. If not found and "unlisted"
|
|
// is true, try finding an unlisted buffer.
|
|
find_listed = true;
|
|
for (;;) {
|
|
for (attempt = 0; attempt <= 3; attempt++) {
|
|
// may add '^' and '$'
|
|
if (toggledollar) {
|
|
*patend = (attempt < 2) ? NUL : '$'; // add/remove '$'
|
|
}
|
|
p = pat;
|
|
if (*p == '^' && !(attempt & 1)) { // add/remove '^'
|
|
p++;
|
|
}
|
|
|
|
regmatch_T regmatch;
|
|
regmatch.regprog = vim_regcomp(p, p_magic ? RE_MAGIC : 0);
|
|
if (regmatch.regprog == NULL) {
|
|
xfree(pat);
|
|
return -1;
|
|
}
|
|
|
|
FOR_ALL_BUFFERS_BACKWARDS(buf) {
|
|
if (buf->b_p_bl == find_listed
|
|
&& (!diffmode || diff_mode_buf(buf))
|
|
&& buflist_match(®match, buf, false) != NULL) {
|
|
if (curtab_only) {
|
|
// Ignore the match if the buffer is not open in
|
|
// the current tab.
|
|
bool found_window = false;
|
|
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
|
|
if (wp->w_buffer == buf) {
|
|
found_window = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found_window) {
|
|
continue;
|
|
}
|
|
}
|
|
if (match >= 0) { // already found a match
|
|
match = -2;
|
|
break;
|
|
}
|
|
match = buf->b_fnum; // remember first match
|
|
}
|
|
}
|
|
|
|
vim_regfree(regmatch.regprog);
|
|
if (match >= 0) { // found one match
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Only search for unlisted buffers if there was no match with
|
|
// a listed buffer.
|
|
if (!unlisted || !find_listed || match != -1) {
|
|
break;
|
|
}
|
|
find_listed = false;
|
|
}
|
|
|
|
xfree(pat);
|
|
}
|
|
|
|
if (match == -2) {
|
|
semsg(_("E93: More than one match for %s"), pattern);
|
|
} else if (match < 0) {
|
|
semsg(_("E94: No matching buffer for %s"), pattern);
|
|
}
|
|
return match;
|
|
}
|
|
|
|
typedef struct {
|
|
buf_T *buf;
|
|
char_u *match;
|
|
} bufmatch_T;
|
|
|
|
/// Compare functions for qsort() below, that compares b_last_used.
|
|
static int buf_time_compare(const void *s1, const void *s2)
|
|
{
|
|
buf_T *buf1 = *(buf_T **)s1;
|
|
buf_T *buf2 = *(buf_T **)s2;
|
|
|
|
if (buf1->b_last_used == buf2->b_last_used) {
|
|
return 0;
|
|
}
|
|
return buf1->b_last_used > buf2->b_last_used ? -1 : 1;
|
|
}
|
|
|
|
/// Find all buffer names that match.
|
|
/// For command line expansion of ":buf" and ":sbuf".
|
|
///
|
|
/// @return OK if matches found, FAIL otherwise.
|
|
int ExpandBufnames(char_u *pat, int *num_file, char_u ***file, int options)
|
|
{
|
|
int count = 0;
|
|
int round;
|
|
char_u *p;
|
|
int attempt;
|
|
char_u *patc;
|
|
bufmatch_T *matches = NULL;
|
|
|
|
*num_file = 0; // return values in case of FAIL
|
|
*file = NULL;
|
|
|
|
if ((options & BUF_DIFF_FILTER) && !curwin->w_p_diff) {
|
|
return FAIL;
|
|
}
|
|
|
|
// Make a copy of "pat" and change "^" to "\(^\|[\/]\)".
|
|
if (*pat == '^') {
|
|
patc = xmalloc(STRLEN(pat) + 11);
|
|
STRCPY(patc, "\\(^\\|[\\/]\\)");
|
|
STRCPY(patc + 11, pat + 1);
|
|
} else {
|
|
patc = pat;
|
|
}
|
|
|
|
// attempt == 0: try match with '\<', match at start of word
|
|
// attempt == 1: try match without '\<', match anywhere
|
|
for (attempt = 0; attempt <= 1; attempt++) {
|
|
if (attempt > 0 && patc == pat) {
|
|
break; // there was no anchor, no need to try again
|
|
}
|
|
|
|
regmatch_T regmatch;
|
|
regmatch.regprog = vim_regcomp(patc + attempt * 11, RE_MAGIC);
|
|
if (regmatch.regprog == NULL) {
|
|
if (patc != pat) {
|
|
xfree(patc);
|
|
}
|
|
return FAIL;
|
|
}
|
|
|
|
// round == 1: Count the matches.
|
|
// round == 2: Build the array to keep the matches.
|
|
for (round = 1; round <= 2; round++) {
|
|
count = 0;
|
|
FOR_ALL_BUFFERS(buf) {
|
|
if (!buf->b_p_bl) { // skip unlisted buffers
|
|
continue;
|
|
}
|
|
if (options & BUF_DIFF_FILTER) {
|
|
// Skip buffers not suitable for
|
|
// :diffget or :diffput completion.
|
|
if (buf == curbuf || !diff_mode_buf(buf)) {
|
|
continue;
|
|
}
|
|
}
|
|
p = buflist_match(®match, buf, p_wic);
|
|
if (p != NULL) {
|
|
if (round == 1) {
|
|
count++;
|
|
} else {
|
|
if (options & WILD_HOME_REPLACE) {
|
|
p = home_replace_save(buf, p);
|
|
} else {
|
|
p = vim_strsave(p);
|
|
}
|
|
if (matches != NULL) {
|
|
matches[count].buf = buf;
|
|
matches[count].match = p;
|
|
count++;
|
|
} else {
|
|
(*file)[count++] = p;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (count == 0) { // no match found, break here
|
|
break;
|
|
}
|
|
if (round == 1) {
|
|
*file = xmalloc((size_t)count * sizeof(**file));
|
|
|
|
if (options & WILD_BUFLASTUSED) {
|
|
matches = xmalloc((size_t)count * sizeof(*matches));
|
|
}
|
|
}
|
|
}
|
|
vim_regfree(regmatch.regprog);
|
|
if (count) { // match(es) found, break here
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (patc != pat) {
|
|
xfree(patc);
|
|
}
|
|
|
|
if (matches != NULL) {
|
|
if (count > 1) {
|
|
qsort(matches, (size_t)count, sizeof(bufmatch_T), buf_time_compare);
|
|
}
|
|
|
|
// if the current buffer is first in the list, place it at the end
|
|
if (matches[0].buf == curbuf) {
|
|
for (int i = 1; i < count; i++) {
|
|
(*file)[i-1] = matches[i].match;
|
|
}
|
|
(*file)[count-1] = matches[0].match;
|
|
} else {
|
|
for (int i = 0; i < count; i++) {
|
|
(*file)[i] = matches[i].match;
|
|
}
|
|
}
|
|
xfree(matches);
|
|
}
|
|
|
|
*num_file = count;
|
|
return count == 0 ? FAIL : OK;
|
|
}
|
|
|
|
|
|
/// Check for a match on the file name for buffer "buf" with regprog "prog".
|
|
///
|
|
/// @param ignore_case When true, ignore case. Use 'fic' otherwise.
|
|
static char_u *buflist_match(regmatch_T *rmp, buf_T *buf, bool ignore_case)
|
|
{
|
|
// First try the short file name, then the long file name.
|
|
char_u *match = fname_match(rmp, buf->b_sfname, ignore_case);
|
|
if (match == NULL) {
|
|
match = fname_match(rmp, buf->b_ffname, ignore_case);
|
|
}
|
|
return match;
|
|
}
|
|
|
|
/// Try matching the regexp in "prog" with file name "name".
|
|
///
|
|
/// @param ignore_case When true, ignore case. Use 'fileignorecase' otherwise.
|
|
///
|
|
/// @return "name" when there is a match, NULL when not.
|
|
static char_u *fname_match(regmatch_T *rmp, char_u *name, bool ignore_case)
|
|
{
|
|
char_u *match = NULL;
|
|
char_u *p;
|
|
|
|
if (name != NULL) {
|
|
// Ignore case when 'fileignorecase' or the argument is set.
|
|
rmp->rm_ic = p_fic || ignore_case;
|
|
if (vim_regexec(rmp, name, (colnr_T)0)) {
|
|
match = name;
|
|
} else {
|
|
// Replace $(HOME) with '~' and try matching again.
|
|
p = home_replace_save(NULL, name);
|
|
if (vim_regexec(rmp, p, (colnr_T)0)) {
|
|
match = name;
|
|
}
|
|
xfree(p);
|
|
}
|
|
}
|
|
|
|
return match;
|
|
}
|
|
|
|
/// Find a file in the buffer list by buffer number.
|
|
buf_T *buflist_findnr(int nr)
|
|
{
|
|
if (nr == 0) {
|
|
nr = curwin->w_alt_fnum;
|
|
}
|
|
|
|
return handle_get_buffer((handle_T)nr);
|
|
}
|
|
|
|
/// Get name of file 'n' in the buffer list.
|
|
/// When the file has no name an empty string is returned.
|
|
/// home_replace() is used to shorten the file name (used for marks).
|
|
///
|
|
/// @param helptail for help buffers return tail only
|
|
///
|
|
/// @return a pointer to allocated memory, of NULL when failed.
|
|
char_u *buflist_nr2name(int n, int fullname, int helptail)
|
|
{
|
|
buf_T *buf;
|
|
|
|
buf = buflist_findnr(n);
|
|
if (buf == NULL) {
|
|
return NULL;
|
|
}
|
|
return home_replace_save(helptail ? buf : NULL,
|
|
fullname ? buf->b_ffname : buf->b_fname);
|
|
}
|
|
|
|
/// Set the line and column numbers for the given buffer and window
|
|
///
|
|
/// @param[in,out] buf Buffer for which line and column are set.
|
|
/// @param[in,out] win Window for which line and column are set.
|
|
/// May be NULL when using :badd.
|
|
/// @param[in] lnum Line number to be set. If it is zero then only
|
|
/// options are touched.
|
|
/// @param[in] col Column number to be set.
|
|
/// @param[in] copy_options If true save the local window option values.
|
|
void buflist_setfpos(buf_T *const buf, win_T *const win, linenr_T lnum, colnr_T col,
|
|
bool copy_options)
|
|
FUNC_ATTR_NONNULL_ARG(1)
|
|
{
|
|
wininfo_T *wip;
|
|
|
|
for (wip = buf->b_wininfo; wip != NULL; wip = wip->wi_next) {
|
|
if (wip->wi_win == win) {
|
|
break;
|
|
}
|
|
}
|
|
if (wip == NULL) {
|
|
// allocate a new entry
|
|
wip = xcalloc(1, sizeof(wininfo_T));
|
|
wip->wi_win = win;
|
|
if (lnum == 0) { // set lnum even when it's 0
|
|
lnum = 1;
|
|
}
|
|
} else {
|
|
// remove the entry from the list
|
|
if (wip->wi_prev) {
|
|
wip->wi_prev->wi_next = wip->wi_next;
|
|
} else {
|
|
buf->b_wininfo = wip->wi_next;
|
|
}
|
|
if (wip->wi_next) {
|
|
wip->wi_next->wi_prev = wip->wi_prev;
|
|
}
|
|
if (copy_options && wip->wi_optset) {
|
|
clear_winopt(&wip->wi_opt);
|
|
deleteFoldRecurse(buf, &wip->wi_folds);
|
|
}
|
|
}
|
|
if (lnum != 0) {
|
|
wip->wi_fpos.lnum = lnum;
|
|
wip->wi_fpos.col = col;
|
|
}
|
|
if (copy_options && win != NULL) {
|
|
// Save the window-specific option values.
|
|
copy_winopt(&win->w_onebuf_opt, &wip->wi_opt);
|
|
wip->wi_fold_manual = win->w_fold_manual;
|
|
cloneFoldGrowArray(&win->w_folds, &wip->wi_folds);
|
|
wip->wi_optset = true;
|
|
}
|
|
|
|
// insert the entry in front of the list
|
|
wip->wi_next = buf->b_wininfo;
|
|
buf->b_wininfo = wip;
|
|
wip->wi_prev = NULL;
|
|
if (wip->wi_next) {
|
|
wip->wi_next->wi_prev = wip;
|
|
}
|
|
}
|
|
|
|
|
|
/// Check that "wip" has 'diff' set and the diff is only for another tab page.
|
|
/// That's because a diff is local to a tab page.
|
|
static bool wininfo_other_tab_diff(wininfo_T *wip)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
if (wip->wi_opt.wo_diff) {
|
|
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
|
|
// return false when it's a window in the current tab page, thus
|
|
// the buffer was in diff mode here
|
|
if (wip->wi_win == wp) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Find info for the current window in buffer "buf".
|
|
/// If not found, return the info for the most recently used window.
|
|
///
|
|
/// @param need_options when true, skip entries where wi_optset is false.
|
|
/// @param skip_diff_buffer when true, avoid windows with 'diff' set that is in another tab page.
|
|
///
|
|
/// @return NULL when there isn't any info.
|
|
static wininfo_T *find_wininfo(buf_T *buf, bool need_options, bool skip_diff_buffer)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
wininfo_T *wip;
|
|
|
|
for (wip = buf->b_wininfo; wip != NULL; wip = wip->wi_next) {
|
|
if (wip->wi_win == curwin
|
|
&& (!skip_diff_buffer || !wininfo_other_tab_diff(wip))
|
|
&& (!need_options || wip->wi_optset)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no wininfo for curwin, use the first in the list (that doesn't have
|
|
// 'diff' set and is in another tab page).
|
|
// If "need_options" is true skip entries that don't have options set,
|
|
// unless the window is editing "buf", so we can copy from the window
|
|
// itself.
|
|
if (wip == NULL) {
|
|
if (skip_diff_buffer) {
|
|
for (wip = buf->b_wininfo; wip != NULL; wip = wip->wi_next) {
|
|
if (!wininfo_other_tab_diff(wip)
|
|
&& (!need_options
|
|
|| wip->wi_optset
|
|
|| (wip->wi_win != NULL
|
|
&& wip->wi_win->w_buffer == buf))) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
wip = buf->b_wininfo;
|
|
}
|
|
}
|
|
return wip;
|
|
}
|
|
|
|
/// Reset the local window options to the values last used in this window.
|
|
/// If the buffer wasn't used in this window before, use the values from
|
|
/// the most recently used window. If the values were never set, use the
|
|
/// global values for the window.
|
|
void get_winopts(buf_T *buf)
|
|
{
|
|
clear_winopt(&curwin->w_onebuf_opt);
|
|
clearFolding(curwin);
|
|
|
|
wininfo_T *const wip = find_wininfo(buf, true, true);
|
|
if (wip != NULL && wip->wi_win != curwin && wip->wi_win != NULL
|
|
&& wip->wi_win->w_buffer == buf) {
|
|
win_T *wp = wip->wi_win;
|
|
copy_winopt(&wp->w_onebuf_opt, &curwin->w_onebuf_opt);
|
|
curwin->w_fold_manual = wp->w_fold_manual;
|
|
curwin->w_foldinvalid = true;
|
|
cloneFoldGrowArray(&wp->w_folds, &curwin->w_folds);
|
|
} else if (wip != NULL && wip->wi_optset) {
|
|
copy_winopt(&wip->wi_opt, &curwin->w_onebuf_opt);
|
|
curwin->w_fold_manual = wip->wi_fold_manual;
|
|
curwin->w_foldinvalid = true;
|
|
cloneFoldGrowArray(&wip->wi_folds, &curwin->w_folds);
|
|
} else {
|
|
copy_winopt(&curwin->w_allbuf_opt, &curwin->w_onebuf_opt);
|
|
}
|
|
|
|
if (curwin->w_float_config.style == kWinStyleMinimal) {
|
|
didset_window_options(curwin);
|
|
win_set_minimal_style(curwin);
|
|
}
|
|
|
|
// Set 'foldlevel' to 'foldlevelstart' if it's not negative.
|
|
if (p_fdls >= 0) {
|
|
curwin->w_p_fdl = p_fdls;
|
|
}
|
|
didset_window_options(curwin);
|
|
}
|
|
|
|
/// Find the position (lnum and col) for the buffer 'buf' for the current
|
|
/// window.
|
|
///
|
|
/// @return a pointer to no_position if no position is found.
|
|
pos_T *buflist_findfpos(buf_T *buf)
|
|
{
|
|
static pos_T no_position = { 1, 0, 0 };
|
|
|
|
wininfo_T *const wip = find_wininfo(buf, false, false);
|
|
return (wip == NULL) ? &no_position : &(wip->wi_fpos);
|
|
}
|
|
|
|
/// Find the lnum for the buffer 'buf' for the current window.
|
|
linenr_T buflist_findlnum(buf_T *buf)
|
|
{
|
|
return buflist_findfpos(buf)->lnum;
|
|
}
|
|
|
|
/// List all known file names (for :files and :buffers command).
|
|
void buflist_list(exarg_T *eap)
|
|
{
|
|
buf_T *buf = firstbuf;
|
|
int len;
|
|
int i;
|
|
|
|
garray_T buflist;
|
|
buf_T **buflist_data = NULL;
|
|
|
|
if (vim_strchr(eap->arg, 't')) {
|
|
ga_init(&buflist, sizeof(buf_T *), 50);
|
|
for (buf = firstbuf; buf != NULL; buf = buf->b_next) {
|
|
ga_grow(&buflist, 1);
|
|
((buf_T **)buflist.ga_data)[buflist.ga_len++] = buf;
|
|
}
|
|
|
|
qsort(buflist.ga_data, (size_t)buflist.ga_len,
|
|
sizeof(buf_T *), buf_time_compare);
|
|
|
|
buflist_data = (buf_T **)buflist.ga_data;
|
|
buf = *buflist_data;
|
|
}
|
|
buf_T **p = buflist_data;
|
|
|
|
for (;
|
|
buf != NULL && !got_int;
|
|
buf = buflist_data != NULL
|
|
? (++p < buflist_data + buflist.ga_len ? *p : NULL) : buf->b_next) {
|
|
const bool is_terminal = buf->terminal;
|
|
const bool job_running = buf->terminal && terminal_running(buf->terminal);
|
|
|
|
// skip unspecified buffers
|
|
if ((!buf->b_p_bl && !eap->forceit && !vim_strchr(eap->arg, 'u'))
|
|
|| (vim_strchr(eap->arg, 'u') && buf->b_p_bl)
|
|
|| (vim_strchr(eap->arg, '+')
|
|
&& ((buf->b_flags & BF_READERR) || !bufIsChanged(buf)))
|
|
|| (vim_strchr(eap->arg, 'a')
|
|
&& (buf->b_ml.ml_mfp == NULL || buf->b_nwindows == 0))
|
|
|| (vim_strchr(eap->arg, 'h')
|
|
&& (buf->b_ml.ml_mfp == NULL || buf->b_nwindows != 0))
|
|
|| (vim_strchr(eap->arg, 'R') && (!is_terminal || !job_running))
|
|
|| (vim_strchr(eap->arg, 'F') && (!is_terminal || job_running))
|
|
|| (vim_strchr(eap->arg, '-') && buf->b_p_ma)
|
|
|| (vim_strchr(eap->arg, '=') && !buf->b_p_ro)
|
|
|| (vim_strchr(eap->arg, 'x') && !(buf->b_flags & BF_READERR))
|
|
|| (vim_strchr(eap->arg, '%') && buf != curbuf)
|
|
|| (vim_strchr(eap->arg, '#')
|
|
&& (buf == curbuf || curwin->w_alt_fnum != buf->b_fnum))) {
|
|
continue;
|
|
}
|
|
if (buf_spname(buf) != NULL) {
|
|
STRLCPY(NameBuff, buf_spname(buf), MAXPATHL);
|
|
} else {
|
|
home_replace(buf, buf->b_fname, NameBuff, MAXPATHL, true);
|
|
}
|
|
|
|
if (message_filtered(NameBuff)) {
|
|
continue;
|
|
}
|
|
|
|
const int changed_char = (buf->b_flags & BF_READERR)
|
|
? 'x'
|
|
: (bufIsChanged(buf) ? '+' : ' ');
|
|
int ro_char = !MODIFIABLE(buf) ? '-' : (buf->b_p_ro ? '=' : ' ');
|
|
if (buf->terminal) {
|
|
ro_char = channel_job_running((uint64_t)buf->b_p_channel) ? 'R' : 'F';
|
|
}
|
|
|
|
msg_putchar('\n');
|
|
len = vim_snprintf((char *)IObuff, IOSIZE - 20, "%3d%c%c%c%c%c \"%s\"",
|
|
buf->b_fnum,
|
|
buf->b_p_bl ? ' ' : 'u',
|
|
buf == curbuf ? '%' : (curwin->w_alt_fnum == buf->b_fnum ? '#' : ' '),
|
|
buf->b_ml.ml_mfp == NULL ? ' ' : (buf->b_nwindows == 0 ? 'h' : 'a'),
|
|
ro_char,
|
|
changed_char,
|
|
NameBuff);
|
|
|
|
if (len > IOSIZE - 20) {
|
|
len = IOSIZE - 20;
|
|
}
|
|
|
|
// put "line 999" in column 40 or after the file name
|
|
i = 40 - vim_strsize(IObuff);
|
|
do {
|
|
IObuff[len++] = ' ';
|
|
} while (--i > 0 && len < IOSIZE - 18);
|
|
if (vim_strchr(eap->arg, 't') && buf->b_last_used) {
|
|
undo_fmt_time(IObuff + len, (size_t)(IOSIZE - len), buf->b_last_used);
|
|
} else {
|
|
vim_snprintf((char *)IObuff + len, (size_t)(IOSIZE - len), _("line %" PRId64),
|
|
buf == curbuf ? (int64_t)curwin->w_cursor.lnum : (int64_t)buflist_findlnum(buf));
|
|
}
|
|
|
|
msg_outtrans(IObuff);
|
|
line_breakcheck();
|
|
}
|
|
|
|
if (buflist_data) {
|
|
ga_clear(&buflist);
|
|
}
|
|
}
|
|
|
|
/// Get file name and line number for file 'fnum'.
|
|
/// Used by DoOneCmd() for translating '%' and '#'.
|
|
/// Used by insert_reg() and cmdline_paste() for '#' register.
|
|
///
|
|
/// @return FAIL if not found, OK for success.
|
|
int buflist_name_nr(int fnum, char_u **fname, linenr_T *lnum)
|
|
{
|
|
buf_T *buf;
|
|
|
|
buf = buflist_findnr(fnum);
|
|
if (buf == NULL || buf->b_fname == NULL) {
|
|
return FAIL;
|
|
}
|
|
|
|
*fname = buf->b_fname;
|
|
*lnum = buflist_findlnum(buf);
|
|
|
|
return OK;
|
|
}
|
|
|
|
/// Set the file name for "buf" to "ffname_arg", short file name to
|
|
/// "sfname_arg".
|
|
/// The file name with the full path is also remembered, for when :cd is used.
|
|
///
|
|
/// @param message give message when buffer already exists
|
|
///
|
|
/// @return FAIL for failure (file name already in use by other buffer) OK otherwise.
|
|
int setfname(buf_T *buf, char_u *ffname_arg, char_u *sfname_arg, bool message)
|
|
{
|
|
char_u *ffname = ffname_arg;
|
|
char_u *sfname = sfname_arg;
|
|
buf_T *obuf = NULL;
|
|
FileID file_id;
|
|
bool file_id_valid = false;
|
|
|
|
if (ffname == NULL || *ffname == NUL) {
|
|
// Removing the name.
|
|
if (buf->b_sfname != buf->b_ffname) {
|
|
XFREE_CLEAR(buf->b_sfname);
|
|
} else {
|
|
buf->b_sfname = NULL;
|
|
}
|
|
XFREE_CLEAR(buf->b_ffname);
|
|
} else {
|
|
fname_expand(buf, &ffname, &sfname); // will allocate ffname
|
|
if (ffname == NULL) { // out of memory
|
|
return FAIL;
|
|
}
|
|
|
|
// If the file name is already used in another buffer:
|
|
// - if the buffer is loaded, fail
|
|
// - if the buffer is not loaded, delete it from the list
|
|
file_id_valid = os_fileid((char *)ffname, &file_id);
|
|
if (!(buf->b_flags & BF_DUMMY)) {
|
|
obuf = buflist_findname_file_id(ffname, &file_id, file_id_valid);
|
|
}
|
|
if (obuf != NULL && obuf != buf) {
|
|
if (obuf->b_ml.ml_mfp != NULL) { // it's loaded, fail
|
|
if (message) {
|
|
emsg(_("E95: Buffer with this name already exists"));
|
|
}
|
|
xfree(ffname);
|
|
return FAIL;
|
|
}
|
|
// delete from the list
|
|
close_buffer(NULL, obuf, DOBUF_WIPE, false, false);
|
|
}
|
|
sfname = vim_strsave(sfname);
|
|
#ifdef USE_FNAME_CASE
|
|
path_fix_case(sfname); // set correct case for short file name
|
|
#endif
|
|
if (buf->b_sfname != buf->b_ffname) {
|
|
xfree(buf->b_sfname);
|
|
}
|
|
xfree(buf->b_ffname);
|
|
buf->b_ffname = ffname;
|
|
buf->b_sfname = sfname;
|
|
}
|
|
buf->b_fname = buf->b_sfname;
|
|
if (!file_id_valid) {
|
|
buf->file_id_valid = false;
|
|
} else {
|
|
buf->file_id_valid = true;
|
|
buf->file_id = file_id;
|
|
}
|
|
|
|
buf_name_changed(buf);
|
|
return OK;
|
|
}
|
|
|
|
/// Crude way of changing the name of a buffer. Use with care!
|
|
/// The name should be relative to the current directory.
|
|
void buf_set_name(int fnum, char_u *name)
|
|
{
|
|
buf_T *buf;
|
|
|
|
buf = buflist_findnr(fnum);
|
|
if (buf != NULL) {
|
|
if (buf->b_sfname != buf->b_ffname) {
|
|
xfree(buf->b_sfname);
|
|
}
|
|
xfree(buf->b_ffname);
|
|
buf->b_ffname = vim_strsave(name);
|
|
buf->b_sfname = NULL;
|
|
// Allocate ffname and expand into full path. Also resolves .lnk
|
|
// files on Win32.
|
|
fname_expand(buf, &buf->b_ffname, &buf->b_sfname);
|
|
buf->b_fname = buf->b_sfname;
|
|
}
|
|
}
|
|
|
|
/// Take care of what needs to be done when the name of buffer "buf" has changed.
|
|
void buf_name_changed(buf_T *buf)
|
|
{
|
|
// If the file name changed, also change the name of the swapfile
|
|
if (buf->b_ml.ml_mfp != NULL) {
|
|
ml_setname(buf);
|
|
}
|
|
|
|
if (curwin->w_buffer == buf) {
|
|
check_arg_idx(curwin); // check file name for arg list
|
|
}
|
|
maketitle(); // set window title
|
|
status_redraw_all(); // status lines need to be redrawn
|
|
fmarks_check_names(buf); // check named file marks
|
|
ml_timestamp(buf); // reset timestamp
|
|
}
|
|
|
|
/// Set alternate file name for current window
|
|
///
|
|
/// Used by do_one_cmd(), do_write() and do_ecmd().
|
|
///
|
|
/// @return the buffer.
|
|
buf_T *setaltfname(char_u *ffname, char_u *sfname, linenr_T lnum)
|
|
{
|
|
buf_T *buf;
|
|
|
|
// Create a buffer. 'buflisted' is not set if it's a new buffer
|
|
buf = buflist_new(ffname, sfname, lnum, 0);
|
|
if (buf != NULL && !cmdmod.keepalt) {
|
|
curwin->w_alt_fnum = buf->b_fnum;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
/// Get alternate file name for current window.
|
|
/// Return NULL if there isn't any, and give error message if requested.
|
|
///
|
|
/// @param errmsg give error message
|
|
char_u *getaltfname(bool errmsg)
|
|
{
|
|
char_u *fname;
|
|
linenr_T dummy;
|
|
|
|
if (buflist_name_nr(0, &fname, &dummy) == FAIL) {
|
|
if (errmsg) {
|
|
emsg(_(e_noalt));
|
|
}
|
|
return NULL;
|
|
}
|
|
return fname;
|
|
}
|
|
|
|
/// Add a file name to the buflist and return its number.
|
|
/// Uses same flags as buflist_new(), except BLN_DUMMY.
|
|
///
|
|
/// Used by qf_init(), main() and doarglist()
|
|
int buflist_add(char_u *fname, int flags)
|
|
{
|
|
buf_T *buf;
|
|
|
|
buf = buflist_new(fname, NULL, (linenr_T)0, flags);
|
|
if (buf != NULL) {
|
|
return buf->b_fnum;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#if defined(BACKSLASH_IN_FILENAME)
|
|
/// Adjust slashes in file names. Called after 'shellslash' was set.
|
|
void buflist_slash_adjust(void)
|
|
{
|
|
FOR_ALL_BUFFERS(bp) {
|
|
if (bp->b_ffname != NULL) {
|
|
slash_adjust(bp->b_ffname);
|
|
}
|
|
if (bp->b_sfname != NULL) {
|
|
slash_adjust(bp->b_sfname);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
/// Set alternate cursor position for the current buffer and window "win".
|
|
/// Also save the local window option values.
|
|
void buflist_altfpos(win_T *win)
|
|
{
|
|
buflist_setfpos(curbuf, win, win->w_cursor.lnum, win->w_cursor.col, true);
|
|
}
|
|
|
|
/// Check that "ffname" is not the same file as current file.
|
|
/// Fname must have a full path (expanded by path_to_absolute()).
|
|
///
|
|
/// @param ffname full path name to check
|
|
bool otherfile(char_u *ffname)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
return otherfile_buf(curbuf, ffname, NULL, false);
|
|
}
|
|
|
|
/// Check that "ffname" is not the same file as the file loaded in "buf".
|
|
/// Fname must have a full path (expanded by path_to_absolute()).
|
|
///
|
|
/// @param buf buffer to check
|
|
/// @param ffname full path name to check
|
|
/// @param file_id_p information about the file at "ffname".
|
|
/// @param file_id_valid whether a valid "file_id_p" was passed in.
|
|
static bool otherfile_buf(buf_T *buf, char_u *ffname, FileID *file_id_p, bool file_id_valid)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
// no name is different
|
|
if (ffname == NULL || *ffname == NUL || buf->b_ffname == NULL) {
|
|
return true;
|
|
}
|
|
if (fnamecmp(ffname, buf->b_ffname) == 0) {
|
|
return false;
|
|
}
|
|
{
|
|
FileID file_id;
|
|
// If no struct stat given, get it now
|
|
if (file_id_p == NULL) {
|
|
file_id_p = &file_id;
|
|
file_id_valid = os_fileid((char *)ffname, file_id_p);
|
|
}
|
|
if (!file_id_valid) {
|
|
// file_id not valid, assume files are different.
|
|
return true;
|
|
}
|
|
// Use dev/ino to check if the files are the same, even when the names
|
|
// are different (possible with links). Still need to compare the
|
|
// name above, for when the file doesn't exist yet.
|
|
// Problem: The dev/ino changes when a file is deleted (and created
|
|
// again) and remains the same when renamed/moved. We don't want to
|
|
// stat() each buffer each time, that would be too slow. Get the
|
|
// dev/ino again when they appear to match, but not when they appear
|
|
// to be different: Could skip a buffer when it's actually the same
|
|
// file.
|
|
if (buf_same_file_id(buf, file_id_p)) {
|
|
buf_set_file_id(buf);
|
|
if (buf_same_file_id(buf, file_id_p)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Set file_id for a buffer.
|
|
/// Must always be called when b_fname is changed!
|
|
void buf_set_file_id(buf_T *buf)
|
|
{
|
|
FileID file_id;
|
|
if (buf->b_fname != NULL
|
|
&& os_fileid((char *)buf->b_fname, &file_id)) {
|
|
buf->file_id_valid = true;
|
|
buf->file_id = file_id;
|
|
} else {
|
|
buf->file_id_valid = false;
|
|
}
|
|
}
|
|
|
|
/// Check that file_id in buffer "buf" matches with "file_id".
|
|
///
|
|
/// @param buf buffer
|
|
/// @param file_id file id
|
|
static bool buf_same_file_id(buf_T *buf, FileID *file_id)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
return buf->file_id_valid && os_fileid_equal(&(buf->file_id), file_id);
|
|
}
|
|
|
|
/// Print info about the current buffer.
|
|
///
|
|
/// @param fullname when non-zero print full path
|
|
void fileinfo(int fullname, int shorthelp, int dont_truncate)
|
|
{
|
|
char_u *name;
|
|
int n;
|
|
char *p;
|
|
char *buffer;
|
|
size_t len;
|
|
|
|
buffer = xmalloc(IOSIZE);
|
|
|
|
if (fullname > 1) { // 2 CTRL-G: include buffer number
|
|
vim_snprintf(buffer, IOSIZE, "buf %d: ", curbuf->b_fnum);
|
|
p = buffer + STRLEN(buffer);
|
|
} else {
|
|
p = buffer;
|
|
}
|
|
|
|
*p++ = '"';
|
|
if (buf_spname(curbuf) != NULL) {
|
|
STRLCPY(p, buf_spname(curbuf), IOSIZE - (p - buffer));
|
|
} else {
|
|
if (!fullname && curbuf->b_fname != NULL) {
|
|
name = curbuf->b_fname;
|
|
} else {
|
|
name = curbuf->b_ffname;
|
|
}
|
|
home_replace(shorthelp ? curbuf : NULL, name, (char_u *)p,
|
|
(size_t)(IOSIZE - (p - buffer)), true);
|
|
}
|
|
|
|
bool dontwrite = bt_dontwrite(curbuf);
|
|
vim_snprintf_add(buffer, IOSIZE, "\"%s%s%s%s%s%s",
|
|
curbufIsChanged()
|
|
? (shortmess(SHM_MOD) ? " [+]" : _(" [Modified]")) : " ",
|
|
(curbuf->b_flags & BF_NOTEDITED) && !dontwrite
|
|
? _("[Not edited]") : "",
|
|
(curbuf->b_flags & BF_NEW) && !dontwrite
|
|
? new_file_message() : "",
|
|
(curbuf->b_flags & BF_READERR)
|
|
? _("[Read errors]") : "",
|
|
curbuf->b_p_ro
|
|
? (shortmess(SHM_RO) ? _("[RO]") : _("[readonly]")) : "",
|
|
(curbufIsChanged()
|
|
|| (curbuf->b_flags & BF_WRITE_MASK)
|
|
|| curbuf->b_p_ro)
|
|
? " " : "");
|
|
// With 32 bit longs and more than 21,474,836 lines multiplying by 100
|
|
// causes an overflow, thus for large numbers divide instead.
|
|
if (curwin->w_cursor.lnum > 1000000L) {
|
|
n = (int)(((long)curwin->w_cursor.lnum) /
|
|
((long)curbuf->b_ml.ml_line_count / 100L));
|
|
} else {
|
|
n = (int)(((long)curwin->w_cursor.lnum * 100L) /
|
|
(long)curbuf->b_ml.ml_line_count);
|
|
}
|
|
if (curbuf->b_ml.ml_flags & ML_EMPTY) {
|
|
vim_snprintf_add(buffer, IOSIZE, "%s", _(no_lines_msg));
|
|
} else if (p_ru) {
|
|
// Current line and column are already on the screen -- webb
|
|
vim_snprintf_add(buffer, IOSIZE,
|
|
NGETTEXT("%" PRId64 " line --%d%%--",
|
|
"%" PRId64 " lines --%d%%--",
|
|
(unsigned long)curbuf->b_ml.ml_line_count),
|
|
(int64_t)curbuf->b_ml.ml_line_count, n);
|
|
} else {
|
|
vim_snprintf_add(buffer, IOSIZE,
|
|
_("line %" PRId64 " of %" PRId64 " --%d%%-- col "),
|
|
(int64_t)curwin->w_cursor.lnum,
|
|
(int64_t)curbuf->b_ml.ml_line_count,
|
|
n);
|
|
validate_virtcol();
|
|
len = STRLEN(buffer);
|
|
col_print((char_u *)buffer + len, IOSIZE - len,
|
|
(int)curwin->w_cursor.col + 1, (int)curwin->w_virtcol + 1);
|
|
}
|
|
|
|
(void)append_arg_number(curwin, (char_u *)buffer, IOSIZE, !shortmess(SHM_FILE));
|
|
|
|
if (dont_truncate) {
|
|
// Temporarily set msg_scroll to avoid the message being truncated.
|
|
// First call msg_start() to get the message in the right place.
|
|
msg_start();
|
|
n = msg_scroll;
|
|
msg_scroll = true;
|
|
msg(buffer);
|
|
msg_scroll = n;
|
|
} else {
|
|
p = msg_trunc_attr(buffer, false, 0);
|
|
if (restart_edit != 0 || (msg_scrolled && !need_wait_return)) {
|
|
// Need to repeat the message after redrawing when:
|
|
// - When restart_edit is set (otherwise there will be a delay
|
|
// before redrawing).
|
|
// - When the screen was scrolled but there is no wait-return
|
|
// prompt.
|
|
set_keep_msg(p, 0);
|
|
}
|
|
}
|
|
|
|
xfree(buffer);
|
|
}
|
|
|
|
void col_print(char_u *buf, size_t buflen, int col, int vcol)
|
|
{
|
|
if (col == vcol) {
|
|
vim_snprintf((char *)buf, buflen, "%d", col);
|
|
} else {
|
|
vim_snprintf((char *)buf, buflen, "%d-%d", col, vcol);
|
|
}
|
|
}
|
|
|
|
static char_u *lasttitle = NULL;
|
|
static char_u *lasticon = NULL;
|
|
|
|
|
|
/// Put the title name in the title bar and icon of the window.
|
|
void maketitle(void)
|
|
{
|
|
char_u *title_str = NULL;
|
|
char_u *icon_str = NULL;
|
|
int maxlen = 0;
|
|
int len;
|
|
int mustset;
|
|
char buf[IOSIZE];
|
|
|
|
if (!redrawing()) {
|
|
// Postpone updating the title when 'lazyredraw' is set.
|
|
need_maketitle = true;
|
|
return;
|
|
}
|
|
|
|
need_maketitle = false;
|
|
if (!p_title && !p_icon && lasttitle == NULL && lasticon == NULL) {
|
|
return; // nothing to do
|
|
}
|
|
|
|
if (p_title) {
|
|
if (p_titlelen > 0) {
|
|
maxlen = (int)(p_titlelen * Columns / 100);
|
|
if (maxlen < 10) {
|
|
maxlen = 10;
|
|
}
|
|
}
|
|
|
|
if (*p_titlestring != NUL) {
|
|
if (stl_syntax & STL_IN_TITLE) {
|
|
int use_sandbox = false;
|
|
int save_called_emsg = called_emsg;
|
|
|
|
use_sandbox = was_set_insecurely(curwin, "titlestring", 0);
|
|
called_emsg = false;
|
|
build_stl_str_hl(curwin, (char_u *)buf, sizeof(buf),
|
|
p_titlestring, use_sandbox,
|
|
0, maxlen, NULL, NULL);
|
|
title_str = (char_u *)buf;
|
|
if (called_emsg) {
|
|
set_string_option_direct("titlestring", -1, (char_u *)"",
|
|
OPT_FREE, SID_ERROR);
|
|
}
|
|
called_emsg |= save_called_emsg;
|
|
} else {
|
|
title_str = p_titlestring;
|
|
}
|
|
} else {
|
|
// Format: "fname + (path) (1 of 2) - VIM".
|
|
|
|
#define SPACE_FOR_FNAME (sizeof(buf) - 100)
|
|
#define SPACE_FOR_DIR (sizeof(buf) - 20)
|
|
#define SPACE_FOR_ARGNR (sizeof(buf) - 10) // At least room for " - NVIM".
|
|
char *buf_p = buf;
|
|
if (curbuf->b_fname == NULL) {
|
|
const size_t size = xstrlcpy(buf_p, _("[No Name]"),
|
|
SPACE_FOR_FNAME + 1);
|
|
buf_p += MIN(size, SPACE_FOR_FNAME);
|
|
} else {
|
|
buf_p += transstr_buf((const char *)path_tail(curbuf->b_fname),
|
|
buf_p, SPACE_FOR_FNAME + 1, true);
|
|
}
|
|
|
|
switch (bufIsChanged(curbuf)
|
|
| (curbuf->b_p_ro << 1)
|
|
| (!MODIFIABLE(curbuf) << 2)) {
|
|
case 0:
|
|
break;
|
|
case 1:
|
|
buf_p = strappend(buf_p, " +"); break;
|
|
case 2:
|
|
buf_p = strappend(buf_p, " ="); break;
|
|
case 3:
|
|
buf_p = strappend(buf_p, " =+"); break;
|
|
case 4:
|
|
case 6:
|
|
buf_p = strappend(buf_p, " -"); break;
|
|
case 5:
|
|
case 7:
|
|
buf_p = strappend(buf_p, " -+"); break;
|
|
default:
|
|
abort();
|
|
}
|
|
|
|
if (curbuf->b_fname != NULL) {
|
|
// Get path of file, replace home dir with ~.
|
|
*buf_p++ = ' ';
|
|
*buf_p++ = '(';
|
|
home_replace(curbuf, curbuf->b_ffname, (char_u *)buf_p,
|
|
(SPACE_FOR_DIR - (size_t)(buf_p - buf)), true);
|
|
#ifdef BACKSLASH_IN_FILENAME
|
|
// Avoid "c:/name" to be reduced to "c".
|
|
if (isalpha((uint8_t)buf_p) && *(buf_p + 1) == ':') {
|
|
buf_p += 2;
|
|
}
|
|
#endif
|
|
// Remove the file name.
|
|
char *p = (char *)path_tail_with_sep((char_u *)buf_p);
|
|
if (p == buf_p) {
|
|
// Must be a help buffer.
|
|
xstrlcpy(buf_p, _("help"), SPACE_FOR_DIR - (size_t)(buf_p - buf));
|
|
} else {
|
|
*p = NUL;
|
|
}
|
|
|
|
// Translate unprintable chars and concatenate. Keep some
|
|
// room for the server name. When there is no room (very long
|
|
// file name) use (...).
|
|
if ((size_t)(buf_p - buf) < SPACE_FOR_DIR) {
|
|
char *const tbuf = transstr(buf_p, true);
|
|
const size_t free_space = SPACE_FOR_DIR - (size_t)(buf_p - buf) + 1;
|
|
const size_t dir_len = xstrlcpy(buf_p, tbuf, free_space);
|
|
buf_p += MIN(dir_len, free_space - 1);
|
|
xfree(tbuf);
|
|
} else {
|
|
const size_t free_space = SPACE_FOR_ARGNR - (size_t)(buf_p - buf) + 1;
|
|
const size_t dots_len = xstrlcpy(buf_p, "...", free_space);
|
|
buf_p += MIN(dots_len, free_space - 1);
|
|
}
|
|
*buf_p++ = ')';
|
|
*buf_p = NUL;
|
|
} else {
|
|
*buf_p = NUL;
|
|
}
|
|
|
|
append_arg_number(curwin, (char_u *)buf_p,
|
|
(int)(SPACE_FOR_ARGNR - (size_t)(buf_p - buf)), false);
|
|
|
|
xstrlcat(buf_p, " - NVIM", (sizeof(buf) - (size_t)(buf_p - buf)));
|
|
|
|
if (maxlen > 0) {
|
|
// Make it shorter by removing a bit in the middle.
|
|
if (vim_strsize((char_u *)buf) > maxlen) {
|
|
trunc_string((char_u *)buf, (char_u *)buf, maxlen, sizeof(buf));
|
|
}
|
|
}
|
|
title_str = (char_u *)buf;
|
|
#undef SPACE_FOR_FNAME
|
|
#undef SPACE_FOR_DIR
|
|
#undef SPACE_FOR_ARGNR
|
|
}
|
|
}
|
|
mustset = value_change(title_str, &lasttitle);
|
|
|
|
if (p_icon) {
|
|
icon_str = (char_u *)buf;
|
|
if (*p_iconstring != NUL) {
|
|
if (stl_syntax & STL_IN_ICON) {
|
|
int use_sandbox = false;
|
|
int save_called_emsg = called_emsg;
|
|
|
|
use_sandbox = was_set_insecurely(curwin, "iconstring", 0);
|
|
called_emsg = false;
|
|
build_stl_str_hl(curwin, icon_str, sizeof(buf),
|
|
p_iconstring, use_sandbox,
|
|
0, 0, NULL, NULL);
|
|
if (called_emsg) {
|
|
set_string_option_direct("iconstring", -1,
|
|
(char_u *)"", OPT_FREE, SID_ERROR);
|
|
}
|
|
called_emsg |= save_called_emsg;
|
|
} else {
|
|
icon_str = p_iconstring;
|
|
}
|
|
} else {
|
|
char_u *buf_p;
|
|
if (buf_spname(curbuf) != NULL) {
|
|
buf_p = buf_spname(curbuf);
|
|
} else { // use file name only in icon
|
|
buf_p = path_tail(curbuf->b_ffname);
|
|
}
|
|
*icon_str = NUL;
|
|
// Truncate name at 100 bytes.
|
|
len = (int)STRLEN(buf_p);
|
|
if (len > 100) {
|
|
len -= 100;
|
|
len += mb_tail_off(buf_p, buf_p + len) + 1;
|
|
buf_p += len;
|
|
}
|
|
STRCPY(icon_str, buf_p);
|
|
trans_characters(icon_str, IOSIZE);
|
|
}
|
|
}
|
|
|
|
mustset |= value_change(icon_str, &lasticon);
|
|
|
|
if (mustset) {
|
|
resettitle();
|
|
}
|
|
}
|
|
|
|
/// Used for title and icon: Check if "str" differs from "*last". Set "*last"
|
|
/// from "str" if it does by freeing the old value of "*last" and duplicating
|
|
/// "str".
|
|
///
|
|
/// @param str desired title string
|
|
/// @param[in,out] last current title string
|
|
///
|
|
/// @return true if resettitle() is to be called.
|
|
static bool value_change(char_u *str, char_u **last)
|
|
FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
if ((str == NULL) != (*last == NULL)
|
|
|| (str != NULL && *last != NULL && STRCMP(str, *last) != 0)) {
|
|
xfree(*last);
|
|
if (str == NULL) {
|
|
*last = NULL;
|
|
resettitle();
|
|
} else {
|
|
*last = vim_strsave(str);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/// Set current window title
|
|
void resettitle(void)
|
|
{
|
|
ui_call_set_icon(cstr_as_string((char *)lasticon));
|
|
ui_call_set_title(cstr_as_string((char *)lasttitle));
|
|
ui_flush();
|
|
}
|
|
|
|
#if defined(EXITFREE)
|
|
void free_titles(void)
|
|
{
|
|
xfree(lasttitle);
|
|
xfree(lasticon);
|
|
}
|
|
|
|
#endif
|
|
|
|
/// Enumeration specifying the valid numeric bases that can
|
|
/// be used when printing numbers in the status line.
|
|
typedef enum {
|
|
kNumBaseDecimal = 10,
|
|
kNumBaseHexadecimal = 16,
|
|
} NumberBase;
|
|
|
|
|
|
/// Build a string from the status line items in "fmt".
|
|
/// Return length of string in screen cells.
|
|
///
|
|
/// Normally works for window "wp", except when working for 'tabline' then it
|
|
/// is "curwin".
|
|
///
|
|
/// Items are drawn interspersed with the text that surrounds it
|
|
/// Specials: %-<wid>(xxx%) => group, %= => separation marker, %< => truncation
|
|
/// Item: %-<minwid>.<maxwid><itemch> All but <itemch> are optional
|
|
///
|
|
/// If maxwidth is not zero, the string will be filled at any middle marker
|
|
/// or truncated if too long, fillchar is used for all whitespace.
|
|
///
|
|
/// @param wp The window to build a statusline for
|
|
/// @param out The output buffer to write the statusline to
|
|
/// Note: This should not be NameBuff
|
|
/// @param outlen The length of the output buffer
|
|
/// @param fmt The statusline format string
|
|
/// @param use_sandbox Use a sandboxed environment when evaluating fmt
|
|
/// @param fillchar Character to use when filling empty space in the statusline
|
|
/// @param maxwidth The maximum width to make the statusline
|
|
/// @param hltab HL attributes (can be NULL)
|
|
/// @param tabtab Tab clicks definition (can be NULL).
|
|
///
|
|
/// @return The final width of the statusline
|
|
int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use_sandbox,
|
|
int fillchar, int maxwidth, stl_hlrec_t **hltab, StlClickRecord **tabtab)
|
|
{
|
|
static size_t stl_items_len = 20; // Initial value, grows as needed.
|
|
static stl_item_t *stl_items = NULL;
|
|
static int *stl_groupitems = NULL;
|
|
static stl_hlrec_t *stl_hltab = NULL;
|
|
static StlClickRecord *stl_tabtab = NULL;
|
|
static int *stl_separator_locations = NULL;
|
|
|
|
#define TMPLEN 70
|
|
char buf_tmp[TMPLEN];
|
|
char_u win_tmp[TMPLEN];
|
|
char_u *usefmt = fmt;
|
|
const int save_must_redraw = must_redraw;
|
|
const int save_redr_type = curwin->w_redr_type;
|
|
|
|
if (stl_items == NULL) {
|
|
stl_items = xmalloc(sizeof(stl_item_t) * stl_items_len);
|
|
stl_groupitems = xmalloc(sizeof(int) * stl_items_len);
|
|
|
|
// Allocate one more, because the last element is used to indicate the
|
|
// end of the list.
|
|
stl_hltab = xmalloc(sizeof(stl_hlrec_t) * (stl_items_len + 1));
|
|
stl_tabtab = xmalloc(sizeof(StlClickRecord) * (stl_items_len + 1));
|
|
|
|
stl_separator_locations = xmalloc(sizeof(int) * stl_items_len);
|
|
}
|
|
|
|
// When the format starts with "%!" then evaluate it as an expression and
|
|
// use the result as the actual format string.
|
|
if (fmt[0] == '%' && fmt[1] == '!') {
|
|
typval_T tv = {
|
|
.v_type = VAR_NUMBER,
|
|
.vval.v_number = wp->handle,
|
|
};
|
|
set_var(S_LEN("g:statusline_winid"), &tv, false);
|
|
|
|
usefmt = eval_to_string_safe(fmt + 2, NULL, use_sandbox);
|
|
if (usefmt == NULL) {
|
|
usefmt = fmt;
|
|
}
|
|
|
|
do_unlet(S_LEN("g:statusline_winid"), true);
|
|
}
|
|
|
|
if (fillchar == 0) {
|
|
fillchar = ' ';
|
|
}
|
|
|
|
// The cursor in windows other than the current one isn't always
|
|
// up-to-date, esp. because of autocommands and timers.
|
|
linenr_T lnum = wp->w_cursor.lnum;
|
|
if (lnum > wp->w_buffer->b_ml.ml_line_count) {
|
|
lnum = wp->w_buffer->b_ml.ml_line_count;
|
|
wp->w_cursor.lnum = lnum;
|
|
}
|
|
|
|
// Get line & check if empty (cursorpos will show "0-1").
|
|
const char_u *line_ptr = ml_get_buf(wp->w_buffer, lnum, false);
|
|
bool empty_line = (*line_ptr == NUL);
|
|
|
|
// Get the byte value now, in case we need it below. This is more
|
|
// efficient than making a copy of the line.
|
|
int byteval;
|
|
const size_t len = STRLEN(line_ptr);
|
|
if (wp->w_cursor.col > (colnr_T)len) {
|
|
// Line may have changed since checking the cursor column, or the lnum
|
|
// was adjusted above.
|
|
wp->w_cursor.col = (colnr_T)len;
|
|
wp->w_cursor.coladd = 0;
|
|
byteval = 0;
|
|
} else {
|
|
byteval = utf_ptr2char(line_ptr + wp->w_cursor.col);
|
|
}
|
|
|
|
int groupdepth = 0;
|
|
int evaldepth = 0;
|
|
|
|
int curitem = 0;
|
|
bool prevchar_isflag = true;
|
|
bool prevchar_isitem = false;
|
|
|
|
// out_p is the current position in the output buffer
|
|
char_u *out_p = out;
|
|
|
|
// out_end_p is the last valid character in the output buffer
|
|
// Note: The null termination character must occur here or earlier,
|
|
// so any user-visible characters must occur before here.
|
|
char_u *out_end_p = (out + outlen) - 1;
|
|
|
|
|
|
// Proceed character by character through the statusline format string
|
|
// fmt_p is the current position in the input buffer
|
|
for (char_u *fmt_p = usefmt; *fmt_p;) {
|
|
if (curitem == (int)stl_items_len) {
|
|
size_t new_len = stl_items_len * 3 / 2;
|
|
|
|
stl_items = xrealloc(stl_items, sizeof(stl_item_t) * new_len);
|
|
stl_groupitems = xrealloc(stl_groupitems, sizeof(int) * new_len);
|
|
stl_hltab = xrealloc(stl_hltab, sizeof(stl_hlrec_t) * (new_len + 1));
|
|
stl_tabtab = xrealloc(stl_tabtab, sizeof(StlClickRecord) * (new_len + 1));
|
|
stl_separator_locations =
|
|
xrealloc(stl_separator_locations, sizeof(int) * new_len);
|
|
|
|
stl_items_len = new_len;
|
|
}
|
|
|
|
if (*fmt_p != NUL && *fmt_p != '%') {
|
|
prevchar_isflag = prevchar_isitem = false;
|
|
}
|
|
|
|
// Copy the formatting verbatim until we reach the end of the string
|
|
// or find a formatting item (denoted by `%`)
|
|
// or run out of room in our output buffer.
|
|
while (*fmt_p != NUL && *fmt_p != '%' && out_p < out_end_p) {
|
|
*out_p++ = *fmt_p++;
|
|
}
|
|
|
|
// If we have processed the entire format string or run out of
|
|
// room in our output buffer, exit the loop.
|
|
if (*fmt_p == NUL || out_p >= out_end_p) {
|
|
break;
|
|
}
|
|
|
|
// The rest of this loop will handle a single `%` item.
|
|
// Note: We increment here to skip over the `%` character we are currently
|
|
// on so we can process the item's contents.
|
|
fmt_p++;
|
|
|
|
// Ignore `%` at the end of the format string
|
|
if (*fmt_p == NUL) {
|
|
break;
|
|
}
|
|
|
|
// Two `%` in a row is the escape sequence to print a
|
|
// single `%` in the output buffer.
|
|
if (*fmt_p == '%') {
|
|
*out_p++ = *fmt_p++;
|
|
prevchar_isflag = prevchar_isitem = false;
|
|
continue;
|
|
}
|
|
|
|
// STL_SEPARATE: Separation place between left and right aligned items.
|
|
if (*fmt_p == STL_SEPARATE) {
|
|
fmt_p++;
|
|
// Ignored when we are inside of a grouping
|
|
if (groupdepth > 0) {
|
|
continue;
|
|
}
|
|
stl_items[curitem].type = Separate;
|
|
stl_items[curitem++].start = out_p;
|
|
continue;
|
|
}
|
|
|
|
// STL_TRUNCMARK: Where to begin truncating if the statusline is too long.
|
|
if (*fmt_p == STL_TRUNCMARK) {
|
|
fmt_p++;
|
|
stl_items[curitem].type = Trunc;
|
|
stl_items[curitem++].start = out_p;
|
|
continue;
|
|
}
|
|
|
|
// The end of a grouping
|
|
if (*fmt_p == ')') {
|
|
fmt_p++;
|
|
// Ignore if we are not actually inside a group currently
|
|
if (groupdepth < 1) {
|
|
continue;
|
|
}
|
|
groupdepth--;
|
|
|
|
// Determine how long the group is.
|
|
// Note: We set the current output position to null
|
|
// so `vim_strsize` will work.
|
|
char_u *t = stl_items[stl_groupitems[groupdepth]].start;
|
|
*out_p = NUL;
|
|
long group_len = vim_strsize(t);
|
|
|
|
// If the group contained internal items
|
|
// and the group did not have a minimum width,
|
|
// and if there were no normal items in the group,
|
|
// move the output pointer back to where the group started.
|
|
// Note: This erases any non-item characters that were in the group.
|
|
// Otherwise there would be no reason to do this step.
|
|
if (curitem > stl_groupitems[groupdepth] + 1
|
|
&& stl_items[stl_groupitems[groupdepth]].minwid == 0) {
|
|
// remove group if all items are empty and highlight group
|
|
// doesn't change
|
|
int group_start_userhl = 0;
|
|
int group_end_userhl = 0;
|
|
int n;
|
|
for (n = stl_groupitems[groupdepth] - 1; n >= 0; n--) {
|
|
if (stl_items[n].type == Highlight) {
|
|
group_start_userhl = group_end_userhl = stl_items[n].minwid;
|
|
break;
|
|
}
|
|
}
|
|
for (n = stl_groupitems[groupdepth] + 1; n < curitem; n++) {
|
|
if (stl_items[n].type == Normal) {
|
|
break;
|
|
}
|
|
if (stl_items[n].type == Highlight) {
|
|
group_end_userhl = stl_items[n].minwid;
|
|
}
|
|
}
|
|
if (n == curitem && group_start_userhl == group_end_userhl) {
|
|
// empty group
|
|
out_p = t;
|
|
group_len = 0;
|
|
for (n = stl_groupitems[groupdepth] + 1; n < curitem; n++) {
|
|
// do not use the highlighting from the removed group
|
|
if (stl_items[n].type == Highlight) {
|
|
stl_items[n].type = Empty;
|
|
}
|
|
// adjust the start position of TabPage to the next
|
|
// item position
|
|
if (stl_items[n].type == TabPage) {
|
|
stl_items[n].start = out_p;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the group is longer than it is allowed to be
|
|
// truncate by removing bytes from the start of the group text.
|
|
if (group_len > stl_items[stl_groupitems[groupdepth]].maxwid) {
|
|
// { Determine the number of bytes to remove
|
|
|
|
// Find the first character that should be included.
|
|
long n = 0;
|
|
while (group_len >= stl_items[stl_groupitems[groupdepth]].maxwid) {
|
|
group_len -= ptr2cells(t + n);
|
|
n += utfc_ptr2len(t + n);
|
|
}
|
|
// }
|
|
|
|
// Prepend the `<` to indicate that the output was truncated.
|
|
*t = '<';
|
|
|
|
// { Move the truncated output
|
|
memmove(t + 1, t + n, (size_t)(out_p - (t + n)));
|
|
out_p = out_p - n + 1;
|
|
// Fill up space left over by half a double-wide char.
|
|
while (++group_len < stl_items[stl_groupitems[groupdepth]].minwid) {
|
|
MB_CHAR2BYTES(fillchar, out_p);
|
|
}
|
|
// }
|
|
|
|
// correct the start of the items for the truncation
|
|
for (int idx = stl_groupitems[groupdepth] + 1; idx < curitem; idx++) {
|
|
// Shift everything back by the number of removed bytes
|
|
stl_items[idx].start -= n;
|
|
|
|
// If the item was partially or completely truncated, set its
|
|
// start to the start of the group
|
|
if (stl_items[idx].start < t) {
|
|
stl_items[idx].start = t;
|
|
}
|
|
}
|
|
// If the group is shorter than the minimum width, add padding characters.
|
|
} else if (abs(stl_items[stl_groupitems[groupdepth]].minwid) > group_len) {
|
|
long min_group_width = stl_items[stl_groupitems[groupdepth]].minwid;
|
|
// If the group is left-aligned, add characters to the right.
|
|
if (min_group_width < 0) {
|
|
min_group_width = 0 - min_group_width;
|
|
while (group_len++ < min_group_width && out_p < out_end_p) {
|
|
MB_CHAR2BYTES(fillchar, out_p);
|
|
}
|
|
// If the group is right-aligned, shift everything to the right and
|
|
// prepend with filler characters.
|
|
} else {
|
|
// { Move the group to the right
|
|
group_len = (min_group_width - group_len) * utf_char2len(fillchar);
|
|
memmove(t + group_len, t, (size_t)(out_p - t));
|
|
if (out_p + group_len >= (out_end_p + 1)) {
|
|
group_len = (long)(out_end_p - out_p);
|
|
}
|
|
out_p += group_len;
|
|
// }
|
|
|
|
// Adjust item start positions
|
|
for (int n = stl_groupitems[groupdepth] + 1; n < curitem; n++) {
|
|
stl_items[n].start += group_len;
|
|
}
|
|
|
|
// Prepend the fill characters
|
|
for (; group_len > 0; group_len--) {
|
|
MB_CHAR2BYTES(fillchar, t);
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
int minwid = 0;
|
|
int maxwid = 9999;
|
|
bool left_align = false;
|
|
|
|
// Denotes that numbers should be left-padded with zeros
|
|
bool zeropad = (*fmt_p == '0');
|
|
if (zeropad) {
|
|
fmt_p++;
|
|
}
|
|
|
|
// Denotes that the item should be left-aligned.
|
|
// This is tracked by using a negative length.
|
|
if (*fmt_p == '-') {
|
|
fmt_p++;
|
|
left_align = true;
|
|
}
|
|
|
|
// The first digit group is the item's min width
|
|
if (ascii_isdigit(*fmt_p)) {
|
|
minwid = getdigits_int(&fmt_p, false, 0);
|
|
}
|
|
|
|
// User highlight groups override the min width field
|
|
// to denote the styling to use.
|
|
if (*fmt_p == STL_USER_HL) {
|
|
stl_items[curitem].type = Highlight;
|
|
stl_items[curitem].start = out_p;
|
|
stl_items[curitem].minwid = minwid > 9 ? 1 : minwid;
|
|
fmt_p++;
|
|
curitem++;
|
|
continue;
|
|
}
|
|
|
|
// TABPAGE pairs are used to denote a region that when clicked will
|
|
// either switch to or close a tab.
|
|
//
|
|
// Ex: tabline=%0Ttab\ zero%X
|
|
// This tabline has a TABPAGENR item with minwid `0`,
|
|
// which is then closed with a TABCLOSENR item.
|
|
// Clicking on this region with mouse enabled will switch to tab 0.
|
|
// Setting the minwid to a different value will switch
|
|
// to that tab, if it exists
|
|
//
|
|
// Ex: tabline=%1Xtab\ one%X
|
|
// This tabline has a TABCLOSENR item with minwid `1`,
|
|
// which is then closed with a TABCLOSENR item.
|
|
// Clicking on this region with mouse enabled will close tab 0.
|
|
// This is determined by the following formula:
|
|
// tab to close = (1 - minwid)
|
|
// This is because for TABPAGENR we use `minwid` = `tab number`.
|
|
// For TABCLOSENR we store the tab number as a negative value.
|
|
// Because 0 is a valid TABPAGENR value, we have to
|
|
// start our numbering at `-1`.
|
|
// So, `-1` corresponds to us wanting to close tab `0`
|
|
//
|
|
// Note: These options are only valid when creating a tabline.
|
|
if (*fmt_p == STL_TABPAGENR || *fmt_p == STL_TABCLOSENR) {
|
|
if (*fmt_p == STL_TABCLOSENR) {
|
|
if (minwid == 0) {
|
|
// %X ends the close label, go back to the previous tab label nr.
|
|
for (long n = curitem - 1; n >= 0; n--) {
|
|
if (stl_items[n].type == TabPage && stl_items[n].minwid >= 0) {
|
|
minwid = stl_items[n].minwid;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// close nrs are stored as negative values
|
|
minwid = -minwid;
|
|
}
|
|
}
|
|
stl_items[curitem].type = TabPage;
|
|
stl_items[curitem].start = out_p;
|
|
stl_items[curitem].minwid = minwid;
|
|
fmt_p++;
|
|
curitem++;
|
|
continue;
|
|
}
|
|
|
|
if (*fmt_p == STL_CLICK_FUNC) {
|
|
fmt_p++;
|
|
char *t = (char *)fmt_p;
|
|
while (*fmt_p != STL_CLICK_FUNC && *fmt_p) {
|
|
fmt_p++;
|
|
}
|
|
if (*fmt_p != STL_CLICK_FUNC) {
|
|
break;
|
|
}
|
|
stl_items[curitem].type = ClickFunc;
|
|
stl_items[curitem].start = out_p;
|
|
stl_items[curitem].cmd = xmemdupz(t, (size_t)(((char *)fmt_p - t)));
|
|
stl_items[curitem].minwid = minwid;
|
|
fmt_p++;
|
|
curitem++;
|
|
continue;
|
|
}
|
|
|
|
// Denotes the end of the minwid
|
|
// the maxwid may follow immediately after
|
|
if (*fmt_p == '.') {
|
|
fmt_p++;
|
|
if (ascii_isdigit(*fmt_p)) {
|
|
maxwid = getdigits_int(&fmt_p, false, 50);
|
|
}
|
|
}
|
|
|
|
// Bound the minimum width at 50.
|
|
// Make the number negative to denote left alignment of the item
|
|
minwid = (minwid > 50 ? 50 : minwid) * (left_align ? -1 : 1);
|
|
|
|
// Denotes the start of a new group
|
|
if (*fmt_p == '(') {
|
|
stl_groupitems[groupdepth++] = curitem;
|
|
stl_items[curitem].type = Group;
|
|
stl_items[curitem].start = out_p;
|
|
stl_items[curitem].minwid = minwid;
|
|
stl_items[curitem].maxwid = maxwid;
|
|
fmt_p++;
|
|
curitem++;
|
|
continue;
|
|
}
|
|
|
|
// Denotes end of expanded %{} block
|
|
if (*fmt_p == '}' && evaldepth > 0) {
|
|
fmt_p++;
|
|
evaldepth--;
|
|
continue;
|
|
}
|
|
|
|
// An invalid item was specified.
|
|
// Continue processing on the next character of the format string.
|
|
if (vim_strchr(STL_ALL, *fmt_p) == NULL) {
|
|
fmt_p++;
|
|
continue;
|
|
}
|
|
|
|
// The status line item type
|
|
char_u opt = *fmt_p++;
|
|
|
|
// OK - now for the real work
|
|
NumberBase base = kNumBaseDecimal;
|
|
bool itemisflag = false;
|
|
bool fillable = true;
|
|
long num = -1;
|
|
char *str = NULL;
|
|
switch (opt) {
|
|
case STL_FILEPATH:
|
|
case STL_FULLPATH:
|
|
case STL_FILENAME:
|
|
// Set fillable to false so that ' ' in the filename will not
|
|
// get replaced with the fillchar
|
|
fillable = false;
|
|
if (buf_spname(wp->w_buffer) != NULL) {
|
|
STRLCPY(NameBuff, buf_spname(wp->w_buffer), MAXPATHL);
|
|
} else {
|
|
char_u *t = (opt == STL_FULLPATH) ? wp->w_buffer->b_ffname
|
|
: wp->w_buffer->b_fname;
|
|
home_replace(wp->w_buffer, t, NameBuff, MAXPATHL, true);
|
|
}
|
|
trans_characters(NameBuff, MAXPATHL);
|
|
if (opt != STL_FILENAME) {
|
|
str = (char *)NameBuff;
|
|
} else {
|
|
str = (char *)path_tail(NameBuff);
|
|
}
|
|
break;
|
|
case STL_VIM_EXPR: // '{'
|
|
{
|
|
char_u *block_start = fmt_p - 1;
|
|
int reevaluate = (*fmt_p == '%');
|
|
itemisflag = true;
|
|
|
|
if (reevaluate) {
|
|
fmt_p++;
|
|
}
|
|
|
|
// Attempt to copy the expression to evaluate into
|
|
// the output buffer as a null-terminated string.
|
|
char_u *t = out_p;
|
|
while ((*fmt_p != '}' || (reevaluate && fmt_p[-1] != '%'))
|
|
&& *fmt_p != NUL && out_p < out_end_p) {
|
|
*out_p++ = *fmt_p++;
|
|
}
|
|
if (*fmt_p != '}') { // missing '}' or out of space
|
|
break;
|
|
}
|
|
fmt_p++;
|
|
if (reevaluate) {
|
|
out_p[-1] = 0; // remove the % at the end of %{% expr %}
|
|
} else {
|
|
*out_p = 0;
|
|
}
|
|
|
|
// Move our position in the output buffer
|
|
// to the beginning of the expression
|
|
out_p = t;
|
|
|
|
// { Evaluate the expression
|
|
|
|
// Store the current buffer number as a string variable
|
|
vim_snprintf(buf_tmp, sizeof(buf_tmp), "%d", curbuf->b_fnum);
|
|
set_internal_string_var("g:actual_curbuf", (char_u *)buf_tmp);
|
|
vim_snprintf((char *)win_tmp, sizeof(win_tmp), "%d", curwin->handle);
|
|
set_internal_string_var("g:actual_curwin", win_tmp);
|
|
|
|
buf_T *const save_curbuf = curbuf;
|
|
win_T *const save_curwin = curwin;
|
|
const int save_VIsual_active = VIsual_active;
|
|
curwin = wp;
|
|
curbuf = wp->w_buffer;
|
|
// Visual mode is only valid in the current window.
|
|
if (curwin != save_curwin) {
|
|
VIsual_active = false;
|
|
}
|
|
|
|
// Note: The result stored in `t` is unused.
|
|
str = (char *)eval_to_string_safe(out_p, &t, use_sandbox);
|
|
|
|
curwin = save_curwin;
|
|
curbuf = save_curbuf;
|
|
VIsual_active = save_VIsual_active;
|
|
|
|
// Remove the variable we just stored
|
|
do_unlet(S_LEN("g:actual_curbuf"), true);
|
|
do_unlet(S_LEN("g:actual_curwin"), true);
|
|
|
|
// }
|
|
|
|
// Check if the evaluated result is a number.
|
|
// If so, convert the number to an int and free the string.
|
|
if (str != NULL && *str != 0) {
|
|
if (*skipdigits((char_u *)str) == NUL) {
|
|
num = atoi(str);
|
|
XFREE_CLEAR(str);
|
|
itemisflag = false;
|
|
}
|
|
}
|
|
|
|
|
|
// If the output of the expression needs to be evaluated
|
|
// replace the %{} block with the result of evaluation
|
|
if (reevaluate && str != NULL && *str != 0
|
|
&& strchr((const char *)str, '%') != NULL
|
|
&& evaldepth < MAX_STL_EVAL_DEPTH) {
|
|
size_t parsed_usefmt = (size_t)(block_start - usefmt);
|
|
size_t str_length = STRLEN(str);
|
|
size_t fmt_length = STRLEN(fmt_p);
|
|
size_t new_fmt_len = parsed_usefmt
|
|
+ str_length + fmt_length + 3;
|
|
char_u *new_fmt = (char_u *)xmalloc(new_fmt_len * sizeof(char_u));
|
|
char_u *new_fmt_p = new_fmt;
|
|
|
|
new_fmt_p = (char_u *)memcpy(new_fmt_p, usefmt, parsed_usefmt)
|
|
+ parsed_usefmt;
|
|
new_fmt_p = (char_u *)memcpy(new_fmt_p, str, str_length)
|
|
+ str_length;
|
|
new_fmt_p = (char_u *)memcpy(new_fmt_p, "%}", 2) + 2;
|
|
new_fmt_p = (char_u *)memcpy(new_fmt_p, fmt_p, fmt_length)
|
|
+ fmt_length;
|
|
*new_fmt_p = 0;
|
|
new_fmt_p = NULL;
|
|
|
|
if (usefmt != fmt) {
|
|
xfree(usefmt);
|
|
}
|
|
XFREE_CLEAR(str);
|
|
usefmt = new_fmt;
|
|
fmt_p = usefmt + parsed_usefmt;
|
|
evaldepth++;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case STL_LINE:
|
|
num = (wp->w_buffer->b_ml.ml_flags & ML_EMPTY)
|
|
? 0L : (long)(wp->w_cursor.lnum);
|
|
break;
|
|
|
|
case STL_NUMLINES:
|
|
num = wp->w_buffer->b_ml.ml_line_count;
|
|
break;
|
|
|
|
case STL_COLUMN:
|
|
num = !(State & INSERT) && empty_line
|
|
? 0 : (int)wp->w_cursor.col + 1;
|
|
break;
|
|
|
|
case STL_VIRTCOL:
|
|
case STL_VIRTCOL_ALT: {
|
|
colnr_T virtcol = wp->w_virtcol + 1;
|
|
// Don't display %V if it's the same as %c.
|
|
if (opt == STL_VIRTCOL_ALT
|
|
&& (virtcol == (colnr_T)(!(State & INSERT) && empty_line
|
|
? 0 : (int)wp->w_cursor.col + 1))) {
|
|
break;
|
|
}
|
|
num = (long)virtcol;
|
|
break;
|
|
}
|
|
|
|
case STL_PERCENTAGE:
|
|
num = (int)(((long)wp->w_cursor.lnum * 100L) /
|
|
(long)wp->w_buffer->b_ml.ml_line_count);
|
|
break;
|
|
|
|
case STL_ALTPERCENT:
|
|
// Store the position percentage in our temporary buffer.
|
|
// Note: We cannot store the value in `num` because
|
|
// `get_rel_pos` can return a named position. Ex: "Top"
|
|
get_rel_pos(wp, (char_u *)buf_tmp, TMPLEN);
|
|
str = buf_tmp;
|
|
break;
|
|
|
|
case STL_ARGLISTSTAT:
|
|
fillable = false;
|
|
|
|
// Note: This is important because `append_arg_number` starts appending
|
|
// at the end of the null-terminated string.
|
|
// Setting the first byte to null means it will place the argument
|
|
// number string at the beginning of the buffer.
|
|
buf_tmp[0] = 0;
|
|
|
|
// Note: The call will only return true if it actually
|
|
// appended data to the `buf_tmp` buffer.
|
|
if (append_arg_number(wp, (char_u *)buf_tmp, (int)sizeof(buf_tmp), false)) {
|
|
str = buf_tmp;
|
|
}
|
|
break;
|
|
|
|
case STL_KEYMAP:
|
|
fillable = false;
|
|
if (get_keymap_str(wp, (char_u *)"<%s>", (char_u *)buf_tmp, TMPLEN)) {
|
|
str = buf_tmp;
|
|
}
|
|
break;
|
|
case STL_PAGENUM:
|
|
num = printer_page_num;
|
|
break;
|
|
|
|
case STL_BUFNO:
|
|
num = wp->w_buffer->b_fnum;
|
|
break;
|
|
|
|
case STL_OFFSET_X:
|
|
base = kNumBaseHexadecimal;
|
|
FALLTHROUGH;
|
|
case STL_OFFSET: {
|
|
long l = ml_find_line_or_offset(wp->w_buffer, wp->w_cursor.lnum, NULL,
|
|
false);
|
|
num = (wp->w_buffer->b_ml.ml_flags & ML_EMPTY) || l < 0 ?
|
|
0L : l + 1 + (!(State & INSERT) && empty_line ?
|
|
0 : (int)wp->w_cursor.col);
|
|
break;
|
|
}
|
|
case STL_BYTEVAL_X:
|
|
base = kNumBaseHexadecimal;
|
|
FALLTHROUGH;
|
|
case STL_BYTEVAL:
|
|
num = byteval;
|
|
if (num == NL) {
|
|
num = 0;
|
|
} else if (num == CAR && get_fileformat(wp->w_buffer) == EOL_MAC) {
|
|
num = NL;
|
|
}
|
|
break;
|
|
|
|
case STL_ROFLAG:
|
|
case STL_ROFLAG_ALT:
|
|
itemisflag = true;
|
|
if (wp->w_buffer->b_p_ro) {
|
|
str = (opt == STL_ROFLAG_ALT) ? ",RO" : _("[RO]");
|
|
}
|
|
break;
|
|
|
|
case STL_HELPFLAG:
|
|
case STL_HELPFLAG_ALT:
|
|
itemisflag = true;
|
|
if (wp->w_buffer->b_help) {
|
|
str = (opt == STL_HELPFLAG_ALT) ? ",HLP" : _("[Help]");
|
|
}
|
|
break;
|
|
|
|
case STL_FILETYPE:
|
|
// Copy the filetype if it is not null and the formatted string will fit
|
|
// in the temporary buffer
|
|
// (including the brackets and null terminating character)
|
|
if (*wp->w_buffer->b_p_ft != NUL
|
|
&& STRLEN(wp->w_buffer->b_p_ft) < TMPLEN - 3) {
|
|
vim_snprintf(buf_tmp, sizeof(buf_tmp), "[%s]",
|
|
wp->w_buffer->b_p_ft);
|
|
str = buf_tmp;
|
|
}
|
|
break;
|
|
|
|
case STL_FILETYPE_ALT:
|
|
itemisflag = true;
|
|
// Copy the filetype if it is not null and the formatted string will fit
|
|
// in the temporary buffer
|
|
// (including the comma and null terminating character)
|
|
if (*wp->w_buffer->b_p_ft != NUL
|
|
&& STRLEN(wp->w_buffer->b_p_ft) < TMPLEN - 2) {
|
|
vim_snprintf(buf_tmp, sizeof(buf_tmp), ",%s",
|
|
wp->w_buffer->b_p_ft);
|
|
// Uppercase the file extension
|
|
for (char_u *t = (char_u *)buf_tmp; *t != 0; t++) {
|
|
*t = (char_u)TOUPPER_LOC(*t);
|
|
}
|
|
str = buf_tmp;
|
|
}
|
|
break;
|
|
case STL_PREVIEWFLAG:
|
|
case STL_PREVIEWFLAG_ALT:
|
|
itemisflag = true;
|
|
if (wp->w_p_pvw) {
|
|
str = (opt == STL_PREVIEWFLAG_ALT) ? ",PRV" : _("[Preview]");
|
|
}
|
|
break;
|
|
|
|
case STL_QUICKFIX:
|
|
if (bt_quickfix(wp->w_buffer)) {
|
|
str = wp->w_llist_ref ? _(msg_loclist) : _(msg_qflist);
|
|
}
|
|
break;
|
|
|
|
case STL_MODIFIED:
|
|
case STL_MODIFIED_ALT:
|
|
itemisflag = true;
|
|
switch ((opt == STL_MODIFIED_ALT)
|
|
+ bufIsChanged(wp->w_buffer) * 2
|
|
+ (!MODIFIABLE(wp->w_buffer)) * 4) {
|
|
case 2:
|
|
str = "[+]"; break;
|
|
case 3:
|
|
str = ",+"; break;
|
|
case 4:
|
|
str = "[-]"; break;
|
|
case 5:
|
|
str = ",-"; break;
|
|
case 6:
|
|
str = "[+-]"; break;
|
|
case 7:
|
|
str = ",+-"; break;
|
|
}
|
|
break;
|
|
|
|
case STL_HIGHLIGHT: {
|
|
// { The name of the highlight is surrounded by `#`
|
|
char_u *t = fmt_p;
|
|
while (*fmt_p != '#' && *fmt_p != NUL) {
|
|
fmt_p++;
|
|
}
|
|
// }
|
|
|
|
// Create a highlight item based on the name
|
|
if (*fmt_p == '#') {
|
|
stl_items[curitem].type = Highlight;
|
|
stl_items[curitem].start = out_p;
|
|
stl_items[curitem].minwid = -syn_name2id_len(t, (size_t)(fmt_p - t));
|
|
curitem++;
|
|
fmt_p++;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If we made it this far, the item is normal and starts at
|
|
// our current position in the output buffer.
|
|
// Non-normal items would have `continued`.
|
|
stl_items[curitem].start = out_p;
|
|
stl_items[curitem].type = Normal;
|
|
|
|
// Copy the item string into the output buffer
|
|
if (str != NULL && *str) {
|
|
// { Skip the leading `,` or ` ` if the item is a flag
|
|
// and the proper conditions are met
|
|
char_u *t = (char_u *)str;
|
|
if (itemisflag) {
|
|
if ((t[0] && t[1])
|
|
&& ((!prevchar_isitem && *t == ',')
|
|
|| (prevchar_isflag && *t == ' '))) {
|
|
t++;
|
|
}
|
|
prevchar_isflag = true;
|
|
}
|
|
// }
|
|
|
|
long l = vim_strsize(t);
|
|
|
|
// If this item is non-empty, record that the last thing
|
|
// we put in the output buffer was an item
|
|
if (l > 0) {
|
|
prevchar_isitem = true;
|
|
}
|
|
|
|
// If the item is too wide, truncate it from the beginning
|
|
if (l > maxwid) {
|
|
while (l >= maxwid) {
|
|
l -= ptr2cells(t);
|
|
t += utfc_ptr2len(t);
|
|
}
|
|
|
|
// Early out if there isn't enough room for the truncation marker
|
|
if (out_p >= out_end_p) {
|
|
break;
|
|
}
|
|
|
|
// Add the truncation marker
|
|
*out_p++ = '<';
|
|
}
|
|
|
|
// If the item is right aligned and not wide enough,
|
|
// pad with fill characters.
|
|
if (minwid > 0) {
|
|
for (; l < minwid && out_p < out_end_p; l++) {
|
|
// Don't put a "-" in front of a digit.
|
|
if (l + 1 == minwid && fillchar == '-' && ascii_isdigit(*t)) {
|
|
*out_p++ = ' ';
|
|
} else {
|
|
MB_CHAR2BYTES(fillchar, out_p);
|
|
}
|
|
}
|
|
minwid = 0;
|
|
} else {
|
|
// Note: The negative value denotes a left aligned item.
|
|
// Here we switch the minimum width back to a positive value.
|
|
minwid *= -1;
|
|
}
|
|
|
|
// { Copy the string text into the output buffer
|
|
for (; *t && out_p < out_end_p; t++) {
|
|
// Change a space by fillchar, unless fillchar is '-' and a
|
|
// digit follows.
|
|
if (fillable && *t == ' '
|
|
&& (!ascii_isdigit(*(t + 1)) || fillchar != '-')) {
|
|
MB_CHAR2BYTES(fillchar, out_p);
|
|
} else {
|
|
*out_p++ = *t;
|
|
}
|
|
}
|
|
// }
|
|
|
|
// For left-aligned items, fill any remaining space with the fillchar
|
|
for (; l < minwid && out_p < out_end_p; l++) {
|
|
MB_CHAR2BYTES(fillchar, out_p);
|
|
}
|
|
|
|
// Otherwise if the item is a number, copy that to the output buffer.
|
|
} else if (num >= 0) {
|
|
if (out_p + 20 > out_end_p) {
|
|
break; // not sufficient space
|
|
}
|
|
prevchar_isitem = true;
|
|
|
|
// { Build the formatting string
|
|
char_u nstr[20];
|
|
char_u *t = nstr;
|
|
if (opt == STL_VIRTCOL_ALT) {
|
|
*t++ = '-';
|
|
minwid--;
|
|
}
|
|
*t++ = '%';
|
|
if (zeropad) {
|
|
*t++ = '0';
|
|
}
|
|
|
|
// Note: The `*` means we take the width as one of the arguments
|
|
*t++ = '*';
|
|
*t++ = (char_u)(base == kNumBaseHexadecimal ? 'X' : 'd');
|
|
*t = 0;
|
|
// }
|
|
|
|
// { Determine how many characters the number will take up when printed
|
|
// Note: We have to cast the base because the compiler uses
|
|
// unsigned ints for the enum values.
|
|
long num_chars = 1;
|
|
for (long n = num; n >= (int)base; n /= (int)base) {
|
|
num_chars++;
|
|
}
|
|
|
|
// VIRTCOL_ALT takes up an extra character because
|
|
// of the `-` we added above.
|
|
if (opt == STL_VIRTCOL_ALT) {
|
|
num_chars++;
|
|
}
|
|
// }
|
|
|
|
assert(out_end_p >= out_p);
|
|
size_t remaining_buf_len = (size_t)(out_end_p - out_p) + 1;
|
|
|
|
// If the number is going to take up too much room
|
|
// Figure out the approximate number in "scientific" type notation.
|
|
// Ex: 14532 with maxwid of 4 -> '14>3'
|
|
if (num_chars > maxwid) {
|
|
// Add two to the width because the power piece will take
|
|
// two extra characters
|
|
num_chars += 2;
|
|
|
|
// How many extra characters there are
|
|
long n = num_chars - maxwid;
|
|
|
|
// { Reduce the number by base^n
|
|
while (num_chars-- > maxwid) {
|
|
num /= (long)base;
|
|
}
|
|
// }
|
|
|
|
// { Add the format string for the exponent bit
|
|
*t++ = '>';
|
|
*t++ = '%';
|
|
// Use the same base as the first number
|
|
*t = t[-3];
|
|
*++t = 0;
|
|
// }
|
|
|
|
vim_snprintf((char *)out_p, remaining_buf_len, (char *)nstr,
|
|
0, num, n);
|
|
} else {
|
|
vim_snprintf((char *)out_p, remaining_buf_len, (char *)nstr,
|
|
minwid, num);
|
|
}
|
|
|
|
// Advance the output buffer position to the end of the
|
|
// number we just printed
|
|
out_p += STRLEN(out_p);
|
|
|
|
// Otherwise, there was nothing to print so mark the item as empty
|
|
} else {
|
|
stl_items[curitem].type = Empty;
|
|
}
|
|
|
|
// Only free the string buffer if we allocated it.
|
|
// Note: This is not needed if `str` is pointing at `tmp`
|
|
if (opt == STL_VIM_EXPR) {
|
|
XFREE_CLEAR(str);
|
|
}
|
|
|
|
if (num >= 0 || (!itemisflag && str && *str)) {
|
|
prevchar_isflag = false; // Item not NULL, but not a flag
|
|
}
|
|
|
|
// Item processed, move to the next
|
|
curitem++;
|
|
}
|
|
|
|
*out_p = NUL;
|
|
int itemcnt = curitem;
|
|
|
|
// Free the format buffer if we allocated it internally
|
|
if (usefmt != fmt) {
|
|
xfree(usefmt);
|
|
}
|
|
|
|
// We have now processed the entire statusline format string.
|
|
// What follows is post-processing to handle alignment and highlighting.
|
|
|
|
int width = vim_strsize(out);
|
|
if (maxwidth > 0 && width > maxwidth) {
|
|
// Result is too long, must truncate somewhere.
|
|
int item_idx = 0;
|
|
char_u *trunc_p;
|
|
|
|
// If there are no items, truncate from beginning
|
|
if (itemcnt == 0) {
|
|
trunc_p = out;
|
|
|
|
// Otherwise, look for the truncation item
|
|
} else {
|
|
// Default to truncating at the first item
|
|
trunc_p = stl_items[0].start;
|
|
item_idx = 0;
|
|
|
|
for (int i = 0; i < itemcnt; i++) {
|
|
if (stl_items[i].type == Trunc) {
|
|
// Truncate at %< stl_items.
|
|
trunc_p = stl_items[i].start;
|
|
item_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the truncation point we found is beyond the maximum
|
|
// length of the string, truncate the end of the string.
|
|
if (width - vim_strsize(trunc_p) >= maxwidth) {
|
|
// Walk from the beginning of the
|
|
// string to find the last character that will fit.
|
|
trunc_p = out;
|
|
width = 0;
|
|
for (;;) {
|
|
width += ptr2cells(trunc_p);
|
|
if (width >= maxwidth) {
|
|
break;
|
|
}
|
|
|
|
// Note: Only advance the pointer if the next
|
|
// character will fit in the available output space
|
|
trunc_p += utfc_ptr2len(trunc_p);
|
|
}
|
|
|
|
// Ignore any items in the statusline that occur after
|
|
// the truncation point
|
|
for (int i = 0; i < itemcnt; i++) {
|
|
if (stl_items[i].start > trunc_p) {
|
|
itemcnt = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Truncate the output
|
|
*trunc_p++ = '>';
|
|
*trunc_p = 0;
|
|
|
|
// Truncate at the truncation point we found
|
|
} else {
|
|
// { Determine how many bytes to remove
|
|
long trunc_len = 0;
|
|
while (width >= maxwidth) {
|
|
width -= ptr2cells(trunc_p + trunc_len);
|
|
trunc_len += utfc_ptr2len(trunc_p + trunc_len);
|
|
}
|
|
// }
|
|
|
|
// { Truncate the string
|
|
char_u *trunc_end_p = trunc_p + trunc_len;
|
|
STRMOVE(trunc_p + 1, trunc_end_p);
|
|
|
|
// Put a `<` to mark where we truncated at
|
|
*trunc_p = '<';
|
|
|
|
if (width + 1 < maxwidth) {
|
|
// Advance the pointer to the end of the string
|
|
trunc_p = trunc_p + STRLEN(trunc_p);
|
|
}
|
|
|
|
// Fill up for half a double-wide character.
|
|
while (++width < maxwidth) {
|
|
MB_CHAR2BYTES(fillchar, trunc_p);
|
|
*trunc_p = NUL;
|
|
}
|
|
// }
|
|
|
|
// { Change the start point for items based on
|
|
// their position relative to our truncation point
|
|
|
|
// Note: The offset is one less than the truncation length because
|
|
// the truncation marker `<` is not counted.
|
|
long item_offset = trunc_len - 1;
|
|
|
|
for (int i = item_idx; i < itemcnt; i++) {
|
|
// Items starting at or after the end of the truncated section need
|
|
// to be moved backwards.
|
|
if (stl_items[i].start >= trunc_end_p) {
|
|
stl_items[i].start -= item_offset;
|
|
// Anything inside the truncated area is set to start
|
|
// at the `<` truncation character.
|
|
} else {
|
|
stl_items[i].start = trunc_p;
|
|
}
|
|
}
|
|
// }
|
|
}
|
|
width = maxwidth;
|
|
|
|
// If there is room left in our statusline, and room left in our buffer,
|
|
// add characters at the separate marker (if there is one) to
|
|
// fill up the available space.
|
|
} else if (width < maxwidth
|
|
&& STRLEN(out) + (size_t)(maxwidth - width) + 1 < outlen) {
|
|
// Find how many separators there are, which we will use when
|
|
// figuring out how many groups there are.
|
|
int num_separators = 0;
|
|
for (int i = 0; i < itemcnt; i++) {
|
|
if (stl_items[i].type == Separate) {
|
|
// Create an array of the start location for each
|
|
// separator mark.
|
|
stl_separator_locations[num_separators] = i;
|
|
num_separators++;
|
|
}
|
|
}
|
|
|
|
// If we have separated groups, then we deal with it now
|
|
if (num_separators) {
|
|
int standard_spaces = (maxwidth - width) / num_separators;
|
|
int final_spaces = (maxwidth - width) -
|
|
standard_spaces * (num_separators - 1);
|
|
|
|
for (int i = 0; i < num_separators; i++) {
|
|
int dislocation = (i == (num_separators - 1)) ? final_spaces : standard_spaces;
|
|
dislocation *= utf_char2len(fillchar);
|
|
char_u *start = stl_items[stl_separator_locations[i]].start;
|
|
char_u *seploc = start + dislocation;
|
|
STRMOVE(seploc, start);
|
|
for (char_u *s = start; s < seploc;) {
|
|
MB_CHAR2BYTES(fillchar, s);
|
|
}
|
|
|
|
for (int item_idx = stl_separator_locations[i] + 1;
|
|
item_idx < itemcnt;
|
|
item_idx++) {
|
|
stl_items[item_idx].start += dislocation;
|
|
}
|
|
}
|
|
|
|
width = maxwidth;
|
|
}
|
|
}
|
|
|
|
// Store the info about highlighting.
|
|
if (hltab != NULL) {
|
|
*hltab = stl_hltab;
|
|
stl_hlrec_t *sp = stl_hltab;
|
|
for (long l = 0; l < itemcnt; l++) {
|
|
if (stl_items[l].type == Highlight) {
|
|
sp->start = stl_items[l].start;
|
|
sp->userhl = stl_items[l].minwid;
|
|
sp++;
|
|
}
|
|
}
|
|
sp->start = NULL;
|
|
sp->userhl = 0;
|
|
}
|
|
|
|
// Store the info about tab pages labels.
|
|
if (tabtab != NULL) {
|
|
*tabtab = stl_tabtab;
|
|
StlClickRecord *cur_tab_rec = stl_tabtab;
|
|
for (long l = 0; l < itemcnt; l++) {
|
|
if (stl_items[l].type == TabPage) {
|
|
cur_tab_rec->start = (char *)stl_items[l].start;
|
|
if (stl_items[l].minwid == 0) {
|
|
cur_tab_rec->def.type = kStlClickDisabled;
|
|
cur_tab_rec->def.tabnr = 0;
|
|
} else {
|
|
int tabnr = stl_items[l].minwid;
|
|
if (stl_items[l].minwid > 0) {
|
|
cur_tab_rec->def.type = kStlClickTabSwitch;
|
|
} else {
|
|
cur_tab_rec->def.type = kStlClickTabClose;
|
|
tabnr = -tabnr;
|
|
}
|
|
cur_tab_rec->def.tabnr = tabnr;
|
|
}
|
|
cur_tab_rec->def.func = NULL;
|
|
cur_tab_rec++;
|
|
} else if (stl_items[l].type == ClickFunc) {
|
|
cur_tab_rec->start = (char *)stl_items[l].start;
|
|
cur_tab_rec->def.type = kStlClickFuncRun;
|
|
cur_tab_rec->def.tabnr = stl_items[l].minwid;
|
|
cur_tab_rec->def.func = stl_items[l].cmd;
|
|
cur_tab_rec++;
|
|
}
|
|
}
|
|
cur_tab_rec->start = NULL;
|
|
cur_tab_rec->def.type = kStlClickDisabled;
|
|
cur_tab_rec->def.tabnr = 0;
|
|
cur_tab_rec->def.func = NULL;
|
|
}
|
|
|
|
// When inside update_screen we do not want redrawing a statusline, ruler,
|
|
// title, etc. to trigger another redraw, it may cause an endless loop.
|
|
if (updating_screen) {
|
|
must_redraw = save_must_redraw;
|
|
curwin->w_redr_type = save_redr_type;
|
|
}
|
|
|
|
return width;
|
|
} // NOLINT(readability/fn_size)
|
|
|
|
/// Get relative cursor position in window into "buf[buflen]", in the form 99%,
|
|
/// using "Top", "Bot" or "All" when appropriate.
|
|
void get_rel_pos(win_T *wp, char_u *buf, int buflen)
|
|
{
|
|
// Need at least 3 chars for writing.
|
|
if (buflen < 3) {
|
|
return;
|
|
}
|
|
|
|
long above; // number of lines above window
|
|
long below; // number of lines below window
|
|
|
|
above = wp->w_topline - 1;
|
|
above += win_get_fill(wp, wp->w_topline) - wp->w_topfill;
|
|
if (wp->w_topline == 1 && wp->w_topfill >= 1) {
|
|
// All buffer lines are displayed and there is an indication
|
|
// of filler lines, that can be considered seeing all lines.
|
|
above = 0;
|
|
}
|
|
below = wp->w_buffer->b_ml.ml_line_count - wp->w_botline + 1;
|
|
if (below <= 0) {
|
|
STRLCPY(buf, (above == 0 ? _("All") : _("Bot")), buflen);
|
|
} else if (above <= 0) {
|
|
STRLCPY(buf, _("Top"), buflen);
|
|
} else {
|
|
vim_snprintf((char *)buf, (size_t)buflen, "%2d%%", above > 1000000L
|
|
? (int)(above / ((above + below) / 100L))
|
|
: (int)(above * 100L / (above + below)));
|
|
}
|
|
}
|
|
|
|
/// Append (file 2 of 8) to "buf[buflen]", if editing more than one file.
|
|
///
|
|
/// @param wp window whose buffers to check
|
|
/// @param[in,out] buf string buffer to add the text to
|
|
/// @param buflen length of the string buffer
|
|
/// @param add_file if true, add "file" before the arg number
|
|
///
|
|
/// @return true if it was appended.
|
|
static bool append_arg_number(win_T *wp, char_u *buf, int buflen, bool add_file)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
// Nothing to do
|
|
if (ARGCOUNT <= 1) {
|
|
return false;
|
|
}
|
|
|
|
char_u *p = buf + STRLEN(buf); // go to the end of the buffer
|
|
|
|
// Early out if the string is getting too long
|
|
if (p - buf + 35 >= buflen) {
|
|
return false;
|
|
}
|
|
|
|
*p++ = ' ';
|
|
*p++ = '(';
|
|
if (add_file) {
|
|
STRCPY(p, "file ");
|
|
p += 5;
|
|
}
|
|
vim_snprintf((char *)p, (size_t)(buflen - (p - buf)),
|
|
wp->w_arg_idx_invalid
|
|
? "(%d) of %d)"
|
|
: "%d of %d)", wp->w_arg_idx + 1, ARGCOUNT);
|
|
return true;
|
|
}
|
|
|
|
/// Make "*ffname" a full file name, set "*sfname" to "*ffname" if not NULL.
|
|
/// "*ffname" becomes a pointer to allocated memory (or NULL).
|
|
/// When resolving a link both "*sfname" and "*ffname" will point to the same
|
|
/// allocated memory.
|
|
/// The "*ffname" and "*sfname" pointer values on call will not be freed.
|
|
/// Note that the resulting "*ffname" pointer should be considered not allocated.
|
|
void fname_expand(buf_T *buf, char_u **ffname, char_u **sfname)
|
|
{
|
|
if (*ffname == NULL) { // no file name given, nothing to do
|
|
return;
|
|
}
|
|
if (*sfname == NULL) { // no short file name given, use ffname
|
|
*sfname = *ffname;
|
|
}
|
|
*ffname = (char_u *)fix_fname((char *)(*ffname)); // expand to full path
|
|
|
|
#ifdef WIN32
|
|
if (!buf->b_p_bin) {
|
|
// If the file name is a shortcut file, use the file it links to.
|
|
char *rfname = os_resolve_shortcut((const char *)(*ffname));
|
|
if (rfname != NULL) {
|
|
xfree(*ffname);
|
|
*ffname = (char_u *)rfname;
|
|
*sfname = (char_u *)rfname;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// Get the file name for an argument list entry.
|
|
char_u *alist_name(aentry_T *aep)
|
|
{
|
|
buf_T *bp;
|
|
|
|
// Use the name from the associated buffer if it exists.
|
|
bp = buflist_findnr(aep->ae_fnum);
|
|
if (bp == NULL || bp->b_fname == NULL) {
|
|
return aep->ae_fname;
|
|
}
|
|
return bp->b_fname;
|
|
}
|
|
|
|
/// do_arg_all(): Open up to 'count' windows, one for each argument.
|
|
///
|
|
/// @param forceit hide buffers in current windows
|
|
/// @param keep_tabs keep current tabs, for ":tab drop file"
|
|
void do_arg_all(int count, int forceit, int keep_tabs)
|
|
{
|
|
char_u *opened; // Array of weight for which args are open:
|
|
// 0: not opened
|
|
// 1: opened in other tab
|
|
// 2: opened in curtab
|
|
// 3: opened in curtab and curwin
|
|
|
|
int opened_len; // length of opened[]
|
|
int use_firstwin = false; // use first window for arglist
|
|
bool tab_drop_empty_window = false;
|
|
int split_ret = OK;
|
|
bool p_ea_save;
|
|
alist_T *alist; // argument list to be used
|
|
buf_T *buf;
|
|
tabpage_T *tpnext;
|
|
int had_tab = cmdmod.tab;
|
|
win_T *old_curwin, *last_curwin;
|
|
tabpage_T *old_curtab, *last_curtab;
|
|
win_T *new_curwin = NULL;
|
|
tabpage_T *new_curtab = NULL;
|
|
|
|
assert(firstwin != NULL); // satisfy coverity
|
|
|
|
if (ARGCOUNT <= 0) {
|
|
// Don't give an error message. We don't want it when the ":all" command is in the .vimrc.
|
|
return;
|
|
}
|
|
setpcmark();
|
|
|
|
opened_len = ARGCOUNT;
|
|
opened = xcalloc((size_t)opened_len, 1);
|
|
|
|
// Autocommands may do anything to the argument list. Make sure it's not
|
|
// freed while we are working here by "locking" it. We still have to
|
|
// watch out for its size to be changed.
|
|
alist = curwin->w_alist;
|
|
alist->al_refcount++;
|
|
|
|
old_curwin = curwin;
|
|
old_curtab = curtab;
|
|
|
|
|
|
// Try closing all windows that are not in the argument list.
|
|
// Also close windows that are not full width;
|
|
// When 'hidden' or "forceit" set the buffer becomes hidden.
|
|
// Windows that have a changed buffer and can't be hidden won't be closed.
|
|
// When the ":tab" modifier was used do this for all tab pages.
|
|
if (had_tab > 0) {
|
|
goto_tabpage_tp(first_tabpage, true, true);
|
|
}
|
|
for (;;) {
|
|
win_T *wpnext = NULL;
|
|
tpnext = curtab->tp_next;
|
|
for (win_T *wp = firstwin; wp != NULL; wp = wpnext) {
|
|
int i;
|
|
wpnext = wp->w_next;
|
|
buf = wp->w_buffer;
|
|
if (buf->b_ffname == NULL
|
|
|| (!keep_tabs && (buf->b_nwindows > 1 || wp->w_width != Columns))) {
|
|
i = opened_len;
|
|
} else {
|
|
// check if the buffer in this window is in the arglist
|
|
for (i = 0; i < opened_len; i++) {
|
|
if (i < alist->al_ga.ga_len
|
|
&& (AARGLIST(alist)[i].ae_fnum == buf->b_fnum
|
|
|| path_full_compare(alist_name(&AARGLIST(alist)[i]),
|
|
buf->b_ffname,
|
|
true, true) & kEqualFiles)) {
|
|
int weight = 1;
|
|
|
|
if (old_curtab == curtab) {
|
|
weight++;
|
|
if (old_curwin == wp) {
|
|
weight++;
|
|
}
|
|
}
|
|
|
|
if (weight > (int)opened[i]) {
|
|
opened[i] = (char_u)weight;
|
|
if (i == 0) {
|
|
if (new_curwin != NULL) {
|
|
new_curwin->w_arg_idx = opened_len;
|
|
}
|
|
new_curwin = wp;
|
|
new_curtab = curtab;
|
|
}
|
|
} else if (keep_tabs) {
|
|
i = opened_len;
|
|
}
|
|
|
|
if (wp->w_alist != alist) {
|
|
// Use the current argument list for all windows containing a file from it.
|
|
alist_unlink(wp->w_alist);
|
|
wp->w_alist = alist;
|
|
wp->w_alist->al_refcount++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
wp->w_arg_idx = i;
|
|
|
|
if (i == opened_len && !keep_tabs) { // close this window
|
|
if (buf_hide(buf) || forceit || buf->b_nwindows > 1
|
|
|| !bufIsChanged(buf)) {
|
|
// If the buffer was changed, and we would like to hide it, try autowriting.
|
|
if (!buf_hide(buf) && buf->b_nwindows <= 1 && bufIsChanged(buf)) {
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
(void)autowrite(buf, false);
|
|
// Check if autocommands removed the window.
|
|
if (!win_valid(wp) || !bufref_valid(&bufref)) {
|
|
wpnext = firstwin; // Start all over...
|
|
continue;
|
|
}
|
|
}
|
|
// don't close last window
|
|
if (ONE_WINDOW
|
|
&& (first_tabpage->tp_next == NULL || !had_tab)) {
|
|
use_firstwin = true;
|
|
} else {
|
|
win_close(wp, !buf_hide(buf) && !bufIsChanged(buf), false);
|
|
// check if autocommands removed the next window
|
|
if (!win_valid(wpnext)) {
|
|
// start all over...
|
|
wpnext = firstwin;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Without the ":tab" modifier only do the current tab page.
|
|
if (had_tab == 0 || tpnext == NULL) {
|
|
break;
|
|
}
|
|
|
|
// check if autocommands removed the next tab page
|
|
if (!valid_tabpage(tpnext)) {
|
|
tpnext = first_tabpage; // start all over...
|
|
}
|
|
goto_tabpage_tp(tpnext, true, true);
|
|
}
|
|
|
|
// Open a window for files in the argument list that don't have one.
|
|
// ARGCOUNT may change while doing this, because of autocommands.
|
|
if (count > opened_len || count <= 0) {
|
|
count = opened_len;
|
|
}
|
|
|
|
// Don't execute Win/Buf Enter/Leave autocommands here.
|
|
autocmd_no_enter++;
|
|
autocmd_no_leave++;
|
|
last_curwin = curwin;
|
|
last_curtab = curtab;
|
|
win_enter(lastwin, false);
|
|
// ":tab drop file" should re-use an empty window to avoid "--remote-tab"
|
|
// leaving an empty tab page when executed locally.
|
|
if (keep_tabs && buf_is_empty(curbuf) && curbuf->b_nwindows == 1
|
|
&& curbuf->b_ffname == NULL && !curbuf->b_changed) {
|
|
use_firstwin = true;
|
|
tab_drop_empty_window = true;
|
|
}
|
|
|
|
for (int i = 0; i < count && !got_int; i++) {
|
|
if (alist == &global_alist && i == global_alist.al_ga.ga_len - 1) {
|
|
arg_had_last = true;
|
|
}
|
|
if (opened[i] > 0) {
|
|
// Move the already present window to below the current window
|
|
if (curwin->w_arg_idx != i) {
|
|
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
|
|
if (wp->w_arg_idx == i) {
|
|
if (keep_tabs) {
|
|
new_curwin = wp;
|
|
new_curtab = curtab;
|
|
} else {
|
|
win_move_after(wp, curwin);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (split_ret == OK) {
|
|
// trigger events for tab drop
|
|
if (tab_drop_empty_window && i == count - 1) {
|
|
autocmd_no_enter--;
|
|
}
|
|
if (!use_firstwin) { // split current window
|
|
p_ea_save = p_ea;
|
|
p_ea = true; // use space from all windows
|
|
split_ret = win_split(0, WSP_ROOM | WSP_BELOW);
|
|
p_ea = p_ea_save;
|
|
if (split_ret == FAIL) {
|
|
continue;
|
|
}
|
|
} else { // first window: do autocmd for leaving this buffer
|
|
autocmd_no_leave--;
|
|
}
|
|
|
|
// edit file "i"
|
|
curwin->w_arg_idx = i;
|
|
if (i == 0) {
|
|
new_curwin = curwin;
|
|
new_curtab = curtab;
|
|
}
|
|
(void)do_ecmd(0, alist_name(&AARGLIST(alist)[i]), NULL, NULL, ECMD_ONE,
|
|
((buf_hide(curwin->w_buffer)
|
|
|| bufIsChanged(curwin->w_buffer))
|
|
? ECMD_HIDE : 0) + ECMD_OLDBUF,
|
|
curwin);
|
|
if (tab_drop_empty_window && i == count - 1) {
|
|
autocmd_no_enter++;
|
|
}
|
|
if (use_firstwin) {
|
|
autocmd_no_leave++;
|
|
}
|
|
use_firstwin = false;
|
|
}
|
|
os_breakcheck();
|
|
|
|
// When ":tab" was used open a new tab for a new window repeatedly.
|
|
if (had_tab > 0 && tabpage_index(NULL) <= p_tpm) {
|
|
cmdmod.tab = 9999;
|
|
}
|
|
}
|
|
|
|
// Remove the "lock" on the argument list.
|
|
alist_unlink(alist);
|
|
|
|
autocmd_no_enter--;
|
|
// restore last referenced tabpage's curwin
|
|
if (last_curtab != new_curtab) {
|
|
if (valid_tabpage(last_curtab)) {
|
|
goto_tabpage_tp(last_curtab, true, true);
|
|
}
|
|
if (win_valid(last_curwin)) {
|
|
win_enter(last_curwin, false);
|
|
}
|
|
}
|
|
// to window with first arg
|
|
if (valid_tabpage(new_curtab)) {
|
|
goto_tabpage_tp(new_curtab, true, true);
|
|
}
|
|
if (win_valid(new_curwin)) {
|
|
win_enter(new_curwin, false);
|
|
}
|
|
|
|
autocmd_no_leave--;
|
|
xfree(opened);
|
|
}
|
|
|
|
/// @return true if "buf" is a prompt buffer.
|
|
bool bt_prompt(buf_T *buf)
|
|
{
|
|
return buf != NULL && buf->b_p_bt[0] == 'p';
|
|
}
|
|
|
|
/// Open a window for a number of buffers.
|
|
void ex_buffer_all(exarg_T *eap)
|
|
{
|
|
buf_T *buf;
|
|
win_T *wp, *wpnext;
|
|
int split_ret = OK;
|
|
bool p_ea_save;
|
|
int open_wins = 0;
|
|
int r;
|
|
long count; // Maximum number of windows to open.
|
|
int all; // When true also load inactive buffers.
|
|
int had_tab = cmdmod.tab;
|
|
tabpage_T *tpnext;
|
|
|
|
if (eap->addr_count == 0) { // make as many windows as possible
|
|
count = 9999;
|
|
} else {
|
|
count = eap->line2; // make as many windows as specified
|
|
}
|
|
if (eap->cmdidx == CMD_unhide || eap->cmdidx == CMD_sunhide) {
|
|
all = false;
|
|
} else {
|
|
all = true;
|
|
}
|
|
|
|
setpcmark();
|
|
|
|
|
|
// Close superfluous windows (two windows for the same buffer).
|
|
// Also close windows that are not full-width.
|
|
if (had_tab > 0) {
|
|
goto_tabpage_tp(first_tabpage, true, true);
|
|
}
|
|
for (;;) {
|
|
tpnext = curtab->tp_next;
|
|
for (wp = firstwin; wp != NULL; wp = wpnext) {
|
|
wpnext = wp->w_next;
|
|
if ((wp->w_buffer->b_nwindows > 1
|
|
|| ((cmdmod.split & WSP_VERT)
|
|
? wp->w_height + wp->w_hsep_height + wp->w_status_height < Rows - p_ch
|
|
- tabline_height() - global_stl_height()
|
|
: wp->w_width != Columns)
|
|
|| (had_tab > 0 && wp != firstwin))
|
|
&& !ONE_WINDOW
|
|
&& !(wp->w_closing
|
|
|| wp->w_buffer->b_locked > 0)) {
|
|
win_close(wp, false, false);
|
|
wpnext = firstwin; // just in case an autocommand does
|
|
// something strange with windows
|
|
tpnext = first_tabpage; // start all over...
|
|
open_wins = 0;
|
|
} else {
|
|
open_wins++;
|
|
}
|
|
}
|
|
|
|
// Without the ":tab" modifier only do the current tab page.
|
|
if (had_tab == 0 || tpnext == NULL) {
|
|
break;
|
|
}
|
|
goto_tabpage_tp(tpnext, true, true);
|
|
}
|
|
|
|
// Go through the buffer list. When a buffer doesn't have a window yet,
|
|
// open one. Otherwise move the window to the right position.
|
|
// Watch out for autocommands that delete buffers or windows!
|
|
//
|
|
// Don't execute Win/Buf Enter/Leave autocommands here.
|
|
autocmd_no_enter++;
|
|
win_enter(lastwin, false);
|
|
autocmd_no_leave++;
|
|
for (buf = firstbuf; buf != NULL && open_wins < count; buf = buf->b_next) {
|
|
// Check if this buffer needs a window
|
|
if ((!all && buf->b_ml.ml_mfp == NULL) || !buf->b_p_bl) {
|
|
continue;
|
|
}
|
|
|
|
if (had_tab != 0) {
|
|
// With the ":tab" modifier don't move the window.
|
|
if (buf->b_nwindows > 0) {
|
|
wp = lastwin; // buffer has a window, skip it
|
|
} else {
|
|
wp = NULL;
|
|
}
|
|
} else {
|
|
// Check if this buffer already has a window
|
|
for (wp = firstwin; wp != NULL; wp = wp->w_next) {
|
|
if (wp->w_buffer == buf) {
|
|
break;
|
|
}
|
|
}
|
|
// If the buffer already has a window, move it
|
|
if (wp != NULL) {
|
|
win_move_after(wp, curwin);
|
|
}
|
|
}
|
|
|
|
if (wp == NULL && split_ret == OK) {
|
|
bufref_T bufref;
|
|
set_bufref(&bufref, buf);
|
|
// Split the window and put the buffer in it.
|
|
p_ea_save = p_ea;
|
|
p_ea = true; // use space from all windows
|
|
split_ret = win_split(0, WSP_ROOM | WSP_BELOW);
|
|
open_wins++;
|
|
p_ea = p_ea_save;
|
|
if (split_ret == FAIL) {
|
|
continue;
|
|
}
|
|
|
|
// Open the buffer in this window.
|
|
swap_exists_action = SEA_DIALOG;
|
|
set_curbuf(buf, DOBUF_GOTO);
|
|
if (!bufref_valid(&bufref)) {
|
|
// Autocommands deleted the buffer.
|
|
swap_exists_action = SEA_NONE;
|
|
break;
|
|
}
|
|
if (swap_exists_action == SEA_QUIT) {
|
|
cleanup_T cs;
|
|
|
|
// Reset the error/interrupt/exception state here so that
|
|
// aborting() returns false when closing a window.
|
|
enter_cleanup(&cs);
|
|
|
|
// User selected Quit at ATTENTION prompt; close this window.
|
|
win_close(curwin, true, false);
|
|
open_wins--;
|
|
swap_exists_action = SEA_NONE;
|
|
swap_exists_did_quit = true;
|
|
|
|
// Restore the error/interrupt/exception state if not
|
|
// discarded by a new aborting error, interrupt, or uncaught
|
|
// exception.
|
|
leave_cleanup(&cs);
|
|
} else {
|
|
handle_swap_exists(NULL);
|
|
}
|
|
}
|
|
|
|
os_breakcheck();
|
|
if (got_int) {
|
|
(void)vgetc(); // only break the file loading, not the rest
|
|
break;
|
|
}
|
|
// Autocommands deleted the buffer or aborted script processing!!!
|
|
if (aborting()) {
|
|
break;
|
|
}
|
|
// When ":tab" was used open a new tab for a new window repeatedly.
|
|
if (had_tab > 0 && tabpage_index(NULL) <= p_tpm) {
|
|
cmdmod.tab = 9999;
|
|
}
|
|
}
|
|
autocmd_no_enter--;
|
|
win_enter(firstwin, false); // back to first window
|
|
autocmd_no_leave--;
|
|
|
|
// Close superfluous windows.
|
|
for (wp = lastwin; open_wins > count;) {
|
|
r = (buf_hide(wp->w_buffer) || !bufIsChanged(wp->w_buffer)
|
|
|| autowrite(wp->w_buffer, false) == OK);
|
|
if (!win_valid(wp)) {
|
|
// BufWrite Autocommands made the window invalid, start over
|
|
wp = lastwin;
|
|
} else if (r) {
|
|
win_close(wp, !buf_hide(wp->w_buffer), false);
|
|
open_wins--;
|
|
wp = lastwin;
|
|
} else {
|
|
wp = wp->w_prev;
|
|
if (wp == NULL) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// do_modelines() - process mode lines for the current file
|
|
///
|
|
/// @param flags
|
|
/// OPT_WINONLY only set options local to window
|
|
/// OPT_NOWIN don't set options local to window
|
|
///
|
|
/// Returns immediately if the "ml" option isn't set.
|
|
void do_modelines(int flags)
|
|
{
|
|
linenr_T lnum;
|
|
int nmlines;
|
|
static int entered = 0;
|
|
|
|
if (!curbuf->b_p_ml || (nmlines = (int)p_mls) == 0) {
|
|
return;
|
|
}
|
|
|
|
// Disallow recursive entry here. Can happen when executing a modeline
|
|
// triggers an autocommand, which reloads modelines with a ":do".
|
|
if (entered) {
|
|
return;
|
|
}
|
|
|
|
entered++;
|
|
for (lnum = 1; curbuf->b_p_ml && lnum <= curbuf->b_ml.ml_line_count
|
|
&& lnum <= nmlines; lnum++) {
|
|
if (chk_modeline(lnum, flags) == FAIL) {
|
|
nmlines = 0;
|
|
}
|
|
}
|
|
|
|
for (lnum = curbuf->b_ml.ml_line_count; curbuf->b_p_ml && lnum > 0
|
|
&& lnum > nmlines && lnum > curbuf->b_ml.ml_line_count - nmlines;
|
|
lnum--) {
|
|
if (chk_modeline(lnum, flags) == FAIL) {
|
|
nmlines = 0;
|
|
}
|
|
}
|
|
entered--;
|
|
}
|
|
|
|
/// chk_modeline() - check a single line for a mode string
|
|
/// Return FAIL if an error encountered.
|
|
///
|
|
/// @param flags Same as for do_modelines().
|
|
static int chk_modeline(linenr_T lnum, int flags)
|
|
{
|
|
char_u *s;
|
|
char_u *e;
|
|
char_u *linecopy; // local copy of any modeline found
|
|
int prev;
|
|
intmax_t vers;
|
|
int end;
|
|
int retval = OK;
|
|
char_u *save_sourcing_name;
|
|
linenr_T save_sourcing_lnum;
|
|
|
|
prev = -1;
|
|
for (s = ml_get(lnum); *s != NUL; s++) {
|
|
if (prev == -1 || ascii_isspace(prev)) {
|
|
if ((prev != -1 && STRNCMP(s, "ex:", (size_t)3) == 0)
|
|
|| STRNCMP(s, "vi:", (size_t)3) == 0) {
|
|
break;
|
|
}
|
|
// Accept both "vim" and "Vim".
|
|
if ((s[0] == 'v' || s[0] == 'V') && s[1] == 'i' && s[2] == 'm') {
|
|
if (s[3] == '<' || s[3] == '=' || s[3] == '>') {
|
|
e = s + 4;
|
|
} else {
|
|
e = s + 3;
|
|
}
|
|
if (!try_getdigits(&e, &vers)) {
|
|
continue;
|
|
}
|
|
|
|
if (*e == ':'
|
|
&& (s[0] != 'V'
|
|
|| STRNCMP(skipwhite(e + 1), "set", 3) == 0)
|
|
&& (s[3] == ':'
|
|
|| (VIM_VERSION_100 >= vers && isdigit(s[3]))
|
|
|| (VIM_VERSION_100 < vers && s[3] == '<')
|
|
|| (VIM_VERSION_100 > vers && s[3] == '>')
|
|
|| (VIM_VERSION_100 == vers && s[3] == '='))) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
prev = *s;
|
|
}
|
|
|
|
if (!*s) {
|
|
return retval;
|
|
}
|
|
|
|
do { // skip over "ex:", "vi:" or "vim:"
|
|
s++;
|
|
} while (s[-1] != ':');
|
|
|
|
s = linecopy = vim_strsave(s); // copy the line, it will change
|
|
|
|
save_sourcing_lnum = sourcing_lnum;
|
|
save_sourcing_name = sourcing_name;
|
|
sourcing_lnum = lnum; // prepare for emsg()
|
|
sourcing_name = (char_u *)"modelines";
|
|
|
|
end = false;
|
|
while (end == false) {
|
|
s = skipwhite(s);
|
|
if (*s == NUL) {
|
|
break;
|
|
}
|
|
|
|
// Find end of set command: ':' or end of line.
|
|
// Skip over "\:", replacing it with ":".
|
|
for (e = s; *e != ':' && *e != NUL; e++) {
|
|
if (e[0] == '\\' && e[1] == ':') {
|
|
STRMOVE(e, e + 1);
|
|
}
|
|
}
|
|
if (*e == NUL) {
|
|
end = true;
|
|
}
|
|
|
|
// If there is a "set" command, require a terminating ':' and
|
|
// ignore the stuff after the ':'.
|
|
// "vi:set opt opt opt: foo" -- foo not interpreted
|
|
// "vi:opt opt opt: foo" -- foo interpreted
|
|
// Accept "se" for compatibility with Elvis.
|
|
if (STRNCMP(s, "set ", (size_t)4) == 0
|
|
|| STRNCMP(s, "se ", (size_t)3) == 0) {
|
|
if (*e != ':') { // no terminating ':'?
|
|
break;
|
|
}
|
|
end = true;
|
|
s = vim_strchr(s, ' ') + 1;
|
|
}
|
|
*e = NUL; // truncate the set command
|
|
|
|
if (*s != NUL) { // skip over an empty "::"
|
|
const int secure_save = secure;
|
|
const sctx_T save_current_sctx = current_sctx;
|
|
current_sctx.sc_sid = SID_MODELINE;
|
|
current_sctx.sc_seq = 0;
|
|
current_sctx.sc_lnum = 0;
|
|
// Make sure no risky things are executed as a side effect.
|
|
secure = 1;
|
|
|
|
retval = do_set(s, OPT_MODELINE | OPT_LOCAL | flags);
|
|
|
|
secure = secure_save;
|
|
current_sctx = save_current_sctx;
|
|
if (retval == FAIL) { // stop if error found
|
|
break;
|
|
}
|
|
}
|
|
s = e + 1; // advance to next part
|
|
}
|
|
|
|
sourcing_lnum = save_sourcing_lnum;
|
|
sourcing_name = save_sourcing_name;
|
|
|
|
xfree(linecopy);
|
|
|
|
return retval;
|
|
}
|
|
|
|
/// @return true if "buf" is a help buffer.
|
|
bool bt_help(const buf_T *const buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
return buf != NULL && buf->b_help;
|
|
}
|
|
|
|
/// @return true if "buf" is a normal buffer, 'buftype' is empty.
|
|
bool bt_normal(const buf_T *const buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
return buf != NULL && buf->b_p_bt[0] == NUL;
|
|
}
|
|
|
|
/// @return true if "buf" is the quickfix buffer.
|
|
bool bt_quickfix(const buf_T *const buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
return buf != NULL && buf->b_p_bt[0] == 'q';
|
|
}
|
|
|
|
/// @return true if "buf" is a terminal buffer.
|
|
bool bt_terminal(const buf_T *const buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
return buf != NULL && buf->b_p_bt[0] == 't';
|
|
}
|
|
|
|
/// @return true if "buf" is a "nofile", "acwrite", "terminal" or "prompt" /
|
|
/// buffer. This means the buffer name is not a file name.
|
|
bool bt_nofile(const buf_T *const buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
return buf != NULL && ((buf->b_p_bt[0] == 'n' && buf->b_p_bt[2] == 'f')
|
|
|| buf->b_p_bt[0] == 'a'
|
|
|| buf->terminal
|
|
|| buf->b_p_bt[0] == 'p');
|
|
}
|
|
|
|
/// @return true if "buf" is a "nowrite", "nofile", "terminal" or "prompt"
|
|
/// buffer.
|
|
bool bt_dontwrite(const buf_T *const buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
return buf != NULL && (buf->b_p_bt[0] == 'n'
|
|
|| buf->terminal
|
|
|| buf->b_p_bt[0] == 'p');
|
|
}
|
|
|
|
bool bt_dontwrite_msg(const buf_T *const buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
|
|
{
|
|
if (bt_dontwrite(buf)) {
|
|
emsg(_("E382: Cannot write, 'buftype' option is set"));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// @return true if the buffer should be hidden, according to 'hidden', ":hide"
|
|
/// and 'bufhidden'.
|
|
bool buf_hide(const buf_T *const buf)
|
|
{
|
|
// 'bufhidden' overrules 'hidden' and ":hide", check it first
|
|
switch (buf->b_p_bh[0]) {
|
|
case 'u': // "unload"
|
|
case 'w': // "wipe"
|
|
case 'd':
|
|
return false; // "delete"
|
|
case 'h':
|
|
return true; // "hide"
|
|
}
|
|
return p_hid || cmdmod.hide;
|
|
}
|
|
|
|
/// @return special buffer name or
|
|
/// NULL when the buffer has a normal file name.
|
|
char_u *buf_spname(buf_T *buf)
|
|
{
|
|
if (bt_quickfix(buf)) {
|
|
// Differentiate between the quickfix and location list buffers using
|
|
// the buffer number stored in the global quickfix stack.
|
|
if (buf->b_fnum == qf_stack_get_bufnr()) {
|
|
return (char_u *)_(msg_qflist);
|
|
}
|
|
return (char_u *)_(msg_loclist);
|
|
}
|
|
// There is no _file_ when 'buftype' is "nofile", b_sfname
|
|
// contains the name as specified by the user.
|
|
if (bt_nofile(buf)) {
|
|
if (buf->b_fname != NULL) {
|
|
return buf->b_fname;
|
|
}
|
|
if (bt_prompt(buf)) {
|
|
return (char_u *)_("[Prompt]");
|
|
}
|
|
return (char_u *)_("[Scratch]");
|
|
}
|
|
if (buf->b_fname == NULL) {
|
|
return buf_get_fname(buf);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/// Find a window for buffer "buf".
|
|
/// If found true is returned and "wp" and "tp" are set to
|
|
/// the window and tabpage.
|
|
/// If not found, false is returned.
|
|
///
|
|
/// @param buf buffer to find a window for
|
|
/// @param[out] wp stores the found window
|
|
/// @param[out] tp stores the found tabpage
|
|
///
|
|
/// @return true if a window was found for the buffer.
|
|
bool find_win_for_buf(buf_T *buf, win_T **wp, tabpage_T **tp)
|
|
{
|
|
*wp = NULL;
|
|
*tp = NULL;
|
|
FOR_ALL_TAB_WINDOWS(tp2, wp2) {
|
|
if (wp2->w_buffer == buf) {
|
|
*tp = tp2;
|
|
*wp = wp2;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int buf_signcols_inner(buf_T *buf, int maximum)
|
|
{
|
|
sign_entry_T *sign; // a sign in the sign list
|
|
int signcols = 0;
|
|
int linesum = 0;
|
|
linenr_T curline = 0;
|
|
|
|
buf->b_signcols.sentinel = 0;
|
|
|
|
FOR_ALL_SIGNS_IN_BUF(buf, sign) {
|
|
if (sign->se_lnum > curline) {
|
|
// Counted all signs, now add extmark signs
|
|
if (curline > 0) {
|
|
linesum += decor_signcols(buf, &decor_state, (int)curline-1, (int)curline-1,
|
|
maximum-linesum);
|
|
}
|
|
curline = sign->se_lnum;
|
|
if (linesum > signcols) {
|
|
signcols = linesum;
|
|
buf->b_signcols.sentinel = curline;
|
|
if (signcols >= maximum) {
|
|
return maximum;
|
|
}
|
|
}
|
|
linesum = 0;
|
|
}
|
|
if (sign->se_has_text_or_icon) {
|
|
linesum++;
|
|
}
|
|
}
|
|
|
|
if (curline > 0) {
|
|
linesum += decor_signcols(buf, &decor_state, (int)curline-1, (int)curline-1, maximum-linesum);
|
|
}
|
|
if (linesum > signcols) {
|
|
signcols = linesum;
|
|
if (signcols >= maximum) {
|
|
return maximum;
|
|
}
|
|
}
|
|
|
|
// Check extmarks between signs
|
|
linesum = decor_signcols(buf, &decor_state, 0, (int)buf->b_ml.ml_line_count-1, maximum);
|
|
|
|
if (linesum > signcols) {
|
|
signcols = linesum;
|
|
buf->b_signcols.sentinel = curline;
|
|
if (signcols >= maximum) {
|
|
return maximum;
|
|
}
|
|
}
|
|
|
|
return signcols;
|
|
}
|
|
|
|
/// Invalidate the signcolumn if needed after deleting
|
|
/// signs between line1 and line2 (inclusive).
|
|
///
|
|
/// @param buf buffer to check
|
|
/// @param line1 start of region being deleted
|
|
/// @param line2 end of region being deleted
|
|
void buf_signcols_del_check(buf_T *buf, linenr_T line1, linenr_T line2)
|
|
{
|
|
if (!buf->b_signcols.valid) {
|
|
return;
|
|
}
|
|
|
|
if (!buf->b_signcols.sentinel) {
|
|
buf->b_signcols.valid = false;
|
|
return;
|
|
}
|
|
|
|
linenr_T sent = buf->b_signcols.sentinel;
|
|
|
|
if (sent >= line1 && sent <= line2) {
|
|
// Only invalidate when removing signs at the sentinel line.
|
|
buf->b_signcols.valid = false;
|
|
}
|
|
}
|
|
|
|
/// Re-calculate the signcolumn after adding a sign.
|
|
///
|
|
/// @param buf buffer to check
|
|
/// @param added sign being added
|
|
void buf_signcols_add_check(buf_T *buf, sign_entry_T *added)
|
|
{
|
|
if (!buf->b_signcols.valid) {
|
|
return;
|
|
}
|
|
|
|
if (!added || !buf->b_signcols.sentinel) {
|
|
buf->b_signcols.valid = false;
|
|
return;
|
|
}
|
|
|
|
if (added->se_lnum == buf->b_signcols.sentinel) {
|
|
if (buf->b_signcols.size == buf->b_signcols.max) {
|
|
buf->b_signcols.max++;
|
|
}
|
|
buf->b_signcols.size++;
|
|
redraw_buf_later(buf, NOT_VALID);
|
|
return;
|
|
}
|
|
|
|
sign_entry_T *s;
|
|
|
|
// Get first sign for added lnum
|
|
for (s = added; s->se_prev && s->se_lnum == s->se_prev->se_lnum; s = s->se_prev) {}
|
|
|
|
// Count signs for lnum
|
|
int linesum = 1;
|
|
for (; s->se_next && s->se_lnum == s->se_next->se_lnum; s = s->se_next) {
|
|
linesum++;
|
|
}
|
|
linesum += decor_signcols(buf, &decor_state, (int)s->se_lnum-1, (int)s->se_lnum-1,
|
|
SIGN_SHOW_MAX-linesum);
|
|
|
|
if (linesum > buf->b_signcols.size) {
|
|
buf->b_signcols.size = linesum;
|
|
buf->b_signcols.max = linesum;
|
|
buf->b_signcols.sentinel = added->se_lnum;
|
|
redraw_buf_later(buf, NOT_VALID);
|
|
}
|
|
}
|
|
|
|
int buf_signcols(buf_T *buf, int maximum)
|
|
{
|
|
// The maximum can be determined from 'signcolumn' which is window scoped so
|
|
// need to invalidate signcols if the maximum is greater than the previous
|
|
// maximum.
|
|
if (maximum > buf->b_signcols.max) {
|
|
buf->b_signcols.valid = false;
|
|
}
|
|
|
|
if (!buf->b_signcols.valid) {
|
|
int signcols = buf_signcols_inner(buf, maximum);
|
|
// Check if we need to redraw
|
|
if (signcols != buf->b_signcols.size) {
|
|
buf->b_signcols.size = signcols;
|
|
buf->b_signcols.max = maximum;
|
|
redraw_buf_later(buf, NOT_VALID);
|
|
}
|
|
|
|
buf->b_signcols.valid = true;
|
|
}
|
|
|
|
return buf->b_signcols.size;
|
|
}
|
|
|
|
/// Get "buf->b_fname", use "[No Name]" if it is NULL.
|
|
char_u *buf_get_fname(const buf_T *buf)
|
|
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
if (buf->b_fname == NULL) {
|
|
return (char_u *)_("[No Name]");
|
|
}
|
|
return buf->b_fname;
|
|
}
|
|
|
|
/// Set 'buflisted' for curbuf to "on" and trigger autocommands if it changed.
|
|
void set_buflisted(int on)
|
|
{
|
|
if (on != curbuf->b_p_bl) {
|
|
curbuf->b_p_bl = on;
|
|
if (on) {
|
|
apply_autocmds(EVENT_BUFADD, NULL, NULL, false, curbuf);
|
|
} else {
|
|
apply_autocmds(EVENT_BUFDELETE, NULL, NULL, false, curbuf);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read the file for "buf" again and check if the contents changed.
|
|
/// Return true if it changed or this could not be checked.
|
|
///
|
|
/// @param buf buffer to check
|
|
///
|
|
/// @return true if the buffer's contents have changed
|
|
bool buf_contents_changed(buf_T *buf)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
bool differ = true;
|
|
|
|
// Allocate a buffer without putting it in the buffer list.
|
|
buf_T *newbuf = buflist_new(NULL, NULL, (linenr_T)1, BLN_DUMMY);
|
|
if (newbuf == NULL) {
|
|
return true;
|
|
}
|
|
|
|
// Force the 'fileencoding' and 'fileformat' to be equal.
|
|
exarg_T ea;
|
|
prep_exarg(&ea, buf);
|
|
|
|
// set curwin/curbuf to buf and save a few things
|
|
aco_save_T aco;
|
|
aucmd_prepbuf(&aco, newbuf);
|
|
|
|
if (ml_open(curbuf) == OK
|
|
&& readfile(buf->b_ffname, buf->b_fname,
|
|
(linenr_T)0, (linenr_T)0, (linenr_T)MAXLNUM,
|
|
&ea, READ_NEW | READ_DUMMY) == OK) {
|
|
// compare the two files line by line
|
|
if (buf->b_ml.ml_line_count == curbuf->b_ml.ml_line_count) {
|
|
differ = false;
|
|
for (linenr_T lnum = 1; lnum <= curbuf->b_ml.ml_line_count; lnum++) {
|
|
if (STRCMP(ml_get_buf(buf, lnum, false), ml_get(lnum)) != 0) {
|
|
differ = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
xfree(ea.cmd);
|
|
|
|
// restore curwin/curbuf and a few other things
|
|
aucmd_restbuf(&aco);
|
|
|
|
if (curbuf != newbuf) { // safety check
|
|
wipe_buffer(newbuf, false);
|
|
}
|
|
|
|
return differ;
|
|
}
|
|
|
|
/// Wipe out a buffer and decrement the last buffer number if it was used for
|
|
/// this buffer. Call this to wipe out a temp buffer that does not contain any
|
|
/// marks.
|
|
///
|
|
/// @param aucmd When true trigger autocommands.
|
|
void wipe_buffer(buf_T *buf, bool aucmd)
|
|
{
|
|
if (!aucmd) {
|
|
// Don't trigger BufDelete autocommands here.
|
|
block_autocmds();
|
|
}
|
|
close_buffer(NULL, buf, DOBUF_WIPE, false, true);
|
|
if (!aucmd) {
|
|
unblock_autocmds();
|
|
}
|
|
}
|
|
|
|
/// Creates or switches to a scratch buffer. :h special-buffers
|
|
/// Scratch buffer is:
|
|
/// - buftype=nofile bufhidden=hide noswapfile
|
|
/// - Always considered 'nomodified'
|
|
///
|
|
/// @param bufnr Buffer to switch to, or 0 to create a new buffer.
|
|
///
|
|
/// @see curbufIsChanged()
|
|
void buf_open_scratch(handle_T bufnr, char *bufname)
|
|
{
|
|
(void)do_ecmd((int)bufnr, NULL, NULL, NULL, ECMD_ONE, ECMD_HIDE, NULL);
|
|
(void)setfname(curbuf, (char_u *)bufname, NULL, true);
|
|
set_option_value("bh", 0L, "hide", OPT_LOCAL);
|
|
set_option_value("bt", 0L, "nofile", OPT_LOCAL);
|
|
set_option_value("swf", 0L, NULL, OPT_LOCAL);
|
|
RESET_BINDING(curwin);
|
|
}
|