aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt9
-rw-r--r--Makefile10
-rw-r--r--runtime/doc/api.txt19
-rw-r--r--runtime/doc/eval.txt44
-rw-r--r--runtime/doc/usr_41.txt4
-rw-r--r--src/nvim/api/buffer.c34
-rw-r--r--src/nvim/buffer_defs.h20
-rw-r--r--src/nvim/buffer_updates.c18
-rw-r--r--src/nvim/eval.c47
-rw-r--r--src/nvim/eval.lua3
-rw-r--r--src/nvim/fileio.c3
-rw-r--r--src/nvim/garray.c8
-rw-r--r--src/nvim/globals.h2
-rw-r--r--src/nvim/mbyte.c33
-rw-r--r--src/nvim/memline.c53
-rw-r--r--src/nvim/misc1.c3
-rw-r--r--src/nvim/os/env.c1
-rw-r--r--src/nvim/screen.c5
-rw-r--r--src/nvim/testdir/test_environ.vim44
-rw-r--r--src/nvim/version.c17
-rw-r--r--test/functional/eval/environ_spec.lua12
-rw-r--r--test/functional/lua/buffer_updates_spec.lua162
-rw-r--r--test/functional/plugin/health_spec.lua3
-rw-r--r--test/functional/plugin/helpers.lua41
-rw-r--r--test/functional/plugin/matchparen_spec.lua6
-rw-r--r--test/functional/plugin/msgpack_spec.lua8
-rw-r--r--test/functional/plugin/shada_spec.lua17
-rw-r--r--test/unit/helpers.lua4
-rw-r--r--test/unit/mbyte_spec.lua1
29 files changed, 508 insertions, 123 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 947de61988..2314384d79 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -668,15 +668,6 @@ else()
COMMENT "lualint: LUACHECK_PRG not defined")
endif()
-if(FLAKE8_PRG)
- add_custom_target(pylint
- COMMAND ${FLAKE8_PRG} contrib/ scripts/ src/ test/
- WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
-else()
- add_custom_target(pylint false
- COMMENT "flake8: FLAKE8_PRG not defined")
-endif()
-
set(CPACK_PACKAGE_NAME "Neovim")
set(CPACK_PACKAGE_VENDOR "neovim.io")
set(CPACK_PACKAGE_VERSION ${NVIM_VERSION_MEDIUM})
diff --git a/Makefile b/Makefile
index e2cf4a3b29..243e371165 100644
--- a/Makefile
+++ b/Makefile
@@ -138,8 +138,8 @@ functionaltest-lua: | nvim
lualint: | build/.ran-cmake deps
$(BUILD_CMD) -C build lualint
-pylint: | build/.ran-cmake deps
- $(BUILD_CMD) -C build pylint
+pylint:
+ flake8 contrib/ scripts/ src/ test/
unittest: | nvim
+$(BUILD_CMD) -C build unittest
@@ -182,7 +182,11 @@ appimage:
appimage-%:
bash scripts/genappimage.sh $*
-lint: check-single-includes clint lualint pylint
+lint: check-single-includes clint lualint
+ @# Run pylint only if flake8 is installed.
+ @command -v flake8 \
+ && { $(MAKE) pylint; exit $?; } \
+ || echo "SKIP: pylint (flake8 not found)"
# Generic pattern rules, allowing for `make build/bin/nvim` etc.
# Does not work with "Unix Makefiles".
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
index 709e5885e4..2c6b053994 100644
--- a/runtime/doc/api.txt
+++ b/runtime/doc/api.txt
@@ -200,17 +200,26 @@ User reloads the buffer with ":edit", emits: >
nvim_buf_detach_event[{buf}]
*api-buffer-updates-lua*
-In-process lua plugins can also recieve buffer updates, in the form of lua
+In-process lua plugins can also receive buffer updates, in the form of lua
callbacks. These callbacks are called frequently in various contexts, buffer
contents or window layout should not be changed inside these |textlock|.
|vim.schedule| can be used to defer these operations to the main loop, where
they are allowed.
|nvim_buf_attach| will take keyword args for the callbacks. "on_lines" will
-receive parameters ("lines", {buf}, {changedtick}, {firstline}, {lastline}, {new_lastline}).
-Unlike remote channels the text contents are not passed. The new text can be
-accessed inside the callback as
-`vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, true)`
+receive parameters ("lines", {buf}, {changedtick}, {firstline}, {lastline},
+{new_lastline}, {old_byte_size}[, {old_utf32_size}, {old_utf16_size}]).
+Unlike remote channel events the text contents are not passed. The new text can
+be accessed inside the callback as
+
+ `vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, true)`
+
+{old_byte_size} is the total size of the replaced region {firstline} to
+{lastline} in bytes, including the final newline after {lastline}. if
+`utf_sizes` is set to true in |nvim_buf_attach()| keyword args, then the
+UTF-32 and UTF-16 sizes of the deleted region is also passed as additional
+arguments {old_utf32_size} and {old_utf16_size}.
+
"on_changedtick" is invoked when |b:changedtick| was incremented but no text
was changed. The parameters recieved are ("changedtick", {buf}, {changedtick}).
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index 697e456817..5e8c5151f2 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -1180,6 +1180,13 @@ $VAR environment variable
The String value of any environment variable. When it is not defined, the
result is an empty string.
+
+The functions `getenv()` and `setenv()` can also be used and work for
+environment variables with non-alphanumeric names.
+The function `environ()` can be used to get a Dict with all environment
+variables.
+
+
*expr-env-expand*
Note that there is a difference between using $VAR directly and using
expand("$VAR"). Using it directly will only expand environment variables that
@@ -2061,6 +2068,7 @@ did_filetype() Number |TRUE| if FileType autocommand event used
diff_filler({lnum}) Number diff filler lines about {lnum}
diff_hlID({lnum}, {col}) Number diff highlighting at {lnum}/{col}
empty({expr}) Number |TRUE| if {expr} is empty
+environ() Dict return environment variables
escape({string}, {chars}) String escape {chars} in {string} with '\'
eval({string}) any evaluate {string} into its value
eventhandler() Number |TRUE| if inside an event handler
@@ -2118,6 +2126,7 @@ getcompletion({pat}, {type} [, {filtered}])
List list of cmdline completion matches
getcurpos() List position of the cursor
getcwd([{winnr} [, {tabnr}]]) String get the current working directory
+getenv({name}) String return environment variable
getfontname([{name}]) String name of font being used
getfperm({fname}) String file permissions of file {fname}
getfsize({fname}) Number size in bytes of file {fname}
@@ -2298,6 +2307,7 @@ setbufline( {expr}, {lnum}, {line})
setbufvar({expr}, {varname}, {val}) set {varname} in buffer {expr} to {val}
setcharsearch({dict}) Dict set character search from {dict}
setcmdpos({pos}) Number set cursor position in command-line
+setenv({name}, {val}) none set environment variable
setfperm({fname}, {mode} Number set {fname} file permissions to {mode}
setline({lnum}, {line}) Number set line {lnum} to {line}
setloclist({nr}, {list}[, {action}[, {what}]])
@@ -3344,6 +3354,14 @@ diff_hlID({lnum}, {col}) *diff_hlID()*
The highlight ID can be used with |synIDattr()| to obtain
syntax information about the highlighting.
+environ() *environ()*
+ Return all of environment variables as dictionary. You can
+ check if an environment variable exists like this: >
+ :echo has_key(environ(), 'HOME')
+< Note that the variable name may be CamelCase; to ignore case
+ use this: >
+ :echo index(keys(environ()), 'HOME', 0, 1) != -1
+
empty({expr}) *empty()*
Return the Number 1 if {expr} is empty, zero otherwise.
A |List| or |Dictionary| is empty when it does not have any
@@ -4333,14 +4351,11 @@ getcwd([{winnr}[, {tabnr}]]) *getcwd()*
< If {winnr} is -1 it is ignored, only the tab is resolved.
{winnr} can be the window number or the |window-ID|.
-
-getfsize({fname}) *getfsize()*
- The result is a Number, which is the size in bytes of the
- given file {fname}.
- If {fname} is a directory, 0 is returned.
- If the file {fname} can't be found, -1 is returned.
- If the size of {fname} is too big to fit in a Number then -2
- is returned.
+getenv({name}) *getenv()*
+ Return the value of environment variable {name}.
+ When the variable does not exist |v:null| is returned. That
+ is different from a variable set to an empty string.
+ See also |expr-env|.
getfontname([{name}]) *getfontname()*
Without an argument returns the name of the normal font being
@@ -4371,6 +4386,14 @@ getfperm({fname}) *getfperm()*
For setting permissions use |setfperm()|.
+getfsize({fname}) *getfsize()*
+ The result is a Number, which is the size in bytes of the
+ given file {fname}.
+ If {fname} is a directory, 0 is returned.
+ If the file {fname} can't be found, -1 is returned.
+ If the size of {fname} is too big to fit in a Number then -2
+ is returned.
+
getftime({fname}) *getftime()*
The result is a Number, which is the last modification time of
the given file {fname}. The value is measured as seconds
@@ -7079,6 +7102,11 @@ setcmdpos({pos}) *setcmdpos()*
Returns 0 when successful, 1 when not editing the command
line.
+setenv({name}, {val}) *setenv()*
+ Set environment variable {name} to {val}.
+ When {val} is |v:null| the environment variable is deleted.
+ See also |expr-env|.
+
setfperm({fname}, {mode}) *setfperm()* *chmod*
Set the file permissions for {fname} to {mode}.
{mode} must be a string with 9 characters. It is of the form
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index c806507c3e..b26b7cb646 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -770,6 +770,9 @@ System functions and manipulation of files:
rename() rename a file
system() get the result of a shell command as a string
systemlist() get the result of a shell command as a list
+ environ() get all environment variables
+ getenv() get one environment variable
+ setenv() set an environment variable
hostname() name of the system
readfile() read a file into a List of lines
readdir() get a List of file names in a directory
@@ -899,6 +902,7 @@ GUI: *gui-functions*
getwinposy() Y position of the Vim window
balloon_show() set the balloon content
balloon_split() split a message for a balloon
+ balloon_gettext() get the text in the balloon
Vim server: *server-functions*
serverlist() return the list of server names
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c
index b0b65545ab..c6f82e9d85 100644
--- a/src/nvim/api/buffer.c
+++ b/src/nvim/api/buffer.c
@@ -109,9 +109,11 @@ String buffer_get_line(Buffer buffer, Integer index, Error *err)
/// `nvim_buf_lines_event`. Otherwise, the first notification will be
/// a `nvim_buf_changedtick_event`. Not used for lua callbacks.
/// @param opts Optional parameters.
-/// `on_lines`: lua callback received on change.
+/// `on_lines`: lua callback received on change.
/// `on_changedtick`: lua callback received on changedtick
/// increment without text change.
+/// `utf_sizes`: include UTF-32 and UTF-16 size of
+/// the replaced region.
/// See |api-buffer-updates-lua| for more information
/// @param[out] err Error details, if any
/// @return False when updates couldn't be enabled because the buffer isn't
@@ -156,6 +158,12 @@ Boolean nvim_buf_attach(uint64_t channel_id,
}
cb.on_detach = v->data.luaref;
v->data.integer = LUA_NOREF;
+ } else if (is_lua && strequal("utf_sizes", k.data)) {
+ if (v->type != kObjectTypeBoolean) {
+ api_set_error(err, kErrorTypeValidation, "utf_sizes must be boolean");
+ goto error;
+ }
+ cb.utf_sizes = v->data.boolean;
} else {
api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
goto error;
@@ -1176,6 +1184,30 @@ free_exit:
return 0;
}
+Dictionary nvim__buf_stats(Buffer buffer, Error *err)
+{
+ Dictionary rv = ARRAY_DICT_INIT;
+
+ buf_T *buf = find_buffer_by_handle(buffer, err);
+ if (!buf) {
+ return rv;
+ }
+
+ // Number of times the cached line was flushed.
+ // This should generally not increase while editing the same
+ // line in the same mode.
+ PUT(rv, "flush_count", INTEGER_OBJ(buf->flush_count));
+ // lnum of current line
+ PUT(rv, "current_lnum", INTEGER_OBJ(buf->b_ml.ml_line_lnum));
+ // whether the line has unflushed changes.
+ PUT(rv, "line_dirty", BOOLEAN_OBJ(buf->b_ml.ml_flags & ML_LINE_DIRTY));
+ // NB: this should be zero at any time API functions are called,
+ // this exists to debug issues
+ PUT(rv, "dirty_bytes", INTEGER_OBJ((Integer)buf->deleted_bytes));
+
+ return rv;
+}
+
// Check if deleting lines made the cursor position invalid.
// Changed lines from `lo` to `hi`; added `extra` lines (negative if deleted).
static void fix_cursor(linenr_T lo, linenr_T hi, linenr_T extra)
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h
index 143737b478..b11eaefdd0 100644
--- a/src/nvim/buffer_defs.h
+++ b/src/nvim/buffer_defs.h
@@ -459,8 +459,9 @@ typedef struct {
LuaRef on_lines;
LuaRef on_changedtick;
LuaRef on_detach;
+ bool utf_sizes;
} BufUpdateCallbacks;
-#define BUF_UPDATE_CALLBACKS_INIT { LUA_NOREF, LUA_NOREF, LUA_NOREF }
+#define BUF_UPDATE_CALLBACKS_INIT { LUA_NOREF, LUA_NOREF, LUA_NOREF, false }
#define BUF_HAS_QF_ENTRY 1
#define BUF_HAS_LL_ENTRY 2
@@ -802,11 +803,26 @@ struct file_buffer {
kvec_t(BufhlLine *) b_bufhl_move_space; // temporary space for highlights
- // array of channelids which have asked to receive updates for this
+ // array of channel_id:s which have asked to receive updates for this
// buffer.
kvec_t(uint64_t) update_channels;
+ // array of lua callbacks for buffer updates.
kvec_t(BufUpdateCallbacks) update_callbacks;
+ // whether an update callback has requested codepoint size of deleted regions.
+ bool update_need_codepoints;
+
+ // Measurements of the deleted or replaced region since the last update
+ // event. Some consumers of buffer changes need to know the byte size (like
+ // tree-sitter) or the corresponding UTF-32/UTF-16 size (like LSP) of the
+ // deleted text.
+ size_t deleted_bytes;
+ size_t deleted_codepoints;
+ size_t deleted_codeunits;
+
+ // The number for times the current line has been flushed in the memline.
+ int flush_count;
+
int b_diff_failed; // internal diff failed for this buffer
};
diff --git a/src/nvim/buffer_updates.c b/src/nvim/buffer_updates.c
index 21efda9fd9..3604578b50 100644
--- a/src/nvim/buffer_updates.c
+++ b/src/nvim/buffer_updates.c
@@ -26,6 +26,9 @@ bool buf_updates_register(buf_T *buf, uint64_t channel_id,
if (channel_id == LUA_INTERNAL_CALL) {
kv_push(buf->update_callbacks, cb);
+ if (cb.utf_sizes) {
+ buf->update_need_codepoints = true;
+ }
return true;
}
@@ -169,6 +172,10 @@ void buf_updates_send_changes(buf_T *buf,
int64_t num_removed,
bool send_tick)
{
+ size_t deleted_codepoints, deleted_codeunits;
+ size_t deleted_bytes = ml_flush_deleted_bytes(buf, &deleted_codepoints,
+ &deleted_codeunits);
+
if (!buf_updates_active(buf)) {
return;
}
@@ -231,8 +238,8 @@ void buf_updates_send_changes(buf_T *buf,
bool keep = true;
if (cb.on_lines != LUA_NOREF) {
Array args = ARRAY_DICT_INIT;
- Object items[5];
- args.size = 5;
+ Object items[8];
+ args.size = 6; // may be increased to 8 below
args.items = items;
// the first argument is always the buffer handle
@@ -250,6 +257,13 @@ void buf_updates_send_changes(buf_T *buf,
// the last line in the updated range
args.items[4] = INTEGER_OBJ(firstline - 1 + num_added);
+ // byte count of previous contents
+ args.items[5] = INTEGER_OBJ((Integer)deleted_bytes);
+ if (cb.utf_sizes) {
+ args.size = 8;
+ args.items[6] = INTEGER_OBJ((Integer)deleted_codepoints);
+ args.items[7] = INTEGER_OBJ((Integer)deleted_codeunits);
+ }
textlock++;
Object res = executor_exec_lua_cb(cb.on_lines, "lines", args, true);
textlock--;
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index d82a081c27..7ffa59f298 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -8495,6 +8495,25 @@ static void f_empty(typval_T *argvars, typval_T *rettv, FunPtr fptr)
rettv->vval.v_number = n;
}
+/// "environ()" function
+static void f_environ(typval_T *argvars, typval_T *rettv, FunPtr fptr)
+{
+ tv_dict_alloc_ret(rettv);
+
+ for (int i = 0; ; i++) {
+ // TODO(justinmk): use os_copyfullenv from #7202 ?
+ char *envname = os_getenvname_at_index((size_t)i);
+ if (envname == NULL) {
+ break;
+ }
+ const char *value = os_getenv(envname);
+ tv_dict_add_str(rettv->vval.v_dict,
+ (char *)envname, STRLEN((char *)envname),
+ value == NULL ? "" : value);
+ xfree(envname);
+ }
+}
+
/*
* "escape({string}, {chars})" function
*/
@@ -8508,6 +8527,20 @@ static void f_escape(typval_T *argvars, typval_T *rettv, FunPtr fptr)
rettv->v_type = VAR_STRING;
}
+/// "getenv()" function
+static void f_getenv(typval_T *argvars, typval_T *rettv, FunPtr fptr)
+{
+ char_u *p = (char_u *)vim_getenv(tv_get_string(&argvars[0]));
+
+ if (p == NULL) {
+ rettv->v_type = VAR_SPECIAL;
+ rettv->vval.v_number = kSpecialVarNull;
+ return;
+ }
+ rettv->vval.v_string = p;
+ rettv->v_type = VAR_STRING;
+}
+
/*
* "eval()" function
*/
@@ -15319,6 +15352,20 @@ static void f_setcmdpos(typval_T *argvars, typval_T *rettv, FunPtr fptr)
}
}
+/// "setenv()" function
+static void f_setenv(typval_T *argvars, typval_T *rettv, FunPtr fptr)
+{
+ char namebuf[NUMBUFLEN];
+ char valbuf[NUMBUFLEN];
+ const char *name = tv_get_string_buf(&argvars[0], namebuf);
+
+ if (argvars[1].v_type == VAR_SPECIAL
+ && argvars[1].vval.v_number == kSpecialVarNull) {
+ os_unsetenv(name);
+ } else {
+ vim_setenv(name, tv_get_string_buf(&argvars[1], valbuf));
+ }
+}
/// "setfperm({fname}, {mode})" function
static void f_setfperm(typval_T *argvars, typval_T *rettv, FunPtr fptr)
diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua
index 0b77a24f7a..db45409e77 100644
--- a/src/nvim/eval.lua
+++ b/src/nvim/eval.lua
@@ -89,6 +89,7 @@ return {
diff_filler={args=1},
diff_hlID={args=2},
empty={args=1},
+ environ={},
escape={args=2},
eval={args=1},
eventhandler={},
@@ -135,6 +136,7 @@ return {
getcompletion={args={2, 3}},
getcurpos={},
getcwd={args={0,2}},
+ getenv={args={1}},
getfontname={args={0, 1}},
getfperm={args=1},
getfsize={args=1},
@@ -274,6 +276,7 @@ return {
setbufvar={args=3},
setcharsearch={args=1},
setcmdpos={args=1},
+ setenv={args=2},
setfperm={args=2},
setline={args=2},
setloclist={args={2, 4}},
diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c
index a164cf47d5..d03b9138d0 100644
--- a/src/nvim/fileio.c
+++ b/src/nvim/fileio.c
@@ -1755,6 +1755,9 @@ failed:
ml_delete(curbuf->b_ml.ml_line_count, false);
linecnt--;
}
+ curbuf->deleted_bytes = 0;
+ curbuf->deleted_codepoints = 0;
+ curbuf->deleted_codeunits = 0;
linecnt = curbuf->b_ml.ml_line_count - linecnt;
if (filesize == 0)
linecnt = 0;
diff --git a/src/nvim/garray.c b/src/nvim/garray.c
index 74fd9d89cb..1cfc2b6176 100644
--- a/src/nvim/garray.c
+++ b/src/nvim/garray.c
@@ -89,6 +89,14 @@ void ga_grow(garray_T *gap, int n)
if (n < gap->ga_growsize) {
n = gap->ga_growsize;
}
+
+ // A linear growth is very inefficient when the array grows big. This
+ // is a compromise between allocating memory that won't be used and too
+ // many copy operations. A factor of 1.5 seems reasonable.
+ if (n < gap->ga_len / 2) {
+ n = gap->ga_len / 2;
+ }
+
int new_maxlen = gap->ga_len + n;
size_t new_size = (size_t)gap->ga_itemsize * (size_t)new_maxlen;
diff --git a/src/nvim/globals.h b/src/nvim/globals.h
index de6f59b3f1..4524c4b2c0 100644
--- a/src/nvim/globals.h
+++ b/src/nvim/globals.h
@@ -627,6 +627,8 @@ EXTERN pos_T Insstart_orig;
EXTERN int orig_line_count INIT(= 0); /* Line count when "gR" started */
EXTERN int vr_lines_changed INIT(= 0); /* #Lines changed by "gR" so far */
+// increase around internal delete/replace
+EXTERN int inhibit_delete_count INIT(= 0);
/*
* These flags are set based upon 'fileencoding'.
diff --git a/src/nvim/mbyte.c b/src/nvim/mbyte.c
index e7579399f3..bf8ce46113 100644
--- a/src/nvim/mbyte.c
+++ b/src/nvim/mbyte.c
@@ -1438,6 +1438,39 @@ int utf16_to_utf8(const wchar_t *strw, char **str)
#endif
+/// Measure the length of a string in corresponding UTF-32 and UTF-16 units.
+///
+/// Invalid UTF-8 bytes, or embedded surrogates, count as one code point/unit
+/// each.
+///
+/// The out parameters are incremented. This is used to measure the size of
+/// a buffer region consisting of multiple line segments.
+///
+/// @param s the string
+/// @param len maximum length (an earlier NUL terminates)
+/// @param[out] codepoints incremented with UTF-32 code point size
+/// @param[out] codeunits incremented with UTF-16 code unit size
+void mb_utflen(const char_u *s, size_t len, size_t *codepoints,
+ size_t *codeunits)
+ FUNC_ATTR_NONNULL_ALL
+{
+ size_t count = 0, extra = 0;
+ size_t clen;
+ for (size_t i = 0; i < len && s[i] != NUL; i += clen) {
+ clen = utf_ptr2len_len(s+i, len-i);
+ // NB: gets the byte value of invalid sequence bytes.
+ // we only care whether the char fits in the BMP or not
+ int c = (clen > 1) ? utf_ptr2char(s+i) : s[i];
+ count++;
+ if (c > 0xFFFF) {
+ extra++;
+ }
+ }
+ *codepoints += count;
+ *codeunits += count + extra;
+}
+
+
/*
* Version of strnicmp() that handles multi-byte characters.
* Needed for Big5, Shift-JIS and UTF-8 encoding. Other DBCS encodings can
diff --git a/src/nvim/memline.c b/src/nvim/memline.c
index b027459706..3220c7d9b8 100644
--- a/src/nvim/memline.c
+++ b/src/nvim/memline.c
@@ -2383,6 +2383,23 @@ static int ml_append_int(
return OK;
}
+void ml_add_deleted_len(char_u *ptr, ssize_t len)
+{
+ if (inhibit_delete_count) {
+ return;
+ }
+ if (len == -1) {
+ len = STRLEN(ptr);
+ }
+ curbuf->deleted_bytes += len+1;
+ if (curbuf->update_need_codepoints) {
+ mb_utflen(ptr, len, &curbuf->deleted_codepoints,
+ &curbuf->deleted_codeunits);
+ curbuf->deleted_codepoints++; // NL char
+ curbuf->deleted_codeunits++;
+ }
+}
+
/*
* Replace line lnum, with buffering, in current buffer.
*
@@ -2403,13 +2420,24 @@ int ml_replace(linenr_T lnum, char_u *line, bool copy)
if (curbuf->b_ml.ml_mfp == NULL && open_buffer(FALSE, NULL, 0) == FAIL)
return FAIL;
+ bool readlen = true;
+
if (copy) {
line = vim_strsave(line);
}
- if (curbuf->b_ml.ml_line_lnum != lnum) /* other line buffered */
- ml_flush_line(curbuf); /* flush it */
- else if (curbuf->b_ml.ml_flags & ML_LINE_DIRTY) /* same line allocated */
- xfree(curbuf->b_ml.ml_line_ptr); /* free it */
+ if (curbuf->b_ml.ml_line_lnum != lnum) { // other line buffered
+ ml_flush_line(curbuf); // flush it
+ } else if (curbuf->b_ml.ml_flags & ML_LINE_DIRTY) { // same line allocated
+ ml_add_deleted_len(curbuf->b_ml.ml_line_ptr, -1);
+ readlen = false; // already added the length
+
+ xfree(curbuf->b_ml.ml_line_ptr); // free it
+ }
+
+ if (readlen && kv_size(curbuf->update_callbacks)) {
+ ml_add_deleted_len(ml_get_buf(curbuf, lnum, false), -1);
+ }
+
curbuf->b_ml.ml_line_ptr = line;
curbuf->b_ml.ml_line_lnum = lnum;
curbuf->b_ml.ml_flags = (curbuf->b_ml.ml_flags | ML_LINE_DIRTY) & ~ML_EMPTY;
@@ -2491,6 +2519,10 @@ static int ml_delete_int(buf_T *buf, linenr_T lnum, bool message)
else
line_size = ((dp->db_index[idx - 1]) & DB_INDEX_MASK) - line_start;
+ // Line should always have an NL char internally (represented as NUL),
+ // even if 'noeol' is set.
+ assert(line_size >= 1);
+ ml_add_deleted_len((char_u *)dp + line_start, line_size-1);
/*
* special case: If there is only one line in the data block it becomes empty.
@@ -2676,6 +2708,17 @@ void ml_clearmarked(void)
return;
}
+size_t ml_flush_deleted_bytes(buf_T *buf, size_t *codepoints, size_t *codeunits)
+{
+ size_t ret = buf->deleted_bytes;
+ *codepoints = buf->deleted_codepoints;
+ *codeunits = buf->deleted_codeunits;
+ buf->deleted_bytes = 0;
+ buf->deleted_codepoints = 0;
+ buf->deleted_codeunits = 0;
+ return ret;
+}
+
/*
* flush ml_line if necessary
*/
@@ -2704,6 +2747,8 @@ static void ml_flush_line(buf_T *buf)
return;
entered = TRUE;
+ buf->flush_count++;
+
lnum = buf->b_ml.ml_line_lnum;
new_line = buf->b_ml.ml_line_ptr;
diff --git a/src/nvim/misc1.c b/src/nvim/misc1.c
index db0d56b5fd..a62fa6d585 100644
--- a/src/nvim/misc1.c
+++ b/src/nvim/misc1.c
@@ -780,6 +780,7 @@ open_line (
did_append = FALSE;
}
+ inhibit_delete_count++;
if (newindent
|| did_si
) {
@@ -821,6 +822,7 @@ open_line (
did_si = false;
}
}
+ inhibit_delete_count--;
/*
* In REPLACE mode, for each character in the extra leader, there must be
@@ -1685,6 +1687,7 @@ int del_bytes(colnr_T count, bool fixpos_arg, bool use_delcombine)
bool was_alloced = ml_line_alloced(); // check if oldp was allocated
char_u *newp;
if (was_alloced) {
+ ml_add_deleted_len(curbuf->b_ml.ml_line_ptr, oldlen);
newp = oldp; // use same allocated memory
} else { // need to allocate a new line
newp = xmalloc((size_t)(oldlen + 1 - count));
diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c
index 2278c325ea..bef78d8cc8 100644
--- a/src/nvim/os/env.c
+++ b/src/nvim/os/env.c
@@ -45,6 +45,7 @@ void env_init(void)
}
/// Like getenv(), but returns NULL if the variable is empty.
+/// @see os_env_exists
const char *os_getenv(const char *name)
FUNC_ATTR_NONNULL_ALL
{
diff --git a/src/nvim/screen.c b/src/nvim/screen.c
index 06f886d411..fb019a4d2d 100644
--- a/src/nvim/screen.c
+++ b/src/nvim/screen.c
@@ -3143,6 +3143,7 @@ win_line (
c = '>';
mb_c = c;
mb_l = 1;
+ (void)mb_l;
multi_attr = win_hl_attr(wp, HLF_AT);
// put the pointer back to output the double-width
@@ -3153,9 +3154,9 @@ win_line (
n_extra -= mb_l - 1;
p_extra += mb_l - 1;
}
- ++p_extra;
+ p_extra++;
}
- --n_extra;
+ n_extra--;
} else {
int c0;
diff --git a/src/nvim/testdir/test_environ.vim b/src/nvim/testdir/test_environ.vim
new file mode 100644
index 0000000000..094c4ce36f
--- /dev/null
+++ b/src/nvim/testdir/test_environ.vim
@@ -0,0 +1,44 @@
+scriptencoding utf-8
+
+func Test_environ()
+ unlet! $TESTENV
+ call assert_equal(0, has_key(environ(), 'TESTENV'))
+ let $TESTENV = 'foo'
+ call assert_equal(1, has_key(environ(), 'TESTENV'))
+ let $TESTENV = 'こんにちわ'
+ call assert_equal('こんにちわ', environ()['TESTENV'])
+endfunc
+
+func Test_getenv()
+ unlet! $TESTENV
+ call assert_equal(v:null, getenv('TESTENV'))
+ let $TESTENV = 'foo'
+ call assert_equal('foo', getenv('TESTENV'))
+endfunc
+
+func Test_setenv()
+ unlet! $TESTENV
+ call setenv('TEST ENV', 'foo')
+ call assert_equal('foo', getenv('TEST ENV'))
+ call setenv('TEST ENV', v:null)
+ call assert_equal(v:null, getenv('TEST ENV'))
+endfunc
+
+func Test_external_env()
+ call setenv('FOO', 'HelloWorld')
+ if has('win32')
+ let result = system('echo %FOO%')
+ else
+ let result = system('echo $FOO')
+ endif
+ let result = substitute(result, '[ \r\n]', '', 'g')
+ call assert_equal('HelloWorld', result)
+
+ call setenv('FOO', v:null)
+ if has('win32')
+ let result = system('set | grep ^FOO=')
+ else
+ let result = system('env | grep ^FOO=')
+ endif
+ call assert_equal('', result)
+endfunc
diff --git a/src/nvim/version.c b/src/nvim/version.c
index 33b2a05cbb..e07865a410 100644
--- a/src/nvim/version.c
+++ b/src/nvim/version.c
@@ -2034,14 +2034,19 @@ static void version_msg(char *s)
version_msg_wrap((char_u *)s, false);
}
-/// List all features aligned in columns, dictionary style.
+/// List all features.
+/// This does not use list_in_columns (as in Vim), because there are only a
+/// few, and we do not start at a new line.
static void list_features(void)
{
- list_in_columns((char_u **)features, -1, -1);
- if (msg_col > 0) {
- msg_putchar('\n');
+ version_msg(_("\n\nFeatures: "));
+ for (int i = 0; features[i] != NULL; i++) {
+ version_msg(features[i]);
+ if (features[i+1] != NULL) {
+ version_msg(" ");
+ }
}
- MSG_PUTS("See \":help feature-compile\"\n\n");
+ version_msg("\nSee \":help feature-compile\"\n\n");
}
/// List string items nicely aligned in columns.
@@ -2150,8 +2155,6 @@ void list_version(void)
}
#endif // ifdef HAVE_PATHDEF
- version_msg(_("\n\nFeatures: "));
-
list_features();
#ifdef SYS_VIMRC_FILE
diff --git a/test/functional/eval/environ_spec.lua b/test/functional/eval/environ_spec.lua
new file mode 100644
index 0000000000..eb52f9e2da
--- /dev/null
+++ b/test/functional/eval/environ_spec.lua
@@ -0,0 +1,12 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
+local eq = helpers.eq
+local environ = helpers.funcs.environ
+
+describe('environ()', function()
+ it('handles empty env variable', function()
+ clear({env={EMPTY_VAR=""}})
+ eq("", environ()['EMPTY_VAR'])
+ eq(nil, environ()['DOES_NOT_EXIST'])
+ end)
+end)
diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua
index c419d89be3..990cb97fec 100644
--- a/test/functional/lua/buffer_updates_spec.lua
+++ b/test/functional/lua/buffer_updates_spec.lua
@@ -5,28 +5,31 @@ local command = helpers.command
local meths = helpers.meths
local clear = helpers.clear
local eq = helpers.eq
+local exec_lua = helpers.exec_lua
+local feed = helpers.feed
local origlines = {"original line 1",
"original line 2",
"original line 3",
"original line 4",
"original line 5",
- "original line 6"}
+ "original line 6",
+ " indented line"}
describe('lua: buffer event callbacks', function()
before_each(function()
clear()
- meths.execute_lua([[
+ exec_lua([[
local events = {}
- function test_register(bufnr, id, changedtick)
+ function test_register(bufnr, id, changedtick, utf_sizes)
local function callback(...)
table.insert(events, {id, ...})
if test_unreg == id then
return true
end
end
- local opts = {on_lines=callback, on_detach=callback}
+ local opts = {on_lines=callback, on_detach=callback, utf_sizes=utf_sizes}
if changedtick then
opts.on_changedtick = callback
end
@@ -38,55 +41,166 @@ describe('lua: buffer event callbacks', function()
events = {}
return ret_events
end
- ]], {})
+ ]])
end)
- it('works', function()
+
+ -- verifying the sizes with nvim_buf_get_offset is nice (checks we cannot
+ -- assert the wrong thing), but masks errors with unflushed lines (as
+ -- nvim_buf_get_offset forces a flush of the memline). To be safe run the
+ -- test both ways.
+ local function check(verify,utf_sizes)
+ local lastsize
meths.buf_set_lines(0, 0, -1, true, origlines)
- meths.execute_lua("return test_register(...)", {0, "test1"})
+ if verify then
+ lastsize = meths.buf_get_offset(0, meths.buf_line_count(0))
+ end
+ exec_lua("return test_register(...)", 0, "test1",false,utf_sizes)
local tick = meths.buf_get_changedtick(0)
+ local verify_name = "test1"
+ local function check_events(expected)
+ local events = exec_lua("return get_events(...)" )
+ if utf_sizes then
+ -- this test case uses ASCII only, so sizes sshould be the same.
+ -- Unicode is tested below.
+ for _, event in ipairs(expected) do
+ event[9] = event[8]
+ event[10] = event[8]
+ end
+ end
+ eq(expected, events)
+ if verify then
+ for _, event in ipairs(events) do
+ if event[1] == verify_name and event[2] == "lines" then
+ local startline, endline = event[5], event[7]
+ local newrange = meths.buf_get_offset(0, endline) - meths.buf_get_offset(0, startline)
+ local newsize = meths.buf_get_offset(0, meths.buf_line_count(0))
+ local oldrange = newrange + lastsize - newsize
+ eq(oldrange, event[8])
+ lastsize = newsize
+ end
+ end
+ end
+ end
+
+ command('set autoindent')
command('normal! GyyggP')
tick = tick + 1
- eq({{ "test1", "lines", 1, tick, 0, 0, 1 }},
- meths.execute_lua("return get_events(...)", {}))
+ check_events({{ "test1", "lines", 1, tick, 0, 0, 1, 0}})
meths.buf_set_lines(0, 3, 5, true, {"changed line"})
tick = tick + 1
- eq({{ "test1", "lines", 1, tick, 3, 5, 4 }},
- meths.execute_lua("return get_events(...)", {}))
+ check_events({{ "test1", "lines", 1, tick, 3, 5, 4, 32 }})
- meths.execute_lua("return test_register(...)", {0, "test2", true})
+ exec_lua("return test_register(...)", 0, "test2", true, utf_sizes)
tick = tick + 1
command('undo')
-- plugins can opt in to receive changedtick events, or choose
-- to only recieve actual changes.
- eq({{ "test1", "lines", 1, tick, 3, 4, 5 },
- { "test2", "lines", 1, tick, 3, 4, 5 },
- { "test2", "changedtick", 1, tick+1 } },
- meths.execute_lua("return get_events(...)", {}))
+ check_events({{ "test1", "lines", 1, tick, 3, 4, 5, 13 },
+ { "test2", "lines", 1, tick, 3, 4, 5, 13 },
+ { "test2", "changedtick", 1, tick+1 } })
tick = tick + 1
-- simulate next callback returning true
- meths.execute_lua("test_unreg = 'test1'", {})
+ exec_lua("test_unreg = 'test1'")
meths.buf_set_lines(0, 6, 7, true, {"x1","x2","x3"})
tick = tick + 1
-- plugins can opt in to receive changedtick events, or choose
-- to only recieve actual changes.
- eq({{ "test1", "lines", 1, tick, 6, 7, 9 },
- { "test2", "lines", 1, tick, 6, 7, 9 }},
- meths.execute_lua("return get_events(...)", {}))
+ check_events({{ "test1", "lines", 1, tick, 6, 7, 9, 16 },
+ { "test2", "lines", 1, tick, 6, 7, 9, 16 }})
+
+ verify_name = "test2"
meths.buf_set_lines(0, 1, 1, true, {"added"})
tick = tick + 1
- eq({{ "test2", "lines", 1, tick, 1, 1, 2 }},
- meths.execute_lua("return get_events(...)", {}))
+ check_events({{ "test2", "lines", 1, tick, 1, 1, 2, 0 }})
+
+ feed('wix')
+ tick = tick + 1
+ check_events({{ "test2", "lines", 1, tick, 4, 5, 5, 16 }})
+
+ -- check hot path for multiple insert
+ feed('yz')
+ tick = tick + 1
+ check_events({{ "test2", "lines", 1, tick, 4, 5, 5, 17 }})
+
+ feed('<bs>')
+ tick = tick + 1
+ check_events({{ "test2", "lines", 1, tick, 4, 5, 5, 19 }})
+
+ feed('<esc>Go')
+ tick = tick + 1
+ check_events({{ "test2", "lines", 1, tick, 11, 11, 12, 0 }})
+
+ feed('x')
+ tick = tick + 1
+ check_events({{ "test2", "lines", 1, tick, 11, 12, 12, 5 }})
command('bwipe!')
- eq({{ "test2", "detach", 1 }},
- meths.execute_lua("return get_events(...)", {}))
+ check_events({{ "test2", "detach", 1 }})
+ end
+
+ it('works', function()
+ check(false)
end)
+
+ it('works with verify', function()
+ check(true)
+ end)
+
+ it('works with utf_sizes and ASCII text', function()
+ check(false,true)
+ end)
+
+ it('works with utf_sizes and unicode text', function()
+ local unicode_text = {"ascii text",
+ "latin text åäö",
+ "BMP text ɧ αλφά",
+ "BMP text 汉语 ↥↧",
+ "SMP 🤦 🦄🦃",
+ "combining å بِيَّة"}
+ meths.buf_set_lines(0, 0, -1, true, unicode_text)
+ feed('gg')
+ exec_lua("return test_register(...)", 0, "test1", false, true)
+ local tick = meths.buf_get_changedtick(0)
+
+ feed('dd')
+ tick = tick + 1
+ eq({{ "test1", "lines", 1, tick, 0, 1, 0, 11, 11, 11 }}, exec_lua("return get_events(...)" ))
+
+ feed('A<bs>')
+ tick = tick + 1
+ eq({{ "test1", "lines", 1, tick, 0, 1, 1, 18, 15, 15 }}, exec_lua("return get_events(...)" ))
+
+ feed('<esc>jylp')
+ tick = tick + 1
+ eq({{ "test1", "lines", 1, tick, 1, 2, 2, 21, 16, 16 }}, exec_lua("return get_events(...)" ))
+
+ feed('+eea<cr>')
+ tick = tick + 1
+ eq({{ "test1", "lines", 1, tick, 2, 3, 4, 23, 15, 15 }}, exec_lua("return get_events(...)" ))
+
+ feed('<esc>jdw')
+ tick = tick + 1
+ -- non-BMP chars count as 2 UTF-2 codeunits
+ eq({{ "test1", "lines", 1, tick, 4, 5, 5, 18, 9, 12 }}, exec_lua("return get_events(...)" ))
+
+ feed('+rx')
+ tick = tick + 1
+ -- count the individual codepoints of a composed character.
+ eq({{ "test1", "lines", 1, tick, 5, 6, 6, 27, 20, 20 }}, exec_lua("return get_events(...)" ))
+
+ feed('kJ')
+ tick = tick + 1
+ -- NB: this is inefficient (but not really wrong).
+ eq({{ "test1", "lines", 1, tick, 4, 5, 5, 14, 5, 8 },
+ { "test1", "lines", 1, tick+1, 5, 6, 5, 27, 20, 20 }}, exec_lua("return get_events(...)" ))
+ end)
+
end)
diff --git a/test/functional/plugin/health_spec.lua b/test/functional/plugin/health_spec.lua
index f2d5e433db..3525e235de 100644
--- a/test/functional/plugin/health_spec.lua
+++ b/test/functional/plugin/health_spec.lua
@@ -1,6 +1,5 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
-local plugin_helpers = require('test.functional.plugin.helpers')
local clear = helpers.clear
local curbuf_contents = helpers.curbuf_contents
@@ -41,7 +40,7 @@ end)
describe('health.vim', function()
before_each(function()
- plugin_helpers.reset()
+ clear{args={'-u', 'NORC'}}
-- Provides functions:
-- health#broken#check()
-- health#success1#check()
diff --git a/test/functional/plugin/helpers.lua b/test/functional/plugin/helpers.lua
deleted file mode 100644
index 4359380bd7..0000000000
--- a/test/functional/plugin/helpers.lua
+++ /dev/null
@@ -1,41 +0,0 @@
-local paths = require('test.config.paths')
-
-local helpers = require('test.functional.helpers')(nil)
-local spawn, set_session, nvim_prog, merge_args =
- helpers.spawn, helpers.set_session, helpers.nvim_prog, helpers.merge_args
-
-local additional_cmd = ''
-
-local function nvim_argv(shada_file)
- local rtp_value = ('\'%s/runtime\''):format(
- paths.test_source_path:gsub('\'', '\'\''))
- local nvim_args = {nvim_prog, '-u', 'NORC', '-i', shada_file or 'NONE', '-N',
- '--cmd', 'set shortmess+=I background=light noswapfile belloff= noshowcmd noruler',
- '--cmd', 'let &runtimepath=' .. rtp_value,
- '--cmd', additional_cmd,
- '--embed', '--headless'}
- if helpers.prepend_argv then
- return merge_args(helpers.prepend_argv, nvim_args)
- else
- return nvim_args
- end
-end
-
-local session = nil
-
-local function reset(...)
- if session then
- session:close()
- end
- session = spawn(nvim_argv(...))
- set_session(session)
-end
-
-local function set_additional_cmd(s)
- additional_cmd = s
-end
-
-return {
- reset=reset,
- set_additional_cmd=set_additional_cmd,
-}
diff --git a/test/functional/plugin/matchparen_spec.lua b/test/functional/plugin/matchparen_spec.lua
index 51ec7e4870..13e1283e2c 100644
--- a/test/functional/plugin/matchparen_spec.lua
+++ b/test/functional/plugin/matchparen_spec.lua
@@ -1,19 +1,17 @@
local helpers = require('test.functional.helpers')(after_each)
-local plugin_helpers = require('test.functional.plugin.helpers')
local Screen = require('test.functional.ui.screen')
+local clear = helpers.clear
local command = helpers.command
local meths = helpers.meths
local feed = helpers.feed
local eq = helpers.eq
-local reset = plugin_helpers.reset
-
describe('matchparen', function()
local screen
before_each(function()
- reset()
+ clear{args={'-u', 'NORC'}}
screen = Screen.new(20,5)
screen:attach()
screen:set_default_attr_ids( {
diff --git a/test/functional/plugin/msgpack_spec.lua b/test/functional/plugin/msgpack_spec.lua
index 2ce9974812..d841cb8ce0 100644
--- a/test/functional/plugin/msgpack_spec.lua
+++ b/test/functional/plugin/msgpack_spec.lua
@@ -1,15 +1,15 @@
local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
local meths = helpers.meths
local eq, nvim_eval, nvim_command, exc_exec =
helpers.eq, helpers.eval, helpers.command, helpers.exc_exec
local ok = helpers.ok
local NIL = helpers.NIL
-local plugin_helpers = require('test.functional.plugin.helpers')
-local reset = plugin_helpers.reset
-
describe('autoload/msgpack.vim', function()
- before_each(reset)
+ before_each(function()
+ clear{args={'-u', 'NORC'}}
+ end)
local sp = function(typ, val)
return ('{"_TYPE": v:msgpack_types.%s, "_VAL": %s}'):format(typ, val)
diff --git a/test/functional/plugin/shada_spec.lua b/test/functional/plugin/shada_spec.lua
index 1482d83ee6..778dc4e219 100644
--- a/test/functional/plugin/shada_spec.lua
+++ b/test/functional/plugin/shada_spec.lua
@@ -1,4 +1,6 @@
+local paths = require('test.config.paths')
local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
local eq, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf =
helpers.eq, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec,
helpers.funcs, helpers.feed, helpers.curbuf
@@ -7,12 +9,21 @@ local read_file = helpers.read_file
local mpack = require('mpack')
-local plugin_helpers = require('test.functional.plugin.helpers')
-local reset = plugin_helpers.reset
-
local shada_helpers = require('test.functional.shada.helpers')
local get_shada_rw = shada_helpers.get_shada_rw
+local function reset(shada_file)
+ -- TODO(justinmk): why is this needed?
+ local rtp_value = ('\'%s/runtime\''):format(
+ paths.test_source_path:gsub('\'', '\'\''))
+ clear{args_rm={'-u', '-i'},
+ args={'-u', 'NORC',
+ '-i', shada_file or 'NONE',
+ '--cmd', 'set laststatus&',
+ '--cmd', 'let &runtimepath='..rtp_value,
+ }}
+end
+
local mpack_eq = function(expected, mpack_result)
local mpack_keys = {'type', 'timestamp', 'length', 'value'}
diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua
index b5d3dd9f47..24dbc65bd0 100644
--- a/test/unit/helpers.lua
+++ b/test/unit/helpers.lua
@@ -456,8 +456,8 @@ else
if bytes_written == -1 then
local err = ffi.errno(0)
if err ~= ffi.C.kPOSIXErrnoEINTR then
- assert(false, ("write() error: %u: %s"):format(
- err, ffi.string(ffi.C.strerror(err))))
+ assert(false, ("write() error: %u: %s ('%s')"):format(
+ err, ffi.string(ffi.C.strerror(err)), s))
end
elseif bytes_written == 0 then
break
diff --git a/test/unit/mbyte_spec.lua b/test/unit/mbyte_spec.lua
index 1ff1bed9fe..d27f52923a 100644
--- a/test/unit/mbyte_spec.lua
+++ b/test/unit/mbyte_spec.lua
@@ -10,6 +10,7 @@ local charset = helpers.cimport('./src/nvim/charset.h')
describe('mbyte', function()
if helpers.isCI('quickbuild') then
pending("crashes on quickbuild", function() end)
+ return
end
-- Array for composing characters