diff options
-rw-r--r-- | runtime/doc/eval.txt | 3 | ||||
-rw-r--r-- | runtime/doc/options.txt | 2 | ||||
-rw-r--r-- | runtime/doc/windows.txt | 3 | ||||
-rw-r--r-- | src/nvim/buffer.c | 97 | ||||
-rw-r--r-- | src/nvim/buffer_defs.h | 2 | ||||
-rw-r--r-- | src/nvim/eval.c | 2 | ||||
-rw-r--r-- | src/nvim/ex_cmds.c | 2 | ||||
-rw-r--r-- | src/nvim/ex_getln.c | 28 | ||||
-rw-r--r-- | src/nvim/ex_getln.h | 1 | ||||
-rw-r--r-- | src/nvim/misc1.c | 23 | ||||
-rw-r--r-- | src/nvim/normal.c | 2 | ||||
-rw-r--r-- | src/nvim/option.c | 2 | ||||
-rw-r--r-- | src/nvim/option_defs.h | 1 | ||||
-rw-r--r-- | src/nvim/terminal.c | 5 | ||||
-rw-r--r-- | src/nvim/testdir/test_bufwintabinfo.vim | 7 | ||||
-rw-r--r-- | src/nvim/testdir/test_cmdline.vim | 46 | ||||
-rw-r--r-- | src/nvim/testdir/test_excmd.vim | 32 | ||||
-rw-r--r-- | src/nvim/undo.c | 35 | ||||
-rw-r--r-- | test/functional/ex_cmds/ls_spec.lua | 12 |
19 files changed, 257 insertions, 48 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 29c5c37bcd..4ff6269004 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -4164,6 +4164,9 @@ getbufinfo([{dict}]) changed TRUE if the buffer is modified. changedtick number of changes made to the buffer. hidden TRUE if the buffer is hidden. + lastused timestamp in seconds, like + |localtime()|, when the buffer was + last used. listed TRUE if the buffer is listed. lnum current line number in buffer. linecount number of lines in the buffer (only diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 5fb80d75ce..009695c13c 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -6742,6 +6742,8 @@ A jump table for the options with a short description can be found at |Q_op|. complete first match. "list:longest" When more than one match, list all matches and complete till longest common string. + "list:lastused" When more than one buffer matches, sort buffers + by time last used (other than the current buffer). When there is only a single match, it is fully completed in all cases. Examples: > diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt index 977e0daef7..b5623f4ea4 100644 --- a/runtime/doc/windows.txt +++ b/runtime/doc/windows.txt @@ -1021,6 +1021,9 @@ list of buffers. |unlisted-buffer| x buffers with a read error % current buffer # alternate buffer + R terminal buffers with a running job + F terminal buffers with a finished job + t show time last used and sort buffers Combining flags means they are "and"ed together, e.g.: h+ hidden buffers which are modified a+ active buffers which are modified diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 24ba43676a..7c8f93163a 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -1618,6 +1618,7 @@ void enter_buffer(buf_T *buf) if (!curbuf->b_help && curwin->w_p_spell && *curwin->w_s->b_p_spl != NUL) { (void)did_set_spelllang(curwin); } + curbuf->b_last_used = time(NULL); redraw_later(NOT_VALID); } @@ -2250,6 +2251,23 @@ int buflist_findpat( return match; } +typedef struct { + buf_T *buf; + char_u *match; +} bufmatch_T; + +/// Compare functions for qsort() below, that compares b_last_used. +static int +buf_time_compare(const void *s1, const void *s2) +{ + buf_T *buf1 = *(buf_T **)s1; + buf_T *buf2 = *(buf_T **)s2; + + if (buf1->b_last_used == buf2->b_last_used) { + return 0; + } + return buf1->b_last_used > buf2->b_last_used ? -1 : 1; +} /* * Find all buffer names that match. @@ -2263,6 +2281,7 @@ int ExpandBufnames(char_u *pat, int *num_file, char_u ***file, int options) char_u *p; int attempt; char_u *patc; + bufmatch_T *matches = NULL; *num_file = 0; // return values in case of FAIL *file = NULL; @@ -2313,7 +2332,13 @@ int ExpandBufnames(char_u *pat, int *num_file, char_u ***file, int options) } else { p = vim_strsave(p); } - (*file)[count++] = p; + if (matches != NULL) { + matches[count].buf = buf; + matches[count].match = p; + count++; + } else { + (*file)[count++] = p; + } } } } @@ -2322,6 +2347,10 @@ int ExpandBufnames(char_u *pat, int *num_file, char_u ***file, int options) } if (round == 1) { *file = xmalloc((size_t)count * sizeof(**file)); + + if (options & WILD_BUFLASTUSED) { + matches = xmalloc((size_t)count * sizeof(*matches)); + } } } vim_regfree(regmatch.regprog); @@ -2334,6 +2363,25 @@ int ExpandBufnames(char_u *pat, int *num_file, char_u ***file, int options) xfree(patc); } + if (matches != NULL) { + if (count > 1) { + qsort(matches, (size_t)count, sizeof(bufmatch_T), buf_time_compare); + } + + // if the current buffer is first in the list, place it at the end + if (matches[0].buf == curbuf) { + for (int i = 1; i < count; i++) { + (*file)[i-1] = matches[i].match; + } + (*file)[count-1] = matches[0].match; + } else { + for (int i = 0; i < count; i++) { + (*file)[i] = matches[i].match; + } + } + xfree(matches); + } + *num_file = count; return count == 0 ? FAIL : OK; } @@ -2594,11 +2642,35 @@ linenr_T buflist_findlnum(buf_T *buf) // List all known file names (for :files and :buffers command). void buflist_list(exarg_T *eap) { - buf_T *buf; + buf_T *buf = firstbuf; int len; int i; - for (buf = firstbuf; buf != NULL && !got_int; buf = buf->b_next) { + garray_T buflist; + buf_T **buflist_data = NULL, **p; + + if (vim_strchr(eap->arg, 't')) { + ga_init(&buflist, sizeof(buf_T *), 50); + for (buf = firstbuf; buf != NULL; buf = buf->b_next) { + ga_grow(&buflist, 1); + ((buf_T **)buflist.ga_data)[buflist.ga_len++] = buf; + } + + qsort(buflist.ga_data, (size_t)buflist.ga_len, + sizeof(buf_T *), buf_time_compare); + + p = buflist_data = (buf_T **)buflist.ga_data; + buf = *p; + } + + for (; + buf != NULL && !got_int; + buf = buflist_data + ? (++p < buflist_data + buflist.ga_len ? *p : NULL) + : buf->b_next) { + const bool is_terminal = buf->terminal; + const bool job_running = buf->terminal && terminal_running(buf->terminal); + // skip unspecified buffers if ((!buf->b_p_bl && !eap->forceit && !strchr((char *)eap->arg, 'u')) || (strchr((char *)eap->arg, 'u') && buf->b_p_bl) @@ -2608,6 +2680,8 @@ void buflist_list(exarg_T *eap) && (buf->b_ml.ml_mfp == NULL || buf->b_nwindows == 0)) || (strchr((char *)eap->arg, 'h') && (buf->b_ml.ml_mfp == NULL || buf->b_nwindows != 0)) + || (strchr((char *)eap->arg, 'R') && (!is_terminal || !job_running)) + || (strchr((char *)eap->arg, 'F') && (!is_terminal || job_running)) || (strchr((char *)eap->arg, '-') && buf->b_p_ma) || (strchr((char *)eap->arg, '=') && !buf->b_p_ro) || (strchr((char *)eap->arg, 'x') && !(buf->b_flags & BF_READERR)) @@ -2654,13 +2728,22 @@ void buflist_list(exarg_T *eap) do { IObuff[len++] = ' '; } while (--i > 0 && len < IOSIZE - 18); - vim_snprintf((char *)IObuff + len, (size_t)(IOSIZE - len), - _("line %" PRId64), - buf == curbuf ? (int64_t)curwin->w_cursor.lnum - : (int64_t)buflist_findlnum(buf)); + if (vim_strchr(eap->arg, 't') && buf->b_last_used) { + add_time(IObuff + len, (size_t)(IOSIZE - len), buf->b_last_used); + } else { + vim_snprintf((char *)IObuff + len, (size_t)(IOSIZE - len), + _("line %" PRId64), + buf == curbuf ? (int64_t)curwin->w_cursor.lnum + : (int64_t)buflist_findlnum(buf)); + } + msg_outtrans(IObuff); line_breakcheck(); } + + if (buflist_data) { + ga_clear(&buflist); + } } /* diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 2460159fc0..3dbd3ea121 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -543,6 +543,8 @@ struct file_buffer { long b_mtime_read; // last change time when reading uint64_t b_orig_size; // size of original file in bytes int b_orig_mode; // mode of original file + time_t b_last_used; // time when the buffer was last used; used + // for viminfo fmark_T b_namedm[NMARKS]; // current named marks (mark.c) diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 12b13a1f08..bcdb4a18f1 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -7241,6 +7241,8 @@ dict_T *get_buffer_info(buf_T *buf) tv_dict_add_list(dict, S_LEN("signs"), get_buffer_signs(buf)); } + tv_dict_add_nr(dict, S_LEN("lastused"), buf->b_last_used); + return dict; } diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index d26d3387f8..8a0f2e634a 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -2670,6 +2670,8 @@ int do_ecmd( msg_scrolled_ign = FALSE; } + curbuf->b_last_used = time(NULL); + if (command != NULL) do_cmdline(command, NULL, NULL, DOCMD_VERBOSE); diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index ecaab76612..d9a63ff6da 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -947,6 +947,10 @@ static int command_line_execute(VimState *state, int key) // - wildcard expansion is only done when the 'wildchar' key is really // typed, not when it comes from a macro if ((s->c == p_wc && !s->gotesc && KeyTyped) || s->c == p_wcm) { + int options = WILD_NO_BEEP; + if (wim_flags[s->wim_index] & WIM_BUFLASTUSED) { + options |= WILD_BUFLASTUSED; + } if (s->xpc.xp_numfiles > 0) { // typed p_wc at least twice // if 'wildmode' contains "list" may still need to list if (s->xpc.xp_numfiles > 1 @@ -960,11 +964,11 @@ static int command_line_execute(VimState *state, int key) } if (wim_flags[s->wim_index] & WIM_LONGEST) { - s->res = nextwild(&s->xpc, WILD_LONGEST, WILD_NO_BEEP, - s->firstc != '@'); + s->res = nextwild(&s->xpc, WILD_LONGEST, options, + s->firstc != '@'); } else if (wim_flags[s->wim_index] & WIM_FULL) { - s->res = nextwild(&s->xpc, WILD_NEXT, WILD_NO_BEEP, - s->firstc != '@'); + s->res = nextwild(&s->xpc, WILD_NEXT, options, + s->firstc != '@'); } else { s->res = OK; // don't insert 'wildchar' now } @@ -975,11 +979,11 @@ static int command_line_execute(VimState *state, int key) // if 'wildmode' first contains "longest", get longest // common part if (wim_flags[0] & WIM_LONGEST) { - s->res = nextwild(&s->xpc, WILD_LONGEST, WILD_NO_BEEP, - s->firstc != '@'); + s->res = nextwild(&s->xpc, WILD_LONGEST, options, + s->firstc != '@'); } else { - s->res = nextwild(&s->xpc, WILD_EXPAND_KEEP, WILD_NO_BEEP, - s->firstc != '@'); + s->res = nextwild(&s->xpc, WILD_EXPAND_KEEP, options, + s->firstc != '@'); } // if interrupted while completing, behave like it failed @@ -1016,11 +1020,11 @@ static int command_line_execute(VimState *state, int key) s->did_wild_list = true; if (wim_flags[s->wim_index] & WIM_LONGEST) { - nextwild(&s->xpc, WILD_LONGEST, WILD_NO_BEEP, - s->firstc != '@'); + nextwild(&s->xpc, WILD_LONGEST, options, + s->firstc != '@'); } else if (wim_flags[s->wim_index] & WIM_FULL) { - nextwild(&s->xpc, WILD_NEXT, WILD_NO_BEEP, - s->firstc != '@'); + nextwild(&s->xpc, WILD_NEXT, options, + s->firstc != '@'); } } else { vim_beep(BO_WILD); diff --git a/src/nvim/ex_getln.h b/src/nvim/ex_getln.h index 84b2b41f30..dc4395e081 100644 --- a/src/nvim/ex_getln.h +++ b/src/nvim/ex_getln.h @@ -31,6 +31,7 @@ #define WILD_ALLLINKS 0x200 #define WILD_IGNORE_COMPLETESLASH 0x400 #define WILD_NOERROR 0x800 // sets EW_NOERROR +#define WILD_BUFLASTUSED 0x1000 /// Present history tables typedef enum { diff --git a/src/nvim/misc1.c b/src/nvim/misc1.c index e7fb38e801..e10770b6bd 100644 --- a/src/nvim/misc1.c +++ b/src/nvim/misc1.c @@ -1162,3 +1162,26 @@ int goto_im(void) { return p_im && stuff_empty() && typebuf_typed(); } + +/// Put the timestamp of an undo header in "buf[buflen]" in a nice format. +void add_time(char_u *buf, size_t buflen, time_t tt) +{ + struct tm curtime; + + if (time(NULL) - tt >= 100) { + os_localtime_r(&tt, &curtime); + if (time(NULL) - tt < (60L * 60L * 12L)) { + // within 12 hours + (void)strftime((char *)buf, buflen, "%H:%M:%S", &curtime); + } else { + // longer ago + (void)strftime((char *)buf, buflen, "%Y/%m/%d %H:%M:%S", &curtime); + } + } else { + int64_t seconds = time(NULL) - tt; + vim_snprintf((char *)buf, buflen, + NGETTEXT("%" PRId64 " second ago", + "%" PRId64 " seconds ago", (uint32_t)seconds), + seconds); + } +} diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 58993426dd..76de60cf4f 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -1260,6 +1260,8 @@ static void normal_redraw(NormalState *s) maketitle(); } + curbuf->b_last_used = time(NULL); + // Display message after redraw. If an external message is still visible, // it contains the kept message already. if (keep_msg != NULL && !msg_ext_is_visible()) { diff --git a/src/nvim/option.c b/src/nvim/option.c index 25a8d432ee..eae52ff260 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -6988,6 +6988,8 @@ static int check_opt_wim(void) new_wim_flags[idx] |= WIM_FULL; } else if (i == 4 && STRNCMP(p, "list", 4) == 0) { new_wim_flags[idx] |= WIM_LIST; + } else if (i == 8 && STRNCMP(p, "lastused", 8) == 0) { + new_wim_flags[idx] |= WIM_BUFLASTUSED; } else { return FAIL; } diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 192e57a642..ecaa941082 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -277,6 +277,7 @@ enum { #define WIM_FULL 1 #define WIM_LONGEST 2 #define WIM_LIST 4 +#define WIM_BUFLASTUSED 8 // arguments for can_bs() #define BS_INDENT 'i' // "Indent" diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index a37cc60928..560a345333 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -650,6 +650,11 @@ Buffer terminal_buf(const Terminal *term) return term->buf_handle; } +bool terminal_running(const Terminal *term) +{ + return !term->closed; +} + // }}} // libvterm callbacks {{{ diff --git a/src/nvim/testdir/test_bufwintabinfo.vim b/src/nvim/testdir/test_bufwintabinfo.vim index 176d49d28e..cb7ab44798 100644 --- a/src/nvim/testdir/test_bufwintabinfo.vim +++ b/src/nvim/testdir/test_bufwintabinfo.vim @@ -149,3 +149,10 @@ func Test_getbufinfo_lines() edit Xfoo bw! endfunc + +function Test_getbufinfo_lastused() + new Xfoo + let info = getbufinfo('Xfoo')[0] + call assert_equal(has_key(info, 'lastused'), 1) + call assert_equal(type(info.lastused), type(0)) +endfunc diff --git a/src/nvim/testdir/test_cmdline.vim b/src/nvim/testdir/test_cmdline.vim index 9c3c33a943..7f1e1f4456 100644 --- a/src/nvim/testdir/test_cmdline.vim +++ b/src/nvim/testdir/test_cmdline.vim @@ -755,3 +755,49 @@ func Test_cmdwin_feedkeys() " This should not generate E488 call feedkeys("q:\<CR>", 'x') endfunc + +func Test_buffers_lastused() + " check that buffers are sorted by time when wildmode has lastused + edit bufc " oldest + + sleep 1200m + enew + edit bufa " middle + + sleep 1200m + enew + edit bufb " newest + + enew + + call assert_equal(['bufc', 'bufa', 'bufb'], + \ getcompletion('', 'buffer')) + + let save_wildmode = &wildmode + set wildmode=full:lastused + + let cap = "\<c-r>=execute('let X=getcmdline()')\<cr>" + call feedkeys(":b \<tab>" .. cap .. "\<esc>", 'xt') + call assert_equal('b bufb', X) + call feedkeys(":b \<tab>\<tab>" .. cap .. "\<esc>", 'xt') + call assert_equal('b bufa', X) + call feedkeys(":b \<tab>\<tab>\<tab>" .. cap .. "\<esc>", 'xt') + call assert_equal('b bufc', X) + enew + + sleep 1200m + edit other + call feedkeys(":b \<tab>" .. cap .. "\<esc>", 'xt') + call assert_equal('b bufb', X) + call feedkeys(":b \<tab>\<tab>" .. cap .. "\<esc>", 'xt') + call assert_equal('b bufa', X) + call feedkeys(":b \<tab>\<tab>\<tab>" .. cap .. "\<esc>", 'xt') + call assert_equal('b bufc', X) + enew + + let &wildmode = save_wildmode + + bwipeout bufa + bwipeout bufb + bwipeout bufc +endfunc diff --git a/src/nvim/testdir/test_excmd.vim b/src/nvim/testdir/test_excmd.vim index f5ce979208..4a027c3864 100644 --- a/src/nvim/testdir/test_excmd.vim +++ b/src/nvim/testdir/test_excmd.vim @@ -8,3 +8,35 @@ func Test_ex_delete() .dl call assert_equal(['a', 'c'], getline(1, 2)) endfunc + +func Test_buffers_lastused() + edit bufc " oldest + + sleep 1200m + edit bufa " middle + + sleep 1200m + edit bufb " newest + + enew + + let ls = split(execute('buffers t', 'silent!'), '\n') + let bufs = [] + for line in ls + let bufs += [split(line, '"\s*')[1:2]] + endfor + + let names = [] + for buf in bufs + if buf[0] !=# '[No Name]' + let names += [buf[0]] + endif + endfor + + call assert_equal(['bufb', 'bufa', 'bufc'], names) + call assert_match('[0-2] seconds ago', bufs[1][1]) + + bwipeout bufa + bwipeout bufb + bwipeout bufc +endfunc diff --git a/src/nvim/undo.c b/src/nvim/undo.c index df92b2c036..97018f6c02 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -2441,10 +2441,11 @@ static void u_undo_end( uhp = curbuf->b_u_newhead; } - if (uhp == NULL) + if (uhp == NULL) { *msgbuf = NUL; - else - u_add_time(msgbuf, sizeof(msgbuf), uhp->uh_time); + } else { + add_time(msgbuf, sizeof(msgbuf), uhp->uh_time); + } { FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { @@ -2509,8 +2510,8 @@ void ex_undolist(exarg_T *eap) && uhp->uh_walk != mark) { vim_snprintf((char *)IObuff, IOSIZE, "%6ld %7d ", uhp->uh_seq, changes); - u_add_time(IObuff + STRLEN(IObuff), IOSIZE - STRLEN(IObuff), - uhp->uh_time); + add_time(IObuff + STRLEN(IObuff), IOSIZE - STRLEN(IObuff), + uhp->uh_time); if (uhp->uh_save_nr > 0) { while (STRLEN(IObuff) < 33) STRCAT(IObuff, " "); @@ -2575,30 +2576,6 @@ void ex_undolist(exarg_T *eap) } /* - * Put the timestamp of an undo header in "buf[buflen]" in a nice format. - */ -static void u_add_time(char_u *buf, size_t buflen, time_t tt) -{ - struct tm curtime; - - if (time(NULL) - tt >= 100) { - os_localtime_r(&tt, &curtime); - if (time(NULL) - tt < (60L * 60L * 12L)) - /* within 12 hours */ - (void)strftime((char *)buf, buflen, "%H:%M:%S", &curtime); - else - /* longer ago */ - (void)strftime((char *)buf, buflen, "%Y/%m/%d %H:%M:%S", &curtime); - } else { - int64_t seconds = time(NULL) - tt; - vim_snprintf((char *)buf, buflen, - NGETTEXT("%" PRId64 " second ago", - "%" PRId64 " seconds ago", (uint32_t)seconds), - seconds); - } -} - -/* * ":undojoin": continue adding to the last entry list */ void ex_undojoin(exarg_T *eap) diff --git a/test/functional/ex_cmds/ls_spec.lua b/test/functional/ex_cmds/ls_spec.lua index f7bacd7386..9853084c47 100644 --- a/test/functional/ex_cmds/ls_spec.lua +++ b/test/functional/ex_cmds/ls_spec.lua @@ -31,6 +31,18 @@ describe(':ls', function() -- Terminal buffer [F]inished. eq('\n 3 %aF', string.match(ls_output, '\n *3....')) end) + + retry(nil, 5000, function() + local ls_output = eval('execute("ls R")') + -- Just the [R]unning terminal buffer. + eq('\n 2 #aR ', string.match(ls_output, '^\n *2 ... ')) + end) + + retry(nil, 5000, function() + local ls_output = eval('execute("ls F")') + -- Just the [F]inished terminal buffer. + eq('\n 3 %aF ', string.match(ls_output, '^\n *3 ... ')) + end) end) end) |