aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--CMakeLists.txt6
-rw-r--r--runtime/doc/nvim_terminal_emulator.txt100
-rw-r--r--runtime/doc/options.txt15
-rw-r--r--runtime/doc/tagsrch.txt44
-rw-r--r--runtime/doc/various.txt16
-rw-r--r--src/.valgrind.supp2
-rw-r--r--src/nvim/buffer_defs.h21
-rw-r--r--src/nvim/edit.c4
-rw-r--r--src/nvim/eval.c111
-rw-r--r--src/nvim/ex_cmds.c11
-rw-r--r--src/nvim/globals.h1
-rw-r--r--src/nvim/msgpack_rpc/channel.c33
-rw-r--r--src/nvim/normal.c28
-rw-r--r--src/nvim/option.c53
-rw-r--r--src/nvim/option_defs.h6
-rw-r--r--src/nvim/options.lua8
-rw-r--r--src/nvim/os/fileio.c8
-rw-r--r--src/nvim/os/fileio.h2
-rw-r--r--src/nvim/screen.c3
-rw-r--r--src/nvim/search.c14
-rw-r--r--src/nvim/tag.c6
-rw-r--r--src/nvim/terminal.c205
-rw-r--r--src/nvim/testdir/test_alot.vim1
-rw-r--r--src/nvim/testdir/test_tagcase.vim73
-rw-r--r--src/nvim/undo.c10
-rw-r--r--src/nvim/version.c2
-rw-r--r--src/nvim/window.c3
-rw-r--r--test/functional/eval/writefile_spec.lua140
-rw-r--r--test/functional/helpers.lua11
-rw-r--r--test/functional/terminal/edit_spec.lua36
-rw-r--r--test/functional/terminal/ex_terminal_spec.lua22
-rw-r--r--test/functional/terminal/helpers.lua51
-rw-r--r--test/functional/terminal/mouse_spec.lua2
-rw-r--r--test/functional/terminal/scrollback_spec.lua385
-rw-r--r--test/functional/terminal/window_split_tab_spec.lua14
-rw-r--r--test/helpers.lua4
37 files changed, 931 insertions, 522 deletions
diff --git a/.travis.yml b/.travis.yml
index 337da74a03..d28fc9d7f1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,7 +27,7 @@ env:
-DCMAKE_INSTALL_PREFIX:PATH=$INSTALL_PREFIX
-DBUSTED_OUTPUT_TYPE=gtest
-DDEPS_PREFIX=$DEPS_BUILD_DIR/usr
- -DMIN_LOG_LEVEL=0"
+ -DMIN_LOG_LEVEL=2"
- DEPS_CMAKE_FLAGS="-DDEPS_DOWNLOAD_DIR:PATH=$DEPS_DOWNLOAD_DIR"
# Additional CMake flags for 32-bit builds.
- CMAKE_FLAGS_32BIT="-DCMAKE_SYSTEM_LIBRARY_PATH=/lib32:/usr/lib32:/usr/local/lib32
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5d2d0ec97c..2fd5209396 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -439,11 +439,11 @@ install_helper(
# MIN_LOG_LEVEL for log.h
if(DEFINED MIN_LOG_LEVEL)
if(NOT MIN_LOG_LEVEL MATCHES "^[0-3]$")
- message(FATAL_ERROR "MIN_LOG_LEVEL must be a number DEBUG (0), INFO (1), WARNING (2) or ERROR (3)")
+ message(FATAL_ERROR "MIN_LOG_LEVEL must be a number 0-3")
endif()
- message(STATUS "Log level set to ${MIN_LOG_LEVEL}")
+ message(STATUS "MIN_LOG_LEVEL set to ${MIN_LOG_LEVEL}")
else()
- message(STATUS "Log level not specified, defaulting to INFO(1)")
+ message(STATUS "MIN_LOG_LEVEL not specified, defaulting to INFO(1)")
endif()
# Go down the tree.
diff --git a/runtime/doc/nvim_terminal_emulator.txt b/runtime/doc/nvim_terminal_emulator.txt
index 8f7dc0dbf0..0954dcb5a7 100644
--- a/runtime/doc/nvim_terminal_emulator.txt
+++ b/runtime/doc/nvim_terminal_emulator.txt
@@ -4,28 +4,19 @@
NVIM REFERENCE MANUAL by Thiago de Arruda
-Embedded terminal emulator *terminal-emulator*
+Terminal emulator *terminal-emulator*
-1. Introduction |terminal-emulator-intro|
-2. Spawning |terminal-emulator-spawning|
-3. Input |terminal-emulator-input|
-4. Configuration |terminal-emulator-configuration|
-5. Status Variables |terminal-emulator-status|
+Nvim embeds a VT220/xterm terminal emulator based on libvterm. The terminal is
+presented as a special buffer type, asynchronously updated from the virtual
+terminal as data is received from the program connected to it.
-==============================================================================
-1. Introduction *terminal-emulator-intro*
-
-Nvim offers a mostly complete VT220/xterm terminal emulator. The terminal is
-presented as a special buffer type, asynchronously updated to mirror the
-virtual terminal display as data is received from the program connected to it.
-For most purposes, terminal buffers behave a lot like normal buffers with
-'nomodifiable' set.
-
-The implementation is powered by libvterm, a powerful abstract terminal
-emulation library. http://www.leonerd.org.uk/code/libvterm/
+Terminal buffers behave mostly like normal 'nomodifiable' buffers, except:
+- Plugins can set 'modifiable' to modify text, but lines cannot be deleted.
+- 'scrollback' controls how many off-screen lines are kept.
+- Terminal output is followed if the cursor is on the last line.
==============================================================================
-2. Spawning *terminal-emulator-spawning*
+Spawning *terminal-emulator-spawning*
There are 3 ways to create a terminal buffer:
@@ -40,34 +31,27 @@ There are 3 ways to create a terminal buffer:
Note: The "term://" pattern is handled by a BufReadCmd handler, so the
|autocmd-nested| modifier is required to use it in an autocmd. >
autocmd VimEnter * nested split term://sh
-< This is only mentioned for reference; you should use the |:terminal|
- command instead.
+< This is only mentioned for reference; use |:terminal| instead.
When the terminal spawns the program, the buffer will start to mirror the
-terminal display and change its name to `term://$CWD//$PID:$COMMAND`.
-Note that |:mksession| will "save" the terminal buffers by restarting all
-programs when the session is restored.
+terminal display and change its name to `term://{cwd}//{pid}:{cmd}`.
+The "term://..." scheme enables |:mksession| to "restore" a terminal buffer by
+restarting the {cmd} when the session is loaded.
==============================================================================
-3. Input *terminal-emulator-input*
-
-Sending input is possible by entering terminal mode, which is achieved by
-pressing any key that would enter insert mode in a normal buffer (|i| or |a|
-for example). The |:terminal| ex command will automatically enter terminal
-mode once it's spawned. While in terminal mode, Nvim will forward all keys to
-the underlying program. The only exception is the <C-\><C-n> key combo,
-which will exit back to normal mode.
-
-Terminal mode has its own namespace for mappings, which is accessed with the
-"t" prefix. It's possible to use terminal mappings to customize interaction
-with the terminal. For example, here's how to map <Esc> to exit terminal mode:
->
+Input *terminal-emulator-input*
+
+To send input, enter terminal mode using any command that would enter "insert
+mode" in a normal buffer, such as |i| or |:startinsert|. In this mode all keys
+except <C-\><C-N> are sent to the underlying program. Use <C-\><C-N> to return
+to normal mode. |CTRL-\_CTRL-N|
+
+Terminal mode has its own |:tnoremap| namespace for mappings, this can be used
+to automate any terminal interaction. To map <Esc> to exit terminal mode: >
:tnoremap <Esc> <C-\><C-n>
<
-Navigating to other windows is only possible by exiting to normal mode, which
-can be cumbersome with <C-\><C-n> keys. To improve the navigation experience,
-you could use the following mappings:
->
+Navigating to other windows is only possible in normal mode. For convenience,
+you could use these mappings: >
:tnoremap <A-h> <C-\><C-n><C-w>h
:tnoremap <A-j> <C-\><C-n><C-w>j
:tnoremap <A-k> <C-\><C-n><C-w>k
@@ -77,11 +61,9 @@ you could use the following mappings:
:nnoremap <A-k> <C-w>k
:nnoremap <A-l> <C-w>l
<
-This configuration allows using `Alt+{h,j,k,l}` to navigate between windows no
-matter if they are displaying a normal buffer or a terminal buffer in terminal
-mode.
+Then you can use `Alt+{h,j,k,l}` to navigate between windows from any mode.
-Mouse input is also fully supported, and has the following behavior:
+Mouse input is supported, and has the following behavior:
- If the program has enabled mouse events, the corresponding events will be
forwarded to the program.
@@ -93,28 +75,23 @@ Mouse input is also fully supported, and has the following behavior:
the terminal wont lose focus and the hovered window will be scrolled.
==============================================================================
-4. Configuration *terminal-emulator-configuration*
+Configuration *terminal-emulator-configuration*
+
+Options: 'scrollback'
+Events: |TermOpen|, |TermClose|
+Highlight groups: |hl-TermCursor|, |hl-TermCursorNC|
-Terminal buffers can be customized through the following global/buffer-local
-variables (set via the |TermOpen| autocmd):
+Terminal colors can be customized with these variables:
-- `{g,b}:terminal_scrollback_buffer_size`: Scrollback buffer size, between 1
- and 100000 inclusive. The default is 1000.
- `{g,b}:terminal_color_$NUM`: The terminal color palette, where `$NUM` is the
color index, between 0 and 255 inclusive. This setting only affects UIs with
RGB capabilities; for normal terminals the color index is simply forwarded.
-The configuration variables are only processed when the terminal starts, which
-is why it needs to be done with the |TermOpen| autocmd or setting global
-variables before the terminal is started.
-
-There is also a corresponding |TermClose| event.
-
-The terminal cursor can be highlighted via |hl-TermCursor| and
-|hl-TermCursorNC|.
+The `{g,b}:terminal_color_$NUM` variables are processed only when the terminal
+starts (after |TermOpen|).
==============================================================================
-5. Status Variables *terminal-emulator-status*
+Status Variables *terminal-emulator-status*
Terminal buffers maintain some information about the terminal in buffer-local
variables:
@@ -127,11 +104,8 @@ variables:
- *b:terminal_job_pid* The PID of the top-level process running in the
terminal.
-These variables will have a value by the time the TermOpen autocmd runs, and
-will continue to have a value for the lifetime of the terminal buffer, making
-them suitable for use in 'statusline'. For example, to show the terminal title
-as the status line:
->
+These variables are initialized before TermOpen, so you can use them in
+a local 'statusline'. Example: >
:autocmd TermOpen * setlocal statusline=%{b:term_title}
<
==============================================================================
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 4ae4725617..25dca5fb51 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -4949,6 +4949,16 @@ A jump table for the options with a short description can be found at |Q_op|.
be used as the new value for 'scroll'. Reset to half the window
height with ":set scroll=0".
+ *'scrollback'* *'scbk'*
+'scrollback' 'scbk' number (default: 1000
+ in normal buffers: -1)
+ local to buffer
+ Maximum number of lines kept beyond the visible screen. Lines at the
+ top are deleted if new lines exceed this limit.
+ Only in |terminal-emulator| buffers. 'buftype'
+ -1 means "unlimited" for normal buffers, 100000 otherwise.
+ Minimum is 1.
+
*'scrollbind'* *'scb'* *'noscrollbind'* *'noscb'*
'scrollbind' 'scb' boolean (default off)
local to window
@@ -6273,6 +6283,9 @@ A jump table for the options with a short description can be found at |Q_op|.
By default, tag searches are case-sensitive. Case is ignored when
'ignorecase' is set and 'tagcase' is "followic", or when 'tagcase' is
"ignore".
+ Also when 'tagcase' is "followscs" and 'smartcase' is set, or
+ 'tagcase' is "smart", and the pattern contains only lowercase
+ characters.
When 'tagbsearch' is off, tags searching is slower when a full match
exists, but faster when no full match exists. Tags in unsorted tags
@@ -6290,8 +6303,10 @@ A jump table for the options with a short description can be found at |Q_op|.
This option specifies how case is handled when searching the tags
file:
followic Follow the 'ignorecase' option
+ followscs Follow the 'smartcase' and 'ignorecase' options
ignore Ignore case
match Match case
+ smart Ignore case unless an upper case letter is used
*'taglength'* *'tl'*
'taglength' 'tl' number (default 0)
diff --git a/runtime/doc/tagsrch.txt b/runtime/doc/tagsrch.txt
index 75d820d072..047abb42cc 100644
--- a/runtime/doc/tagsrch.txt
+++ b/runtime/doc/tagsrch.txt
@@ -83,14 +83,23 @@ Note that when the current file changes, the priority list is mostly not
changed, to avoid confusion when using ":tnext". It is changed when using
":tag {ident}".
-The ignore-case matches are not found for a ":tag" command when the
-'ignorecase' option is off and 'tagcase' is "followic" or when 'tagcase' is
-"match". They are found when a pattern is used (starting with a "/") and for
-":tselect", also when 'ignorecase' is off and 'tagcase' is "followic" or when
-'tagcase' is "match". Note that using ignore-case tag searching disables
-binary searching in the tags file, which causes a slowdown. This can be
-avoided by fold-case sorting the tag file. See the 'tagbsearch' option for an
-explanation.
+The ignore-case matches are not found for a ":tag" command when:
+- the 'ignorecase' option is off and 'tagcase' is "followic"
+- 'tagcase' is "match"
+- 'tagcase' is "smart" and the pattern contains an upper case character.
+- 'tagcase' is "followscs" and 'smartcase' option is on and the pattern
+ contains an upper case character.
+
+The gnore-case matches are found when:
+- a pattern is used (starting with a "/")
+- for ":tselect"
+- when 'tagcase' is "followic" and 'ignorecase' is off
+- when 'tagcase' is "match"
+- when 'tagcase' is "followscs" and the 'smartcase' option is off
+
+Note that using ignore-case tag searching disables binary searching in the
+tags file, which causes a slowdown. This can be avoided by fold-case sorting
+the tag file. See the 'tagbsearch' option for an explanation.
==============================================================================
2. Tag stack *tag-stack* *tagstack* *E425*
@@ -420,13 +429,18 @@ file "tags". It can also be used to access a common tags file.
The next file in the list is not used when:
- A matching static tag for the current buffer has been found.
- A matching global tag has been found.
-This also depends on whether case is ignored. Case is ignored when
-'ignorecase' is set and 'tagcase' is "followic", or when 'tagcase' is
-"ignore". If case is not ignored, and the tags file only has a match without
-matching case, the next tags file is searched for a match with matching case.
-If no tag with matching case is found, the first match without matching case
-is used. If case is ignored, and a matching global tag with or without
-matching case is found, this one is used, no further tags files are searched.
+This also depends on whether case is ignored. Case is ignored when:
+- 'tagcase' is "followic" and 'ignorecase' is set
+- 'tagcase' is "ignore"
+- 'tagcase' is "smart" and and the pattern only contains lower case
+ characters.
+- 'tagcase' is "followscs" and 'smartcase' is set and and the pattern only
+ contains lower case characters.
+If case is not ignored, and the tags file only has a match without matching
+case, the next tags file is searched for a match with matching case. If no
+tag with matching case is found, the first match without matching case is
+used. If case is ignored, and a matching global tag with or without matching
+case is found, this one is used, no further tags files are searched.
When a tag file name starts with "./", the '.' is replaced with the path of
the current file. This makes it possible to use a tags file in the directory
diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt
index 7d08a6f32a..9a2472e394 100644
--- a/runtime/doc/various.txt
+++ b/runtime/doc/various.txt
@@ -207,23 +207,15 @@ g8 Print the hex values of the bytes used in the
:sh[ell] Removed. |vim-differences| {Nvim}
*:terminal* *:te*
-:te[rminal][!] {cmd} Spawns {cmd} using the current value of 'shell' and
- 'shellcmdflag' in a new terminal buffer. This is
- equivalent to: >
-
+:te[rminal][!] {cmd} Execute {cmd} with 'shell' in a |terminal-emulator|
+ buffer. Equivalent to: >
:enew
:call termopen('{cmd}')
:startinsert
<
- If no {cmd} is given, 'shellcmdflag' will not be sent
- to |termopen()|.
-
- Like |:enew|, it will fail if the current buffer is
- modified, but can be forced with "!". See |termopen()|
- and |terminal-emulator|.
+ See |jobstart()|.
- To switch to terminal mode automatically:
->
+ To enter terminal mode automatically: >
autocmd BufEnter term://* startinsert
<
*:!cmd* *:!* *E34*
diff --git a/src/.valgrind.supp b/src/.valgrind.supp
index 8b630fcaaf..cce22bd632 100644
--- a/src/.valgrind.supp
+++ b/src/.valgrind.supp
@@ -10,7 +10,7 @@
Memcheck:Leak
fun:malloc
fun:uv_spawn
- fun:pipe_process_spawn
+ fun:libuv_process_spawn
fun:process_spawn
fun:job_start
}
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h
index 31e95c9fa6..a1b5633c32 100644
--- a/src/nvim/buffer_defs.h
+++ b/src/nvim/buffer_defs.h
@@ -36,7 +36,7 @@ typedef struct {
// for Map(K, V)
#include "nvim/map.h"
-#define MODIFIABLE(buf) (!buf->terminal && buf->b_p_ma)
+#define MODIFIABLE(buf) (buf->b_p_ma)
/*
* Flags for w_valid.
@@ -91,32 +91,22 @@ typedef struct frame_S frame_T;
// for struct memline (it needs memfile_T)
#include "nvim/memline_defs.h"
-
// for struct memfile, bhdr_T, blocknr_T... (it needs buf_T)
#include "nvim/memfile_defs.h"
-/*
- * This is here because regexp_defs.h needs win_T and buf_T. regprog_T is
- * used below.
- */
+// for regprog_T. Needs win_T and buf_T.
#include "nvim/regexp_defs.h"
-
-// for synstate_T (needs reg_extmatch_T, win_T and buf_T)
+// for synstate_T (needs reg_extmatch_T, win_T, buf_T)
#include "nvim/syntax_defs.h"
-
// for signlist_T
#include "nvim/sign_defs.h"
-
// for bufhl_*_T
#include "nvim/bufhl_defs.h"
typedef Map(linenr_T, bufhl_vec_T) bufhl_info_T;
-// for FileID
-#include "nvim/os/fs_defs.h"
-
-// for Terminal
-#include "nvim/terminal.h"
+#include "nvim/os/fs_defs.h" // for FileID
+#include "nvim/terminal.h" // for Terminal
/*
* The taggy struct is used to store the information about a :tag command.
@@ -663,6 +653,7 @@ struct file_buffer {
char_u *b_p_qe; ///< 'quoteescape'
int b_p_ro; ///< 'readonly'
long b_p_sw; ///< 'shiftwidth'
+ long b_p_scbk; ///< 'scrollback'
int b_p_si; ///< 'smartindent'
long b_p_sts; ///< 'softtabstop'
long b_p_sts_nopaste; ///< b_p_sts saved for paste mode
diff --git a/src/nvim/edit.c b/src/nvim/edit.c
index 73d8793a84..ecc794fb14 100644
--- a/src/nvim/edit.c
+++ b/src/nvim/edit.c
@@ -801,8 +801,8 @@ static int insert_handle_key(InsertState *s)
goto normalchar; // insert CTRL-Z as normal char
}
do_cmdline_cmd("stop");
- s->c = Ctrl_O;
- // FALLTHROUGH
+ ui_cursor_shape(); // may need to update cursor shape
+ break;
case Ctrl_O: // execute one command
if (ctrl_x_mode == CTRL_X_OMNI) {
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index 81a9603994..942a82a040 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -30,6 +30,7 @@
#include "nvim/ex_eval.h"
#include "nvim/ex_getln.h"
#include "nvim/fileio.h"
+#include "nvim/os/fileio.h"
#include "nvim/func_attr.h"
#include "nvim/fold.h"
#include "nvim/getchar.h"
@@ -5846,8 +5847,8 @@ bool garbage_collect(bool testing)
garbage_collect_at_exit = false;
}
- // We advance by two because we add one for items referenced through
- // previous_funccal.
+ // We advance by two (COPYID_INC) because we add one for items referenced
+ // through previous_funccal.
const int copyID = get_copyID();
// 1. Go through all accessible variables and mark all lists and dicts
@@ -18467,29 +18468,60 @@ static void f_winsaveview(typval_T *argvars, typval_T *rettv, FunPtr fptr)
}
/// Writes list of strings to file
-static bool write_list(FILE *fd, list_T *list, bool binary)
-{
- int ret = true;
-
- for (listitem_T *li = list->lv_first; li != NULL; li = li->li_next) {
- for (char_u *s = get_tv_string(&li->li_tv); *s != NUL; ++s) {
- if (putc(*s == '\n' ? NUL : *s, fd) == EOF) {
- ret = false;
- break;
+///
+/// @param fp File to write to.
+/// @param[in] list List to write.
+/// @param[in] binary Whether to write in binary mode.
+///
+/// @return true in case of success, false otherwise.
+static bool write_list(FileDescriptor *const fp, const list_T *const list,
+ const bool binary)
+{
+ int error = 0;
+ for (const listitem_T *li = list->lv_first; li != NULL; li = li->li_next) {
+ const char *const s = (const char *)get_tv_string_chk(
+ (typval_T *)&li->li_tv);
+ if (s == NULL) {
+ return false;
+ }
+ const char *hunk_start = s;
+ for (const char *p = hunk_start;; p++) {
+ if (*p == NUL || *p == NL) {
+ if (p != hunk_start) {
+ const ptrdiff_t written = file_write(fp, hunk_start,
+ (size_t)(p - hunk_start));
+ if (written < 0) {
+ error = (int)written;
+ goto write_list_error;
+ }
+ }
+ if (*p == NUL) {
+ break;
+ } else {
+ hunk_start = p + 1;
+ const ptrdiff_t written = file_write(fp, (char[]){ NUL }, 1);
+ if (written < 0) {
+ error = (int)written;
+ break;
+ }
+ }
}
}
if (!binary || li->li_next != NULL) {
- if (putc('\n', fd) == EOF) {
- ret = false;
- break;
+ const ptrdiff_t written = file_write(fp, "\n", 1);
+ if (written < 0) {
+ error = (int)written;
+ goto write_list_error;
}
}
- if (ret == false) {
- EMSG(_(e_write));
- break;
- }
}
- return ret;
+ if ((error = file_fsync(fp)) != 0) {
+ goto write_list_error;
+ }
+ return true;
+write_list_error:
+ emsgf(_("E80: Error while writing: %s"), os_strerror(error));
+ return false;
}
/// Initializes a static list with 10 items.
@@ -18617,27 +18649,42 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, FunPtr fptr)
bool binary = false;
bool append = false;
if (argvars[2].v_type != VAR_UNKNOWN) {
- if (vim_strchr(get_tv_string(&argvars[2]), 'b')) {
+ const char *const flags = (const char *)get_tv_string_chk(&argvars[2]);
+ if (flags == NULL) {
+ return;
+ }
+ if (strchr(flags, 'b')) {
binary = true;
}
- if (vim_strchr(get_tv_string(&argvars[2]), 'a')) {
+ if (strchr(flags, 'a')) {
append = true;
}
}
- // Always open the file in binary mode, library functions have a mind of
- // their own about CR-LF conversion.
- char_u *fname = get_tv_string(&argvars[1]);
- FILE *fd;
- if (*fname == NUL || (fd = mch_fopen((char *)fname,
- append ? APPENDBIN : WRITEBIN)) == NULL) {
- EMSG2(_(e_notcreate), *fname == NUL ? (char_u *)_("<empty>") : fname);
- rettv->vval.v_number = -1;
+ const char buf[NUMBUFLEN];
+ const char *const fname = (const char *)get_tv_string_buf_chk(&argvars[1],
+ (char_u *)buf);
+ if (fname == NULL) {
+ return;
+ }
+ FileDescriptor *fp;
+ int error;
+ rettv->vval.v_number = -1;
+ if (*fname == NUL) {
+ EMSG(_("E482: Can't open file with an empty name"));
+ } else if ((fp = file_open_new(&error, fname,
+ ((append ? kFileAppend : kFileTruncate)
+ | kFileCreate), 0666)) == NULL) {
+ emsgf(_("E482: Can't open file %s for writing: %s"),
+ fname, os_strerror(error));
} else {
- if (write_list(fd, argvars[0].vval.v_list, binary) == false) {
- rettv->vval.v_number = -1;
+ if (write_list(fp, argvars[0].vval.v_list, binary)) {
+ rettv->vval.v_number = 0;
+ }
+ if ((error = file_free(fp)) != 0) {
+ emsgf(_("E80: Error when closing file %s: %s"),
+ fname, os_strerror(error));
}
- fclose(fd);
}
}
/*
diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c
index a7ef11f168..159e027793 100644
--- a/src/nvim/ex_cmds.c
+++ b/src/nvim/ex_cmds.c
@@ -1131,11 +1131,12 @@ static void do_filter(
*/
++no_wait_return; /* don't call wait_return() while busy */
if (itmp != NULL && buf_write(curbuf, itmp, NULL, line1, line2, eap,
- FALSE, FALSE, FALSE, TRUE) == FAIL) {
- msg_putchar('\n'); /* keep message from buf_write() */
- --no_wait_return;
- if (!aborting())
- (void)EMSG2(_(e_notcreate), itmp); /* will call wait_return */
+ false, false, false, true) == FAIL) {
+ msg_putchar('\n'); // Keep message from buf_write().
+ no_wait_return--;
+ if (!aborting()) {
+ EMSG2(_("E482: Can't create file %s"), itmp); // Will call wait_return.
+ }
goto filterend;
}
if (curbuf != old_curbuf)
diff --git a/src/nvim/globals.h b/src/nvim/globals.h
index 1e0dfcd441..07ea045c13 100644
--- a/src/nvim/globals.h
+++ b/src/nvim/globals.h
@@ -1150,7 +1150,6 @@ EXTERN char_u e_noprev[] INIT(= N_("E34: No previous command"));
EXTERN char_u e_noprevre[] INIT(= N_("E35: No previous regular expression"));
EXTERN char_u e_norange[] INIT(= N_("E481: No range allowed"));
EXTERN char_u e_noroom[] INIT(= N_("E36: Not enough room"));
-EXTERN char_u e_notcreate[] INIT(= N_("E482: Can't create file %s"));
EXTERN char_u e_notmp[] INIT(= N_("E483: Can't get temp file name"));
EXTERN char_u e_notopen[] INIT(= N_("E484: Can't open file %s"));
EXTERN char_u e_notread[] INIT(= N_("E485: Can't read file %s"));
diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c
index 98636263b9..71db7506ad 100644
--- a/src/nvim/msgpack_rpc/channel.c
+++ b/src/nvim/msgpack_rpc/channel.c
@@ -321,14 +321,15 @@ static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data,
if (eof) {
close_channel(channel);
- call_set_error(channel, "Channel was closed by the client");
+ char buf[256];
+ snprintf(buf, sizeof(buf), "channel %" PRIu64 " was closed by the client",
+ channel->id);
+ call_set_error(channel, buf);
goto end;
}
size_t count = rbuffer_size(rbuf);
- DLOG("Feeding the msgpack parser with %u bytes of data from Stream(%p)",
- count,
- stream);
+ DLOG("parsing %u bytes of msgpack data from Stream(%p)", count, stream);
// Feed the unpacker with data
msgpack_unpacker_reserve_buffer(channel->unpacker, count);
@@ -350,11 +351,9 @@ static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data,
complete_call(&unpacked.data, channel);
} else {
char buf[256];
- snprintf(buf,
- sizeof(buf),
- "Channel %" PRIu64 " returned a response that doesn't have "
- "a matching request id. Ensure the client is properly "
- "synchronized",
+ snprintf(buf, sizeof(buf),
+ "channel %" PRIu64 " sent a response without a matching "
+ "request id. Ensure the client is properly synchronized",
channel->id);
call_set_error(channel, buf);
}
@@ -406,7 +405,7 @@ static void handle_request(Channel *channel, msgpack_object *request)
&out_buffer))) {
char buf[256];
snprintf(buf, sizeof(buf),
- "Channel %" PRIu64 " sent an invalid message, closed.",
+ "channel %" PRIu64 " sent an invalid message, closed.",
channel->id);
call_set_error(channel, buf);
}
@@ -716,7 +715,7 @@ static void complete_call(msgpack_object *obj, Channel *channel)
static void call_set_error(Channel *channel, char *msg)
{
- ELOG("msgpack-rpc: %s", msg);
+ ELOG("RPC: %s", msg);
for (size_t i = 0; i < kv_size(channel->call_stack); i++) {
ChannelCallFrame *frame = kv_A(channel->call_stack, i);
frame->returned = true;
@@ -789,10 +788,10 @@ static void decref(Channel *channel)
}
#if MIN_LOG_LEVEL <= DEBUG_LOG_LEVEL
-#define REQ "[request] "
-#define RES "[response] "
-#define NOT "[notification] "
-#define ERR "[error] "
+#define REQ "[request] "
+#define RES "[response] "
+#define NOT "[notify] "
+#define ERR "[error] "
// Cannot define array with negative offsets, so this one is needed to be added
// to MSGPACK_UNPACK_\* values.
@@ -810,7 +809,7 @@ static void log_server_msg(uint64_t channel_id,
{
msgpack_unpacked unpacked;
msgpack_unpacked_init(&unpacked);
- DLOGN("[msgpack-rpc] nvim -> client(%" PRIu64 ") ", channel_id);
+ DLOGN("RPC ->ch %" PRIu64 ": ", channel_id);
const msgpack_unpack_return result =
msgpack_unpack_next(&unpacked, packed->data, packed->size, NULL);
switch (result) {
@@ -847,7 +846,7 @@ static void log_client_msg(uint64_t channel_id,
bool is_request,
msgpack_object msg)
{
- DLOGN("[msgpack-rpc] client(%" PRIu64 ") -> nvim ", channel_id);
+ DLOGN("RPC <-ch %" PRIu64 ": ", channel_id);
log_lock();
FILE *f = open_log_file();
fprintf(f, is_request ? REQ : RES);
diff --git a/src/nvim/normal.c b/src/nvim/normal.c
index 756053dc73..dbd8e153a8 100644
--- a/src/nvim/normal.c
+++ b/src/nvim/normal.c
@@ -2696,8 +2696,6 @@ do_mouse (
else if (((mod_mask & MOD_MASK_CTRL)
|| (mod_mask & MOD_MASK_MULTI_CLICK) == MOD_MASK_2CLICK)
&& bt_quickfix(curbuf)) {
- if (State & INSERT)
- stuffcharReadbuff(Ctrl_O);
if (curwin->w_llist_ref == NULL) { // quickfix window
do_cmdline_cmd(".cc");
} else { // location list window
@@ -7420,27 +7418,23 @@ static void nv_esc(cmdarg_T *cap)
restart_edit = 'a';
}
-/*
- * Handle "A", "a", "I", "i" and <Insert> commands.
- */
+/// Handle "A", "a", "I", "i" and <Insert> commands.
static void nv_edit(cmdarg_T *cap)
{
- /* <Insert> is equal to "i" */
- if (cap->cmdchar == K_INS || cap->cmdchar == K_KINS)
+ // <Insert> is equal to "i"
+ if (cap->cmdchar == K_INS || cap->cmdchar == K_KINS) {
cap->cmdchar = 'i';
+ }
- /* in Visual mode "A" and "I" are an operator */
- if (VIsual_active && (cap->cmdchar == 'A' || cap->cmdchar == 'I'))
+ // in Visual mode "A" and "I" are an operator
+ if (VIsual_active && (cap->cmdchar == 'A' || cap->cmdchar == 'I')) {
v_visop(cap);
-
- /* in Visual mode and after an operator "a" and "i" are for text objects */
- else if ((cap->cmdchar == 'a' || cap->cmdchar == 'i')
- && (cap->oap->op_type != OP_NOP
- || VIsual_active
- )) {
+ // in Visual mode and after an operator "a" and "i" are for text objects
+ } else if ((cap->cmdchar == 'a' || cap->cmdchar == 'i')
+ && (cap->oap->op_type != OP_NOP || VIsual_active)) {
nv_object(cap);
- } else if (!curbuf->b_p_ma && !p_im) {
- /* Only give this error when 'insertmode' is off. */
+ } else if (!curbuf->b_p_ma && !p_im && !curbuf->terminal) {
+ // Only give this error when 'insertmode' is off.
EMSG(_(e_modifiable));
clearop(cap->oap);
} else if (!checkclearopq(cap->oap)) {
diff --git a/src/nvim/option.c b/src/nvim/option.c
index e697ab3f51..8990b59f57 100644
--- a/src/nvim/option.c
+++ b/src/nvim/option.c
@@ -1,24 +1,18 @@
-/*
- * Code to handle user-settable options. This is all pretty much table-
- * driven. Checklist for adding a new option:
- * - Put it in the options array below (copy an existing entry).
- * - For a global option: Add a variable for it in option_defs.h.
- * - For a buffer or window local option:
- * - Add a PV_XX entry to the enum below.
- * - Add a variable to the window or buffer struct in buffer_defs.h.
- * - For a window option, add some code to copy_winopt().
- * - For a buffer option, add some code to buf_copy_options().
- * - For a buffer string option, add code to check_buf_options().
- * - If it's a numeric option, add any necessary bounds checks to do_set().
- * - If it's a list of flags, add some code in do_set(), search for WW_ALL.
- * - When adding an option with expansion (P_EXPAND), but with a different
- * default for Vi and Vim (no P_VI_DEF), add some code at VIMEXP.
- * - Add documentation! One line in doc/help.txt, full description in
- * options.txt, and any other related places.
- * - Add an entry in runtime/optwin.vim.
- * When making changes:
- * - Adjust the help for the option in doc/option.txt.
- */
+// User-settable options. Checklist for adding a new option:
+// - Put it in options.lua
+// - For a global option: Add a variable for it in option_defs.h.
+// - For a buffer or window local option:
+// - Add a BV_XX or WV_XX entry to option_defs.h
+// - Add a variable to the window or buffer struct in buffer_defs.h.
+// - For a window option, add some code to copy_winopt().
+// - For a buffer option, add some code to buf_copy_options().
+// - For a buffer string option, add code to check_buf_options().
+// - If it's a numeric option, add any necessary bounds checks to do_set().
+// - If it's a list of flags, add some code in do_set(), search for WW_ALL.
+// - When adding an option with expansion (P_EXPAND), but with a different
+// default for Vi and Vim (no P_VI_DEF), add some code at VIMEXP.
+// - Add documentation! doc/options.txt, and any other related places.
+// - Add an entry in runtime/optwin.vim.
#define IN_OPTION_C
#include <assert.h>
@@ -161,6 +155,7 @@ static long p_ts;
static long p_tw;
static int p_udf;
static long p_wm;
+static long p_scbk;
static char_u *p_keymap;
/* Saved values for when 'bin' is set. */
@@ -4201,7 +4196,19 @@ set_num_option (
FOR_ALL_TAB_WINDOWS(tp, wp) {
check_colorcolumn(wp);
}
-
+ } else if (pp == &curbuf->b_p_scbk) {
+ // 'scrollback'
+ if (!curbuf->terminal) {
+ errmsg = e_invarg;
+ curbuf->b_p_scbk = -1;
+ } else {
+ if (curbuf->b_p_scbk < -1 || curbuf->b_p_scbk > 100000) {
+ errmsg = e_invarg;
+ curbuf->b_p_scbk = 1000;
+ }
+ // Force the scrollback to take effect.
+ terminal_resize(curbuf->terminal, UINT16_MAX, UINT16_MAX);
+ }
}
/*
@@ -5426,6 +5433,7 @@ static char_u *get_varp(vimoption_T *p)
case PV_PI: return (char_u *)&(curbuf->b_p_pi);
case PV_QE: return (char_u *)&(curbuf->b_p_qe);
case PV_RO: return (char_u *)&(curbuf->b_p_ro);
+ case PV_SCBK: return (char_u *)&(curbuf->b_p_scbk);
case PV_SI: return (char_u *)&(curbuf->b_p_si);
case PV_STS: return (char_u *)&(curbuf->b_p_sts);
case PV_SUA: return (char_u *)&(curbuf->b_p_sua);
@@ -5636,6 +5644,7 @@ void buf_copy_options(buf_T *buf, int flags)
buf->b_p_ai = p_ai;
buf->b_p_ai_nopaste = p_ai_nopaste;
buf->b_p_sw = p_sw;
+ buf->b_p_scbk = -1;
buf->b_p_tw = p_tw;
buf->b_p_tw_nopaste = p_tw_nopaste;
buf->b_p_tw_nobin = p_tw_nobin;
diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h
index 602497461d..9c6393e014 100644
--- a/src/nvim/option_defs.h
+++ b/src/nvim/option_defs.h
@@ -601,11 +601,14 @@ EXTERN int p_tbs; ///< 'tagbsearch'
EXTERN char_u *p_tc; ///< 'tagcase'
EXTERN unsigned tc_flags; ///< flags from 'tagcase'
#ifdef IN_OPTION_C
-static char *(p_tc_values[]) = { "followic", "ignore", "match", NULL };
+static char *(p_tc_values[]) =
+ { "followic", "ignore", "match", "followscs", "smart", NULL };
#endif
#define TC_FOLLOWIC 0x01
#define TC_IGNORE 0x02
#define TC_MATCH 0x04
+#define TC_FOLLOWSCS 0x08
+#define TC_SMART 0x10
EXTERN long p_tl; ///< 'taglength'
EXTERN int p_tr; ///< 'tagrelative'
EXTERN char_u *p_tags; ///< 'tags'
@@ -736,6 +739,7 @@ enum {
, BV_PI
, BV_QE
, BV_RO
+ , BV_SCBK
, BV_SI
, BV_SMC
, BV_SYN
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
index 4130e69858..e12860c0cc 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -1913,6 +1913,14 @@ return {
defaults={if_true={vi=12}}
},
{
+ full_name='scrollback', abbreviation='scbk',
+ type='number', scope={'buffer'},
+ vi_def=true,
+ varname='p_scbk',
+ redraw={'current_buffer'},
+ defaults={if_true={vi=-1}}
+ },
+ {
full_name='scrollbind', abbreviation='scb',
type='bool', scope={'window'},
vi_def=true,
diff --git a/src/nvim/os/fileio.c b/src/nvim/os/fileio.c
index cf5bfd60ae..775f2bd449 100644
--- a/src/nvim/os/fileio.c
+++ b/src/nvim/os/fileio.c
@@ -62,6 +62,8 @@ int file_open(FileDescriptor *const ret_fp, const char *const fname,
FLAG(flags, kFileCreate, O_CREAT|O_WRONLY, kTrue, !(flags & kFileCreateOnly));
FLAG(flags, kFileTruncate, O_TRUNC|O_WRONLY, kTrue,
!(flags & kFileCreateOnly));
+ FLAG(flags, kFileAppend, O_APPEND|O_WRONLY, kTrue,
+ !(flags & kFileCreateOnly));
FLAG(flags, kFileReadOnly, O_RDONLY, kFalse, wr != kTrue);
#ifdef O_NOFOLLOW
FLAG(flags, kFileNoSymlink, O_NOFOLLOW, kNone, true);
@@ -153,7 +155,11 @@ int file_fsync(FileDescriptor *const fp)
fp->_error = 0;
return error;
}
- return os_fsync(fp->fd);
+ const int error = os_fsync(fp->fd);
+ if (error != UV_EINVAL && error != UV_EROFS) {
+ return error;
+ }
+ return 0;
}
/// Buffer used for writing
diff --git a/src/nvim/os/fileio.h b/src/nvim/os/fileio.h
index 2cffd5c851..0b55cc695f 100644
--- a/src/nvim/os/fileio.h
+++ b/src/nvim/os/fileio.h
@@ -30,6 +30,8 @@ typedef enum {
kFileTruncate = 32, ///< Truncate the file if it exists.
///< Implies kFileWriteOnly. Cannot be used with
///< kFileCreateOnly.
+ kFileAppend = 64, ///< Append to the file. Implies kFileWriteOnly. Cannot
+ ///< be used with kFileCreateOnly.
} FileOpenFlags;
static inline bool file_eof(const FileDescriptor *const fp)
diff --git a/src/nvim/screen.c b/src/nvim/screen.c
index 6df443754b..f981fcb875 100644
--- a/src/nvim/screen.c
+++ b/src/nvim/screen.c
@@ -7113,8 +7113,9 @@ void showruler(int always)
}
if ((*p_stl != NUL || *curwin->w_p_stl != NUL) && curwin->w_status_height) {
redraw_custom_statusline(curwin);
- } else
+ } else {
win_redr_ruler(curwin, always);
+ }
if (need_maketitle
|| (p_icon && (stl_syntax & STL_IN_ICON))
diff --git a/src/nvim/search.c b/src/nvim/search.c
index a9766a406e..ba6c4e6548 100644
--- a/src/nvim/search.c
+++ b/src/nvim/search.c
@@ -308,13 +308,19 @@ void free_search_patterns(void)
*/
int ignorecase(char_u *pat)
{
- int ic = p_ic;
+ return ignorecase_opt(pat, p_ic, p_scs);
+}
- if (ic && !no_smartcase && p_scs
+/// As ignorecase() put pass the "ic" and "scs" flags.
+int ignorecase_opt(char_u *pat, int ic_in, int scs)
+{
+ int ic = ic_in;
+ if (ic && !no_smartcase && scs
&& !(ctrl_x_mode && curbuf->b_p_inf)
- )
+ ) {
ic = !pat_has_uppercase(pat);
- no_smartcase = FALSE;
+ }
+ no_smartcase = false;
return ic;
}
diff --git a/src/nvim/tag.c b/src/nvim/tag.c
index e723ea20c9..cc5aac6094 100644
--- a/src/nvim/tag.c
+++ b/src/nvim/tag.c
@@ -1164,6 +1164,12 @@ find_tags (
case TC_MATCH:
p_ic = false;
break;
+ case TC_FOLLOWSCS:
+ p_ic = ignorecase(pat);
+ break;
+ case TC_SMART:
+ p_ic = ignorecase_opt(pat, true, true);
+ break;
default:
assert(false);
}
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
index 76839a9cea..e23c2e5748 100644
--- a/src/nvim/terminal.c
+++ b/src/nvim/terminal.c
@@ -1,18 +1,17 @@
-// VT220/xterm-like terminal emulator implementation for nvim. Powered by
-// libvterm (http://www.leonerd.org.uk/code/libvterm/).
+// VT220/xterm-like terminal emulator.
+// Powered by libvterm http://www.leonerd.org.uk/code/libvterm
//
// libvterm is a pure C99 terminal emulation library with abstract input and
// display. This means that the library needs to read data from the master fd
// and feed VTerm instances, which will invoke user callbacks with screen
// update instructions that must be mirrored to the real display.
//
-// Keys are pressed in VTerm instances by calling
+// Keys are sent to VTerm instances by calling
// vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that
// must be fed back to the master fd.
//
-// This implementation uses nvim buffers as the display mechanism for both
-// the visible screen and the scrollback buffer. When focused, the window
-// "pins" to the bottom of the buffer and mirrors libvterm screen state.
+// Nvim buffers are used as the display mechanism for both the visible screen
+// and the scrollback buffer.
//
// When a line becomes invisible due to a decrease in screen height or because
// a line was pushed up during normal terminal output, we store the line
@@ -23,18 +22,17 @@
// scrollback buffer, which is mirrored in the nvim buffer displaying lines
// that were previously invisible.
//
-// The vterm->nvim synchronization is performed in intervals of 10
-// milliseconds. This is done to minimize screen updates when receiving large
-// bursts of data.
+// The vterm->nvim synchronization is performed in intervals of 10 milliseconds,
+// to minimize screen updates when receiving large bursts of data.
//
// This module is decoupled from the processes that normally feed it data, so
// it's possible to use it as a general purpose console buffer (possibly as a
// log/display mechanism for nvim in the future)
//
-// Inspired by vimshell (http://www.wana.at/vimshell/) and
-// Conque (https://code.google.com/p/conque/). Libvterm usage instructions (plus
-// some extra code) were taken from
-// pangoterm (http://www.leonerd.org.uk/code/pangoterm/)
+// Inspired by: vimshell http://www.wana.at/vimshell
+// Conque https://code.google.com/p/conque
+// Some code from pangoterm http://www.leonerd.org.uk/code/pangoterm
+
#include <assert.h>
#include <stdio.h>
#include <stdint.h>
@@ -87,10 +85,10 @@ typedef struct terminal_state {
# include "terminal.c.generated.h"
#endif
-#define SCROLLBACK_BUFFER_DEFAULT_SIZE 1000
+#define SB_MAX 100000 // Maximum 'scrollback' value.
+
// Delay for refreshing the terminal buffer after receiving updates from
-// libvterm. This is greatly improves performance when receiving large bursts
-// of data.
+// libvterm. Improves performance when receiving large bursts of data.
#define REFRESH_DELAY 10
static TimeWatcher refresh_timer;
@@ -102,27 +100,23 @@ typedef struct {
} ScrollbackLine;
struct terminal {
- // options passed to terminal_open
- TerminalOptions opts;
- // libvterm structures
+ TerminalOptions opts; // options passed to terminal_open
VTerm *vt;
VTermScreen *vts;
// buffer used to:
// - convert VTermScreen cell arrays into utf8 strings
// - receive data from libvterm as a result of key presses.
char textbuf[0x1fff];
- // Scrollback buffer storage for libvterm.
- // TODO(tarruda): Use a doubly-linked list
- ScrollbackLine **sb_buffer;
- // number of rows pushed to sb_buffer
- size_t sb_current;
- // sb_buffer size;
- size_t sb_size;
+
+ ScrollbackLine **sb_buffer; // Scrollback buffer storage for libvterm
+ size_t sb_current; // number of rows pushed to sb_buffer
+ size_t sb_size; // sb_buffer size
// "virtual index" that points to the first sb_buffer row that we need to
// push to the terminal buffer when refreshing the scrollback. When negative,
// it actually points to entries that are no longer in sb_buffer (because the
// window height has increased) and must be deleted from the terminal buffer
int sb_pending;
+
// buf_T instance that acts as a "drawing surface" for libvterm
// we can't store a direct reference to the buffer because the
// refresh_timer_cb may be called after the buffer was freed, and there's
@@ -130,20 +124,18 @@ struct terminal {
handle_T buf_handle;
// program exited
bool closed, destroy;
+
// some vterm properties
bool forward_mouse;
- // invalid rows libvterm screen
- int invalid_start, invalid_end;
+ int invalid_start, invalid_end; // invalid rows in libvterm screen
struct {
int row, col;
bool visible;
} cursor;
- // which mouse button is pressed
- int pressed_button;
- // pending width/height
- bool pending_resize;
- // With a reference count of 0 the terminal can be freed.
- size_t refcount;
+ int pressed_button; // which mouse button is pressed
+ bool pending_resize; // pending width/height
+
+ size_t refcount; // reference count
};
static VTermScreenCallbacks vterm_screen_callbacks = {
@@ -237,25 +229,22 @@ Terminal *terminal_open(TerminalOptions opts)
rv->invalid_end = opts.height;
refresh_screen(rv, curbuf);
set_option_value((uint8_t *)"buftype", 0, (uint8_t *)"terminal", OPT_LOCAL);
- // some sane settings for terminal buffers
+
+ // Default settings for terminal buffers
+ curbuf->b_p_ma = false; // 'nomodifiable'
+ curbuf->b_p_ul = -1; // disable undo
+ curbuf->b_p_scbk = 1000; // 'scrollback'
set_option_value((uint8_t *)"wrap", false, NULL, OPT_LOCAL);
set_option_value((uint8_t *)"number", false, NULL, OPT_LOCAL);
set_option_value((uint8_t *)"relativenumber", false, NULL, OPT_LOCAL);
buf_set_term_title(curbuf, (char *)curbuf->b_ffname);
RESET_BINDING(curwin);
- // Apply TermOpen autocmds so the user can configure the terminal
+
+ // Apply TermOpen autocmds _before_ configuring the scrollback buffer.
apply_autocmds(EVENT_TERMOPEN, NULL, NULL, false, curbuf);
- // Configure the scrollback buffer. Try to get the size from:
- //
- // - b:terminal_scrollback_buffer_size
- // - g:terminal_scrollback_buffer_size
- // - SCROLLBACK_BUFFER_DEFAULT_SIZE
- //
- // but limit to 100k.
- int size = get_config_int("terminal_scrollback_buffer_size");
- rv->sb_size = size > 0 ? (size_t)size : SCROLLBACK_BUFFER_DEFAULT_SIZE;
- rv->sb_size = MIN(rv->sb_size, 100000);
+ // Configure the scrollback buffer.
+ rv->sb_size = curbuf->b_p_scbk < 0 ? SB_MAX : (size_t)curbuf->b_p_scbk;;
rv->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * rv->sb_size);
if (!true_color) {
@@ -334,22 +323,22 @@ void terminal_close(Terminal *term, char *msg)
void terminal_resize(Terminal *term, uint16_t width, uint16_t height)
{
if (term->closed) {
- // will be called after exited if two windows display the same terminal and
- // one of the is closed as a consequence of pressing a key.
+ // If two windows display the same terminal and one is closed by keypress.
return;
}
+ bool force = width == UINT16_MAX || height == UINT16_MAX;
int curwidth, curheight;
vterm_get_size(term->vt, &curheight, &curwidth);
- if (!width) {
+ if (force || !width) {
width = (uint16_t)curwidth;
}
- if (!height) {
+ if (force || !height) {
height = (uint16_t)curheight;
}
- if (curheight == height && curwidth == width) {
+ if (!force && curheight == height && curwidth == width) {
return;
}
@@ -381,8 +370,7 @@ void terminal_enter(void)
State = TERM_FOCUS;
mapped_ctrl_c |= TERM_FOCUS; // Always map CTRL-C to avoid interrupt.
RedrawingDisabled = false;
- // go to the bottom when the terminal is focused
- adjust_topline(s->term, buf, false);
+ adjust_topline(s->term, buf, 0); // scroll to end
// erase the unfocused cursor
invalidate_terminal(s->term, s->term->cursor.row, s->term->cursor.row + 1);
showmode();
@@ -667,10 +655,15 @@ static int term_bell(void *data)
return 1;
}
-// the scrollback push/pop handlers were copied almost verbatim from pangoterm
+// Scrollback push handler (from pangoterm).
static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
{
Terminal *term = data;
+
+ if (!term->sb_size) {
+ return 0;
+ }
+
// copy vterm cells into sb_buffer
size_t c = (size_t)cols;
ScrollbackLine *sbrow = NULL;
@@ -682,10 +675,12 @@ static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
xfree(term->sb_buffer[term->sb_current - 1]);
}
+ // Make room at the start by shifting to the right.
memmove(term->sb_buffer + 1, term->sb_buffer,
sizeof(term->sb_buffer[0]) * (term->sb_current - 1));
} else if (term->sb_current > 0) {
+ // Make room at the start by shifting to the right.
memmove(term->sb_buffer + 1, term->sb_buffer,
sizeof(term->sb_buffer[0]) * term->sb_current);
}
@@ -695,6 +690,7 @@ static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
sbrow->cols = c;
}
+ // New row is added at the start of the storage buffer.
term->sb_buffer[0] = sbrow;
if (term->sb_current < term->sb_size) {
term->sb_current++;
@@ -710,6 +706,11 @@ static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
return 1;
}
+/// Scrollback pop handler (from pangoterm).
+///
+/// @param cols
+/// @param cells VTerm state to update.
+/// @param data Terminal
static int term_sb_pop(int cols, VTermScreenCell *cells, void *data)
{
Terminal *term = data;
@@ -722,24 +723,24 @@ static int term_sb_pop(int cols, VTermScreenCell *cells, void *data)
term->sb_pending--;
}
- // restore vterm state
- size_t c = (size_t)cols;
ScrollbackLine *sbrow = term->sb_buffer[0];
term->sb_current--;
+ // Forget the "popped" row by shifting the rest onto it.
memmove(term->sb_buffer, term->sb_buffer + 1,
sizeof(term->sb_buffer[0]) * (term->sb_current));
- size_t cols_to_copy = c;
+ size_t cols_to_copy = (size_t)cols;
if (cols_to_copy > sbrow->cols) {
cols_to_copy = sbrow->cols;
}
// copy to vterm state
memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy);
- for (size_t col = cols_to_copy; col < c; col++) {
+ for (size_t col = cols_to_copy; col < (size_t)cols; col++) {
cells[col].chars[0] = 0;
cells[col].width = 1;
}
+
xfree(sbrow);
pmap_put(ptr_t)(invalidated_terminals, term, NULL);
@@ -885,7 +886,7 @@ static bool send_mouse_event(Terminal *term, int c)
// terminal buffer refresh & misc {{{
-void fetch_row(Terminal *term, int row, int end_col)
+static void fetch_row(Terminal *term, int row, int end_col)
{
int col = 0;
size_t line_len = 0;
@@ -958,23 +959,23 @@ static void refresh_terminal(Terminal *term)
buf_T *buf = handle_get_buffer(term->buf_handle);
bool valid = true;
if (!buf || !(valid = buf_valid(buf))) {
- // destroyed by `close_buffer`. Dont do anything else
+ // Destroyed by `close_buffer`. Do not do anything else.
if (!valid) {
term->buf_handle = 0;
}
return;
}
- bool pending_resize = term->pending_resize;
+ long ml_before = buf->b_ml.ml_line_count;
WITH_BUFFER(buf, {
refresh_size(term, buf);
refresh_scrollback(term, buf);
refresh_screen(term, buf);
redraw_buf_later(buf, NOT_VALID);
});
- adjust_topline(term, buf, pending_resize);
+ long ml_added = buf->b_ml.ml_line_count - ml_before;
+ adjust_topline(term, buf, ml_added);
}
-// libuv timer callback. This will enqueue on_refresh to be processed as an
-// event.
+// Calls refresh_terminal() on all invalidated_terminals.
static void refresh_timer_cb(TimeWatcher *watcher, void *data)
{
if (exiting) { // Cannot redraw (requires event loop) during teardown/exit.
@@ -1008,7 +1009,37 @@ static void refresh_size(Terminal *term, buf_T *buf)
term->opts.resize_cb((uint16_t)width, (uint16_t)height, term->opts.data);
}
-// Refresh the scrollback of a invalidated terminal
+/// Adjusts scrollback storage after 'scrollback' option changed.
+static void on_scrollback_option_changed(Terminal *term, buf_T *buf)
+{
+ const size_t scbk = curbuf->b_p_scbk < 0
+ ? SB_MAX : (size_t)MAX(1, curbuf->b_p_scbk);
+ assert(term->sb_current < SIZE_MAX);
+ if (term->sb_pending > 0) { // Pending rows must be processed first.
+ abort();
+ }
+
+ // Delete lines exceeding the new 'scrollback' limit.
+ if (scbk < term->sb_current) {
+ size_t diff = term->sb_current - scbk;
+ for (size_t i = 0; i < diff; i++) {
+ ml_delete(1, false);
+ term->sb_current--;
+ xfree(term->sb_buffer[term->sb_current]);
+ }
+ deleted_lines(1, (long)diff);
+ }
+
+ // Resize the scrollback storage.
+ size_t sb_region = sizeof(ScrollbackLine *) * scbk;
+ if (scbk != term->sb_size) {
+ term->sb_buffer = xrealloc(term->sb_buffer, sb_region);
+ }
+
+ term->sb_size = scbk;
+}
+
+// Refresh the scrollback of an invalidated terminal.
static void refresh_scrollback(Terminal *term, buf_T *buf)
{
int width, height;
@@ -1037,9 +1068,11 @@ static void refresh_scrollback(Terminal *term, buf_T *buf)
ml_delete(buf->b_ml.ml_line_count, false);
deleted_lines(buf->b_ml.ml_line_count, 1);
}
+
+ on_scrollback_option_changed(term, buf);
}
-// Refresh the screen(visible part of the buffer when the terminal is
+// Refresh the screen (visible part of the buffer when the terminal is
// focused) of a invalidated terminal
static void refresh_screen(Terminal *term, buf_T *buf)
{
@@ -1048,8 +1081,7 @@ static void refresh_screen(Terminal *term, buf_T *buf)
int height;
int width;
vterm_get_size(term->vt, &height, &width);
- // It's possible that the terminal height decreased and `term->invalid_end`
- // doesn't reflect it yet
+ // Terminal height may have decreased before `invalid_end` reflects it.
term->invalid_end = MIN(term->invalid_end, height);
for (int r = term->invalid_start, linenr = row_to_linenr(term, r);
@@ -1094,14 +1126,6 @@ static void redraw(bool restore_cursor)
update_screen(0);
}
- redraw_statuslines();
-
- if (need_maketitle) {
- maketitle();
- }
-
- showruler(false);
-
if (term && is_focused(term)) {
curwin->w_wrow = term->cursor.row;
curwin->w_wcol = term->cursor.col + win_col_off(curwin);
@@ -1121,21 +1145,21 @@ static void redraw(bool restore_cursor)
ui_flush();
}
-static void adjust_topline(Terminal *term, buf_T *buf, bool force)
+static void adjust_topline(Terminal *term, buf_T *buf, long added)
{
int height, width;
vterm_get_size(term->vt, &height, &width);
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (wp->w_buffer == buf) {
- // for every window that displays a terminal, ensure the cursor is in a
- // valid line
- wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, buf->b_ml.ml_line_count);
- if (force || curbuf != buf || is_focused(term)) {
- // if the terminal is not in the current window or if it's focused,
- // adjust topline/cursor so the window will "follow" the terminal
- // output
- wp->w_cursor.lnum = buf->b_ml.ml_line_count;
+ linenr_T ml_end = buf->b_ml.ml_line_count;
+ bool following = ml_end == wp->w_cursor.lnum + added; // cursor at end?
+ if (following || (wp == curwin && is_focused(term))) {
+ // "Follow" the terminal output
+ wp->w_cursor.lnum = ml_end;
set_topline(wp, MAX(wp->w_cursor.lnum - height + 1, 1));
+ } else {
+ // Ensure valid cursor for each window displaying this terminal.
+ wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, ml_end);
}
}
}
@@ -1178,17 +1202,6 @@ static char *get_config_string(char *key)
return NULL;
}
-static int get_config_int(char *key)
-{
- Object obj;
- GET_CONFIG_VALUE(key, obj);
- if (obj.type == kObjectTypeInteger) {
- return (int)obj.data.integer;
- }
- api_free_object(obj);
- return 0;
-}
-
// }}}
// vim: foldmethod=marker
diff --git a/src/nvim/testdir/test_alot.vim b/src/nvim/testdir/test_alot.vim
index 375d8219e6..04cc279619 100644
--- a/src/nvim/testdir/test_alot.vim
+++ b/src/nvim/testdir/test_alot.vim
@@ -25,6 +25,7 @@ source test_statusline.vim
source test_syn_attr.vim
source test_tabline.vim
source test_tabpage.vim
+source test_tagcase.vim
source test_tagjump.vim
source test_unlet.vim
source test_window_cmd.vim
diff --git a/src/nvim/testdir/test_tagcase.vim b/src/nvim/testdir/test_tagcase.vim
new file mode 100644
index 0000000000..833cb9f990
--- /dev/null
+++ b/src/nvim/testdir/test_tagcase.vim
@@ -0,0 +1,73 @@
+" test 'tagcase' option
+
+func Test_tagcase()
+ call writefile(["Bar\tXtext\t3", "Foo\tXtext\t2", "foo\tXtext\t4"], 'Xtags')
+ set tags=Xtags
+ e Xtext
+
+ for &ic in [0, 1]
+ for &scs in [0, 1]
+ for &g:tc in ["followic", "ignore", "match", "followscs", "smart"]
+ for &l:tc in ["", "followic", "ignore", "match", "followscs", "smart"]
+ let smart = 0
+ if &l:tc != ''
+ let tc = &l:tc
+ else
+ let tc = &g:tc
+ endif
+ if tc == 'followic'
+ let ic = &ic
+ elseif tc == 'ignore'
+ let ic = 1
+ elseif tc == 'followscs'
+ let ic = &ic
+ let smart = &scs
+ elseif tc == 'smart'
+ let ic = 1
+ let smart = 1
+ else
+ let ic = 0
+ endif
+ if ic && smart
+ call assert_equal(['foo', 'Foo'], map(taglist("^foo$"), {i, v -> v.name}))
+ call assert_equal(['Foo'], map(taglist("^Foo$"), {i, v -> v.name}))
+ elseif ic
+ call assert_equal(['foo', 'Foo'], map(taglist("^foo$"), {i, v -> v.name}))
+ call assert_equal(['Foo', 'foo'], map(taglist("^Foo$"), {i, v -> v.name}))
+ else
+ call assert_equal(['foo'], map(taglist("^foo$"), {i, v -> v.name}))
+ call assert_equal(['Foo'], map(taglist("^Foo$"), {i, v -> v.name}))
+ endif
+ endfor
+ endfor
+ endfor
+ endfor
+
+ call delete('Xtags')
+ set ic&
+ setg tc&
+ setl tc&
+ set scs&
+endfunc
+
+func Test_set_tagcase()
+ " Verify default values.
+ set ic&
+ setg tc&
+ setl tc&
+ call assert_equal(0, &ic)
+ call assert_equal('followic', &g:tc)
+ call assert_equal('followic', &l:tc)
+ call assert_equal('followic', &tc)
+
+ " Verify that the local setting accepts <empty> but that the global setting
+ " does not. The first of these (setting the local value to <empty>) should
+ " succeed; the other two should fail.
+ setl tc=
+ call assert_fails('setg tc=', 'E474:')
+ call assert_fails('set tc=', 'E474:')
+
+ set ic&
+ setg tc&
+ setl tc&
+endfunc
diff --git a/src/nvim/undo.c b/src/nvim/undo.c
index 8cedfcb905..c95a795587 100644
--- a/src/nvim/undo.c
+++ b/src/nvim/undo.c
@@ -305,16 +305,12 @@ bool undo_allowed(void)
return true;
}
-/*
- * Get the undolevle value for the current buffer.
- */
+/// Get the 'undolevels' value for the current buffer.
static long get_undolevel(void)
{
- if (curbuf->terminal) {
- return -1;
- }
- if (curbuf->b_p_ul == NO_LOCAL_UNDOLEVEL)
+ if (curbuf->b_p_ul == NO_LOCAL_UNDOLEVEL) {
return p_ul;
+ }
return curbuf->b_p_ul;
}
diff --git a/src/nvim/version.c b/src/nvim/version.c
index 3787ba6908..fe443ce01f 100644
--- a/src/nvim/version.c
+++ b/src/nvim/version.c
@@ -210,7 +210,7 @@ static int included_patches[] = {
2233,
// 2232 NA
// 2231,
- // 2230,
+ 2230,
// 2229,
// 2228,
2227,
diff --git a/src/nvim/window.c b/src/nvim/window.c
index 28269e8889..73a60b2e04 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -4848,7 +4848,7 @@ void scroll_to_fraction(win_T *wp, int prev_height)
if (wp->w_buffer->terminal) {
terminal_resize(wp->w_buffer->terminal, 0, wp->w_height);
- redraw_win_later(wp, CLEAR);
+ redraw_win_later(wp, NOT_VALID);
}
}
@@ -4872,7 +4872,6 @@ void win_new_width(win_T *wp, int width)
if (wp->w_height != 0) {
terminal_resize(wp->w_buffer->terminal, wp->w_width, 0);
}
- redraw_win_later(wp, CLEAR);
}
}
diff --git a/test/functional/eval/writefile_spec.lua b/test/functional/eval/writefile_spec.lua
new file mode 100644
index 0000000000..3052c616e0
--- /dev/null
+++ b/test/functional/eval/writefile_spec.lua
@@ -0,0 +1,140 @@
+local helpers = require('test.functional.helpers')(after_each)
+local lfs = require('lfs')
+
+local clear = helpers.clear
+local eq = helpers.eq
+local funcs = helpers.funcs
+local meths = helpers.meths
+local exc_exec = helpers.exc_exec
+local read_file = helpers.read_file
+local write_file = helpers.write_file
+local redir_exec = helpers.redir_exec
+
+local fname = 'Xtest-functional-eval-writefile'
+local dname = fname .. '.d'
+local dfname_tail = '1'
+local dfname = dname .. '/' .. dfname_tail
+local ddname_tail = '2'
+local ddname = dname .. '/' .. ddname_tail
+
+before_each(function()
+ lfs.mkdir(dname)
+ lfs.mkdir(ddname)
+ clear()
+end)
+
+after_each(function()
+ os.remove(fname)
+ os.remove(dfname)
+ lfs.rmdir(ddname)
+ lfs.rmdir(dname)
+end)
+
+describe('writefile()', function()
+ it('writes empty list to a file', function()
+ eq(nil, read_file(fname))
+ eq(0, funcs.writefile({}, fname))
+ eq('', read_file(fname))
+ os.remove(fname)
+ eq(nil, read_file(fname))
+ eq(0, funcs.writefile({}, fname, 'b'))
+ eq('', read_file(fname))
+ os.remove(fname)
+ eq(nil, read_file(fname))
+ eq(0, funcs.writefile({}, fname, 'ab'))
+ eq('', read_file(fname))
+ os.remove(fname)
+ eq(nil, read_file(fname))
+ eq(0, funcs.writefile({}, fname, 'a'))
+ eq('', read_file(fname))
+ end)
+
+ it('writes list with an empty string to a file', function()
+ eq(0, exc_exec(
+ ('call writefile([$XXX_NONEXISTENT_VAR_XXX], "%s", "b")'):format(
+ fname)))
+ eq('', read_file(fname))
+ eq(0, exc_exec(('call writefile([$XXX_NONEXISTENT_VAR_XXX], "%s")'):format(
+ fname)))
+ eq('\n', read_file(fname))
+ end)
+
+ it('appends to a file', function()
+ eq(nil, read_file(fname))
+ eq(0, funcs.writefile({'abc', 'def', 'ghi'}, fname))
+ eq('abc\ndef\nghi\n', read_file(fname))
+ eq(0, funcs.writefile({'jkl'}, fname, 'a'))
+ eq('abc\ndef\nghi\njkl\n', read_file(fname))
+ os.remove(fname)
+ eq(nil, read_file(fname))
+ eq(0, funcs.writefile({'abc', 'def', 'ghi'}, fname, 'b'))
+ eq('abc\ndef\nghi', read_file(fname))
+ eq(0, funcs.writefile({'jkl'}, fname, 'ab'))
+ eq('abc\ndef\nghijkl', read_file(fname))
+ end)
+
+ it('correctly treats NLs', function()
+ eq(0, funcs.writefile({'\na\nb\n'}, fname, 'b'))
+ eq('\0a\0b\0', read_file(fname))
+ eq(0, funcs.writefile({'a\n\n\nb'}, fname, 'b'))
+ eq('a\0\0\0b', read_file(fname))
+ end)
+
+ it('correctly overwrites file', function()
+ eq(0, funcs.writefile({'\na\nb\n'}, fname, 'b'))
+ eq('\0a\0b\0', read_file(fname))
+ eq(0, funcs.writefile({'a\n'}, fname, 'b'))
+ eq('a\0', read_file(fname))
+ end)
+
+ it('shows correct file name when supplied numbers', function()
+ meths.set_current_dir(dname)
+ eq('\nE482: Can\'t open file 2 for writing: illegal operation on a directory',
+ redir_exec(('call writefile([42], %s)'):format(ddname_tail)))
+ end)
+
+ it('errors out with invalid arguments', function()
+ write_file(fname, 'TEST')
+ eq('\nE119: Not enough arguments for function: writefile',
+ redir_exec('call writefile()'))
+ eq('\nE119: Not enough arguments for function: writefile',
+ redir_exec('call writefile([])'))
+ eq('\nE118: Too many arguments for function: writefile',
+ redir_exec(('call writefile([], "%s", "b", 1)'):format(fname)))
+ for _, arg in ipairs({'0', '0.0', 'function("tr")', '{}', '"test"'}) do
+ eq('\nE686: Argument of writefile() must be a List',
+ redir_exec(('call writefile(%s, "%s", "b")'):format(arg, fname)))
+ end
+ for _, args in ipairs({'[], %s, "b"', '[], "' .. fname .. '", %s'}) do
+ eq('\nE806: using Float as a String',
+ redir_exec(('call writefile(%s)'):format(args:format('0.0'))))
+ eq('\nE730: using List as a String',
+ redir_exec(('call writefile(%s)'):format(args:format('[]'))))
+ eq('\nE731: using Dictionary as a String',
+ redir_exec(('call writefile(%s)'):format(args:format('{}'))))
+ eq('\nE729: using Funcref as a String',
+ redir_exec(('call writefile(%s)'):format(args:format('function("tr")'))))
+ end
+ eq('TEST', read_file(fname))
+ end)
+
+ it('stops writing to file after error in list', function()
+ local args = '["tset"] + repeat([%s], 3), "' .. fname .. '"'
+ eq('\nE806: using Float as a String',
+ redir_exec(('call writefile(%s)'):format(args:format('0.0'))))
+ eq('tset\n', read_file(fname))
+ write_file(fname, 'TEST')
+ eq('\nE730: using List as a String',
+ redir_exec(('call writefile(%s)'):format(args:format('[]'))))
+ eq('tset\n', read_file(fname))
+ write_file(fname, 'TEST')
+ eq('\nE731: using Dictionary as a String',
+ redir_exec(('call writefile(%s)'):format(args:format('{}'))))
+ eq('tset\n', read_file(fname))
+ write_file(fname, 'TEST')
+ eq('\nE729: using Funcref as a String',
+ redir_exec(('call writefile(%s)'):format(args:format('function("tr")'))))
+ eq('tset\n', read_file(fname))
+ write_file(fname, 'TEST')
+ end)
+end)
diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua
index 14cab293ac..65d1ad76ef 100644
--- a/test/functional/helpers.lua
+++ b/test/functional/helpers.lua
@@ -344,6 +344,16 @@ local function write_file(name, text, dont_dedent)
file:close()
end
+local function read_file(name)
+ local file = io.open(name, 'r')
+ if not file then
+ return nil
+ end
+ local ret = file:read('*a')
+ file:close()
+ return ret
+end
+
local function source(code)
local fname = tmpname()
write_file(fname, code)
@@ -593,6 +603,7 @@ local M = {
sleep = sleep,
set_session = set_session,
write_file = write_file,
+ read_file = read_file,
os_name = os_name,
rmdir = rmdir,
mkdir = lfs.mkdir,
diff --git a/test/functional/terminal/edit_spec.lua b/test/functional/terminal/edit_spec.lua
index 8edcfa56b7..42a5c768bb 100644
--- a/test/functional/terminal/edit_spec.lua
+++ b/test/functional/terminal/edit_spec.lua
@@ -31,45 +31,41 @@ describe(':edit term://*', function()
eq(termopen_runs[1], termopen_runs[1]:match('^term://.//%d+:$'))
end)
- it('runs TermOpen early enough to respect terminal_scrollback_buffer_size', function()
+ it("runs TermOpen early enough to set buffer-local 'scrollback'", function()
local columns, lines = 20, 4
local scr = get_screen(columns, lines)
local rep = 'a'
meths.set_option('shellcmdflag', 'REP ' .. rep)
- local rep_size = rep:byte()
+ local rep_size = rep:byte() -- 'a' => 97
local sb = 10
- local gsb = 20
- meths.set_var('terminal_scrollback_buffer_size', gsb)
- command('autocmd TermOpen * :let b:terminal_scrollback_buffer_size = '
- .. tostring(sb))
+ command('autocmd TermOpen * :setlocal scrollback='..tostring(sb)
+ ..'|call feedkeys("G", "n")')
command('edit term://foobar')
+
local bufcontents = {}
local winheight = curwinmeths.get_height()
- -- I have no idea why there is + 4 needed. But otherwise it works fine with
- -- different scrollbacks.
- local shift = -4
- local buf_cont_start = rep_size - 1 - sb - winheight - shift
- local bufline = function(i) return ('%d: foobar'):format(i) end
+ local buf_cont_start = rep_size - sb - winheight + 2
+ local function bufline (i)
+ return ('%d: foobar'):format(i)
+ end
for i = buf_cont_start,(rep_size - 1) do
bufcontents[#bufcontents + 1] = bufline(i)
end
bufcontents[#bufcontents + 1] = ''
bufcontents[#bufcontents + 1] = '[Process exited 0]'
- -- Do not ask me why displayed screen is one line *before* buffer
- -- contents: buffer starts with 87:, screen with 86:.
+
local exp_screen = '\n'
- local did_cursor = false
- for i = 0,(winheight - 1) do
- local line = bufline(buf_cont_start + i - 1)
+ for i = 1,(winheight - 1) do
+ local line = bufcontents[#bufcontents - winheight + i]
exp_screen = (exp_screen
- .. (did_cursor and '' or '^')
.. line
.. (' '):rep(columns - #line)
.. '|\n')
- did_cursor = true
end
- exp_screen = exp_screen .. (' '):rep(columns) .. '|\n'
+ exp_screen = exp_screen..'^[Process exited 0] |\n'
+
+ exp_screen = exp_screen..(' '):rep(columns)..'|\n'
scr:expect(exp_screen)
- eq(bufcontents, curbufmeths.get_lines(1, -1, true))
+ eq(bufcontents, curbufmeths.get_lines(0, -1, true))
end)
end)
diff --git a/test/functional/terminal/ex_terminal_spec.lua b/test/functional/terminal/ex_terminal_spec.lua
index 7c391db18c..7a9d2a9b36 100644
--- a/test/functional/terminal/ex_terminal_spec.lua
+++ b/test/functional/terminal/ex_terminal_spec.lua
@@ -20,26 +20,34 @@ describe(':terminal', function()
source([[
echomsg "msg1"
echomsg "msg2"
+ echomsg "msg3"
]])
-- Invoke a command that emits frequent terminal activity.
execute([[terminal while true; do echo X; done]])
helpers.feed([[<C-\><C-N>]])
- screen:expect([[
- X |
- X |
- ^X |
- |
- ]])
+ wait()
helpers.sleep(10) -- Let some terminal activity happen.
execute("messages")
screen:expect([[
- X |
msg1 |
msg2 |
+ msg3 |
Press ENTER or type command to continue^ |
]])
end)
+ it("in normal-mode :split does not move cursor", function()
+ execute([[terminal while true; do echo foo; sleep .1; done]])
+ helpers.feed([[<C-\><C-N>M]]) -- move cursor away from last line
+ wait()
+ eq(3, eval("line('$')")) -- window height
+ eq(2, eval("line('.')")) -- cursor is in the middle
+ execute('vsplit')
+ eq(2, eval("line('.')")) -- cursor stays where we put it
+ execute('split')
+ eq(2, eval("line('.')")) -- cursor stays where we put it
+ end)
+
end)
describe(':terminal (with fake shell)', function()
diff --git a/test/functional/terminal/helpers.lua b/test/functional/terminal/helpers.lua
index ae5e6d4b1f..934c01e3bf 100644
--- a/test/functional/terminal/helpers.lua
+++ b/test/functional/terminal/helpers.lua
@@ -1,7 +1,7 @@
local helpers = require('test.functional.helpers')(nil)
local Screen = require('test.functional.ui.screen')
local nvim_dir = helpers.nvim_dir
-local execute, nvim, wait = helpers.execute, helpers.nvim, helpers.wait
+local execute, nvim = helpers.execute, helpers.nvim
local function feed_data(data)
nvim('set_var', 'term_data', data)
@@ -34,13 +34,15 @@ local function disable_mouse() feed_termcode('[?1002l') end
local default_command = '["'..nvim_dir..'/tty-test'..'"]'
-local function screen_setup(extra_height, command)
+local function screen_setup(extra_rows, command, cols)
+ extra_rows = extra_rows and extra_rows or 0
+ command = command and command or default_command
+ cols = cols and cols or 50
+
nvim('command', 'highlight TermCursor cterm=reverse')
nvim('command', 'highlight TermCursorNC ctermbg=11')
- nvim('set_var', 'terminal_scrollback_buffer_size', 10)
- if not extra_height then extra_height = 0 end
- if not command then command = default_command end
- local screen = Screen.new(50, 7 + extra_height)
+
+ local screen = Screen.new(cols, 7 + extra_rows)
screen:set_default_attr_ids({
[1] = {reverse = true}, -- focused cursor
[2] = {background = 11}, -- unfocused cursor
@@ -55,31 +57,42 @@ local function screen_setup(extra_height, command)
})
screen:attach({rgb=false})
- -- tty-test puts the terminal into raw mode and echoes all input. tests are
- -- done by feeding it with terminfo codes to control the display and
- -- verifying output with screen:expect.
- execute('enew | call termopen('..command..') | startinsert')
+
+ execute('enew | call termopen('..command..')')
+ nvim('input', '<CR>')
+ local vim_errmsg = nvim('eval', 'v:errmsg')
+ if vim_errmsg and "" ~= vim_errmsg then
+ error(vim_errmsg)
+ end
+
+ execute('setlocal scrollback=10')
+ execute('startinsert')
+
+ -- tty-test puts the terminal into raw mode and echoes input. Tests work by
+ -- feeding termcodes to control the display and asserting by screen:expect.
if command == default_command then
- -- wait for "tty ready" to be printed before each test or the terminal may
- -- still be in canonical mode(will echo characters for example)
- --
- local empty_line = ' '
+ -- Wait for "tty ready" to be printed before each test or the terminal may
+ -- still be in canonical mode (will echo characters for example).
+ local empty_line = (' '):rep(cols + 1)
local expected = {
- 'tty ready ',
- '{1: } ',
+ 'tty ready'..(' '):rep(cols - 8),
+ '{1: }' ..(' '):rep(cols),
empty_line,
empty_line,
empty_line,
empty_line,
}
- for _ = 1, extra_height do
+ for _ = 1, extra_rows do
table.insert(expected, empty_line)
end
- table.insert(expected, '{3:-- TERMINAL --} ')
+ table.insert(expected, '{3:-- TERMINAL --}' .. ((' '):rep(cols - 13)))
screen:expect(table.concat(expected, '\n'))
else
- wait()
+ -- This eval also acts as a wait().
+ if 0 == nvim('eval', "exists('b:terminal_job_id')") then
+ error("terminal job failed to start")
+ end
end
return screen
end
diff --git a/test/functional/terminal/mouse_spec.lua b/test/functional/terminal/mouse_spec.lua
index ecb0b2beb0..da7e1c36db 100644
--- a/test/functional/terminal/mouse_spec.lua
+++ b/test/functional/terminal/mouse_spec.lua
@@ -117,7 +117,7 @@ describe('terminal mouse', function()
rows: 5, cols: 25 |rows: 5, cols: 25 |
{2:^ } |{2: } |
========== ========== |
- |
+ :vsp |
]])
feed(':enew | set number<cr>')
screen:expect([[
diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua
index d60819af65..930d0cf58b 100644
--- a/test/functional/terminal/scrollback_spec.lua
+++ b/test/functional/terminal/scrollback_spec.lua
@@ -3,7 +3,11 @@ local helpers = require('test.functional.helpers')(after_each)
local thelpers = require('test.functional.terminal.helpers')
local clear, eq, curbuf = helpers.clear, helpers.eq, helpers.curbuf
local feed, nvim_dir, execute = helpers.feed, helpers.nvim_dir, helpers.execute
+local eval = helpers.eval
+local command = helpers.command
local wait = helpers.wait
+local retry = helpers.retry
+local curbufmeths = helpers.curbufmeths
local feed_data = thelpers.feed_data
if helpers.pending_win32(pending) then return end
@@ -13,14 +17,14 @@ describe('terminal scrollback', function()
before_each(function()
clear()
- screen = thelpers.screen_setup()
+ screen = thelpers.screen_setup(nil, nil, 30)
end)
after_each(function()
screen:detach()
end)
- describe('when the limit is crossed', function()
+ describe('when the limit is exceeded', function()
before_each(function()
local lines = {}
for i = 1, 30 do
@@ -29,26 +33,26 @@ describe('terminal scrollback', function()
table.insert(lines, '')
feed_data(lines)
screen:expect([[
- line26 |
- line27 |
- line28 |
- line29 |
- line30 |
- {1: } |
- {3:-- TERMINAL --} |
+ line26 |
+ line27 |
+ line28 |
+ line29 |
+ line30 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
end)
it('will delete extra lines at the top', function()
feed('<c-\\><c-n>gg')
screen:expect([[
- ^line16 |
- line17 |
- line18 |
- line19 |
- line20 |
- line21 |
- |
+ ^line16 |
+ line17 |
+ line18 |
+ line19 |
+ line20 |
+ line21 |
+ |
]])
end)
end)
@@ -57,13 +61,13 @@ describe('terminal scrollback', function()
before_each(function()
feed_data({'line1', 'line2', 'line3', 'line4', ''})
screen:expect([[
- tty ready |
- line1 |
- line2 |
- line3 |
- line4 |
- {1: } |
- {3:-- TERMINAL --} |
+ tty ready |
+ line1 |
+ line2 |
+ line3 |
+ line4 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
end)
@@ -72,13 +76,13 @@ describe('terminal scrollback', function()
it('will hide the top line', function()
screen:expect([[
- line1 |
- line2 |
- line3 |
- line4 |
- line5 |
- {1: } |
- {3:-- TERMINAL --} |
+ line1 |
+ line2 |
+ line3 |
+ line4 |
+ line5 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
eq(7, curbuf('line_count'))
end)
@@ -88,46 +92,46 @@ describe('terminal scrollback', function()
it('will hide the top 4 lines', function()
screen:expect([[
- line3 |
- line4 |
- line5 |
- line6 |
- line7 |
- line8{1: } |
- {3:-- TERMINAL --} |
+ line3 |
+ line4 |
+ line5 |
+ line6 |
+ line7 |
+ line8{1: } |
+ {3:-- TERMINAL --} |
]])
feed('<c-\\><c-n>6k')
screen:expect([[
- ^line2 |
- line3 |
- line4 |
- line5 |
- line6 |
- line7 |
- |
+ ^line2 |
+ line3 |
+ line4 |
+ line5 |
+ line6 |
+ line7 |
+ |
]])
feed('gg')
screen:expect([[
- ^tty ready |
- line1 |
- line2 |
- line3 |
- line4 |
- line5 |
- |
+ ^tty ready |
+ line1 |
+ line2 |
+ line3 |
+ line4 |
+ line5 |
+ |
]])
feed('G')
screen:expect([[
- line3 |
- line4 |
- line5 |
- line6 |
- line7 |
- ^line8{2: } |
- |
+ line3 |
+ line4 |
+ line5 |
+ line6 |
+ line7 |
+ ^line8{2: } |
+ |
]])
end)
end)
@@ -138,12 +142,12 @@ describe('terminal scrollback', function()
local function will_hide_top_line()
screen:try_resize(screen._width, screen._height - 1)
screen:expect([[
- line2 |
- line3 |
- line4 |
- rows: 5, cols: 50 |
- {1: } |
- {3:-- TERMINAL --} |
+ line2 |
+ line3 |
+ line4 |
+ rows: 5, cols: 30 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
end
@@ -157,18 +161,18 @@ describe('terminal scrollback', function()
it('will hide the top 3 lines', function()
screen:expect([[
- rows: 5, cols: 50 |
- rows: 3, cols: 50 |
- {1: } |
- {3:-- TERMINAL --} |
+ rows: 5, cols: 30 |
+ rows: 3, cols: 30 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
eq(8, curbuf('line_count'))
feed('<c-\\><c-n>3k')
screen:expect([[
- ^line4 |
- rows: 5, cols: 50 |
- rows: 3, cols: 50 |
- |
+ ^line4 |
+ rows: 5, cols: 30 |
+ rows: 3, cols: 30 |
+ |
]])
end)
end)
@@ -183,11 +187,11 @@ describe('terminal scrollback', function()
local function will_delete_last_two_lines()
screen:expect([[
- tty ready |
- rows: 4, cols: 50 |
- {1: } |
- |
- {3:-- TERMINAL --} |
+ tty ready |
+ rows: 4, cols: 30 |
+ {1: } |
+ |
+ {3:-- TERMINAL --} |
]])
eq(4, curbuf('line_count'))
end
@@ -202,25 +206,25 @@ describe('terminal scrollback', function()
it('will delete the last line and hide the first', function()
screen:expect([[
- rows: 4, cols: 50 |
- rows: 3, cols: 50 |
- {1: } |
- {3:-- TERMINAL --} |
+ rows: 4, cols: 30 |
+ rows: 3, cols: 30 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
eq(4, curbuf('line_count'))
feed('<c-\\><c-n>gg')
screen:expect([[
- ^tty ready |
- rows: 4, cols: 50 |
- rows: 3, cols: 50 |
- |
+ ^tty ready |
+ rows: 4, cols: 30 |
+ rows: 3, cols: 30 |
+ |
]])
feed('a')
screen:expect([[
- rows: 4, cols: 50 |
- rows: 3, cols: 50 |
- {1: } |
- {3:-- TERMINAL --} |
+ rows: 4, cols: 30 |
+ rows: 3, cols: 30 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
end)
end)
@@ -231,20 +235,20 @@ describe('terminal scrollback', function()
before_each(function()
feed_data({'line1', 'line2', 'line3', 'line4', ''})
screen:expect([[
- tty ready |
- line1 |
- line2 |
- line3 |
- line4 |
- {1: } |
- {3:-- TERMINAL --} |
+ tty ready |
+ line1 |
+ line2 |
+ line3 |
+ line4 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
screen:try_resize(screen._width, screen._height - 3)
screen:expect([[
- line4 |
- rows: 3, cols: 50 |
- {1: } |
- {3:-- TERMINAL --} |
+ line4 |
+ rows: 3, cols: 30 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
eq(7, curbuf('line_count'))
end)
@@ -253,11 +257,11 @@ describe('terminal scrollback', function()
local function pop_then_push()
screen:try_resize(screen._width, screen._height + 1)
screen:expect([[
- line4 |
- rows: 3, cols: 50 |
- rows: 4, cols: 50 |
- {1: } |
- {3:-- TERMINAL --} |
+ line4 |
+ rows: 3, cols: 30 |
+ rows: 4, cols: 30 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
end
@@ -272,26 +276,26 @@ describe('terminal scrollback', function()
local function pop3_then_push1()
screen:expect([[
- line2 |
- line3 |
- line4 |
- rows: 3, cols: 50 |
- rows: 4, cols: 50 |
- rows: 7, cols: 50 |
- {1: } |
- {3:-- TERMINAL --} |
+ line2 |
+ line3 |
+ line4 |
+ rows: 3, cols: 30 |
+ rows: 4, cols: 30 |
+ rows: 7, cols: 30 |
+ {1: } |
+ {3:-- TERMINAL --} |
]])
eq(9, curbuf('line_count'))
feed('<c-\\><c-n>gg')
screen:expect([[
- ^tty ready |
- line1 |
- line2 |
- line3 |
- line4 |
- rows: 3, cols: 50 |
- rows: 4, cols: 50 |
- |
+ ^tty ready |
+ line1 |
+ line2 |
+ line3 |
+ line4 |
+ rows: 3, cols: 30 |
+ rows: 4, cols: 30 |
+ |
]])
end
@@ -306,18 +310,18 @@ describe('terminal scrollback', function()
it('will show all lines and leave a blank one at the end', function()
screen:expect([[
- tty ready |
- line1 |
- line2 |
- line3 |
- line4 |
- rows: 3, cols: 50 |
- rows: 4, cols: 50 |
- rows: 7, cols: 50 |
- rows: 11, cols: 50 |
- {1: } |
- |
- {3:-- TERMINAL --} |
+ tty ready |
+ line1 |
+ line2 |
+ line3 |
+ line4 |
+ rows: 3, cols: 30 |
+ rows: 4, cols: 30 |
+ rows: 7, cols: 30 |
+ rows: 11, cols: 30 |
+ {1: } |
+ |
+ {3:-- TERMINAL --} |
]])
-- since there's an empty line after the cursor, the buffer line
-- count equals the terminal screen height
@@ -332,30 +336,115 @@ end)
describe('terminal prints more lines than the screen height and exits', function()
it('will push extra lines to scrollback', function()
clear()
- local screen = Screen.new(50, 7)
+ local screen = Screen.new(30, 7)
screen:attach({rgb=false})
execute('call termopen(["'..nvim_dir..'/tty-test", "10"]) | startinsert')
wait()
screen:expect([[
- line6 |
- line7 |
- line8 |
- line9 |
- |
- [Process exited 0] |
- -- TERMINAL -- |
+ line6 |
+ line7 |
+ line8 |
+ line9 |
+ |
+ [Process exited 0] |
+ -- TERMINAL -- |
]])
feed('<cr>')
-- closes the buffer correctly after pressing a key
screen:expect([[
- ^ |
- ~ |
- ~ |
- ~ |
- ~ |
- ~ |
- |
+ ^ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ ~ |
+ |
]])
end)
end)
+describe("'scrollback' option", function()
+ before_each(function()
+ clear()
+ end)
+
+ local function expect_lines(expected)
+ local actual = eval("line('$')")
+ if expected ~= actual then
+ error('expected: '..expected..', actual: '..tostring(actual))
+ end
+ end
+
+ it('set to 0 behaves as 1', function()
+ local screen = thelpers.screen_setup(nil, "['sh']", 30)
+
+ curbufmeths.set_option('scrollback', 0)
+ feed_data('for i in $(seq 1 30); do echo "line$i"; done\n')
+ screen:expect('line30 ', nil, nil, nil, true)
+ retry(nil, nil, function() expect_lines(7) end)
+
+ screen:detach()
+ end)
+
+ it('deletes lines (only) if necessary', function()
+ local screen = thelpers.screen_setup(nil, "['sh']", 30)
+
+ curbufmeths.set_option('scrollback', 200)
+
+ -- Wait for prompt.
+ screen:expect('$', nil, nil, nil, true)
+
+ wait()
+ feed_data('for i in $(seq 1 30); do echo "line$i"; done\n')
+
+ screen:expect('line30 ', nil, nil, nil, true)
+
+ retry(nil, nil, function() expect_lines(33) end)
+ curbufmeths.set_option('scrollback', 10)
+ wait()
+ retry(nil, nil, function() expect_lines(16) end)
+ curbufmeths.set_option('scrollback', 10000)
+ eq(16, eval("line('$')"))
+ -- Terminal job data is received asynchronously, may happen before the
+ -- 'scrollback' option is synchronized with the internal sb_buffer.
+ command('sleep 100m')
+ feed_data('for i in $(seq 1 40); do echo "line$i"; done\n')
+
+ screen:expect('line40 ', nil, nil, nil, true)
+
+ retry(nil, nil, function() expect_lines(58) end)
+ -- Verify off-screen state
+ eq('line35', eval("getline(line('w0') - 1)"))
+ eq('line26', eval("getline(line('w0') - 10)"))
+
+ screen:detach()
+ end)
+
+ it('defaults to 1000', function()
+ execute('terminal')
+ eq(1000, curbufmeths.get_option('scrollback'))
+ end)
+
+ it('error if set to invalid values', function()
+ local status, rv = pcall(command, 'set scrollback=-2')
+ eq(false, status) -- assert failure
+ eq('E474:', string.match(rv, "E%d*:"))
+
+ status, rv = pcall(command, 'set scrollback=100001')
+ eq(false, status) -- assert failure
+ eq('E474:', string.match(rv, "E%d*:"))
+ end)
+
+ it('defaults to -1 on normal buffers', function()
+ execute('new')
+ eq(-1, curbufmeths.get_option('scrollback'))
+ end)
+
+ it('error if set on a normal buffer', function()
+ command('new')
+ execute('set scrollback=42')
+ feed('<CR>')
+ eq('E474:', string.match(eval("v:errmsg"), "E%d*:"))
+ end)
+
+end)
diff --git a/test/functional/terminal/window_split_tab_spec.lua b/test/functional/terminal/window_split_tab_spec.lua
index 6951b84a69..d3386a641e 100644
--- a/test/functional/terminal/window_split_tab_spec.lua
+++ b/test/functional/terminal/window_split_tab_spec.lua
@@ -28,16 +28,16 @@ describe('terminal', function()
feed('<c-\\><c-n>')
execute('2split')
screen:expect([[
- tty ready |
- ^rows: 2, cols: 50 |
+ rows: 2, cols: 50 |
+ {2:^ } |
========== |
- tty ready |
rows: 2, cols: 50 |
{2: } |
{4:~ }|
{4:~ }|
+ {4:~ }|
========== |
- |
+ :2split |
]])
execute('wincmd p')
screen:expect([[
@@ -54,14 +54,14 @@ describe('terminal', function()
]])
execute('wincmd p')
screen:expect([[
- rows: 5, cols: 50 |
- ^rows: 2, cols: 50 |
+ rows: 2, cols: 50 |
+ {2:^ } |
========== |
- rows: 5, cols: 50 |
rows: 2, cols: 50 |
{2: } |
{4:~ }|
{4:~ }|
+ {4:~ }|
========== |
:wincmd p |
]])
diff --git a/test/helpers.lua b/test/helpers.lua
index 1e01aaa098..25ab80bb50 100644
--- a/test/helpers.lua
+++ b/test/helpers.lua
@@ -207,7 +207,9 @@ local function check_cores(app)
out:write(('\nTests covered by this check: %u\n'):format(tests_skipped + 1))
end
tests_skipped = 0
- assert(0 == found_cores)
+ if found_cores > 0 then
+ error("crash detected (see above)")
+ end
end
return {