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
character in the line of the specified location and
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{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-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' boolean (default off)
global

View File

@@ -383,6 +383,8 @@ Normal commands:
Options:
'ttimeout', 'ttimeoutlen' behavior was simplified
|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
Shell:

View File

@@ -1156,17 +1156,17 @@ Boolean nvim_buf_del_mark(Buffer buffer, String name, Error *err)
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
if (pos == NULL) {
// fm is NULL when there's no mark with the given name
if (fm == NULL) {
api_set_error(err, kErrorTypeValidation, "Invalid mark name: '%c'",
*name.data);
return res;
}
// pos->lnum is 0 when the mark is not valid in the buffer, or is not set.
if (pos->lnum != 0) {
// mark.lnum is 0 when the mark is not valid in the buffer, or is not set.
if (fm->mark.lnum != 0 && fm->fnum == buf->handle) {
// since the mark belongs to the buffer delete it.
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;
}
pos_T *posp;
fmark_T *fm;
pos_T pos;
char mark = *name.data;
try_start();
bufref_T save_buf;
switch_buffer(&save_buf, buf);
posp = getmark(mark, false);
restore_buffer(&save_buf);
if (try_end(err)) {
return rv;
}
if (posp == NULL) {
fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, mark);
if (fm == NULL) {
api_set_error(err, kErrorTypeValidation, "Invalid mark name");
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(posp->col));
ADD(rv, INTEGER_OBJ(pos.lnum));
ADD(rv, INTEGER_OBJ(pos.col));
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);
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 (deleting) {
api_set_error(err, kErrorTypeException,

View File

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

View File

@@ -55,6 +55,7 @@
#include "nvim/main.h"
#include "nvim/mapping.h"
#include "nvim/mark.h"
#include "nvim/mark_defs.h"
#include "nvim/mbyte.h"
#include "nvim/memory.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->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;
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;
win_T *wp = NULL;
pos_T *fpos;
fmark_T *fm = NULL;
colnr_T col;
buf = buflist_findnr(n);
@@ -1963,11 +1965,13 @@ int buflist_getfile(int n, linenr_T lnum, int options, int forceit)
return FAIL;
}
bool restore_view = false;
// altfpos may be changed by getfile(), get it now
if (lnum == 0) {
fpos = buflist_findfpos(buf);
lnum = fpos->lnum;
col = fpos->col;
fm = buflist_findfmark(buf);
lnum = fm->mark.lnum;
col = fm->mark.col;
restore_view = true;
} else {
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_set_curswant = true;
}
if (jop_flags & JOP_VIEW && restore_view) {
mark_view_restore(fm);
}
return OK;
}
RedrawingDisabled--;
@@ -2022,7 +2029,7 @@ void buflist_getfpos(void)
{
pos_T *fpos;
fpos = buflist_findfpos(curbuf);
fpos = &buflist_findfmark(curbuf)->mark;
curwin->w_cursor.lnum = fpos->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) {
wip->wi_fpos.lnum = lnum;
wip->wi_fpos.col = col;
wip->wi_mark.mark.lnum = lnum;
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) {
// Save the window-specific option values.
@@ -2581,24 +2591,23 @@ void get_winopts(buf_T *buf)
didset_window_options(curwin);
}
/// Find the position (lnum and col) for the buffer 'buf' for the current
/// window.
/// Find the mark 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)
fmark_T *buflist_findfmark(buf_T *buf)
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);
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.
linenr_T buflist_findlnum(buf_T *buf)
FUNC_ATTR_PURE
{
return buflist_findfpos(buf)->lnum;
return buflist_findfmark(buf)->mark.lnum;
}
/// List all known file names (for :files and :buffers command).

View File

@@ -283,8 +283,8 @@ typedef struct {
struct wininfo_S {
wininfo_T *wi_next; // next entry or NULL for last entry
wininfo_T *wi_prev; // previous entry or NULL for first entry
win_T *wi_win; // pointer to window that did set wi_fpos
pos_T wi_fpos; // last cursor position in the file
win_T *wi_win; // pointer to window that did set wi_mark
fmark_T wi_mark; // last cursor mark in the file
bool wi_optset; // true when wi_opt has useful values
winopt_T wi_opt; // local window options
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
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
// 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.
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] == '\'') {
// mark
const pos_T *const pp = getmark_buf_fnum(curbuf, (uint8_t)name[1], false, ret_fnum);
if (pp == NULL || pp == (pos_T *)-1 || pp->lnum <= 0) {
int mname = (uint8_t)name[1];
const fmark_T *const fm = mark_get(curbuf, curwin, NULL, kMarkAll, mname);
if (fm == NULL || fm->mark.lnum <= 0) {
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 (charcol) {

View File

@@ -8521,7 +8521,7 @@ static void set_position(typval_T *argvars, typval_T *rettv, bool charpos)
rettv->vval.v_number = 0;
} else if (name[0] == '\'' && name[1] != NUL && name[2] == NUL) {
// 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;
}
} 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
// for explicitly
if ((oldbuf && newlnum == ECMD_LASTL) || newlnum == ECMD_LAST) {
pos = buflist_findfpos(buf);
pos = &buflist_findfmark(buf)->mark;
newlnum = pos->lnum;
solcol = pos->col;
}

View File

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

View File

@@ -621,7 +621,7 @@ static int makeopens(FILE *fd, char_u *dirnow)
if (fprintf(fd, "badd +%" PRId64 " ",
buf->b_wininfo == NULL
? (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) {
return FAIL;
}

View File

@@ -13,7 +13,9 @@
#include "nvim/ascii.h"
#include "nvim/buffer.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/diff.h"
#include "nvim/edit.h"
#include "nvim/eval.h"
#include "nvim/ex_cmds.h"
#include "nvim/extmark.h"
@@ -24,6 +26,7 @@
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/move.h"
#include "nvim/normal.h"
#include "nvim/option.h"
#include "nvim/os/input.h"
@@ -61,7 +64,8 @@ static xfmark_T namedfm[NGLOBALMARKS];
*/
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
@@ -90,9 +94,10 @@ void clear_fmark(fmark_T *fm)
* When "c" is upper case use file "fnum".
* 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;
fmarkv_T view = view_pt != NULL ? *view_pt : (fmarkv_T)INIT_FMARKV;
// Check for a special key (may cause islower() to crash).
if (c < 0) {
@@ -117,7 +122,7 @@ int setmark_pos(int c, pos_T *pos, int fnum)
}
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;
}
@@ -147,7 +152,7 @@ int setmark_pos(int c, pos_T *pos, int fnum)
if (ASCII_ISLOWER(c)) {
i = c - 'a';
RESET_FMARK(buf->b_namedm + i, *pos, fnum);
RESET_FMARK(buf->b_namedm + i, *pos, fnum, view);
return OK;
}
if (ASCII_ISUPPER(c) || ascii_isdigit(c)) {
@@ -156,7 +161,7 @@ int setmark_pos(int c, pos_T *pos, int fnum)
} else {
i = c - 'A';
}
RESET_XFMARK(namedfm + i, *pos, fnum, NULL);
RESET_XFMARK(namedfm + i, *pos, fnum, view, NULL);
return OK;
}
return FAIL;
@@ -202,7 +207,8 @@ void setpcmark(void)
curwin->w_jumplistidx = curwin->w_jumplistlen;
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
}
/*
* move "count" positions in the jump list (count may be negative)
*/
pos_T *movemark(int count)
/// Get mark in "count" position in the |jumplist| relative to the current index.
///
/// If the mark is in a different buffer, it will be skipped unless the buffer exists.
///
/// @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;
xfmark_T *jmp = NULL;
cleanup_jumplist(curwin, true);
cleanup_jumplist(win, true);
if (curwin->w_jumplistlen == 0) { // nothing to jump to
return (pos_T *)NULL;
if (win->w_jumplistlen == 0) { // nothing to jump to
return NULL;
}
for (;;) {
if (curwin->w_jumplistidx + count < 0
|| curwin->w_jumplistidx + count >= curwin->w_jumplistlen) {
return (pos_T *)NULL;
if (win->w_jumplistidx + count < 0
|| win->w_jumplistidx + count >= win->w_jumplistlen) {
return NULL;
}
/*
* 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
* starting Vim on a file), another entry may have been removed.
*/
if (curwin->w_jumplistidx == curwin->w_jumplistlen) {
// 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
// starting Vim on a file), another entry may have been removed.
if (win->w_jumplistidx == win->w_jumplistlen) {
setpcmark();
--curwin->w_jumplistidx; // skip the new entry
if (curwin->w_jumplistidx + count < 0) {
return (pos_T *)NULL;
win->w_jumplistidx--; // skip the new entry
if (win->w_jumplistidx + count < 0) {
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) {
// Resolve the fnum (buff number) in the mark before returning it (shada)
fname2fnum(jmp);
}
if (jmp->fmark.fnum != curbuf->b_fnum) {
// jump to other file
if (buflist_findnr(jmp->fmark.fnum) == NULL) { // Skip this one ..
// Needs to switch buffer, if it can't find it skip the mark
if (buflist_findnr(jmp->fmark.fnum) == NULL) {
count += count < 0 ? -1 : 1;
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
curwin->w_cursor = jmp->fmark.mark;
pos = (pos_T *)-1;
} else {
pos = &(jmp->fmark.mark);
}
return pos;
break;
}
return jmp != NULL ? &jmp->fmark : NULL;
}
/*
* Move "count" positions in the changelist (count may be negative).
*/
pos_T *movechangelist(int count)
/// Get mark in "count" position in the |changelist| relative to the current index.
///
/// @note Changes the win->w_changelistidx.
/// @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;
fmark_T *fm;
if (curbuf->b_changelistlen == 0) { // nothing to jump to
return (pos_T *)NULL;
if (buf->b_changelistlen == 0) { // nothing to jump to
return NULL;
}
n = curwin->w_changelistidx;
n = win->w_changelistidx;
if (n + count < 0) {
if (n == 0) {
return (pos_T *)NULL;
return NULL;
}
n = 0;
} else if (n + count >= curbuf->b_changelistlen) {
if (n == curbuf->b_changelistlen - 1) {
return (pos_T *)NULL;
} else if (n + count >= buf->b_changelistlen) {
if (n == buf->b_changelistlen - 1) {
return NULL;
}
n = curbuf->b_changelistlen - 1;
n = buf->b_changelistlen - 1;
} else {
n += count;
}
curwin->w_changelistidx = n;
return &(curbuf->b_changelist[n].mark);
win->w_changelistidx = n;
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]);
}
/*
* Find mark "c" in buffer pointed to by "buf".
* If "changefile" is TRUE it's allowed to edit another file for '0, 'A, etc.
* If "fnum" is not NULL store the fnum there for '0, 'A etc., don't edit
* another file.
* Returns:
* - pointer to pos_T if found. lnum is 0 when mark not set, -1 when mark is
* in another file which can't be gotten. (caller needs to check lnum!)
* - NULL if there is no mark called 'c'.
* - -1 if mark is in other file and jumped there (only if changefile is TRUE)
*/
pos_T *getmark_buf(buf_T *buf, int c, bool changefile)
/// Get a named mark.
///
/// All types of marks, even those that are not technically a mark will be returned as such. Use
/// mark_move_to() to move to the mark.
/// @note Some of the pointers are statically allocated, if in doubt make a copy. For more
/// information read mark_get_local().
/// @param buf Buffer to get the mark from.
/// @param win Window to get or calculate the mark from (motion type marks, context mark).
/// @param fmp[out] Optional pointer to store the result in, as a workaround for the note above.
/// @param flag MarkGet value
/// @param name Name of the mark.
///
/// @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
xfmark_T *xfm = mark_get_global(!(flag & kMarkAllNoResolve), name);
fm = &xfm->fmark;
// Only wanted marks belonging to the buffer
if ((flag & kMarkBufLocal) && xfm->fmark.fnum != buf->handle) {
return NULL;
}
} else if (name > 0 && name < NMARK_LOCAL_MAX) {
// Local Marks
fm = mark_get_local(buf, win, name);
}
if (fmp != NULL && fm != NULL) {
*fmp = *fm;
return fmp;
}
return fm;
}
pos_T *getmark(int c, bool changefile)
/// Get a global mark {A-Z0-9}.
///
/// @param name the name of the mark.
/// @param resolve Whether to try resolving the mark fnum (i.e., load the buffer stored in
/// the mark fname and update the xfmark_T (expensive)).
///
/// @return Mark
xfmark_T *mark_get_global(bool resolve, int name)
{
return getmark_buf_fnum(curbuf, c, changefile, NULL);
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;
}
pos_T *getmark_buf_fnum(buf_T *buf, int c, bool changefile, int *fnum)
/// 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)
{
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;
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);
}
if (c > '~') { // check for islower()/isupper()
} else if (c == '\'' || c == '`') { // previous context mark
pos_copy = curwin->w_pcmark; // need to make a copy because
posp = &pos_copy; // w_pcmark may be changed soon
} else if (c == '"') { // to pos when leaving buffer
posp = &(buf->b_last_cursor.mark);
} else if (c == '^') { // to where Insert mode stopped
posp = &(buf->b_last_insert.mark);
} else if (c == '.') { // to where last change was made
posp = &(buf->b_last_change.mark);
} else if (c == '[') { // to start of previous operator
posp = &(buf->b_op_start);
} else if (c == ']') { // to end of previous operator
posp = &(buf->b_op_end);
} else if (c == '{' || c == '}') { // to previous/next paragraph
pos_T pos;
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;
bool slcb = listcmd_busy;
pos = curwin->w_cursor;
listcmd_busy = true; // avoid that '' is changed
if (findpar(&oa.inclusive,
c == '}' ? FORWARD : BACKWARD, 1L, NUL, FALSE)) {
pos_copy = curwin->w_cursor;
posp = &pos_copy;
name == '}' ? FORWARD : BACKWARD, 1L, NUL, false)) {
mark = pos_to_mark(buf, NULL, win->w_cursor);
}
curwin->w_cursor = pos;
listcmd_busy = slcb;
} else if (c == '(' || c == ')') { // to previous/next sentence
pos_T pos;
// to previous/next sentence
} else if (name == '(' || name == ')') {
bool slcb = listcmd_busy;
pos = curwin->w_cursor;
listcmd_busy = true; // avoid that '' is changed
if (findsent(c == ')' ? FORWARD : BACKWARD, 1L)) {
pos_copy = curwin->w_cursor;
posp = &pos_copy;
if (findsent(name == ')' ? FORWARD : BACKWARD, 1L)) {
mark = pos_to_mark(buf, NULL, win->w_cursor);
}
curwin->w_cursor = pos;
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
/// @param dir direction for search
/// This marks are different to normal marks:
/// 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.
pos_T *getnextmark(pos_T *startpos, int dir, int begin_line)
/// @return[static] Mark
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;
pos_T *result = NULL;
fmark_T *result = NULL;
pos_T pos;
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) {
pos.col = 0;
} 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++) {
if (curbuf->b_namedm[i].mark.lnum > 0) {
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)) {
result = &curbuf->b_namedm[i].mark;
result = &curbuf->b_namedm[i];
}
} 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)) {
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 a if a position from a mark is valid.
* Give and error message and return FAIL if not.
*/
int check_mark(pos_T *pos)
/// Check the position in @a fm is valid.
///
/// Emit error message and return accordingly.
///
/// 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));
return FAIL;
}
if (pos->lnum <= 0) {
// lnum is negative if mark is in another file can can't get that
// file, error message already give then.
if (pos->lnum == 0) {
return false;
} else if (fm->mark.lnum <= 0) {
// In both cases it's an error but only raise when equals to 0
if (fm->mark.lnum == 0) {
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));
return FAIL;
return false;
}
return OK;
return true;
}
/// 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
///
/// @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.
///
/// @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
{
if (iter == NULL && win->w_jumplistlen == 0) {
*fm = (xfmark_T) { { { 0, 0, 0 }, 0, 0, NULL }, NULL };
*fm = (xfmark_T)INIT_XFMARK;
return NULL;
}
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
///
/// @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.
///
/// @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
///
/// @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.
///
/// @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)
{
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
///
/// @note Mark might not have it's fnum resolved.
/// @param[in] Name of named 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)];
}

View File

@@ -13,42 +13,43 @@
#include "nvim/pos.h"
/// Set fmark using given value
#define SET_FMARK(fmarkp_, mark_, fnum_) \
#define SET_FMARK(fmarkp_, mark_, fnum_, view_) \
do { \
fmark_T *const fmarkp__ = fmarkp_; \
fmarkp__->mark = mark_; \
fmarkp__->fnum = fnum_; \
fmarkp__->timestamp = os_time(); \
fmarkp__->view = view_; \
fmarkp__->additional_data = NULL; \
} while (0)
/// Free and set fmark using given value
#define RESET_FMARK(fmarkp_, mark_, fnum_) \
#define RESET_FMARK(fmarkp_, mark_, fnum_, view_) \
do { \
fmark_T *const fmarkp___ = fmarkp_; \
free_fmark(*fmarkp___); \
SET_FMARK(fmarkp___, mark_, fnum_); \
SET_FMARK(fmarkp___, mark_, fnum_, view_); \
} while (0)
/// Clear given fmark
#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)
#define SET_XFMARK(xfmarkp_, mark_, fnum_, fname_) \
#define SET_XFMARK(xfmarkp_, mark_, fnum_, view_, fname_) \
do { \
xfmark_T *const xfmarkp__ = xfmarkp_; \
xfmarkp__->fname = fname_; \
SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_); \
SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_, view_); \
} while (0)
/// 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 { \
xfmark_T *const xfmarkp__ = xfmarkp_; \
free_xfmark(*xfmarkp__); \
xfmarkp__->fname = fname_; \
SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_); \
SET_FMARK(&(xfmarkp__->fmark), mark_, fnum_, view_); \
} while (0)
/// 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)
*/
/// 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
#define EXTRA_MARKS ('9' - '0' + 1)
@@ -25,24 +52,39 @@
/// but they are not saved in ShaDa files.
#define NLOCALMARKS (NMARKS + 3)
/// Max value of local mark
#define NMARK_LOCAL_MAX 126 // Index of '~'
/// Maximum number of marks in jump list
#define JUMPLISTSIZE 100
/// Maximum number of tags in tag stack
#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
typedef struct filemark {
pos_T mark; ///< Cursor position.
int fnum; ///< File number.
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.
} fmark_T;
#define INIT_FMARK { { 0, 0, 0 }, 0, 0, INIT_FMARKV, NULL }
/// Structure defining extended mark (mark with file name attached)
typedef struct xfilemark {
fmark_T fmark; ///< Actual mark.
char *fname; ///< File name, used when fnum == 0.
} xfmark_T;
#define INIT_XFMARK { INIT_FMARK, NULL }
#endif // NVIM_MARK_DEFS_H

