feat(marks): restore viewport on jump #15831

** Refactor

Previously most functions used to "get" a mark returned a position,
changed the line number and sometimes changed even the current buffer.

Now functions return a {x}fmark_T making calling context aware whether
the mark is in another buffer without arcane casting. A new function is
provided for switching to the mark buffer and returning a flag style
Enum to convey what happen in the movement. If the cursor changed, line,
columns, if it changed buffer, etc.

The function to get named mark was split into multiple functions.

- mark_get() -> fmark_T
- mark_get_global() -> xfmark_T
- mark_get_local() -> fmark_T
  - mark_get_motion() -> fmark_T
  - mark_get_visual() -> fmark_T

Functions that manage the changelist and jumplist were also modified to
return mark types.

- get_jumplist -> fmark_T
- get_changelist -> fmark_T

The refactor is also seen mainly on normal.c, where all the mark
movement has been siphoned through one function nv_gomark, while the
other functions handle getting the mark and setting their movement
flags. To handle whether context marks should be left, etc.

** Mark View

While doing the refactor the concept of a mark view was also
implemented:

The view of a mark currently implemented as the number of lines between
the mark position on creation and the window topline. This allows for
moving not only back to the position of a mark but having the window
look similar to when the mark was defined. This is done by carrying and
extra element in the fmark_T struct, which can be extended later to also
restore horizontal shift.

*** User space features

1. There's a new option, jumpoptions+=view enables the mark view restoring
automatically when using the jumplist, changelist, alternate-file and
mark motions. <C-O> <C-I> g; g, <C-^> '[mark] `[mark]

** Limitations

- The view information is not saved in shada.
- Calls to get_mark should copy the value in the pointer since we are
  using pos_to_mark() to wrap and provide a homogeneous interfaces. This
  was also a limitation in the previous state of things.
This commit is contained in:
Javier Lopez
2022-06-30 07:59:52 -05:00
committed by GitHub
parent cb84f5ee53
commit 565f72b968
26 changed files with 1104 additions and 344 deletions

View File

@@ -735,6 +735,11 @@ Jumping to a mark can be done in two ways:
2. With ' (single quote): The cursor is positioned on the first non-blank 2. With ' (single quote): The cursor is positioned on the first non-blank
character in the line of the specified location and character in the line of the specified location and
the motion is linewise. the motion is linewise.
*mark-view*
3. Apart from the above if 'jumpoptions' contains "view", they will also try to
restore the mark view. This is the number of lines between the cursor position
and the window topline (first buffer line displayed in the window) when it was
set.
*m* *mark* *Mark* *m* *mark* *Mark*
m{a-zA-Z} Set mark {a-zA-Z} at cursor position (does not move m{a-zA-Z} Set mark {a-zA-Z} at cursor position (does not move

View File

@@ -3531,6 +3531,10 @@ A jump table for the options with a short description can be found at |Q_op|.
jumplist and then jumping to a location. jumplist and then jumping to a location.
|jumplist-stack| |jumplist-stack|
view When moving through the jumplist, |changelist|,
|alternate-file| or using |mark-motions| try to
restore the |mark-view| in which the action occurred.
*'joinspaces'* *'js'* *'nojoinspaces'* *'nojs'* *'joinspaces'* *'js'* *'nojoinspaces'* *'nojs'*
'joinspaces' 'js' boolean (default off) 'joinspaces' 'js' boolean (default off)
global global

View File

@@ -383,6 +383,8 @@ Normal commands:
Options: Options:
'ttimeout', 'ttimeoutlen' behavior was simplified 'ttimeout', 'ttimeoutlen' behavior was simplified
|jumpoptions| "stack" behavior |jumpoptions| "stack" behavior
|jumpoptions| "view" tries to restore the |mark-view| when moving through
the |jumplist|, |changelist|, |alternate-file| or using |mark-motions|.
'shortmess' the "F" flag does not affect output from autocommands 'shortmess' the "F" flag does not affect output from autocommands
Shell: Shell:

View File

@@ -1156,17 +1156,17 @@ Boolean nvim_buf_del_mark(Buffer buffer, String name, Error *err)
return res; return res;
} }
pos_T *pos = getmark_buf(buf, *name.data, false); fmark_T *fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, *name.data);
// pos point to NULL when there's no mark with name // fm is NULL when there's no mark with the given name
if (pos == NULL) { if (fm == NULL) {
api_set_error(err, kErrorTypeValidation, "Invalid mark name: '%c'", api_set_error(err, kErrorTypeValidation, "Invalid mark name: '%c'",
*name.data); *name.data);
return res; return res;
} }
// pos->lnum is 0 when the mark is not valid in the buffer, or is not set. // mark.lnum is 0 when the mark is not valid in the buffer, or is not set.
if (pos->lnum != 0) { if (fm->mark.lnum != 0 && fm->fnum == buf->handle) {
// since the mark belongs to the buffer delete it. // since the mark belongs to the buffer delete it.
res = set_mark(buf, name, 0, 0, err); res = set_mark(buf, name, 0, 0, err);
} }
@@ -1239,26 +1239,25 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Error *err)
return rv; return rv;
} }
pos_T *posp; fmark_T *fm;
pos_T pos;
char mark = *name.data; char mark = *name.data;
try_start(); fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, mark);
bufref_T save_buf; if (fm == NULL) {
switch_buffer(&save_buf, buf);
posp = getmark(mark, false);
restore_buffer(&save_buf);
if (try_end(err)) {
return rv;
}
if (posp == NULL) {
api_set_error(err, kErrorTypeValidation, "Invalid mark name"); api_set_error(err, kErrorTypeValidation, "Invalid mark name");
return rv; return rv;
} }
// (0, 0) uppercase/file mark set in another buffer.
if (fm->fnum != buf->handle) {
pos.lnum = 0;
pos.col = 0;
} else {
pos = fm->mark;
}
ADD(rv, INTEGER_OBJ(posp->lnum)); ADD(rv, INTEGER_OBJ(pos.lnum));
ADD(rv, INTEGER_OBJ(posp->col)); ADD(rv, INTEGER_OBJ(pos.col));
return rv; return rv;
} }

View File

@@ -916,7 +916,7 @@ bool set_mark(buf_T *buf, String name, Integer line, Integer col, Error *err)
} }
assert(INT32_MIN <= line && line <= INT32_MAX); assert(INT32_MIN <= line && line <= INT32_MAX);
pos_T pos = { (linenr_T)line, (int)col, (int)col }; pos_T pos = { (linenr_T)line, (int)col, (int)col };
res = setmark_pos(*name.data, &pos, buf->handle); res = setmark_pos(*name.data, &pos, buf->handle, NULL);
if (!res) { if (!res) {
if (deleting) { if (deleting) {
api_set_error(err, kErrorTypeException, api_set_error(err, kErrorTypeException,

View File

@@ -2027,20 +2027,20 @@ Array nvim_get_mark(String name, Dictionary opts, Error *err)
return rv; return rv;
} }
xfmark_T mark = get_global_mark(*name.data); xfmark_T *mark = mark_get_global(false, *name.data); // false avoids loading the mark buffer
pos_T pos = mark.fmark.mark; pos_T pos = mark->fmark.mark;
bool allocated = false; bool allocated = false;
int bufnr; int bufnr;
char *filename; char *filename;
// Marks are from an open buffer it fnum is non zero // Marks are from an open buffer it fnum is non zero
if (mark.fmark.fnum != 0) { if (mark->fmark.fnum != 0) {
bufnr = mark.fmark.fnum; bufnr = mark->fmark.fnum;
filename = (char *)buflist_nr2name(bufnr, true, true); filename = (char *)buflist_nr2name(bufnr, true, true);
allocated = true; allocated = true;
// Marks comes from shada // Marks comes from shada
} else { } else {
filename = mark.fname; filename = mark->fname;
bufnr = 0; bufnr = 0;
} }

View File

@@ -55,6 +55,7 @@
#include "nvim/main.h" #include "nvim/main.h"
#include "nvim/mapping.h" #include "nvim/mapping.h"
#include "nvim/mark.h" #include "nvim/mark.h"
#include "nvim/mark_defs.h"
#include "nvim/mbyte.h" #include "nvim/mbyte.h"
#include "nvim/memory.h" #include "nvim/memory.h"
#include "nvim/message.h" #include "nvim/message.h"
@@ -1789,7 +1790,8 @@ buf_T *buflist_new(char_u *ffname_arg, char_u *sfname_arg, linenr_T lnum, int fl
buf_copy_options(buf, BCO_ALWAYS); buf_copy_options(buf, BCO_ALWAYS);
} }
buf->b_wininfo->wi_fpos.lnum = lnum; buf->b_wininfo->wi_mark = (fmark_T)INIT_FMARK;
buf->b_wininfo->wi_mark.mark.lnum = lnum;
buf->b_wininfo->wi_win = curwin; buf->b_wininfo->wi_win = curwin;
hash_init(&buf->b_s.b_keywtab); hash_init(&buf->b_s.b_keywtab);
@@ -1937,7 +1939,7 @@ int buflist_getfile(int n, linenr_T lnum, int options, int forceit)
{ {
buf_T *buf; buf_T *buf;
win_T *wp = NULL; win_T *wp = NULL;
pos_T *fpos; fmark_T *fm = NULL;
colnr_T col; colnr_T col;
buf = buflist_findnr(n); buf = buflist_findnr(n);
@@ -1963,11 +1965,13 @@ int buflist_getfile(int n, linenr_T lnum, int options, int forceit)
return FAIL; return FAIL;
} }
bool restore_view = false;
// altfpos may be changed by getfile(), get it now // altfpos may be changed by getfile(), get it now
if (lnum == 0) { if (lnum == 0) {
fpos = buflist_findfpos(buf); fm = buflist_findfmark(buf);
lnum = fpos->lnum; lnum = fm->mark.lnum;
col = fpos->col; col = fm->mark.col;
restore_view = true;
} else { } else {
col = 0; col = 0;
} }
@@ -2011,6 +2015,9 @@ int buflist_getfile(int n, linenr_T lnum, int options, int forceit)
curwin->w_cursor.coladd = 0; curwin->w_cursor.coladd = 0;
curwin->w_set_curswant = true; curwin->w_set_curswant = true;
} }
if (jop_flags & JOP_VIEW && restore_view) {
mark_view_restore(fm);
}
return OK; return OK;
} }
RedrawingDisabled--; RedrawingDisabled--;
@@ -2022,7 +2029,7 @@ void buflist_getfpos(void)
{ {
pos_T *fpos; pos_T *fpos;
fpos = buflist_findfpos(curbuf); fpos = &buflist_findfmark(curbuf)->mark;
curwin->w_cursor.lnum = fpos->lnum; curwin->w_cursor.lnum = fpos->lnum;
check_cursor_lnum(); check_cursor_lnum();
@@ -2462,8 +2469,11 @@ void buflist_setfpos(buf_T *const buf, win_T *const win, linenr_T lnum, colnr_T
} }
} }
if (lnum != 0) { if (lnum != 0) {
wip->wi_fpos.lnum = lnum; wip->wi_mark.mark.lnum = lnum;
wip->wi_fpos.col = col; wip->wi_mark.mark.col = col;
if (win != NULL) {
wip->wi_mark.view = mark_view_make(win->w_topline, wip->wi_mark.mark);
}
} }
if (copy_options && win != NULL) { if (copy_options && win != NULL) {
// Save the window-specific option values. // Save the window-specific option values.
@@ -2581,24 +2591,23 @@ void get_winopts(buf_T *buf)
didset_window_options(curwin); didset_window_options(curwin);
} }
/// Find the position (lnum and col) for the buffer 'buf' for the current /// Find the mark for the buffer 'buf' for the current window.
/// window.
/// ///
/// @return a pointer to no_position if no position is found. /// @return a pointer to no_position if no position is found.
pos_T *buflist_findfpos(buf_T *buf) fmark_T *buflist_findfmark(buf_T *buf)
FUNC_ATTR_PURE FUNC_ATTR_PURE
{ {
static pos_T no_position = { 1, 0, 0 }; static fmark_T no_position = { { 1, 0, 0 }, 0, 0, { 0 }, NULL };
wininfo_T *const wip = find_wininfo(buf, false, false); wininfo_T *const wip = find_wininfo(buf, false, false);
return (wip == NULL) ? &no_position : &(wip->wi_fpos); return (wip == NULL) ? &no_position : &(wip->wi_mark);
} }
/// Find the lnum for the buffer 'buf' for the current window. /// Find the lnum for the buffer 'buf' for the current window.
linenr_T buflist_findlnum(buf_T *buf) linenr_T buflist_findlnum(buf_T *buf)
FUNC_ATTR_PURE FUNC_ATTR_PURE
{ {
return buflist_findfpos(buf)->lnum; return buflist_findfmark(buf)->mark.lnum;
} }
/// List all known file names (for :files and :buffers command). /// List all known file names (for :files and :buffers command).

View File

@@ -283,8 +283,8 @@ typedef struct {
struct wininfo_S { struct wininfo_S {
wininfo_T *wi_next; // next entry or NULL for last entry wininfo_T *wi_next; // next entry or NULL for last entry
wininfo_T *wi_prev; // previous entry or NULL for first entry wininfo_T *wi_prev; // previous entry or NULL for first entry
win_T *wi_win; // pointer to window that did set wi_fpos win_T *wi_win; // pointer to window that did set wi_mark
pos_T wi_fpos; // last cursor position in the file fmark_T wi_mark; // last cursor mark in the file
bool wi_optset; // true when wi_opt has useful values bool wi_optset; // true when wi_opt has useful values
winopt_T wi_opt; // local window options winopt_T wi_opt; // local window options
bool wi_fold_manual; // copy of w_fold_manual bool wi_fold_manual; // copy of w_fold_manual

View File

@@ -149,7 +149,13 @@ static void changed_common(linenr_T lnum, colnr_T col, linenr_T lnume, linenr_T
// set the '. mark // set the '. mark
if ((cmdmod.cmod_flags & CMOD_KEEPJUMPS) == 0) { if ((cmdmod.cmod_flags & CMOD_KEEPJUMPS) == 0) {
RESET_FMARK(&curbuf->b_last_change, ((pos_T) { lnum, col, 0 }), 0); fmarkv_T view = INIT_FMARKV;
// Set the markview only if lnum is visible, as changes might be done
// outside of the current window view.
if (lnum >= curwin->w_topline && lnum <= curwin->w_botline) {
view = mark_view_make(curwin->w_topline, curwin->w_cursor);
}
RESET_FMARK(&curbuf->b_last_change, ((pos_T) { lnum, col, 0 }), curbuf->handle, view);
// Create a new entry if a new undo-able change was started or we // Create a new entry if a new undo-able change was started or we
// don't have an entry yet. // don't have an entry yet.

View File

@@ -7928,7 +7928,8 @@ static bool ins_esc(long *count, int cmdchar, bool nomove)
// Remember the last Insert position in the '^ mark. // Remember the last Insert position in the '^ mark.
if ((cmdmod.cmod_flags & CMOD_KEEPJUMPS) == 0) { if ((cmdmod.cmod_flags & CMOD_KEEPJUMPS) == 0) {
RESET_FMARK(&curbuf->b_last_insert, curwin->w_cursor, curbuf->b_fnum); fmarkv_T view = mark_view_make(curwin->w_topline, curwin->w_cursor);
RESET_FMARK(&curbuf->b_last_insert, curwin->w_cursor, curbuf->b_fnum, view);
} }
/* /*

View File

@@ -7754,11 +7754,14 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret
} }
} else if (name[0] == '\'') { } else if (name[0] == '\'') {
// mark // mark
const pos_T *const pp = getmark_buf_fnum(curbuf, (uint8_t)name[1], false, ret_fnum); int mname = (uint8_t)name[1];
if (pp == NULL || pp == (pos_T *)-1 || pp->lnum <= 0) { const fmark_T *const fm = mark_get(curbuf, curwin, NULL, kMarkAll, mname);
if (fm == NULL || fm->mark.lnum <= 0) {
return NULL; return NULL;
} }
pos = *pp; pos = fm->mark;
// Vimscript behavior, only provide fnum if mark is global.
*ret_fnum = ASCII_ISUPPER(mname) || ascii_isdigit(mname) ? fm->fnum: *ret_fnum;
} }
if (pos.lnum != 0) { if (pos.lnum != 0) {
if (charcol) { if (charcol) {

View File

@@ -8521,7 +8521,7 @@ static void set_position(typval_T *argvars, typval_T *rettv, bool charpos)
rettv->vval.v_number = 0; rettv->vval.v_number = 0;
} else if (name[0] == '\'' && name[1] != NUL && name[2] == NUL) { } else if (name[0] == '\'' && name[1] != NUL && name[2] == NUL) {
// set mark // set mark
if (setmark_pos((uint8_t)name[1], &pos, fnum) == OK) { if (setmark_pos((uint8_t)name[1], &pos, fnum, NULL) == OK) {
rettv->vval.v_number = 0; rettv->vval.v_number = 0;
} }
} else { } else {

View File

@@ -2480,7 +2480,7 @@ int do_ecmd(int fnum, char *ffname, char *sfname, exarg_T *eap, linenr_T newlnum
// May jump to last used line number for a loaded buffer or when asked // May jump to last used line number for a loaded buffer or when asked
// for explicitly // for explicitly
if ((oldbuf && newlnum == ECMD_LASTL) || newlnum == ECMD_LAST) { if ((oldbuf && newlnum == ECMD_LASTL) || newlnum == ECMD_LAST) {
pos = buflist_findfpos(buf); pos = &buflist_findfmark(buf)->mark;
newlnum = pos->lnum; newlnum = pos->lnum;
solcol = pos->col; solcol = pos->col;
} }

View File

@@ -2820,16 +2820,16 @@ int parse_cmd_address(exarg_T *eap, char **errormsg, bool silent)
eap->cmd++; eap->cmd++;
if (!eap->skip) { if (!eap->skip) {
pos_T *fp = getmark('<', false); fmark_T *fm = mark_get_visual(curbuf, '<');
if (check_mark(fp) == FAIL) { if (!mark_check(fm)) {
goto theend; goto theend;
} }
eap->line1 = fp->lnum; eap->line1 = fm->mark.lnum;
fp = getmark('>', false); fm = mark_get_visual(curbuf, '>');
if (check_mark(fp) == FAIL) { if (!mark_check(fm)) {
goto theend; goto theend;
} }
eap->line2 = fp->lnum; eap->line2 = fm->mark.lnum;
eap->addr_count++; eap->addr_count++;
} }
} }
@@ -4230,7 +4230,6 @@ static linenr_T get_address(exarg_T *eap, char **ptr, cmd_addr_T addr_type, int
linenr_T n; linenr_T n;
char *cmd; char *cmd;
pos_T pos; pos_T pos;
pos_T *fp;
linenr_T lnum; linenr_T lnum;
buf_T *buf; buf_T *buf;
@@ -4340,17 +4339,19 @@ static linenr_T get_address(exarg_T *eap, char **ptr, cmd_addr_T addr_type, int
} else { } else {
// Only accept a mark in another file when it is // Only accept a mark in another file when it is
// used by itself: ":'M". // used by itself: ":'M".
fp = getmark(*cmd, to_other_file && cmd[1] == NUL); MarkGet flag = to_other_file && cmd[1] == NUL ? kMarkAll : kMarkBufLocal;
++cmd; fmark_T *fm = mark_get(curbuf, curwin, NULL, flag, *cmd);
if (fp == (pos_T *)-1) { MarkMoveRes move_res = mark_move_to(fm, kMarkBeginLine);
// Jumped to another file. cmd++;
// Switched buffer
if (move_res & kMarkSwitchedBuf) {
lnum = curwin->w_cursor.lnum; lnum = curwin->w_cursor.lnum;
} else { } else {
if (check_mark(fp) == FAIL) { if (fm == NULL) {
cmd = NULL; cmd = NULL;
goto error; goto error;
} }
lnum = fp->lnum; lnum = fm->mark.lnum;
} }
} }
break; break;

View File

@@ -621,7 +621,7 @@ static int makeopens(FILE *fd, char_u *dirnow)
if (fprintf(fd, "badd +%" PRId64 " ", if (fprintf(fd, "badd +%" PRId64 " ",
buf->b_wininfo == NULL buf->b_wininfo == NULL
? (int64_t)1L ? (int64_t)1L
: (int64_t)buf->b_wininfo->wi_fpos.lnum) < 0 : (int64_t)buf->b_wininfo->wi_mark.mark.lnum) < 0
|| ses_fname(fd, buf, &ssop_flags, true) == FAIL) { || ses_fname(fd, buf, &ssop_flags, true) == FAIL) {
return FAIL; return FAIL;
} }

View File

@@ -13,7 +13,9 @@
#include "nvim/ascii.h" #include "nvim/ascii.h"
#include "nvim/buffer.h" #include "nvim/buffer.h"
#include "nvim/charset.h" #include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/diff.h" #include "nvim/diff.h"
#include "nvim/edit.h"
#include "nvim/eval.h" #include "nvim/eval.h"
#include "nvim/ex_cmds.h" #include "nvim/ex_cmds.h"
#include "nvim/extmark.h" #include "nvim/extmark.h"
@@ -24,6 +26,7 @@
#include "nvim/memline.h" #include "nvim/memline.h"
#include "nvim/memory.h" #include "nvim/memory.h"
#include "nvim/message.h" #include "nvim/message.h"
#include "nvim/move.h"
#include "nvim/normal.h" #include "nvim/normal.h"
#include "nvim/option.h" #include "nvim/option.h"
#include "nvim/os/input.h" #include "nvim/os/input.h"
@@ -61,7 +64,8 @@ static xfmark_T namedfm[NGLOBALMARKS];
*/ */
int setmark(int c) int setmark(int c)
{ {
return setmark_pos(c, &curwin->w_cursor, curbuf->b_fnum); fmarkv_T view = mark_view_make(curwin->w_topline, curwin->w_cursor);
return setmark_pos(c, &curwin->w_cursor, curbuf->b_fnum, &view);
} }
/// Free fmark_T item /// Free fmark_T item
@@ -90,9 +94,10 @@ void clear_fmark(fmark_T *fm)
* When "c" is upper case use file "fnum". * When "c" is upper case use file "fnum".
* Returns OK on success, FAIL if bad name given. * Returns OK on success, FAIL if bad name given.
*/ */
int setmark_pos(int c, pos_T *pos, int fnum) int setmark_pos(int c, pos_T *pos, int fnum, fmarkv_T *view_pt)
{ {
int i; int i;
fmarkv_T view = view_pt != NULL ? *view_pt : (fmarkv_T)INIT_FMARKV;
// Check for a special key (may cause islower() to crash). // Check for a special key (may cause islower() to crash).
if (c < 0) { if (c < 0) {
@@ -117,7 +122,7 @@ int setmark_pos(int c, pos_T *pos, int fnum)
} }
if (c == '"') { if (c == '"') {
RESET_FMARK(&buf->b_last_cursor, *pos, buf->b_fnum); RESET_FMARK(&buf->b_last_cursor, *pos, buf->b_fnum, view);
return OK; return OK;
} }
@@ -147,7 +152,7 @@ int setmark_pos(int c, pos_T *pos, int fnum)
if (ASCII_ISLOWER(c)) { if (ASCII_ISLOWER(c)) {
i = c - 'a'; i = c - 'a';
RESET_FMARK(buf->b_namedm + i, *pos, fnum); RESET_FMARK(buf->b_namedm + i, *pos, fnum, view);
return OK; return OK;
} }
if (ASCII_ISUPPER(c) || ascii_isdigit(c)) { if (ASCII_ISUPPER(c) || ascii_isdigit(c)) {
@@ -156,7 +161,7 @@ int setmark_pos(int c, pos_T *pos, int fnum)
} else { } else {
i = c - 'A'; i = c - 'A';
} }
RESET_XFMARK(namedfm + i, *pos, fnum, NULL); RESET_XFMARK(namedfm + i, *pos, fnum, view, NULL);
return OK; return OK;
} }
return FAIL; return FAIL;
@@ -202,7 +207,8 @@ void setpcmark(void)
curwin->w_jumplistidx = curwin->w_jumplistlen; curwin->w_jumplistidx = curwin->w_jumplistlen;
fm = &curwin->w_jumplist[curwin->w_jumplistlen - 1]; fm = &curwin->w_jumplist[curwin->w_jumplistlen - 1];
SET_XFMARK(fm, curwin->w_pcmark, curbuf->b_fnum, NULL); fmarkv_T view = mark_view_make(curwin->w_topline, curwin->w_pcmark);
SET_XFMARK(fm, curwin->w_pcmark, curbuf->b_fnum, view, NULL);
} }
/* /*
@@ -221,245 +227,407 @@ void checkpcmark(void)
curwin->w_prev_pcmark.lnum = 0; // it has been checked curwin->w_prev_pcmark.lnum = 0; // it has been checked
} }
/* /// Get mark in "count" position in the |jumplist| relative to the current index.
* move "count" positions in the jump list (count may be negative) ///
*/ /// If the mark is in a different buffer, it will be skipped unless the buffer exists.
pos_T *movemark(int count) ///
/// @note cleanup_jumplist() is run, which removes duplicate marks, and
/// changes win->w_jumplistidx.
/// @param[in] win window to get jumplist from.
/// @param[in] count count to move may be negative.
///
/// @return mark, NULL if out of jumplist bounds.
fmark_T *get_jumplist(win_T *win, int count)
{ {
pos_T *pos; xfmark_T *jmp = NULL;
xfmark_T *jmp;
cleanup_jumplist(curwin, true); cleanup_jumplist(win, true);
if (curwin->w_jumplistlen == 0) { // nothing to jump to if (win->w_jumplistlen == 0) { // nothing to jump to
return (pos_T *)NULL; return NULL;
} }
for (;;) { for (;;) {
if (curwin->w_jumplistidx + count < 0 if (win->w_jumplistidx + count < 0
|| curwin->w_jumplistidx + count >= curwin->w_jumplistlen) { || win->w_jumplistidx + count >= win->w_jumplistlen) {
return (pos_T *)NULL; return NULL;
} }
/* // if first CTRL-O or CTRL-I command after a jump, add cursor position
* if first CTRL-O or CTRL-I command after a jump, add cursor position // to list. Careful: If there are duplicates (CTRL-O immediately after
* to list. Careful: If there are duplicates (CTRL-O immediately after // starting Vim on a file), another entry may have been removed.
* starting Vim on a file), another entry may have been removed. if (win->w_jumplistidx == win->w_jumplistlen) {
*/
if (curwin->w_jumplistidx == curwin->w_jumplistlen) {
setpcmark(); setpcmark();
--curwin->w_jumplistidx; // skip the new entry win->w_jumplistidx--; // skip the new entry
if (curwin->w_jumplistidx + count < 0) { if (win->w_jumplistidx + count < 0) {
return (pos_T *)NULL; return NULL;
} }
} }
curwin->w_jumplistidx += count; win->w_jumplistidx += count;
jmp = curwin->w_jumplist + curwin->w_jumplistidx; jmp = win->w_jumplist + win->w_jumplistidx;
if (jmp->fmark.fnum == 0) { if (jmp->fmark.fnum == 0) {
// Resolve the fnum (buff number) in the mark before returning it (shada)
fname2fnum(jmp); fname2fnum(jmp);
} }
if (jmp->fmark.fnum != curbuf->b_fnum) { if (jmp->fmark.fnum != curbuf->b_fnum) {
// jump to other file // Needs to switch buffer, if it can't find it skip the mark
if (buflist_findnr(jmp->fmark.fnum) == NULL) { // Skip this one .. if (buflist_findnr(jmp->fmark.fnum) == NULL) {
count += count < 0 ? -1 : 1; count += count < 0 ? -1 : 1;
continue; continue;
} }
if (buflist_getfile(jmp->fmark.fnum, jmp->fmark.mark.lnum,
0, FALSE) == FAIL) {
return (pos_T *)NULL;
} }
// Set lnum again, autocommands my have changed it break;
curwin->w_cursor = jmp->fmark.mark;
pos = (pos_T *)-1;
} else {
pos = &(jmp->fmark.mark);
}
return pos;
} }
return jmp != NULL ? &jmp->fmark : NULL;
} }
/* /// Get mark in "count" position in the |changelist| relative to the current index.
* Move "count" positions in the changelist (count may be negative). ///
*/ /// @note Changes the win->w_changelistidx.
pos_T *movechangelist(int count) /// @param[in] win window to get jumplist from.
/// @param[in] count count to move may be negative.
///
/// @return mark, NULL if out of bounds.
fmark_T *get_changelist(buf_T *buf, win_T *win, int count)
{ {
int n; int n;
fmark_T *fm;
if (curbuf->b_changelistlen == 0) { // nothing to jump to if (buf->b_changelistlen == 0) { // nothing to jump to
return (pos_T *)NULL; return NULL;
} }
n = curwin->w_changelistidx; n = win->w_changelistidx;
if (n + count < 0) { if (n + count < 0) {
if (n == 0) { if (n == 0) {
return (pos_T *)NULL; return NULL;
} }
n = 0; n = 0;
} else if (n + count >= curbuf->b_changelistlen) { } else if (n + count >= buf->b_changelistlen) {
if (n == curbuf->b_changelistlen - 1) { if (n == buf->b_changelistlen - 1) {
return (pos_T *)NULL; return NULL;
} }
n = curbuf->b_changelistlen - 1; n = buf->b_changelistlen - 1;
} else { } else {
n += count; n += count;
} }
curwin->w_changelistidx = n; win->w_changelistidx = n;
return &(curbuf->b_changelist[n].mark); fm = &(buf->b_changelist[n]);
// Changelist marks are always buffer local, Shada does not set it when loading
fm->fnum = curbuf->handle;
return &(buf->b_changelist[n]);
} }
/* /// Get a named mark.
* Find mark "c" in buffer pointed to by "buf". ///
* If "changefile" is TRUE it's allowed to edit another file for '0, 'A, etc. /// All types of marks, even those that are not technically a mark will be returned as such. Use
* If "fnum" is not NULL store the fnum there for '0, 'A etc., don't edit /// mark_move_to() to move to the mark.
* another file. /// @note Some of the pointers are statically allocated, if in doubt make a copy. For more
* Returns: /// information read mark_get_local().
* - pointer to pos_T if found. lnum is 0 when mark not set, -1 when mark is /// @param buf Buffer to get the mark from.
* in another file which can't be gotten. (caller needs to check lnum!) /// @param win Window to get or calculate the mark from (motion type marks, context mark).
* - NULL if there is no mark called 'c'. /// @param fmp[out] Optional pointer to store the result in, as a workaround for the note above.
* - -1 if mark is in other file and jumped there (only if changefile is TRUE) /// @param flag MarkGet value
*/ /// @param name Name of the mark.
pos_T *getmark_buf(buf_T *buf, int c, bool changefile) ///
/// @return Mark if found, otherwise NULL. For @c kMarkBufLocal, NULL is returned
/// when no mark is found in @a buf.
fmark_T *mark_get(buf_T *buf, win_T *win, fmark_T *fmp, MarkGet flag, int name)
{ {
return getmark_buf_fnum(buf, c, changefile, NULL); fmark_T *fm = NULL;
} if (ASCII_ISUPPER(name) || ascii_isdigit(name)) {
// Global marks
pos_T *getmark(int c, bool changefile) xfmark_T *xfm = mark_get_global(!(flag & kMarkAllNoResolve), name);
{ fm = &xfm->fmark;
return getmark_buf_fnum(curbuf, c, changefile, NULL); // Only wanted marks belonging to the buffer
} if ((flag & kMarkBufLocal) && xfm->fmark.fnum != buf->handle) {
return NULL;
pos_T *getmark_buf_fnum(buf_T *buf, int c, bool changefile, int *fnum)
{
pos_T *posp;
pos_T *startp, *endp;
static pos_T pos_copy;
posp = NULL;
// Check for special key, can't be a mark name and might cause islower()
// to crash.
if (c < 0) {
return posp;
} }
if (c > '~') { // check for islower()/isupper() } else if (name > 0 && name < NMARK_LOCAL_MAX) {
} else if (c == '\'' || c == '`') { // previous context mark // Local Marks
pos_copy = curwin->w_pcmark; // need to make a copy because fm = mark_get_local(buf, win, name);
posp = &pos_copy; // w_pcmark may be changed soon }
} else if (c == '"') { // to pos when leaving buffer if (fmp != NULL && fm != NULL) {
posp = &(buf->b_last_cursor.mark); *fmp = *fm;
} else if (c == '^') { // to where Insert mode stopped return fmp;
posp = &(buf->b_last_insert.mark); }
} else if (c == '.') { // to where last change was made return fm;
posp = &(buf->b_last_change.mark); }
} else if (c == '[') { // to start of previous operator
posp = &(buf->b_op_start); /// Get a global mark {A-Z0-9}.
} else if (c == ']') { // to end of previous operator ///
posp = &(buf->b_op_end); /// @param name the name of the mark.
} else if (c == '{' || c == '}') { // to previous/next paragraph /// @param resolve Whether to try resolving the mark fnum (i.e., load the buffer stored in
pos_T pos; /// the mark fname and update the xfmark_T (expensive)).
///
/// @return Mark
xfmark_T *mark_get_global(bool resolve, int name)
{
xfmark_T *mark;
if (ascii_isdigit(name)) {
name = name - '0' + NMARKS;
} else if (ASCII_ISUPPER(name)) {
name -= 'A';
} else {
// Not a valid mark name
assert(false);
}
mark = &namedfm[name];
if (resolve && mark->fmark.fnum == 0) {
// Resolve filename to fnum (SHADA marks)
fname2fnum(mark);
}
return mark;
}
/// Get a local mark (lowercase and symbols).
///
/// Some marks are not actually marks, but positions that are never adjusted or motions presented as
/// marks. Search first for marks and fallback to finding motion type marks. If it's known
/// ahead of time that the mark is actually a motion use the mark_get_motion() directly.
///
/// @note Lowercase, last_cursor '"', last insert '^', last change '.' are not statically
/// allocated, everything else is.
/// @param name the name of the mark.
/// @param win window to retrieve marks that belong to it (motions and context mark).
/// @param buf buf to retrieve marks that belong to it.
///
/// @return Mark, NULL if not found.
fmark_T *mark_get_local(buf_T *buf, win_T *win, int name)
{
fmark_T *mark = NULL;
if (ASCII_ISLOWER(name)) {
// normal named mark
mark = &buf->b_namedm[name - 'a'];
// to start of previous operator
} else if (name == '[') {
mark = pos_to_mark(buf, NULL, buf->b_op_start);
// to end of previous operator
} else if (name == ']') {
mark = pos_to_mark(buf, NULL, buf->b_op_end);
// visual marks
} else if (name == '<' || name == '>') {
mark = mark_get_visual(buf, name);
// previous context mark
} else if (name == '\'' || name == '`') {
// TODO(muniter): w_pcmark should be stored as a mark, but causes a nasty bug.
mark = pos_to_mark(curbuf, NULL, win->w_pcmark);
// to position when leaving buffer
} else if (name == '"') {
mark = &(buf->b_last_cursor);
// to where last Insert mode stopped
} else if (name == '^') {
mark = &(buf->b_last_insert);
// to where last change was made
} else if (name == '.') {
mark = &buf->b_last_change;
// Mark that are actually not marks but motions, e.g {, }, (, ), ...
} else {
mark = mark_get_motion(buf, win, name);
}
return mark;
}
/// Get marks that are actually motions but return them as marks
///
/// Gets the following motions as marks: '{', '}', '(', ')'
/// @param name name of the mark
/// @param win window to retrieve the cursor to calculate the mark.
/// @param buf buf to wrap motion marks with it's buffer number (fm->fnum).
///
/// @return[static] Mark.
fmark_T *mark_get_motion(buf_T *buf, win_T *win, int name)
{
fmark_T *mark = NULL;
if (name == '{' || name == '}') {
// to previous/next paragraph
oparg_T oa; oparg_T oa;
bool slcb = listcmd_busy; bool slcb = listcmd_busy;
pos = curwin->w_cursor;
listcmd_busy = true; // avoid that '' is changed listcmd_busy = true; // avoid that '' is changed
if (findpar(&oa.inclusive, if (findpar(&oa.inclusive,
c == '}' ? FORWARD : BACKWARD, 1L, NUL, FALSE)) { name == '}' ? FORWARD : BACKWARD, 1L, NUL, false)) {
pos_copy = curwin->w_cursor; mark = pos_to_mark(buf, NULL, win->w_cursor);
posp = &pos_copy;
} }
curwin->w_cursor = pos;
listcmd_busy = slcb; listcmd_busy = slcb;
} else if (c == '(' || c == ')') { // to previous/next sentence // to previous/next sentence
pos_T pos; } else if (name == '(' || name == ')') {
bool slcb = listcmd_busy; bool slcb = listcmd_busy;
pos = curwin->w_cursor;
listcmd_busy = true; // avoid that '' is changed listcmd_busy = true; // avoid that '' is changed
if (findsent(c == ')' ? FORWARD : BACKWARD, 1L)) {
pos_copy = curwin->w_cursor; if (findsent(name == ')' ? FORWARD : BACKWARD, 1L)) {
posp = &pos_copy; mark = pos_to_mark(buf, NULL, win->w_cursor);
} }
curwin->w_cursor = pos;
listcmd_busy = slcb; listcmd_busy = slcb;
} else if (c == '<' || c == '>') { // start/end of visual area
startp = &buf->b_visual.vi_start;
endp = &buf->b_visual.vi_end;
if (((c == '<') == lt(*startp, *endp) || endp->lnum == 0)
&& startp->lnum != 0) {
posp = startp;
} else {
posp = endp;
} }
return mark;
// For Visual line mode, set mark at begin or end of line
if (buf->b_visual.vi_mode == 'V') {
pos_copy = *posp;
posp = &pos_copy;
if (c == '<') {
pos_copy.col = 0;
} else {
pos_copy.col = MAXCOL;
}
pos_copy.coladd = 0;
}
} else if (ASCII_ISLOWER(c)) { // normal named mark
posp = &(buf->b_namedm[c - 'a'].mark);
} else if (ASCII_ISUPPER(c) || ascii_isdigit(c)) { // named file mark
if (ascii_isdigit(c)) {
c = c - '0' + NMARKS;
} else {
c -= 'A';
}
posp = &(namedfm[c].fmark.mark);
if (namedfm[c].fmark.fnum == 0) {
fname2fnum(&namedfm[c]);
}
if (fnum != NULL) {
*fnum = namedfm[c].fmark.fnum;
} else if (namedfm[c].fmark.fnum != buf->b_fnum) {
// mark is in another file
posp = &pos_copy;
if (namedfm[c].fmark.mark.lnum != 0
&& changefile && namedfm[c].fmark.fnum) {
if (buflist_getfile(namedfm[c].fmark.fnum,
(linenr_T)1, GETF_SETMARK, FALSE) == OK) {
// Set the lnum now, autocommands could have changed it
curwin->w_cursor = namedfm[c].fmark.mark;
return (pos_T *)-1;
}
pos_copy.lnum = -1; // can't get file
} else {
pos_copy.lnum = 0; // mark exists, but is not valid in current buffer
}
}
}
return posp;
} }
/// Search for the next named mark in the current file. /// Get visual marks '<', '>'
/// ///
/// @param startpos where to start /// This marks are different to normal marks:
/// @param dir direction for search /// 1. Never adjusted.
/// 2. Different behavior depending on editor state (visual mode).
/// 3. Not saved in shada.
/// 4. Re-ordered when defined in reverse.
/// @param buf Buffer to get the mark from.
/// @param name Mark name '<' or '>'.
/// ///
/// @return pointer to pos_T of the next mark or NULL if no mark is found. /// @return[static] Mark
pos_T *getnextmark(pos_T *startpos, int dir, int begin_line) fmark_T *mark_get_visual(buf_T *buf, int name)
{
fmark_T *mark = NULL;
if (name == '<' || name == '>') {
// start/end of visual area
pos_T startp = buf->b_visual.vi_start;
pos_T endp = buf->b_visual.vi_end;
if (((name == '<') == lt(startp, endp) || endp.lnum == 0)
&& startp.lnum != 0) {
mark = pos_to_mark(buf, NULL, startp);
} else {
mark = pos_to_mark(buf, NULL, endp);
}
if (mark != NULL && buf->b_visual.vi_mode == 'V') {
if (name == '<') {
mark->mark.col = 0;
} else {
mark->mark.col = MAXCOL;
}
mark->mark.coladd = 0;
}
}
return mark;
}
/// Wrap a pos_T into an fmark_T, used to abstract marks handling.
///
/// Pass an fmp if multiple c
/// @note view fields are set to 0.
/// @param buf for fmark->fnum.
/// @param pos for fmrak->mark.
/// @param fmp pointer to save the mark.
///
/// @return[static] Mark with the given information.
fmark_T *pos_to_mark(buf_T *buf, fmark_T *fmp, pos_T pos)
{
static fmark_T fms = INIT_FMARK;
fmark_T *fm = fmp == NULL ? &fms : fmp;
fm->fnum = buf->handle;
fm->mark = pos;
return fm;
}
/// Attempt to switch to the buffer of the given global mark
///
/// @param fm
/// @param pcmark_on_switch leave a context mark when switching buffer.
/// @return whether the buffer was switched or not.
static MarkMoveRes switch_to_mark_buf(fmark_T *fm, bool pcmark_on_switch)
{
bool res;
if (fm->fnum != curbuf->b_fnum) {
// Switch to another file.
int getfile_flag = pcmark_on_switch ? GETF_SETMARK : 0;
res = buflist_getfile(fm->fnum, (linenr_T)1, getfile_flag, false) == OK;
return res == true ? kMarkSwitchedBuf : kMarkMoveFailed;
}
return 0;
}
/// Move to the given file mark, changing the buffer and cursor position.
///
/// Validate the mark, switch to the buffer, and move the cursor.
/// @param fm Mark, can be NULL will raise E78: Unknown mark
/// @param flags MarkMove flags to configure the movement to the mark.
///
/// @return MarkMovekRes flags representing the outcome
MarkMoveRes mark_move_to(fmark_T *fm, MarkMove flags)
{
static fmark_T fm_copy = INIT_FMARK;
MarkMoveRes res = kMarkMoveSuccess;
if (!mark_check(fm)) {
res = kMarkMoveFailed;
goto end;
}
if (fm->fnum != curbuf->handle) {
// Need to change buffer
fm_copy = *fm; // Copy, autocommand may change it
fm = &fm_copy;
res |= switch_to_mark_buf(fm, !(flags & kMarkJumpList));
// Failed switching buffer
if (res & kMarkMoveFailed) {
goto end;
}
// Check line count now that the **destination buffer is loaded**.
if (!mark_check_line_bounds(curbuf, fm)) {
res |= kMarkMoveFailed;
goto end;
}
} else if (flags & kMarkContext) {
// Doing it in this condition avoids double context mark when switching buffer.
setpcmark();
}
// Move the cursor while keeping track of what changed for the caller
pos_T prev_pos = curwin->w_cursor;
pos_T pos = fm->mark;
curwin->w_cursor = fm->mark;
if (flags & kMarkBeginLine) {
beginline(BL_WHITE | BL_FIX);
}
res = prev_pos.lnum != pos.lnum ? res | kMarkChangedLine | kMarkChangedCursor : res;
res = prev_pos.col != pos.col ? res | kMarkChangedCol | kMarkChangedCursor : res;
if (flags & kMarkSetView) {
mark_view_restore(fm);
}
if (res & kMarkSwitchedBuf || res & kMarkChangedCursor) {
check_cursor();
}
end:
return res;
}
/// Restore the mark view.
/// By remembering the offset between topline and mark lnum at the time of
/// definition, this function restores the "view".
/// @note Assumes the mark has been checked, is valid.
/// @param fm the named mark.
void mark_view_restore(fmark_T *fm)
{
if (fm != NULL && fm->view.topline_offset >= 0) {
linenr_T topline = fm->mark.lnum - fm->view.topline_offset;
if (topline >= 1) {
set_topline(curwin, topline);
}
}
}
fmarkv_T mark_view_make(linenr_T topline, pos_T pos)
{
return (fmarkv_T){ pos.lnum - topline };
}
/// Search for the next named mark in the current file from a start position.
///
/// @param startpos where to start.
/// @param dir direction for search.
///
/// @return next mark or NULL if no mark is found.
fmark_T *getnextmark(pos_T *startpos, int dir, int begin_line)
{ {
int i; int i;
pos_T *result = NULL; fmark_T *result = NULL;
pos_T pos; pos_T pos;
pos = *startpos; pos = *startpos;
// When searching backward and leaving the cursor on the first non-blank,
// position must be in a previous line.
// When searching forward and leaving the cursor on the first non-blank,
// position must be in a next line.
if (dir == BACKWARD && begin_line) { if (dir == BACKWARD && begin_line) {
pos.col = 0; pos.col = 0;
} else if (dir == FORWARD && begin_line) { } else if (dir == FORWARD && begin_line) {
@@ -469,14 +637,14 @@ pos_T *getnextmark(pos_T *startpos, int dir, int begin_line)
for (i = 0; i < NMARKS; i++) { for (i = 0; i < NMARKS; i++) {
if (curbuf->b_namedm[i].mark.lnum > 0) { if (curbuf->b_namedm[i].mark.lnum > 0) {
if (dir == FORWARD) { if (dir == FORWARD) {
if ((result == NULL || lt(curbuf->b_namedm[i].mark, *result)) if ((result == NULL || lt(curbuf->b_namedm[i].mark, result->mark))
&& lt(pos, curbuf->b_namedm[i].mark)) { && lt(pos, curbuf->b_namedm[i].mark)) {
result = &curbuf->b_namedm[i].mark; result = &curbuf->b_namedm[i];
} }
} else { } else {
if ((result == NULL || lt(*result, curbuf->b_namedm[i].mark)) if ((result == NULL || lt(result->mark, curbuf->b_namedm[i].mark))
&& lt(curbuf->b_namedm[i].mark, pos)) { && lt(curbuf->b_namedm[i].mark, pos)) {
result = &curbuf->b_namedm[i].mark; result = &curbuf->b_namedm[i];
} }
} }
} }
@@ -557,29 +725,48 @@ static void fmarks_check_one(xfmark_T *fm, char_u *name, buf_T *buf)
} }
} }
/* /// Check the position in @a fm is valid.
* Check a if a position from a mark is valid. ///
* Give and error message and return FAIL if not. /// Emit error message and return accordingly.
*/ ///
int check_mark(pos_T *pos) /// Checks for:
/// - NULL raising unknown mark error.
/// - Line number <= 0 raising mark not set.
/// - Line number > buffer line count, raising invalid mark.
/// @param fm[in] File mark to check.
///
/// @return true if the mark passes all the above checks, else false.
bool mark_check(fmark_T *fm)
{ {
if (pos == NULL) { if (fm == NULL) {
emsg(_(e_umark)); emsg(_(e_umark));
return FAIL; return false;
} } else if (fm->mark.lnum <= 0) {
if (pos->lnum <= 0) { // In both cases it's an error but only raise when equals to 0
// lnum is negative if mark is in another file can can't get that if (fm->mark.lnum == 0) {
// file, error message already give then.
if (pos->lnum == 0) {
emsg(_(e_marknotset)); emsg(_(e_marknotset));
} }
return FAIL; return false;
} }
if (pos->lnum > curbuf->b_ml.ml_line_count) { // Only check for valid line number if the buffer is loaded.
if (fm->fnum == curbuf->handle && !mark_check_line_bounds(curbuf, fm)) {
return false;
}
return true;
}
/// Check if a mark line number is greater than the buffer line count, and set e_markinval.
/// @note Should be done after the buffer is loaded into memory.
/// @param buf Buffer where the mark is set.
/// @param fm Mark to check.
/// @return true if below line count else false.
bool mark_check_line_bounds(buf_T *buf, fmark_T *fm)
{
if (buf != NULL && fm->mark.lnum > buf->b_ml.ml_line_count) {
emsg(_(e_markinval)); emsg(_(e_markinval));
return FAIL; return false;
} }
return OK; return true;
} }
/// Clear all marks and change list in the given buffer /// Clear all marks and change list in the given buffer
@@ -1318,7 +1505,7 @@ void copy_jumplist(win_T *from, win_T *to)
/// Iterate over jumplist items /// Iterate over jumplist items
/// ///
/// @warning No jumplist-editing functions must be run while iteration is in /// @warning No jumplist-editing functions must be called while iteration is in
/// progress. /// progress.
/// ///
/// @param[in] iter Iterator. Pass NULL to start iteration. /// @param[in] iter Iterator. Pass NULL to start iteration.
@@ -1331,7 +1518,7 @@ const void *mark_jumplist_iter(const void *const iter, const win_T *const win, x
FUNC_ATTR_NONNULL_ARG(2, 3) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(2, 3) FUNC_ATTR_WARN_UNUSED_RESULT
{ {
if (iter == NULL && win->w_jumplistlen == 0) { if (iter == NULL && win->w_jumplistlen == 0) {
*fm = (xfmark_T) { { { 0, 0, 0 }, 0, 0, NULL }, NULL }; *fm = (xfmark_T)INIT_XFMARK;
return NULL; return NULL;
} }
const xfmark_T *const iter_mark = const xfmark_T *const iter_mark =
@@ -1348,7 +1535,7 @@ const void *mark_jumplist_iter(const void *const iter, const win_T *const win, x
/// Iterate over global marks /// Iterate over global marks
/// ///
/// @warning No mark-editing functions must be run while iteration is in /// @warning No mark-editing functions must be called while iteration is in
/// progress. /// progress.
/// ///
/// @param[in] iter Iterator. Pass NULL to start iteration. /// @param[in] iter Iterator. Pass NULL to start iteration.
@@ -1422,7 +1609,7 @@ static inline const fmark_T *next_buffer_mark(const buf_T *const buf, char *cons
/// Iterate over buffer marks /// Iterate over buffer marks
/// ///
/// @warning No mark-editing functions must be run while iteration is in /// @warning No mark-editing functions must be called while iteration is in
/// progress. /// progress.
/// ///
/// @param[in] iter Iterator. Pass NULL to start iteration. /// @param[in] iter Iterator. Pass NULL to start iteration.
@@ -1539,7 +1726,7 @@ void free_jumplist(win_T *wp)
void set_last_cursor(win_T *win) void set_last_cursor(win_T *win)
{ {
if (win->w_buffer != NULL) { if (win->w_buffer != NULL) {
RESET_FMARK(&win->w_buffer->b_last_cursor, win->w_cursor, 0); RESET_FMARK(&win->w_buffer->b_last_cursor, win->w_cursor, 0, ((fmarkv_T)INIT_FMARKV));
} }
} }
@@ -1640,9 +1827,10 @@ void get_buf_local_marks(const buf_T *buf, list_T *l)
/// Get a global mark /// Get a global mark
/// ///
/// @note Mark might not have it's fnum resolved.
/// @param[in] Name of named mark /// @param[in] Name of named mark
/// @param[out] Global/file mark /// @param[out] Global/file mark
xfmark_T get_global_mark(char name) xfmark_T get_raw_global_mark(char name)
{ {
return namedfm[mark_global_index(name)]; return namedfm[mark_global_index(name)];
} }

View File

@@ -13,42 +13,43 @@
#include "nvim/pos.h" #include "nvim/pos.h"
/// Set fmark using given value /// Set fmark using given value
#define SET_FMARK(fmarkp_, mark_, fnum_) \ #define SET_FMARK(fmarkp_, mark_, fnum_, view_) \
do { \ do { \
fmark_T *const fmarkp__ = fmarkp_; \ fmark_T *const fmarkp__ = fmarkp_; \
fmarkp__->mark = mark_; \ fmarkp__->mark = mark_; \
fmarkp__->fnum = fnum_; \ fmarkp__->fnum = fnum_; \
fmarkp__->timestamp = os_time(); \ fmarkp__->timestamp = os_time(); \
fmarkp__->view = view_; \
fmarkp__->additional_data = NULL; \ fmarkp__->additional_data = NULL; \
} while (0) } while (0)
/// Free and set fmark using given value /// Free and set fmark using given value
#define RESET_FMARK(fmarkp_, mark_, fnum_) \ #define RESET_FMARK(fmarkp_, mark_, fnum_, view_) \
do { \ do { \
fmark_T *const fmarkp___ = fmarkp_; \ fmark_T *const fmarkp___ = fmarkp_; \
free_fmark(*fmarkp___); \ free_fmark(*fmarkp___); \
SET_FMARK(fmarkp___, mark_, fnum_); \ SET_FMARK(fmarkp___, mark_, fnum_, view_); \
} while (0) } while (0)
/// Clear given fmark /// Clear given fmark
#define CLEAR_FMARK(fmarkp_) \ #define CLEAR_FMARK(fmarkp_) \
RESET_FMARK(fmarkp_, ((pos_T) { 0, 0, 0 }), 0) RESET_FMARK(fmarkp_, ((pos_T) { 0, 0, 0 }), 0, ((fmarkv_T) { 0 }))
/// Set given extended mark (regular mark + file name) /// Set given extended mark (regular mark + file name)
#define SET_XFMARK(xfmarkp_, mark_, fnum_, fname_) \ #define SET_XFMARK(xfmarkp_, mark_, fnum_, view_, fname_) \
do { \ do { \
xfmark_T *const xfmarkp__ = xfmarkp_; \ xfmark_T *const xfmarkp__ = xfmarkp_; \
xfmarkp__->fname = fname_; \ xfmarkp__->fname = fname_; \
SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_); \ SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_, view_); \
} while (0) } while (0)
/// Free and set given extended mark (regular mark + file name) /// Free and set given extended mark (regular mark + file name)
#define RESET_XFMARK(xfmarkp_, mark_, fnum_, fname_) \ #define RESET_XFMARK(xfmarkp_, mark_, fnum_, view_, fname_) \
do { \ do { \
xfmark_T *const xfmarkp__ = xfmarkp_; \ xfmark_T *const xfmarkp__ = xfmarkp_; \
free_xfmark(*xfmarkp__); \ free_xfmark(*xfmarkp__); \
xfmarkp__->fname = fname_; \ xfmarkp__->fname = fname_; \
SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_); \ SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_, view_); \
} while (0) } while (0)
/// Convert mark name to the offset /// Convert mark name to the offset

View File

@@ -10,6 +10,33 @@
* (a normal mark is a lnum/col pair, the same as a file position) * (a normal mark is a lnum/col pair, the same as a file position)
*/ */
/// Flags for outcomes when moving to a mark.
typedef enum {
kMarkMoveSuccess = 1, ///< Successful move.
kMarkMoveFailed = 2, ///< Failed to move.
kMarkSwitchedBuf = 4, ///< Switched curbuf.
kMarkChangedCol = 8, ///< Changed the cursor col.
kMarkChangedLine = 16, ///< Changed the cursor line.
kMarkChangedCursor = 32, ///< Changed the cursor.
kMarkChangedView = 64, ///< Changed the view.
} MarkMoveRes;
/// Flags to configure the movement to a mark.
typedef enum {
kMarkBeginLine = 1, ///< Move cursor to the beginning of the line.
kMarkContext = 2, ///< Leave context mark when moving the cursor.
KMarkNoContext = 4, ///< Don't leave a context mark.
kMarkSetView = 8, ///< Set the mark view after moving
kMarkJumpList = 16, ///< Special case, don't leave context mark when switching buffer
} MarkMove;
/// Options when getting a mark
typedef enum {
kMarkBufLocal, ///< Only return marks that belong to the buffer.
kMarkAll, ///< Return all types of marks.
kMarkAllNoResolve, ///< Return all types of marks but don't resolve fnum (global marks).
} MarkGet;
/// Number of possible numbered global marks /// Number of possible numbered global marks
#define EXTRA_MARKS ('9' - '0' + 1) #define EXTRA_MARKS ('9' - '0' + 1)
@@ -25,24 +52,39 @@
/// but they are not saved in ShaDa files. /// but they are not saved in ShaDa files.
#define NLOCALMARKS (NMARKS + 3) #define NLOCALMARKS (NMARKS + 3)
/// Max value of local mark
#define NMARK_LOCAL_MAX 126 // Index of '~'
/// Maximum number of marks in jump list /// Maximum number of marks in jump list
#define JUMPLISTSIZE 100 #define JUMPLISTSIZE 100
/// Maximum number of tags in tag stack /// Maximum number of tags in tag stack
#define TAGSTACKSIZE 20 #define TAGSTACKSIZE 20
/// Represents view in which the mark was created
typedef struct fmarkv {
linenr_T topline_offset; ///< Amount of lines from the mark lnum to the top of the window.
} fmarkv_T;
#define INIT_FMARKV { 0 }
/// Structure defining single local mark /// Structure defining single local mark
typedef struct filemark { typedef struct filemark {
pos_T mark; ///< Cursor position. pos_T mark; ///< Cursor position.
int fnum; ///< File number. int fnum; ///< File number.
Timestamp timestamp; ///< Time when this mark was last set. Timestamp timestamp; ///< Time when this mark was last set.
fmarkv_T view; ///< View the mark was created on
dict_T *additional_data; ///< Additional data from ShaDa file. dict_T *additional_data; ///< Additional data from ShaDa file.
} fmark_T; } fmark_T;
#define INIT_FMARK { { 0, 0, 0 }, 0, 0, INIT_FMARKV, NULL }
/// Structure defining extended mark (mark with file name attached) /// Structure defining extended mark (mark with file name attached)
typedef struct xfilemark { typedef struct xfilemark {
fmark_T fmark; ///< Actual mark. fmark_T fmark; ///< Actual mark.
char *fname; ///< File name, used when fnum == 0. char *fname; ///< File name, used when fnum == 0.
} xfmark_T; } xfmark_T;
#define INIT_XFMARK { INIT_FMARK, NULL }
#endif // NVIM_MARK_DEFS_H #endif // NVIM_MARK_DEFS_H

View File

@@ -4983,19 +4983,22 @@ static void nv_brackets(cmdarg_T *cap)
nv_put_opt(cap, true); nv_put_opt(cap, true);
} else if (cap->nchar == '\'' || cap->nchar == '`') { } else if (cap->nchar == '\'' || cap->nchar == '`') {
// "['", "[`", "]'" and "]`": jump to next mark // "['", "[`", "]'" and "]`": jump to next mark
fmark_T *fm = NULL;
pos = &curwin->w_cursor; pos = &curwin->w_cursor;
for (n = cap->count1; n > 0; n--) { for (n = cap->count1; n > 0; n--) {
prev_pos = *pos; prev_pos = *pos;
pos = getnextmark(pos, cap->cmdchar == '[' ? BACKWARD : FORWARD, fm = getnextmark(pos, cap->cmdchar == '[' ? BACKWARD : FORWARD,
cap->nchar == '\''); cap->nchar == '\'');
if (pos == NULL) { if (pos == NULL) {
break; break;
} else {
pos = fm != NULL ? &fm->mark : NULL; // Adjust for the next iteration.
} }
} }
if (pos == NULL) { fm = fm == NULL ? pos_to_mark(curbuf, NULL, curwin->w_cursor) : fm;
pos = &prev_pos; MarkMove flags = kMarkContext;
} flags |= cap->nchar == '\'' ? kMarkBeginLine: 0;
nv_cursormark(cap, cap->nchar == '\'', pos); nv_mark_move_to(cap, flags, fm);
} else if (cap->nchar >= K_RIGHTRELEASE && cap->nchar <= K_LEFTMOUSE) { } else if (cap->nchar >= K_RIGHTRELEASE && cap->nchar <= K_LEFTMOUSE) {
// [ or ] followed by a middle mouse click: put selected text with // [ or ] followed by a middle mouse click: put selected text with
// indent adjustment. Any other button just does as usual. // indent adjustment. Any other button just does as usual.
@@ -5465,31 +5468,28 @@ static void n_swapchar(cmdarg_T *cap)
} }
} }
/// Move cursor to mark. /// Move the cursor to the mark position
static void nv_cursormark(cmdarg_T *cap, int flag, pos_T *pos) ///
/// Wrapper to mark_move_to() that also handles normal mode command arguments.
/// @note It will switch the buffer if neccesarry, move the cursor and set the
/// view depending on the given flags.
/// @param cap command line arguments
/// @param flags for mark_move_to()
/// @param mark mark
/// @return The result of calling mark_move_to()
static MarkMoveRes nv_mark_move_to(cmdarg_T *cap, MarkMove flags, fmark_T *fm)
{ {
if (check_mark(pos) == false) { MarkMoveRes res = mark_move_to(fm, flags);
if (res & kMarkMoveFailed) {
clearop(cap->oap); clearop(cap->oap);
} else {
if (cap->cmdchar == '\''
|| cap->cmdchar == '`'
|| cap->cmdchar == '['
|| cap->cmdchar == ']') {
setpcmark();
} }
curwin->w_cursor = *pos; cap->oap->motion_type = flags & kMarkBeginLine ? kMTLineWise : kMTCharWise;
if (flag) {
beginline(BL_WHITE | BL_FIX);
} else {
check_cursor();
}
}
cap->oap->motion_type = flag ? kMTLineWise : kMTCharWise;
if (cap->cmdchar == '`') { if (cap->cmdchar == '`') {
cap->oap->use_reg_one = true; cap->oap->use_reg_one = true;
} }
cap->oap->inclusive = false; // ignored if not kMTCharWise cap->oap->inclusive = false; // ignored if not kMTCharWise
curwin->w_set_curswant = true; curwin->w_set_curswant = true;
return res;
} }
/// Handle commands that are operators in Visual mode. /// Handle commands that are operators in Visual mode.
@@ -5564,36 +5564,32 @@ static void nv_optrans(cmdarg_T *cap)
/// cap->arg is true for "'" and "g'". /// cap->arg is true for "'" and "g'".
static void nv_gomark(cmdarg_T *cap) static void nv_gomark(cmdarg_T *cap)
{ {
pos_T *pos; int name;
int c; MarkMove flags = jop_flags & JOP_VIEW ? kMarkSetView : 0; // flags for moving to the mark
pos_T old_cursor = curwin->w_cursor; MarkMoveRes move_res = 0; // Result from moving to the mark
const bool old_KeyTyped = KeyTyped; // getting file may reset it const bool old_KeyTyped = KeyTyped; // getting file may reset it
if (cap->cmdchar == 'g') { if (cap->cmdchar == 'g') {
c = cap->extra_char; name = cap->extra_char;
flags |= KMarkNoContext;
} else { } else {
c = cap->nchar; name = cap->nchar;
} flags |= kMarkContext;
pos = getmark(c, (cap->oap->op_type == OP_NOP));
if (pos == (pos_T *)-1) { // jumped to other file
if (cap->arg) {
check_cursor_lnum();
beginline(BL_WHITE | BL_FIX);
} else {
check_cursor();
}
} else {
nv_cursormark(cap, cap->arg, pos);
} }
flags |= cap->arg ? kMarkBeginLine : 0;
flags |= cap->count0 ? kMarkSetView : 0;
fmark_T *fm = mark_get(curbuf, curwin, NULL, kMarkAll, name);
move_res = nv_mark_move_to(cap, flags, fm);
// May need to clear the coladd that a mark includes. // May need to clear the coladd that a mark includes.
if (!virtual_active()) { if (!virtual_active()) {
curwin->w_cursor.coladd = 0; curwin->w_cursor.coladd = 0;
} }
check_cursor_col();
if (cap->oap->op_type == OP_NOP if (cap->oap->op_type == OP_NOP
&& pos != NULL && move_res & kMarkMoveSuccess
&& (pos == (pos_T *)-1 || !equalpos(old_cursor, *pos)) && (move_res & kMarkSwitchedBuf || move_res & kMarkChangedCursor)
&& (fdo_flags & FDO_MARK) && (fdo_flags & FDO_MARK)
&& old_KeyTyped) { && old_KeyTyped) {
foldOpenCursor(); foldOpenCursor();
@@ -5601,11 +5597,13 @@ static void nv_gomark(cmdarg_T *cap)
} }
/// Handle CTRL-O, CTRL-I, "g;", "g,", and "CTRL-Tab" commands. /// Handle CTRL-O, CTRL-I, "g;", "g,", and "CTRL-Tab" commands.
/// Movement in the jumplist and changelist.
static void nv_pcmark(cmdarg_T *cap) static void nv_pcmark(cmdarg_T *cap)
{ {
pos_T *pos; fmark_T *fm = NULL;
linenr_T lnum = curwin->w_cursor.lnum; MarkMove flags = jop_flags & JOP_VIEW ? kMarkSetView : 0; // flags for moving to the mark
const bool old_KeyTyped = KeyTyped; // getting file may reset it MarkMoveRes move_res = 0; // Result from moving to the mark
const bool old_KeyTyped = KeyTyped; // getting file may reset it.
if (!checkclearopq(cap->oap)) { if (!checkclearopq(cap->oap)) {
if (cap->cmdchar == TAB && mod_mask == MOD_MASK_CTRL) { if (cap->cmdchar == TAB && mod_mask == MOD_MASK_CTRL) {
@@ -5614,16 +5612,18 @@ static void nv_pcmark(cmdarg_T *cap)
} }
return; return;
} }
if (cap->cmdchar == 'g') { if (cap->cmdchar == 'g') {
pos = movechangelist((int)cap->count1); fm = get_changelist(curbuf, curwin, (int)cap->count1);
} else { } else {
pos = movemark((int)cap->count1); fm = get_jumplist(curwin, (int)cap->count1);
flags |= KMarkNoContext | kMarkJumpList;
} }
if (pos == (pos_T *)-1) { // jump to other file // Changelist and jumplist have their own error messages. Therefore avoid
curwin->w_set_curswant = true; // calling nv_mark_move_to() when not found to avoid incorrect error
check_cursor(); // messages.
} else if (pos != NULL) { // can jump if (fm != NULL) {
nv_cursormark(cap, false, pos); move_res = nv_mark_move_to(cap, flags, fm);
} else if (cap->cmdchar == 'g') { } else if (cap->cmdchar == 'g') {
if (curbuf->b_changelistlen == 0) { if (curbuf->b_changelistlen == 0) {
emsg(_("E664: changelist is empty")); emsg(_("E664: changelist is empty"));
@@ -5636,7 +5636,7 @@ static void nv_pcmark(cmdarg_T *cap)
clearopbeep(cap->oap); clearopbeep(cap->oap);
} }
if (cap->oap->op_type == OP_NOP if (cap->oap->op_type == OP_NOP
&& (pos == (pos_T *)-1 || lnum != curwin->w_cursor.lnum) && (move_res & kMarkSwitchedBuf || move_res & kMarkChangedLine)
&& (fdo_flags & FDO_MARK) && (fdo_flags & FDO_MARK)
&& old_KeyTyped) { && old_KeyTyped) {
foldOpenCursor(); foldOpenCursor();
@@ -6804,7 +6804,6 @@ void set_cursor_for_append_to_line(void)
curwin->w_set_curswant = true; curwin->w_set_curswant = true;
if (get_ve_flags() == VE_ALL) { if (get_ve_flags() == VE_ALL) {
const int save_State = State; const int save_State = State;
// Pretend Insert mode here to allow the cursor on the // Pretend Insert mode here to allow the cursor on the
// character past the end of the line // character past the end of the line
State = MODE_INSERT; State = MODE_INSERT;

View File

@@ -491,9 +491,10 @@ EXTERN int p_js; // 'joinspaces'
EXTERN char_u *p_jop; // 'jumpooptions' EXTERN char_u *p_jop; // 'jumpooptions'
EXTERN unsigned jop_flags; EXTERN unsigned jop_flags;
#ifdef IN_OPTION_C #ifdef IN_OPTION_C
static char *(p_jop_values[]) = { "stack", NULL }; static char *(p_jop_values[]) = { "stack", "view", NULL };
#endif #endif
#define JOP_STACK 0x01 #define JOP_STACK 0x01
#define JOP_VIEW 0x02
EXTERN char_u *p_kp; // 'keywordprg' EXTERN char_u *p_kp; // 'keywordprg'
EXTERN char_u *p_km; // 'keymodel' EXTERN char_u *p_km; // 'keymodel'
EXTERN char_u *p_langmap; // 'langmap' EXTERN char_u *p_langmap; // 'langmap'

View File

@@ -3706,7 +3706,8 @@ static bool regmatch(char_u *scan, proftime_T *tm, int *timed_out)
pos_T *pos; pos_T *pos;
size_t col = REG_MULTI ? rex.input - rex.line : 0; size_t col = REG_MULTI ? rex.input - rex.line : 0;
pos = getmark_buf(rex.reg_buf, mark, false); // fm will be NULL if the mark is not set in reg_buf
fmark_T *fm = mark_get(rex.reg_buf, curwin, NULL, kMarkBufLocal, mark);
// Line may have been freed, get it again. // Line may have been freed, get it again.
if (REG_MULTI) { if (REG_MULTI) {
@@ -3714,10 +3715,11 @@ static bool regmatch(char_u *scan, proftime_T *tm, int *timed_out)
rex.input = rex.line + col; rex.input = rex.line + col;
} }
if (pos == NULL // mark doesn't exist if (fm == NULL // mark doesn't exist
|| pos->lnum <= 0) { // mark isn't set in reg_buf || fm->mark.lnum <= 0) { // mark isn't set in reg_buf
status = RA_NOMATCH; status = RA_NOMATCH;
} else { } else {
pos = &fm->mark;
const colnr_T pos_col = pos->lnum == rex.lnum + rex.reg_firstlnum const colnr_T pos_col = pos->lnum == rex.lnum + rex.reg_firstlnum
&& pos->col == MAXCOL && pos->col == MAXCOL
? (colnr_T)STRLEN(reg_getline(pos->lnum - rex.reg_firstlnum)) ? (colnr_T)STRLEN(reg_getline(pos->lnum - rex.reg_firstlnum))

View File

@@ -6930,10 +6930,10 @@ static int nfa_regmatch(nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *subm
case NFA_MARK: case NFA_MARK:
case NFA_MARK_GT: case NFA_MARK_GT:
case NFA_MARK_LT: { case NFA_MARK_LT: {
pos_T *pos; fmark_T *fm;
size_t col = REG_MULTI ? rex.input - rex.line : 0; size_t col = REG_MULTI ? rex.input - rex.line : 0;
// fm will be NULL if the mark is not set, doesn't belong to reg_buf
pos = getmark_buf(rex.reg_buf, t->state->val, false); fm = mark_get(rex.reg_buf, curwin, NULL, kMarkBufLocal, t->state->val);
// Line may have been freed, get it again. // Line may have been freed, get it again.
if (REG_MULTI) { if (REG_MULTI) {
@@ -6943,7 +6943,8 @@ static int nfa_regmatch(nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *subm
// Compare the mark position to the match position, if the mark // Compare the mark position to the match position, if the mark
// exists and mark is set in reg_buf. // exists and mark is set in reg_buf.
if (pos != NULL && pos->lnum > 0) { if (fm != NULL && fm->mark.lnum > 0) {
pos_T *pos = &fm->mark;
const colnr_T pos_col = pos->lnum == rex.lnum + rex.reg_firstlnum const colnr_T pos_col = pos->lnum == rex.lnum + rex.reg_firstlnum
&& pos->col == MAXCOL && pos->col == MAXCOL
? (colnr_T)STRLEN(reg_getline(pos->lnum - rex.reg_firstlnum)) ? (colnr_T)STRLEN(reg_getline(pos->lnum - rex.reg_firstlnum))

View File

@@ -1331,8 +1331,9 @@ static void shada_read(ShaDaReadDef *const sd_reader, const int flags)
buflist_new((char_u *)cur_entry.data.buffer_list.buffers[i].fname, (char_u *)sfname, 0, buflist_new((char_u *)cur_entry.data.buffer_list.buffers[i].fname, (char_u *)sfname, 0,
BLN_LISTED); BLN_LISTED);
if (buf != NULL) { if (buf != NULL) {
fmarkv_T view = INIT_FMARKV;
RESET_FMARK(&buf->b_last_cursor, RESET_FMARK(&buf->b_last_cursor,
cur_entry.data.buffer_list.buffers[i].pos, 0); cur_entry.data.buffer_list.buffers[i].pos, 0, view);
buflist_setfpos(buf, curwin, buf->b_last_cursor.mark.lnum, buflist_setfpos(buf, curwin, buf->b_last_cursor.mark.lnum,
buf->b_last_cursor.mark.col, false); buf->b_last_cursor.mark.col, false);
buf->additional_data = buf->additional_data =

View File

@@ -116,7 +116,7 @@ static char_u *tagmatchname = NULL; // name of last used tag
* Tag for preview window is remembered separately, to avoid messing up the * Tag for preview window is remembered separately, to avoid messing up the
* normal tagstack. * normal tagstack.
*/ */
static taggy_T ptag_entry = { NULL, { { 0, 0, 0 }, 0, 0, NULL }, 0, 0, NULL }; static taggy_T ptag_entry = { NULL, INIT_FMARK, 0, 0, NULL };
static int tfu_in_use = false; // disallow recursive call of tagfunc static int tfu_in_use = false; // disallow recursive call of tagfunc

View File

@@ -1,4 +1,5 @@
local helpers = require('test.functional.helpers')(after_each) local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local clear = helpers.clear local clear = helpers.clear
local command = helpers.command local command = helpers.command
@@ -7,6 +8,7 @@ local funcs = helpers.funcs
local feed = helpers.feed local feed = helpers.feed
local exec_capture = helpers.exec_capture local exec_capture = helpers.exec_capture
local write_file = helpers.write_file local write_file = helpers.write_file
local curbufmeths = helpers.curbufmeths
describe('jumplist', function() describe('jumplist', function()
local fname1 = 'Xtest-functional-normal-jump' local fname1 = 'Xtest-functional-normal-jump'
@@ -137,3 +139,116 @@ describe("jumpoptions=stack behaves like 'tagstack'", function()
exec_capture('jumps')) exec_capture('jumps'))
end) end)
end) end)
describe("jumpoptions=view", function()
local file1 = 'Xtestfile-functional-editor-jumps'
local file2 = 'Xtestfile-functional-editor-jumps-2'
local function content()
local c = {}
for i=1,30 do
c[i] = i .. " line"
end
return table.concat(c, "\n")
end
before_each(function()
clear()
write_file(file1, content(), false, false)
write_file(file2, content(), false, false)
command('set jumpoptions=view')
end)
after_each(function()
os.remove(file1)
os.remove(file2)
end)
it('restores the view', function()
local screen = Screen.new(5, 8)
screen:attach()
command("edit " .. file1)
feed("12Gztj")
feed("gg<C-o>")
screen:expect([[
12 line |
^13 line |
14 line |
15 line |
16 line |
17 line |
18 line |
|
]])
end)
it('restores the view across files', function()
local screen = Screen.new(5, 5)
screen:attach()
command("args " .. file1 .. " " .. file2)
feed("12Gzt")
command("next")
feed("G")
screen:expect([[
27 line |
28 line |
29 line |
^30 line |
|
]])
feed("<C-o><C-o>")
screen:expect([[
^12 line |
13 line |
14 line |
15 line |
|
]])
end)
it('restores the view across files with <C-^>', function()
local screen = Screen.new(5, 5)
screen:attach()
command("args " .. file1 .. " " .. file2)
feed("12Gzt")
command("next")
feed("G")
screen:expect([[
27 line |
28 line |
29 line |
^30 line |
|
]])
feed("<C-^>")
screen:expect([[
^12 line |
13 line |
14 line |
15 line |
|
]])
end)
it('falls back to standard behavior when view can\'t be recovered', function()
local screen = Screen.new(5, 8)
screen:attach()
command("edit " .. file1)
feed("7GzbG")
curbufmeths.set_lines(0, 2, true, {})
-- Move to line 7, and set it as the last line visible on the view with zb, meaning to recover
-- the view it needs to put the cursor 7 lines from the top line. Then go to the end of the
-- file, delete 2 lines before line 7, meaning the jump/mark is moved 2 lines up to line 5.
-- Therefore when trying to jump back to it it's not possible to set a 7 line offset from the
-- mark position to the top line, since there's only 5 lines from the mark position to line 0.
-- Therefore falls back to standard behavior which is centering the view/line.
feed("<C-o>")
screen:expect([[
4 line |
5 line |
6 line |
^7 line |
8 line |
9 line |
10 line |
|
]])
end)
end)

View File

@@ -0,0 +1,380 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local meths = helpers.meths
local curbufmeths = helpers.curbufmeths
local clear = helpers.clear
local command = helpers.command
local funcs = helpers.funcs
local eq = helpers.eq
local feed = helpers.feed
local write_file = helpers.write_file
local pcall_err = helpers.pcall_err
local cursor = function() return helpers.meths.win_get_cursor(0) end
describe('named marks', function()
local file1 = 'Xtestfile-functional-editor-marks'
local file2 = 'Xtestfile-functional-editor-marks-2'
before_each(function()
clear()
write_file(file1, '1test1\n1test2\n1test3\n1test4', false, false)
write_file(file2, '2test1\n2test2\n2test3\n2test4', false, false)
end)
after_each(function()
os.remove(file1)
os.remove(file2)
end)
it("can be set", function()
command("edit " .. file1)
command("mark a")
eq({1, 0}, curbufmeths.get_mark("a"))
feed("jmb")
eq({2, 0}, curbufmeths.get_mark("b"))
feed("jmB")
eq({3, 0}, curbufmeths.get_mark("B"))
command("4kc")
eq({4, 0}, curbufmeths.get_mark("c"))
end)
it("errors when set out of range with :mark", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "1000mark x")
eq("Vim(mark):E16: Invalid range: 1000mark x", err)
end)
it("errors when set out of range with :k", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "1000kx")
eq("Vim(k):E16: Invalid range: 1000kx", err)
end)
it("errors on unknown mark name with :mark", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "mark #")
eq("Vim(mark):E191: Argument must be a letter or forward/backward quote", err)
end)
it("errors on unknown mark name with '", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "normal! '#")
eq("Vim(normal):E78: Unknown mark", err)
end)
it("errors on unknown mark name with `", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "normal! `#")
eq("Vim(normal):E78: Unknown mark", err)
end)
it("errors when moving to a mark that is not set with '", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "normal! 'z")
eq("Vim(normal):E20: Mark not set", err)
err = pcall_err(helpers.exec_capture, "normal! '.")
eq("Vim(normal):E20: Mark not set", err)
end)
it("errors when moving to a mark that is not set with `", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "normal! `z")
eq("Vim(normal):E20: Mark not set", err)
err = pcall_err(helpers.exec_capture, "normal! `>")
eq("Vim(normal):E20: Mark not set", err)
end)
it("errors when moving to a global mark that is not set with '", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "normal! 'Z")
eq("Vim(normal):E20: Mark not set", err)
end)
it("errors when moving to a global mark that is not set with `", function()
command("edit " .. file1)
local err = pcall_err(helpers.exec_capture, "normal! `Z")
eq("Vim(normal):E20: Mark not set", err)
end)
it("can move to them using '", function()
command("args " .. file1 .. " " .. file2)
feed("j")
feed("ma")
feed("G'a")
eq({2, 0}, cursor())
feed("mA")
command("next")
feed("'A")
eq(1, meths.get_current_buf().id)
eq({2, 0}, cursor())
end)
it("can move to them using `", function()
command("args " .. file1 .. " " .. file2)
feed("jll")
feed("ma")
feed("G`a")
eq({2, 2}, cursor())
feed("mA")
command("next")
feed("`A")
eq(1, meths.get_current_buf().id)
eq({2, 2}, cursor())
end)
it("can move to them using g'", function()
command("args " .. file1 .. " " .. file2)
feed("jll")
feed("ma")
feed("Gg'a")
eq({2, 0}, cursor())
feed("mA")
command("next")
feed("g'A")
eq(1, meths.get_current_buf().id)
eq({2, 0}, cursor())
end)
it("can move to them using g`", function()
command("args " .. file1 .. " " .. file2)
feed("jll")
feed("ma")
feed("Gg`a")
eq({2, 2}, cursor())
feed("mA")
command("next")
feed("g`A")
eq(1, meths.get_current_buf().id)
eq({2, 2}, cursor())
end)
it("errors when it can't find the buffer", function()
command("args " .. file1 .. " " .. file2)
feed("mA")
command("next")
command("bw! " .. file1 )
local err = pcall_err(helpers.exec_capture, "normal! 'A")
eq("Vim(normal):E92: Buffer 1 not found", err)
os.remove(file1)
end)
it("leave a context mark when moving with '", function()
command("edit " .. file1)
feed("llmamA")
feed("10j0") -- first col, last line
local pos = cursor()
feed("'a")
feed("<C-o>")
eq(pos, cursor())
feed("'A")
feed("<C-o>")
eq(pos, cursor())
end)
it("leave a context mark when moving with `", function()
command("edit " .. file1)
feed("llmamA")
feed("10j0") -- first col, last line
local pos = cursor()
feed("`a")
feed("<C-o>")
eq(pos, cursor())
feed("`A")
feed("<C-o>")
eq(pos, cursor())
end)
it("leave a context mark when the mark changes buffer with g'", function()
command("args " .. file1 .. " " .. file2)
local pos
feed("GmA")
command("next")
pos = cursor()
command("clearjumps")
feed("g'A") -- since the mark is in another buffer, it leaves a context mark
feed("<C-o>")
eq(pos, cursor())
end)
it("leave a context mark when the mark changes buffer with g`", function()
command("args " .. file1 .. " " .. file2)
local pos
feed("GmA")
command("next")
pos = cursor()
command("clearjumps")
feed("g`A") -- since the mark is in another buffer, it leaves a context mark
feed("<C-o>")
eq(pos, cursor())
end)
it("do not leave a context mark when moving with g'", function()
command("edit " .. file1)
local pos
feed("ma")
pos = cursor() -- Mark pos
feed("10j0") -- first col, last line
feed("g'a")
feed("<C-o>") -- should do nothing
eq(pos, cursor())
feed("mA")
pos = cursor() -- Mark pos
feed("10j0") -- first col, last line
feed("g'a")
feed("<C-o>") -- should do nothing
eq(pos, cursor())
end)
it("do not leave a context mark when moving with g`", function()
command("edit " .. file1)
local pos
feed("ma")
pos = cursor() -- Mark pos
feed("10j0") -- first col, last line
feed("g`a")
feed("<C-o>") -- should do nothing
eq(pos, cursor())
feed("mA")
pos = cursor() -- Mark pos
feed("10j0") -- first col, last line
feed("g'a")
feed("<C-o>") -- should do nothing
eq(pos, cursor())
end)
it("open folds when moving to them", function()
command("edit " .. file1)
feed("jzfG") -- Fold from the second line to the end
command("3mark a")
feed("G") -- On top of the fold
assert(funcs.foldclosed('.') ~= -1) -- folded
feed("'a")
eq(-1, funcs.foldclosed('.'))
feed("zc")
assert(funcs.foldclosed('.') ~= -1) -- folded
-- TODO: remove this workaround after fixing #15873
feed("k`a")
eq(-1, funcs.foldclosed('.'))
feed("zc")
assert(funcs.foldclosed('.') ~= -1) -- folded
feed("kg'a")
eq(-1, funcs.foldclosed('.'))
feed("zc")
assert(funcs.foldclosed('.') ~= -1) -- folded
feed("kg`a")
eq(-1, funcs.foldclosed('.'))
end)
it("do not open folds when moving to them doesn't move the cursor", function()
command("edit " .. file1)
feed("jzfG") -- Fold from the second line to the end
assert(funcs.foldclosed('.') == 2) -- folded
feed("ma")
feed("'a")
feed("`a")
feed("g'a")
feed("g`a")
-- should still be folded
eq(2, funcs.foldclosed('.'))
end)
end)
describe('named marks view', function()
local file1 = 'Xtestfile-functional-editor-marks'
local file2 = 'Xtestfile-functional-editor-marks-2'
local function content()
local c = {}
for i=1,30 do
c[i] = i .. " line"
end
return table.concat(c, "\n")
end
before_each(function()
clear()
write_file(file1, content(), false, false)
write_file(file2, content(), false, false)
command("set jumpoptions+=view")
end)
after_each(function()
os.remove(file1)
os.remove(file2)
end)
it('is restored', function()
local screen = Screen.new(5, 8)
screen:attach()
command("edit " .. file1)
feed("<C-e>jWma")
feed("G'a")
local expected = [[
2 line |
^3 line |
4 line |
5 line |
6 line |
7 line |
8 line |
|
]]
screen:expect({grid=expected})
feed("G`a")
screen:expect([[
2 line |
3 ^line |
4 line |
5 line |
6 line |
7 line |
8 line |
|
]])
end)
it('is restored across files', function()
local screen = Screen.new(5, 5)
screen:attach()
command("args " .. file1 .. " " .. file2)
feed("<C-e>mA")
local mark_view = [[
^2 line |
3 line |
4 line |
5 line |
|
]]
screen:expect(mark_view)
command("next")
screen:expect([[
^1 line |
2 line |
3 line |
4 line |
|
]])
feed("'A")
screen:expect(mark_view)
end)
it('fallback to standard behavior when view can\'t be recovered', function()
local screen = Screen.new(10, 10)
screen:attach()
command("edit " .. file1)
feed("7GzbmaG") -- Seven lines from the top
command("new") -- Screen size for window is now half the height can't be restored
feed("<C-w>p'a")
screen:expect([[
|
~ |
~ |
~ |
[No Name] |
6 line |
^7 line |
8 line |
{MATCH:.*} |
|
]])
end)
end)