View File

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

View File

@@ -491,9 +491,10 @@ EXTERN int p_js; // 'joinspaces'
EXTERN char_u *p_jop; // 'jumpooptions'
EXTERN unsigned jop_flags;
#ifdef IN_OPTION_C
static char *(p_jop_values[]) = { "stack", NULL };
static char *(p_jop_values[]) = { "stack", "view", NULL };
#endif
#define JOP_STACK 0x01
#define JOP_VIEW 0x02
EXTERN char_u *p_kp; // 'keywordprg'
EXTERN char_u *p_km; // 'keymodel'
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;
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.
if (REG_MULTI) {
@@ -3714,10 +3715,11 @@ static bool regmatch(char_u *scan, proftime_T *tm, int *timed_out)
rex.input = rex.line + col;
}
if (pos == NULL // mark doesn't exist
|| pos->lnum <= 0) { // mark isn't set in reg_buf
if (fm == NULL // mark doesn't exist
|| fm->mark.lnum <= 0) { // mark isn't set in reg_buf
status = RA_NOMATCH;
} else {
pos = &fm->mark;
const colnr_T pos_col = pos->lnum == rex.lnum + rex.reg_firstlnum
&& pos->col == MAXCOL
? (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_GT:
case NFA_MARK_LT: {
pos_T *pos;
fmark_T *fm;
size_t col = REG_MULTI ? rex.input - rex.line : 0;
pos = getmark_buf(rex.reg_buf, t->state->val, false);
// fm will be NULL if the mark is not set, doesn't belong to reg_buf
fm = mark_get(rex.reg_buf, curwin, NULL, kMarkBufLocal, t->state->val);
// Line may have been freed, get it again.
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
// 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
&& pos->col == MAXCOL
? (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,
BLN_LISTED);
if (buf != NULL) {
fmarkv_T view = INIT_FMARKV;
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,
buf->b_last_cursor.mark.col, false);
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
* 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

View File

@@ -1,4 +1,5 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local clear = helpers.clear
local command = helpers.command
@@ -7,6 +8,7 @@ local funcs = helpers.funcs
local feed = helpers.feed
local exec_capture = helpers.exec_capture
local write_file = helpers.write_file
local curbufmeths = helpers.curbufmeths
describe('jumplist', function()
local fname1 = 'Xtest-functional-normal-jump'
@@ -137,3 +139,116 @@ describe("jumpoptions=stack behaves like 'tagstack'", function()
exec_capture('jumps'))
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)